@@ -241,6 +241,60 @@ def test_canonical_cli_writer_keeps_multiline_prompt_on_one_yaml_line(tmp_path,
241241 assert data ["hooks" ][0 ]["prompt" ] == "first\n second"
242242
243243
244+ @pytest .mark .parametrize ("variant" , ["sh" , "ps" , "py" ])
245+ @pytest .mark .parametrize ("phase" , ["pre" , "post" ])
246+ def test_canonical_backslash_escape_matches_python (tmp_path , variant , phase ):
247+ if variant == "ps" and not shutil .which ("pwsh" ):
248+ pytest .skip ("PowerShell not installed" )
249+ from specify_cli .extensions import HookExecutor
250+
251+ event = f"{ 'before' if phase == 'pre' else 'after' } _plan"
252+ prompt = "Path C:\\ workspace\n keep \\ literal"
253+ HookExecutor (tmp_path ).save_project_config ({
254+ "hooks" : {event : [
255+ {"extension" : "git" , "command" : "speckit.git.commit" , "prompt" : prompt }
256+ ]}
257+ })
258+ code , data = run_hook (tmp_path , phase , variant = variant )
259+ assert code == 0
260+ assert data == run_hook (tmp_path , phase )[1 ]
261+ assert data ["hooks" ][0 ]["prompt" ] == prompt
262+
263+
264+ @pytest .mark .parametrize ("variant" , ["sh" , "ps" , "py" ])
265+ @pytest .mark .parametrize ("phase" , ["pre" , "post" ])
266+ @pytest .mark .parametrize ("field,value" , [
267+ ("extension" , "false" ), ("command" , "123" ), ("extension" , "yes" ),
268+ ("command" , "0x10" ), ("extension" , "null" ), ("command" , "1.2" ),
269+ ("extension" , "2026-10-08" ), ("command" , ".nan" ),
270+ ])
271+ def test_native_identifiers_reject_implicitly_typed_yaml (tmp_path , variant , phase , field , value ):
272+ if variant == "ps" and not shutil .which ("pwsh" ):
273+ pytest .skip ("PowerShell not installed" )
274+ event = f"{ 'before' if phase == 'pre' else 'after' } _plan"
275+ write_config (
276+ tmp_path ,
277+ f"hooks:\n { event } :\n - extension: git\n "
278+ f" command: speckit.git.commit\n { field } : { value } \n " ,
279+ )
280+ code , data = run_hook (tmp_path , phase , variant = variant )
281+ assert code == 1
282+ assert "needs extension and command" in data ["error" ]
283+ assert data ["hooks" ] == []
284+
285+
286+ @pytest .mark .parametrize ("variant" , ["sh" , "ps" , "py" ])
287+ @pytest .mark .parametrize ("phase" , ["pre" , "post" ])
288+ def test_quoted_numeric_identifier_remains_string (tmp_path , variant , phase ):
289+ if variant == "ps" and not shutil .which ("pwsh" ):
290+ pytest .skip ("PowerShell not installed" )
291+ event = f"{ 'before' if phase == 'pre' else 'after' } _plan"
292+ write_config (tmp_path , f"hooks:\n { event } :\n - extension: '123'\n command: 'false'\n " )
293+ code , data = run_hook (tmp_path , phase , variant = variant )
294+ assert code == 0
295+ assert data == run_hook (tmp_path , phase )[1 ]
296+
297+
244298@pytest .mark .parametrize ("variant" , ["sh" , "ps" ])
245299@pytest .mark .parametrize ("phase" , ["pre" , "post" ])
246300def test_native_resolvers_do_not_invoke_python (tmp_path , variant , phase ):
0 commit comments