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