from __future__ import annotations import json from pathlib import Path import subprocess import sys import tempfile import unittest META_ROOT = Path(__file__).resolve().parents[1] RELEASE_TOOLS_ROOT = META_ROOT / "tools" / "release" if str(RELEASE_TOOLS_ROOT) not in sys.path: sys.path.insert(0, str(RELEASE_TOOLS_ROOT)) from govoplan_release.version_alignment import ( # noqa: E402 candidate_catalog_version_issues, release_composition_issues, selected_release_webui_bundle_issues, repository_version_issues, selected_repository_version_issues, ) from govoplan_release.model import ( # noqa: E402 CatalogSnapshot, DashboardSummary, ReleaseDashboard, RepositorySnapshot, RepositorySpec, VersionSnapshot, ) from govoplan_release.selective_planner import build_selective_release_plan, build_unit # noqa: E402 from govoplan_release.selective_catalog import enforce_selected_version_alignment # noqa: E402 class VersionAlignmentTests(unittest.TestCase): def test_candidate_catalog_refs_and_selected_units_must_match_versions(self) -> None: payload = { "core_release": { "version": "1.2.3", "python_ref": "govoplan-core @ git+ssh://git@example.test/acme/govoplan-core.git@v1.2.3", }, "modules": [ { "module_id": "files", "version": "1.2.3", "python_ref": "govoplan-files @ git+ssh://git@example.test/acme/govoplan-files.git@v1.2.4", "webui_package": "@govoplan/files-webui", "webui_ref": "git+ssh://git@example.test/acme/govoplan-files.git#v1.2.3", } ], "release": { "selected_units": [ {"repo": "govoplan-files", "version": "1.2.5", "tag": "v1.2.6"}, ] }, } issues = candidate_catalog_version_issues(payload) messages = {issue.message for issue in issues} self.assertIn("Python ref tag must match the catalog entry version", messages) self.assertIn("selected-unit tag must match its version", messages) self.assertIn("selected-unit version must match the catalog entry", messages) def test_catalog_candidate_gate_rejects_selected_repo_drift(self) -> None: with tempfile.TemporaryDirectory() as tmp: workspace = Path(tmp) repo = workspace / "govoplan-files" (repo / "webui").mkdir(parents=True) (repo / "pyproject.toml").write_text('[project]\nname="govoplan-files"\nversion="1.2.3"\n') (repo / "webui" / "package.json").write_text('{"version":"1.2.4"}\n') with self.assertRaisesRegex(ValueError, "Version alignment gate failed"): enforce_selected_version_alignment( repo_versions={"govoplan-files": "1.2.5"}, workspace=workspace, ) def test_catalog_candidate_gate_rejects_requested_version_that_differs_from_source(self) -> None: with tempfile.TemporaryDirectory() as tmp: workspace = Path(tmp) repo = workspace / "govoplan-files" (repo / "webui").mkdir(parents=True) (repo / "pyproject.toml").write_text('[project]\nname="govoplan-files"\nversion="1.2.3"\n') (repo / "webui" / "package.json").write_text('{"version":"1.2.3"}\n') with self.assertRaisesRegex(ValueError, "source version must match the requested release version"): enforce_selected_version_alignment( repo_versions={"govoplan-files": "1.2.4"}, workspace=workspace, ) def test_selected_webui_release_must_match_core_bundle_input_and_lock(self) -> None: with tempfile.TemporaryDirectory() as tmp: workspace = Path(tmp) core_webui = workspace / "govoplan-core" / "webui" campaign_webui = workspace / "govoplan-campaign" / "webui" core_webui.mkdir(parents=True) campaign_webui.mkdir(parents=True) dependency = "@govoplan/campaign-webui" old_ref = "git+ssh://git@example.test/acme/govoplan-campaign.git#v1.2.2" (campaign_webui / "package.json").write_text( json.dumps({"name": dependency, "version": "1.2.3"}) ) (core_webui / "package.release.json").write_text( json.dumps({"dependencies": {dependency: old_ref}}) ) (core_webui / "package-lock.release.json").write_text( json.dumps( { "packages": { "": {"dependencies": {dependency: old_ref}}, f"node_modules/{dependency}": {"version": "1.2.2"}, } } ) ) issues = selected_release_webui_bundle_issues( repo_versions={"govoplan-core": "2.0.0", "govoplan-campaign": "1.2.3"}, workspace=workspace, ) with self.assertRaisesRegex(ValueError, "Core release WebUI dependency"): enforce_selected_version_alignment( repo_versions={"govoplan-campaign": "1.2.3"}, workspace=workspace, ) self.assertEqual(2, len(issues)) self.assertTrue(all(issue.repo == "govoplan-campaign" for issue in issues)) self.assertEqual( { "Core release WebUI dependency must match the selected module version", "Core release lock must resolve the selected module version", }, {issue.message for issue in issues}, ) def test_selected_webui_gate_ignores_unselected_module_pins(self) -> None: with tempfile.TemporaryDirectory() as tmp: workspace = Path(tmp) core_webui = workspace / "govoplan-core" / "webui" campaign_webui = workspace / "govoplan-campaign" / "webui" core_webui.mkdir(parents=True) campaign_webui.mkdir(parents=True) campaign_package = "@govoplan/campaign-webui" files_package = "@govoplan/files-webui" campaign_ref = "git+ssh://git@example.test/acme/govoplan-campaign.git#v1.2.3" files_ref = "git+ssh://git@example.test/acme/govoplan-files.git#v0.9.0" (campaign_webui / "package.json").write_text( json.dumps({"name": campaign_package, "version": "1.2.3"}) ) dependencies = {campaign_package: campaign_ref, files_package: files_ref} (core_webui / "package.release.json").write_text(json.dumps({"dependencies": dependencies})) (core_webui / "package-lock.release.json").write_text( json.dumps( { "packages": { "": {"dependencies": dependencies}, f"node_modules/{campaign_package}": {"version": "1.2.3"}, f"node_modules/{files_package}": {"version": "0.9.0"}, } } ) ) issues = selected_release_webui_bundle_issues( repo_versions={"govoplan-campaign": "1.2.3"}, workspace=workspace, ) self.assertEqual((), issues) def test_selective_plan_blocks_mismatched_selected_webui_bundle(self) -> None: with tempfile.TemporaryDirectory() as tmp: workspace = Path(tmp) core_webui = workspace / "govoplan-core" / "webui" campaign_webui = workspace / "govoplan-campaign" / "webui" core_webui.mkdir(parents=True) campaign_webui.mkdir(parents=True) package_name = "@govoplan/campaign-webui" old_ref = "git+ssh://git@example.test/acme/govoplan-campaign.git#v1.2.2" (campaign_webui / "package.json").write_text( json.dumps({"name": package_name, "version": "1.2.3"}) ) (core_webui / "package.release.json").write_text( json.dumps({"dependencies": {package_name: old_ref}}) ) (core_webui / "package-lock.release.json").write_text( json.dumps( { "packages": { "": {"dependencies": {package_name: old_ref}}, f"node_modules/{package_name}": {"version": "1.2.2"}, } } ) ) campaign = RepositorySnapshot( spec=RepositorySpec( name="govoplan-campaign", category="module", subtype="domain", remote="git@example.test:acme/govoplan-campaign.git", path="govoplan-campaign", ), absolute_path=str(workspace / "govoplan-campaign"), exists=True, is_git=True, has_head=True, branch="main", versions=VersionSnapshot(pyproject="1.2.3", webui_package="1.2.3"), ) dashboard = ReleaseDashboard( generated_at="2026-01-01T00:00:00Z", meta_root=str(workspace / "govoplan"), workspace_root=str(workspace), target_version=None, target_tag=None, online=False, include_migrations=False, summary=DashboardSummary( repository_count=1, missing_count=0, dirty_count=0, ahead_count=0, behind_count=0, no_head_count=0, error_count=0, safe_directory_count=0, local_target_tag_missing_count=0, status="ready", ), repositories=(campaign,), catalog=CatalogSnapshot( channel="stable", website_path="", catalog_path="", catalog_exists=False, keyring_path="", keyring_exists=False, ), ) plan = build_selective_release_plan( dashboard, selected_repos=("govoplan-campaign",), repo_versions={"govoplan-campaign": "1.2.3"}, ) self.assertEqual("blocked", plan.status) self.assertEqual("blocked", plan.units[0].status) self.assertTrue(any("Core release WebUI dependency" in item for item in plan.units[0].blockers)) def test_selected_repository_gate_reports_missing_version_metadata(self) -> None: with tempfile.TemporaryDirectory() as tmp: workspace = Path(tmp) (workspace / "govoplan-files").mkdir() issues = selected_repository_version_issues( repo_versions={"govoplan-files": "1.2.3"}, workspace=workspace, ) self.assertEqual(1, len(issues)) self.assertEqual("repository has no version metadata", issues[0].message) def test_selective_release_unit_is_blocked_by_version_drift(self) -> None: repo = RepositorySnapshot( spec=RepositorySpec( name="govoplan-example", category="module", subtype="domain", remote="git@example.test:acme/govoplan-example.git", path="govoplan-example", ), absolute_path="/tmp/govoplan-example", exists=True, is_git=True, has_head=True, versions=VersionSnapshot(pyproject="1.2.3", webui_package="1.2.4", manifests=("1.2.3",)), ) unit = build_unit(repo, target_version="1.2.5", contracts=None) self.assertEqual("blocked", unit.status) self.assertIn("version metadata is not aligned", unit.blockers[0]) def test_repository_versions_and_lockfiles_must_match(self) -> None: with tempfile.TemporaryDirectory() as tmp: root = Path(tmp) / "govoplan-example" (root / "src" / "govoplan_example" / "backend").mkdir(parents=True) (root / "webui").mkdir() (root / "pyproject.toml").write_text('[project]\nname = "govoplan-example"\nversion = "1.2.3"\n') (root / "package.json").write_text('{"name":"@govoplan/example","version":"1.2.3"}\n') (root / "webui" / "package.json").write_text('{"name":"@govoplan/example-webui","version":"1.2.4"}\n') (root / "webui" / "package-lock.json").write_text( json.dumps({"packages": {"": {"version": "1.2.2"}}}) ) (root / "src" / "govoplan_example" / "backend" / "manifest.py").write_text( 'manifest = ModuleManifest(\n version="1.2.3",\n)\n' ) issues = repository_version_issues(root) self.assertEqual(2, len(issues)) self.assertEqual({"webui/package.json", "webui/package-lock.json"}, {issue.source for issue in issues}) def test_public_runtime_version_must_match_package_metadata(self) -> None: with tempfile.TemporaryDirectory() as tmp: root = Path(tmp) / "govoplan-example" package = root / "src" / "govoplan_example" package.mkdir(parents=True) (root / "pyproject.toml").write_text( '[project]\nname = "govoplan-example"\nversion = "1.2.3"\n' ) (package / "__init__.py").write_text('__version__ = "1.2.2"\n') issues = repository_version_issues(root) self.assertEqual(1, len(issues)) self.assertEqual("package __init__.py 1", issues[0].source) self.assertEqual("1.2.3", issues[0].expected) self.assertEqual("1.2.2", issues[0].actual) def test_release_backend_and_frontend_refs_must_match(self) -> None: with tempfile.TemporaryDirectory() as tmp: meta = Path(tmp) / "govoplan" core = Path(tmp) / "govoplan-core" meta.mkdir() (core / "webui").mkdir(parents=True) (meta / "requirements-release.txt").write_text( "govoplan-example @ git+ssh://git@example.test/acme/govoplan-example.git@v1.2.3\n" ) (core / "webui" / "package.release.json").write_text( json.dumps( { "dependencies": { "@govoplan/example-webui": ( "git+ssh://git@example.test/acme/govoplan-example.git#v1.2.4" ) } } ) ) issues = release_composition_issues(meta, core_root=core) self.assertEqual(1, len(issues)) self.assertEqual("1.2.3", issues[0].expected) self.assertEqual("1.2.4", issues[0].actual) def test_release_refs_must_point_at_installable_versioned_artifacts(self) -> None: with tempfile.TemporaryDirectory() as tmp: workspace = Path(tmp) meta = workspace / "govoplan" core = workspace / "govoplan-core" module = workspace / "govoplan-example" meta.mkdir() (core / "webui").mkdir(parents=True) module.mkdir() (meta / "requirements-release.txt").write_text( "govoplan-example @ git+ssh://git@example.test/acme/govoplan-example.git@v1.2.3\n" ) (core / "webui" / "package.release.json").write_text( json.dumps( { "dependencies": { "@govoplan/example-webui": ( "git+ssh://git@example.test/acme/govoplan-example.git#v1.2.3" ) } } ) ) subprocess.run(["git", "init", "-q", str(module)], check=True) subprocess.run(["git", "-C", str(module), "config", "user.email", "test@example.test"], check=True) subprocess.run(["git", "-C", str(module), "config", "user.name", "Test"], check=True) (module / "README.md").write_text("uninstallable release\n") subprocess.run(["git", "-C", str(module), "add", "README.md"], check=True) subprocess.run(["git", "-C", str(module), "commit", "-qm", "release"], check=True) subprocess.run(["git", "-C", str(module), "tag", "v1.2.3"], check=True) issues = release_composition_issues(meta, core_root=core) self.assertEqual(2, len(issues)) self.assertEqual( {"v1.2.3:pyproject.toml", "v1.2.3:webui/package.json"}, {issue.source for issue in issues}, ) self.assertTrue(all("must contain" in issue.message for issue in issues)) if __name__ == "__main__": unittest.main()