201 lines
8.3 KiB
Python
201 lines
8.3 KiB
Python
from importlib import metadata
|
|
|
|
import pytest
|
|
|
|
from haiku.rag.store import Store
|
|
from haiku.rag.store.exceptions import MigrationRequiredError
|
|
|
|
|
|
class TestMigrationRequiredError:
|
|
def test_migration_required_error_is_exception(self):
|
|
"""MigrationRequiredError should be a subclass of Exception."""
|
|
assert issubclass(MigrationRequiredError, Exception)
|
|
|
|
def test_migration_required_error_can_be_raised(self):
|
|
"""MigrationRequiredError can be raised and caught."""
|
|
with pytest.raises(MigrationRequiredError) as exc_info:
|
|
raise MigrationRequiredError("Run 'haiku-rag migrate' to upgrade")
|
|
assert "migrate" in str(exc_info.value)
|
|
|
|
|
|
class TestMigrationCheck:
|
|
@pytest.mark.asyncio
|
|
async def test_new_database_sets_version(self, temp_db_path):
|
|
"""New database should set the current package version."""
|
|
async with Store(temp_db_path, create=True) as store:
|
|
version = await store.get_haiku_version()
|
|
expected = metadata.version("haiku.rag-slim")
|
|
assert version == expected
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_existing_database_same_version_no_error(self, temp_db_path):
|
|
"""Opening a database with the same version should not error."""
|
|
async with Store(temp_db_path, create=True):
|
|
pass
|
|
|
|
# Re-open - should work without error
|
|
async with Store(temp_db_path):
|
|
pass
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_version_bump_without_pending_migrations_updates_silently(
|
|
self, temp_db_path
|
|
):
|
|
"""When version is outdated but no migrations pending, update version silently."""
|
|
async with Store(temp_db_path, create=True) as store:
|
|
# Set an older version that has no pending migrations
|
|
# (newer than all current upgrade steps)
|
|
await store.set_haiku_version("100.0.0")
|
|
|
|
# Re-open - should update version silently, no error
|
|
async with Store(temp_db_path) as store:
|
|
# Version should now be current
|
|
version = await store.get_haiku_version()
|
|
expected = metadata.version("haiku.rag-slim")
|
|
assert version == expected
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_pending_migrations_raises_error(self, temp_db_path):
|
|
"""When actual migrations are pending, should raise MigrationRequiredError."""
|
|
async with Store(temp_db_path, create=True) as store:
|
|
# Set version to before the first upgrade step
|
|
await store.set_haiku_version("0.19.0")
|
|
|
|
# Re-open should raise
|
|
with pytest.raises(MigrationRequiredError) as exc_info:
|
|
async with Store(temp_db_path) as store:
|
|
pass
|
|
assert "migrate" in str(exc_info.value).lower()
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_pending_migrations_raises_error_with_create_flag(self, temp_db_path):
|
|
"""Opening an existing DB with create=True must still check migrations."""
|
|
async with Store(temp_db_path, create=True) as store:
|
|
await store.set_haiku_version("0.19.0")
|
|
|
|
# create=True is idempotent — must not mark an existing populated DB as new
|
|
with pytest.raises(MigrationRequiredError):
|
|
async with Store(temp_db_path, create=True) as store:
|
|
pass
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_pending_migrations_read_only_raises_error(self, temp_db_path):
|
|
"""Read-only mode with pending migrations should still raise."""
|
|
async with Store(temp_db_path, create=True) as store:
|
|
await store.set_haiku_version("0.19.0")
|
|
|
|
with pytest.raises(MigrationRequiredError):
|
|
async with Store(temp_db_path, read_only=True) as store:
|
|
pass
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_read_only_version_bump_without_migrations_ok(self, temp_db_path):
|
|
"""Read-only mode with version bump but no migrations should work."""
|
|
async with Store(temp_db_path, create=True) as store:
|
|
# Set a version newer than all upgrade steps
|
|
await store.set_haiku_version("100.0.0")
|
|
|
|
# Read-only open should work (version not updated, but no error)
|
|
async with Store(temp_db_path, read_only=True) as store:
|
|
# Version should stay at the old value (can't update in read-only)
|
|
assert await store.get_haiku_version() == "100.0.0"
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_skip_migration_check_bypasses_error(self, temp_db_path):
|
|
"""skip_migration_check=True should bypass migration error."""
|
|
async with Store(temp_db_path, create=True) as store:
|
|
await store.set_haiku_version("0.19.0")
|
|
|
|
# Open with skip_migration_check should work
|
|
async with Store(temp_db_path, skip_migration_check=True) as store:
|
|
# Version should remain old (no auto-migration)
|
|
assert await store.get_haiku_version() == "0.19.0"
|
|
|
|
|
|
class TestMigrateMethod:
|
|
@pytest.mark.asyncio
|
|
async def test_migrate_applies_pending_upgrades(self, temp_db_path):
|
|
"""Store.migrate() should apply pending upgrades and update version."""
|
|
async with Store(temp_db_path, create=True) as store:
|
|
await store.set_haiku_version("0.19.0")
|
|
|
|
# Open with skip_migration_check to avoid error
|
|
async with Store(temp_db_path, skip_migration_check=True) as store:
|
|
old_version = await store.get_haiku_version()
|
|
assert old_version == "0.19.0"
|
|
|
|
# Run migration
|
|
applied = await store.migrate()
|
|
|
|
# Should have applied migrations
|
|
assert len(applied) > 0
|
|
|
|
# Version should be updated
|
|
new_version = await store.get_haiku_version()
|
|
expected = metadata.version("haiku.rag-slim")
|
|
assert new_version == expected
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_migrate_returns_applied_upgrades(self, temp_db_path):
|
|
"""Store.migrate() should return list of applied upgrade descriptions."""
|
|
async with Store(temp_db_path, create=True) as store:
|
|
await store.set_haiku_version("0.19.0")
|
|
|
|
async with Store(temp_db_path, skip_migration_check=True) as store:
|
|
applied = await store.migrate()
|
|
|
|
# Should return descriptions of applied upgrades
|
|
assert isinstance(applied, list)
|
|
for item in applied:
|
|
assert isinstance(item, str)
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_migrate_with_no_pending_returns_empty(self, temp_db_path):
|
|
"""Store.migrate() with no pending migrations returns empty list."""
|
|
async with Store(temp_db_path, create=True) as store:
|
|
# Already at current version
|
|
pass
|
|
|
|
async with Store(temp_db_path, skip_migration_check=True) as store:
|
|
applied = await store.migrate()
|
|
assert applied == []
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_migrate_raises_read_only_error(self, temp_db_path):
|
|
"""Store.migrate() should raise ReadOnlyError in read-only mode."""
|
|
from haiku.rag.store.exceptions import ReadOnlyError
|
|
|
|
async with Store(temp_db_path, create=True) as store:
|
|
await store.set_haiku_version("0.19.0")
|
|
|
|
async with Store(
|
|
temp_db_path, skip_migration_check=True, read_only=True
|
|
) as store:
|
|
with pytest.raises(ReadOnlyError):
|
|
await store.migrate()
|
|
|
|
|
|
class TestGetPendingUpgrades:
|
|
def test_get_pending_upgrades_returns_list(self):
|
|
"""get_pending_upgrades() should return a list of Upgrade objects."""
|
|
from haiku.rag.store.upgrades import get_pending_upgrades
|
|
|
|
pending = get_pending_upgrades("0.19.0")
|
|
assert isinstance(pending, list)
|
|
# Should have at least the v0.20.0 upgrade
|
|
assert len(pending) > 0
|
|
|
|
def test_get_pending_upgrades_from_current_version_is_empty(self):
|
|
"""get_pending_upgrades() from current version should be empty."""
|
|
from haiku.rag.store.upgrades import get_pending_upgrades
|
|
|
|
current = metadata.version("haiku.rag-slim")
|
|
pending = get_pending_upgrades(current)
|
|
assert pending == []
|
|
|
|
def test_get_pending_upgrades_from_future_version_is_empty(self):
|
|
"""get_pending_upgrades() from a future version should be empty."""
|
|
from haiku.rag.store.upgrades import get_pending_upgrades
|
|
|
|
pending = get_pending_upgrades("100.0.0")
|
|
assert pending == []
|