Files
skills/tests/install_test.sh
T
mathias 025f01aa80
CI / Check / Test (push) Successful in 0s
CI / Tag release (push) Failing after 1s
feat(install): serve several sources instead of displacing them
~/.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
2026-08-29 10:05:46 +02:00

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 ]