* feat(spec): add project-template skill init and check flow * refactor(spec): remove repo-init migrate references to project-template * test(spec): structural tests for project-template skill and command * fix(test): wrap long lines and use non_templated var in project template tests * fix(ci): reformat test_project_template_skill.py for ruff format --------- Co-authored-by: opencode-agent <agent@opencode.local>
203 lines
8.7 KiB
Python
203 lines
8.7 KiB
Python
"""Structural tests for the project-template skill and command.
|
|
|
|
Validates acceptance criteria from issue #230:
|
|
- SKILL.md exists with init + check flow
|
|
- command exists with agent: build
|
|
- Phase A (GitHub remote + branch protection) migrated from repo-init
|
|
- Phase B replaced by cookiecutter
|
|
- repo-init skill removed
|
|
- references to repo-init updated in spec/SKILL.md, add-skill/SKILL.md,
|
|
repo-readme/SKILL.md
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
REPO_ROOT = Path(__file__).resolve().parent.parent
|
|
SKILLS_DIR = REPO_ROOT / ".opencode" / "skills"
|
|
COMMANDS_DIR = REPO_ROOT / ".opencode" / "commands"
|
|
|
|
SKILL_PATH = SKILLS_DIR / "project-template" / "SKILL.md"
|
|
COMMAND_PATH = COMMANDS_DIR / "project-template.md"
|
|
REPO_INIT_PATH = SKILLS_DIR / "repo-init" / "SKILL.md"
|
|
|
|
|
|
def test_skill_file_exists():
|
|
"""SKILL.md must exist at the canonical path."""
|
|
assert SKILL_PATH.is_file(), f"missing: {SKILL_PATH}"
|
|
|
|
|
|
def test_command_file_exists():
|
|
"""Command file must exist at the canonical path."""
|
|
assert COMMAND_PATH.is_file(), f"missing: {COMMAND_PATH}"
|
|
|
|
|
|
def test_repo_init_removed():
|
|
"""repo-init skill must be removed (acceptance criterion)."""
|
|
assert not REPO_INIT_PATH.exists(), f"repo-init still present: {REPO_INIT_PATH}"
|
|
assert not (SKILLS_DIR / "repo-init").exists(), "repo-init/ dir still present"
|
|
|
|
|
|
def test_skill_frontmatter():
|
|
"""SKILL.md frontmatter has name + description."""
|
|
text = SKILL_PATH.read_text(encoding="utf-8")
|
|
assert text.startswith("---\n"), "missing frontmatter opening"
|
|
end = text.find("\n---\n", 4)
|
|
assert end != -1, "missing frontmatter closing"
|
|
fm = text[4:end]
|
|
assert "name: project-template" in fm, "frontmatter name missing"
|
|
assert "description:" in fm, "frontmatter description missing"
|
|
|
|
|
|
def test_skill_has_init_and_check_flows():
|
|
"""SKILL.md documents both init and check flows."""
|
|
text = SKILL_PATH.read_text(encoding="utf-8")
|
|
assert "## init flow" in text, "init flow section missing"
|
|
assert "## check flow" in text, "check flow section missing"
|
|
|
|
|
|
def test_skill_init_flow_steps():
|
|
"""init flow references cookiecutter, git, gh repo create, branch protection, project-status."""
|
|
text = SKILL_PATH.read_text(encoding="utf-8")
|
|
assert "cookiecutter" in text, "cookiecutter not referenced"
|
|
assert "git init" in text, "git init not referenced"
|
|
assert "gh repo create" in text, "gh repo create not referenced"
|
|
assert "branch protection" in text.lower() or "Защита ветки" in text, (
|
|
"branch protection missing"
|
|
)
|
|
assert "project-status" in text, "project-status not referenced"
|
|
|
|
|
|
def test_skill_check_flow_steps():
|
|
"""check flow references project-status tool + recommendations + subagents."""
|
|
text = SKILL_PATH.read_text(encoding="utf-8")
|
|
assert "project-status({})" in text, "project-status tool call missing"
|
|
assert "Рекомендации" in text or "Рекомендации:" in text, "recommendations section missing"
|
|
assert "subagent" in text.lower(), "subagent delegation missing"
|
|
|
|
|
|
def test_skill_phase_a_migrated():
|
|
"""Phase A (squash-only merge, branch protection rules) migrated verbatim."""
|
|
text = SKILL_PATH.read_text(encoding="utf-8")
|
|
assert "allow_squash_merge=true" in text, "squash merge setting missing"
|
|
assert "allow_merge_commit=false" in text, "merge commit setting missing"
|
|
assert "allow_rebase_merge=false" in text, "rebase merge setting missing"
|
|
assert "delete_branch_on_merge=true" in text, "delete branch on merge missing"
|
|
assert "squash_merge_commit_title=COMMIT_OR_PR_TITLE" in text, "squash title missing"
|
|
assert "required_status_checks" in text, "required_status_checks rule missing"
|
|
assert "non_fast_forward" in text, "non_fast_forward rule missing"
|
|
assert "refs/heads/main" in text, "main branch ref missing"
|
|
|
|
|
|
def test_skill_phase_b_cookiecutter():
|
|
"""Phase B replaced by cookiecutter (no manual pyproject/biome templates)."""
|
|
text = SKILL_PATH.read_text(encoding="utf-8")
|
|
assert ".opencode/templates/" in text, "templates dir not referenced"
|
|
assert "cookiecutter.json" in text, "cookiecutter.json not referenced"
|
|
assert "use_auth" in text, "use_auth variable missing"
|
|
assert "use_db" in text, "use_db variable missing"
|
|
assert "post_gen_project" in text, "post_gen_project hook not referenced"
|
|
|
|
|
|
def test_skill_boundary_cases():
|
|
"""Boundary cases documented.
|
|
|
|
existing repo, GitHub repo exists, cookiecutter missing, project-status missing.
|
|
"""
|
|
text = SKILL_PATH.read_text(encoding="utf-8")
|
|
assert "Существующий репо" in text or "существующий репо" in text.lower(), (
|
|
"existing repo case missing"
|
|
)
|
|
assert "GitHub repo уже существует" in text or "gh repo view" in text, (
|
|
"existing GitHub repo case missing"
|
|
)
|
|
assert "cookiecutter не установлен" in text, "cookiecutter missing case missing"
|
|
assert "uv tool install cookiecutter" in text, "cookiecutter install instruction missing"
|
|
assert "project-status не найден" in text or "project-status tool не доступен" in text, (
|
|
"project-status missing case missing"
|
|
)
|
|
|
|
|
|
def test_skill_delegates_not_edits():
|
|
"""Skill delegates to subagents (Template INIT/GITHUB/FIX), main agent = orchestrator."""
|
|
text = SKILL_PATH.read_text(encoding="utf-8")
|
|
assert "Template INIT" in text, "Template INIT missing"
|
|
assert "Template GITHUB" in text, "Template GITHUB missing"
|
|
assert "Template FIX" in text, "Template FIX missing"
|
|
assert "оркестратор" in text, "orchestrator role not stated"
|
|
assert "subagent" in text.lower(), "subagent delegation not mentioned"
|
|
|
|
|
|
def test_skill_valid_types_reference():
|
|
"""SKILL.md references VALID_TYPES from spec-status.py."""
|
|
text = SKILL_PATH.read_text(encoding="utf-8")
|
|
for ptype in ("backend", "fullstack", "mcp-server", "cli", "bot", "worker"):
|
|
assert ptype in text, f"project type {ptype} not mentioned"
|
|
|
|
|
|
def test_skill_cookiecutter_types_documented():
|
|
"""SKILL.md documents which types have cookiecutter templates (backend, fullstack, cli)."""
|
|
text = SKILL_PATH.read_text(encoding="utf-8")
|
|
templated = ["backend", "fullstack", "cli"]
|
|
non_templated = ["mcp-server", "bot", "worker"]
|
|
for ptype in templated:
|
|
assert ptype in text, f"templated type {ptype} missing"
|
|
for ptype in non_templated:
|
|
assert ptype in text, f"non-templated type {ptype} missing"
|
|
assert "cookiecutter template" in text.lower() or "cookiecutter templates" in text.lower(), (
|
|
"cookiecutter template availability not documented"
|
|
)
|
|
|
|
|
|
def test_command_agent_build():
|
|
"""Command file has agent: build frontmatter."""
|
|
text = COMMAND_PATH.read_text(encoding="utf-8")
|
|
assert "agent: build" in text, "agent: build missing in command"
|
|
assert 'skill({name: "project-template"})' in text, "skill load instruction missing"
|
|
|
|
|
|
def test_command_no_repo_init_reference():
|
|
"""Command file must not reference repo-init."""
|
|
text = COMMAND_PATH.read_text(encoding="utf-8")
|
|
assert "repo-init" not in text, "command still references repo-init"
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"rel_path",
|
|
[
|
|
".opencode/skills/spec/SKILL.md",
|
|
".opencode/skills/add-skill/SKILL.md",
|
|
".opencode/skills/repo-readme/SKILL.md",
|
|
],
|
|
)
|
|
def test_no_repo_init_references_in_skills(rel_path):
|
|
"""No stale repo-init references in dependent skills (handoff/decisions docs excluded)."""
|
|
path = REPO_ROOT / rel_path
|
|
assert path.is_file(), f"missing: {rel_path}"
|
|
text = path.read_text(encoding="utf-8")
|
|
assert "repo-init" not in text, f"stale repo-init reference in {rel_path}"
|
|
|
|
|
|
def test_spec_skill_references_project_template():
|
|
"""spec/SKILL.md should reference project-template (not repo-init) for scaffolding."""
|
|
text = (REPO_ROOT / ".opencode" / "skills" / "spec" / "SKILL.md").read_text(encoding="utf-8")
|
|
assert "project-template" in text, "spec/SKILL.md does not reference project-template"
|
|
|
|
|
|
def test_add_skill_tree_lists_project_template():
|
|
"""add-skill/SKILL.md skill tree should list project-template (not repo-init)."""
|
|
text = (REPO_ROOT / ".opencode" / "skills" / "add-skill" / "SKILL.md").read_text(
|
|
encoding="utf-8"
|
|
)
|
|
assert "project-template/SKILL.md" in text, "add-skill tree missing project-template"
|
|
|
|
|
|
def test_repo_readme_references_project_template():
|
|
"""repo-readme/SKILL.md section 6 should reference project-template (not repo-init)."""
|
|
text = (REPO_ROOT / ".opencode" / "skills" / "repo-readme" / "SKILL.md").read_text(
|
|
encoding="utf-8"
|
|
)
|
|
assert "project-template" in text, "repo-readme missing project-template reference"
|