Files
github-spec-kit/tests/test_command_template_py_scripts.py
Marsel Safin 55da30c66d feat(templates): add py: lines to command templates' scripts frontmatter (#3403)
* feat(templates): add py: lines to command templates' scripts frontmatter

Every templates/commands/*.md with a scripts: block now declares a py:
variant so --script py renders a Python invocation via the existing
interpreter-prefixing in process_template.

Fixes #3283

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix: install scripts/python for the py script type

install_shared_infra mapped every non-sh script type to powershell, so
--script py rendered invocations pointing at files that were never
installed. Map py to the python variant dir and skip __pycache__
artifacts during the copy.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* test: read templates with explicit utf-8 encoding

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix: keep py: lines standalone, enforce script existence in tests

Drop the plan/tasks py: lines that referenced scripts shipping in the
core port (#3280); they move to that PR. Tests now assert every py:
line points at a script the repo ships, so a dangling reference can
never merge green.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-07-09 08:51:20 -05:00

97 lines
3.3 KiB
Python

"""Command templates with a py: script line must render for --script py.
Covers #3283: ``py:`` lines in the ``scripts:`` frontmatter of
``templates/commands/*.md`` reference Python scripts that exist in the repo,
and ``process_template`` turns them into a valid Python invocation
(interpreter-prefixed, path rewritten to the ``.specify`` tree).
``plan.md`` and ``tasks.md`` gain their ``py:`` lines together with
``setup_plan.py``/``setup_tasks.py`` in the core-scripts port (#3280); the
existence check below enforces that ordering.
"""
import re
from pathlib import Path
import pytest
from specify_cli.integrations.base import IntegrationBase
REPO_ROOT = Path(__file__).parent.parent
TEMPLATES_DIR = REPO_ROOT / "templates" / "commands"
_PY_LINE = re.compile(r"^\s*py: (scripts/python/\S+\.py)", re.MULTILINE)
def _py_script(name: str) -> str | None:
match = _PY_LINE.search((TEMPLATES_DIR / name).read_text(encoding="utf-8"))
return match.group(1) if match else None
PY_TEMPLATES = sorted(
p.name for p in TEMPLATES_DIR.glob("*.md") if _py_script(p.name)
)
@pytest.fixture(autouse=True)
def _pin_interpreter(monkeypatch):
monkeypatch.setattr(
"specify_cli.integrations.base.shutil.which",
lambda name: "/usr/bin/python3" if name == "python3" else None,
)
def test_py_templates_discovered():
# Guard: the glob must find the known py-scripted templates, otherwise
# the parametrized tests below would silently pass on an empty set.
assert "implement.md" in PY_TEMPLATES
assert "clarify.md" in PY_TEMPLATES
@pytest.mark.parametrize("name", PY_TEMPLATES)
def test_referenced_python_script_exists(name: str):
# A py: line must never point at a script the repo does not ship —
# rendering would produce a broken invocation at runtime.
script = _py_script(name)
assert (REPO_ROOT / script).is_file(), f"{name} references missing {script}"
@pytest.mark.parametrize("name", PY_TEMPLATES)
def test_template_renders_python_invocation(name: str):
content = (TEMPLATES_DIR / name).read_text(encoding="utf-8")
result = IntegrationBase.process_template(content, "agent", "py")
assert "{SCRIPT}" not in result
assert re.search(
r"python3 \.specify/scripts/python/\w+\.py(?: --[\w-]+)*", result
), f"{name} did not render a Python invocation"
@pytest.mark.parametrize("name", PY_TEMPLATES)
def test_sh_rendering_unchanged(name: str):
# Negative: adding py: lines must not leak into sh rendering.
content = (TEMPLATES_DIR / name).read_text(encoding="utf-8")
result = IntegrationBase.process_template(content, "agent", "sh")
assert "{SCRIPT}" not in result
assert "scripts/python" not in result
def test_install_shared_infra_copies_python_scripts(tmp_path):
# --script py must install scripts/python/ into .specify/scripts/python/
# so the rendered invocations point at files that exist.
from rich.console import Console
from specify_cli.shared_infra import install_shared_infra
install_shared_infra(
tmp_path,
"py",
version="0.0.0",
core_pack=None,
repo_root=REPO_ROOT,
console=Console(quiet=True),
force=False,
)
dest = tmp_path / ".specify" / "scripts" / "python"
assert (dest / "check_prerequisites.py").is_file()
assert not (tmp_path / ".specify" / "scripts" / "powershell").exists()