Files
claude/lib/tests/doctor-vendored.test.sh
T
bastien 18f8c898f8 feat(superpowers): vendor the 7 wired skills at v6.4.1, drop the plugin
plugins.lock.json gains a superpowers entry (obra/superpowers @ 5bf4e78,
path skills, per-skill file lists, always_on) that lib/vendor-skills.sh
fetches byte-for-byte: brainstorming, writing-plans,
subagent-driven-development, test-driven-development,
requesting-code-review, using-git-worktrees, writing-skills. install-plugins
STEP 8e vendors it, update-all refreshes it at the pin, link.sh links the
seven, .gitignore ignores them. The plugin is no longer installed or
protected: its 8 other skills duplicated personal flows and its
SessionStart injection cost ~900 tokens per start, clear and compact.
detect_superpowers is one file test on the linked skill; doctor and
session-start stop charging the injection. doctor-vendored gains an
always_on class (third lock column) so always-on externals are
link-checked instead of reported parked.
2026-09-28 14:54:52 +02:00

236 lines
8.9 KiB
Bash

#!/usr/bin/env bash
# lib/tests/doctor-vendored.test.sh — lib/doctor-vendored.sh's
# check_vendored_skills(): a fixture repo under mktemp with a fake
# plugins.lock.json (single-path shape, list shape, dict shape with a
# references/ file), a fake link.sh holding a multi-line
# EXTERNAL_SKILLS=(...) array, a fake <claude_home>/skills dir and a fake
# profile file. Cases: every file present + linked (ALL_PRESENT), a
# list-shape skill missing its SKILL.md (FILE_MISSING), a dict-shape
# skill missing one of two files — only that file is named
# (DICT_FILES_COMPLETE), a profile-listed name with no symlink
# (SYMLINK_MISSING_ACTIVE) or a symlink to the wrong target
# (SYMLINK_WRONG_TARGET), a name absent from the profile reported parked
# rather than failed (SYMLINK_PARKED), the same name treated as
# expected-linked (fail, not parked) when no profile file is passed at
# all (NO_PROFILE_EXPECTS_LINK), an unreadable lock file degrading to a
# warn instead of a fail (LOCK_UNREADABLE, rc 0), a lock entry whose
# "skills" is neither null/list/dict degrading the same way with no
# Python traceback leaking (LOCK_MALFORMED_ENTRY, rc 0), the
# profile-name allowlist rejecting a path-traversal value
# (REJECTS_BAD_PROFILE_NAME), the item-name allowlist rejecting a
# link.sh entry with a ".." segment — warned and skipped, not failed
# (REJECTS_BAD_NAME), and an "always_on": true lock entry's name, absent
# from the profile and with no symlink, checked (and failed) instead of
# reported parked (ALWAYS_ON_LINK_CHECKED).
set -u
ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
LIB="$ROOT/lib/doctor-vendored.sh"
pass=0; fail=0
check_bool() {
local name="$1" ok="$2"
if [ "$ok" = 1 ]; then pass=$((pass+1)); echo "PASS $name"
else fail=$((fail+1)); echo "FAIL $name"; fi
}
WORK="$(mktemp -d)"; trap 'rm -rf "$WORK"' EXIT
REPO="$WORK/repo"
CLAUDE_HOME="$WORK/claude_home"
mkdir -p "$REPO/skills-external" "$CLAUDE_HOME/skills"
# ── Lock: "ok-skill" mirrors the emil-design-eng single-file "path"
# shape (the key IS the name); "list-entry" mirrors the agent-skills
# bare-list shape (5 names, each defaults to SKILL.md); "dict-entry"
# mirrors the mengto-skills explicit shape (dict-skill needs SKILL.md +
# references/notes.md, and only the latter is ever missing below).
cat > "$REPO/plugins.lock.json" <<'JSON'
{
"ok-skill": {
"path": "skills/ok-skill/SKILL.md",
"managed_by": "curl"
},
"list-entry": {
"managed_by": "curl",
"skills": [
"missing-skill", "active-nolink-skill", "active-wronglink-skill",
"parked-skill", "noprofile-skill"
]
},
"dict-entry": {
"managed_by": "curl",
"skills": {"dict-skill": ["SKILL.md", "references/notes.md"]}
},
"always-on-entry": {
"managed_by": "curl",
"always_on": true,
"skills": ["always-on-skill"]
}
}
JSON
# ── link.sh: a multi-line EXTERNAL_SKILLS array, tolerance-tested.
cat > "$REPO/link.sh" <<'SH'
#!/usr/bin/env bash
EXTERNAL_SKILLS=(ok-skill missing-skill dict-skill
active-nolink-skill active-wronglink-skill
parked-skill noprofile-skill always-on-skill)
SH
# ── skills-external/ tree: every name's SKILL.md present, except
# missing-skill (nothing at all) and dict-skill's references/notes.md.
# always-on-skill has its SKILL.md too — only its symlink is missing.
for n in ok-skill dict-skill active-nolink-skill active-wronglink-skill \
parked-skill noprofile-skill always-on-skill; do
mkdir -p "$REPO/skills-external/$n"
echo "v1" > "$REPO/skills-external/$n/SKILL.md"
done
# ── claude_home symlinks: ok-skill correct, active-wronglink-skill
# points elsewhere, active-nolink-skill, noprofile-skill and
# always-on-skill have none.
ln -sf "$REPO/skills-external/ok-skill" "$CLAUDE_HOME/skills/ok-skill"
mkdir -p "$WORK/elsewhere"
ln -sf "$WORK/elsewhere" "$CLAUDE_HOME/skills/active-wronglink-skill"
# ── active.profile: lists everything EXCEPT parked-skill,
# noprofile-skill and always-on-skill (all three proven absent from it).
cat > "$REPO/active.profile" <<'PROF'
# DESC: fixture profile
ok-skill external
missing-skill external
dict-skill external
active-nolink-skill external
active-wronglink-skill external
PROF
# shellcheck source=../doctor-vendored.sh disable=SC1091
source "$LIB"
# ── With the active profile passed ──────────────────────────────────
out1="$(check_vendored_skills "$REPO" "$CLAUDE_HOME" \
"$REPO/active.profile" 2>&1)"
check_bool ALL_PRESENT \
"$(printf '%s' "$out1" | grep -qF 'ok-skill: vendored + linked' \
&& echo 1 || echo 0)"
check_bool FILE_MISSING \
"$(printf '%s' "$out1" | \
grep -qF 'missing-skill: skills-external/missing-skill/SKILL.md missing' \
&& echo 1 || echo 0)"
check_bool DICT_FILES_COMPLETE \
"$(printf '%s' "$out1" | grep -qF \
'dict-skill: skills-external/dict-skill/references/notes.md missing' \
&& ! printf '%s' "$out1" | grep -qF \
'dict-skill: skills-external/dict-skill/SKILL.md missing' \
&& echo 1 || echo 0)"
check_bool SYMLINK_MISSING_ACTIVE \
"$(printf '%s' "$out1" | \
grep -qF 'active-nolink-skill: symlink missing/wrong' && echo 1 || echo 0)"
check_bool SYMLINK_WRONG_TARGET \
"$(printf '%s' "$out1" | \
grep -qF 'active-wronglink-skill: symlink missing/wrong' \
&& echo 1 || echo 0)"
check_bool SYMLINK_PARKED \
"$(printf '%s' "$out1" | grep -qF 'parked-skill: parked by profile active' \
&& ! printf '%s' "$out1" | \
grep -qF 'parked-skill: symlink missing/wrong' \
&& echo 1 || echo 0)"
# ── always-on-skill: absent from active.profile (same as parked-skill)
# but its lock entry is "always_on": true — checked (and failed, no
# symlink) instead of reported parked.
check_bool ALWAYS_ON_LINK_CHECKED \
"$(printf '%s' "$out1" | \
grep -qF 'always-on-skill: symlink missing/wrong' \
&& ! printf '%s' "$out1" | \
grep -qF 'always-on-skill: parked by profile' \
&& echo 1 || echo 0)"
# ── No profile file passed at all: noprofile-skill (absent from
# active.profile, parked above) must now be treated as expected-linked.
out2="$(check_vendored_skills "$REPO" "$CLAUDE_HOME" 2>&1)"
check_bool NO_PROFILE_EXPECTS_LINK \
"$(printf '%s' "$out2" | \
grep -qF 'noprofile-skill: symlink missing/wrong' && echo 1 || echo 0)"
# ── LOCK_UNREADABLE — a second, minimal fixture with an invalid
# plugins.lock.json: check_vendored_skills degrades to a warn (not a
# fail) and still returns 0.
BROKEN="$WORK/broken"
mkdir -p "$BROKEN/skills-external/solo-skill"
echo "v1" > "$BROKEN/skills-external/solo-skill/SKILL.md"
echo "not valid json" > "$BROKEN/plugins.lock.json"
cat > "$BROKEN/link.sh" <<'SH'
#!/usr/bin/env bash
EXTERNAL_SKILLS=(solo-skill)
SH
out3="$(check_vendored_skills "$BROKEN" "$CLAUDE_HOME" 2>&1)"
rc3=$?
check_bool LOCK_UNREADABLE \
"$([ "$rc3" -eq 0 ] && printf '%s' "$out3" | \
grep -qF 'plugins.lock.json unreadable' && echo 1 || echo 0)"
# ── LOCK_MALFORMED_ENTRY — a valid-JSON lock whose "skills" is a bare
# number (neither null, list nor dict): degrades to the same warn as an
# unreadable lock, rc 0, and no Python traceback text anywhere in the
# captured output.
MALFORMED="$WORK/malformed"
mkdir -p "$MALFORMED/skills-external/bad-entry"
echo "v1" > "$MALFORMED/skills-external/bad-entry/SKILL.md"
cat > "$MALFORMED/plugins.lock.json" <<'JSON'
{
"bad-entry": {
"managed_by": "curl",
"skills": 42
}
}
JSON
cat > "$MALFORMED/link.sh" <<'SH'
#!/usr/bin/env bash
EXTERNAL_SKILLS=(bad-entry)
SH
out4="$(check_vendored_skills "$MALFORMED" "$CLAUDE_HOME" 2>&1)"
rc4=$?
check_bool LOCK_MALFORMED_ENTRY \
"$([ "$rc4" -eq 0 ] \
&& printf '%s' "$out4" | grep -qF 'plugins.lock.json unreadable' \
&& ! printf '%s' "$out4" | grep -qi 'traceback' \
&& ! printf '%s' "$out4" | grep -q 'Error:' \
&& echo 1 || echo 0)"
# ── REJECTS_BAD_PROFILE_NAME — the profile-name allowlist helper
# rejects a path-traversal value and accepts a plain one.
check_bool REJECTS_BAD_PROFILE_NAME \
"$( { _dv_valid_profile_name "full" \
&& ! _dv_valid_profile_name "../x" \
&& ! _dv_valid_profile_name "a/b" \
&& ! _dv_valid_profile_name "a.b"; } && echo 1 || echo 0)"
# ── REJECTS_BAD_NAME — a link.sh EXTERNAL_SKILLS entry with a ".."
# segment: warned and skipped, never reaches _dv_check_files/
# _dv_check_link (no fail line names it), rc 0.
BADNAME="$WORK/badname"
mkdir -p "$BADNAME/skills-external"
echo '{}' > "$BADNAME/plugins.lock.json"
cat > "$BADNAME/link.sh" <<'SH'
#!/usr/bin/env bash
EXTERNAL_SKILLS=(../evil)
SH
out5="$(check_vendored_skills "$BADNAME" "$CLAUDE_HOME" 2>&1)"
rc5=$?
check_bool REJECTS_BAD_NAME \
"$([ "$rc5" -eq 0 ] \
&& printf '%s' "$out5" | \
grep -qF '"../evil" rejected by the item-name allowlist' \
&& ! printf '%s' "$out5" | grep -qF 'skills-external/../evil' \
&& ! printf '%s' "$out5" | grep -qF '../evil: symlink' \
&& echo 1 || echo 0)"
echo "PASS=$pass FAIL=$fail"
[ "$fail" -eq 0 ]