Files
retroSoC/tests/test_agent_skills.py
T
maksyuki 649758fcdf feat: add references and improve documentation for retroSoC skills
- Introduced a new maintenance reference for dependency and environment routing.
- Enhanced the feature implementation skill with migration references for behavior-preserving changes.
- Added regression and RTL migration references to support feature review processes.
- Created a new physical flow skill with detailed execution and recovery guidelines.
- Updated evals for feature implementation and review skills to include new scenarios.
- Improved the AGENTS.md and tests to reflect the new skills and references.
2026-09-30 15:08:29 +08:00

227 lines
9.9 KiB
Python

from __future__ import annotations
import json
import re
import shlex
from pathlib import Path
from urllib.parse import unquote, urlsplit
ROOT = Path(__file__).resolve().parents[1]
AGENTS_ROOT = ROOT / ".agents"
SKILLS_ROOT = AGENTS_ROOT / "skills"
FEATURE_SKILLS = {
"retrosoc-feature-design",
"retrosoc-feature-implementation",
"retrosoc-feature-review",
}
ENGINEERING_SKILLS = {
"retrosoc-datasheet",
"retrosoc-dependency-maintenance",
"retrosoc-physical-flow",
}
EXPECTED_SKILLS = FEATURE_SKILLS | ENGINEERING_SKILLS
ENGINEERING_DEFAULTS = {
"retrosoc-datasheet": ("inspect", {"Target SoCs"}),
"retrosoc-dependency-maintenance": ("inspect", {"Scope"}),
"retrosoc-physical-flow": ("preflight", {"Target SoCs", "Profile", "Flow", "Stage"}),
}
HANDBOOKS = ("feature-development-prompts.md", "engineering-workflow-prompts.md")
def skill_directories() -> list[Path]:
return sorted(path.parent for path in SKILLS_ROOT.glob("*/SKILL.md"))
def load_skill(path: Path) -> tuple[dict[str, str], str]:
# These entrypoints use single-line name/description fields. The skill
# creator's quick_validate.py additionally validates the full YAML format.
text = path.read_text(encoding="utf-8")
assert text.startswith("---\n")
frontmatter, separator, body = text[4:].partition("\n---\n")
assert separator
metadata: dict[str, str] = {}
for line in frontmatter.splitlines():
if not line.strip():
continue
key, separator, value = line.partition(":")
assert separator and key in {"name", "description"}
assert key not in metadata, f"duplicate field: {path}: {key}"
metadata[key] = value.strip()
assert set(metadata) == {"name", "description"}
return metadata, body
def test_repository_has_feature_and_engineering_skills() -> None:
assert FEATURE_SKILLS.isdisjoint(ENGINEERING_SKILLS)
assert {path.name for path in skill_directories()} == EXPECTED_SKILLS
def test_skill_metadata_is_discoverable() -> None:
names: list[str] = []
for directory in skill_directories():
metadata, body = load_skill(directory / "SKILL.md")
assert metadata["name"] == directory.name
assert re.fullmatch(r"[a-z0-9]+(?:-[a-z0-9]+)*", metadata["name"])
assert len(metadata["name"]) < 64
description = metadata["description"]
assert "retroSoC" in description
assert 0 < len(description) <= 1024
assert body.strip() and len(body.splitlines()) < 500
names.append(metadata["name"])
assert len(names) == len(set(names))
def test_local_markdown_links_resolve() -> None:
for source in AGENTS_ROOT.rglob("*.md"):
body = source.read_text(encoding="utf-8")
for target in re.findall(r"\[[^\]\n]+\]\(([^)\n]+)\)", body):
target = target.strip().strip("<>")
parsed = urlsplit(target)
if parsed.scheme or not parsed.path:
continue
destination = (source.parent / unquote(parsed.path)).resolve()
assert destination.is_relative_to(ROOT), (source, target)
assert destination.is_file(), (source, target)
def test_openai_metadata_routes_to_the_registered_skill() -> None:
for directory in skill_directories():
document = (directory / "agents/openai.yaml").read_text(encoding="utf-8")
assert document.startswith("interface:\n")
for field in ("display_name", "short_description", "default_prompt"):
assert re.search(rf'^ {field}: "[^\"]+', document, re.MULTILINE)
short = re.search(r'^ short_description: "([^\"]+)"', document, re.MULTILINE)
assert short is not None and 25 <= len(short.group(1)) <= 64
prompt = re.search(r'^ default_prompt: "([^\"]+)"', document, re.MULTILINE)
assert prompt is not None
assert f"${directory.name}" in prompt.group(1)
if directory.name in FEATURE_SKILLS:
assert "Target SoCs:" in prompt.group(1)
assert "Feature slug:" in prompt.group(1)
assert "Stage:" in prompt.group(1) or "Mode:" in prompt.group(1)
else:
mode, fields = ENGINEERING_DEFAULTS[directory.name]
assert re.search(rf"\bMode:\s*{mode}\b", prompt.group(1))
for field in fields:
assert f"{field}:" in prompt.group(1), (directory, field)
assert "Feature slug:" not in prompt.group(1)
assert "Phase:" not in prompt.group(1)
assert re.search(r"^ allow_implicit_invocation: true$", document, re.MULTILINE)
def test_eval_corpora_have_valid_cases_and_fixtures() -> None:
for directory in skill_directories():
document = json.loads((directory / "evals/evals.json").read_text(encoding="utf-8"))
assert document["skill_name"] == directory.name
evals = document["evals"]
assert isinstance(evals, list)
assert len(evals) >= (6 if directory.name in ENGINEERING_SKILLS else 2)
identifiers: set[int] = set()
prompts: set[str] = set()
for case in evals:
assert set(case) == {"id", "prompt", "expected_output", "files"}
assert type(case["id"]) is int and case["id"] > 0
assert case["id"] not in identifiers
identifiers.add(case["id"])
for field in ("prompt", "expected_output"):
assert isinstance(case[field], str) and case[field].strip()
assert case["prompt"] not in prompts
prompts.add(case["prompt"])
assert isinstance(case["files"], list)
for relative in case["files"]:
assert isinstance(relative, str) and relative.strip()
fixture = (directory / "evals" / relative).resolve()
assert fixture.is_relative_to(ROOT) and fixture.is_file()
assert any(len(case["prompt"]) <= 120 for case in evals)
def test_skill_mentions_and_guide_links_resolve() -> None:
sources = [path for path in AGENTS_ROOT.rglob("*") if path.suffix in {".md", ".yaml"}]
sources.extend((ROOT / "docs").rglob("*.md"))
for source in sources:
text = source.read_text(encoding="utf-8")
for name in re.findall(r"[$@](retrosoc-[a-z0-9-]+)", text):
assert (SKILLS_ROOT / name / "SKILL.md").is_file(), (source, name)
guide = (AGENTS_ROOT / "README.md").read_text(encoding="utf-8")
for handbook in HANDBOOKS:
assert (AGENTS_ROOT / handbook).is_file()
assert f"({handbook})" in guide
for name in EXPECTED_SKILLS:
assert f"(skills/{name}/SKILL.md)" in guide
def test_documented_profiles_exist() -> None:
profiles: set[str] = set()
for source in AGENTS_ROOT.rglob("*.md"):
profiles.update(re.findall(r"configs/[a-zA-Z0-9_./-]+\.mk", source.read_text("utf-8")))
assert profiles
for profile in profiles:
assert (ROOT / profile).is_file(), profile
def test_engineering_handbook_covers_each_public_mode() -> None:
handbook = (AGENTS_ROOT / HANDBOOKS[1]).read_text(encoding="utf-8")
prompts = re.findall(r"```text\n(.*?)```", handbook, re.DOTALL)
expected_modes = {
"retrosoc-datasheet": {"inspect", "update", "validate"},
"retrosoc-dependency-maintenance": {"inspect", "restore", "upgrade"},
"retrosoc-physical-flow": {"preflight", "run", "resume", "summarize"},
}
for name, modes in expected_modes.items():
found: set[str] = set()
for prompt in prompts:
if f"${name}" not in prompt:
continue
# Both field-based prompts and direct English actions are public examples.
for mode in modes:
if re.search(rf"(?:Mode:\s*|\${name}\s+){mode}\b", prompt, re.IGNORECASE):
found.add(mode)
assert found == modes, (name, found)
def test_shared_engineering_references_are_reachable() -> None:
implementation = SKILLS_ROOT / "retrosoc-feature-implementation"
review = (SKILLS_ROOT / "retrosoc-feature-review/SKILL.md").read_text("utf-8")
entrypoint = (implementation / "SKILL.md").read_text("utf-8")
for name in ("regression.md", "rtl-migration.md"):
assert (implementation / "references" / name).is_file()
assert f"(references/{name})" in entrypoint
assert f"(../retrosoc-feature-implementation/references/{name})" in review
def test_engineering_command_examples_use_existing_entrypoints() -> None:
sources = [AGENTS_ROOT / HANDBOOKS[1]]
for name in ENGINEERING_SKILLS | {"retrosoc-feature-implementation"}:
sources.extend((SKILLS_ROOT / name / "references").glob("*.md"))
makefiles = [
ROOT / "Makefile",
ROOT / "physical/librelane/mini/Makefile",
ROOT / "physical/librelane/tiny/Makefile",
ROOT / "physical/ecc/Makefile",
]
recipes = "\n".join(path.read_text("utf-8") for path in makefiles)
checked = 0
for source in sources:
for block in re.findall(r"```sh\n(.*?)```", source.read_text("utf-8"), re.DOTALL):
for line in block.replace("\\\n", " ").splitlines():
parts = shlex.split(line, comments=True)
if len(parts) < 2:
continue
if parts[0] in {"python", "python3"} and parts[1].endswith(".py"):
entrypoint = (ROOT / parts[1]).resolve()
assert entrypoint.is_relative_to(ROOT) and entrypoint.is_file(), (source, line)
checked += 1
elif parts[0] == "make":
# These examples use root Make and explicit assignments, not -C or -f.
targets = [part for part in parts[1:] if "=" not in part]
assert targets and all(not part.startswith("-") for part in targets)
for target in targets:
assert re.search(rf"^{re.escape(target)}\s*:", recipes, re.MULTILINE), (
source,
target,
)
checked += 1
assert checked > 0