diff --git a/.claude/scripts/worktree.sh b/.claude/scripts/worktree.sh index 72fa589..2f9272e 100755 --- a/.claude/scripts/worktree.sh +++ b/.claude/scripts/worktree.sh @@ -22,6 +22,22 @@ esac # Two-root derivation (issue #63): script_dir = sibling scripts, root = consumer project. # shellcheck source=resolve-roots.sh . "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/resolve-roots.sh" + +# On `setup`, link the main checkout's gitignored .env into the worktree (same +# pattern as prepare-pr.sh for test-pr worktrees): .env is never checked out, +# so gates and app code that read it fail in a fresh worktree for lack of +# credentials rather than real defects. Resolved from the CALLER's cwd, not +# $root — in the plugin-cache layout $root is the main checkout, while the +# implementer invoking this script stands inside its worktree. Best-effort and +# skip-if-present (a worktree-local .env wins); runs before the adapter checks +# below so unconfigured repos still get it. +if [ "$phase" = setup ]; then + wt="$(git rev-parse --show-toplevel 2>/dev/null || true)" + main="$(cd "${wt:-.}" 2>/dev/null && cd "$(git rev-parse --git-common-dir 2>/dev/null)/.." 2>/dev/null && pwd)" + if [ -n "$wt" ] && [ -n "$main" ] && [ "$main" != "$wt" ] && [ -f "$main/.env" ] && [ ! -e "$wt/.env" ]; then + ln -s "$main/.env" "$wt/.env" && echo "▶ linked $wt/.env → $main/.env" + fi +fi # Which adapter to read. Defaults to the project adapter; set GATES_FILE to run a # different one (e.g. GATES_FILE=.claude/self/gates.json). Relative paths resolve # from the repo root. diff --git a/.claude/scripts/worktree.test.sh b/.claude/scripts/worktree.test.sh new file mode 100644 index 0000000..573d36b --- /dev/null +++ b/.claude/scripts/worktree.test.sh @@ -0,0 +1,116 @@ +#!/usr/bin/env bash +# worktree.test.sh — offline smoke test for worktree.sh's .env bootstrap. +# Builds a THROWAWAY temp git repo with real `git worktree add` worktrees and +# exercises the real worktree.sh against it, asserting: +# 1. setup inside a fresh worktree -> .env symlinked from the main checkout +# (and the link resolves to the main checkout's content) +# 2. setup with a worktree-local .env already present -> preserved, not +# replaced (worktree-local wins) +# 3. setup in the MAIN checkout -> no self-link, .env untouched +# 4. setup with no .env in the main checkout -> no link, no crash +# 5. teardown -> never creates a link +# 6. the linked .env still resolves as gitignored inside the worktree +# +# Exit 0 on success, non-zero if any assertion fails. Runnable bare: +# bash .claude/scripts/worktree.test.sh +set -uo pipefail + +script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +worktree_src="$script_dir/worktree.sh" +resolve_roots_src="$script_dir/resolve-roots.sh" + +work="$(mktemp -d "${TMPDIR:-/tmp}/worktree-test.XXXXXX")" +trap 'rm -rf "$work"' EXIT + +fail=0 +ok=0 +check() { + local desc="$1"; shift + if "$@"; then + ok=$((ok + 1)) + echo "ok - $desc" + else + fail=1 + echo "FAIL - $desc" + fi +} + +# Throwaway "consumer project" git repo: real commits + real `git worktree +# add`, with the REAL worktree.sh + resolve-roots.sh copied into its +# .claude/scripts/ (mirrors worktree-cleanup.test.sh's fixture pattern), so +# root derivation matches how the script resolves things in production. +repo="$work/repo" +mkdir -p "$repo/.claude/scripts" +git init -q -b main "$repo" +git -C "$repo" config user.email "test@example.com" +git -C "$repo" config user.name "Test" +printf '.env\n.env.*\n' > "$repo/.gitignore" +cp "$worktree_src" "$repo/.claude/scripts/worktree.sh" +cp "$resolve_roots_src" "$repo/.claude/scripts/resolve-roots.sh" +git -C "$repo" add .gitignore .claude/scripts/worktree.sh .claude/scripts/resolve-roots.sh +git -C "$repo" commit -q -m "seed" +echo "SECRET=main-checkout" > "$repo/.env" + +run_setup() { + # $1 = directory to run from (the worktree the "implementer" stands in). + ( cd "$1" && bash .claude/scripts/worktree.sh setup >/dev/null 2>&1 ) +} + +# --------------------------------------------------------------------------- +# Scenario 1: fresh worktree -> .env symlinked from the main checkout. +# --------------------------------------------------------------------------- +wt1="$repo/.claude/worktrees/agent-one" +git -C "$repo" worktree add -q -b feat/one "$wt1" main +run_setup "$wt1" +check "s1: .env exists in fresh worktree after setup" test -e "$wt1/.env" +check "s1: .env is a symlink" test -L "$wt1/.env" +check "s1: .env resolves to main checkout content" grep -q "SECRET=main-checkout" "$wt1/.env" + +# --------------------------------------------------------------------------- +# Scenario 2: worktree-local .env already present -> preserved. +# --------------------------------------------------------------------------- +wt2="$repo/.claude/worktrees/agent-two" +git -C "$repo" worktree add -q -b feat/two "$wt2" main +echo "SECRET=worktree-local" > "$wt2/.env" +run_setup "$wt2" +check "s2: pre-existing worktree .env preserved" grep -q "SECRET=worktree-local" "$wt2/.env" +check "s2: pre-existing worktree .env not turned into a link" test ! -L "$wt2/.env" + +# --------------------------------------------------------------------------- +# Scenario 3: setup in the MAIN checkout -> no self-link, file untouched. +# --------------------------------------------------------------------------- +run_setup "$repo" +check "s3: main checkout .env untouched" grep -q "SECRET=main-checkout" "$repo/.env" +check "s3: main checkout .env is not a symlink" test ! -L "$repo/.env" + +# --------------------------------------------------------------------------- +# Scenario 4: no .env in the main checkout -> no link, clean exit. +# --------------------------------------------------------------------------- +rm "$repo/.env" +wt4="$repo/.claude/worktrees/agent-four" +git -C "$repo" worktree add -q -b feat/four "$wt4" main +run_setup "$wt4" +rc=$? +check "s4: setup exits 0 without a main .env" test "$rc" -eq 0 +check "s4: no .env materialized in worktree" test ! -e "$wt4/.env" +echo "SECRET=main-checkout" > "$repo/.env" + +# --------------------------------------------------------------------------- +# Scenario 5: teardown never creates a link. +# --------------------------------------------------------------------------- +wt5="$repo/.claude/worktrees/agent-five" +git -C "$repo" worktree add -q -b feat/five "$wt5" main +( cd "$wt5" && bash .claude/scripts/worktree.sh teardown >/dev/null 2>&1 ) +check "s5: teardown creates no .env" test ! -e "$wt5/.env" + +# --------------------------------------------------------------------------- +# Scenario 6: the linked .env is still gitignored inside the worktree. +# --------------------------------------------------------------------------- +check "s6: linked .env resolves as ignored" git -C "$wt1" check-ignore -q .env + +echo +if [ "$fail" -ne 0 ]; then + echo "worktree.test.sh: FAILED" + exit 1 +fi +echo "worktree.test.sh: all $ok checks passed"