@@ -28,6 +28,83 @@ def _roster() -> Roster:
2828 )
2929
3030
31+ def test_cell_identity_payload_is_locked ():
32+ # Snapshot of the frozen identity contract (docs/phase-eval-cell-identity.md).
33+ # Any change to the field set below must come with a CELL_SCHEMA bump.
34+ manifest = {
35+ "schema" : "brigade.eval_manifest.v1" ,
36+ "name" : "identity-lock" ,
37+ "trials" : 1 ,
38+ "seats" : ["cursor" ],
39+ "cases" : [{"id" : "hello" , "prompt" : "Say hello" }],
40+ "graders" : [{"type" : "exact_output" , "expected" : "hello" }],
41+ }
42+ cell = model_trials .expand_cells (manifest , _roster ())[0 ]
43+ expected_identity = {
44+ "schema" : "brigade.eval_cell.v1" ,
45+ "case" : {"id" : "hello" , "prompt" : "Say hello" },
46+ "seat" : {
47+ "seat" : "cursor" ,
48+ "cli" : "cursor" ,
49+ "model" : "composer-2.5" ,
50+ "reasoning" : None ,
51+ "transport" : "direct" ,
52+ "transport_version" : None ,
53+ "env" : None ,
54+ "codex_transport" : None ,
55+ },
56+ "trial" : 1 ,
57+ "graders" : [{"type" : "exact_output" , "expected" : "hello" }],
58+ "execution_mode" : "read-only" ,
59+ }
60+ assert model_trials ._canonical_digest (expected_identity ) == cell .cell_id
61+ assert cell .cell_id == "55c07e87f401b5aa49f956b2cec1bfee87986088702bc0b1762cc722ff2638c1"
62+
63+
64+ def test_prompt_line_endings_do_not_change_identity ():
65+ crlf = _manifest ()
66+ crlf ["cases" ][0 ]["prompt" ] = "Say hello\r \n again\r now"
67+ unix = _manifest ()
68+ unix ["cases" ][0 ]["prompt" ] = "Say hello\n again\n now"
69+ crlf_cells = model_trials .expand_cells (crlf , _roster ())
70+ unix_cells = model_trials .expand_cells (unix , _roster ())
71+ assert [cell .cell_id for cell in crlf_cells ] == [cell .cell_id for cell in unix_cells ]
72+ assert crlf_cells [0 ].prompt == "Say hello\n again\n now"
73+
74+
75+ def test_attempt_number_uses_max_plus_one_and_tolerates_gaps (tmp_path ):
76+ assert model_trials ._attempt_number (tmp_path ) == 1
77+ attempts = tmp_path / "attempts"
78+ attempts .mkdir ()
79+ (attempts / "attempt-001" ).mkdir ()
80+ (attempts / "attempt-003" ).mkdir ()
81+ (attempts / "scratch" ).mkdir ()
82+ (attempts / "attempt-002-partial" ).mkdir ()
83+ assert model_trials ._attempt_number (tmp_path ) == 4
84+
85+
86+ def test_attempt_number_does_not_reuse_deleted_highest_attempt (tmp_path ):
87+ attempts = tmp_path / "attempts"
88+ attempts .mkdir ()
89+ (attempts / "attempt-001" ).mkdir ()
90+ (tmp_path / "cell.json" ).write_text (json .dumps ({"attempt" : 3 }))
91+ assert model_trials ._attempt_number (tmp_path ) == 4
92+
93+
94+ def test_attempt_number_counts_running_marker_without_attempt_dir (tmp_path ):
95+ # Crash window: cell.json was written as running before attempt-001 existed.
96+ (tmp_path / "cell.json" ).write_text (json .dumps ({"state" : "running" , "attempt" : 1 }))
97+ assert model_trials ._attempt_number (tmp_path ) == 2
98+
99+
100+ def test_attempt_number_ignores_nonpositive_recorded_attempt (tmp_path ):
101+ # Corrupt-but-valid-JSON markers must not produce attempt-000.
102+ (tmp_path / "cell.json" ).write_text (json .dumps ({"state" : "running" , "attempt" : - 1 }))
103+ assert model_trials ._attempt_number (tmp_path ) == 1
104+ (tmp_path / "cell.json" ).write_text (json .dumps ({"state" : "running" , "attempt" : 0 }))
105+ assert model_trials ._attempt_number (tmp_path ) == 1
106+
107+
31108def test_expand_cells_is_stable_and_conditions_change_identity ():
32109 first = model_trials .expand_cells (_manifest (), _roster ())
33110 second = model_trials .expand_cells (_manifest (), _roster ())
@@ -228,6 +305,92 @@ def fake_run(task, roster, **kwargs):
228305 assert summary ["stale_counts" ] == {"accepted" : 2 }
229306
230307
308+ def test_resume_after_manifest_edit_reruns_only_changed_cells (tmp_path , monkeypatch , capsys ):
309+ manifest = _manifest ()
310+ manifest ["trials" ] = 1
311+ manifest ["cases" ] = [
312+ {"id" : "alpha" , "prompt" : "Say alpha" },
313+ {"id" : "beta" , "prompt" : "Say beta" },
314+ ]
315+ manifest_path = tmp_path / "eval.json"
316+ manifest_path .write_text (json .dumps (manifest ))
317+ tasks : list [str ] = []
318+
319+ def fake_run (task , roster , ** kwargs ):
320+ tasks .append (task )
321+ out = kwargs ["output_dir" ]
322+ out .mkdir (parents = True , exist_ok = True )
323+ (out / "final.txt" ).write_text ("hello\n " )
324+ (out / "run.json" ).write_text (json .dumps ({"status" : "ok" , "duration_seconds" : 0.5 }))
325+ return 0
326+
327+ monkeypatch .setattr (model_trials .aboyeur , "run" , fake_run )
328+ root = tmp_path / "results"
329+ assert model_trials .execute (manifest_path , _roster (), workspace = tmp_path , output_dir = root , resume = False ) == 0
330+ assert sorted (tasks ) == ["Say alpha" , "Say beta" ]
331+
332+ original_ids = {cell .case_id : cell .cell_id for cell in model_trials .expand_cells (manifest , _roster ())}
333+ alpha_path = root / "cells" / original_ids ["alpha" ] / "cell.json"
334+
335+ edited = json .loads (manifest_path .read_text ())
336+ edited ["cases" ][1 ]["prompt" ] = "Say beta differently"
337+ manifest_path .write_text (json .dumps (edited ))
338+ tasks .clear ()
339+ capsys .readouterr ()
340+ assert model_trials .execute (manifest_path , _roster (), workspace = tmp_path , output_dir = root , resume = True ) == 0
341+
342+ # The unchanged cell is skipped; the edited cell re-runs under a new cell_id.
343+ assert tasks == ["Say beta differently" ]
344+ alpha = json .loads (alpha_path .read_text ())
345+ assert alpha ["state" ] == "accepted"
346+ assert alpha ["attempt" ] == 1
347+ edited_ids = {cell .case_id : cell .cell_id for cell in model_trials .expand_cells (edited , _roster ())}
348+ assert edited_ids ["alpha" ] == original_ids ["alpha" ]
349+ assert edited_ids ["beta" ] != original_ids ["beta" ]
350+ new_beta = json .loads ((root / "cells" / edited_ids ["beta" ] / "cell.json" ).read_text ())
351+ assert new_beta ["state" ] == "accepted"
352+ plan = json .loads ((root / "plan.json" ).read_text ())
353+ assert new_beta ["manifest_digest" ] == plan ["manifest_digest" ]
354+
355+ # The old cell is kept and reported, not pruned; resume warns on stderr.
356+ assert (root / "cells" / original_ids ["beta" ] / "cell.json" ).is_file ()
357+ summary = json .loads ((root / "summary.json" ).read_text ())
358+ assert summary ["counts" ] == {"accepted" : 2 }
359+ assert summary ["stale_counts" ] == {"accepted" : 1 }
360+ assert "1 stale cell(s)" in capsys .readouterr ().err
361+
362+
363+ def test_resume_reruns_killed_running_cell_as_new_attempt (tmp_path , monkeypatch ):
364+ manifest = _manifest ()
365+ manifest ["trials" ] = 1
366+ manifest_path = tmp_path / "eval.json"
367+ manifest_path .write_text (json .dumps (manifest ))
368+
369+ def fake_run (task , roster , ** kwargs ):
370+ out = kwargs ["output_dir" ]
371+ out .mkdir (parents = True , exist_ok = True )
372+ (out / "final.txt" ).write_text ("hello\n " )
373+ (out / "run.json" ).write_text (json .dumps ({"status" : "ok" , "duration_seconds" : 0.5 }))
374+ return 0
375+
376+ monkeypatch .setattr (model_trials .aboyeur , "run" , fake_run )
377+ root = tmp_path / "results"
378+ assert model_trials .execute (manifest_path , _roster (), workspace = tmp_path , output_dir = root , resume = False ) == 0
379+ cell_path = next ((root / "cells" ).glob ("*/cell.json" ))
380+
381+ # Simulate a kill mid-run: the last durable state is "running".
382+ killed = json .loads (cell_path .read_text ())
383+ killed ["state" ] = "running"
384+ cell_path .write_text (json .dumps (killed ))
385+
386+ assert model_trials .execute (manifest_path , _roster (), workspace = tmp_path , output_dir = root , resume = True ) == 0
387+ final = json .loads (cell_path .read_text ())
388+ assert final ["state" ] == "accepted"
389+ assert final ["attempt" ] == 2
390+ attempts = sorted (p .name for p in (cell_path .parent / "attempts" ).iterdir ())
391+ assert attempts == ["attempt-001" , "attempt-002" ]
392+
393+
231394def test_grader_envelope_links_digested_output (tmp_path , monkeypatch ):
232395 manifest_path = tmp_path / "eval.json"
233396 manifest_path .write_text (json .dumps (_manifest ()))
0 commit comments