test: final source recheck for V1 jet CV and V3 Taylor-Green runs

Both runners now re-derive the recorded source identity after the run
(source_provenance.final_source_check) and only then mark the receipt
complete. A changed source, or a recheck that fails, rewrites the receipt
as invalid and raises:
- V1: in-run receipts carry run_status=INCOMPLETE; finish_receipt sets
  COMPLETE or INVALID_SOURCE_CHANGED (+ source_recheck/source_after).
- V3: completed stays False, invalid=SOURCE_CHANGED_DURING_RUN, no
  screening_pass; the assessor requires source_recheck UNCHANGED (frozen
  mode still accepts receipts from before the field existed).
Interrupted runs keep INCOMPLETE / completed=False. Host tests cover
unchanged, changed and failed rechecks and incomplete runs. No threshold
or solver change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Claude
2026-10-20 18:44:23 +00:00
co-authored by Claude Opus 5.5
parent 8cae58cdf1
commit df0f586331
7 changed files with 159 additions and 15 deletions
+35
View File
@@ -196,5 +196,40 @@ class Host(unittest.TestCase):
self.assertTrue(record["src_sha256"])
def test_final_source_recheck_completes_or_invalidates_receipt(self):
import json
import tempfile
import run_jet_circle_cv as v
recorded = dict(source_mode="current_checkout", checkout_revision="a" * 40,
checkout_dirty=False, src_tree="b" * 40)
payload = dict(status="PILOT_DIAGNOSTIC_ONLY", run_status="INCOMPLETE", windows=[1, 2])
with tempfile.TemporaryDirectory() as d:
path = Path(d) / "re100_s1.json"
path.write_text(json.dumps(payload))
done = v.finish_receipt(path, payload, recorded, lambda: dict(recorded))
self.assertEqual(json.loads(path.read_text()), done)
self.assertEqual((done["run_status"], done["source_recheck"]), ("COMPLETE", "UNCHANGED"))
for recheck, after in ((lambda: dict(recorded, checkout_dirty=True), True),
(lambda: v.verify(Path(d)), None)):
path.write_text(json.dumps(payload))
with self.assertRaisesRegex(RuntimeError, "Source changed during the run"):
v.finish_receipt(path, payload, recorded, recheck)
bad = json.loads(path.read_text())
self.assertEqual((bad["run_status"], bad["source_recheck"]),
("INVALID_SOURCE_CHANGED", "CHANGED"))
self.assertEqual(bad["source_after"] is None, after is None)
self.assertEqual(bad["windows"], [1, 2])
self.assertFalse(Path(str(path) + ".tmp").exists())
def test_receipts_written_during_the_run_are_incomplete(self):
"""Static check (run_case needs CUDA): the in-run payload is INCOMPLETE and only
main() -> finish_receipt can mark it COMPLETE."""
import inspect
import run_jet_circle_cv as v
self.assertIn('run_status="INCOMPLETE"', inspect.getsource(v.run_case))
self.assertNotIn("COMPLETE\"", inspect.getsource(v.run_case).replace("INCOMPLETE\"", ""))
self.assertIn("finish_receipt(", inspect.getsource(v.main))
if __name__ == "__main__":
unittest.main()
@@ -111,3 +111,20 @@ def test_historical_files_reports_unreadable_blob_as_history_unavailable(tmp_pat
with pytest.raises(HistoryUnavailable, match="blob not in this clone"):
source_provenance.historical_files(ROOT, ORIGIN, ["src/a.py"], tmp_path)
assert not (tmp_path / "src/a.py").exists()
def test_final_source_check_unchanged_changed_and_failed_recheck():
from source_provenance import final_source_check
recorded = dict(revision="a" * 40, dirty=False)
assert final_source_check(recorded, lambda: dict(recorded)) == dict(source_recheck="UNCHANGED")
changed = final_source_check(recorded, lambda: dict(recorded, dirty=True))
assert changed == dict(source_recheck="CHANGED", source_after=dict(recorded, dirty=True),
source_recheck_error=None)
def fails():
raise RuntimeError("Source hash mismatch: src/x.py")
failed = final_source_check(recorded, fails)
assert (failed["source_recheck"], failed["source_after"]) == ("CHANGED", None)
assert "Source hash mismatch" in failed["source_recheck_error"]
+36 -11
View File
@@ -244,6 +244,37 @@ def current_checkout(repo):
for f in files})
def source_identity(repo, current):
"""Recorded source of a run; recomputed after the run by finish_receipt."""
if current:
provenance = dict(**checkout_provenance(repo), **current_checkout(repo))
if provenance["checkout_dirty"]:
raise RuntimeError("Current-checkout runs require a clean checkout")
return provenance
source = verify(repo)
return dict(source_experiment=source["experiment"], source_mode="pinned",
**checkout_provenance(repo))
def finish_receipt(path, payload, provenance, recheck):
"""Mark the receipt COMPLETE only if the source is unchanged; otherwise INVALID.
Receipts written during the run carry run_status="INCOMPLETE", so an interrupted
run never leaves a complete-looking receipt.
"""
from source_provenance import final_source_check
fields = final_source_check(provenance, recheck)
unchanged = fields["source_recheck"] == "UNCHANGED"
payload = dict(payload, **fields,
run_status="COMPLETE" if unchanged else "INVALID_SOURCE_CHANGED")
temporary = Path(str(path) + ".tmp")
temporary.write_text(json.dumps(payload, indent=2) + "\n")
temporary.replace(path)
if not unchanged:
raise RuntimeError("Source changed during the run; receipt marked invalid")
return payload
def run_case(c, ctx, output, provenance, continuation=False):
from CelerisLab.body.geometry.circle import CircleGeometry
from CelerisLab.body.objects import SimObject
@@ -556,6 +587,7 @@ def run_case(c, ctx, output, provenance, continuation=False):
case=c,
**{"source_revision": PIN, **provenance},
status="PILOT_DIAGNOSTIC_ONLY",
run_status="INCOMPLETE", # final source recheck in main() completes it
windows=rows,
physics_validation_pass=False,
precision="FP32 evolution, FP64 host diagnostics",
@@ -638,16 +670,7 @@ def main():
):
p.error("Root must select one verified idle GPU via CUDA_VISIBLE_DEVICES")
repo = a.repo.resolve()
if a.current_checkout:
provenance = dict(**checkout_provenance(repo), **current_checkout(repo))
if provenance["checkout_dirty"]:
raise RuntimeError("Current-checkout runs require a clean checkout")
else:
source = verify(repo)
provenance = dict(
source_experiment=source["experiment"], source_mode="pinned",
**checkout_provenance(repo)
)
provenance = source_identity(repo, a.current_checkout)
sys.path.insert(0, str(repo / "src"))
import CelerisLab
@@ -663,7 +686,9 @@ def main():
free, total = cuda.mem_get_info()
if free < max(1024, 2 * plan["estimated_peak_device_mib"]) * 1024**2:
raise RuntimeError("Insufficient free device memory")
run_case(c, ctx, a.output, provenance, continuation=a.continuation)
payload = run_case(c, ctx, a.output, provenance, continuation=a.continuation)
finish_receipt(a.output / f"re{c['re_d']}_s{c['scale']}.json", payload, provenance,
lambda: source_identity(repo, a.current_checkout))
print(
json.dumps(
dict(status="COMPLETE_DIAGNOSTIC_ONLY", physics_validation_pass=False)
+17
View File
@@ -15,6 +15,23 @@ def checkout_provenance(repo):
return dict(checkout_revision=revision, checkout_dirty=bool(dirty))
def final_source_check(recorded, recheck):
"""Re-derive the source identity after a run and compare it with the recorded one.
Returns receipt fields: source_recheck "UNCHANGED", or "CHANGED" with source_after
(None if the recheck itself failed) and the recheck error. Callers mark a receipt
complete only for "UNCHANGED"; a changed source invalidates the run.
"""
try:
after = recheck()
except Exception as error: # a failed recheck cannot confirm the source
return dict(source_recheck="CHANGED", source_after=None,
source_recheck_error=f"{type(error).__name__}: {error}")
if after == recorded:
return dict(source_recheck="UNCHANGED")
return dict(source_recheck="CHANGED", source_after=after, source_recheck_error=None)
class HistoryUnavailable(RuntimeError):
"""The clone cannot show the recorded commit (no git, shallow clone, installed copy)."""
@@ -144,6 +144,10 @@ def assess(records, expected_revision=None, current=False):
checks[label + " reference"] = (r.get("numerical_revision") == NUMERICAL_REVISION
and "source_mode" not in r
and r.get("reference_harness") == REFERENCE_HARNESS)
# Final source recheck (run_case); receipts from before it existed carry no field
# and are accepted only in the frozen mode.
recheck = r.get("source_recheck") if current else r.get("source_recheck", "UNCHANGED")
checks[label + " source_recheck"] = recheck == "UNCHANGED" and "invalid" not in r
checks[label + " numerics"] = r.get("numerics") == NUMERICS and r.get("initialization") == INITIALIZATION
runtime = r.get("runtime_source", {})
package = Path(runtime.get("checkout", "")) / "src/CelerisLab"
+11
View File
@@ -319,6 +319,17 @@ def main():
),
flush=True,
)
from tests.validation.source_provenance import final_source_check
recheck = final_source_check(
dict(source=source, runtime_source=runtime),
lambda: dict(source=verify_source(repo, args.expected_revision,
current=args.current_checkout),
runtime_source=runtime_source(repo)))
report.update(recheck)
if recheck["source_recheck"] != "UNCHANGED":
report.update(invalid="SOURCE_CHANGED_DURING_RUN") # completed stays False
save()
raise RuntimeError("Source changed during the run; receipt marked invalid")
report.update(
completed=True,
seconds=time.monotonic() - started,
@@ -12,7 +12,7 @@ import sys
import tempfile
from types import SimpleNamespace
import unittest
from unittest.mock import patch
from unittest.mock import call, patch
import numpy as np
@@ -322,6 +322,7 @@ class ProjectedPairsTests(unittest.TestCase):
source_mode="current_checkout",
source_capture=run_case.CURRENT_CAPTURE)
r["numerical_revision"], r["source_mode"] = None, "current_checkout"
r["source_recheck"] = "UNCHANGED"
ok = assess(current, MOCK_REVISION, current=True)
self.assertTrue(ok["checks"]["clean_current_checkout_source"])
self.assertTrue(all(v for k, v in ok["checks"].items() if k.endswith(" reference")))
@@ -333,6 +334,23 @@ class ProjectedPairsTests(unittest.TestCase):
self.assertFalse(assess(current, None, current=True)["checks"]["clean_current_checkout_source"])
self.assertTrue(assess(frozen, MOCK_REVISION)["checks"]["clean_a135252_numerical_source"])
def test_assessor_requires_unchanged_final_source(self):
frozen = records()
label = lambda r: f"{r['n']}/{r['depth']}/{r['explosion']} source_recheck"
self.assertTrue(all(assess(frozen, MOCK_REVISION)["checks"][label(r)] for r in frozen))
for mutate in (dict(source_recheck="CHANGED"), dict(invalid="SOURCE_CHANGED_DURING_RUN")):
bad = deepcopy(frozen)
bad[0].update(mutate)
out = assess(bad, MOCK_REVISION)
self.assertFalse(out["checks"][label(bad[0])])
self.assertFalse(out["passed"])
current = deepcopy(frozen)
for r in current:
r["source"] = dict(r["source"], source_mode="current_checkout")
r["numerical_revision"], r["source_mode"] = None, "current_checkout"
# Current-checkout receipts are new: a missing recheck is not accepted.
self.assertFalse(assess(current, MOCK_REVISION, current=True)["checks"][label(current[0])])
def test_explicit_opt_in_selector_and_dry_run_without_cuda(self):
with tempfile.TemporaryDirectory() as tmp:
output = Path(tmp) / "case.json"
@@ -393,7 +411,8 @@ class ProjectedPairsTests(unittest.TestCase):
# Replace the GPU context and steppers before import. No CUDA module or flow is used.
for early, depth, fail in ((False, 1, None), (False, 2, None), (True, 2, None),
(True, 2, "resource"), (True, 2, "nonfinite"),
(True, 2, "probe_nonfinite")):
(True, 2, "probe_nonfinite"),
(False, 2, "changed"), (False, 1, "recheck_error")):
calls, selectors = [], []
class FakeRun:
@@ -448,13 +467,29 @@ class ProjectedPairsTests(unittest.TestCase):
patch.object(run_case, "runtime_identity", return_value=deepcopy(RUNTIME)), \
patch.object(run_case, "build_capture", return_value=nullcontext()), \
patch.object(run_case, "metrics", side_effect=metric), redirect_stdout(io.StringIO()):
if fail:
if fail in ("changed", "recheck_error"):
verify.side_effect = [source(), dict(source(), source_tree_sha256="d" * 64)
if fail == "changed" else RuntimeError("Wrong numerical source fingerprint")]
with self.assertRaisesRegex(RuntimeError, "Source changed during the run"):
run_case.main()
elif fail:
with self.assertRaises(MemoryError if fail == "resource" else FloatingPointError):
run_case.main()
else:
self.assertEqual(run_case.main(), 0)
receipt = json.loads(output.read_text())
verify.assert_called_once_with(REPO, MOCK_REVISION, current=False)
rechecked = fail in (None, "changed", "recheck_error")
self.assertEqual(verify.call_args_list,
[call(REPO, MOCK_REVISION, current=False)] * (2 if rechecked else 1))
if fail in ("changed", "recheck_error"):
# Full trajectory, but invalid: never a complete-looking receipt.
self.assertEqual(receipt["source_recheck"], "CHANGED")
self.assertEqual(receipt["invalid"], "SOURCE_CHANGED_DURING_RUN")
self.assertFalse(receipt["completed"])
self.assertNotIn("screening_pass", receipt)
self.assertEqual(receipt["source_after"] is None, fail == "recheck_error")
continue
self.assertEqual(receipt.get("source_recheck"), None if fail else "UNCHANGED")
self.assertEqual(selectors, ["projected_linear" if early else "constant"] if depth == 2 else [])
self.assertEqual(receipt["source"], source())
self.assertEqual(receipt["runtime_identity"], RUNTIME)