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.
452 lines
18 KiB
Python
Executable File
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()
|