From 2220cdd1577ea29c64b9efd7f4ddd92883b0ac04 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 20 Oct 2026 18:28:25 +0000 Subject: [PATCH] test: DFG Re20 provenance refuses dirty trees; versioned receipts Per review of aceea70e: a dirty checkout is refused (HEAD would not describe the code that runs, and a dirty-to-dirty edit would pass the end-of-run check). On a clean tree HEAD pins the imported helpers too. plan.json and result.json carry schema=2; schema-1 receipts (bare list, e.g. evidence 70f6617) are documented. Adds a dirty-refusal test. Co-Authored-By: Claude Opus 5.5 --- docs/validation/dfg_re20.md | 3 +++ tests/unit/test_dfg_re20.py | 10 +++++++++- tests/validation/run_dfg_re20.py | 14 ++++++++++---- 3 files changed, 22 insertions(+), 5 deletions(-) diff --git a/docs/validation/dfg_re20.md b/docs/validation/dfg_re20.md index 26d56bf..1f1dc55 100644 --- a/docs/validation/dfg_re20.md +++ b/docs/validation/dfg_re20.md @@ -46,4 +46,7 @@ CUDA_VISIBLE_DEVICES= python tests/validation/run_dfg_re20.py --case both \ The uniform D80 control separates method error from refinement error. `--plan` writes `plan.json` only. A GPU run writes `plan.json` and `result.json` (per case: lattice parameters, every sample, window ranges, status, verdict). +Receipts: `plan.json` and `result.json` carry `schema` 2 and `provenance` (HEAD, src tree, +runner hash; clean checkout required, refused if it changes during the run). Receipts +written before this (schema 1, e.g. evidence 70f6617) are a bare list of cases. Host contract tests: `tests/unit/test_dfg_re20.py`. diff --git a/tests/unit/test_dfg_re20.py b/tests/unit/test_dfg_re20.py index ec7b685..6036fbc 100644 --- a/tests/unit/test_dfg_re20.py +++ b/tests/unit/test_dfg_re20.py @@ -82,7 +82,15 @@ class Re20Provenance(unittest.TestCase): self.assertEqual(len(record["checkout_revision"]), 40) self.assertEqual(len(record["src_tree"]), 40) self.assertEqual(len(record["runner_sha256"]), 64) - self.assertIn("checkout_dirty", record) + self.assertFalse(record["checkout_dirty"]) + self.assertEqual(v.RESULT_SCHEMA, 2) + + def test_dirty_checkout_is_refused(self): + from unittest.mock import patch + with patch("source_provenance.subprocess.check_output", + side_effect=["f" * 40 + "\n", " M src/x.py\n"]): + with self.assertRaisesRegex(RuntimeError, "clean checkout"): + v.provenance(rr.DEFAULT_SOURCE) if __name__ == "__main__": diff --git a/tests/validation/run_dfg_re20.py b/tests/validation/run_dfg_re20.py index 9700042..190cad6 100644 --- a/tests/validation/run_dfg_re20.py +++ b/tests/validation/run_dfg_re20.py @@ -57,6 +57,8 @@ WINDOW_TSTAR = 10.0 # trailing 5 s, 21 samples BANDS = dict(cd=(5.57, 5.59), cl=(0.0104, 0.0110), p_diff=(0.1172, 0.1176)) STEADY_RANGE = {k: (hi - lo) / 10 for k, (lo, hi) in BANDS.items()} STEADY_SLOPE = {k: (hi - lo) / 50 for k, (lo, hi) in BANDS.items()} +# 2: {schema, provenance, cases}; the earlier receipts (schema 1) are a bare list of cases. +RESULT_SCHEMA = 2 DEVIATIONS = ("pressure-NEQ outlet (neq_extrap), not do-nothing", "local Zou-He inlet") _re100_config = rr.config @@ -201,8 +203,11 @@ def provenance(source): from source_provenance import checkout_provenance tree = subprocess.check_output(["git", "-C", str(source), "rev-parse", "HEAD:src"], text=True).strip() - return dict(source_mode="current_checkout", **checkout_provenance(source), src_tree=tree, - runner_sha256=hashlib.sha256(Path(__file__).read_bytes()).hexdigest()) + record = dict(source_mode="current_checkout", **checkout_provenance(source), src_tree=tree, + runner_sha256=hashlib.sha256(Path(__file__).read_bytes()).hexdigest()) + if record["checkout_dirty"]: # HEAD would not describe the code that runs + raise RuntimeError("DFG Re20 runs require a clean checkout") + return record def main(): @@ -215,7 +220,7 @@ def main(): parser.add_argument("--max-wall-seconds", type=float) a = parser.parse_args() a.source = a.source.resolve() - record = dict(plan(a.collision), provenance=provenance(a.source)) + record = dict(plan(a.collision), schema=RESULT_SCHEMA, provenance=provenance(a.source)) a.output.mkdir(parents=True, exist_ok=False) (a.output / "plan.json").write_text(json.dumps(record, indent=2, allow_nan=False)) if a.plan: @@ -230,7 +235,8 @@ def main(): after = provenance(a.source) if after != record["provenance"]: raise RuntimeError("Source checkout changed during the run") - (a.output / "result.json").write_text(json.dumps(dict(provenance=after, cases=results), + (a.output / "result.json").write_text(json.dumps(dict(schema=RESULT_SCHEMA, provenance=after, + cases=results), indent=2, allow_nan=False))