"""Bounded planning fixtures: no application imports, compilers or servers.""" from __future__ import annotations import argparse import json import shutil from pathlib import Path import sys import tempfile from types import SimpleNamespace import unittest from unittest.mock import patch sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "tools/devkit")) from govoplan_devkit.catalog import ( _expanded_repositories, build_stages, module_ui_stages, ) from govoplan_devkit.docs import build_doc_stages from govoplan_devkit.workspace import Project, Repository class CatalogTests(unittest.TestCase): def setUp(self): self.temp = tempfile.TemporaryDirectory(prefix="govoplan-devkit-catalog-") self.addCleanup(self.temp.cleanup) self.root = Path(self.temp.name) self.core = Repository("govoplan-core", self.root / "govoplan-core", ("core",)) self.meta = Repository("govoplan", self.root / "govoplan", ("meta",)) self.module = Repository( "govoplan-example", self.root / "govoplan-example", ("example",) ) self.project = Project("fixture", (self.meta, self.core, self.module), {}) for repo in self.project.repositories: repo.path.mkdir() (self.meta.path / "tools/checks").mkdir(parents=True) shutil.copyfile( Path(__file__).resolve().parents[1] / "tools/checks/focused-phases.json", self.meta.path / "tools/checks/focused-phases.json", ) def write(self, relative, content): path = self.root / relative path.parent.mkdir(parents=True, exist_ok=True) path.write_text(content, encoding="utf-8") def plan(self, profile, repos=None, changed=False): with patch("govoplan_devkit.workspace.load_project", return_value=self.project): return build_stages(self.root, profile, repos or [], changed) def test_ui_compiles_once_and_quick_does_not_compile(self): self.write( "govoplan-core/webui/package.json", json.dumps( {"scripts": {"test:components": "node scripts/run-component-tests.mjs"}} ), ) self.write("govoplan-core/webui/scripts/run-component-tests.mjs", "// fixture") self.assertFalse( any( "run-component-tests.mjs" in " ".join(item["argv"]) for item in self.plan("quick") ) ) matches = [ item for item in self.plan("ui") if "run-component-tests.mjs" in " ".join(item["argv"]) ] self.assertEqual(len(matches), 1) self.assertEqual(matches[0]["id"], "core.component-batch") def test_module_script_metadata_is_bounded_and_deduplicated(self): self.write( "govoplan-example/webui/package.json", json.dumps( { "scripts": { "test:interface-pattern": "node scripts/test-interface-pattern-language.mjs", "test:source": "node --test tests/source.test.mjs", "test:dangerous-chain": "node tests/source.test.mjs && npm run dev", "test:flags": "node --eval 'startServer()'", } } ), ) self.write( "govoplan-example/webui/scripts/test-interface-pattern-language.mjs", "// source-only fixture", ) self.write( "govoplan-example/webui/tests/source.test.mjs", "// source-only fixture" ) stages = module_ui_stages(self.module, reason="fixture") self.assertEqual(len(stages), 2) self.assertEqual(len({item["id"] for item in stages}), 2) self.assertTrue(all(item["argv"][0] == "{node}" for item in stages)) self.assertFalse(any("&&" in item["argv"] for item in stages)) def test_full_is_never_narrowed_by_repo_filter(self): result = self.plan("full", ["example"]) self.assertEqual( [item["id"] for item in result], [ "focused." + identity for identity in ( "preflight", "tooling", "backend", "core-ui", "module-builds", "browser", "module-ui", ) ], ) self.assertEqual( [item["after"] for item in result], [[], *[[item["id"]] for item in result[:-1]]], ) self.assertTrue(all(item["deps"] == [] for item in result)) self.assertTrue( all( item["argv"] == [ "bash", str(self.meta.path / "tools/checks/check-focused.sh"), "--phase", item["id"].removeprefix("focused."), ] for item in result ) ) self.assertIn("webui:govoplan-example", result[-1]["resources"]) self.assertIn("backend:test-state", result[0]["resources"]) def test_changed_empty_is_not_full_verification(self): with ( patch("govoplan_devkit.workspace.load_project", return_value=self.project), patch("govoplan_devkit.workspace.selected_repositories", return_value=[]), ): self.assertEqual(build_stages(self.root, "quick", [], True), []) self.assertEqual(len(build_stages(self.root, "full", [], True)), 7) def test_full_ui_scope_excludes_backend_only_but_includes_all_ui_owners(self): backend = Repository( "govoplan-backend-only", self.root / "govoplan-backend-only" ) backend.path.mkdir() (self.module.path / "webui").mkdir() (self.core.path / "webui").mkdir() project = Project("fixture", (*self.project.repositories, backend), {}) with patch("govoplan_devkit.workspace.load_project", return_value=project): checks = build_stages(self.root, "full", ["example"], False) for check in checks[:3]: self.assertNotIn("inputs", check) for check in checks[3:]: self.assertEqual( check["inputs"]["repos"], ["govoplan", "govoplan-core", "govoplan-example"], ) (backend.path / "webui").mkdir() with patch("govoplan_devkit.workspace.load_project", return_value=project): replanned = build_stages(self.root, "full", [], False) self.assertIn(backend.name, replanned[3]["inputs"]["repos"]) def test_full_missing_checkout_explicitly_falls_back_to_workspace_inputs(self): missing = Repository("govoplan-missing", self.root / "govoplan-missing") project = Project("fixture", (*self.project.repositories, missing), {}) with patch("govoplan_devkit.workspace.load_project", return_value=project): checks = build_stages(self.root, "full", [], False) self.assertTrue(all("inputs" not in check for check in checks)) self.assertTrue( all( any("workspace-wide" in note for note in check["coverage_notes"]) for check in checks[3:] ) ) def test_full_requires_authoritative_phase_metadata(self): (self.meta.path / "tools/checks/focused-phases.json").write_text("{}") with self.assertRaisesRegex(ValueError, "phase metadata"): self.plan("full") def test_native_ui_guards_and_component_batch_share_safe_ui_owner_scope(self): backend = Repository( "govoplan-backend-only", self.root / "govoplan-backend-only" ) backend.path.mkdir() (self.module.path / "webui").mkdir() (self.core.path / "webui").mkdir() project = Project("fixture", (*self.project.repositories, backend), {}) with patch("govoplan_devkit.workspace.load_project", return_value=project): checks = build_stages(self.root, "ui", ["example"], False) for check in checks: if check["id"] in { "jsx-value-imports", "heading-help", "core.component-batch", }: self.assertEqual( check["inputs"]["repos"], ["govoplan", "govoplan-core", "govoplan-example"], ) else: self.assertNotIn("inputs", check) def test_unregistered_src_or_webui_disables_all_native_reuse(self): for directory in ("src", "webui"): with self.subTest(directory=directory): unknown = self.root / "govoplan-unregistered" / directory unknown.mkdir(parents=True) try: for profile in ("quick", "ui", "backend", "full"): checks = self.plan(profile) self.assertTrue(checks) self.assertTrue( all(check["reuse"] == "never" for check in checks) ) self.assertTrue( all( any( "Unregistered sibling" in note for note in check["coverage_notes"] ) for check in checks ) ) finally: unknown.rmdir() unknown.parent.rmdir() (self.root / "govoplan-unused-empty").mkdir() self.assertTrue(all("reuse" not in check for check in self.plan("full"))) def test_unregistered_broken_source_symlink_does_not_allow_reuse(self): unknown = self.root / "govoplan-unregistered" unknown.mkdir() (unknown / "src").symlink_to(self.root / "missing") self.assertTrue(all(check["reuse"] == "never" for check in self.plan("full"))) def test_unknown_repo_is_not_silently_ignored(self): with self.assertRaisesRegex(ValueError, "Unknown repository"): self.plan("ui", ["not-a-repo"]) def test_changed_provider_selects_transitive_declared_consumers(self): other = Repository("govoplan-other", self.root / "govoplan-other") final = Repository("govoplan-final", self.root / "govoplan-final") project = Project("fixture", (self.meta, self.module, other, final), {}) self.write("govoplan/tools/release/govoplan_release/contracts.py", "# fixture") for name in ("example", "other", "final"): self.write(f"govoplan-{name}/src/fixture/backend/manifest.py", "# fixture") def contract(_path, repo_name): gives = { self.module.name: ["first"], other.name: ["second"], final.name: [], }[repo_name] needs = { self.module.name: [], other.name: ["first"], final.name: ["second"], }[repo_name] return SimpleNamespace( repo=repo_name, provides_interfaces=[SimpleNamespace(name=value) for value in gives], requires_interfaces=[SimpleNamespace(name=value) for value in needs], ) with patch.dict( sys.modules, { "govoplan_release.contracts": SimpleNamespace( parse_manifest_contract=contract ) }, ): selected, reason = _expanded_repositories( project, [self.module], changed=True ) self.assertEqual( [repo.name for repo in selected], [self.module.name, other.name, final.name] ) self.assertIn("declared interface consumers", reason) def test_generic_dependency_closure_includes_filtered_prerequisites(self): config = { "schema_version": 1, "name": "generic", "repositories": [ {"name": "one", "path": "one"}, {"name": "two", "path": "two"}, ], "checks": [ { "id": "compile", "argv": ["{node}", "compile.mjs"], "cwd": "one", "repos": ["two"], }, { "id": "test", "argv": ["{node}", "test.mjs"], "cwd": "one", "deps": ["compile"], "repos": ["one"], }, { "id": "other", "argv": ["{python}", "test.py"], "cwd": "two", "repos": ["two"], }, ], "profiles": {"quick": ["test", "other"]}, } self.write("project.json", json.dumps(config)) result = build_stages( self.root, "quick", ["one"], False, self.root / "project.json" ) self.assertEqual([item["id"] for item in result], ["compile", "test"]) self.assertEqual(result[1]["deps"], ["compile"]) self.assertEqual(result[1]["cwd"], str(self.root / "one")) def test_generic_cycles_and_duplicate_ids_fail(self): config = { "schema_version": 1, "name": "generic", "repositories": [{"name": "one", "path": "one"}], "checks": [ {"id": "one", "argv": ["test"], "deps": ["two"]}, {"id": "two", "argv": ["test"], "deps": ["one"]}, ], "profiles": {"quick": ["one"]}, } self.write("project.json", json.dumps(config)) with self.assertRaisesRegex(ValueError, "Cyclic"): build_stages(self.root, "quick", [], False, self.root / "project.json") config["checks"][1]["id"] = "one" self.write("project.json", json.dumps(config)) with self.assertRaisesRegex(ValueError, "Duplicate"): build_stages(self.root, "quick", [], False, self.root / "project.json") def test_generic_order_prerequisites_inputs_and_reuse_survive_planning(self): config = { "schema_version": 1, "name": "generic", "repositories": [ {"name": "one", "path": "one"}, {"name": "two", "path": "two"}, ], "checks": [ { "id": "prepare", "argv": ["prepare"], "repos": ["two"], "reuse": "never", "inputs": {"repos": ["two"]}, }, { "id": "test", "argv": ["test"], "repos": ["one"], "after": ["prepare"], "reuse": "verified", "inputs": {"repos": ["one"]}, }, {"id": "broad", "argv": ["check"], "repos": ["one"]}, ], "profiles": {"quick": ["test", "broad"]}, } self.write("project.json", json.dumps(config)) result = build_stages( self.root, "quick", ["one"], False, self.root / "project.json" ) self.assertEqual([item["id"] for item in result], ["prepare", "test", "broad"]) self.assertEqual(result[1]["after"], ["prepare"]) self.assertEqual(result[1]["deps"], []) self.assertEqual(result[0]["reuse"], "never") self.assertEqual(result[1]["reuse"], "verified") self.assertEqual(result[0]["inputs"], {"repos": ["two"]}) self.assertEqual(result[1]["inputs"], {"repos": ["one"]}) self.assertNotIn("inputs", result[2]) def test_generic_original_types_are_validated_before_coercion(self): for field, invalid in ( ("argv", "false"), ("argv", None), ("resources", "shared"), ("cwd", None), ("cwd", "../outside"), ("title", 5), ("timeout_seconds", True), ): with self.subTest(field=field, invalid=invalid): config = { "schema_version": 1, "name": "generic", "repositories": [{"name": "one", "path": "one"}], "checks": [{"id": "test", "argv": ["test"], field: invalid}], "profiles": {"quick": ["test"]}, } self.write("project.json", json.dumps(config)) with self.assertRaises(ValueError): build_stages( self.root, "quick", [], False, self.root / "project.json" ) def test_ui_coverage_notes_make_excluded_suites_explicit_and_discover_tests_folder( self, ): self.write( "govoplan-example/webui/package.json", json.dumps({"scripts": {"test:full-ui": "tsc && node tests/full-ui.js"}}), ) self.write( "govoplan-example/webui/tests/aggregate-report-structure.test.mjs", "// fixture", ) stages = self.plan("ui", ["example"]) self.assertTrue( any("aggregate-report-structure" in item["id"] for item in stages) ) self.assertTrue( any("test:full-ui" in note for note in stages[0]["coverage_notes"]) ) def test_docs_reuses_existing_guards_and_only_narrows_plain_labels(self): args = argparse.Namespace( workspace_root=self.root, state_dir=self.root / "state", project=None, repo=["example"], changed=False, ) with patch("govoplan_devkit.docs.load_project", return_value=self.project): stages = build_doc_stages(args) self.assertEqual(len(stages), 4) self.assertIn("check-manifest-shapes.py", " ".join(stages[0]["argv"])) self.assertIn("platform-interface-inventory.py", " ".join(stages[1]["argv"])) self.assertIn("--strict-declarations", stages[1]["argv"]) self.assertEqual(stages[3]["argv"][-2:], ["--repo", "govoplan-example"]) self.assertFalse( (self.root / "state").exists(), "Planning must not create artifacts" ) if __name__ == "__main__": unittest.main()