Files
holocron/tests/test-apm-current-hook.sh
Defame1297 ea119d83b0 fix(gates): close six PR #135 review findings in gates and their tests
B1: check-skill-version-bump.sh resolves every merge-base with `git merge-base
--all` instead of the single base git happens to pick. A criss-cross history has
two, so the verdict turned on that choice: a skill byte-identical to main's tip
could still be reported "not above merge-base" / "not above main tip" and fail a
push that should pass. A skill now counts as changed only when it differs from
EVERY base, and its version must exceed the version at every base it exists at
as well as at the main tip; with more than one base the failure names which one.
Case 40 in tests/test-skill-version-bump.sh builds the criss-cross fixture and
pins both directions.

B2: check-apm-current.sh no longer assumes the remote default branch is `main`
when origin/HEAD is unset. A checkout whose default is `master` was standing on
its default branch and being told "this is a feature branch, so discard it" --
to throw away a real lock update. With origin/HEAD unset nothing is asserted and
the neutral advice stands. tests/test-apm-current-hook.sh covers the unset case
on both `main` and `master`.

#4: the required-frontmatter checks folded into skill-size-check.sh by c8a7c9e
were untested apart from the leading-zero shape -- mutating the missing-version
ERROR into a no-op left every suite green. tests/test-adr0020-frontmatter.sh now
pins name presence and non-emptiness, metadata.version presence and semver
shape, and the four grep defects the deleted test-skill-frontmatter.sh named.

#5: nothing asked whether a Vale rule still MATCHES anything -- rewriting
CompositionNote.yml's tokens to match nothing left test-vale-wrap.sh at 63/63.
Case 35 enumerates the rule files under the Kyberforge* style directories at run
time, requires an alert from each on its own fixture, and fails when a
discovered rule has no fixture row. The stale comment at case 31 is corrected.

#6: tests/run-tests.sh --strict exited 0 when discovery found no test-*.sh at
all; strictness only ever acted on skips, and with no suites there were none. It
now cross-checks the git index the way run-bats.sh does and fails
unconditionally on an empty set, naming the search root.

N9: the skill-size-check hook description in .pre-commit-config.yaml covered
only the size, context-budget and boundary-target gates. It now also names the
required frontmatter fields, matching docs/spec/gates.md.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NwD8Egs5r4ndqeFLmhusX2
2026-09-19 21:08:10 +00:00

460 lines
22 KiB
Bash
Executable File

#!/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
# The hook asks git which branch it is on. Stop git's discovery at the temp
# root so a $TMPDIR that happens to sit inside a checkout cannot leak a branch
# into the fixtures that are meant to be outside one.
export GIT_CEILING_DIRECTORIES
GIT_CEILING_DIRECTORIES="$(dirname "$WORK")"
# Mock `apm`. $1 chooses what `apm outdated` reports; $2 the exit code of
# `apm update`. Sentinel files record whether update was actually invoked and
# which directory the calls ran in — the hook must run them against the same
# directory its lockfile guard checked, not the session's cwd.
make_apm() {
local outdated_line="$1" update_exit="$2"
cat > "$FAKE_BIN/apm" << EOF
#!/usr/bin/env bash
pwd > "$WORK/apm-cwd"
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"
}
# CLAUDE_PROJECT_DIR is cleared rather than merely left alone: a session in this
# repo exports it, and an inherited value would point every case at the real
# repo root (which has a real apm.lock.yaml) instead of the fixture. The
# project-directory cases below set it deliberately.
run_hook() { (cd "$WORK" && env -u CLAUDE_PROJECT_DIR PATH="$FAKE_BIN:$PATH" bash "$HOOK" 2>/dev/null); }
# Same, with an explicit cwd and CLAUDE_PROJECT_DIR. $1 is the cwd; $2 the value
# for CLAUDE_PROJECT_DIR, or the literal `-` to leave it unset.
run_hook_in() {
local cwd="$1" project_dir="$2"
if [[ "$project_dir" == "-" ]]; then
(cd "$cwd" && env -u CLAUDE_PROJECT_DIR PATH="$FAKE_BIN:$PATH" bash "$HOOK" 2>/dev/null)
else
(cd "$cwd" && env CLAUDE_PROJECT_DIR="$project_dir" PATH="$FAKE_BIN:$PATH" bash "$HOOK" 2>/dev/null)
fi
}
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"
grep -q "6 package" <<< "$(json_field additionalContext <<< "$out")" \
&& 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.
grep -q "apm.lock.yaml" <<< "$(json_field additionalContext <<< "$out")" \
&& pass "warns that apm.lock.yaml was rewritten" || fail "should warn about the lockfile rewrite"
else
fail "emits valid JSON"
fi
# ---------------------------------------------------------------------------
echo ""
echo "--- lock advice follows the branch ---"
# ---------------------------------------------------------------------------
# ADR-0019: on the default branch the rewritten lock is a real update to commit or
# discard; on a feature branch it is unrelated churn to discard. Outside a git
# checkout (the fixture above) the neutral advice stands.
advice_of() { json_field additionalContext <<< "$1"; }
grep -q "commit it or discard it deliberately" <<< "$(advice_of "$out")" \
&& pass "gives neutral lock advice outside a git checkout" \
|| fail "outside a git checkout the advice should stay neutral"
if command -v git > /dev/null 2>&1; then
REPO="$WORK/repo"
mkdir -p "$REPO"
git -C "$REPO" init -q -b main
git -C "$REPO" -c user.email=probe@example.invalid -c user.name=probe \
commit -q --allow-empty -m init
touch "$REPO/apm.lock.yaml"
# origin/HEAD is unset here — git writes it on clone and `git remote add` does
# not — so the hook cannot know what the default branch is and must not guess.
# It used to assume `main`, which is why the `master` case further down was a
# live defect.
out="$(run_hook_in "$REPO" "$REPO")"
advice="$(advice_of "$out")"
grep -q "commit it or discard it deliberately" <<< "$advice" \
&& pass "with origin/HEAD unset, keeps the neutral lock advice" \
|| fail "with origin/HEAD unset the advice should stay neutral: $advice"
# Needles are the two DECISION phrases, not the bare words: the fixed prefix
# of every notice already says "behind the remote default branch".
grep -qE "this is (the default|a feature) branch, so" <<< "$advice" \
&& fail "with origin/HEAD unset the hook must not claim to know which branch this is: $advice" \
|| pass "with origin/HEAD unset, claims nothing about which branch this is"
git -C "$REPO" update-ref refs/remotes/origin/main HEAD
git -C "$REPO" symbolic-ref refs/remotes/origin/HEAD refs/remotes/origin/main
out="$(run_hook_in "$REPO" "$REPO")"
grep -q "default branch, so commit it or discard it deliberately" <<< "$(advice_of "$out")" \
&& pass "on main, says to commit or discard the lock deliberately" \
|| fail "on main the advice should be commit-or-discard: $(advice_of "$out")"
git -C "$REPO" checkout -q -b feature/x
out="$(run_hook_in "$REPO" "$REPO")"
advice="$(advice_of "$out")"
grep -qF "feature branch, so discard it: git checkout -- apm.lock.yaml && apm install" <<< "$advice" \
&& pass "on a feature branch, says to discard the lock and reinstall" \
|| fail "on a feature branch the advice should be discard-and-install: $advice"
grep -q "commit it" <<< "$advice" \
&& fail "on a feature branch the advice must not suggest committing the lock" \
|| pass "on a feature branch, does not suggest committing the lock"
echo "$out" | python3 -m json.tool > /dev/null 2>&1 \
&& pass "feature-branch notice is valid JSON" || fail "feature-branch notice broke the JSON"
# A remote whose default branch is not `main` is honoured via origin/HEAD.
git -C "$REPO" update-ref refs/remotes/origin/feature/x HEAD
git -C "$REPO" symbolic-ref refs/remotes/origin/HEAD refs/remotes/origin/feature/x
out="$(run_hook_in "$REPO" "$REPO")"
grep -q "default branch, so commit it" <<< "$(advice_of "$out")" \
&& pass "reads the default branch from origin/HEAD when it is set" \
|| fail "should treat origin/HEAD's branch as the default: $(advice_of "$out")"
# The regression the `${default_branch:-main}` fallback caused: a repo whose
# default branch is `master`, with origin/HEAD unset (no clone wrote it), was
# standing on its DEFAULT branch and was told to discard the lock as feature
# churn. Assuming `main` is the only way to reach that verdict, so the case is
# pinned on the branch name that makes the assumption wrong.
MASTER_REPO="$WORK/master-repo"
mkdir -p "$MASTER_REPO"
git -C "$MASTER_REPO" init -q -b master
git -C "$MASTER_REPO" -c user.email=probe@example.invalid -c user.name=probe \
commit -q --allow-empty -m init
touch "$MASTER_REPO/apm.lock.yaml"
out="$(run_hook_in "$MASTER_REPO" "$MASTER_REPO")"
advice="$(advice_of "$out")"
grep -qF "feature branch, so discard it" <<< "$advice" \
&& fail "on master with origin/HEAD unset the hook assumed main and told the reader to discard a real lock update: $advice" \
|| pass "on master with origin/HEAD unset, does not misread the default branch as a feature branch"
grep -q "commit it or discard it deliberately" <<< "$advice" \
&& pass "on master with origin/HEAD unset, falls back to the neutral lock advice" \
|| fail "on master with origin/HEAD unset the advice should be neutral: $advice"
else
echo " (git not on PATH — branch-specific advice cases not run)"
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"
grep -q "apm update" <<< "$(json_field additionalContext <<< "$out")" \
&& 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 "--- anchors on the project root, not the session cwd ---"
# ---------------------------------------------------------------------------
# `[[ -f apm.lock.yaml ]]` resolves against the cwd, and a session started in a
# subdirectory of an apm-consuming repo therefore no-opped silently — and would
# have run `apm outdated`/`apm update` against that wrong directory had the
# guard passed. Claude Code exports CLAUDE_PROJECT_DIR for SessionStart hooks,
# so that is the anchor; the cwd is only the fallback.
ELSEWHERE="$WORK/elsewhere"
mkdir -p "$ELSEWHERE"
rm -f "$ELSEWHERE/apm.lock.yaml"
make_apm "[!] 6 outdated dependencies found" 0
rm -f "$WORK/update-was-called" "$WORK/apm-cwd"
out="$(run_hook_in "$ELSEWHERE" "$WORK")"
[[ -f "$WORK/update-was-called" ]] \
&& pass "finds the lockfile via CLAUDE_PROJECT_DIR when the cwd is elsewhere" \
|| fail "a session started in a subdirectory must still see the project's lockfile"
[[ "$(cat "$WORK/apm-cwd" 2>/dev/null)" == "$WORK" ]] \
&& pass "runs apm in the directory the guard checked, not the cwd" \
|| fail "apm ran in '$(cat "$WORK/apm-cwd" 2>/dev/null)' — must run in the resolved project directory"
grep -q "6 package" <<< "$(json_field additionalContext <<< "$out")" \
&& pass "reports the count found via CLAUDE_PROJECT_DIR" || fail "should report the count"
# The fallback is not cosmetic: a host that installed this plugin natively sets
# no CLAUDE_PROJECT_DIR, and the hook must stay inert-but-harmless there rather
# than erroring on an unset variable (the script runs under `set -u`).
rm -f "$WORK/update-was-called" "$WORK/apm-cwd"
out="$(run_hook_in "$WORK" "-")"
[[ -f "$WORK/update-was-called" ]] \
&& pass "falls back to the cwd when CLAUDE_PROJECT_DIR is unset" \
|| fail "must still work with no CLAUDE_PROJECT_DIR in the environment"
[[ "$(cat "$WORK/apm-cwd" 2>/dev/null)" == "$WORK" ]] \
&& pass "runs apm in the cwd under the fallback" \
|| fail "apm ran in '$(cat "$WORK/apm-cwd" 2>/dev/null)' — should be the cwd"
rm -f "$WORK/update-was-called" "$WORK/apm-cwd"
out="$(run_hook_in "$ELSEWHERE" "$ELSEWHERE")"; rc=$?
[[ $rc -eq 0 ]] && pass "exits 0 when neither the project dir nor the cwd has a lockfile" \
|| fail "should exit 0 when there is no lockfile anywhere"
[[ -z "$out" ]] && pass "stays silent when neither the project dir nor the cwd has a lockfile" \
|| fail "should stay silent when there is no lockfile anywhere"
[[ ! -f "$WORK/update-was-called" ]] \
&& pass "does not run apm update when there is no lockfile anywhere" \
|| fail "must not touch a project that does not use apm"
# ---------------------------------------------------------------------------
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"
# The host's timeout must strictly exceed everything the script can spend, or
# the host SIGKILLs the hook mid-`apm update` and leaves a half-redeployed
# .claude/skills/ with no notice emitted — the silent failure this hook exists
# to prevent. Asserted as an invariant over both files rather than against a
# literal, so raising either internal `timeout` without raising the host budget
# fails here instead of reintroducing the gap quietly.
#
# Every `timeout N` in the script counts, comments included: a stray "timeout
# 300" in prose only makes this stricter, which is the safe direction.
script_budget=0
timeout_count=0
while read -r n; do
[[ -n "$n" ]] || continue
script_budget=$((script_budget + n))
timeout_count=$((timeout_count + 1))
done < <(grep -oE '\btimeout [0-9]+\b' "$HOOK" | grep -oE '[0-9]+')
hook_timeout="$(python3 -c 'import json,sys; d=json.load(open(sys.argv[1])); print(d["hooks"]["SessionStart"][0]["hooks"][0]["timeout"])' "$HOOKS_JSON")"
if [[ $timeout_count -eq 0 ]]; then
fail "found no 'timeout N' in $HOOK — the budget assertion below would be vacuous"
else
pass "parsed $timeout_count internal timeout(s) totalling ${script_budget}s from the hook script"
[[ $hook_timeout -gt $script_budget ]] \
&& pass "hooks.json timeout (${hook_timeout}s) exceeds the script's own budget (${script_budget}s)" \
|| fail "hooks.json timeout is ${hook_timeout}s but the script can spend ${script_budget}s — the host would SIGKILL it mid-update"
fi
# ---------------------------------------------------------------------------
echo ""
echo "--- the greped phrase, against the real apm ---"
# ---------------------------------------------------------------------------
# Everything above mocks `apm`, so an apm release that reworded its summary line
# would revert this hook to its pre-fix behaviour with a fully green suite.
# `apm outdated` has no --json/machine-readable flag (verified against 0.28.0),
# so the phrase match cannot be replaced — it can only be pinned.
#
# The probe stages a genuinely outdated dependency with no network: a local git
# repo stands in for the upstream, reached through `url.<path>.insteadOf`
# rewrites of every URL spelling apm may build (it picks ssh or https depending
# on ambient auth config, so all three are mapped). apm appends `.git` to the
# repo URL, which is why the local repo is named `upstream.git` and the rewrite
# target omits the suffix. HOME is redirected so no user-level apm cache or
# credential state can influence the result.
#
# The genuine output is then replayed into the real hook through the mock, so
# what is asserted is the hook's own matching logic against real apm text —
# no pattern is duplicated here to drift out of sync.
SKIP_REASON=""
if ! command -v apm > /dev/null 2>&1 || ! command -v git > /dev/null 2>&1; then
SKIP_REASON="SKIP: apm and git are both required to verify the hook's phrase match against real \`apm outdated\` output — everything above ran, this axis did not"
echo " $SKIP_REASON"
else
PROBE="$(mktemp -d)"
trap 'rm -rf "$FAKE_BIN" "$WORK" "$PROBE"' EXIT
UPSTREAM="$PROBE/upstream.git"
git init -q "$UPSTREAM"
git -C "$UPSTREAM" -c user.email=probe@example.invalid -c user.name=probe \
commit -q --allow-empty -m one
LOCKED_SHA="$(git -C "$UPSTREAM" rev-parse HEAD)"
git -C "$UPSTREAM" -c user.email=probe@example.invalid -c user.name=probe \
commit -q --allow-empty -m two
BRANCH="$(git -C "$UPSTREAM" symbolic-ref --short HEAD)"
{
for repo in alpha beta; do
printf '[url "%s/upstream"]\n' "$PROBE"
printf '\tinsteadOf = git@apm-probe.invalid:probe/%s\n' "$repo"
printf '\tinsteadOf = https://apm-probe.invalid/probe/%s\n' "$repo"
printf '\tinsteadOf = ssh://git@apm-probe.invalid/probe/%s\n' "$repo"
done
} > "$PROBE/gitconfig"
mkdir -p "$PROBE/consumer" "$PROBE/home"
# $1 = how many stale dependencies to stage. Writes a lockfile and echoes what
# the real `apm outdated` printed for it.
real_apm_outdated() {
local want="$1" repo
{
echo "lockfile_version: '1'"
echo "generated_at: '2026-01-01T00:00:00+00:00'"
echo "apm_version: 0.0.0"
echo "dependencies:"
for repo in $( [[ "$want" == 1 ]] && echo alpha || echo alpha beta ); do
echo "- host: apm-probe.invalid"
echo " name: probe-$repo"
echo " package_type: apm_package"
echo " repo_url: probe/$repo"
echo " resolved_ref: $BRANCH"
echo " resolved_commit: $LOCKED_SHA"
echo " version: 1.0.0"
done
echo "deployments: []"
} > "$PROBE/consumer/apm.lock.yaml"
(
cd "$PROBE/consumer" &&
env HOME="$PROBE/home" \
GIT_CONFIG_GLOBAL="$PROBE/gitconfig" \
GIT_CONFIG_NOSYSTEM=1 \
GIT_TERMINAL_PROMPT=0 \
apm outdated 2>&1
)
}
# Replay genuine output through the hook. A harness that stages nothing would
# make every assertion below vacuously true, so the staged row is checked
# first and a failure to stage is a FAIL, not a quiet pass.
for want in 1 2; do
genuine="$(real_apm_outdated "$want" || true)"
if ! grep -q "outdated" <<< "$genuine"; then
fail "probe staged no outdated dependency against the real apm (harness broken, not the hook): $(tr '\n' ' ' <<< "$genuine" | cut -c1-160)"
continue
fi
printf '%s\n' "$genuine" > "$PROBE/genuine-$want.txt"
cat > "$FAKE_BIN/apm" << EOF
#!/usr/bin/env bash
pwd > "$WORK/apm-cwd"
case "\$1" in
outdated) cat "$PROBE/genuine-$want.txt"; exit 0 ;;
update) touch "$WORK/update-was-called"; exit 0 ;;
esac
exit 0
EOF
chmod +x "$FAKE_BIN/apm"
rm -f "$WORK/update-was-called"
out="$(run_hook)"
if [[ -n "$out" ]] && grep -q "$want package" <<< "$(json_field additionalContext 2>/dev/null <<< "$out")"; then
pass "detects staleness in real \`apm outdated\` output and counts $want package(s)"
else
fail "real apm reported $want outdated dependency/dependencies but the hook did not act on it — apm reworded its summary line. Real output: $(tr '\n' ' ' < "$PROBE/genuine-$want.txt" | tail -c 120)"
fi
done
fi
# ---------------------------------------------------------------------------
echo ""
echo "Results: $PASS passed, $FAIL failed"
[[ $FAIL -eq 0 ]] || exit 1
# A skip only after everything runnable has run and passed: the mocked axis is
# still worth executing on a machine without apm, but the suite must not read
# as green when the real-apm axis was not verified. run-tests.sh reports 77 as
# SKIPPED and, at pre-push (--strict), as a setup error naming this reason.
[[ -z "$SKIP_REASON" ]] || exit 77
exit 0