opencode-config/tests/test_project_template_skill.py
Sergey b3aac737eb
feat(spec): project template skill init and check flow (#237)
* 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>
2026-08-03 17:37:55 +03:00

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"