Files
govoplan/tests/test_devkit_catalog.py
T
zemion 2ffdb23f69
Dependency Audit / dependency-audit (push) Successful in 1m45s
Deployment Installer / deployment-installer (push) Successful in 6s
Security Audit / security-audit (push) Successful in 11m30s
feat(devkit): add resumable workspace automation and UI review tooling
Verified with the coordinated workspace changes by devkit full run
2026-09-08T225814-186389-0000-3e3ed7cd (all seven phases passed).
This shared UI pass does not mark the individual module reviews complete.
2026-09-09 02:03:17 +02:00

452 lines
18 KiB
Python
Executable File

"""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()