mirror of
https://github.com/github/spec-kit.git
synced 2026-08-15 14:02:04 +02:00
264 lines
9.0 KiB
Python
264 lines
9.0 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
|
||
|
|
|
||
|
|
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
|
||
|
|
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"
|
||
|
|
)
|