~/.agents/skills was a single link straight at this checkout. That encoded an assumption nobody had stated — that this checkout is the only source of skills on the host. On flamingo it was not: Crush's directory held Anthropic's official set, and the only way to install this library was to move that set aside. A capability loss dressed up as a tidy-up. The canonical location becomes a real directory of per-skill symlinks, fed by the checkout plus anything in SKILLS_EXTRA_SOURCES. Harnesses keep their one directory link, so the count is N + one per harness rather than N x harnesses, which is what #23 was actually about. The three requirements #27 named, each with a test: - collisions are logged with both paths and the checkout wins, never resolved in silence; - removing a skill from a source prunes its link; - anything in the managed directory that is not our symlink is reported and left in place — the installer does not delete content it did not create. doctor learns the same model: sources are recorded at ~/.agents/skills-sources, a link into a configured source is healthy, a link into an unknown one is not, and the old count check becomes a per-skill check because a per-skill layout can lose one skill without changing any total. The pre-#27 wiring is named as legacy rather than reported as damage. count_skills needed find -L; without it symlinked skill directories count as zero. Verified live on koala: migrated in place to 42 per-skill links, then a second source added a 43rd alongside them, then a re-run without that source pruned it back to 42. Only doctor finding throughout was the genuine stale plugin. Closes #27 Bump-Type: minor
161 lines
5.3 KiB
Bash
161 lines
5.3 KiB
Bash
#!/bin/sh
|
|
# Tests for install.sh's wiring model.
|
|
#
|
|
# The regression these exist for: install.sh wired each harness with ONE
|
|
# directory symlink to this checkout, which quietly encoded "this checkout is
|
|
# the only source of skills on the host". On flamingo the Crush directory held
|
|
# Anthropic's official skill set, and the only way to install this library was
|
|
# to displace it — a real capability loss dressed up as a tidy-up (#27).
|
|
#
|
|
# Fixtures drive install.sh entirely through env seams, so no test touches the
|
|
# real host wiring.
|
|
|
|
set -u
|
|
|
|
ROOT=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)
|
|
SUT="$ROOT/install.sh"
|
|
|
|
pass=0
|
|
fail=0
|
|
WORK=${TMPDIR:-/tmp}/install_test.$$
|
|
|
|
if [ ! -f "$SUT" ]; then
|
|
printf 'FAIL: %s does not exist\n' "$SUT"
|
|
exit 1
|
|
fi
|
|
|
|
cleanup() { rm -rf "$WORK"; }
|
|
trap cleanup EXIT INT TERM
|
|
|
|
ok() { pass=$((pass + 1)); printf ' ok %s\n' "$1"; }
|
|
no() { fail=$((fail + 1)); printf ' FAIL %s\n%s\n' "$1" "${2:-}"; }
|
|
|
|
# skill <dir> <name> — a minimal valid skill.
|
|
skill() {
|
|
mkdir -p "$1/$2"
|
|
printf -- '---\nname: %s\ndescription: d\n---\n' "$2" > "$1/$2/SKILL.md"
|
|
}
|
|
|
|
# new_fixture <name> [extra-source-names...] — checkout with alpha+beta.
|
|
new_fixture() {
|
|
_f="$WORK/$1"
|
|
mkdir -p "$_f/ck/skills" "$_f/home"
|
|
skill "$_f/ck/skills" alpha
|
|
skill "$_f/ck/skills" beta
|
|
printf '%s' "$_f"
|
|
}
|
|
|
|
# run_install <fixture> [extra-sources] — echoes output, sets $rc.
|
|
run_install() {
|
|
_f=$1
|
|
_extra=${2:-}
|
|
_out=$(HOME="$_f/home" \
|
|
SKILLS_SKIP_FETCH=1 \
|
|
SKILLS_CHECKOUT_DIR="$_f/ck" \
|
|
AGENTS_SKILLS_DIR="$_f/home/.agents/skills" \
|
|
CRUSH_SKILLS_DIR="$_f/home/.config/crush/skills" \
|
|
OPENCODE_SKILLS_DIR="$_f/home/.config/opencode/skills" \
|
|
SKILLS_EXTRA_SOURCES="$_extra" \
|
|
bash "$SUT" 2>&1)
|
|
rc=$?
|
|
printf '%s' "$_out"
|
|
}
|
|
|
|
# --- 1. the new shape: a real directory of per-skill links -------------------
|
|
f=$(new_fixture fresh)
|
|
out=$(run_install "$f")
|
|
A="$f/home/.agents/skills"
|
|
if [ ! -d "$A" ] || [ -L "$A" ]; then
|
|
no 'agents dir is a real directory' "$out"
|
|
elif [ ! -L "$A/alpha" ] || [ ! -L "$A/beta" ]; then
|
|
no 'each skill is its own link' "$out"
|
|
elif [ ! -L "$f/home/.config/crush/skills" ] || [ ! -L "$f/home/.config/opencode/skills" ]; then
|
|
no 'harnesses keep one directory link each' "$out"
|
|
else
|
|
ok 'fresh install builds per-skill links under a real agents dir'
|
|
fi
|
|
|
|
# --- 2. a second source is served WITHOUT displacing the first --------------
|
|
f=$(new_fixture twosource)
|
|
mkdir -p "$f/other"
|
|
skill "$f/other" xlsx
|
|
out=$(run_install "$f" "$f/other")
|
|
if [ -L "$f/home/.agents/skills/xlsx" ] && [ -L "$f/home/.agents/skills/alpha" ]; then
|
|
ok 'a second source is linked alongside the checkout'
|
|
else
|
|
no 'a second source is linked alongside the checkout' "$out"
|
|
fi
|
|
|
|
# --- 3. name collisions are REPORTED, never silently resolved ---------------
|
|
# Two sources offering `code-review` is a real possibility, and picking one
|
|
# quietly is how stale content wins.
|
|
f=$(new_fixture collide)
|
|
mkdir -p "$f/other"
|
|
skill "$f/other" alpha
|
|
out=$(run_install "$f" "$f/other")
|
|
if printf '%s' "$out" | grep -qi 'collision' \
|
|
&& printf '%s' "$out" | grep -q "$f/other/alpha"; then
|
|
target=$(readlink "$f/home/.agents/skills/alpha")
|
|
if [ "$target" = "$f/ck/skills/alpha" ]; then
|
|
ok 'a name collision is reported and the checkout wins'
|
|
else
|
|
no 'a name collision is reported and the checkout wins' "resolved to $target"
|
|
fi
|
|
else
|
|
no 'a name collision is reported and the checkout wins' "$out"
|
|
fi
|
|
|
|
# --- 4. removing a skill from a source removes its link ---------------------
|
|
# Without this the previous generation's problem returns in a new shape: links
|
|
# that resolve to nothing, invisible to git.
|
|
f=$(new_fixture prune)
|
|
out=$(run_install "$f")
|
|
rm -rf "$f/ck/skills/beta"
|
|
out=$(run_install "$f")
|
|
if [ -e "$f/home/.agents/skills/beta" ] || [ -L "$f/home/.agents/skills/beta" ]; then
|
|
no 'a removed skill loses its link' "$out"
|
|
else
|
|
ok 'a removed skill loses its link'
|
|
fi
|
|
|
|
# --- 5. migration from the old single-link shape ----------------------------
|
|
f=$(new_fixture migrate)
|
|
mkdir -p "$f/home/.agents"
|
|
ln -s "$f/ck/skills" "$f/home/.agents/skills"
|
|
out=$(run_install "$f")
|
|
if [ -L "$f/home/.agents/skills" ]; then
|
|
no 'the old single-link wiring is migrated in place' "$out"
|
|
elif [ -L "$f/home/.agents/skills/alpha" ]; then
|
|
ok 'the old single-link wiring is migrated in place'
|
|
else
|
|
no 'the old single-link wiring is migrated in place' "$out"
|
|
fi
|
|
|
|
# --- 6. unmanaged real content in the agents dir is never deleted -----------
|
|
f=$(new_fixture protect)
|
|
mkdir -p "$f/home/.agents/skills"
|
|
skill "$f/home/.agents/skills" handwritten
|
|
out=$(run_install "$f")
|
|
if [ -f "$f/home/.agents/skills/handwritten/SKILL.md" ] \
|
|
&& printf '%s' "$out" | grep -qi 'unmanaged'; then
|
|
ok 'unmanaged content is reported and left in place'
|
|
else
|
|
no 'unmanaged content is reported and left in place' "$out"
|
|
fi
|
|
|
|
# --- 7. the source list is recorded for doctor ------------------------------
|
|
f=$(new_fixture manifest)
|
|
mkdir -p "$f/other"
|
|
skill "$f/other" xlsx
|
|
out=$(run_install "$f" "$f/other")
|
|
if [ -f "$f/home/.agents/skills-sources" ] \
|
|
&& grep -q "$f/other" "$f/home/.agents/skills-sources" \
|
|
&& grep -q "$f/ck/skills" "$f/home/.agents/skills-sources"; then
|
|
ok 'configured sources are recorded for doctor'
|
|
else
|
|
no 'configured sources are recorded for doctor' "$out"
|
|
fi
|
|
|
|
printf '\ninstall: %d passed, %d failed\n' "$pass" "$fail"
|
|
[ "$fail" -eq 0 ]
|