mirror of
https://github.com/codestable/CodeStable.git
synced 2026-09-19 09:03:09 +08:00
359 lines
12 KiB
Python
359 lines
12 KiB
Python
from __future__ import annotations
|
|
|
|
import importlib.util
|
|
import re
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
SKILLS = ROOT / "plugins/codestable/skills"
|
|
|
|
COMPATIBILITY_ENTRIES = {
|
|
"cs-feat-design": ("cs-feat", "requested_stage", "design"),
|
|
"cs-feat-design-review": ("cs-feat", "requested_stage", "design-review"),
|
|
"cs-feat-impl": ("cs-feat", "requested_stage", "implementation"),
|
|
"cs-feat-qa": ("cs-feat", "requested_stage", "qa"),
|
|
"cs-feat-accept": ("cs-feat", "requested_stage", "acceptance"),
|
|
"cs-feat-ff": ("cs-feat", "requested_mode", "fastforward"),
|
|
"cs-issue-report": ("cs-issue", "requested_stage", "report"),
|
|
"cs-issue-analyze": ("cs-issue", "requested_stage", "analyze"),
|
|
"cs-issue-fix": ("cs-issue", "requested_stage", "fix"),
|
|
"cs-refactor-ff": ("cs-refactor", "requested_mode", "fastforward"),
|
|
"cs-doc-tutorial": ("cs-docs", "requested_mode", "tutorial"),
|
|
"cs-doc-api": ("cs-docs", "requested_mode", "api"),
|
|
"cs-roadmap": ("cs-epic", "requested_stage", "planning"),
|
|
"cs-roadmap-review": ("cs-epic", "requested_stage", "review"),
|
|
"cs-roadmap-impl-goal": ("cs-epic", "requested_stage", "goal-package"),
|
|
}
|
|
|
|
MAIN_REFERENCE_PATHS = [
|
|
"cs-feat/references/design/protocol.md",
|
|
"cs-feat/references/design-review/protocol.md",
|
|
"cs-feat/references/implementation/protocol.md",
|
|
"cs-feat/references/qa/protocol.md",
|
|
"cs-feat/references/acceptance/protocol.md",
|
|
"cs-feat/references/fastforward/protocol.md",
|
|
"cs-feat/references/goal/protocol.md",
|
|
"cs-issue/references/report/protocol.md",
|
|
"cs-issue/references/analyze/protocol.md",
|
|
"cs-issue/references/fix/protocol.md",
|
|
"cs-refactor/references/standard/protocol.md",
|
|
"cs-refactor/references/fastforward/protocol.md",
|
|
"cs-docs/references/tutorial/protocol.md",
|
|
"cs-docs/references/api/protocol.md",
|
|
"cs-epic/references/planning/protocol.md",
|
|
"cs-epic/references/review/protocol.md",
|
|
"cs-epic/references/goal/protocol.md",
|
|
"cs-code-review/references/independent-review/protocol.md",
|
|
]
|
|
|
|
REFACTOR_LIBRARY_PATHS = [
|
|
"cs-refactor/references/library/methods.md",
|
|
"cs-refactor/references/library/methods-l4.md",
|
|
"cs-refactor/references/library/methods-architecture.md",
|
|
"cs-refactor/references/library/refusal-routing.md",
|
|
"cs-refactor/references/library/scan-checklist-format.md",
|
|
]
|
|
|
|
REFERENCE_MIGRATION_MARKERS = [
|
|
"不是独立 skill 入口",
|
|
"SKILL.md 只保留",
|
|
"按 `SKILL.md` 所在目录",
|
|
"同包 `reference.md`",
|
|
"## 启动必读",
|
|
]
|
|
|
|
LEGACY_STAGE_SKILL_NAMES = [
|
|
"cs-feat-design",
|
|
"cs-feat-design-review",
|
|
"cs-feat-impl",
|
|
"cs-feat-qa",
|
|
"cs-feat-accept",
|
|
"cs-feat-ff",
|
|
"cs-issue-report",
|
|
"cs-issue-analyze",
|
|
"cs-issue-fix",
|
|
"cs-doc-tutorial",
|
|
"cs-doc-api",
|
|
"cs-roadmap",
|
|
"cs-roadmap-review",
|
|
"cs-roadmap-impl-goal",
|
|
"cs-refactor-ff",
|
|
]
|
|
|
|
USER_ROUTING_GUIDANCE_PATHS = [
|
|
"cs-brainstorm/SKILL.md",
|
|
"cs-brainstorm/reference.md",
|
|
"cs-req/SKILL.md",
|
|
]
|
|
|
|
LEGACY_ROUTING_PHRASES = [
|
|
"`cs-roadmap`",
|
|
"cs-roadmap",
|
|
"`cs-feat-design`",
|
|
"`cs-feat-impl`",
|
|
"`cs-feat-qa`",
|
|
"`cs-feat-accept`",
|
|
"`cs-doc-tutorial`",
|
|
"`cs-doc-api`",
|
|
"`cs-issue-report`",
|
|
"`cs-issue-analyze`",
|
|
"`cs-issue-fix`",
|
|
"`cs-refactor-ff`",
|
|
]
|
|
|
|
|
|
def load_codestable_common():
|
|
module_path = SKILLS / "cs-onboard/tools/codestable_common.py"
|
|
spec = importlib.util.spec_from_file_location("codestable_common", module_path)
|
|
assert spec and spec.loader
|
|
module = importlib.util.module_from_spec(spec)
|
|
sys.modules[spec.name] = module
|
|
spec.loader.exec_module(module)
|
|
return module
|
|
|
|
|
|
def test_skill_markdown_files_stay_under_line_limit() -> None:
|
|
oversized = []
|
|
for path in SKILLS.rglob("*.md"):
|
|
line_count = len(path.read_text(encoding="utf-8").splitlines())
|
|
if line_count > 300:
|
|
oversized.append((path.relative_to(ROOT).as_posix(), line_count))
|
|
|
|
assert oversized == []
|
|
|
|
|
|
def test_compatibility_entries_delegate_to_main_protocols() -> None:
|
|
for skill, (main, key, value) in COMPATIBILITY_ENTRIES.items():
|
|
path = SKILLS / skill / "SKILL.md"
|
|
text = path.read_text(encoding="utf-8")
|
|
|
|
assert "兼容入口" in text
|
|
assert (SKILLS / main / "SKILL.md").is_file(), main
|
|
assert f"按已安装 skill 名称加载 `{main}`" in text
|
|
assert "../" not in text
|
|
assert f"{key}: {value}" in text
|
|
assert "不维护独立流程规则" in text
|
|
assert len(text.splitlines()) <= 40
|
|
|
|
|
|
MAIN_ENTRY_SKILLS = [
|
|
"cs",
|
|
"cs-feat",
|
|
"cs-issue",
|
|
"cs-refactor",
|
|
"cs-docs",
|
|
"cs-epic",
|
|
"cs-code-review",
|
|
"cs-docs-neat",
|
|
]
|
|
|
|
MAIN_ENTRY_ARGUMENT_HINTS = {
|
|
"cs": "[request]",
|
|
"cs-feat": "[--stage design|design-review|impl|qa|accept|goal-package] [--mode fastforward] <feature>",
|
|
"cs-issue": "[--stage report|analyze|fix] <issue>",
|
|
"cs-refactor": "[--stage scan|design|apply] [--mode standard|fastforward] <target>",
|
|
"cs-docs": "[--mode tutorial|api] <topic>",
|
|
"cs-epic": "[--stage planning|review|goal-package] <epic>",
|
|
"cs-code-review": "[--range <git-range>] [scope]",
|
|
"cs-docs-neat": "[scope]",
|
|
}
|
|
|
|
|
|
def frontmatter_of(text: str) -> str:
|
|
return text.split("---", 2)[1]
|
|
|
|
|
|
def test_main_entries_declare_argument_hint_and_intent_fallback() -> None:
|
|
for skill in MAIN_ENTRY_SKILLS:
|
|
text = (SKILLS / skill / "SKILL.md").read_text(encoding="utf-8")
|
|
frontmatter = frontmatter_of(text)
|
|
|
|
assert f"argument-hint: \"{MAIN_ENTRY_ARGUMENT_HINTS[skill]}\"" in frontmatter, skill
|
|
assert "$ARGUMENTS" in text, skill
|
|
# 宿主不替换参数时必须优雅降级,不能把字面 $ARGUMENTS 当诉求。
|
|
assert "字面 `$ARGUMENTS`" in text, skill
|
|
assert "无参数默认行为" in text, skill
|
|
|
|
|
|
def test_main_entry_argument_hints_use_flags_for_control_intents() -> None:
|
|
flag_entries = {
|
|
"cs-feat": ["--stage", "--mode"],
|
|
"cs-issue": ["--stage"],
|
|
"cs-refactor": ["--stage", "--mode"],
|
|
"cs-docs": ["--mode"],
|
|
"cs-epic": ["--stage"],
|
|
"cs-code-review": ["--range"],
|
|
}
|
|
|
|
for skill, flags in flag_entries.items():
|
|
text = (SKILLS / skill / "SKILL.md").read_text(encoding="utf-8")
|
|
frontmatter = frontmatter_of(text)
|
|
for flag in flags:
|
|
assert flag in frontmatter, skill
|
|
assert "首个 token 命中" not in text, skill
|
|
|
|
|
|
def test_skill_catalog_documents_no_argument_default() -> None:
|
|
zh_text = (ROOT / "SKILL_CATALOG.md").read_text(encoding="utf-8")
|
|
en_text = (ROOT / "SKILL_CATALOG.en.md").read_text(encoding="utf-8")
|
|
|
|
assert "不传参数时按仓库事实和用户原话恢复或路由" in zh_text
|
|
assert "no-argument calls recover or route" in en_text
|
|
|
|
|
|
def test_feat_and_epic_document_goal_driver_dispatch() -> None:
|
|
feat = (SKILLS / "cs-feat/SKILL.md").read_text(encoding="utf-8")
|
|
epic = (SKILLS / "cs-epic/SKILL.md").read_text(encoding="utf-8")
|
|
router = (SKILLS / "cs/SKILL.md").read_text(encoding="utf-8")
|
|
conventions = (SKILLS / "cs-onboard/references/execution-conventions.md").read_text(encoding="utf-8")
|
|
feat_goal = (SKILLS / "cs-feat/references/goal/protocol.md").read_text(encoding="utf-8")
|
|
epic_goal = (SKILLS / "cs-epic/references/goal/protocol.md").read_text(encoding="utf-8")
|
|
|
|
assert "--stage goal-package" in feat
|
|
assert "references/goal/protocol.md" in feat
|
|
assert "goal 包、impl" in router
|
|
assert "可见 driver 长程执行" in router
|
|
assert "Goal driver" in conventions
|
|
assert "可见 Task agent" in conventions
|
|
assert "派发失败" in feat_goal
|
|
assert "派发失败" in epic_goal
|
|
assert "fenced `/goal`" in feat_goal
|
|
assert "fenced `/goal`" in epic_goal
|
|
|
|
|
|
def test_compatibility_entries_do_not_declare_argument_hint() -> None:
|
|
for skill in COMPATIBILITY_ENTRIES:
|
|
text = (SKILLS / skill / "SKILL.md").read_text(encoding="utf-8")
|
|
|
|
assert "argument-hint" not in text, skill
|
|
assert "$ARGUMENTS" not in text, skill
|
|
|
|
|
|
def test_main_entry_references_exist() -> None:
|
|
for rel_path in MAIN_REFERENCE_PATHS:
|
|
assert (SKILLS / rel_path).is_file(), rel_path
|
|
|
|
|
|
def test_protocol_references_are_not_shadow_skills() -> None:
|
|
for rel_path in MAIN_REFERENCE_PATHS:
|
|
path = SKILLS / rel_path
|
|
text = path.read_text(encoding="utf-8")
|
|
head = "\n".join(text.splitlines()[:5])
|
|
|
|
assert text.startswith("# "), rel_path
|
|
assert not text.startswith("---"), rel_path
|
|
assert "\nname:" not in head, rel_path
|
|
assert "\ndescription:" not in head, rel_path
|
|
assert "## 启动必读" not in text, rel_path
|
|
|
|
|
|
def test_references_do_not_keep_migration_markers() -> None:
|
|
offenders = []
|
|
for path in (SKILLS).rglob("references/**/*.md"):
|
|
text = path.read_text(encoding="utf-8")
|
|
head = "\n".join(text.splitlines()[:5])
|
|
if re.search(r"(?m)^name:", head):
|
|
offenders.append((path.relative_to(ROOT).as_posix(), "frontmatter name"))
|
|
for marker in REFERENCE_MIGRATION_MARKERS:
|
|
if marker in text:
|
|
offenders.append((path.relative_to(ROOT).as_posix(), marker))
|
|
|
|
assert offenders == []
|
|
|
|
|
|
def test_references_prefer_main_entries_over_legacy_stage_skills() -> None:
|
|
offenders = []
|
|
for path in (SKILLS).rglob("references/**/*.md"):
|
|
text = path.read_text(encoding="utf-8")
|
|
for legacy_name in LEGACY_STAGE_SKILL_NAMES:
|
|
if legacy_name in text:
|
|
offenders.append((path.relative_to(ROOT).as_posix(), legacy_name))
|
|
|
|
assert offenders == []
|
|
|
|
|
|
def test_user_routing_guidance_prefers_main_entries() -> None:
|
|
offenders = []
|
|
for rel_path in USER_ROUTING_GUIDANCE_PATHS:
|
|
path = SKILLS / rel_path
|
|
text = path.read_text(encoding="utf-8")
|
|
for phrase in LEGACY_ROUTING_PHRASES:
|
|
if phrase in text:
|
|
offenders.append((rel_path, phrase))
|
|
|
|
assert offenders == []
|
|
|
|
|
|
def test_code_review_ad_hoc_git_range_uses_range_diff() -> None:
|
|
text = (SKILLS / "cs-code-review/SKILL.md").read_text(encoding="utf-8")
|
|
|
|
assert "--range <git-range>" in text
|
|
assert "ad-hoc 参数如果含 `--range`" in text
|
|
assert "git diff {range}" in text
|
|
assert "ad-hoc range 审查允许工作区干净" in text
|
|
|
|
|
|
def test_no_directory_contains_both_reference_spellings() -> None:
|
|
conflicts = []
|
|
for directory in [SKILLS, *[path for path in SKILLS.rglob("*") if path.is_dir()]]:
|
|
if not (directory / "reference").is_dir() or not (directory / "references").is_dir():
|
|
continue
|
|
conflicts.append(directory.relative_to(ROOT).as_posix())
|
|
|
|
assert conflicts == []
|
|
|
|
|
|
def test_skill_package_reference_directories_follow_agent_skills_convention() -> None:
|
|
invalid = []
|
|
for directory in [path for path in SKILLS.rglob("*") if path.is_dir()]:
|
|
rel_parts = directory.relative_to(SKILLS).parts
|
|
if directory.name == "reference":
|
|
invalid.append(directory.relative_to(ROOT).as_posix())
|
|
if directory.name == "references" and (len(rel_parts) != 2 or rel_parts[1] != "references"):
|
|
invalid.append(directory.relative_to(ROOT).as_posix())
|
|
|
|
assert invalid == []
|
|
|
|
|
|
def test_main_entry_reference_pointers_resolve_to_files() -> None:
|
|
main_entries = [
|
|
"cs-feat",
|
|
"cs-issue",
|
|
"cs-refactor",
|
|
"cs-docs",
|
|
"cs-epic",
|
|
"cs-code-review",
|
|
"cs-docs-neat",
|
|
]
|
|
missing = []
|
|
|
|
for skill in main_entries:
|
|
skill_dir = SKILLS / skill
|
|
text = (skill_dir / "SKILL.md").read_text(encoding="utf-8")
|
|
for match in re.finditer(r"`(references/[^`]+?\.md)`", text):
|
|
rel_path = match.group(1)
|
|
if "*" in rel_path:
|
|
if not list(skill_dir.glob(rel_path)):
|
|
missing.append(f"{skill}/{rel_path}")
|
|
elif not (skill_dir / rel_path).is_file():
|
|
missing.append(f"{skill}/{rel_path}")
|
|
|
|
assert missing == []
|
|
|
|
|
|
def test_refactor_library_references_are_migrated() -> None:
|
|
assert not (SKILLS / "cs-refactor/reference").exists()
|
|
for rel_path in REFACTOR_LIBRARY_PATHS:
|
|
assert (SKILLS / rel_path).is_file(), rel_path
|
|
|
|
|
|
def test_new_main_entries_are_registered() -> None:
|
|
common = load_codestable_common()
|
|
|
|
assert "cs-docs" in common.KNOWN_SKILL_DIRS
|
|
assert "cs-epic" in common.KNOWN_SKILL_DIRS
|
|
for skill in COMPATIBILITY_ENTRIES:
|
|
assert skill in common.KNOWN_SKILL_DIRS
|