@@ -83,7 +83,7 @@ def test_cli_successor_refresh_resets_periodic_window(tmp_path: Path, capsys) ->
8383 [* reversed (new_runs ), compact , * runs ], agent_todos = {}, agent_id = AGENT ) is not None
8484
8585
86- @pytest .mark .parametrize ("guard" , [None , "replan-different " ])
86+ @pytest .mark .parametrize ("guard" , [None , "replan-0000000000000000 " ])
8787def test_transition_cannot_settle_a_different_turn_guard (guard ) -> None :
8888 runs = history ()
8989 obligation = autonomous_replan_obligation_from_runs (runs , agent_todos = {}, agent_id = AGENT )
@@ -106,6 +106,145 @@ def test_transition_settles_only_exact_guard_and_owner() -> None:
106106 state_text = successor_state (obligation ["obligation_id" ], owner = "another-agent" ), ** kwargs )
107107
108108
109+ @pytest .mark .parametrize ("invalid" , ["done" , "deferred" , "missing_target" ])
110+ def test_non_runnable_successor_cannot_settle_original_guard (invalid : str ) -> None :
111+ runs = history ()
112+ obligation = autonomous_replan_obligation_from_runs (runs , agent_todos = {}, agent_id = AGENT )
113+ state = successor_state (obligation ["obligation_id" ])
114+ state = (state .replace ("target_key=source-audit " , "" ) if invalid == "missing_target"
115+ else state .replace ("status=open" , f"status={ invalid } " ))
116+ with pytest .raises (ReplanWritebackRejected ):
117+ enforce_open_replan_writeback (newest_first_runs = runs , state_text = state ,
118+ agent_id = AGENT , goal_id = GOAL , guard_scoped = True ,
119+ guard_semantic_replan_obligation_id = obligation ["obligation_id" ])
120+
121+
122+ @pytest .mark .parametrize ("provider" , ["file" , "sqlite" ])
123+ def test_canonical_periodic_successor_settles_original_open_validation_todo (
124+ tmp_path : Path , monkeypatch : pytest .MonkeyPatch , provider : str ,
125+ ) -> None :
126+ """A new task changes the path, not the original task's completion."""
127+ import subprocess
128+ import sys
129+
130+ from canonical_authority_fixture import (
131+ initialize_canonical_authority , isolate_sqlite_runtime ,
132+ )
133+ from loopx .control_plane .coordination .runtime_shadow import build_todo_runtime_shadow_projection
134+ from loopx .control_plane .goals .goal_vision import compact_goal_vision_packet , normalize_goal_vision_packet
135+ from loopx .control_plane .todos .active_state_todo_parser import parse_active_state_todos
136+
137+ if provider == "sqlite" :
138+ isolate_sqlite_runtime (tmp_path , monkeypatch )
139+ project = tmp_path / "project"
140+ project .mkdir ()
141+ state = project / "ACTIVE_GOAL_STATE.md"
142+ original_todo = "todo_original_validation"
143+ state .write_text (
144+ "---\n status: active\n ---\n \n # Goal\n \n ## Agent Todo\n \n "
145+ "- [ ] [P1] Validate the original artifact.\n "
146+ f" <!-- loopx:todo todo_id={ original_todo } status=open "
147+ f"task_class=advancement_task claimed_by={ AGENT } action_kind=validate "
148+ "validation_command=pytest -->\n "
149+ )
150+ runtime = tmp_path / "runtime"
151+ index = runtime / "goals" / GOAL / "runs" / "index.jsonl"
152+ index .parent .mkdir (parents = True )
153+ runs = history ()
154+ baseline_json = index .parent / "synthetic-baseline.json"
155+ baseline_markdown = index .parent / "synthetic-baseline.md"
156+ baseline_json .write_text (json .dumps ({"ok" : True , "fixture" : "synthetic-replan" }))
157+ baseline_markdown .write_text ("# Synthetic prior delivery\n " )
158+ for row in runs :
159+ row .update (json_path = str (baseline_json ), markdown_path = str (baseline_markdown ))
160+ runs [0 ]["agent_vision" ] = compact_goal_vision_packet (normalize_goal_vision_packet ({
161+ "goal_id" : GOAL , "agent_id" : AGENT , "state" : "vision_drift_detected" ,
162+ "todo_delta" : [f"retain:{ original_todo } " ],
163+ "vision_patch" : {
164+ "acceptance_summary" : "Independently validate the source artifact." ,
165+ "replan_trigger_summary" : "The source acceptance remains open." ,
166+ "advancement_policy" : "repeat_until_closed" ,
167+ },
168+ }, goal_id = GOAL , agent_id = AGENT ))
169+ runs [0 ]["vision_checkpoint" ] = {
170+ "agent_id" : AGENT , "required" : True , "satisfied" : False ,
171+ "triggers" : [{"kind" : "material_delivery_outcome" , "delivery_outcome" : "outcome_progress" }],
172+ }
173+ index .write_text ("" .join (json .dumps (row ) + "\n " for row in reversed (runs )))
174+ registry = tmp_path / "registry.json"
175+ registry .write_text (json .dumps ({"common_runtime_root" : str (runtime ), "goals" : [{
176+ "id" : GOAL , "status" : "active" , "repo" : str (project ), "state_file" : state .name ,
177+ "domain" : "synthetic-replan" ,
178+ "adapter" : {"kind" : "fixture_connected_delivery_v0" , "status" : "connected-delivery" },
179+ "quota" : {"compute" : 1.0 , "window_hours" : 24 },
180+ "coordination" : {"agent_model" : "peer_v1" , "registered_agents" : [AGENT ]},
181+ }]}))
182+ todos = parse_active_state_todos (state .read_text (), item_limit = None )["agent_todos" ]["items" ]
183+ initialize_canonical_authority (runtime , GOAL , build_todo_runtime_shadow_projection (
184+ goal_id = GOAL , todos = todos , handoff_mode = "soft_claim" , leases = [],
185+ ), state_path = state , provider = provider )
186+
187+ def call (* args : str , expected_error : str | None = None ) -> dict :
188+ result = subprocess .run ([sys .executable , "-m" , "loopx.cli" , "--registry" , str (registry ),
189+ "--runtime-root" , str (runtime ), "--format" , "json" , * args ], cwd = project ,
190+ capture_output = True , text = True , timeout = 60 )
191+ payload = json .loads (result .stdout )
192+ if expected_error is not None :
193+ assert result .returncode == 1 , payload
194+ assert expected_error in payload ["error" ], payload
195+ else :
196+ assert result .returncode == 0 , (payload .get ("error" ), payload .get ("reason" ), payload .get ("status" ))
197+ return payload
198+
199+ binding = ["--goal-id" , GOAL , "--agent-id" , AGENT , "--todo-id" , original_todo ,
200+ "--turn-instance-id" , "turn-original-periodic-review" ]
201+ guard = call ("quota" , "should-run" , "--codex-app" , "--goal-id" , GOAL ,
202+ "--agent-id" , AGENT , "--turn-instance-id" , "turn-original-periodic-review" )
203+ assert guard ["selected_todo" ]["todo_id" ] == original_todo
204+ obligation = guard ["autonomous_replan_obligation" ]
205+ added = call ("todo" , "add" , "--goal-id" , GOAL , "--role" , "agent" , "--claimed-by" , AGENT ,
206+ "--text" , "Verify an independent source artifact" ,
207+ "--task-class" , "advancement_task" , "--action-kind" , "validate" ,
208+ "--target-key" , "independent-source-artifact" , "--operation-id" , "periodic-successor" ,
209+ "--replan-obligation-id" , obligation ["obligation_id" ])
210+ assert added ["replan_transition" ]["recorded" ] is True
211+ refresh_args = ("refresh-state" , * binding , "--classification" , "bounded_replan_progress" ,
212+ "--delivery-batch-scale" , "single_surface" , "--delivery-outcome" , "outcome_progress" ,
213+ "--vision-unchanged-reason" , "The original validation remains open; the independent successor changes the path." ,
214+ "--no-global-sync" , "--suppress-external-sinks" )
215+ refreshed = call (* refresh_args )
216+ persisted = json .loads (Path (refreshed ["json_path" ]).read_text ())
217+ delta = persisted ["autonomous_replan_ack" ]["semantic_delta" ]
218+ assert delta ["obligation_id" ] == obligation ["obligation_id" ]
219+ assert delta ["successor_todo_id" ] == added ["todo_id" ]
220+ spend_args = ("quota" , "spend-slot" , * binding , "--slots" , "1" ,
221+ "--source" , "heartbeat" , "--execute" )
222+ spent = call (* spend_args )
223+ assert spent ["appended" ] is True
224+ assert spent ["settlement_progress" ]["state" ] == "settled"
225+ assert call (* refresh_args )["appended" ] is False
226+ assert call (* spend_args )["appended" ] is False
227+ settlement_runs = [json .loads (line ) for line in index .read_text ().splitlines ()
228+ if json .loads (line ).get ("turn_instance_id" ) == "turn-original-periodic-review" ]
229+ assert len (settlement_runs ) == 2
230+ assert sum (row .get ("classification" ) == "quota_slot_spent" for row in settlement_runs ) == 1
231+ original = call ("todo" , "list" , "--goal-id" , GOAL , "--todo-id" , original_todo )["todo" ]
232+ assert original ["status" ] == "open"
233+ # Review settlement does not establish Vision acceptance or Todo completion.
234+ next_guard = call ("quota" , "should-run" , "--codex-app" , "--goal-id" , GOAL ,
235+ "--agent-id" , AGENT , "--turn-instance-id" , "turn-next-vision-review" )
236+ frontier = next_guard ["goal_frontier_projection" ]
237+ assert "vision_outcome_checkpoint_required" in [gap ["kind" ] for gap in frontier ["acceptance_gaps" ]]
238+ assert frontier ["vision_continuation_audit" ]["decision" ] == "acceptance_gap_open"
239+ call ("quota" , "should-run" , "--codex-app" , "--goal-id" , GOAL ,
240+ "--agent-id" , AGENT , "--todo-id" , original_todo ,
241+ "--turn-instance-id" , "turn-next-vision-review" )
242+ call ("refresh-state" , "--goal-id" , GOAL , "--agent-id" , AGENT , "--todo-id" , original_todo ,
243+ "--turn-instance-id" , "turn-next-vision-review" , "--classification" , "evidence_validated" ,
244+ "--delivery-outcome" , "outcome_progress" , "--no-global-sync" , "--suppress-external-sinks" ,
245+ expected_error = "controller-declared completion validation" )
246+
247+
109248@pytest .mark .parametrize ("route" , ["writeback" , "successor" , "canonical-successor" ])
110249def test_long_chain_ack_survives_real_cli_history_and_peer_claim (tmp_path : Path , route : str ) -> None :
111250 import subprocess
0 commit comments