Antigravity discovers skills via a path-entries JSON (~/.gemini/config/skills.json), not a symlink directory. One entry points at $AGENTS_DIR — the same managed, surfaces-filtered, collision-resolved directory Crush/OpenCode/Pi already read — so Antigravity sees exactly the same skill set as every other harness, for free. Requires jq; every other target in this script stays dependency-free on purpose, so wire_antigravity skips with a warning (not a hard failure) if jq is missing. Merges into skills.json rather than overwriting it: the file is documented as user-editable (json_configs.md — e.g. a personal skills dir), so a hand-added entry must survive a re-run. Was previously removed as "unused" (mathias/skills#41 — Antigravity usage picked back up). Mistral Vibe stays removed. Closes #41 Bump-Type: minor Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
237 lines
8.8 KiB
Bash
237 lines
8.8 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" \
|
|
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 ]
|