@@ -18,6 +18,7 @@ def _make_deferred_recall_ctx(name: str) -> MagicMock:
1818 ctx .gate .enabled = True
1919 ctx .recipe_name = name
2020 ctx .kitchen_id = "test-kitchen"
21+ ctx .gate_infrastructure_ready = True
2122 return ctx
2223
2324
@@ -196,3 +197,190 @@ async def test_deferred_recall_fails_closed_when_valid_missing():
196197 assert parsed ["stage" ] == "recipe_validation"
197198 assert parsed ["errors" ] == []
198199 assert "user_visible_message" in parsed
200+
201+
202+ def _make_pre_revealed_ctx (name : str ) -> MagicMock :
203+ ctx = _make_mock_ctx ()
204+ ctx .gate .enabled = True
205+ ctx .recipe_name = ""
206+ ctx .kitchen_id = "test-kitchen"
207+ ctx .gate_infrastructure_ready = True
208+ ctx .recipes .load_and_validate .return_value = {
209+ "content" : "name: test-recipe\n steps:\n build:\n cmd: task build\n " ,
210+ "valid" : True ,
211+ "errors" : [],
212+ "requires_packs" : [],
213+ "requires_features" : [],
214+ "content_hash" : "abc123" ,
215+ "composite_hash" : "def456" ,
216+ "recipe_version" : "1.0" ,
217+ "suggestions" : [],
218+ "post_prune_step_names" : ["build" , "test" ],
219+ }
220+ return ctx
221+
222+
223+ @pytest .mark .anyio
224+ async def test_pre_reveal_then_open_does_not_re_execute_handler ():
225+ """Pre-revealed state (gate enabled, recipe_name empty, infrastructure ready)
226+ must skip _open_kitchen_handler and still load the recipe."""
227+ from autoskillit .server .tools import tools_kitchen
228+
229+ mock_ctx = _make_pre_revealed_ctx ("test-recipe" )
230+ mock_recipe_info = MagicMock ()
231+ mock_recipe_info .path = Path ("/fake/.autoskillit/recipes/test-recipe.yaml" )
232+ mock_ctx .recipes .find .return_value = mock_recipe_info
233+ mock_recipe_obj = MagicMock ()
234+ mock_recipe_obj .steps = {"build" : {"cmd" : "task build" }}
235+ mock_recipe_obj .ingredients = {"ing1" : "val1" }
236+ mock_ctx .recipes .load .return_value = mock_recipe_obj
237+
238+ with (
239+ patch ("autoskillit.server._get_ctx" , return_value = mock_ctx ),
240+ patch .object (
241+ tools_kitchen , "_open_kitchen_handler" , new_callable = MagicMock
242+ ) as mock_handler ,
243+ ):
244+ result = await tools_kitchen .open_kitchen (name = "test-recipe" , ctx = mock_ctx )
245+
246+ mock_handler .assert_not_called ()
247+ parsed = json .loads (result )
248+ assert parsed ["success" ] is True
249+
250+
251+ @pytest .mark .anyio
252+ async def test_deferred_recall_strips_content_when_ingredients_only_true ():
253+ """Deferred-recall path must respect ingredients_only flag."""
254+ from autoskillit .server .tools import tools_kitchen
255+
256+ mock_ctx = _make_deferred_recall_ctx ("test-recipe" )
257+ mock_ctx .recipes .load_and_validate .return_value = {
258+ "content" : "name: test-recipe\n steps:\n build:\n cmd: task build\n " ,
259+ "valid" : True ,
260+ "errors" : [],
261+ "requires_packs" : [],
262+ "requires_features" : [],
263+ "content_hash" : "abc" ,
264+ "composite_hash" : "def" ,
265+ "recipe_version" : "1.0" ,
266+ "suggestions" : [],
267+ "orchestration_rules" : "some rules" ,
268+ "stop_step_semantics" : "some semantics" ,
269+ }
270+ mock_recipe_info = MagicMock ()
271+ mock_recipe_info .path = Path ("/fake/.autoskillit/recipes/test-recipe.yaml" )
272+ mock_ctx .recipes .find .return_value = mock_recipe_info
273+ mock_recipe_obj = MagicMock ()
274+ mock_recipe_obj .steps = {"build" : {"cmd" : "task build" }}
275+ mock_recipe_obj .ingredients = {"ing1" : "val1" }
276+ mock_ctx .recipes .load .return_value = mock_recipe_obj
277+
278+ with patch ("autoskillit.server._get_ctx" , return_value = mock_ctx ):
279+ result = await tools_kitchen .open_kitchen (
280+ name = "test-recipe" , ingredients_only = True , ctx = mock_ctx
281+ )
282+
283+ parsed = json .loads (result )
284+ assert "content" not in parsed
285+ assert "orchestration_rules" not in parsed
286+ assert "stop_step_semantics" not in parsed
287+
288+
289+ @pytest .mark .anyio
290+ async def test_double_open_kitchen_no_name_does_not_re_execute_handler ():
291+ """Calling open_kitchen() with name=None while infrastructure is ready
292+ must not re-run _open_kitchen_handler."""
293+ from autoskillit .server .tools import tools_kitchen
294+
295+ mock_ctx = _make_mock_ctx ()
296+ mock_ctx .gate .enabled = True
297+ mock_ctx .gate_infrastructure_ready = True
298+ mock_ctx .kitchen_id = "test-kitchen"
299+
300+ with (
301+ patch ("autoskillit.server._get_ctx" , return_value = mock_ctx ),
302+ patch .object (
303+ tools_kitchen , "_open_kitchen_handler" , new_callable = MagicMock
304+ ) as mock_handler ,
305+ ):
306+ result = await tools_kitchen .open_kitchen (ctx = mock_ctx )
307+
308+ mock_handler .assert_not_called ()
309+ assert isinstance (result , str )
310+
311+
312+ @pytest .mark .anyio
313+ async def test_gate_rollback_resets_gate_infrastructure_ready ():
314+ """When recipe validation fails in deferred-recall path, gate_infrastructure_ready
315+ must be reset so the next open_kitchen call re-runs the handler."""
316+ from autoskillit .server .tools import tools_kitchen
317+
318+ mock_ctx = _make_pre_revealed_ctx ("bad-recipe" )
319+ mock_ctx .recipes .load_and_validate .return_value = {
320+ "content" : "" ,
321+ "valid" : False ,
322+ "errors" : ["structural error" ],
323+ "requires_packs" : [],
324+ "requires_features" : [],
325+ "content_hash" : "abc" ,
326+ "composite_hash" : "def" ,
327+ "recipe_version" : "1.0" ,
328+ "suggestions" : [],
329+ }
330+
331+ with patch ("autoskillit.server._get_ctx" , return_value = mock_ctx ):
332+ result = await tools_kitchen .open_kitchen (name = "bad-recipe" , ctx = mock_ctx )
333+
334+ parsed = json .loads (result )
335+ assert parsed ["success" ] is False
336+ assert mock_ctx .gate_infrastructure_ready is False
337+
338+
339+ @pytest .mark .anyio
340+ async def test_pre_reveal_backend_does_not_support_tool_list_changed ():
341+ """Simulates Codex pre-reveal: gate enabled, recipe_name empty, infrastructure ready.
342+ Handler must be skipped, recipe loaded normally."""
343+ from autoskillit .server .tools import tools_kitchen
344+
345+ mock_ctx = _make_pre_revealed_ctx ("test-recipe" )
346+ mock_recipe_info = MagicMock ()
347+ mock_recipe_info .path = Path ("/fake/.autoskillit/recipes/test-recipe.yaml" )
348+ mock_ctx .recipes .find .return_value = mock_recipe_info
349+ mock_recipe_obj = MagicMock ()
350+ mock_recipe_obj .steps = {"build" : {"cmd" : "task build" }}
351+ mock_recipe_obj .ingredients = {"ing1" : "val1" }
352+ mock_ctx .recipes .load .return_value = mock_recipe_obj
353+
354+ with (
355+ patch ("autoskillit.server._get_ctx" , return_value = mock_ctx ),
356+ patch .object (
357+ tools_kitchen , "_open_kitchen_handler" , new_callable = MagicMock
358+ ) as mock_handler ,
359+ ):
360+ result = await tools_kitchen .open_kitchen (name = "test-recipe" , ctx = mock_ctx )
361+
362+ mock_handler .assert_not_called ()
363+ parsed = json .loads (result )
364+ assert parsed ["success" ] is True
365+
366+
367+ @pytest .mark .anyio
368+ async def test_cold_open_kitchen_runs_handler ():
369+ """When gate_infrastructure_ready is False (cold state), handler must run."""
370+ from autoskillit .server .tools import tools_kitchen
371+
372+ mock_ctx = _make_mock_ctx ()
373+ mock_ctx .gate .enabled = False
374+ mock_ctx .gate_infrastructure_ready = False
375+
376+ with (
377+ patch ("autoskillit.server._get_ctx" , return_value = mock_ctx ),
378+ patch .object (
379+ tools_kitchen , "_open_kitchen_handler" , new_callable = MagicMock
380+ ) as mock_handler ,
381+ ):
382+ mock_handler .return_value = None
383+ result = await tools_kitchen .open_kitchen (name = "test-recipe" , ctx = mock_ctx )
384+
385+ mock_handler .assert_called_once ()
386+ assert isinstance (result , str )
0 commit comments