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
+393
@@ -0,0 +1,393 @@
|
||||
"""Canonical phase dispatch tested with inert tools in disposable workspaces."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
META_ROOT = Path(__file__).resolve().parents[1]
|
||||
SCRIPT = META_ROOT / "tools/checks/check-focused.sh"
|
||||
METADATA = META_ROOT / "tools/checks/focused-phases.json"
|
||||
PHASE_IDS = [
|
||||
"preflight",
|
||||
"tooling",
|
||||
"backend",
|
||||
"core-ui",
|
||||
"module-builds",
|
||||
"browser",
|
||||
"module-ui",
|
||||
]
|
||||
# These hashes bind the original working-copy check bodies at the phase split.
|
||||
# Only two explicit Core/webui cd lines were added for independent invocation.
|
||||
# Future deliberate changes to the canonical gate must update this contract.
|
||||
LEGACY_BODY_SHA256 = {
|
||||
"preflight": "b9c90fd3c84df788de5f2c001443672f683a9918459e1a7955ed4a225e6f20dd",
|
||||
"tooling": "3d9a5bf32acbe97134f51327ed4b2457a69ad23b723c15a2b9bd0dce7f82396b",
|
||||
"backend": "dd57c33919f06bd240516bbe47861be022e0b1c047334eedc7f6c596c2c49632",
|
||||
"core-ui": "613f806a602970f1dbfb243f4c96ccc6ca4836849bb1f1a2c4cff97aea6f3aeb",
|
||||
"module-builds": "1f2cd1e2c336f748fbf2972a2da87a0efec58ef9b084a9433511f794a456fa84",
|
||||
"browser": "aa20d1e1d4ec9f00bc27c06cdee7ffa2456d3d3b8a7155da4e888a7e2209306d",
|
||||
"module-ui": "c47794d82eb7bb47de114e86a95264e5898886f743c31eb7bdf82a079c827c9f",
|
||||
}
|
||||
EXTRA_TEST_COMMAND = '"$PYTHON" -m pytest -q tests/test_focused_phases.py\n'
|
||||
|
||||
|
||||
def definitions():
|
||||
source = SCRIPT.read_text()
|
||||
return dict(
|
||||
re.findall(
|
||||
r"^# devkit-phase: ([a-z-]+) begin\n(.*?)^# devkit-phase: \1 end$",
|
||||
source,
|
||||
re.MULTILINE | re.DOTALL,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def test_original_commands_remain_exactly_once_in_original_order():
|
||||
bodies = definitions()
|
||||
metadata = json.loads(METADATA.read_text())
|
||||
assert list(bodies) == PHASE_IDS
|
||||
assert [phase["id"] for phase in metadata["phases"]] == PHASE_IDS
|
||||
assert SCRIPT.read_text().count(EXTRA_TEST_COMMAND) == 1
|
||||
for identity, body in bodies.items():
|
||||
assert SCRIPT.read_text().count(f"# devkit-phase: {identity} begin") == 1
|
||||
if identity == "tooling":
|
||||
assert body.count(EXTRA_TEST_COMMAND) == 1
|
||||
body = body.replace(EXTRA_TEST_COMMAND, "")
|
||||
assert hashlib.sha256(body.encode()).hexdigest() == LEGACY_BODY_SHA256[identity]
|
||||
|
||||
|
||||
def test_ordering_and_artifact_dependencies_are_distinct():
|
||||
phases = json.loads(METADATA.read_text())["phases"]
|
||||
assert [phase["order_after"] for phase in phases] == [
|
||||
[],
|
||||
*[[identity] for identity in PHASE_IDS[:-1]],
|
||||
]
|
||||
assert all(phase["depends_on"] == [] for phase in phases)
|
||||
by_id = {phase["id"]: phase for phase in phases}
|
||||
assert "{core}/webui/dist" in by_id["module-builds"]["outputs"]
|
||||
assert any(
|
||||
"does not serve or require module-builds dist" in note
|
||||
for note in by_id["browser"]["notes"]
|
||||
)
|
||||
assert "port:4174" in by_id["browser"]["resources"]
|
||||
cwd_lines = {
|
||||
"core": 'cd "$ROOT"',
|
||||
"meta": 'cd "$META_ROOT"',
|
||||
"core-webui": 'cd "$ROOT/webui"',
|
||||
"access-webui": 'cd "${WORKSPACE_ROOT}/govoplan-access/webui"',
|
||||
}
|
||||
for phase in phases:
|
||||
assert definitions()[phase["id"]].splitlines()[0] == cwd_lines[phase["cwd"]]
|
||||
|
||||
|
||||
FAKE_TOOL = r"""
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import sys
|
||||
|
||||
log = Path(os.environ["FOCUSED_FIXTURE_LOG"])
|
||||
record = {
|
||||
"tool": Path(sys.argv[0]).name,
|
||||
"argv": sys.argv[1:],
|
||||
"cwd": os.getcwd(),
|
||||
"env": {key: os.environ.get(key) for key in (
|
||||
"GOVOPLAN_WORKSPACE_ROOT", "NPM_CONFIG_USERCONFIG", "GOVOPLAN_NPM_USERCONFIG",
|
||||
"NPM_CONFIG_TMP", "npm_config_tmp", "PYTHONPATH", "PATH",
|
||||
)},
|
||||
}
|
||||
if "-" in sys.argv[1:]:
|
||||
record["stdin"] = sys.stdin.read()
|
||||
with log.open("a") as handle:
|
||||
handle.write(json.dumps(record) + "\n")
|
||||
if os.environ.get("FOCUSED_FIXTURE_FAIL_TOKEN") in sys.argv[1:]:
|
||||
raise SystemExit(7)
|
||||
"""
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def fixture_workspace(tmp_path):
|
||||
workspace = tmp_path / "workspace"
|
||||
meta = workspace / "govoplan"
|
||||
core = workspace / "govoplan-core"
|
||||
copied = meta / "tools/checks/check-focused.sh"
|
||||
copied.parent.mkdir(parents=True)
|
||||
shutil.copyfile(SCRIPT, copied)
|
||||
shutil.copyfile(METADATA, copied.with_name("focused-phases.json"))
|
||||
for name in (
|
||||
"govoplan",
|
||||
"govoplan-core",
|
||||
"govoplan-access",
|
||||
"govoplan-payments",
|
||||
"govoplan-dataflow",
|
||||
"govoplan-datasources",
|
||||
"govoplan-workflow",
|
||||
"govoplan-dashboard",
|
||||
"govoplan-approvals",
|
||||
"govoplan-postbox",
|
||||
"govoplan-mail",
|
||||
"govoplan-files",
|
||||
"govoplan-campaign",
|
||||
"govoplan-policy",
|
||||
"govoplan-wiki",
|
||||
):
|
||||
(workspace / name / "webui").mkdir(parents=True, exist_ok=True)
|
||||
(workspace / name / "src").mkdir(exist_ok=True)
|
||||
(meta / "tests").mkdir()
|
||||
for name in ("test_devkit_alpha.py", "test_devkit_beta.py"):
|
||||
(meta / "tests" / name).write_text("# inert glob fixture\n")
|
||||
fake_bin = tmp_path / "bin"
|
||||
fake_bin.mkdir()
|
||||
for target in [
|
||||
*(fake_bin / name for name in ("python-check", "node", "npm", "bash")),
|
||||
core / "webui/node_modules/.bin/tsc",
|
||||
meta / "tools/checks/check_dependency_boundaries.py",
|
||||
]:
|
||||
target.parent.mkdir(parents=True, exist_ok=True)
|
||||
target.write_text(f"#!{sys.executable}\n" + FAKE_TOOL)
|
||||
target.chmod(0o700)
|
||||
temporary = tmp_path / "temporary"
|
||||
temporary.mkdir()
|
||||
log = tmp_path / "calls.jsonl"
|
||||
env = {
|
||||
**os.environ,
|
||||
"PATH": str(fake_bin) + os.pathsep + os.environ["PATH"],
|
||||
"GOVOPLAN_WORKSPACE_ROOT": str(workspace),
|
||||
"GOVOPLAN_CORE_ROOT": str(core),
|
||||
"PYTHON": str(fake_bin / "python-check"),
|
||||
"NODE": str(fake_bin / "node"),
|
||||
"NPM": str(fake_bin / "npm"),
|
||||
"TMPDIR": str(temporary),
|
||||
"FOCUSED_FIXTURE_LOG": str(log),
|
||||
"PYTHONPATH": "inherited-fixture-tail",
|
||||
"NPM_CONFIG_TMP": "must-be-unset",
|
||||
"npm_config_tmp": "must-be-unset",
|
||||
}
|
||||
env.pop("FOCUSED_FIXTURE_FAIL_TOKEN", None)
|
||||
return {
|
||||
"workspace": workspace,
|
||||
"meta": meta,
|
||||
"core": core,
|
||||
"script": copied,
|
||||
"env": env,
|
||||
"log": log,
|
||||
"temporary": temporary,
|
||||
}
|
||||
|
||||
|
||||
def invoke(fixture, *arguments, env=None):
|
||||
return subprocess.run(
|
||||
["/bin/bash", str(fixture["script"]), *arguments],
|
||||
cwd=fixture["workspace"].parent,
|
||||
env=env or fixture["env"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=30,
|
||||
)
|
||||
|
||||
|
||||
def records(fixture):
|
||||
path = fixture["log"]
|
||||
return (
|
||||
[json.loads(line) for line in path.read_text().splitlines()]
|
||||
if path.exists()
|
||||
else []
|
||||
)
|
||||
|
||||
|
||||
def normalized(calls):
|
||||
result = []
|
||||
for call in calls:
|
||||
call = json.loads(json.dumps(call))
|
||||
call["env"].pop("NPM_CONFIG_USERCONFIG")
|
||||
call["env"].pop("GOVOPLAN_NPM_USERCONFIG")
|
||||
result.append(call)
|
||||
return result
|
||||
|
||||
|
||||
def test_default_gate_equals_sequential_independent_phases(fixture_workspace):
|
||||
fixture = fixture_workspace
|
||||
full = invoke(fixture)
|
||||
assert full.returncode == 0, full.stderr
|
||||
original = records(fixture)
|
||||
offset = len(original)
|
||||
for identity in PHASE_IDS:
|
||||
isolated = invoke(fixture, "--phase", identity)
|
||||
assert isolated.returncode == 0, isolated.stderr
|
||||
assert normalized(original) == normalized(records(fixture)[offset:])
|
||||
assert len({call["env"]["NPM_CONFIG_USERCONFIG"] for call in original}) == len(
|
||||
PHASE_IDS
|
||||
)
|
||||
assert not list(fixture["temporary"].iterdir())
|
||||
assert sum("test:conformance" in call["argv"] for call in original) == 1
|
||||
assert sum("test:module-permutations" in call["argv"] for call in original) == 1
|
||||
assert sum("tests/test_focused_phases.py" in call["argv"] for call in original) == 1
|
||||
# The here-document remains one Python invocation, not executed shell text.
|
||||
ast_scan = [call for call in original if "stdin" in call]
|
||||
assert len(ast_scan) == 1
|
||||
assert "AST syntax check passed for" in ast_scan[0]["stdin"]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("identity", PHASE_IDS)
|
||||
def test_each_phase_initializes_its_own_cwd_and_environment(
|
||||
fixture_workspace, identity
|
||||
):
|
||||
fixture = fixture_workspace
|
||||
result = invoke(fixture, "--phase", identity)
|
||||
assert result.returncode == 0, result.stderr
|
||||
calls = records(fixture)
|
||||
assert calls
|
||||
phase = next(
|
||||
item
|
||||
for item in json.loads(METADATA.read_text())["phases"]
|
||||
if item["id"] == identity
|
||||
)
|
||||
starts = {
|
||||
"core": fixture["core"],
|
||||
"meta": fixture["meta"],
|
||||
"core-webui": fixture["core"] / "webui",
|
||||
"access-webui": fixture["workspace"] / "govoplan-access/webui",
|
||||
}
|
||||
assert calls[0]["cwd"] == str(starts[phase["cwd"]])
|
||||
for call in calls:
|
||||
env = call["env"]
|
||||
assert env["GOVOPLAN_WORKSPACE_ROOT"] == str(fixture["workspace"])
|
||||
assert env["NPM_CONFIG_USERCONFIG"] == env["GOVOPLAN_NPM_USERCONFIG"]
|
||||
assert Path(env["NPM_CONFIG_USERCONFIG"]).parent == fixture["temporary"]
|
||||
assert not Path(env["NPM_CONFIG_USERCONFIG"]).exists()
|
||||
assert env["NPM_CONFIG_TMP"] is None and env["npm_config_tmp"] is None
|
||||
assert env["PATH"].split(os.pathsep)[0] == str(
|
||||
fixture["core"] / "webui/node_modules/.bin"
|
||||
)
|
||||
assert env["PYTHONPATH"].endswith(os.pathsep + "inherited-fixture-tail")
|
||||
assert str(fixture["core"] / "src") in env["PYTHONPATH"].split(os.pathsep)
|
||||
assert not list(fixture["temporary"].iterdir())
|
||||
|
||||
|
||||
def test_full_is_fail_fast_and_failure_cleans_setup(fixture_workspace):
|
||||
fixture = fixture_workspace
|
||||
result = invoke(
|
||||
fixture,
|
||||
env={
|
||||
**fixture["env"],
|
||||
"FOCUSED_FIXTURE_FAIL_TOKEN": "test:module-permutations",
|
||||
},
|
||||
)
|
||||
assert result.returncode == 7
|
||||
calls = records(fixture)
|
||||
assert calls[-1]["argv"] == ["run", "test:module-permutations"]
|
||||
assert not any("test:conformance" in call["argv"] for call in calls)
|
||||
assert not any("test:passwords" in call["argv"] for call in calls)
|
||||
assert not list(fixture["temporary"].iterdir())
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"arguments",
|
||||
[
|
||||
["--unknown"],
|
||||
["--phase"],
|
||||
["--phase", ""],
|
||||
["--phase", "not-a-phase"],
|
||||
["--phase", "browser", "--phase", "tooling"],
|
||||
["--list-phases", "--phase", "browser"],
|
||||
["--phase", "browser", "--list-phases"],
|
||||
["--list-phases", "--list-phases"],
|
||||
["--json"],
|
||||
["--list-phases", "--json", "--json"],
|
||||
["--phase", "browser; touch unexpected"],
|
||||
],
|
||||
)
|
||||
def test_invalid_selection_is_rejected_before_setup(fixture_workspace, arguments):
|
||||
fixture = fixture_workspace
|
||||
env = {
|
||||
**fixture["env"],
|
||||
"GOVOPLAN_CORE_ROOT": str(fixture["workspace"] / "absent-core"),
|
||||
"NODE": "absent-node",
|
||||
"NPM": "absent-npm",
|
||||
}
|
||||
result = invoke(fixture, *arguments, env=env)
|
||||
assert result.returncode == 2
|
||||
assert "check-focused:" in result.stderr
|
||||
assert not fixture["log"].exists()
|
||||
assert not list(fixture["temporary"].iterdir())
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"arguments",
|
||||
[["--list-phases"], ["--list-phases", "--json"], ["--json", "--list-phases"]],
|
||||
)
|
||||
def test_listing_is_read_only_without_product_tools(fixture_workspace, arguments):
|
||||
fixture = fixture_workspace
|
||||
env = {
|
||||
**fixture["env"],
|
||||
"GOVOPLAN_CORE_ROOT": str(fixture["workspace"] / "absent-core"),
|
||||
"PYTHON": "/absent-python",
|
||||
"NODE": "absent-node",
|
||||
"NPM": "absent-npm",
|
||||
}
|
||||
result = invoke(fixture, *arguments, env=env)
|
||||
assert result.returncode == 0, result.stderr
|
||||
if "--json" in arguments:
|
||||
assert json.loads(result.stdout) == json.loads(METADATA.read_text())
|
||||
else:
|
||||
assert [line.split("\t")[0] for line in result.stdout.splitlines()] == PHASE_IDS
|
||||
assert not fixture["log"].exists()
|
||||
assert not list(fixture["temporary"].iterdir())
|
||||
|
||||
|
||||
def test_metadata_loading_never_imports_project_python_modules(fixture_workspace):
|
||||
fixture = fixture_workspace
|
||||
foreign = fixture["workspace"].parent / "json.py"
|
||||
foreign.write_text(
|
||||
"raise AssertionError('project module imported during listing')\n"
|
||||
)
|
||||
result = invoke(
|
||||
fixture,
|
||||
"--list-phases",
|
||||
"--json",
|
||||
env={**fixture["env"], "PYTHONPATH": str(foreign.parent)},
|
||||
)
|
||||
assert result.returncode == 0, result.stderr
|
||||
assert json.loads(result.stdout)["schema_version"] == 1
|
||||
assert not fixture["log"].exists()
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"change",
|
||||
[
|
||||
"unknown-fields",
|
||||
"duplicate-id",
|
||||
"unknown-cwd",
|
||||
"later-prerequisite",
|
||||
"invalid-version",
|
||||
],
|
||||
)
|
||||
def test_malformed_metadata_fails_before_any_check(fixture_workspace, change):
|
||||
fixture = fixture_workspace
|
||||
path = fixture["script"].with_name("focused-phases.json")
|
||||
catalog = json.loads(path.read_text())
|
||||
if change == "unknown-fields":
|
||||
catalog["phases"][0]["shell"] = "touch should-not-run"
|
||||
elif change == "duplicate-id":
|
||||
catalog["phases"][1]["id"] = catalog["phases"][0]["id"]
|
||||
elif change == "unknown-cwd":
|
||||
catalog["phases"][0]["cwd"] = "elsewhere"
|
||||
elif change == "later-prerequisite":
|
||||
catalog["phases"][0]["depends_on"] = ["browser"]
|
||||
else:
|
||||
catalog["schema_version"] = True
|
||||
path.write_text(json.dumps(catalog))
|
||||
result = invoke(fixture, "--phase", "browser")
|
||||
assert result.returncode == 2
|
||||
assert not fixture["log"].exists()
|
||||
assert not list(fixture["temporary"].iterdir())
|
||||
Reference in New Issue
Block a user