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.
This commit is contained in:
Executable
+451
@@ -0,0 +1,451 @@
|
||||
"""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()
|
||||
Reference in New Issue
Block a user