#!/usr/bin/env bash # Tests for plugins/kyberforge/.apm/hooks/check-apm-current.sh — the SessionStart # hook that keeps an apm-consumed install level with its remote. # # `apm` is mocked throughout: the hook's contract is "read `apm outdated`, decide, # emit SessionStart JSON", and that is testable without a network or a real # install. set -euo pipefail REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" HOOK="$REPO_ROOT/plugins/kyberforge/.apm/hooks/check-apm-current.sh" HOOKS_JSON="$REPO_ROOT/plugins/kyberforge/.apm/hooks/hooks.json" PASS=0 FAIL=0 pass() { echo " PASS: $1"; PASS=$((PASS + 1)); } fail() { echo " FAIL: $1"; FAIL=$((FAIL + 1)); } command -v python3 > /dev/null 2>&1 || { echo "python3 required"; exit 77; } FAKE_BIN="$(mktemp -d)" WORK="$(mktemp -d)" trap 'rm -rf "$FAKE_BIN" "$WORK"' EXIT # Mock `apm`. $1 chooses what `apm outdated` reports; $2 the exit code of # `apm update`. A sentinel file records whether update was actually invoked. make_apm() { local outdated_line="$1" update_exit="$2" cat > "$FAKE_BIN/apm" << EOF #!/usr/bin/env bash case "\$1" in outdated) echo "$outdated_line"; exit 0 ;; update) touch "$WORK/update-was-called"; exit $update_exit ;; esac exit 0 EOF chmod +x "$FAKE_BIN/apm" } run_hook() { (cd "$WORK" && PATH="$FAKE_BIN:$PATH" bash "$HOOK" 2>/dev/null); } json_field() { python3 -c 'import json,sys; print(json.load(sys.stdin)["hookSpecificOutput"][sys.argv[1]])' "$1"; } # --------------------------------------------------------------------------- echo "--- inert without an apm-consumed install ---" # --------------------------------------------------------------------------- make_apm "[!] 6 outdated dependencies found" 0 rm -f "$WORK/apm.lock.yaml" "$WORK/update-was-called" out="$(run_hook)"; rc=$? [[ $rc -eq 0 ]] && pass "exits 0 with no apm.lock.yaml" || fail "should exit 0 with no apm.lock.yaml" [[ -z "$out" ]] && pass "emits nothing with no apm.lock.yaml" || fail "should stay silent with no apm.lock.yaml" [[ ! -f "$WORK/update-was-called" ]] && pass "does not run apm update with no apm.lock.yaml" \ || fail "must not touch a project that does not use apm" # A native (non-apm) install of this plugin hits exactly this path, so it is the # guard that keeps the hook from acting on someone else's repo. touch "$WORK/apm.lock.yaml" # --------------------------------------------------------------------------- echo "" echo "--- inert when apm is absent ---" # --------------------------------------------------------------------------- rm -f "$WORK/update-was-called" out="$( (cd "$WORK" && PATH="$(dirname "$(command -v bash)")" bash "$HOOK" 2>/dev/null) )"; rc=$? [[ $rc -eq 0 ]] && pass "exits 0 when apm is not on PATH" || fail "should exit 0 when apm is missing" [[ -z "$out" ]] && pass "emits nothing when apm is not on PATH" || fail "should stay silent when apm is missing" # --------------------------------------------------------------------------- echo "" echo "--- install already current ---" # --------------------------------------------------------------------------- make_apm "[*] All dependencies are up-to-date" 0 rm -f "$WORK/update-was-called" out="$(run_hook)"; rc=$? [[ $rc -eq 0 ]] && pass "exits 0 when current" || fail "should exit 0 when current" [[ -z "$out" ]] && pass "emits nothing when current" || fail "should stay silent when current" [[ ! -f "$WORK/update-was-called" ]] && pass "does not run apm update when current" \ || fail "must not update when nothing is stale" # --------------------------------------------------------------------------- echo "" echo "--- stale, refresh succeeds ---" # --------------------------------------------------------------------------- make_apm "[!] 6 outdated dependencies found" 0 rm -f "$WORK/update-was-called" out="$(run_hook)"; rc=$? [[ $rc -eq 0 ]] && pass "exits 0 when stale" || fail "should exit 0 when stale" [[ -f "$WORK/update-was-called" ]] && pass "runs apm update when stale" || fail "should run apm update when stale" if echo "$out" | python3 -m json.tool > /dev/null 2>&1; then pass "emits valid JSON" [[ "$(echo "$out" | json_field hookEventName)" == "SessionStart" ]] \ && pass "declares hookEventName SessionStart" || fail "wrong hookEventName" [[ "$(echo "$out" | json_field reloadSkills)" == "True" ]] \ && pass "asks the host to reload skills after a successful refresh" || fail "reloadSkills should be true" echo "$out" | json_field additionalContext | grep -q "6 package" \ && pass "reports the stale package count" || fail "should report the count" # The lockfile rewrite is the surprising part of auto-updating; the notice has # to say so or a dirty worktree looks like something else went wrong. echo "$out" | json_field additionalContext | grep -q "apm.lock.yaml" \ && pass "warns that apm.lock.yaml was rewritten" || fail "should warn about the lockfile rewrite" else fail "emits valid JSON" fi # --------------------------------------------------------------------------- echo "" echo "--- stale, refresh fails ---" # --------------------------------------------------------------------------- make_apm "[!] 3 outdated dependencies found" 1 out="$(run_hook)"; rc=$? [[ $rc -eq 0 ]] && pass "exits 0 when the refresh fails" || fail "must never fail the session start" if echo "$out" | python3 -m json.tool > /dev/null 2>&1; then pass "emits valid JSON on failure" [[ "$(echo "$out" | json_field reloadSkills)" == "False" ]] \ && pass "does not ask for a skill reload when nothing was deployed" || fail "reloadSkills should be false" echo "$out" | json_field additionalContext | grep -q "apm update" \ && pass "tells the reader how to refresh by hand" || fail "should name the manual command" else fail "emits valid JSON on failure" fi # --------------------------------------------------------------------------- echo "" echo "--- unparseable count degrades instead of breaking the JSON ---" # --------------------------------------------------------------------------- make_apm "[!] lots of outdated dependencies found" 0 out="$(run_hook)" echo "$out" | python3 -m json.tool > /dev/null 2>&1 \ && pass "still emits valid JSON when the count cannot be parsed" || fail "JSON broke on an unparseable count" # --------------------------------------------------------------------------- echo "" echo "--- hooks.json wiring ---" # --------------------------------------------------------------------------- # apm resolves script paths relative to the package root, and `apm pack` keeps # only *.json from .apm/hooks/ — so a ${CLAUDE_PLUGIN_ROOT}/hooks/... reference # points at a directory the script never reaches. It must be .apm/-relative. referenced="$(python3 -c 'import json,sys; d=json.load(open(sys.argv[1])); print(d["hooks"]["SessionStart"][0]["hooks"][0]["command"])' "$HOOKS_JSON")" [[ "$referenced" == '${CLAUDE_PLUGIN_ROOT}/.apm/hooks/check-apm-current.sh' ]] \ && pass "hooks.json references the script at its .apm/ path" \ || fail "hooks.json references '$referenced' — must be \${CLAUDE_PLUGIN_ROOT}/.apm/hooks/check-apm-current.sh" [[ -x "$HOOK" ]] && pass "hook script is executable" || fail "hook script must be executable" matcher="$(python3 -c 'import json,sys; d=json.load(open(sys.argv[1])); print(d["hooks"]["SessionStart"][0]["matcher"])' "$HOOKS_JSON")" [[ "$matcher" == "startup" ]] && pass "fires on startup only" \ || fail "matcher is '$matcher' — resume/clear/compact would re-run this every compaction" # --------------------------------------------------------------------------- echo "" echo "Results: $PASS passed, $FAIL failed" [[ $FAIL -eq 0 ]]