The skill scans a bot with subagents (never runs it) and writes HANDOVER.md, an 11-section ops guide in the operator's order: where is what, how to run, how long to wait, after the run, how to see an exception, exception list, how you will know, how to tell us, don'ts, issues seen before. Dev material (check against 12 failure classes from Tebra Auto Post, gaps with file:line, incidents) goes to HANDOVER-DEV.md. Also adds ADR-0004, glossary terms, five fixture bots and a handoff eval runner (--resume, --regrade) with results from 47 runs of the earlier 13-section version. The grader still expects that structure and must be updated before the next eval run. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
63 lines
2.8 KiB
Python
63 lines
2.8 KiB
Python
"""Python templates must stay valid Python before their placeholders are filled."""
|
|
import re
|
|
import shutil
|
|
import subprocess
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
HOOK_RULES = "E9,F63,F7,F82,F811" # same rule set as the PostToolUse lint hook
|
|
|
|
|
|
@pytest.mark.parametrize("template", ["skills/rpa-dashboard-no-ai/templates/bridge.py",
|
|
"skills/rpa-dashboard-no-ai/templates/bot_runner.py",
|
|
"skills/rpa-dashboard-ai/templates/desktop/ai_bridge.py"])
|
|
def test_template_passes_lint_hook_unfilled(template):
|
|
ruff = shutil.which("ruff") or pytest.skip("ruff not on PATH")
|
|
r = subprocess.run([ruff, "check", "--select", HOOK_RULES, "--no-cache", str(ROOT / template)],
|
|
capture_output=True, text=True)
|
|
assert r.returncode == 0, r.stdout
|
|
|
|
|
|
def test_tebra_sky_preset_is_the_template_default():
|
|
templates = ROOT / "skills" / "rpa-dashboard-no-ai" / "templates"
|
|
themes = (templates / "themes.md").read_text(encoding="utf-8")
|
|
sky = themes.split("## 1. Tebra Sky")[1].split("```css\n")[1].split("```")[0]
|
|
assert sky in (templates / "dashboard.html").read_text(encoding="utf-8")
|
|
|
|
|
|
@pytest.mark.parametrize("skill", ["rpa-dashboard-no-ai", "rpa-dashboard-ai", "rpa-bot-handoff"])
|
|
def test_skill_description_fits_the_1024_char_limit(skill):
|
|
text = (ROOT / "skills" / skill / "SKILL.md").read_text(encoding="utf-8")
|
|
description = re.search(r"^description: (.+)$", text, re.MULTILINE)[1]
|
|
assert len(description) <= 1024, len(description)
|
|
|
|
|
|
def test_ai_skill_references_files_that_exist():
|
|
skill = ROOT / "skills" / "rpa-dashboard-ai"
|
|
text = (skill / "SKILL.md").read_text(encoding="utf-8")
|
|
paths = re.findall(r"`((?:templates/|<no-ai>)[\w./-]+)`", text)
|
|
assert len(paths) > 5
|
|
missing = [p for p in paths
|
|
if not (skill / p.replace("<no-ai>", "../rpa-dashboard-no-ai/templates/")).exists()]
|
|
assert not missing
|
|
|
|
|
|
def test_handoff_skill_references_files_that_exist():
|
|
skill = ROOT / "skills" / "rpa-bot-handoff"
|
|
paths = re.findall(r"`(templates/[\w./-]+)`", (skill / "SKILL.md").read_text(encoding="utf-8"))
|
|
assert len(paths) >= 3
|
|
assert not [p for p in paths if not (skill / p).exists()]
|
|
|
|
|
|
def test_handoff_lists_twelve_failure_classes():
|
|
skill = ROOT / "skills" / "rpa-bot-handoff" / "templates"
|
|
classes = (skill / "failure-classes.md").read_text(encoding="utf-8")
|
|
assert len(re.findall(r"^## F\d+\.", classes, re.MULTILINE)) == 12
|
|
|
|
|
|
def test_handoff_ops_guide_has_eleven_sections_in_order():
|
|
guide = (ROOT / "skills" / "rpa-bot-handoff" / "templates" / "HANDOVER.md").read_text(encoding="utf-8")
|
|
assert [int(n) for n in re.findall(r"^## (\d+)\.", guide, re.MULTILINE)] == list(range(1, 12))
|