@@ -262,6 +262,59 @@ def test_plan_mode_converts_plain_plan_and_discards_early_actions(self):
262262 main ._interaction_controller = previous_controller
263263 main ._active_interaction_mode .reset (mode_token )
264264
265+ def test_plan_mode_repairs_rejected_action_into_proposal (self ):
266+ class LearningStub :
267+ def feedback_signal (self , _text ): return None
268+ def route_profile (self , _text , _profile = None ): return "coder"
269+ def begin_run (self , profile , _task , provider_model = None ):
270+ return {"run_id" : "plan-repair" , "profile" : profile , "provider_model" : provider_model }
271+ def artifact_context (self , _run ): return "" , []
272+
273+ class RuntimeStub :
274+ def turn_start (self , ** _kwargs ): return None
275+ def turn_end (self , ** _kwargs ): return None
276+
277+ original = (
278+ main .tasks , main ._interaction_controller , main .learning_engine ,
279+ main ._get_workspace_root (), main ._execution_capability_token ,
280+ )
281+ with tempfile .TemporaryDirectory () as directory :
282+ root = Path (directory )
283+ store = EventStore (root / "state.sqlite3" )
284+ main .tasks = TaskManager (store , workspace_id = "repair" , session_id = "repair" )
285+ main ._interaction_controller = InteractionController (
286+ store , workspace_id = "repair" , session_id = "repair" ,
287+ )
288+ main ._interaction_controller .set_mode ("plan" )
289+ main .learning_engine = LearningStub ()
290+ main ._set_workspace_root (root )
291+ responses = [
292+ 'Action: {"action":"write_file","args":"unsafe.txt|bad"}' ,
293+ 'PlanProposal:\n ```json\n {"title":"Safe plan","summary":"Write only after approval.",'
294+ '"assumptions":[],"steps":[{"id":"write","title":"Write file",'
295+ '"description":"Create the requested file.","acceptance":["File exists"]}]}\n ```' ,
296+ ]
297+ try :
298+ with patch .object (main , "_plugin_runtime_for_surface" , return_value = RuntimeStub ()), \
299+ patch .object (main , "_touch_detached_learning_heartbeat" ), \
300+ patch .object (main , "dispatch_learning_cycle" ), \
301+ patch .object (main , "_summarize_old_turns" ), \
302+ patch .object (main , "_build_messages" , return_value = []), \
303+ patch .object (main , "_classify_complexity" , return_value = "simple" ), \
304+ patch .object (main , "_call_llm_with_spinner" , side_effect = responses ), \
305+ patch .object (main , "_finish_learning_run" , side_effect = lambda run , receipts , task ,
306+ result , records , tokens , started : result ):
307+ reply = main ._chat_turn ("Create the file" )
308+ self .assertIn ("Safe plan (v1)" , reply )
309+ self .assertFalse ((root / "unsafe.txt" ).exists ())
310+ self .assertEqual (
311+ main ._interaction_controller .state ()["pending_plan" ]["title" ], "Safe plan" ,
312+ )
313+ finally :
314+ (main .tasks , main ._interaction_controller , main .learning_engine ,
315+ previous_root , main ._execution_capability_token ) = original
316+ main ._set_workspace_root (previous_root )
317+
265318 def test_control_blocks_combined_with_actions_fail_closed (self ):
266319 parsed = main ._observe_model_response (
267320 'AskUser:\n ```json\n {"questions":[{"id":"scope","header":"Scope",'
0 commit comments