test: gate V1 side files and V3 legacy receipts; early output rejection
- analyze_jet_cv_continuation: samples are analyzed only if the sibling re*_s*.json receipt is the same case with run_status COMPLETE and source_recheck UNCHANGED; receipts without run_status (pre-recheck runners) are refused unless --legacy-receipts, and labelled LEGACY_NO_FINAL_SOURCE_RECHECK. Results carry receipt_gate. - V3: receipts are schema 2 and need source_recheck UNCHANGED; a missing recheck is accepted only for frozen-mode schema 1 receipts whose run_case.py hash is one of the pre-recheck runners (9805383,2cf9853, db3d098; hashes checked against git history in a test). - V1/V3 refuse --output inside the checkout before source checks or CUDA (shared source_provenance.reject_output_inside). - A failing recheck is recorded as RECHECK_FAILED (still invalid); the in-run V1 receipt write is tmp+replace. Negative host tests for each gate. No threshold, solver or evidence edits. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
df0f586331
commit
36ffc8b1b2
9 files changed
+185
-33
No files matched your search
@@ -209,15 +209,15 @@ class Host(unittest.TestCase):
|
||||
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)):
|
||||
for recheck, state in ((lambda: dict(recorded, checkout_dirty=True), "CHANGED"),
|
||||
(lambda: v.verify(Path(d)), "RECHECK_FAILED")):
|
||||
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)
|
||||
("INVALID_SOURCE_CHANGED", state))
|
||||
self.assertEqual(bad["source_after"] is None, state == "RECHECK_FAILED")
|
||||
self.assertEqual(bad["windows"], [1, 2])
|
||||
self.assertFalse(Path(str(path) + ".tmp").exists())
|
||||
|
||||
@@ -231,5 +231,16 @@ class Host(unittest.TestCase):
|
||||
self.assertIn("finish_receipt(", inspect.getsource(v.main))
|
||||
|
||||
|
||||
def test_output_inside_checkout_is_refused_before_source_or_cuda(self):
|
||||
from unittest import mock
|
||||
import run_jet_circle_cv as v
|
||||
argv = ["run_jet_circle_cv.py", "--repo", str(REPO), "--output", str(REPO / "v1-out")]
|
||||
with mock.patch("sys.argv", argv), mock.patch.dict("os.environ", CUDA_VISIBLE_DEVICES="0"), \
|
||||
mock.patch.object(v, "source_identity", side_effect=AssertionError("work started")):
|
||||
with self.assertRaisesRegex(SystemExit, "outside the source checkout"):
|
||||
v.main()
|
||||
self.assertFalse((REPO / "v1-out").exists())
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -151,6 +151,8 @@ class Measurements(unittest.TestCase):
|
||||
(path / "samples_s2.json").write_text(
|
||||
json.dumps(dict(case=c, sample_file="samples_s2.npz"))
|
||||
)
|
||||
(path / f"re{c['re_d']}_s2.json").write_text(json.dumps(
|
||||
dict(case=c, run_status="COMPLETE", source_recheck="UNCHANGED")))
|
||||
np.savez(
|
||||
path / "samples_s2.npz",
|
||||
t_reference=t,
|
||||
@@ -163,6 +165,7 @@ class Measurements(unittest.TestCase):
|
||||
velocity_error=np.zeros(n),
|
||||
)
|
||||
result = analyze(path)
|
||||
self.assertEqual(result["receipt_gate"], "COMPLETE_SOURCE_UNCHANGED")
|
||||
self.assertEqual(len(result["windows"]), 32)
|
||||
np.testing.assert_allclose(
|
||||
result["windows"][0]["normalized_cv_residual"], 0, atol=1e-14
|
||||
@@ -178,5 +181,36 @@ class Measurements(unittest.TestCase):
|
||||
np.testing.assert_allclose(integrate(t, y), [112, 8], atol=1e-15)
|
||||
|
||||
|
||||
def test_receipt_gate_refuses_incomplete_invalid_and_unmarked_legacy(self):
|
||||
import json
|
||||
from analyze_jet_cv_continuation import receipt_gate
|
||||
|
||||
c = case(2, steps=3200)
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
path = Path(tmp)
|
||||
receipt = path / f"re{c['re_d']}_s2.json"
|
||||
with self.assertRaisesRegex(ValueError, "No sibling run receipt"):
|
||||
receipt_gate(path, c)
|
||||
for fields, message in (
|
||||
(dict(run_status="INCOMPLETE"), "INCOMPLETE"),
|
||||
(dict(run_status="INVALID_SOURCE_CHANGED", source_recheck="CHANGED"), "INVALID"),
|
||||
(dict(run_status="COMPLETE", source_recheck="RECHECK_FAILED"), "RECHECK_FAILED"),
|
||||
(dict(run_status="COMPLETE"), "source_recheck=None"),
|
||||
(dict(), "Legacy run receipt"),
|
||||
):
|
||||
receipt.write_text(json.dumps(dict(case=c, **fields)))
|
||||
with self.assertRaisesRegex(ValueError, message):
|
||||
receipt_gate(path, c)
|
||||
receipt.write_text(json.dumps(dict(case=dict(c, scale=1), run_status="COMPLETE",
|
||||
source_recheck="UNCHANGED")))
|
||||
with self.assertRaisesRegex(ValueError, "different case"):
|
||||
receipt_gate(path, c)
|
||||
receipt.write_text(json.dumps(dict(case=c)))
|
||||
self.assertEqual(receipt_gate(path, c, legacy=True), "LEGACY_NO_FINAL_SOURCE_RECHECK")
|
||||
receipt.write_text(json.dumps(dict(case=c, run_status="INCOMPLETE")))
|
||||
with self.assertRaisesRegex(ValueError, "INCOMPLETE"):
|
||||
receipt_gate(path, c, legacy=True) # legacy opt-in never admits new statuses
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -126,5 +126,14 @@ def test_final_source_check_unchanged_changed_and_failed_recheck():
|
||||
raise RuntimeError("Source hash mismatch: src/x.py")
|
||||
|
||||
failed = final_source_check(recorded, fails)
|
||||
assert (failed["source_recheck"], failed["source_after"]) == ("CHANGED", None)
|
||||
assert (failed["source_recheck"], failed["source_after"]) == ("RECHECK_FAILED", None)
|
||||
assert "Source hash mismatch" in failed["source_recheck_error"]
|
||||
|
||||
|
||||
def test_reject_output_inside_checkout(tmp_path):
|
||||
from source_provenance import reject_output_inside
|
||||
|
||||
for output in (ROOT, ROOT / "out", ROOT / "src" / "x.json"):
|
||||
with pytest.raises(SystemExit, match="outside the source checkout"):
|
||||
reject_output_inside(output, ROOT)
|
||||
reject_output_inside(tmp_path / "out", ROOT)
|
||||
Reference in new issue
Block a user