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.
394 lines
13 KiB
Python
Executable File
394 lines
13 KiB
Python
Executable File
"""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())
|