@@ -478,3 +478,120 @@ def test_capture_free_chat_preserves_pre_promotion_receipt(tmp_path, monkeypatch
478478 assert replay ["lane_todo_ids" ] == applied ["receipt" ]["resource_ids" ]["lane_todo_ids" ]
479479 assert read_canonical_todos_if_promoted (runtime_root = runtime , goal_id = "goal-a" ) == current
480480 assert state .read_bytes () == display
481+
482+ # Simulate promotion finishing after the first mode observation but before
483+ # acquiring source locks; its selected backend subsequently goes offline.
484+ from loopx .control_plane .coordination .local_authority import LocalCoordinationAuthorityUnavailable
485+ from loopx .control_plane .work_items import team_plan_adapter
486+ read_authority = team_plan_adapter .read_canonical_todos_if_promoted
487+ observed = []
488+
489+ def promotion_race (** kwargs ):
490+ observed .append (True )
491+ return None if len (observed ) == 1 else read_authority (** kwargs )
492+
493+ backend = runtime / "authority" / current ["source_authority" ].replace ("_" , "-" )
494+ offline = backend .with_name (backend .name + "-offline" )
495+ backend .rename (offline )
496+ try :
497+ with monkeypatch .context () as race :
498+ race .setattr (team_plan_adapter , "read_canonical_todos_if_promoted" , promotion_race )
499+ saved = service .store .load (preview ["proposal_id" ])
500+ with pytest .raises (LocalCoordinationAuthorityUnavailable ):
501+ team_plan_adapter .settle_team_plan (
502+ registry_path = registry , goal_id = "goal-a" , agent_id = None ,
503+ proposal = {** saved ["normalized_parameters" ]["plan" ], "proposal_id" : preview ["proposal_id" ]},
504+ expected_state_fingerprint = saved ["expected_state_fingerprint" ], basis_agent_id = "alpha" )
505+ assert len (observed ) == 2
506+ assert state .read_bytes () == display and not backend .exists ()
507+ finally :
508+ offline .rename (backend )
509+ assert read_canonical_todos_if_promoted (runtime_root = runtime , goal_id = "goal-a" ) == current
510+
511+
512+ @pytest .mark .skipif (os .name != "posix" , reason = "real bootstrap crash barrier uses select on a pipe" )
513+ def test_legacy_chat_replay_survives_interrupted_shadow_bootstrap (tmp_path , monkeypatch ):
514+ """A stopped writer must not strand an already-published plan receipt."""
515+ import select
516+ from loopx .control_plane .work_items import team_plan_adapter
517+ from loopx .control_plane .coordination .runtime_shadow import build_runtime_shadow_source_snapshot
518+ from loopx .control_plane .coordination .shadow_management import read_shadow_management_state
519+ from loopx .control_plane .coordination .coordination_state_contract_generated import (
520+ COORDINATION_RUNTIME_SHADOW_BOOTSTRAP_REQUEST_SCHEMA ,
521+ )
522+
523+ runtime = tmp_path / "runtime"
524+ state = tmp_path / "state.md"
525+ state .write_text ("# Goal\n \n ## Agent Todo\n \n " )
526+ goal = {"id" : "goal-a" , "repo" : str (tmp_path ), "state_file" : "state.md" ,
527+ "coordination" : {"registered_agents" : ["alpha" ], "agent_model" : "peer_v1" }}
528+ registry = tmp_path / "registry.json"
529+ registry .write_text (json .dumps ({"common_runtime_root" : str (runtime ), "goals" : [goal ]}))
530+ service = ChatActionService (store = ChatActionStore (runtime / "chat/actions" ), registry_path = registry )
531+ preview = service .preview ({"action_kind" : "team.plan" , "summary" : "Assign work" , "context" : {},
532+ "normalized_parameters" : {"goal_id" : "goal-a" , "plan" : _agent_plan ()},
533+ "idempotency_key" : "lost-original-response" })
534+ write = team_plan_adapter .write_captured_todo_state
535+
536+ def lose_response (* args , ** kwargs ):
537+ write (* args , ** kwargs )
538+ raise OSError ("response lost after publication" )
539+
540+ with monkeypatch .context () as failed_response :
541+ failed_response .setattr (team_plan_adapter , "write_captured_todo_state" , lose_response )
542+ assert service .apply (preview ["proposal_id" ])["proposal" ]["status" ] == "failed"
543+ assert state .read_text ().count ("loopx:team-plan:" ) == 1
544+ state .write_text (state .read_text () + "\n Later owner data must survive.\n " )
545+ original = state .read_bytes ()
546+ projection , snapshot = build_runtime_shadow_source_snapshot (
547+ goal = goal , runtime_root = runtime , state_path = state , registry_path = registry )
548+ request = {"schema_version" : COORDINATION_RUNTIME_SHADOW_BOOTSTRAP_REQUEST_SCHEMA ,
549+ "runtime_root" : str (runtime ), "goal_id" : "goal-a" , "operation_id" : "bootstrap:plan-recovery" ,
550+ "source_version" : "source:before-crash" , "projection" : projection , "source_snapshot" : snapshot }
551+ repo = Path (__file__ ).resolve ().parents [2 ]
552+ child = subprocess .Popen (["node" , "--no-warnings" , "--experimental-strip-types" ,
553+ str (repo / "tests/control_plane_ts/fixtures/shadow_management_crash_worker.ts" ),
554+ "bootstrap-public" , json .dumps (request ), "bootstrap_prepared" ],
555+ cwd = repo , stdout = subprocess .PIPE , stderr = subprocess .PIPE , text = True )
556+ try :
557+ ready , _ , _ = select .select ([child .stdout ], [], [], 15 )
558+ assert ready , "bootstrap did not reach its real filesystem barrier"
559+ assert child .stdout .readline ().strip () == "ready:bootstrap_prepared"
560+ finally :
561+ child .kill ()
562+ child .communicate (timeout = 10 )
563+ pending = read_shadow_management_state (runtime , "goal-a" )
564+ assert pending ["status" ] == "bootstrapping"
565+ # Restart from an isolated package with the old capture producer absent.
566+ invoke = _capture_free_chat ((runtime , state , service , preview ), tmp_path )
567+ replay = invoke ("historical" )
568+ assert replay ["action" ] == "reused" and replay ["created_todo_ids" ] == []
569+ recovered = service .apply (preview ["proposal_id" ])["proposal" ]
570+ assert recovered ["status" ] == "applied" , recovered
571+ assert replay ["lane_todo_ids" ] == recovered ["receipt" ]["resource_ids" ]["lane_todo_ids" ]
572+ assert state .read_bytes () == original
573+ assert read_shadow_management_state (runtime , "goal-a" ) == pending
574+
575+ # Recovery is tied to the exact intent, actor and original reviewed basis.
576+ saved = service .store .load (preview ["proposal_id" ])
577+ original_plan = {** saved ["normalized_parameters" ]["plan" ], "proposal_id" : preview ["proposal_id" ]}
578+ for changed in (
579+ {"proposal" : {** original_plan , "objective" : "Changed intent" }},
580+ {"agent_id" : "alpha" },
581+ {"expected_state_fingerprint" : "different-basis" },
582+ ):
583+ with pytest .raises (team_plan_adapter .EffectRuntimeRejected , match = "identity mismatch" ):
584+ team_plan_adapter .settle_team_plan (** {
585+ "registry_path" : registry , "goal_id" : "goal-a" , "agent_id" : None ,
586+ "proposal" : original_plan , "expected_state_fingerprint" : saved ["expected_state_fingerprint" ],
587+ "basis_agent_id" : "alpha" , ** changed ,
588+ })
589+ assert state .read_bytes () == original
590+ # A different plan still needs normal admission; recovery cannot restart work.
591+ fresh = service .preview ({"action_kind" : "team.plan" , "summary" : "New work" , "context" : {},
592+ "normalized_parameters" : {"goal_id" : "goal-a" , "plan" : _agent_plan ()},
593+ "idempotency_key" : "new-request-during-hold" })
594+ refused = service .apply (fresh ["proposal_id" ])["proposal" ]
595+ assert refused ["status" ] == "failed" , refused
596+ assert state .read_bytes () == original
597+ assert read_shadow_management_state (runtime , "goal-a" ) == pending
0 commit comments