#!/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 — a minimal valid skill. skill() { mkdir -p "$1/$2" printf -- '---\nname: %s\ndescription: d\n---\n' "$2" > "$1/$2/SKILL.md" } # new_fixture [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 [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" \ ANTIGRAVITY_SKILLS_JSON="$_f/home/.gemini/config/skills.json" \ 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 # --- 8. surfaces: the library link set is not "every skill" ----------------- # claude.ai has no shell, no cluster and no checkout, so a chat-only skill # linked into Crush is a skill that can never fire, competing for selection # with ones that can (#29). f=$(new_fixture surfaces) mkdir -p "$f/ck/skills/chatonly" printf -- '---\nname: chatonly\nsurfaces: [chat]\ndescription: c\n---\n' > "$f/ck/skills/chatonly/SKILL.md" printf -- '---\nname: alpha\nsurfaces: [claude-code, library]\ndescription: a\n---\n' > "$f/ck/skills/alpha/SKILL.md" out=$(run_install "$f") if [ -L "$f/home/.agents/skills/alpha" ] && [ ! -e "$f/home/.agents/skills/chatonly" ]; then ok 'a skill that does not ship to the library is not linked' else no 'a skill that does not ship to the library is not linked' "$out" fi # A foreign source has no reason to know this convention, so no declaration # means "link it" — otherwise adding Anthropic's set would link nothing. f=$(new_fixture foreign) mkdir -p "$f/other/xlsx" printf -- '---\nname: xlsx\ndescription: x\n---\n' > "$f/other/xlsx/SKILL.md" out=$(run_install "$f" "$f/other") if [ -L "$f/home/.agents/skills/xlsx" ]; then ok 'a source with no surfaces convention is linked as-is' else no 'a source with no surfaces convention is linked as-is' "$out" fi # --- 9. Antigravity: the managed agents dir is registered, not the raw checkout # skills.json entries must point at $AGENTS_DIR — that's what already carries # surfaces-filtering, collision handling and multi-source merge. Pointing at # the checkout directly would bypass all of it and give Antigravity a # different skill set than every other harness. f=$(new_fixture antigravity) out=$(run_install "$f") AG="$f/home/.gemini/config/skills.json" if [ ! -f "$AG" ]; then no 'Antigravity skills.json is created' "$out" elif ! command -v jq >/dev/null 2>&1; then no 'jq available to verify Antigravity wiring' "(jq missing on test host)" else registered=$(jq -r --arg p "$f/home/.agents/skills" '[.entries[]? | select(.path == $p)] | length' "$AG") if [ "$registered" = "1" ]; then ok 'Antigravity registers the managed agents dir, not the raw checkout' else no 'Antigravity registers the managed agents dir, not the raw checkout' "$(cat "$AG")" fi fi # --- 10. Antigravity wiring is idempotent ------------------------------------ f=$(new_fixture antigravity_idempotent) run_install "$f" >/dev/null out=$(run_install "$f") AG="$f/home/.gemini/config/skills.json" count=$(jq '.entries | length' "$AG" 2>/dev/null || echo "?") if [ "$count" = "1" ]; then ok 'Antigravity wiring is idempotent (no duplicate entries)' else no 'Antigravity wiring is idempotent (no duplicate entries)' "entries=$count"$'\n'"$out" fi # --- 11. Antigravity wiring preserves entries it doesn't own ----------------- # A user may hand-edit skills.json to point at their own personal skills dir # (json_configs.md documents this). Our merge must not clobber it. f=$(new_fixture antigravity_preserve) mkdir -p "$f/home/.gemini/config" printf '{"entries":[{"path":"/Users/mathias/personal-skills"}]}' > "$f/home/.gemini/config/skills.json" out=$(run_install "$f") AG="$f/home/.gemini/config/skills.json" personal=$(jq -r '[.entries[]? | select(.path == "/Users/mathias/personal-skills")] | length' "$AG" 2>/dev/null || echo 0) if [ "$personal" = "1" ]; then ok 'Antigravity wiring preserves pre-existing hand-authored entries' else no 'Antigravity wiring preserves pre-existing hand-authored entries' "$(cat "$AG")"$'\n'"$out" fi printf '\ninstall: %d passed, %d failed\n' "$pass" "$fail" [ "$fail" -eq 0 ]