mirror of
https://github.com/github/spec-kit.git
synced 2026-03-17 02:43:08 +00:00
* fix: prepend YAML frontmatter to Cursor .mdc files for auto-inclusion Cursor IDE requires YAML frontmatter with `alwaysApply: true` in .mdc rule files for them to be automatically loaded. Without this frontmatter, users must manually configure glob patterns for the rules to take effect. This fix adds frontmatter generation to both the bash and PowerShell update-agent-context scripts, handling three scenarios: - New .mdc file creation (frontmatter prepended after template processing) - Existing .mdc file update without frontmatter (frontmatter added) - Existing .mdc file with frontmatter (no duplication) Closes #669 🤖 Generated with [Claude Code](https://claude.com/code) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * refactor: address Copilot review suggestions - Handle CRLF line endings in frontmatter detection (grep '^---' instead of '^---$') - Fix double blank line after frontmatter in PowerShell New-AgentFile - Remove unused tempfile import from tests - Add encoding="utf-8" to all open() calls for cross-platform safety Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
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"
|
|
)
|