Files
ai-agent/sidecar/test_skills_list.py
Gabriel Vidal a698f5b929 feat(composer): guideline chips and skill mentions follow "Runs on"
Prompt shortcuts are now shared across runners or scoped to one: `shared` +
`runner` ("local" = the lab, or a worker id). The composer resolves the
runner its prompt lands on (the pick, else the account's online worker, else
the lab) and offers the shared chips first, then that runner's; switching
runner seeds the newly revealed chips' default-on cascade. The resume box
uses the session's own runner. Settings → Prompt shortcuts becomes a Shared
section plus one tab per runner (Lab, each paired worker), with a Shared
checkbox per card and reordering kept within a section. Settings v3
migrates existing configs: worktree, commit, pr, notify, ask, clarify and
tasks become shared; everything else stays the lab's.

A worker run also lists that machine's skills instead of the lab catalog:
the sidecar's new GET /skills reports the default cwd's and the user-level
skills, the hub proxies it at /api/workers/{id}/skills (60 s cache, empty
when offline), and the composer swaps its mentions when the runner is a
worker (no lab agents there either).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-06 16:32:47 +02:00

69 lines
2.6 KiB
Python

"""/skills: what a run on this runner can use — project then user skills."""
import pytest
from fastapi import FastAPI, HTTPException
from fastapi.testclient import TestClient
import skills_list
TOKEN = "t0ken"
def _auth(h):
if h != f"Bearer {TOKEN}":
raise HTTPException(401, "unauthorized")
def _skill(root, name, fm):
d = root / name
d.mkdir(parents=True)
(d / "SKILL.md").write_text(fm)
@pytest.fixture()
def dirs(tmp_path):
cfg, cwd = tmp_path / "cfg", tmp_path / "repo"
_skill(cfg / "skills", "notify", "---\nname: notify\ndescription: push: ask, form\n"
"trigger_words:\n - notify\n - ping me\n---\n# notify\n")
_skill(cfg / "skills", "shadowed", "---\ndescription: user copy\n---\n")
_skill(cwd / ".claude" / "skills", "shadowed", "---\ndescription: project copy\n---\n")
_skill(cwd / ".claude" / "skills", "plain", "no frontmatter here\n")
(cwd / ".claude" / "skills" / "noskill").mkdir()
return cfg, cwd
def test_lists_project_then_user_and_dedupes(dirs):
cfg, cwd = dirs
out = skills_list.list_skills(skills_list.skill_roots(cfg, cwd))
by = {s["name"]: s for s in out}
assert [s["name"] for s in out] == ["plain", "shadowed", "notify"]
assert by["shadowed"]["description"] == "project copy"
assert by["notify"]["description"] == "push: ask, form" # colon-safe
assert by["notify"]["triggerWords"] == ["notify", "ping me"]
assert by["plain"] == {"name": "plain", "title": "plain", "description": "",
"triggerWords": [], "source": "project",
"path": str(cwd / ".claude/skills/plain/SKILL.md")}
assert by["notify"]["source"] == "user"
def test_follows_a_linked_skill_dir(tmp_path, dirs):
cfg, cwd = dirs
real = tmp_path / "elsewhere" / "linked"
_skill(tmp_path / "elsewhere", "linked", "---\ndescription: via symlink\n---\n")
(cfg / "skills" / "linked").symlink_to(real, target_is_directory=True)
names = [s["name"] for s in skills_list.list_skills(skills_list.skill_roots(cfg, cwd))]
assert "linked" in names
def test_router_needs_the_bearer(dirs):
cfg, cwd = dirs
app = FastAPI()
app.include_router(skills_list.make_router(_auth, lambda: skills_list.skill_roots(cfg, cwd)))
assert TestClient(app).get("/skills").status_code == 401
r = TestClient(app, headers={"Authorization": f"Bearer {TOKEN}"}).get("/skills")
assert r.status_code == 200 and r.json()["count"] == 3
def test_missing_roots_are_empty(tmp_path):
assert skills_list.list_skills(skills_list.skill_roots(tmp_path / "nope", tmp_path / "nor")) == []