mirror of
https://github.com/github/spec-kit.git
synced 2026-07-04 04:45:43 +08:00
* ci: add windows-latest to test matrix Add windows-latest to the pytest job OS matrix so tests run on both Ubuntu and Windows for all Python versions. Closes #2232 * test: skip bash-specific tests on Windows Add sys.platform skip markers to all test classes and methods that execute bash scripts via subprocess, so they are skipped on Windows where bash is not available. Mixed classes with both bash and pwsh tests have markers on individual bash methods only. * test: fix 3 Windows-specific test failures - test_manifest: use platform-appropriate absolute path (C:\ on Windows vs /tmp on POSIX) since /tmp is not absolute on Windows - test_extensions: add agent_scripts.ps entry and platform-conditional assertions for codex skill fallback variant test - test_timestamp_branches: use json.dumps() instead of f-string to properly escape Windows backslash paths in feature.json * test: extract requires_bash marker and fix PS test skip Address PR review feedback: - Define a reusable requires_bash marker in conftest.py and use it across all 3 test files instead of repeating the skipif inline - Move test_powershell_scanner_uses_long_tryparse_for_large_prefixes into its own TestSequentialBranchPowerShell class so it is not incorrectly skipped on Windows by the class-level bash marker * test: use runtime bash check instead of platform check Replace sys.platform == 'win32' with an actual bash invocation test to handle environments where bash exists but is non-functional (e.g., WSL stub on Windows without an installed distro). * test: reject WSL bash, accept only MSYS/MINGW on Windows On Windows, verify uname -s reports MSYS, MINGW, or CYGWIN so the WSL launcher (System32\bash.exe) is rejected — it cannot handle native Windows paths used by test fixtures. Add SPECKIT_TEST_BASH=1 env var escape hatch to force-enable bash tests in non-standard setups. * ci: add comment explaining Windows bash test behavior * test: early-reject WSL launcher, fix remaining f-string JSON - Check resolved bash path for System32 before spawning any subprocess to avoid WSL init prompts and timeout during test collection - Convert remaining feature_json f-string writes to json.dumps() so paths with backslashes produce valid JSON on Windows * test: use bare 'bash' for detection to match test invocation On Windows, subprocess.run(['bash', ...]) uses CreateProcess which searches System32 before PATH — finding WSL bash even when shutil.which('bash') returns Git-for-Windows. Probe with bare 'bash' (same as test helpers) so the detection matches actual test behavior.
267 lines
9.1 KiB
Python
267 lines
9.1 KiB
Python
"""
|
|
Tests for Cursor .mdc frontmatter generation (issue #669).
|
|
|
|
Verifies that update-agent-context.sh properly prepends YAML frontmatter
|
|
to .mdc files so that Cursor IDE auto-includes the rules.
|
|
"""
|
|
|
|
import os
|
|
import shutil
|
|
import subprocess
|
|
import textwrap
|
|
|
|
import pytest
|
|
|
|
from tests.conftest import requires_bash
|
|
|
|
SCRIPT_PATH = os.path.join(
|
|
os.path.dirname(__file__),
|
|
os.pardir,
|
|
"scripts",
|
|
"bash",
|
|
"update-agent-context.sh",
|
|
)
|
|
|
|
EXPECTED_FRONTMATTER_LINES = [
|
|
"---",
|
|
"description: Project Development Guidelines",
|
|
'globs: ["**/*"]',
|
|
"alwaysApply: true",
|
|
"---",
|
|
]
|
|
|
|
requires_git = pytest.mark.skipif(
|
|
shutil.which("git") is None,
|
|
reason="git is not installed",
|
|
)
|
|
|
|
|
|
class TestScriptFrontmatterPattern:
|
|
"""Static analysis — no git required."""
|
|
|
|
def test_create_new_has_mdc_frontmatter_logic(self):
|
|
"""create_new_agent_file() must contain .mdc frontmatter logic."""
|
|
with open(SCRIPT_PATH, encoding="utf-8") as f:
|
|
content = f.read()
|
|
assert 'if [[ "$target_file" == *.mdc ]]' in content
|
|
assert "alwaysApply: true" in content
|
|
|
|
def test_update_existing_has_mdc_frontmatter_logic(self):
|
|
"""update_existing_agent_file() must also handle .mdc frontmatter."""
|
|
with open(SCRIPT_PATH, encoding="utf-8") as f:
|
|
content = f.read()
|
|
# There should be two occurrences of the .mdc check — one per function
|
|
occurrences = content.count('if [[ "$target_file" == *.mdc ]]')
|
|
assert occurrences >= 2, (
|
|
f"Expected at least 2 .mdc frontmatter checks, found {occurrences}"
|
|
)
|
|
|
|
def test_powershell_script_has_mdc_frontmatter_logic(self):
|
|
"""PowerShell script must also handle .mdc frontmatter."""
|
|
ps_path = os.path.join(
|
|
os.path.dirname(__file__),
|
|
os.pardir,
|
|
"scripts",
|
|
"powershell",
|
|
"update-agent-context.ps1",
|
|
)
|
|
with open(ps_path, encoding="utf-8") as f:
|
|
content = f.read()
|
|
assert "alwaysApply: true" in content
|
|
occurrences = content.count(r"\.mdc$")
|
|
assert occurrences >= 2, (
|
|
f"Expected at least 2 .mdc frontmatter checks in PS script, found {occurrences}"
|
|
)
|
|
|
|
|
|
@requires_git
|
|
@requires_bash
|
|
class TestCursorFrontmatterIntegration:
|
|
"""Integration tests using a real git repo."""
|
|
|
|
@pytest.fixture
|
|
def git_repo(self, tmp_path):
|
|
"""Create a minimal git repo with the spec-kit structure."""
|
|
repo = tmp_path / "repo"
|
|
repo.mkdir()
|
|
|
|
# Init git repo
|
|
subprocess.run(
|
|
["git", "init"], cwd=str(repo), capture_output=True, check=True
|
|
)
|
|
subprocess.run(
|
|
["git", "config", "user.email", "test@test.com"],
|
|
cwd=str(repo),
|
|
capture_output=True,
|
|
check=True,
|
|
)
|
|
subprocess.run(
|
|
["git", "config", "user.name", "Test"],
|
|
cwd=str(repo),
|
|
capture_output=True,
|
|
check=True,
|
|
)
|
|
|
|
# Create .specify dir with config
|
|
specify_dir = repo / ".specify"
|
|
specify_dir.mkdir()
|
|
(specify_dir / "config.yaml").write_text(
|
|
textwrap.dedent("""\
|
|
project_type: webapp
|
|
language: python
|
|
framework: fastapi
|
|
database: N/A
|
|
""")
|
|
)
|
|
|
|
# Create template
|
|
templates_dir = specify_dir / "templates"
|
|
templates_dir.mkdir()
|
|
(templates_dir / "agent-file-template.md").write_text(
|
|
"# [PROJECT NAME] Development Guidelines\n\n"
|
|
"Auto-generated from all feature plans. Last updated: [DATE]\n\n"
|
|
"## Active Technologies\n\n"
|
|
"[EXTRACTED FROM ALL PLAN.MD FILES]\n\n"
|
|
"## Project Structure\n\n"
|
|
"[ACTUAL STRUCTURE FROM PLANS]\n\n"
|
|
"## Development Commands\n\n"
|
|
"[ONLY COMMANDS FOR ACTIVE TECHNOLOGIES]\n\n"
|
|
"## Coding Conventions\n\n"
|
|
"[LANGUAGE-SPECIFIC, ONLY FOR LANGUAGES IN USE]\n\n"
|
|
"## Recent Changes\n\n"
|
|
"[LAST 3 FEATURES AND WHAT THEY ADDED]\n"
|
|
)
|
|
|
|
# Create initial commit
|
|
subprocess.run(
|
|
["git", "add", "-A"], cwd=str(repo), capture_output=True, check=True
|
|
)
|
|
subprocess.run(
|
|
["git", "commit", "-m", "init"],
|
|
cwd=str(repo),
|
|
capture_output=True,
|
|
check=True,
|
|
)
|
|
|
|
# Create a feature branch so CURRENT_BRANCH detection works
|
|
subprocess.run(
|
|
["git", "checkout", "-b", "001-test-feature"],
|
|
cwd=str(repo),
|
|
capture_output=True,
|
|
check=True,
|
|
)
|
|
|
|
# Create a spec so the script detects the feature
|
|
spec_dir = repo / "specs" / "001-test-feature"
|
|
spec_dir.mkdir(parents=True)
|
|
(spec_dir / "plan.md").write_text(
|
|
"# Test Feature Plan\n\n"
|
|
"## Technology Stack\n\n"
|
|
"- Language: Python\n"
|
|
"- Framework: FastAPI\n"
|
|
)
|
|
|
|
return repo
|
|
|
|
def _run_update(self, repo, agent_type="cursor-agent"):
|
|
"""Run update-agent-context.sh for a specific agent type."""
|
|
script = os.path.abspath(SCRIPT_PATH)
|
|
result = subprocess.run(
|
|
["bash", script, agent_type],
|
|
cwd=str(repo),
|
|
capture_output=True,
|
|
text=True,
|
|
timeout=30,
|
|
)
|
|
return result
|
|
|
|
def test_new_mdc_file_has_frontmatter(self, git_repo):
|
|
"""Creating a new .mdc file must include YAML frontmatter."""
|
|
result = self._run_update(git_repo)
|
|
assert result.returncode == 0, f"Script failed: {result.stderr}"
|
|
|
|
mdc_file = git_repo / ".cursor" / "rules" / "specify-rules.mdc"
|
|
assert mdc_file.exists(), "Cursor .mdc file was not created"
|
|
|
|
content = mdc_file.read_text()
|
|
lines = content.splitlines()
|
|
|
|
# First line must be the opening ---
|
|
assert lines[0] == "---", f"Expected frontmatter start, got: {lines[0]}"
|
|
|
|
# Check all frontmatter lines are present
|
|
for expected in EXPECTED_FRONTMATTER_LINES:
|
|
assert expected in content, f"Missing frontmatter line: {expected}"
|
|
|
|
# Content after frontmatter should be the template content
|
|
assert "Development Guidelines" in content
|
|
|
|
def test_existing_mdc_without_frontmatter_gets_it_added(self, git_repo):
|
|
"""Updating an existing .mdc file that lacks frontmatter must add it."""
|
|
# First, create the file WITHOUT frontmatter (simulating pre-fix state)
|
|
cursor_dir = git_repo / ".cursor" / "rules"
|
|
cursor_dir.mkdir(parents=True, exist_ok=True)
|
|
mdc_file = cursor_dir / "specify-rules.mdc"
|
|
mdc_file.write_text(
|
|
"# repo Development Guidelines\n\n"
|
|
"Auto-generated from all feature plans. Last updated: 2025-01-01\n\n"
|
|
"## Active Technologies\n\n"
|
|
"- Python + FastAPI (main)\n\n"
|
|
"## Recent Changes\n\n"
|
|
"- main: Added Python + FastAPI\n"
|
|
)
|
|
|
|
result = self._run_update(git_repo)
|
|
assert result.returncode == 0, f"Script failed: {result.stderr}"
|
|
|
|
content = mdc_file.read_text()
|
|
lines = content.splitlines()
|
|
|
|
assert lines[0] == "---", f"Expected frontmatter start, got: {lines[0]}"
|
|
for expected in EXPECTED_FRONTMATTER_LINES:
|
|
assert expected in content, f"Missing frontmatter line: {expected}"
|
|
|
|
def test_existing_mdc_with_frontmatter_not_duplicated(self, git_repo):
|
|
"""Updating an .mdc file that already has frontmatter must not duplicate it."""
|
|
cursor_dir = git_repo / ".cursor" / "rules"
|
|
cursor_dir.mkdir(parents=True, exist_ok=True)
|
|
mdc_file = cursor_dir / "specify-rules.mdc"
|
|
|
|
frontmatter = (
|
|
"---\n"
|
|
"description: Project Development Guidelines\n"
|
|
'globs: ["**/*"]\n'
|
|
"alwaysApply: true\n"
|
|
"---\n\n"
|
|
)
|
|
body = (
|
|
"# repo Development Guidelines\n\n"
|
|
"Auto-generated from all feature plans. Last updated: 2025-01-01\n\n"
|
|
"## Active Technologies\n\n"
|
|
"- Python + FastAPI (main)\n\n"
|
|
"## Recent Changes\n\n"
|
|
"- main: Added Python + FastAPI\n"
|
|
)
|
|
mdc_file.write_text(frontmatter + body)
|
|
|
|
result = self._run_update(git_repo)
|
|
assert result.returncode == 0, f"Script failed: {result.stderr}"
|
|
|
|
content = mdc_file.read_text()
|
|
# Count occurrences of the frontmatter delimiter
|
|
assert content.count("alwaysApply: true") == 1, (
|
|
"Frontmatter was duplicated"
|
|
)
|
|
|
|
def test_non_mdc_file_has_no_frontmatter(self, git_repo):
|
|
"""Non-.mdc agent files (e.g., Claude) must NOT get frontmatter."""
|
|
result = self._run_update(git_repo, agent_type="claude")
|
|
assert result.returncode == 0, f"Script failed: {result.stderr}"
|
|
|
|
claude_file = git_repo / ".claude" / "CLAUDE.md"
|
|
if claude_file.exists():
|
|
content = claude_file.read_text()
|
|
assert not content.startswith("---"), (
|
|
"Non-mdc file should not have frontmatter"
|
|
)
|