Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions .claude/scripts/worktree.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
116 changes: 116 additions & 0 deletions .claude/scripts/worktree.test.sh
Original file line number Diff line number Diff line change
@@ -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"
Loading