@@ -192,3 +192,166 @@ async def test_provider_override_allows_skill_requiring_claude_code(
192192 assert mock_ssm .init_session .called , (
193193 "Expected init_session to be called after provider override rerouted codex→claude-code"
194194 )
195+
196+
197+ class TestHookFixRequiredDispatchGate :
198+ """Dispatch-time gate for fix-required hook entries in HOOK_REGISTRY."""
199+
200+ def test_refuses_codex_dispatch_when_fix_required_hook_exists (
201+ self , monkeypatch : pytest .MonkeyPatch
202+ ) -> None :
203+ import json
204+ from unittest .mock import MagicMock
205+
206+ from autoskillit .core import AGENT_BACKEND_CODEX , CodingAgentBackend
207+ from autoskillit .hook_registry import HookDef
208+ from autoskillit .server .tools import tools_execution
209+ from autoskillit .server .tools .tools_execution import _check_backend_compat
210+
211+ registry = [
212+ HookDef (
213+ matcher = r"Read|Write" ,
214+ scripts = ["guards/test_guard.py" ],
215+ codex_status = "fix-required" ,
216+ ),
217+ ]
218+ monkeypatch .setattr (tools_execution , "HOOK_REGISTRY" , registry )
219+
220+ backend = MagicMock (spec = CodingAgentBackend )
221+ backend .name = AGENT_BACKEND_CODEX
222+ backend .capabilities .applicable_guards = frozenset ({"write_guard" })
223+
224+ skill_info = MagicMock ()
225+ skill_info .backend_requirements = frozenset ({AGENT_BACKEND_CODEX })
226+
227+ result = _check_backend_compat (
228+ skill_command = "/autoskillit:test" ,
229+ resolved_command = "/autoskillit:test" ,
230+ effective_order_id = "ord-1" ,
231+ target_name = "test" ,
232+ skill_info = skill_info ,
233+ effective_backend_obj = backend ,
234+ skill_resolver = MagicMock (),
235+ )
236+ assert result is not None
237+ parsed = json .loads (result )
238+ assert parsed ["subtype" ] == "crashed"
239+ error_text = parsed .get ("result" ) or ""
240+ assert "Read|Write" in error_text
241+
242+ def test_allows_claude_code_dispatch_even_with_fix_required_hook (
243+ self , monkeypatch : pytest .MonkeyPatch
244+ ) -> None :
245+ from unittest .mock import MagicMock
246+
247+ from autoskillit .core import AGENT_BACKEND_CLAUDE_CODE , CodingAgentBackend
248+ from autoskillit .hook_registry import HookDef
249+ from autoskillit .server .tools import tools_execution
250+ from autoskillit .server .tools .tools_execution import _check_backend_compat
251+
252+ registry = [
253+ HookDef (
254+ matcher = r"Read|Write" ,
255+ scripts = ["guards/test_guard.py" ],
256+ codex_status = "fix-required" ,
257+ ),
258+ ]
259+ monkeypatch .setattr (tools_execution , "HOOK_REGISTRY" , registry )
260+
261+ backend = MagicMock (spec = CodingAgentBackend )
262+ backend .name = AGENT_BACKEND_CLAUDE_CODE
263+ backend .capabilities .applicable_guards = frozenset ({"test_guard" })
264+
265+ skill_info = MagicMock ()
266+ skill_info .backend_requirements = frozenset ({AGENT_BACKEND_CLAUDE_CODE })
267+
268+ result = _check_backend_compat (
269+ skill_command = "/autoskillit:test" ,
270+ resolved_command = "/autoskillit:test" ,
271+ effective_order_id = "" ,
272+ target_name = "test" ,
273+ skill_info = skill_info ,
274+ effective_backend_obj = backend ,
275+ skill_resolver = MagicMock (),
276+ )
277+ assert result is None
278+
279+ def test_allows_codex_dispatch_when_no_fix_required_hooks (
280+ self , monkeypatch : pytest .MonkeyPatch
281+ ) -> None :
282+ from unittest .mock import MagicMock
283+
284+ from autoskillit .core import AGENT_BACKEND_CODEX , CodingAgentBackend
285+ from autoskillit .hook_registry import HookDef
286+ from autoskillit .server .tools import tools_execution
287+ from autoskillit .server .tools .tools_execution import _check_backend_compat
288+
289+ registry = [
290+ HookDef (matcher = r"Read|Write" , codex_status = "works-as-is" ),
291+ HookDef (matcher = r"Bash" , codex_status = "not-applicable" ),
292+ ]
293+ monkeypatch .setattr (tools_execution , "HOOK_REGISTRY" , registry )
294+
295+ backend = MagicMock (spec = CodingAgentBackend )
296+ backend .name = AGENT_BACKEND_CODEX
297+ backend .capabilities .applicable_guards = frozenset ({"write_guard" })
298+
299+ skill_info = MagicMock ()
300+ skill_info .backend_requirements = frozenset ({AGENT_BACKEND_CODEX })
301+
302+ result = _check_backend_compat (
303+ skill_command = "/autoskillit:test" ,
304+ resolved_command = "/autoskillit:test" ,
305+ effective_order_id = "" ,
306+ target_name = "test" ,
307+ skill_info = skill_info ,
308+ effective_backend_obj = backend ,
309+ skill_resolver = MagicMock (),
310+ )
311+ assert result is None
312+
313+ def test_crash_message_lists_affected_matchers (self , monkeypatch : pytest .MonkeyPatch ) -> None :
314+ import json
315+ from unittest .mock import MagicMock
316+
317+ from autoskillit .core import AGENT_BACKEND_CODEX , CodingAgentBackend
318+ from autoskillit .hook_registry import HookDef
319+ from autoskillit .server .tools import tools_execution
320+ from autoskillit .server .tools .tools_execution import _check_backend_compat
321+
322+ registry = [
323+ HookDef (
324+ matcher = r"Read|Write" ,
325+ scripts = ["guards/guard_a.py" ],
326+ codex_status = "fix-required" ,
327+ ),
328+ HookDef (
329+ matcher = r"Bash|Grep" ,
330+ scripts = ["guards/guard_b.py" ],
331+ codex_status = "fix-required" ,
332+ ),
333+ ]
334+ monkeypatch .setattr (tools_execution , "HOOK_REGISTRY" , registry )
335+
336+ backend = MagicMock (spec = CodingAgentBackend )
337+ backend .name = AGENT_BACKEND_CODEX
338+ backend .capabilities .applicable_guards = frozenset ({"write_guard" })
339+
340+ skill_info = MagicMock ()
341+ skill_info .backend_requirements = frozenset ({AGENT_BACKEND_CODEX })
342+
343+ result = _check_backend_compat (
344+ skill_command = "/autoskillit:test" ,
345+ resolved_command = "/autoskillit:test" ,
346+ effective_order_id = "ord-2" ,
347+ target_name = "test" ,
348+ skill_info = skill_info ,
349+ effective_backend_obj = backend ,
350+ skill_resolver = MagicMock (),
351+ )
352+ assert result is not None
353+ parsed = json .loads (result )
354+ assert parsed ["subtype" ] == "crashed"
355+ error_text = parsed .get ("result" ) or ""
356+ assert "Read|Write" in error_text
357+ assert "Bash|Grep" in error_text
0 commit comments