#!/usr/bin/env bash set -euo pipefail REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" SCRIPT="$REPO_ROOT/scripts/sync-marketplace-mirror.sh" PASS=0 FAIL=0 pass() { echo " PASS: $1"; PASS=$((PASS + 1)); } fail() { echo " FAIL: $1"; FAIL=$((FAIL + 1)); } # Fixture: a temp directory that is its own git worktree. Since # scripts/sync-marketplace-mirror.sh resolves REPO_ROOT via # `git rev-parse --show-toplevel 2>/dev/null || pwd`, isolation has to come # from that call answering "the fixture" -- so the fixture owns a real .git. # # Relying instead on `git rev-parse` FAILING inside a bare `mktemp -d` (its # previous form) is not isolation: two ordinary conditions make it succeed and # resolve to the LIVE repo, at which point every test writes to this repo's own # tracked .github/plugin/marketplace.json. Both are reproduced and fixed here: # - TMPDIR pointing inside a git worktree, which puts the fixture in one. # - An inherited GIT_DIR/GIT_WORK_TREE, which re-targets `git -C` and # `git rev-parse` regardless of cwd. tests/run-tests.sh is itself a pre-push # hook, and git hooks export exactly those variables -- the same leak # tests/test-git-hooks-install.sh:6-10 already defends against. # run_script() strips GIT_DIR/GIT_WORK_TREE for the second; `git init` here # covers the first and makes the resolution positive rather than accidental. # run-tests.sh:52-53 asserts no test writes back into the live repo tree, and # that claim now carries the concurrent runner. make_fixture() { local dir dir="$(mktemp -d)" # Physical path: `git rev-parse --show-toplevel` reports the resolved path, # and on macOS `mktemp -d` hands back one under the /tmp -> /private/tmp # symlink. Without -P the script's REPO_ROOT and the assertions' $FIXTURE # would name the same directory differently and every diff would compare # against a path the script never wrote. dir="$(cd "$dir" && pwd -P)" # Checked, not best-effort: this init IS the isolation invariant. `echo "$dir"` # is the last statement, so a silently-failed init would return a plain temp # dir, `git rev-parse --show-toplevel` would walk up to whatever repo encloses # it, and the suite would go back to writing into the live tree. if ! env -u GIT_DIR -u GIT_WORK_TREE git -C "$dir" init -q >/dev/null 2>&1; then echo "make_fixture: 'git init' failed in $dir — every test would then resolve REPO_ROOT to an enclosing repo and write outside the fixture" >&2 exit 1 fi echo "$dir" } SRC_REL=".claude-plugin/marketplace.json" DST_REL=".github/plugin/marketplace.json" CONTENT_A='{"plugins":[{"name":"demo","version":"1.0.0"}]}' CONTENT_B='{"plugins":[{"name":"demo","version":"2.0.0"}]}' write_src() { local dir="$1" content="$2" mkdir -p "$dir/.claude-plugin" printf '%s' "$content" > "$dir/$SRC_REL" } write_dst() { local dir="$1" content="$2" mkdir -p "$dir/.github/plugin" printf '%s' "$content" > "$dir/$DST_REL" } # `env -u GIT_DIR -u GIT_WORK_TREE` mirrors tests/test-git-hooks-install.sh:10: # under a git hook (run-tests.sh runs as pre-push) those are exported, and the # script's `git rev-parse --show-toplevel` would then answer with the LIVE repo # no matter which directory it was invoked from. run_script() { local dir="$1" shift (cd "$dir" && env -u GIT_DIR -u GIT_WORK_TREE bash "$SCRIPT" "$@") } CLEANUP_DIRS=() trap 'rm -rf ${CLEANUP_DIRS[@]+"${CLEANUP_DIRS[@]}"}' EXIT track() { CLEANUP_DIRS+=("$1"); } # --- 1. No .claude-plugin/marketplace.json at all: real-sync mode is a no-op, exit 0 --- echo "" echo "--- missing source: real sync exits 0 and creates nothing ---" FIXTURE1="$(make_fixture)"; track "$FIXTURE1" if run_script "$FIXTURE1" > /dev/null 2>&1 && [[ ! -e "$FIXTURE1/$DST_REL" ]]; then pass "missing source exits 0 in real-sync mode without creating a mirror" else fail "missing source should exit 0 and create nothing in real-sync mode" fi # --- 2. No source: --check mode is also a no-op, exit 0 (nothing to drift-check) --- echo "" echo "--- missing source: --check exits 0 (nothing to compare) ---" FIXTURE2="$(make_fixture)"; track "$FIXTURE2" if run_script "$FIXTURE2" --check > /dev/null 2>&1; then pass "missing source exits 0 in --check mode" else fail "missing source should exit 0 in --check mode, not report drift" fi # --- 2b. Source missing but a mirror still present: --check must FAIL --- # --check used to exit 0 on any missing source, so deleting # .claude-plugin/marketplace.json left a stale .github/plugin/marketplace.json # reported as "no drift" -- a mirror of a file that no longer exists. That is # the silent divergence this script's header says it prevents ("keeps that # legacy mirror byte-identical ... instead of letting it silently drift"), and # scripts/sync-plugin-content.sh --check --all already errors on the same # condition. Case 2 above still holds: neither file present stays a no-op. echo "" echo "--- missing source with a surviving mirror: --check reports drift ---" FIXTURE2B="$(make_fixture)"; track "$FIXTURE2B" write_dst "$FIXTURE2B" "$CONTENT_A" if run_script "$FIXTURE2B" --check > /dev/null 2>&1; then fail "exited 0 with a stale mirror and no source -- expected drift (exit 1)" else pass "a mirror with no source left to mirror is reported as drift" fi # Real-sync mode keeps its no-op: it has nothing to copy, and deleting a # tracked file is not this script's call to make. if run_script "$FIXTURE2B" > /dev/null 2>&1 && [[ -f "$FIXTURE2B/$DST_REL" ]]; then pass "real-sync mode still no-ops on a missing source, leaving the mirror alone" else fail "real-sync mode should no-op on a missing source, not fail or delete the mirror" fi # --- 3. Source exists, mirror missing entirely: --check reports drift (exit 1) --- echo "" echo "--- --check reports drift when the mirror file does not exist yet ---" FIXTURE3="$(make_fixture)"; track "$FIXTURE3" write_src "$FIXTURE3" "$CONTENT_A" if run_script "$FIXTURE3" --check > /dev/null 2>&1; then fail "exited 0 with no mirror file present -- expected drift (exit 1)" else pass "missing mirror file is reported as drift" fi # --- 4. Real sync creates the mirror (and its parent dir) byte-identical to the source --- echo "" echo "--- real sync creates .github/plugin/marketplace.json byte-identical to source ---" FIXTURE4="$(make_fixture)"; track "$FIXTURE4" write_src "$FIXTURE4" "$CONTENT_A" if run_script "$FIXTURE4" > /dev/null 2>&1 \ && [[ -f "$FIXTURE4/$DST_REL" ]] \ && diff -q "$FIXTURE4/$SRC_REL" "$FIXTURE4/$DST_REL" > /dev/null 2>&1; then pass "real sync creates the mirror file identical to the source" else fail "real sync did not create a byte-identical mirror file" fi # --- 5. --check is clean immediately after a real sync --- echo "" echo "--- --check is clean right after syncing ---" if run_script "$FIXTURE4" --check > /dev/null 2>&1; then pass "no drift reported immediately after syncing" else fail "drift reported right after syncing -- sync and check disagree" fi # --- 6. Editing the source after a sync introduces drift; re-sync clears it --- echo "" echo "--- source edited after sync is detected as drift, cleared by re-sync ---" write_src "$FIXTURE4" "$CONTENT_B" if run_script "$FIXTURE4" --check > /dev/null 2>&1; then fail "no drift reported after editing the source -- expected drift" else pass "editing the source after a sync is detected as drift" run_script "$FIXTURE4" > /dev/null 2>&1 if run_script "$FIXTURE4" --check > /dev/null 2>&1; then pass "re-sync clears the drift" else fail "re-sync did not clear the drift" fi fi # --- 7. A mirror that was hand-seeded already identical to the source: --check is clean --- echo "" echo "--- --check is clean when the mirror already matches, without ever syncing ---" FIXTURE7="$(make_fixture)"; track "$FIXTURE7" write_src "$FIXTURE7" "$CONTENT_A" write_dst "$FIXTURE7" "$CONTENT_A" if run_script "$FIXTURE7" --check > /dev/null 2>&1; then pass "an already-in-sync mirror reports no drift" else fail "an already-in-sync mirror should report no drift" fi # --- 8. A stale, pre-existing mirror that differs from source: --check flags it, sync fixes it --- echo "" echo "--- a stale pre-existing mirror is flagged by --check and overwritten by sync ---" FIXTURE8="$(make_fixture)"; track "$FIXTURE8" write_src "$FIXTURE8" "$CONTENT_A" write_dst "$FIXTURE8" "$CONTENT_B" if run_script "$FIXTURE8" --check > /dev/null 2>&1; then fail "a stale mirror was not flagged as drift" else pass "a stale pre-existing mirror is flagged as drift" fi run_script "$FIXTURE8" > /dev/null 2>&1 if diff -q "$FIXTURE8/$SRC_REL" "$FIXTURE8/$DST_REL" > /dev/null 2>&1; then pass "real sync overwrites the stale mirror to match the source" else fail "real sync did not overwrite the stale mirror" fi # --- 9. An unknown/extra positional argument is rejected with a usage error --- echo "" echo "--- an unrecognized argument is rejected (usage, exit 1) ---" FIXTURE9="$(make_fixture)"; track "$FIXTURE9" write_src "$FIXTURE9" "$CONTENT_A" if run_script "$FIXTURE9" --bogus > /dev/null 2>&1; then fail "exited 0 with an unrecognized argument -- expected a usage error" else pass "an unrecognized argument is rejected" fi # --- 10. --check followed by a stray extra argument is also rejected --- echo "" echo "--- --check plus a trailing extra argument is rejected (usage, exit 1) ---" if run_script "$FIXTURE9" --check extra > /dev/null 2>&1; then fail "exited 0 with --check plus a trailing argument -- expected a usage error" else pass "--check plus a trailing extra argument is rejected" fi # --- 11. Real sync is idempotent: running it twice in a row leaves the mirror unchanged --- echo "" echo "--- running real sync twice in a row is idempotent ---" FIXTURE11="$(make_fixture)"; track "$FIXTURE11" write_src "$FIXTURE11" "$CONTENT_A" run_script "$FIXTURE11" > /dev/null 2>&1 run_script "$FIXTURE11" > /dev/null 2>&1 if run_script "$FIXTURE11" --check > /dev/null 2>&1; then pass "running sync twice in a row is idempotent" else fail "a second sync run introduced unexpected drift" fi # --- 12. Fixture isolation survives an inherited GIT_DIR/GIT_WORK_TREE --- # The whole suite's isolation is REPO_ROOT resolving to the fixture. With # GIT_DIR/GIT_WORK_TREE exported -- which is every git-hook context, and # run-tests.sh runs as pre-push -- `git rev-parse --show-toplevel` answers with # THAT repo from any cwd, so the script wrote to the live tree and 5 of these # cases failed. Point both variables at a decoy repo (never the live one, so # this assertion cannot itself write where it must not) and assert the fixture # still wins: the mirror lands in the fixture and the decoy stays untouched. echo "" echo "--- fixture isolation holds with GIT_DIR/GIT_WORK_TREE inherited from elsewhere ---" FIXTURE12="$(make_fixture)"; track "$FIXTURE12" DECOY="$(make_fixture)"; track "$DECOY" write_src "$FIXTURE12" "$CONTENT_A" if (export GIT_DIR="$DECOY/.git" GIT_WORK_TREE="$DECOY"; run_script "$FIXTURE12" > /dev/null 2>&1) \ && [[ -f "$FIXTURE12/$DST_REL" ]] && [[ ! -e "$DECOY/$DST_REL" ]]; then pass "an inherited GIT_DIR/GIT_WORK_TREE does not redirect writes out of the fixture" else fail "an inherited GIT_DIR/GIT_WORK_TREE redirected the sync outside the fixture" fi # --- 13. Outside any git worktree, REPO_ROOT cannot be guessed: hard error --- # `git rev-parse --show-toplevel 2>/dev/null || pwd` used to fall back to $PWD. # Both of this script's exit-0 paths are "the two files agree" or "neither file # exists", so a REPO_ROOT that is not this repo reports "no drift" over a tree it # never inspected — run --check from an empty non-worktree directory and that was # the literal outcome. Case 2 above (a real worktree with no source file) still # exits 0; the difference is whether the tree was identified at all. # # GIT_CEILING_DIRECTORIES rather than trusting `mktemp -d` to land outside a # worktree: TMPDIR may itself sit inside one (the same hazard make_fixture's # header documents), in which case rev-parse would succeed and this case would # quietly test nothing. The ceiling stops git's upward walk at the fixture's # parent, and the precondition below asserts it actually did. echo "" echo "--- outside a git worktree, --check errors instead of guessing \$PWD ---" NOREPO_PARENT="$(mktemp -d)"; track "$NOREPO_PARENT" NOREPO_PARENT="$(cd "$NOREPO_PARENT" && pwd -P)" NOREPO="$NOREPO_PARENT/not-a-worktree" mkdir -p "$NOREPO" if (cd "$NOREPO" && env -u GIT_DIR -u GIT_WORK_TREE GIT_CEILING_DIRECTORIES="$NOREPO_PARENT" \ git rev-parse --show-toplevel > /dev/null 2>&1); then fail "precondition: git rev-parse still resolves a worktree under the ceiling — this case would test nothing" else for MODE in "--check" ""; do RC13=0 OUT13="$( (cd "$NOREPO" && env -u GIT_DIR -u GIT_WORK_TREE \ GIT_CEILING_DIRECTORIES="$NOREPO_PARENT" bash "$SCRIPT" ${MODE:+"$MODE"}) 2>&1 )" || RC13=$? case "$RC13:$OUT13" in 0:*) fail "'${MODE:-real sync}' exited 0 outside a git worktree — it reported on a tree it never identified" ;; *"not inside a git worktree"*) pass "'${MODE:-real sync}' errors with a not-a-worktree message instead of falling back to \$PWD" ;; *) fail "'${MODE:-real sync}' failed for an unexpected reason (rc=$RC13): $OUT13" ;; esac done # And it must not have written anything into the directory it refused to trust. if [[ ! -e "$NOREPO/.github" ]]; then pass "nothing is written into the unidentified directory" else fail "the script created files under a directory it could not identify as the repo root" fi fi echo "" echo "Results: $PASS passed, $FAIL failed" [[ $FAIL -eq 0 ]]