Release v0.1.7
All checks were successful
Dependency Audit / dependency-audit (push) Successful in 1m43s

This commit is contained in:
2026-07-11 02:46:04 +02:00
parent edb4687826
commit bfc882f0db
24 changed files with 2714 additions and 201 deletions

View File

@@ -57,6 +57,7 @@ from govoplan_core.core.module_installer import (
list_module_installer_requests,
module_installer_daemon_status,
module_install_preflight,
module_install_catalog_companion_module_ids,
module_manifest_compatibility_issues,
module_installer_lock_status,
queue_module_installer_request,
@@ -65,6 +66,7 @@ from govoplan_core.core.module_installer import (
retry_module_installer_request,
rollback_module_install_run,
run_module_install_plan,
structured_install_commands,
supervise_module_install_plan,
update_module_installer_daemon_status,
update_module_installer_request,
@@ -95,13 +97,14 @@ from govoplan_core.core.module_package_catalog import (
sign_module_package_catalog,
validate_module_package_catalog,
)
from govoplan_core.core.modules import FrontendModule, FrontendRoute, MigrationRetirementPlan, ModuleCompatibility, ModuleUninstallGuardResult
from govoplan_core.core.modules import FrontendModule, FrontendRoute, MigrationRetirementPlan, ModuleCompatibility, ModuleMigrationTask, ModuleMigrationTaskContext, ModuleMigrationTaskResult, ModuleUninstallGuardResult
from govoplan_core.core.module_guards import drop_table_retirement_provider
from govoplan_core.core.modules import MigrationSpec, ModuleInterfaceProvider, ModuleInterfaceRequirement, ModuleManifest
from govoplan_core.core.registry import PlatformRegistry, RegistryError
from govoplan_core.admin.models import SystemSettings
from govoplan_core.db.base import Base
from govoplan_core.db.bootstrap import bootstrap_dev_data
from govoplan_core.db.migrations import run_registered_module_migration_tasks
from govoplan_core.db.session import configure_database as configure_database_handle, get_database, reset_database
from govoplan_core.security.module_permissions import scopes_grant_compatible
from govoplan_core.security.permissions import scope_grants
@@ -1228,6 +1231,103 @@ finally:
self.assertTrue(companion_issues)
self.assertTrue(any("files" in issue.message for issue in companion_issues))
def test_module_installer_suggests_catalog_companion_provider_updates(self) -> None:
available = {
"files": ModuleManifest(
id="files",
name="Files",
version="1.0.0",
provides_interfaces=(ModuleInterfaceProvider(name="files.attachments", version="1.0.0"),),
),
"campaigns": ModuleManifest(id="campaigns", name="Campaigns", version="1.0.0"),
}
plan = ModuleInstallPlan(items=(
ModuleInstallPlanItem(
module_id="campaigns",
action="update",
source="catalog",
python_package="govoplan-campaign",
python_ref="govoplan-campaign==2.0.0",
),
))
validation = {
"configured": True,
"valid": True,
"warnings": [],
"modules": [
{
"module_id": "files",
"provides_interfaces": [{"name": "files.attachments", "version": "2.0.0"}],
},
{
"module_id": "campaigns",
"requires_interfaces": [{
"name": "files.attachments",
"version_min": "2.0.0",
"version_max_exclusive": "3.0.0",
}],
},
],
}
companions = module_install_catalog_companion_module_ids(plan, available, validation=validation)
self.assertEqual(("files",), companions)
def test_module_installer_suggests_catalog_companion_consumer_updates(self) -> None:
available = {
"files": ModuleManifest(
id="files",
name="Files",
version="1.0.0",
provides_interfaces=(ModuleInterfaceProvider(name="files.attachments", version="1.0.0"),),
),
"campaigns": ModuleManifest(
id="campaigns",
name="Campaigns",
version="1.0.0",
requires_interfaces=(
ModuleInterfaceRequirement(
name="files.attachments",
version_min="1.0.0",
version_max_exclusive="2.0.0",
),
),
),
}
plan = ModuleInstallPlan(items=(
ModuleInstallPlanItem(
module_id="files",
action="update",
source="catalog",
python_package="govoplan-files",
python_ref="govoplan-files==2.0.0",
),
))
validation = {
"configured": True,
"valid": True,
"warnings": [],
"modules": [
{
"module_id": "files",
"provides_interfaces": [{"name": "files.attachments", "version": "2.0.0"}],
},
{
"module_id": "campaigns",
"requires_interfaces": [{
"name": "files.attachments",
"version_min": "2.0.0",
"version_max_exclusive": "3.0.0",
}],
},
],
}
companions = module_install_catalog_companion_module_ids(plan, available, validation=validation)
self.assertEqual(("campaigns",), companions)
def test_module_installer_preflight_blocks_unacknowledged_forward_only_catalog_update(self) -> None:
available = {"files": ModuleManifest(id="files", name="Files", version="1.0.0")}
plan = ModuleInstallPlan(items=(
@@ -1251,6 +1351,8 @@ finally:
"version": "2.0.0",
"migration_safety": "forward_only",
"migration_notes": "Database rollback requires restoring the pre-update snapshot.",
"recovery_tested": True,
"recovery_notes": "Snapshot restore was rehearsed on the staging dataset.",
}],
},
):
@@ -1266,7 +1368,7 @@ finally:
blockers = {issue.code for issue in preflight.issues if issue.severity == "blocker"}
self.assertIn("forward_only_migration_acknowledgement_required", blockers)
def test_module_installer_preflight_allows_acknowledged_forward_only_catalog_update(self) -> None:
def test_module_installer_preflight_requires_recovery_validation_for_forward_only_update(self) -> None:
available = {"files": ModuleManifest(id="files", name="Files", version="1.0.0")}
plan = ModuleInstallPlan(items=(
ModuleInstallPlanItem(
@@ -1301,16 +1403,234 @@ finally:
maintenance_mode=True,
)
self.assertFalse(preflight.allowed)
blockers = {issue.code for issue in preflight.issues if issue.severity == "blocker"}
self.assertIn("recovery_validation_required", blockers)
self.assertIn("recovery_notes_required", blockers)
def test_module_installer_preflight_allows_acknowledged_forward_only_catalog_update(self) -> None:
available = {"files": ModuleManifest(id="files", name="Files", version="1.0.0")}
plan = ModuleInstallPlan(items=(
ModuleInstallPlanItem(
module_id="files",
action="update",
source="catalog",
python_package="govoplan-files",
python_ref="govoplan-files==2.0.0",
data_safety_acknowledged=True,
),
))
with patch(
"govoplan_core.core.module_package_catalog.validate_module_package_catalog",
return_value={
"configured": True,
"valid": True,
"warnings": [],
"modules": [{
"module_id": "files",
"version": "2.0.0",
"migration_safety": "forward_only",
"migration_notes": "Database rollback requires restoring the pre-update snapshot.",
"recovery_tested": True,
"recovery_notes": "Snapshot restore was rehearsed on the staging dataset.",
}],
},
):
preflight = module_install_preflight(
plan=plan,
available=available,
current_enabled=(),
desired_enabled=(),
maintenance_mode=True,
)
self.assertTrue(preflight.allowed)
self.assertEqual(("files",), tuple(item.module_id for item in preflight.target_plan))
self.assertEqual("1.0.0", preflight.target_plan[0].current_version)
self.assertEqual("2.0.0", preflight.target_plan[0].target_version)
self.assertEqual("forward_only", preflight.target_plan[0].migration_safety)
self.assertTrue(preflight.target_plan[0].recovery_tested)
self.assertEqual("Snapshot restore was rehearsed on the staging dataset.", preflight.target_plan[0].recovery_notes)
self.assertTrue(preflight.target_plan[0].data_safety_acknowledged)
self.assertEqual("2.0.0", preflight.as_dict()["target_plan"][0]["target_version"])
warnings = {issue.code for issue in preflight.issues if issue.severity == "warning"}
self.assertIn("forward_only_migration_acknowledged", warnings)
def test_module_installer_preflight_blocks_catalog_downgrade_and_noop_updates_by_default(self) -> None:
available = {
"files": ModuleManifest(id="files", name="Files", version="1.0.0"),
"mail": ModuleManifest(id="mail", name="Mail", version="1.0.0"),
}
plan = ModuleInstallPlan(items=(
ModuleInstallPlanItem(
module_id="files",
action="update",
source="catalog",
python_package="govoplan-files",
python_ref="govoplan-files==0.9.0",
),
ModuleInstallPlanItem(
module_id="mail",
action="update",
source="catalog",
python_package="govoplan-mail",
python_ref="govoplan-mail==1.0.0",
),
))
with patch(
"govoplan_core.core.module_package_catalog.validate_module_package_catalog",
return_value={
"configured": True,
"valid": True,
"warnings": [],
"modules": [
{"module_id": "files", "version": "0.9.0"},
{"module_id": "mail", "version": "1.0.0"},
],
},
):
preflight = module_install_preflight(
plan=plan,
available=available,
current_enabled=(),
desired_enabled=(),
maintenance_mode=True,
)
self.assertFalse(preflight.allowed)
blockers = {issue.code for issue in preflight.issues if issue.severity == "blocker"}
self.assertIn("catalog_downgrade_blocked", blockers)
self.assertIn("catalog_noop_update_blocked", blockers)
def test_module_installer_preflight_allows_reviewed_downgrade_and_noop_updates(self) -> None:
available = {
"files": ModuleManifest(id="files", name="Files", version="1.0.0"),
"mail": ModuleManifest(id="mail", name="Mail", version="1.0.0"),
}
plan = ModuleInstallPlan(items=(
ModuleInstallPlanItem(
module_id="files",
action="update",
source="catalog",
python_package="govoplan-files",
python_ref="govoplan-files==0.9.0",
),
ModuleInstallPlanItem(
module_id="mail",
action="update",
source="catalog",
python_package="govoplan-mail",
python_ref="govoplan-mail==1.0.0",
),
))
with patch(
"govoplan_core.core.module_package_catalog.validate_module_package_catalog",
return_value={
"configured": True,
"valid": True,
"warnings": [],
"modules": [
{"module_id": "files", "version": "0.9.0", "allow_downgrade": True},
{"module_id": "mail", "version": "1.0.0", "allow_same_version": True},
],
},
):
preflight = module_install_preflight(
plan=plan,
available=available,
current_enabled=(),
desired_enabled=(),
maintenance_mode=True,
)
self.assertTrue(preflight.allowed)
blocker_codes = {issue.code for issue in preflight.issues if issue.severity == "blocker"}
self.assertNotIn("catalog_downgrade_blocked", blocker_codes)
self.assertNotIn("catalog_noop_update_blocked", blocker_codes)
def test_module_installer_preflight_requires_catalog_update_bridge_when_current_version_is_outside_window(self) -> None:
available = {"files": ModuleManifest(id="files", name="Files", version="1.0.0")}
plan = ModuleInstallPlan(items=(
ModuleInstallPlanItem(
module_id="files",
action="update",
source="catalog",
python_package="govoplan-files",
python_ref="govoplan-files==3.0.0",
),
))
with patch(
"govoplan_core.core.module_package_catalog.validate_module_package_catalog",
return_value={
"configured": True,
"valid": True,
"warnings": [],
"modules": [{
"module_id": "files",
"version": "3.0.0",
"current_version_min": "2.0.0",
"current_version_max_exclusive": "3.0.0",
"bridge_notes": "Install 2.x bridge before moving to 3.x.",
}],
},
):
preflight = module_install_preflight(
plan=plan,
available=available,
current_enabled=(),
desired_enabled=(),
maintenance_mode=True,
)
self.assertFalse(preflight.allowed)
blockers = {issue.code for issue in preflight.issues if issue.severity == "blocker"}
self.assertIn("catalog_update_bridge_required", blockers)
self.assertEqual("2.0.0", preflight.target_plan[0].current_version_min)
self.assertEqual("3.0.0", preflight.target_plan[0].current_version_max_exclusive)
def test_module_installer_preflight_reports_catalog_bridge_release_metadata(self) -> None:
available = {"files": ModuleManifest(id="files", name="Files", version="1.0.0")}
plan = ModuleInstallPlan(items=(
ModuleInstallPlanItem(
module_id="files",
action="update",
source="catalog",
python_package="govoplan-files",
python_ref="govoplan-files==2.0.0",
),
))
with patch(
"govoplan_core.core.module_package_catalog.validate_module_package_catalog",
return_value={
"configured": True,
"valid": True,
"warnings": [],
"modules": [{
"module_id": "files",
"version": "2.0.0",
"bridge_release": True,
"bridge_notes": "Keeps both 1.x and 2.x attachment interfaces available.",
}],
},
):
preflight = module_install_preflight(
plan=plan,
available=available,
current_enabled=(),
desired_enabled=(),
maintenance_mode=True,
)
self.assertTrue(preflight.allowed)
self.assertIn("catalog_bridge_release", {issue.code for issue in preflight.issues if issue.severity == "info"})
self.assertTrue(preflight.target_plan[0].bridge_release)
self.assertEqual("Keeps both 1.x and 2.x attachment interfaces available.", preflight.target_plan[0].bridge_notes)
def test_module_installer_preflight_requires_destructive_catalog_update_plan(self) -> None:
available = {"files": ModuleManifest(id="files", name="Files", version="1.0.0")}
plan = ModuleInstallPlan(items=(
@@ -1371,6 +1691,8 @@ finally:
"module_id": "files",
"migration_safety": "destructive",
"migration_notes": "Drops obsolete cache tables after exporting the retained documents.",
"recovery_tested": True,
"recovery_notes": "Restore and forward-recovery path verified on staging.",
}],
},
):
@@ -1446,6 +1768,275 @@ finally:
self.assertTrue(preflight.allowed)
self.assertNotIn("companion_update_required", {issue.code for issue in preflight.issues})
def test_module_installer_preflight_orders_migration_plan_from_interface_requirements(self) -> None:
metadata = MetaData()
available = {
"files": ModuleManifest(
id="files",
name="Files",
version="1.0.0",
provides_interfaces=(ModuleInterfaceProvider(name="files.attachments", version="1.0.0"),),
migration_spec=MigrationSpec(module_id="files", metadata=metadata),
),
"campaigns": ModuleManifest(
id="campaigns",
name="Campaigns",
version="1.0.0",
requires_interfaces=(
ModuleInterfaceRequirement(
name="files.attachments",
version_min="1.0.0",
version_max_exclusive="2.0.0",
),
),
migration_spec=MigrationSpec(module_id="campaigns", metadata=metadata),
),
}
plan = ModuleInstallPlan(items=(
ModuleInstallPlanItem(
module_id="campaigns",
action="update",
python_package="govoplan-campaign",
python_ref="govoplan-campaign==2.0.0",
),
ModuleInstallPlanItem(
module_id="files",
action="update",
python_package="govoplan-files",
python_ref="govoplan-files==2.0.0",
),
))
preflight = module_install_preflight(
plan=plan,
available=available,
current_enabled=("files", "campaigns"),
desired_enabled=("files", "campaigns"),
maintenance_mode=True,
)
self.assertTrue(preflight.allowed)
self.assertEqual(("files", "campaigns"), tuple(step.module_id for step in preflight.migration_plan.steps))
self.assertEqual(("files", "campaigns"), tuple(preflight.migration_plan.enabled_modules))
self.assertEqual(["files", "campaigns"], preflight.as_dict()["migration_plan"]["enabled_modules"])
def test_module_installer_preflight_blocks_migration_graph_cycles(self) -> None:
metadata = MetaData()
available = {
"files": ModuleManifest(
id="files",
name="Files",
version="1.0.0",
migration_spec=MigrationSpec(module_id="files", metadata=metadata, migration_after=("campaigns",)),
),
"campaigns": ModuleManifest(
id="campaigns",
name="Campaigns",
version="1.0.0",
migration_spec=MigrationSpec(module_id="campaigns", metadata=metadata, migration_after=("files",)),
),
}
plan = ModuleInstallPlan(items=(
ModuleInstallPlanItem(
module_id="campaigns",
action="update",
python_package="govoplan-campaign",
python_ref="govoplan-campaign==2.0.0",
),
ModuleInstallPlanItem(
module_id="files",
action="update",
python_package="govoplan-files",
python_ref="govoplan-files==2.0.0",
),
))
preflight = module_install_preflight(
plan=plan,
available=available,
current_enabled=("files", "campaigns"),
desired_enabled=("files", "campaigns"),
maintenance_mode=True,
)
self.assertFalse(preflight.allowed)
blockers = {issue.code for issue in preflight.issues if issue.severity == "blocker"}
self.assertIn("migration_graph_cycle", blockers)
def test_structured_install_commands_pass_target_modules_and_migration_order(self) -> None:
plan = ModuleInstallPlan(items=(
ModuleInstallPlanItem(
module_id="files",
action="install",
python_package="govoplan-files",
python_ref="govoplan-files==2.0.0",
),
))
commands = structured_install_commands(
plan,
webui_root=None,
migrate_database_url="sqlite:///example.db",
migration_enabled_modules=("access", "files"),
migration_module_order=("files",),
migration_task_record_path=Path("/tmp/govoplan-migration-tasks.json"),
verify_modules=True,
)
init_db = commands[-1]["argv"]
self.assertIn("govoplan_core.commands.init_db", init_db)
self.assertIn("--enabled-module", init_db)
self.assertIn("access", init_db)
self.assertIn("files", init_db)
self.assertIn("--migration-module", init_db)
self.assertIn("--migration-task-record-output", init_db)
self.assertEqual("/tmp/govoplan-migration-tasks.json", commands[-1]["migration_task_record_path"])
def test_module_installer_preflight_reports_and_blocks_unsafe_migration_tasks(self) -> None:
metadata = MetaData()
available = {
"files": ModuleManifest(
id="files",
name="Files",
version="1.0.0",
migration_spec=MigrationSpec(
module_id="files",
metadata=metadata,
migration_tasks=(
ModuleMigrationTask(
task_id="legacy_path_check",
phase="pre_migration_check",
summary="Check legacy paths before the schema migration.",
executor=lambda _context: ModuleMigrationTaskResult(),
),
ModuleMigrationTask(
task_id="unsafe_backfill",
phase="post_migration_backfill",
summary="Backfill legacy file spaces.",
safety="forward_only",
idempotent=False,
executor=lambda _context: ModuleMigrationTaskResult(),
),
),
),
),
}
plan = ModuleInstallPlan(items=(
ModuleInstallPlanItem(
module_id="files",
action="update",
python_package="govoplan-files",
python_ref="govoplan-files==2.0.0",
),
))
preflight = module_install_preflight(
plan=plan,
available=available,
current_enabled=("files",),
desired_enabled=("files",),
maintenance_mode=True,
)
self.assertFalse(preflight.allowed)
task_ids = {task.task_id for task in preflight.migration_plan.tasks}
self.assertEqual({"legacy_path_check", "unsafe_backfill"}, task_ids)
blockers = {issue.code for issue in preflight.issues if issue.severity == "blocker"}
self.assertIn("migration_task_not_idempotent", blockers)
self.assertIn("migration_task_acknowledgement_required", blockers)
def test_module_installer_preflight_allows_acknowledged_forward_only_migration_task(self) -> None:
metadata = MetaData()
available = {
"files": ModuleManifest(
id="files",
name="Files",
version="1.0.0",
migration_spec=MigrationSpec(
module_id="files",
metadata=metadata,
migration_tasks=(
ModuleMigrationTask(
task_id="backfill_spaces",
phase="post_migration_backfill",
summary="Backfill file spaces.",
safety="forward_only",
executor=lambda _context: ModuleMigrationTaskResult(status="ok"),
),
),
),
),
}
plan = ModuleInstallPlan(items=(
ModuleInstallPlanItem(
module_id="files",
action="update",
python_package="govoplan-files",
python_ref="govoplan-files==2.0.0",
data_safety_acknowledged=True,
),
))
preflight = module_install_preflight(
plan=plan,
available=available,
current_enabled=("files",),
desired_enabled=("files",),
maintenance_mode=True,
)
self.assertTrue(preflight.allowed)
self.assertEqual(("backfill_spaces",), tuple(task.task_id for task in preflight.migration_plan.tasks))
self.assertIn("tasks", preflight.as_dict()["migration_plan"])
def test_registered_module_migration_tasks_execute_in_phase_order(self) -> None:
root = Path(tempfile.mkdtemp(prefix="govoplan-module-migration-tasks-", dir=_TEST_ROOT))
database_url = f"sqlite:///{root / 'tasks.db'}"
calls: list[tuple[str, str, bool, str | None]] = []
def executor(context: ModuleMigrationTaskContext) -> ModuleMigrationTaskResult:
calls.append((context.module_id, context.task_id, context.session is not None, context.target_version))
return ModuleMigrationTaskResult(status="warning", message=f"ran {context.task_id}", details={"phase": context.phase})
manifest = ModuleManifest(
id="taskmod",
name="Task Module",
version="2.0.0",
migration_spec=MigrationSpec(
module_id="taskmod",
metadata=MetaData(),
migration_tasks=(
ModuleMigrationTask(
task_id="verify",
phase="post_migration_verify",
summary="Verify migrated state.",
executor=executor,
),
ModuleMigrationTask(
task_id="check",
phase="pre_migration_check",
summary="Check source state.",
executor=executor,
),
),
),
)
records = run_registered_module_migration_tasks(
database_url=database_url,
enabled_modules=("taskmod",),
migration_module_order=("taskmod",),
phases=("pre_migration_check", "post_migration_verify"),
manifest_factories=(lambda: manifest,),
)
self.assertEqual(
[("taskmod", "check", True, "2.0.0"), ("taskmod", "verify", True, "2.0.0")],
calls,
)
self.assertEqual(["check", "verify"], [record["task_id"] for record in records])
self.assertEqual(["warning", "warning"], [record["status"] for record in records])
def test_module_installer_preflight_blocks_provider_update_that_breaks_installed_consumer(self) -> None:
available = {
"files": ModuleManifest(
@@ -2155,6 +2746,25 @@ finally:
"optional_dependencies": ["mail"],
"migration_safety": "forward_only",
"migration_notes": "Requires a tested backup restore path.",
"migration_after": ["access"],
"migration_before": ["campaigns"],
"migration_tasks": [{
"task_id": "backfill_spaces",
"phase": "post_migration_backfill",
"summary": "Backfill file spaces after schema migration.",
"task_version": "2",
"safety": "forward_only",
"idempotent": True,
"timeout_seconds": 30,
}],
"current_version_min": "0.1.0",
"current_version_max_exclusive": "0.2.0",
"bridge_release": True,
"bridge_notes": "Keeps 0.1.x and 0.2.x file-space contracts available.",
"allow_downgrade": True,
"allow_same_version": True,
"recovery_tested": True,
"recovery_notes": "Restore rehearsal completed for the release candidate.",
"provides_interfaces": [{"name": "files.spaces", "version": "1.2.0"}],
"requires_interfaces": [{"name": "access.directory", "version_min": "1.0.0", "optional": True}],
"artifact_integrity": {
@@ -2177,6 +2787,25 @@ finally:
self.assertEqual(["mail"], catalog[0]["optional_dependencies"])
self.assertEqual("forward_only", catalog[0]["migration_safety"])
self.assertEqual("Requires a tested backup restore path.", catalog[0]["migration_notes"])
self.assertEqual(["access"], catalog[0]["migration_after"])
self.assertEqual(["campaigns"], catalog[0]["migration_before"])
self.assertEqual([{
"task_id": "backfill_spaces",
"phase": "post_migration_backfill",
"summary": "Backfill file spaces after schema migration.",
"task_version": "2",
"safety": "forward_only",
"idempotent": True,
"timeout_seconds": 30,
}], catalog[0]["migration_tasks"])
self.assertEqual("0.1.0", catalog[0]["current_version_min"])
self.assertEqual("0.2.0", catalog[0]["current_version_max_exclusive"])
self.assertTrue(catalog[0]["bridge_release"])
self.assertEqual("Keeps 0.1.x and 0.2.x file-space contracts available.", catalog[0]["bridge_notes"])
self.assertTrue(catalog[0]["allow_downgrade"])
self.assertTrue(catalog[0]["allow_same_version"])
self.assertTrue(catalog[0]["recovery_tested"])
self.assertEqual("Restore rehearsal completed for the release candidate.", catalog[0]["recovery_notes"])
self.assertEqual(["official"], catalog[0]["tags"])
self.assertEqual([{"name": "files.spaces", "version": "1.2.0"}], catalog[0]["provides_interfaces"])
self.assertEqual(
@@ -2245,6 +2874,22 @@ finally:
self.assertFalse(validation["valid"])
self.assertIn("migration_safety", str(validation["error"]))
def test_module_package_catalog_rejects_invalid_current_version_window(self) -> None:
root = Path(tempfile.mkdtemp(prefix="govoplan-module-package-catalog-invalid-current-window-", dir=_TEST_ROOT))
catalog_path = root / "catalog.json"
catalog_path.write_text(json.dumps({
"modules": [{
"module_id": "files",
"current_version_min": "2.0.0",
"current_version_max_exclusive": "2.0.0",
}],
}), encoding="utf-8")
validation = validate_module_package_catalog(catalog_path)
self.assertFalse(validation["valid"])
self.assertIn("current-version range", str(validation["error"]))
def test_module_package_catalog_rejects_invalid_interface_ranges(self) -> None:
root = Path(tempfile.mkdtemp(prefix="govoplan-module-package-catalog-invalid-interface-", dir=_TEST_ROOT))
catalog_path = root / "catalog.json"
@@ -2344,7 +2989,7 @@ finally:
sys.executable,
"scripts/generate-release-catalog.py",
"--version",
"0.1.6",
"0.1.7",
"--catalog-output",
str(catalog_path),
],
@@ -2377,8 +3022,8 @@ finally:
self.assertEqual(["files", "mail"], modules["campaigns"]["optional_dependencies"])
self.assertEqual("requires_review", modules["files"]["migration_safety"])
self.assertIn("migration", modules["files"]["migration_notes"].lower())
self.assertEqual("0.1.6", modules["files"]["version"])
self.assertIn("@v0.1.6", modules["files"]["python_ref"])
self.assertEqual("0.1.7", modules["files"]["version"])
self.assertIn("@v0.1.7", modules["files"]["python_ref"])
def test_module_package_catalog_validates_remote_url_and_cache_fallback(self) -> None:
root = Path(tempfile.mkdtemp(prefix="govoplan-module-package-catalog-remote-", dir=_TEST_ROOT))
@@ -3158,9 +3803,11 @@ finally:
self.assertEqual(1, len(core_plan.script_locations))
self.assertTrue(core_plan.script_locations[0].endswith("govoplan_access/backend/migrations/versions"))
self.assertEqual(1, len(core_plan.metadata))
self.assertEqual(("access",), tuple(item.module_id for item in core_plan.modules))
files_plan = migration_metadata_plan(build_platform_registry(("files",)))
self.assertEqual(2, len(files_plan.script_locations))
self.assertIn("files", {item.module_id for item in files_plan.modules})
files_locations = "\n".join(files_plan.script_locations)
self.assertIn("govoplan_access/backend/migrations/versions", files_locations)
self.assertIn("govoplan_files/backend/migrations/versions", files_locations)