mirror of
https://github.com/github/spec-kit.git
synced 2026-08-03 06:26:30 +08:00
* feat(scripts): port create-new-feature, setup-plan and setup-tasks to Python Ports the three core workflow scripts to Python as part of #3280, following the check-prerequisites PoC pattern from #3302. Adds resolve_template() to the shared common.py module and parity tests that run bash and Python side by side. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(tests): treat only None env as unset in parity run helper Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(scripts): fall back to directory scan on any registry error, skip hidden preset dirs Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * feat(templates): add py: lines for setup_plan and setup_tasks Ships with the scripts they reference; the remaining templates got their py: lines in #3403. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: support py variant in skills placeholder resolver resolve_skill_placeholders only accepted sh/ps, so a py init option fell into the fallback path and {SCRIPT} rendered without an interpreter prefix. Accept py and prefix the resolved interpreter, matching process_template. Also guard ps_cmd against a missing PowerShell with a clear assert. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * test: pin clean-error behavior for invalid --number Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * docs(scripts): reword unused-arg comment to match implementation The loop accepts and silently ignores extra positional args (it doesn't build a collected list); match the wording to what the code and setup-plan.sh actually do. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: fall back when configured script variant is missing from frontmatter Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(scripts): reject signed/whitespace --number values to match bash 10# parity The bash twin uses $((10#$BRANCH_NUMBER)), which rejects signed and whitespace-padded values. Python's int() accepted them (e.g. -1), producing a malformed -01-... prefix that sequential scans ignore. Restrict --number to unsigned decimal digits before conversion, and pin the parity with a bash-comparison test. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(scripts): complete Python port installation Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(integrations): fall back for missing script variants Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * test: make Python script checks platform-aware Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix Windows Python command invocation parity Use PowerShell's call operator for spaced Python interpreter paths and align setup-tasks missing-template errors across script variants. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(scripts): preserve cross-platform Python parity Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: reject signed PowerShell feature numbers Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(scripts): align feature number range Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(scripts): reject exhausted feature numbers Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(scripts): complete create feature parity Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(scripts): align create feature outputs Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(scripts): harden cross-platform parity Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(scripts): keep truncation JSON clean Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(scripts): align setup failure parity Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(scripts): close parity edge cases Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(scripts): propagate PowerShell setup errors Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(scripts): harden fallback resolution Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(scripts): stabilize PowerShell fallbacks Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(scripts): complete setup-plan parity Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(cli): require runnable script fallbacks Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(cli): preserve shell fallback without preference Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(scripts): restore help and symlink parity - setup-tasks.ps1: check -Help before unknown-argument validation so '-Help --bogus' exits 0 like the Bash/Python variants - common.py: strip the repo root prefix lexically in persist_feature_json instead of resolve(), so a symlinked specs/ still persists the relative 'specs/NNN-name' path the Bash/PowerShell helpers store Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(scripts): align persist-hint quoting with shlex.quote - create-new-feature.sh: replace printf %q with a shell_quote helper that emits shlex.quote-identical output, so the persistence hints stay byte-identical between the Bash and Python variants (printf %q output also varies between bash versions) - promote the negative --number test to an all-variants parity test now that Bash and PowerShell reject signed values consistently - add a spaced-repo-path parity test for the persistence hints Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
153 lines
4.9 KiB
Python
153 lines
4.9 KiB
Python
"""resolve_skill_placeholders must support the py script variant (#3280)."""
|
|
|
|
import os
|
|
from pathlib import Path
|
|
from types import SimpleNamespace
|
|
|
|
import pytest
|
|
|
|
from specify_cli._init_options import save_init_options
|
|
from specify_cli.agents import CommandRegistrar
|
|
|
|
FRONTMATTER = {
|
|
"scripts": {
|
|
"sh": "scripts/bash/setup-plan.sh --json",
|
|
"ps": "scripts/powershell/setup-plan.ps1 -Json",
|
|
"py": "scripts/python/setup_plan.py --json",
|
|
}
|
|
}
|
|
|
|
|
|
def _resolve(tmp_path: Path, script: str | None, monkeypatch) -> str:
|
|
monkeypatch.setattr(
|
|
"specify_cli.integrations.base.shutil.which",
|
|
lambda name: "/usr/bin/python3" if name == "python3" else None,
|
|
)
|
|
monkeypatch.setattr(
|
|
"specify_cli.integrations.base.IntegrationBase._interpreter_runs",
|
|
staticmethod(lambda path: True),
|
|
)
|
|
if script:
|
|
save_init_options(tmp_path, {"script": script})
|
|
return CommandRegistrar.resolve_skill_placeholders(
|
|
"codex", FRONTMATTER, "Run {SCRIPT} now.", tmp_path
|
|
)
|
|
|
|
|
|
def test_py_variant_prefixes_interpreter(tmp_path, monkeypatch):
|
|
body = _resolve(tmp_path, "py", monkeypatch)
|
|
assert "python3 .specify/scripts/python/setup_plan.py --json" in body
|
|
assert "{SCRIPT}" not in body
|
|
|
|
|
|
def test_sh_variant_is_not_prefixed(tmp_path, monkeypatch):
|
|
body = _resolve(tmp_path, "sh", monkeypatch)
|
|
assert ".specify/scripts/bash/setup-plan.sh --json" in body
|
|
assert "python3" not in body
|
|
|
|
|
|
def test_py_interpreter_with_spaces_uses_powershell_call_operator(
|
|
tmp_path, monkeypatch
|
|
):
|
|
interpreter = r"C:\Program Files\Py$thon's\python.exe"
|
|
quoted_interpreter = interpreter.replace("'", "''")
|
|
monkeypatch.setattr(
|
|
"specify_cli.integrations.base.shutil.which", lambda name: None
|
|
)
|
|
monkeypatch.setattr(
|
|
"specify_cli.integrations.base.sys.executable",
|
|
interpreter,
|
|
)
|
|
monkeypatch.setattr(
|
|
"specify_cli.integrations.base.os", SimpleNamespace(name="nt")
|
|
)
|
|
save_init_options(tmp_path, {"script": "py"})
|
|
body = CommandRegistrar.resolve_skill_placeholders(
|
|
"codex", FRONTMATTER, "Run {SCRIPT} now.", tmp_path
|
|
)
|
|
assert f"& '{quoted_interpreter}' " in body
|
|
|
|
|
|
def test_missing_py_variant_falls_back_to_available_script(tmp_path, monkeypatch):
|
|
"""script=py with a template that only ships sh/ps must not leave {SCRIPT} unresolved."""
|
|
monkeypatch.setattr(
|
|
"specify_cli.integrations.base.shutil.which",
|
|
lambda name: "/usr/bin/python3" if name == "python3" else None,
|
|
)
|
|
save_init_options(tmp_path, {"script": "py"})
|
|
frontmatter = {
|
|
"scripts": {
|
|
"sh": "scripts/bash/setup-plan.sh --json",
|
|
"ps": "scripts/powershell/setup-plan.ps1 -Json",
|
|
}
|
|
}
|
|
body = CommandRegistrar.resolve_skill_placeholders(
|
|
"codex", frontmatter, "Run {SCRIPT} now.", tmp_path
|
|
)
|
|
assert "{SCRIPT}" not in body
|
|
assert "setup-plan" in body
|
|
|
|
|
|
def test_py_install_includes_python_and_fallback_scripts(tmp_path, monkeypatch):
|
|
from specify_cli import _install_shared_infra
|
|
|
|
monkeypatch.setattr(
|
|
"specify_cli.integrations.base.shutil.which",
|
|
lambda name: "/usr/bin/python3" if name == "python3" else None,
|
|
)
|
|
_install_shared_infra(tmp_path, "py", force=True)
|
|
|
|
assert (tmp_path / ".specify/scripts/python/setup_plan.py").is_file()
|
|
assert (tmp_path / ".specify/scripts/python/setup_tasks.py").is_file()
|
|
|
|
save_init_options(tmp_path, {"script": "py"})
|
|
frontmatter = {
|
|
"scripts": {
|
|
"sh": "scripts/bash/check-prerequisites.sh --json",
|
|
"ps": "scripts/powershell/check-prerequisites.ps1 -Json",
|
|
}
|
|
}
|
|
body = CommandRegistrar.resolve_skill_placeholders(
|
|
"codex", frontmatter, "Run {SCRIPT} now.", tmp_path
|
|
)
|
|
fallback = (
|
|
".specify/scripts/bash/check-prerequisites.sh"
|
|
if "scripts/bash/" in body
|
|
else ".specify/scripts/powershell/check-prerequisites.ps1"
|
|
)
|
|
assert (tmp_path / fallback).is_file()
|
|
|
|
|
|
def test_py_rejects_one_sided_opposite_platform_fallback(
|
|
tmp_path, monkeypatch
|
|
):
|
|
from specify_cli import _install_shared_infra
|
|
from specify_cli import shared_infra
|
|
|
|
class WindowsOs:
|
|
name = "nt"
|
|
|
|
def __getattr__(self, attr):
|
|
return getattr(os, attr)
|
|
|
|
monkeypatch.setattr(shared_infra, "os", WindowsOs())
|
|
monkeypatch.setattr(
|
|
"specify_cli.integrations.base.platform.system", lambda: "Windows"
|
|
)
|
|
_install_shared_infra(tmp_path, "py", force=True)
|
|
|
|
save_init_options(tmp_path, {"script": "py"})
|
|
frontmatter = {
|
|
"scripts": {
|
|
"sh": "scripts/bash/check-prerequisites.sh --json",
|
|
}
|
|
}
|
|
with pytest.raises(ValueError, match="No runnable script variant"):
|
|
CommandRegistrar.resolve_skill_placeholders(
|
|
"codex", frontmatter, "Run {SCRIPT} now.", tmp_path
|
|
)
|
|
|
|
assert not (
|
|
tmp_path / ".specify/scripts/bash/check-prerequisites.sh"
|
|
).exists()
|