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>
69 lines
2.6 KiB
Python
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")) == []
|