--check's throwaway pack copy seeded .claude-plugin/plugin.json and
.github/plugin/plugin.json from the real plugin dir, then packed without
--force -- apm pack silently skips regenerating a plugin.json that already
exists, so the diff always compared the copy against itself and never caught
drift in the compiled name/version/description/mcpServers. --force is now
always passed; in check mode it forces regeneration inside the throwaway copy
only, which sync_plugin_manifest() then diffs against the real committed
manifest.
sync_hooks_json() returned early whenever .apm/hooks/ was missing, without
checking whether a stale hooks.json was still sitting at the plugin root from
a prior sync -- unlike sync_dir(), which already detects that kind of orphaned
mirrored output. It now mirrors sync_dir()'s shape: flagged as drift in
--check, removed on a real sync.
Running the corrected --check --all against this repo's own plugins surfaced
3 real orphans: plugins/{git,gitea,core}/hooks.json, empty stubs added in
4edaaac only to satisfy an old plugin.json pointer-field check that no longer
exists (their compiled plugin.json has never had a hooks field, and none of
the three ever had .apm/hooks/). Removed as part of this fix since they're
exactly the drift the corrected check now catches -- leaving them would break
the sync-plugin-content pre-push gate on this branch.
Also extracts two shared helpers into scripts/lib/, sourced by this script and
others so a future bug fix doesn't need hand-applying three times:
- marketplace-plugins.sh: walks marketplace.json for local plugin dirs (this
script's --all branch and check-manifests.sh had near-identical copies)
- batch-run.sh: the bounded-batch concurrent job runner (this script,
tests/run-tests.sh, and tests/run-bats.sh each hand-rolled the same
core-count-capped wait loop independently)
Extended tests/test-sync-plugin-content.sh with coverage for both drift cases
(plugin.json version-bump drift, orphaned-hooks.json drift), including that a
re-sync clears each.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01X7GvKuJfy2WrdBmUttV4DT
104 lines
2.9 KiB
Bash
Executable File
104 lines
2.9 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# Run all test-*.sh files in the repo (including plugins) and the bats suite.
|
|
# Usage: bash tests/run-tests.sh [--bats-only]
|
|
#
|
|
# A script exiting 77 (the automake convention) is reported as SKIPPED, not
|
|
# passed — a suite that can't run for lack of a binary must not read as green.
|
|
#
|
|
# TEST_DIR — override root to search for test-*.sh (default: REPO_ROOT); used by tests.
|
|
set -euo pipefail
|
|
|
|
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
BATS="$REPO_ROOT/tests/run-bats.sh"
|
|
BATS_ONLY=false
|
|
[[ "${1:-}" == "--bats-only" ]] && BATS_ONLY=true
|
|
|
|
SEARCH_ROOT="${TEST_DIR:-$REPO_ROOT}"
|
|
|
|
FAILED=()
|
|
SKIPPED=()
|
|
PASSED=0
|
|
SKIP_EXIT=77
|
|
|
|
run_bats() {
|
|
if [[ -x "$BATS" ]]; then
|
|
echo "=== bats ==="
|
|
bash "$BATS"
|
|
echo ""
|
|
fi
|
|
}
|
|
|
|
if $BATS_ONLY; then
|
|
run_bats
|
|
exit 0
|
|
fi
|
|
|
|
run_bats
|
|
|
|
# Collected with a `while read` loop rather than `mapfile` — macOS ships
|
|
# /bin/bash 3.2, which has no `mapfile`. Process substitution (not a pipe)
|
|
# keeps the loop in this shell so the appends survive. `sort` is still fed
|
|
# newline-delimited output, exactly as before.
|
|
SCRIPTS=()
|
|
while IFS= read -r script; do
|
|
SCRIPTS+=("$script")
|
|
done < <(
|
|
find "$SEARCH_ROOT" -name "test-*.sh" \
|
|
-not -path "*/.git/*" \
|
|
-not -path "*/.claude/worktrees/*" \
|
|
| sort
|
|
)
|
|
|
|
# Each test-*.sh is independent (fixtures live under its own mktemp dir, none
|
|
# write back into the live repo tree -- verified before adding this), so they
|
|
# run concurrently in fixed-size batches instead of one at a time. Dispatch and
|
|
# throttling is scripts/lib/batch-run.sh's batch_run -- shared with
|
|
# scripts/sync-plugin-content.sh and tests/run-bats.sh so a batching bug fix
|
|
# only needs to land once; see that file for why this is batched rather than a
|
|
# rolling `wait -n` pool.
|
|
SCRATCH_ROOT="$(mktemp -d)"
|
|
trap 'rm -rf "$SCRATCH_ROOT"' EXIT
|
|
# shellcheck source=lib/batch-run.sh
|
|
source "$REPO_ROOT/scripts/lib/batch-run.sh"
|
|
|
|
declare -a batch_args=()
|
|
idx=0
|
|
for script in ${SCRIPTS[@]+"${SCRIPTS[@]}"}; do
|
|
idx=$((idx + 1))
|
|
cmd="$(printf 'rc=0; bash %q || rc=$?; echo "$rc" >%q' "$script" "$SCRATCH_ROOT/$idx.status")"
|
|
batch_args+=("$idx" "$cmd")
|
|
done
|
|
batch_run "$SCRATCH_ROOT" ${batch_args[@]+"${batch_args[@]}"}
|
|
|
|
idx=0
|
|
for script in ${SCRIPTS[@]+"${SCRIPTS[@]}"}; do
|
|
idx=$((idx + 1))
|
|
rel="${script#"$SEARCH_ROOT/"}"
|
|
echo "=== $rel ==="
|
|
cat "$SCRATCH_ROOT/$idx.log"
|
|
rc="$(cat "$SCRATCH_ROOT/$idx.status" 2>/dev/null || echo 1)"
|
|
if [[ $rc -eq 0 ]]; then
|
|
PASSED=$((PASSED + 1))
|
|
elif [[ $rc -eq $SKIP_EXIT ]]; then
|
|
SKIPPED+=("$rel")
|
|
else
|
|
FAILED+=("$rel")
|
|
fi
|
|
echo ""
|
|
done
|
|
|
|
echo "=== Summary: $PASSED passed, ${#SKIPPED[@]} skipped, ${#FAILED[@]} failed ==="
|
|
if [[ ${#SKIPPED[@]} -gt 0 ]]; then
|
|
echo "Skipped scripts:"
|
|
for s in ${SKIPPED[@]+"${SKIPPED[@]}"}; do
|
|
echo " $s"
|
|
done
|
|
fi
|
|
if [[ ${#FAILED[@]} -gt 0 ]]; then
|
|
echo "Failed scripts:"
|
|
for s in ${FAILED[@]+"${FAILED[@]}"}; do
|
|
echo " $s"
|
|
done
|
|
exit 1
|
|
fi
|