diff --git a/docs/tools-reference.md b/docs/tools-reference.md index b48526e..4e5fd8e 100644 --- a/docs/tools-reference.md +++ b/docs/tools-reference.md @@ -26,7 +26,7 @@ whenever a tool's inputs/outputs change.** | Tool | Purpose | Run | Reads → Writes | |---|---|---|---| -| `validate.py` | Layered project validation front door. `core` regenerates/runtime-checks opcode metadata, lints canonical registries, runs pure Python tooling tests and all .NET engine tests, checks generated opcode references, and runs `git diff --check`. `workspace` adds corpus-derived global generation, the real-data Python suites, full SYS4 decode, and Python RECOVER. `runtime` adds the Godot C# build and forced-portable threaded self-test. `full` combines all phases and adds the booted faithful-wait C# scene sweep. Selected prerequisites are strict: an unavailable game/corpus/Godot requirement fails before execution instead of becoming a green skip. Each gate has a timeout and UTF-8 log under `build/validation/validate-/`; the final table reports results/durations and a before/after Godot-process leak audit. | `validate.py` (defaults to `--level full`) · `--level core|workspace|runtime|full` · `--godot ` · `--game-root ` · `--verbose` · `--fail-fast` | sources + selected toolchain/game/corpus prerequisites → console summary + ⚙ `build/validation/validate-*/.log` | +| `validate.py` | Layered project validation front door. `core` regenerates/runtime-checks opcode metadata, lints canonical registries, runs pure Python tooling tests and all .NET engine tests, checks generated opcode references, and runs `git diff --check`. `workspace` adds corpus-derived global generation, the real-data Python suites, full SYS4 decode, and Python RECOVER. `runtime` adds the Godot C# build and forced-portable threaded self-test. `full` combines all phases and adds the booted faithful-wait C# scene sweep. Selected prerequisites are strict: an unavailable game/corpus/Godot requirement fails before execution instead of becoming a green skip. Each gate has a timeout and UTF-8 log under `build/validation/validate-/`; Godot receives an isolated validation-owned user-data/log root there so it cannot read or modify the developer's saves/settings. The final table reports results/durations and a before/after Godot-process leak audit. | `validate.py` (defaults to `--level full`) · `--level core|workspace|runtime|full` · `--godot ` · `--game-root ` · `--verbose` · `--fail-fast` | sources + selected toolchain/game/corpus prerequisites → console summary + ⚙ `build/validation/validate-*/.log` | | `test_validate.py` | Pure tests for launcher-equivalent explicit/environment/PATH/conventional resolution precedence, invalid-explicit hard failure, level composition, and final gate ordering. | `test_validate.py` | temporary files only | Levels are cumulative around `core`: `workspace` means core+workspace-corpus, `runtime` means core+Godot, diff --git a/tools/test_validate.py b/tools/test_validate.py index c5529b2..f2a4ef7 100644 --- a/tools/test_validate.py +++ b/tools/test_validate.py @@ -90,11 +90,16 @@ class GatePlanTests(unittest.TestCase): def test_full_contains_every_phase(self) -> None: fake_godot = Path("godot") fake_root = Path("game") - keys = {gate.key for gate in validate.build_gate_plan("full", fake_godot, fake_root)} + state_root = Path("state") + plan = validate.build_gate_plan("full", fake_godot, fake_root, state_root) + keys = {gate.key for gate in plan} self.assertIn("sys4-corpus-validate", keys) self.assertIn("godot-selftest", keys) self.assertIn("age-cli-sweep", keys) - self.assertEqual("diff-check", validate.build_gate_plan("full", fake_godot, fake_root)[-1].key) + self.assertEqual("diff-check", plan[-1].key) + selftest = next(gate for gate in plan if gate.key == "godot-selftest") + self.assertIn(str(state_root / "godot.log"), selftest.command) + self.assertIn(("APPDATA", str(state_root / "appdata")), selftest.environment) if __name__ == "__main__": diff --git a/tools/validate.py b/tools/validate.py index 479da35..7863c95 100644 --- a/tools/validate.py +++ b/tools/validate.py @@ -56,6 +56,7 @@ class Gate: label: str command: tuple[str, ...] timeout_seconds: int = 180 + environment: tuple[tuple[str, str], ...] = () @dataclass(frozen=True) @@ -124,6 +125,7 @@ def build_gate_plan( level: str, godot: Path | None = None, game_root: Path | None = None, + runtime_state_root: Path | None = None, ) -> list[Gate]: phases = selected_phases(level) gates = [ @@ -185,6 +187,13 @@ def build_gate_plan( if "runtime" in phases: if godot is None or game_root is None: raise ValueError("runtime validation requires resolved Godot and game-root paths") + runtime_root = runtime_state_root or paths.BUILD / "validation" / "runtime-user" + runtime_environment = ( + ("APPDATA", str(runtime_root / "appdata")), + ("LOCALAPPDATA", str(runtime_root / "local-appdata")), + ("XDG_DATA_HOME", str(runtime_root / "xdg-data")), + ("XDG_CONFIG_HOME", str(runtime_root / "xdg-config")), + ) gates.extend(( Gate( "godot-build", @@ -196,10 +205,12 @@ def build_gate_plan( "godot-selftest", "Godot threaded self-test", ( - str(godot), "--headless", "--path", str(REPO / "godot"), "--", + str(godot), "--headless", "--log-file", str(runtime_root / "godot.log"), + "--path", str(REPO / "godot"), "--", "--selftest", "--game-root", str(game_root), "--text-backend", "portable", ), - 180, + 300, + runtime_environment, ), )) @@ -274,6 +285,7 @@ def run_gate(gate: Gate, log_dir: Path, verbose: bool) -> GateResult: "DOTNET_CLI_TELEMETRY_OPTOUT": "1", "DOTNET_NOLOGO": "1", }) + environment.update(dict(gate.environment)) popen_arguments = { "args": gate.command, "cwd": REPO, @@ -401,7 +413,9 @@ def main(argv: list[str] | None = None) -> int: log_dir.mkdir(parents=True, exist_ok=True) initial_godot = snapshot_godot_processes() results: list[GateResult] = [] - for gate in build_gate_plan(arguments.level, godot, game_root): + runtime_state_root = log_dir / "godot-user" + runtime_state_root.mkdir(parents=True, exist_ok=True) + for gate in build_gate_plan(arguments.level, godot, game_root, runtime_state_root): result = run_gate(gate, log_dir, arguments.verbose) results.append(result) if result.status == "FAIL" and arguments.fail_fast: