mirror of
https://github.com/0xWheatyz/handler.git
synced 2026-08-30 10:36:24 +00:00
b1cdc3d55d
Seven skills now travel with Handler itself and are seeded into the managed skill store when the API boots: gate recovery, the test authorship standard, checkpoint quality, memory discipline, mise-task rules, scheduled-run continuity, and secrets hygiene. They cover the judgment layer the hard gates cannot enforce — the gates check that tests pass, not that an agent responded to a blocked gate sensibly or kept credentials out of logs. Seeding is idempotent by name: existing rows are never touched, so operator edits and enable/disable choices survive every upgrade; deleting a built-in restores it as shipped on the next start (disable is the supported off-switch). Rows are created shared and enabled, so they sync to every worker like any managed skill and remain admin-editable from the dashboard or the mobile app. Seeding failures log and never block the API from serving. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01731mKtVzsfeT4Vi3TvkR48
75 lines
2.6 KiB
Python
75 lines
2.6 KiB
Python
"""Built-in operator skills: seeding is idempotent and respects operator changes."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from handler.builtin_skills import BUILTIN_SKILLS, seed_builtin_skills
|
|
from handler.db import repository as repo
|
|
|
|
|
|
def test_seed_creates_all_builtins(conn):
|
|
created = seed_builtin_skills(conn)
|
|
assert sorted(created) == sorted(name for name, _, _ in BUILTIN_SKILLS)
|
|
|
|
rows = {s["name"]: s for s in repo.list_claude_skills(conn)}
|
|
for name, description, body in BUILTIN_SKILLS:
|
|
row = rows[name]
|
|
assert row["enabled"] is True
|
|
assert row["owner_user_id"] is None # shared: visible to everyone
|
|
assert row["description"] == description
|
|
assert row["content"] == body
|
|
|
|
|
|
def test_seed_is_idempotent(conn):
|
|
seed_builtin_skills(conn)
|
|
assert seed_builtin_skills(conn) == []
|
|
names = [s["name"] for s in repo.list_claude_skills(conn)]
|
|
assert len(names) == len(set(names))
|
|
|
|
|
|
def test_seed_preserves_operator_edits_and_disables(conn):
|
|
seed_builtin_skills(conn)
|
|
row = repo.get_claude_skill_by_name(conn, "handler-gate-recovery")
|
|
repo.update_claude_skill(conn, row["id"], content="operator version", enabled=False)
|
|
|
|
assert seed_builtin_skills(conn) == []
|
|
after = repo.get_claude_skill_by_name(conn, "handler-gate-recovery")
|
|
assert after["content"] == "operator version"
|
|
assert after["enabled"] is False
|
|
|
|
|
|
def test_seed_restores_deleted_builtin(conn):
|
|
seed_builtin_skills(conn)
|
|
row = repo.get_claude_skill_by_name(conn, "handler-secrets")
|
|
repo.delete_claude_skill(conn, row["id"])
|
|
|
|
assert seed_builtin_skills(conn) == ["handler-secrets"]
|
|
assert repo.get_claude_skill_by_name(conn, "handler-secrets") is not None
|
|
|
|
|
|
def test_builtin_names_are_valid_slugs():
|
|
# The API's skill-name pattern; content synced to workers relies on these being
|
|
# safe directory names.
|
|
import re
|
|
|
|
slug = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._-]*$")
|
|
for name, description, body in BUILTIN_SKILLS:
|
|
assert slug.match(name), name
|
|
assert len(name) <= 64
|
|
assert description.strip()
|
|
assert body.strip()
|
|
|
|
|
|
def test_api_startup_seeds_builtins(env):
|
|
# The lifespan hook fires when the app is entered as a context manager.
|
|
from fastapi.testclient import TestClient
|
|
|
|
from handler.api.app import create_app
|
|
from handler.db.engine import get_engine
|
|
|
|
with TestClient(create_app()):
|
|
pass
|
|
|
|
with get_engine().connect() as conn:
|
|
names = {s["name"] for s in repo.list_claude_skills(conn)}
|
|
assert {name for name, _, _ in BUILTIN_SKILLS} <= names
|