@@ -79,6 +79,84 @@ def cli(action, *args, success=True, output_format="json", runtime_override=None
7979 return cli , state , backup , body , runtime , receiver , env
8080
8181
82+ @pytest .mark .parametrize ("old_producers_present" , [True , False ])
83+ @pytest .mark .parametrize ("configuration" , ["absent" , "disabled" , "retired" ])
84+ def test_unpromoted_default_off_lease_lifecycle_without_shadow_producers (
85+ tmp_path , monkeypatch , old_producers_present , configuration ,
86+ ):
87+ _ , state , _ , _ , runtime , receiver , env = workspace (tmp_path , monkeypatch )
88+ registry = tmp_path / "project/.loopx/registry.json"
89+ data = json .loads (registry .read_text ())
90+ coordination = data ["goals" ][0 ]["coordination" ]
91+ coordination ["registered_agents" ] = ["agent-a" , "agent-b" ]
92+ if configuration == "disabled" :
93+ coordination ["runtime_shadow" ] = {
94+ "schema_version" : "loopx_coordination_runtime_shadow_config_v0" ,
95+ "enabled" : False , "provider" : "file_v0" ,
96+ }
97+ elif configuration == "retired" :
98+ coordination ["authority_shadow" ] = {
99+ "schema_version" : "loopx_local_authority_shadow_config_v0" ,
100+ "mode" : "file_one_way" ,
101+ }
102+ registry .write_text (json .dumps (data ))
103+ state .write_text (state .read_text ().replace ("handoff_mode: legacy" , "handoff_mode: hard_lease" )
104+ .replace (" claimed_by=agent-a" , "" ))
105+ original_state , original_registry = state .read_bytes (), registry .read_bytes ()
106+ if not old_producers_present :
107+ for name in ("runtime_shadow.py" , "runtime_shadow_writer_adapter.py" ,
108+ "local_authority_shadow_adapter.py" , "local_authority_shadow_outbox.py" ):
109+ (receiver / "loopx/control_plane/coordination" / name ).unlink ()
110+
111+ def lease (action , * args , owner = "agent-a" , key = "original" , success = True ):
112+ child = subprocess .run ([sys .executable , "-c" ,
113+ "import loopx,runpy,sys; print(loopx.__file__,file=sys.stderr); runpy.run_module('loopx.cli',run_name='__main__')" ,
114+ "--registry" , str (registry ), "--runtime-root" , str (runtime ), "--format" , "json" ,
115+ "task-lease" , action , "--goal-id" , "cold" , "--todo-id" , "todo_current" ,
116+ "--owner" , owner , "--idempotency-key" , key , * map (str , args )],
117+ cwd = tmp_path , env = env , capture_output = True , text = True , timeout = 60 )
118+ assert str (receiver / "loopx/__init__.py" ) in child .stderr
119+ assert "Traceback" not in child .stderr , child .stderr
120+ assert (child .returncode == 0 ) is success , child .stdout + child .stderr
121+ result = json .loads (child .stdout )
122+ assert "coordination_runtime_shadow_capture" not in result
123+ assert "coordination_runtime_shadow" not in result
124+ assert state .read_bytes () == original_state
125+ assert registry .read_bytes () == original_registry
126+ assert not (runtime / "authority-shadow" ).exists ()
127+ assert not any (path .is_file () for path in (runtime / "authority-transition" ).rglob ("*" ))
128+ return result
129+
130+ acquired = lease ("acquire" , "--ttl-seconds" , 120 , "--write-scope" , "docs/**" )
131+ assert acquired ["acquired" ] is True
132+ version = acquired ["lease" ]["version" ]
133+ path = Path (acquired ["lease_path" ])
134+ original_lease = path .read_bytes ()
135+ replay = lease ("acquire" , "--ttl-seconds" , 120 , "--write-scope" , "docs/**" )
136+ assert replay ["idempotent" ] is True and replay ["lease" ] == acquired ["lease" ]
137+ assert path .read_bytes () == original_lease
138+ rejected = lease ("renew" , "--expected-version" , version + 1 , "--ttl-seconds" , 180 , success = False )
139+ assert rejected ["error_code" ] == "version_mismatch"
140+ assert path .read_bytes () == original_lease
141+ renewed = lease ("renew" , "--expected-version" , version , "--ttl-seconds" , 180 )
142+ assert renewed ["lease" ]["version" ] == version + 1
143+ assert renewed ["lease" ]["lease_epoch" ] == acquired ["lease" ]["lease_epoch" ]
144+ transferred = lease ("transfer" , "--expected-version" , version + 1 ,
145+ "--new-owner" , "agent-b" , "--new-idempotency-key" , "receiver" ,
146+ "--ttl-seconds" , 180 )
147+ assert transferred ["lease" ]["owner" ] == "agent-b"
148+ assert transferred ["lease" ]["version" ] == version + 2
149+ assert transferred ["lease" ]["lease_epoch" ] == acquired ["lease" ]["lease_epoch" ] + 1
150+ released = lease ("release" , "--expected-version" , version + 2 , owner = "agent-b" , key = "receiver" )
151+ assert released ["released" ] is True and released ["lease" ]["status" ] == "released"
152+ assert released ["lease" ]["write_scopes" ] == ["docs/**" ]
153+ assert released ["lease" ]["lease_epoch" ] == transferred ["lease" ]["lease_epoch" ]
154+ retained = path .read_bytes ()
155+ assert lease ("release" , "--expected-version" , version + 2 ,
156+ owner = "agent-b" , key = "receiver" )["idempotent" ] is True
157+ assert path .read_bytes () == retained
158+
159+
82160def test_coordination_command_help_and_rejections_match_full_cli (tmp_path ):
83161 # Independently compare the existing compatibility parser/handler with the
84162 # selected dispatcher, including diagnostics that deliberately fall back.
0 commit comments