3939import pytest
4040import yaml
4141
42- import specify_cli
4342from specify_cli import (
4443 AGENT_CONFIG ,
4544 _TOML_AGENTS ,
@@ -86,8 +85,15 @@ def _run_release_script(agent: str, script_type: str, bash: str, output_dir: Pat
8685 capture_output = True , text = True ,
8786 cwd = str (_REPO_ROOT ),
8887 env = env ,
88+ timeout = 300 ,
8989 )
9090
91+ if result .returncode != 0 :
92+ pytest .fail (
93+ f"Release script failed with exit code { result .returncode } \n "
94+ f"stdout:\n { result .stdout } \n stderr:\n { result .stderr } "
95+ )
96+
9197 zip_pattern = f"spec-kit-template-{ agent } -{ script_type } -v0.0.0.zip"
9298 zip_path = output_dir / zip_pattern
9399 if not zip_path .exists ():
@@ -102,7 +108,6 @@ def _run_release_script(agent: str, script_type: str, bash: str, output_dir: Pat
102108# ---------------------------------------------------------------------------
103109
104110# Number of source command templates (one per .md file in templates/commands/)
105- _SOURCE_TEMPLATES : list [str ] = []
106111
107112
108113def _commands_dir () -> Path :
@@ -167,10 +172,32 @@ def source_template_stems() -> list[str]:
167172 return _get_source_template_stems ()
168173
169174
170- @pytest .fixture
171- def bundled_project (tmp_path ):
172- """Run scaffold_from_core_pack for each test; caller picks agent."""
173- return tmp_path
175+ @pytest .fixture (scope = "session" )
176+ def scaffolded_sh (tmp_path_factory ):
177+ """Session-scoped cache: scaffold once per agent with script_type='sh'."""
178+ cache = {}
179+ def _get (agent : str ) -> Path :
180+ if agent not in cache :
181+ project = tmp_path_factory .mktemp (f"scaffold_sh_{ agent } " )
182+ ok = scaffold_from_core_pack (project , agent , "sh" )
183+ assert ok , f"scaffold_from_core_pack returned False for agent '{ agent } '"
184+ cache [agent ] = project
185+ return cache [agent ]
186+ return _get
187+
188+
189+ @pytest .fixture (scope = "session" )
190+ def scaffolded_ps (tmp_path_factory ):
191+ """Session-scoped cache: scaffold once per agent with script_type='ps'."""
192+ cache = {}
193+ def _get (agent : str ) -> Path :
194+ if agent not in cache :
195+ project = tmp_path_factory .mktemp (f"scaffold_ps_{ agent } " )
196+ ok = scaffold_from_core_pack (project , agent , "ps" )
197+ assert ok , f"scaffold_from_core_pack returned False for agent '{ agent } '"
198+ cache [agent ] = project
199+ return cache [agent ]
200+ return _get
174201
175202
176203# ---------------------------------------------------------------------------
@@ -185,35 +212,29 @@ def bundled_project(tmp_path):
185212# ---------------------------------------------------------------------------
186213
187214@pytest .mark .parametrize ("agent" , _TESTABLE_AGENTS )
188- def test_scaffold_creates_specify_scripts (tmp_path , agent ):
215+ def test_scaffold_creates_specify_scripts (agent , scaffolded_sh ):
189216 """scaffold_from_core_pack copies at least one script into .specify/scripts/."""
190- project = tmp_path / "proj"
191- ok = scaffold_from_core_pack (project , agent , "sh" )
192- assert ok , f"scaffold_from_core_pack returned False for agent '{ agent } '"
217+ project = scaffolded_sh (agent )
193218
194219 scripts_dir = project / ".specify" / "scripts" / "bash"
195220 assert scripts_dir .is_dir (), f".specify/scripts/bash/ missing for agent '{ agent } '"
196221 assert any (scripts_dir .iterdir ()), f".specify/scripts/bash/ is empty for agent '{ agent } '"
197222
198223
199224@pytest .mark .parametrize ("agent" , _TESTABLE_AGENTS )
200- def test_scaffold_creates_specify_templates (tmp_path , agent ):
225+ def test_scaffold_creates_specify_templates (agent , scaffolded_sh ):
201226 """scaffold_from_core_pack copies at least one page template into .specify/templates/."""
202- project = tmp_path / "proj"
203- ok = scaffold_from_core_pack (project , agent , "sh" )
204- assert ok
227+ project = scaffolded_sh (agent )
205228
206229 tpl_dir = project / ".specify" / "templates"
207230 assert tpl_dir .is_dir (), f".specify/templates/ missing for agent '{ agent } '"
208231 assert any (tpl_dir .iterdir ()), ".specify/templates/ is empty"
209232
210233
211234@pytest .mark .parametrize ("agent" , _TESTABLE_AGENTS )
212- def test_scaffold_command_dir_location (tmp_path , agent , source_template_stems ):
235+ def test_scaffold_command_dir_location (agent , scaffolded_sh , source_template_stems ):
213236 """Command files land in the directory declared by AGENT_CONFIG."""
214- project = tmp_path / "proj"
215- ok = scaffold_from_core_pack (project , agent , "sh" )
216- assert ok
237+ project = scaffolded_sh (agent )
217238
218239 cmd_dir = _expected_cmd_dir (project , agent )
219240 assert cmd_dir .is_dir (), (
@@ -226,11 +247,9 @@ def test_scaffold_command_dir_location(tmp_path, agent, source_template_stems):
226247# ---------------------------------------------------------------------------
227248
228249@pytest .mark .parametrize ("agent" , _TESTABLE_AGENTS )
229- def test_scaffold_command_file_count (tmp_path , agent , source_template_stems ):
250+ def test_scaffold_command_file_count (agent , scaffolded_sh , source_template_stems ):
230251 """One command file is generated per source template for every agent."""
231- project = tmp_path / "proj"
232- ok = scaffold_from_core_pack (project , agent , "sh" )
233- assert ok
252+ project = scaffolded_sh (agent )
234253
235254 cmd_dir = _expected_cmd_dir (project , agent )
236255 generated = _list_command_files (cmd_dir , agent )
@@ -241,11 +260,9 @@ def test_scaffold_command_file_count(tmp_path, agent, source_template_stems):
241260
242261
243262@pytest .mark .parametrize ("agent" , _TESTABLE_AGENTS )
244- def test_scaffold_command_file_names (tmp_path , agent , source_template_stems ):
263+ def test_scaffold_command_file_names (agent , scaffolded_sh , source_template_stems ):
245264 """Each source template stem maps to a corresponding speckit.<stem>.<ext> file."""
246- project = tmp_path / "proj"
247- ok = scaffold_from_core_pack (project , agent , "sh" )
248- assert ok
265+ project = scaffolded_sh (agent )
249266
250267 cmd_dir = _expected_cmd_dir (project , agent )
251268 for stem in source_template_stems :
@@ -264,10 +281,9 @@ def test_scaffold_command_file_names(tmp_path, agent, source_template_stems):
264281# ---------------------------------------------------------------------------
265282
266283@pytest .mark .parametrize ("agent" , _TESTABLE_AGENTS )
267- def test_no_unresolved_script_placeholder (tmp_path , agent ):
284+ def test_no_unresolved_script_placeholder (agent , scaffolded_sh ):
268285 """{SCRIPT} must not appear in any generated command file."""
269- project = tmp_path / "proj"
270- scaffold_from_core_pack (project , agent , "sh" )
286+ project = scaffolded_sh (agent )
271287
272288 cmd_dir = _expected_cmd_dir (project , agent )
273289 for f in cmd_dir .rglob ("*" ):
@@ -279,10 +295,9 @@ def test_no_unresolved_script_placeholder(tmp_path, agent):
279295
280296
281297@pytest .mark .parametrize ("agent" , _TESTABLE_AGENTS )
282- def test_no_unresolved_agent_placeholder (tmp_path , agent ):
298+ def test_no_unresolved_agent_placeholder (agent , scaffolded_sh ):
283299 """__AGENT__ must not appear in any generated command file."""
284- project = tmp_path / "proj"
285- scaffold_from_core_pack (project , agent , "sh" )
300+ project = scaffolded_sh (agent )
286301
287302 cmd_dir = _expected_cmd_dir (project , agent )
288303 for f in cmd_dir .rglob ("*" ):
@@ -294,10 +309,9 @@ def test_no_unresolved_agent_placeholder(tmp_path, agent):
294309
295310
296311@pytest .mark .parametrize ("agent" , _TESTABLE_AGENTS )
297- def test_no_unresolved_args_placeholder (tmp_path , agent ):
312+ def test_no_unresolved_args_placeholder (agent , scaffolded_sh ):
298313 """{ARGS} must not appear in any generated command file (replaced with agent-specific token)."""
299- project = tmp_path / "proj"
300- scaffold_from_core_pack (project , agent , "sh" )
314+ project = scaffolded_sh (agent )
301315
302316 cmd_dir = _expected_cmd_dir (project , agent )
303317 for f in cmd_dir .rglob ("*" ):
@@ -317,14 +331,13 @@ def test_no_unresolved_args_placeholder(tmp_path, agent):
317331
318332
319333@pytest .mark .parametrize ("agent" , _TESTABLE_AGENTS )
320- def test_argument_token_format (tmp_path , agent ):
334+ def test_argument_token_format (agent , scaffolded_sh ):
321335 """For templates that carry an {ARGS} token:
322336 - TOML agents must emit {{args}}
323337 - Markdown agents must emit $ARGUMENTS
324338 Templates without {ARGS} (e.g. implement, plan) are skipped.
325339 """
326- project = tmp_path / "proj"
327- scaffold_from_core_pack (project , agent , "sh" )
340+ project = scaffolded_sh (agent )
328341
329342 cmd_dir = _expected_cmd_dir (project , agent )
330343
@@ -350,10 +363,9 @@ def test_argument_token_format(tmp_path, agent):
350363
351364
352365@pytest .mark .parametrize ("agent" , _TESTABLE_AGENTS )
353- def test_path_rewrites_applied (tmp_path , agent ):
366+ def test_path_rewrites_applied (agent , scaffolded_sh ):
354367 """Bare scripts/ and templates/ paths must be rewritten to .specify/ variants."""
355- project = tmp_path / "proj"
356- scaffold_from_core_pack (project , agent , "sh" )
368+ project = scaffolded_sh (agent )
357369
358370 cmd_dir = _expected_cmd_dir (project , agent )
359371 for f in cmd_dir .rglob ("*" ):
@@ -374,10 +386,9 @@ def test_path_rewrites_applied(tmp_path, agent):
374386# ---------------------------------------------------------------------------
375387
376388@pytest .mark .parametrize ("agent" , sorted (_TOML_AGENTS ))
377- def test_toml_format_valid (tmp_path , agent ):
389+ def test_toml_format_valid (agent , scaffolded_sh ):
378390 """TOML agents: every command file must have description and prompt fields."""
379- project = tmp_path / "proj"
380- scaffold_from_core_pack (project , agent , "sh" )
391+ project = scaffolded_sh (agent )
381392
382393 cmd_dir = _expected_cmd_dir (project , agent )
383394 for f in cmd_dir .glob ("speckit.*.toml" ):
@@ -398,10 +409,9 @@ def test_toml_format_valid(tmp_path, agent):
398409
399410
400411@pytest .mark .parametrize ("agent" , _MARKDOWN_AGENTS )
401- def test_markdown_has_frontmatter (tmp_path , agent ):
412+ def test_markdown_has_frontmatter (agent , scaffolded_sh ):
402413 """Markdown agents: every command file must start with valid YAML frontmatter."""
403- project = tmp_path / "proj"
404- scaffold_from_core_pack (project , agent , "sh" )
414+ project = scaffolded_sh (agent )
405415
406416 cmd_dir = _expected_cmd_dir (project , agent )
407417 for f in _list_command_files (cmd_dir , agent ):
@@ -422,11 +432,9 @@ def test_markdown_has_frontmatter(tmp_path, agent):
422432# 6. Copilot-specific: companion .prompt.md files
423433# ---------------------------------------------------------------------------
424434
425- def test_copilot_companion_prompt_files (tmp_path , source_template_stems ):
435+ def test_copilot_companion_prompt_files (scaffolded_sh , source_template_stems ):
426436 """Copilot: a speckit.<stem>.prompt.md companion is created for every .agent.md file."""
427- project = tmp_path / "proj"
428- ok = scaffold_from_core_pack (project , "copilot" , "sh" )
429- assert ok
437+ project = scaffolded_sh ("copilot" )
430438
431439 prompts_dir = project / ".github" / "prompts"
432440 assert prompts_dir .is_dir (), ".github/prompts/ not created for copilot"
@@ -438,10 +446,9 @@ def test_copilot_companion_prompt_files(tmp_path, source_template_stems):
438446 )
439447
440448
441- def test_copilot_prompt_file_content (tmp_path , source_template_stems ):
449+ def test_copilot_prompt_file_content (scaffolded_sh , source_template_stems ):
442450 """Copilot companion .prompt.md files must reference their parent .agent.md."""
443- project = tmp_path / "proj"
444- scaffold_from_core_pack (project , "copilot" , "sh" )
451+ project = scaffolded_sh ("copilot" )
445452
446453 prompts_dir = project / ".github" / "prompts"
447454 for stem in source_template_stems :
@@ -457,11 +464,9 @@ def test_copilot_prompt_file_content(tmp_path, source_template_stems):
457464# ---------------------------------------------------------------------------
458465
459466@pytest .mark .parametrize ("agent" , _TESTABLE_AGENTS )
460- def test_scaffold_powershell_variant (tmp_path , agent , source_template_stems ):
467+ def test_scaffold_powershell_variant (agent , scaffolded_ps , source_template_stems ):
461468 """scaffold_from_core_pack with script_type='ps' creates correct files."""
462- project = tmp_path / "proj"
463- ok = scaffold_from_core_pack (project , agent , "ps" )
464- assert ok
469+ project = scaffolded_ps (agent )
465470
466471 scripts_dir = project / ".specify" / "scripts" / "powershell"
467472 assert scripts_dir .is_dir (), f".specify/scripts/powershell/ missing for '{ agent } '"
0 commit comments