test: add run-tests.sh, fix stale tests for marketplace model

- Add tests/run-tests.sh: discovers and runs all test-*.sh (including
  plugin subdirs) and the bats suite; replaces per-script pre-push calls
- Add tests/run-tests.bats: TDD coverage for run-tests.sh behaviours
- Update setup-hooks.sh: pre-push block now calls run-tests.sh
- Fix test-install.sh: remove provider adapter symlink tests (adapter
  removed in marketplace migration), guard skills loops on dir existence
- Fix test-instructions-and-docs.sh: content index checks now point to
  core/AGENTS.md (where it lives), remove ard/bug dir assertions
- Fix test-setup-hooks.sh: assert pre-push hook calls run-tests.sh

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-25 19:47:20 +00:00
parent 93e3de4d02
commit 32cd2e3128
6 changed files with 207 additions and 112 deletions

View File

@@ -197,11 +197,7 @@ pre_push_block() {
HOOKS_SCRIPT_DIR="$script_dir" HOOKS_SCRIPT_DIR="$script_dir"
REPO_ROOT="\$(git rev-parse --show-toplevel)" REPO_ROOT="\$(git rev-parse --show-toplevel)"
echo "Running test suite..." bash "\$REPO_ROOT/tests/run-tests.sh"
bash "\$REPO_ROOT/tests/test-install.sh"
bash "\$REPO_ROOT/tests/test-governance-layer.sh"
bash "\$REPO_ROOT/tests/test-check-manifests.sh"
bash "\$REPO_ROOT/tests/test-setup-hooks.sh"
echo "Checking manifests..." echo "Checking manifests..."
bash "\$HOOKS_SCRIPT_DIR/check-manifests.sh" "\$REPO_ROOT" bash "\$HOOKS_SCRIPT_DIR/check-manifests.sh" "\$REPO_ROOT"

66
tests/run-tests.bats Normal file
View File

@@ -0,0 +1,66 @@
#!/usr/bin/env bats
REPO_ROOT="$(cd "$(dirname "$BATS_TEST_FILENAME")/.." && pwd)"
RUN_TESTS="$REPO_ROOT/tests/run-tests.sh"
setup() {
SCRATCH="$(mktemp -d)"
}
teardown() {
rm -rf "$SCRATCH"
}
# --- Tracer bullet ---
@test "exits 0 when all test scripts pass" {
mkdir -p "$SCRATCH/tests"
printf '#!/usr/bin/env bash\necho "ok"\n' > "$SCRATCH/tests/test-one.sh"
chmod +x "$SCRATCH/tests/test-one.sh"
TEST_DIR="$SCRATCH" run bash "$RUN_TESTS"
[ "$status" -eq 0 ]
}
# --- Failure detection ---
@test "exits non-zero when a test script fails" {
mkdir -p "$SCRATCH/tests"
printf '#!/usr/bin/env bash\nexit 1\n' > "$SCRATCH/tests/test-bad.sh"
chmod +x "$SCRATCH/tests/test-bad.sh"
TEST_DIR="$SCRATCH" run bash "$RUN_TESTS"
[ "$status" -ne 0 ]
}
@test "reports the name of the failing script" {
mkdir -p "$SCRATCH/tests"
printf '#!/usr/bin/env bash\nexit 1\n' > "$SCRATCH/tests/test-bad.sh"
chmod +x "$SCRATCH/tests/test-bad.sh"
TEST_DIR="$SCRATCH" run bash "$RUN_TESTS"
[[ "$output" == *"test-bad.sh"* ]]
}
# --- Subdirectory discovery ---
@test "discovers test scripts in subdirectories" {
mkdir -p "$SCRATCH/plugins/myplugin"
printf '#!/usr/bin/env bash\necho "plugin test ok"\n' > "$SCRATCH/plugins/myplugin/test-plugin.sh"
chmod +x "$SCRATCH/plugins/myplugin/test-plugin.sh"
TEST_DIR="$SCRATCH" run bash "$RUN_TESTS"
[ "$status" -eq 0 ]
[[ "$output" == *"test-plugin.sh"* ]]
}
@test "does not pick up scripts not named test-*.sh" {
mkdir -p "$SCRATCH/tests"
printf '#!/usr/bin/env bash\nexit 1\n' > "$SCRATCH/tests/run-bats.sh"
chmod +x "$SCRATCH/tests/run-bats.sh"
TEST_DIR="$SCRATCH" run bash "$RUN_TESTS"
[ "$status" -eq 0 ]
}
# --- Bats integration ---
@test "runs the bats suite when no TEST_DIR override is set" {
run bash "$RUN_TESTS" --bats-only
[ "$status" -eq 0 ]
}

57
tests/run-tests.sh Executable file
View File

@@ -0,0 +1,57 @@
#!/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]
#
# 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=()
PASSED=0
run_bats() {
if [[ -x "$BATS" ]]; then
echo "=== bats ==="
bash "$BATS"
echo ""
fi
}
if $BATS_ONLY; then
run_bats
exit 0
fi
run_bats
mapfile -t SCRIPTS < <(
find "$SEARCH_ROOT" -name "test-*.sh" \
-not -path "*/.git/*" \
| sort
)
for script in "${SCRIPTS[@]}"; do
rel="${script#"$SEARCH_ROOT/"}"
echo "=== $rel ==="
if bash "$script"; then
PASSED=$((PASSED + 1))
else
FAILED+=("$rel")
fi
echo ""
done
echo "=== Summary: $PASSED passed, ${#FAILED[@]} failed ==="
if [[ ${#FAILED[@]} -gt 0 ]]; then
echo "Failed scripts:"
for s in "${FAILED[@]}"; do
echo " $s"
done
exit 1
fi

View File

@@ -18,7 +18,6 @@ echo ""
echo "--- providers/claude-code/ → ~/.claude/ ---" echo "--- providers/claude-code/ → ~/.claude/ ---"
while IFS= read -r -d '' src; do while IFS= read -r -d '' src; do
rel="${src#"$REPO_ROOT/providers/claude-code/"}" rel="${src#"$REPO_ROOT/providers/claude-code/"}"
# provider-manifest.sh is sourced by install.sh, not deployed to ~/.claude/
[[ "$rel" == "provider-manifest.sh" ]] && continue [[ "$rel" == "provider-manifest.sh" ]] && continue
dest="$TEMP_HOME/.claude/$rel" dest="$TEMP_HOME/.claude/$rel"
if diff -q "$src" "$dest" > /dev/null 2>&1; then if diff -q "$src" "$dest" > /dev/null 2>&1; then
@@ -57,12 +56,13 @@ echo "--- core/AGENTS.md → ~/.agents/AGENTS.md (0015) ---"
if diff -q "$REPO_ROOT/core/AGENTS.md" "$TEMP_HOME/.agents/AGENTS.md" > /dev/null 2>&1; then if diff -q "$REPO_ROOT/core/AGENTS.md" "$TEMP_HOME/.agents/AGENTS.md" > /dev/null 2>&1; then
pass "core/AGENTS.md deployed and matches source" pass "core/AGENTS.md deployed and matches source"
else else
fail "~/.agents/AGENTS.md — missing or differs from source" fail "\$HOME/.agents/AGENTS.md — missing or differs from source"
fi fi
echo "" if [[ -d "$REPO_ROOT/.agents/skills" ]]; then
echo "--- skills deployed to ~/.agents/skills/ ---" echo ""
while IFS= read -r -d '' src_skill; do echo "--- skills deployed to ~/.agents/skills/ ---"
while IFS= read -r -d '' src_skill; do
skill_name="$(basename "$src_skill")" skill_name="$(basename "$src_skill")"
dest_skill="$TEMP_HOME/.agents/skills/$skill_name" dest_skill="$TEMP_HOME/.agents/skills/$skill_name"
if [[ -d "$dest_skill" ]]; then if [[ -d "$dest_skill" ]]; then
@@ -70,11 +70,11 @@ while IFS= read -r -d '' src_skill; do
else else
fail "$skill_name missing from ~/.agents/skills/" fail "$skill_name missing from ~/.agents/skills/"
fi fi
done < <(find "$REPO_ROOT/.agents/skills" -mindepth 1 -maxdepth 1 -type d -print0) done < <(find "$REPO_ROOT/.agents/skills" -mindepth 1 -maxdepth 1 -type d -print0)
echo "" echo ""
echo "--- skill files match source ---" echo "--- skill files match source ---"
while IFS= read -r -d '' src; do while IFS= read -r -d '' src; do
rel="${src#"$REPO_ROOT/.agents/skills/"}" rel="${src#"$REPO_ROOT/.agents/skills/"}"
dest="$TEMP_HOME/.agents/skills/$rel" dest="$TEMP_HOME/.agents/skills/$rel"
if diff -q "$src" "$dest" > /dev/null 2>&1; then if diff -q "$src" "$dest" > /dev/null 2>&1; then
@@ -82,31 +82,20 @@ while IFS= read -r -d '' src; do
else else
fail "skills/$rel — missing or differs from source" fail "skills/$rel — missing or differs from source"
fi fi
done < <(find "$REPO_ROOT/.agents/skills" -type f -print0) done < <(find "$REPO_ROOT/.agents/skills" -type f -print0)
echo "" echo ""
echo "--- provider adapter: ~/.claude/skills/ is a symlink to ~/.agents/skills/ ---" echo "--- skills correctly replaced on second install (no double-nesting) ---"
adapter="$TEMP_HOME/.claude/skills" first_skill="$(find "$REPO_ROOT/.agents/skills" -mindepth 1 -maxdepth 1 -type d | head -1)"
skills_canonical="$TEMP_HOME/.agents/skills" if [[ -n "$first_skill" ]]; then
if [[ -L "$adapter" ]]; then skill_name="$(basename "$first_skill")"
resolved="$(readlink "$adapter")" nested="$TEMP_HOME/.agents/skills/$skill_name/$skill_name"
if [[ "$resolved" == "$skills_canonical" ]]; then if [[ -d "$nested" ]]; then
pass "~/.claude/skills → ~/.agents/skills (correct target)"
else
fail "~/.claude/skills symlink points to wrong target: $resolved"
fi
else
fail "~/.claude/skills is not a symlink"
fi
echo ""
echo "--- skills correctly replaced on second install (no double-nesting) ---"
skill_name="$(basename "$(find "$REPO_ROOT/.agents/skills" -mindepth 1 -maxdepth 1 -type d | head -1)")"
nested="$TEMP_HOME/.agents/skills/$skill_name/$skill_name"
if [[ -d "$nested" ]]; then
fail "$skill_name/$skill_name exists — skill was nested instead of replaced" fail "$skill_name/$skill_name exists — skill was nested instead of replaced"
else else
pass "$skill_name not double-nested after second install" pass "$skill_name not double-nested after second install"
fi
fi
fi fi
echo "" echo ""
@@ -123,31 +112,17 @@ else
fail "pre-existing user skill was wiped by install" fail "pre-existing user skill was wiped by install"
fi fi
echo ""
echo "--- warning emitted when adapter target is a real directory ---"
TEMP_HOME3="$(mktemp -d)"
trap 'rm -rf "$TEMP_HOME3"' EXIT
mkdir -p "$TEMP_HOME3/.claude/skills/some-user-skill"
output="$(HOME="$TEMP_HOME3" bash "$REPO_ROOT/scripts/install.sh" 2>&1)"
if echo "$output" | grep -q "Warning"; then
pass "warning emitted when ~/.claude/skills exists as real directory"
else
fail "no warning when ~/.claude/skills is a real directory"
fi
if [[ -d "$TEMP_HOME3/.claude/skills/some-user-skill" ]]; then
pass "real directory left intact when warning emitted"
else
fail "real directory was destroyed despite warning"
fi
echo "" echo ""
echo "--- idempotency: second run state is correct ---" echo "--- idempotency: second run state is correct ---"
HOME="$TEMP_HOME" bash "$REPO_ROOT/scripts/install.sh" > /dev/null 2>&1 \ if HOME="$TEMP_HOME" bash "$REPO_ROOT/scripts/install.sh" > /dev/null 2>&1; then
&& pass "second run exits zero" \ pass "second run exits zero"
|| fail "second run failed" else
fail "second run failed"
fi
# skill files still match source after second run if [[ -d "$REPO_ROOT/.agents/skills" ]]; then
while IFS= read -r -d '' src; do # skill files still match source after second run
while IFS= read -r -d '' src; do
rel="${src#"$REPO_ROOT/.agents/skills/"}" rel="${src#"$REPO_ROOT/.agents/skills/"}"
dest="$TEMP_HOME/.agents/skills/$rel" dest="$TEMP_HOME/.agents/skills/$rel"
if diff -q "$src" "$dest" > /dev/null 2>&1; then if diff -q "$src" "$dest" > /dev/null 2>&1; then
@@ -155,10 +130,10 @@ while IFS= read -r -d '' src; do
else else
fail "idempotent: skills/$rel corrupted after second install" fail "idempotent: skills/$rel corrupted after second install"
fi fi
done < <(find "$REPO_ROOT/.agents/skills" -type f -print0) done < <(find "$REPO_ROOT/.agents/skills" -type f -print0)
# no double-nesting after second run # no double-nesting after second run
while IFS= read -r -d '' skill_dir; do while IFS= read -r -d '' skill_dir; do
skill_name="$(basename "$skill_dir")" skill_name="$(basename "$skill_dir")"
nested="$TEMP_HOME/.agents/skills/$skill_name/$skill_name" nested="$TEMP_HOME/.agents/skills/$skill_name/$skill_name"
if [[ -d "$nested" ]]; then if [[ -d "$nested" ]]; then
@@ -166,14 +141,7 @@ while IFS= read -r -d '' skill_dir; do
else else
pass "idempotent: $skill_name not double-nested after second install" pass "idempotent: $skill_name not double-nested after second install"
fi fi
done < <(find "$REPO_ROOT/.agents/skills" -mindepth 1 -maxdepth 1 -type d -print0) done < <(find "$REPO_ROOT/.agents/skills" -mindepth 1 -maxdepth 1 -type d -print0)
# symlink still correct after second run
adapter="$TEMP_HOME/.claude/skills"
if [[ -L "$adapter" ]] && [[ "$(readlink "$adapter")" == "$TEMP_HOME/.agents/skills" ]]; then
pass "idempotent: ~/.claude/skills symlink intact after second install"
else
fail "idempotent: ~/.claude/skills symlink broken after second install"
fi fi
# AGENTS.md still correct after second run # AGENTS.md still correct after second run

View File

@@ -1,4 +1,5 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# shellcheck disable=SC2015 # pass()/fail() always exit 0; A && pass || fail is safe here
set -euo pipefail set -euo pipefail
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
@@ -35,18 +36,19 @@ CLAUDE="$REPO_ROOT/providers/claude-code/CLAUDE.md"
&& pass "behavior: rules not duplicated in CLAUDE.md (moved to core/AGENTS.md)" \ && pass "behavior: rules not duplicated in CLAUDE.md (moved to core/AGENTS.md)" \
|| fail "behavior: rules still inline in CLAUDE.md — 0015 refactor incomplete" || fail "behavior: rules still inline in CLAUDE.md — 0015 refactor incomplete"
# Content index — must reference each on-demand file # Content index lives in core/AGENTS.md (deployed as ~/.agents/AGENTS.md), not in CLAUDE.md
contains "coding" "$CLAUDE" \ CORE_AGENTS_FOR_0004="$REPO_ROOT/core/AGENTS.md"
&& pass "content index: coding conventions trigger present" \ contains "coding" "$CORE_AGENTS_FOR_0004" \
|| fail "content index: coding conventions trigger missing" && pass "content index: coding conventions trigger present (core/AGENTS.md)" \
|| fail "content index: coding conventions trigger missing from core/AGENTS.md"
contains "git" "$CLAUDE" \ contains "git" "$CORE_AGENTS_FOR_0004" \
&& pass "content index: git conventions trigger present" \ && pass "content index: git conventions trigger present (core/AGENTS.md)" \
|| fail "content index: git conventions trigger missing" || fail "content index: git conventions trigger missing from core/AGENTS.md"
contains "testing" "$CLAUDE" \ contains "testing" "$CORE_AGENTS_FOR_0004" \
&& pass "content index: testing conventions trigger present" \ && pass "content index: testing conventions trigger present (core/AGENTS.md)" \
|| fail "content index: testing conventions trigger missing" || fail "content index: testing conventions trigger missing from core/AGENTS.md"
# global.md must be retired — no longer referenced in content index # global.md must be retired — no longer referenced in content index
! contains "global\.md" "$CLAUDE" \ ! contains "global\.md" "$CLAUDE" \
@@ -156,7 +158,7 @@ echo ""
echo "--- 0008: docs/ restructure ---" echo "--- 0008: docs/ restructure ---"
for dir in prd ard bug notes adr; do for dir in prd notes adr; do
[[ -d "$REPO_ROOT/docs/$dir" ]] \ [[ -d "$REPO_ROOT/docs/$dir" ]] \
&& pass "docs/$dir/ exists" \ && pass "docs/$dir/ exists" \
|| fail "docs/$dir/ missing" || fail "docs/$dir/ missing"
@@ -285,7 +287,8 @@ contains "core/AGENTS\.md" "$ARCH" \
&& pass "architecture.md: core/AGENTS.md entry present" \ && pass "architecture.md: core/AGENTS.md entry present" \
|| fail "architecture.md: core/AGENTS.md entry missing" || fail "architecture.md: core/AGENTS.md entry missing"
contains "~/\.agents/AGENTS\.md" "$ARCH" \ # shellcheck disable=SC2088 # tilde is a literal search string, not a path
grep -qF '~/.agents/AGENTS.md' "$ARCH" \
&& pass "architecture.md: ~/.agents/AGENTS.md deployment path present" \ && pass "architecture.md: ~/.agents/AGENTS.md deployment path present" \
|| fail "architecture.md: ~/.agents/AGENTS.md deployment path missing" || fail "architecture.md: ~/.agents/AGENTS.md deployment path missing"

View File

@@ -140,6 +140,11 @@ if grep -q "check-manifests" "$PUSH_HOOK"; then
else else
fail "pre-push hook missing check-manifests.sh call" fail "pre-push hook missing check-manifests.sh call"
fi fi
if grep -q "run-tests.sh" "$PUSH_HOOK"; then
pass "pre-push hook calls run-tests.sh"
else
fail "pre-push hook missing run-tests.sh call"
fi
# --- 6. Idempotent: second run replaces each block exactly once --- # --- 6. Idempotent: second run replaces each block exactly once ---
echo "" echo ""