"""Tests for agent frontmatter validation — top-level vs permission-nested fields. Covers issue #48 acceptance criteria: - ``reviewer.md``, ``docs-reviewer.md``, ``memory-syncer.md`` frontmatter does NOT contain a top-level ``doom_loop`` field (outside the ``permission:`` block). A top-level ``doom_loop`` is invalid — it is not in the opencode ``AgentV2.Info`` schema and gets forwarded to the provider as a model param, causing ``AI_APICallError: Extra inputs are not permitted`` on strict providers. - ``doom_loop`` INSIDE the ``permission:`` block IS present in all 3 files (valid — it is the real permission guard). - ``steps: 100`` is present and unchanged in all 3 files. PyYAML is not a project dependency, so frontmatter is parsed manually by splitting on ``---`` delimiters and tracking indentation (top-level fields have no indent; ``permission:``-nested fields have 2-space indent). This mirrors the parsing approach used in ``.opencode/scripts/check-permissions.py``. """ from pathlib import Path REPO_ROOT = Path(__file__).resolve().parent.parent AGENTS_DIR = REPO_ROOT / ".opencode" / "agents" AGENT_FILES = [ AGENTS_DIR / "reviewer.md", AGENTS_DIR / "docs-reviewer.md", AGENTS_DIR / "memory-syncer.md", ] def _parse_frontmatter(filepath: Path) -> dict: """Parse YAML frontmatter into a nested dict (manual, no pyyaml). Returns a dict where top-level keys map to either a scalar string or a nested dict (for indented blocks like ``permission:``). """ content = filepath.read_text() parts = content.split("---", 2) assert len(parts) >= 3, f"{filepath.name}: no frontmatter block found" frontmatter = parts[1] return _parse_yaml_block(frontmatter) def _parse_yaml_block(text: str) -> dict: """Minimal YAML parser for the subset used in agent frontmatter. Handles: top-level ``key: value``, nested ``key:`` blocks (indented), and nested ``key: value`` (indented). Values are strings; nested blocks become dicts. Does NOT handle lists, quoting, or complex YAML — agent frontmatter only uses the simple key-value subset. """ result: dict = {} current_dict: dict | None = None for line in text.split("\n"): if not line.strip() or line.strip().startswith("#"): continue stripped = line.lstrip() indent = len(line) - len(stripped) if indent == 0: current_dict = None if ":" in stripped: key, _, value = stripped.partition(":") key = key.strip() value = value.strip() if value: result[key] = value else: current_dict = {} result[key] = current_dict elif current_dict is not None and indent >= 2: if ":" in stripped: key, _, value = stripped.partition(":") key = key.strip() value = value.strip() if value: current_dict[key] = value return result # ── all agent files exist ─────────────────────────────────────────────────── def test_agent_files_exist(): """All 3 agent files exist in .opencode/agents/.""" for f in AGENT_FILES: assert f.exists(), f"agent file missing: {f}" # ── no top-level doom_loop (issue #48 — the invalid field) ────────────────── def test_no_top_level_doom_loop(): """Frontmatter of all 3 agents has NO top-level ``doom_loop`` key. A top-level ``doom_loop`` is invalid — outside ``permission:`` it is not a recognised agent field and gets forwarded to the provider. """ for f in AGENT_FILES: fm = _parse_frontmatter(f) assert "doom_loop" not in fm, ( f"{f.name}: top-level 'doom_loop' field must be removed — it is " "invalid outside the permission: block" ) def test_no_top_level_doom_loop_raw(): """Raw text check: no line ``doom_loop:`` at indent 0 in any agent file. Belt-and-suspenders alongside the parsed check — catches edge cases where the parser might miss something. """ for f in AGENT_FILES: content = f.read_text() parts = content.split("---", 2) assert len(parts) >= 3, f"{f.name}: no frontmatter" fm = parts[1] for line in fm.split("\n"): if line.startswith("doom_loop:"): raise AssertionError( f"{f.name}: top-level 'doom_loop:' line found at indent 0 — " f"must be removed: {line!r}" ) # ── permission.doom_loop present (the valid guard) ────────────────────────── def test_permission_doom_loop_present(): """``doom_loop: deny`` INSIDE ``permission:`` is present in all 3 files. This is the valid permission guard — it must NOT be removed. """ for f in AGENT_FILES: fm = _parse_frontmatter(f) assert "permission" in fm, f"{f.name}: no 'permission' block" permission = fm["permission"] assert isinstance(permission, dict), f"{f.name}: 'permission' is not a block" assert "doom_loop" in permission, ( f"{f.name}: 'doom_loop' missing from permission: block — must be kept" ) assert permission["doom_loop"] == "deny", ( f"{f.name}: permission.doom_loop must be 'deny', got {permission['doom_loop']!r}" ) # ── steps: 100 present and unchanged ───────────────────────────────────────── def test_steps_100_present(): """``steps: 100`` is present in all 3 agent files (must not be changed).""" for f in AGENT_FILES: fm = _parse_frontmatter(f) assert "steps" in fm, f"{f.name}: 'steps' field missing" assert fm["steps"] == "100", f"{f.name}: steps must be '100', got {fm['steps']!r}" # ── frontmatter is well-formed (parses without error) ─────────────────────── def test_frontmatter_parseable(): """All 3 agent files have parseable frontmatter (split by ---).""" for f in AGENT_FILES: content = f.read_text() parts = content.split("---", 2) assert len(parts) >= 3, f"{f.name}: frontmatter not delimited by ---" fm = _parse_frontmatter(f) assert "description" in fm, f"{f.name}: 'description' field missing" assert "mode" in fm, f"{f.name}: 'mode' field missing" if __name__ == "__main__": import pytest pytest.main([__file__, "-v"])