Skip to content
Merged
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
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -122,8 +122,10 @@ sudo apt install bats
brew install bats-core
```

Tests are split one file per command (`test/*.bats`), with `--claude`-flag tests grouped under `test/claude/`. Run the whole suite with `-r` (recursive):

```sh
bats test/wt.bats
bats -r test
```

## License
Expand Down
22 changes: 22 additions & 0 deletions test/cd.bats
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
load helpers

setup() { wt_common_setup; }
teardown() { wt_common_teardown; }

# --- _wt_cd ---

@test "wt <name> navigates to worktree" {
_wt_cd feature
[ "$PWD" = "$TEST_REPO-feature" ]
}

@test "wt cd <name> navigates to worktree" {
wt cd other
[ "$PWD" = "$TEST_REPO-other" ]
}

@test "wt <name> returns error for no match" {
run _wt_cd nonexistent
[ "$status" -eq 1 ]
[[ "$output" == *"no worktree matching"* ]]
}
86 changes: 86 additions & 0 deletions test/claude/ls.bats
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
load ../helpers

setup() { wt_common_setup; }
teardown() { wt_common_teardown; }

# --- wt ls --claude ---

@test "wt ls --claude shows sessions for all worktrees, including the base one" {
local stubbin; stubbin=$(mktemp -d)
cat > "$stubbin/claude" <<EOF
#!/usr/bin/env bash
cat <<JSON
[
{"id":"base111","cwd":"$TEST_REPO","name":"base repo session","state":"done"},
{"id":"feat222","cwd":"$TEST_REPO-feature","name":"feature session","state":"blocked"}
]
JSON
EOF
chmod +x "$stubbin/claude"
cat > "$stubbin/jq" <<EOF
#!/usr/bin/env bash
exec "$(command -v jq)" "\$@"
EOF
Comment thread
Copilot marked this conversation as resolved.
chmod +x "$stubbin/jq"

PATH="$stubbin:$PATH" run wt ls --claude
[ "$status" -eq 0 ]
[[ "$output" == *"BRANCH"*"SESSION"*"NAME"*"STATE"* ]]
[[ "$output" == *"base111"* ]]
[[ "$output" == *"base repo session"* ]]
[[ "$output" == *"feat222"* ]]
[[ "$output" == *"feature session"* ]]
[[ "$output" == *"other"* ]]
# the main repo worktree's branch can drift over time (unlike a dedicated
# per-task worktree), so it should never be reported as a literal branch
# name - only the distinct "(main, branch varies)" label
[[ "$output" == *"(main, branch varies)"* ]]
rm -rf "$stubbin"
}

@test "wt ls --claude attributes sessions via job state when the agents cwd is stale" {
local stubbin; stubbin=$(mktemp -d)
local sid="11111111-2222-3333-4444-555555555555"
# `claude agents` reports the session's cwd as the main repo - the stale
# dispatch-time value, recorded before the agent entered its worktree
cat > "$stubbin/claude" <<EOF
#!/usr/bin/env bash
cat <<JSON
[
{"id":"stale001","sessionId":"$sid","cwd":"$TEST_REPO","name":"stale cwd session","state":"done"}
]
JSON
EOF
chmod +x "$stubbin/claude"
cat > "$stubbin/jq" <<EOF
#!/usr/bin/env bash
exec "$(command -v jq)" "\$@"
EOF
chmod +x "$stubbin/jq"

# ...but the background-job state knows the real worktree
local confdir; confdir=$(mktemp -d)
mkdir -p "$confdir/jobs/stale001"
cat > "$confdir/jobs/stale001/state.json" <<EOF
{"sessionId":"$sid","worktreePath":"$TEST_REPO-feature","worktreeBranch":"feature","state":"done"}
EOF

CLAUDE_CONFIG_DIR="$confdir" PATH="$stubbin:$PATH" run wt ls --claude
[ "$status" -eq 0 ]
# attributed to the feature worktree row, not the main repo row
[[ "$(echo "$output" | grep stale001)" == feature* ]]
! echo "$output" | grep "(main, branch varies)" | grep -q stale001
rm -rf "$stubbin" "$confdir"
}

@test "wt ls --claude falls back to plain listing when claude CLI is missing" {
local stubbin; stubbin=$(mktemp -d)
ln -s "$(command -v git)" "$stubbin/git"
ln -s "$(command -v awk)" "$stubbin/awk"

PATH="$stubbin" run wt ls --claude
[ "$status" -eq 0 ]
[[ "$output" == *"claude CLI not found"* ]]
[[ "$output" == *"feature"* ]]
rm -rf "$stubbin"
}
Comment thread
Copilot marked this conversation as resolved.
170 changes: 170 additions & 0 deletions test/claude/merged.bats
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
load ../helpers

setup() { wt_common_setup; }
teardown() { wt_common_teardown; }

# --- wt merged --claude ---

@test "wt merged --claude warns and falls back when claude CLI is missing" {
local base; base=$(git -C "$TEST_REPO" symbolic-ref --short HEAD)
git -C "$TEST_REPO-feature" commit -q --allow-empty -m "feature commit"
cd "$TEST_REPO"
git merge -q feature

local stubbin; stubbin=$(mktemp -d)
# only symlink the external commands wt.sh actually needs (git, awk) -
# deliberately no claude, so `command -v claude` fails regardless of the host PATH
ln -s "$(command -v git)" "$stubbin/git"
ln -s "$(command -v awk)" "$stubbin/awk"

PATH="$stubbin" run wt merged "$base" --claude
[ "$status" -eq 0 ]
[[ "$output" == *"claude CLI not found"* ]]
[[ "$output" == *"$TEST_REPO-feature"* ]]
rm -rf "$stubbin"
}

@test "wt merged --claude lists agent sessions per worktree" {
local base; base=$(git -C "$TEST_REPO" symbolic-ref --short HEAD)
git -C "$TEST_REPO-feature" commit -q --allow-empty -m "feature commit"
cd "$TEST_REPO"
git merge -q feature

local stubbin; stubbin=$(mktemp -d)
# fake claude returns two sessions: one for our merged worktree's cwd, one
# for an unrelated cwd - the unrelated one must be filtered out client-side
cat > "$stubbin/claude" <<EOF
#!/usr/bin/env bash
cat <<JSON
[
{"id":"abc123","cwd":"$TEST_REPO-feature","name":"do the thing","state":"done"},
{"id":"zzz999","cwd":"/somewhere/else","name":"unrelated","state":"done"}
]
JSON
EOF
chmod +x "$stubbin/claude"
cat > "$stubbin/jq" <<EOF
#!/usr/bin/env bash
exec "$(command -v jq)" "\$@"
EOF
chmod +x "$stubbin/jq"
Comment thread
Copilot marked this conversation as resolved.

PATH="$stubbin:$PATH" run wt merged "$base" --claude
[ "$status" -eq 0 ]
[[ "$output" == *"BRANCH"*"SESSION"*"NAME"*"STATE"* ]]
[[ "$output" == *"feature"* ]]
[[ "$output" == *"abc123"* ]]
[[ "$output" == *"do the thing"* ]]
[[ "$output" == *"done"* ]]
[[ "$output" != *"zzz999"* ]]
[[ "$output" != *"unrelated"* ]]
rm -rf "$stubbin"
}

@test "wt merged --claude labels the main repo worktree distinctly instead of asserting a branch" {
# the main repo worktree can itself end up in the merged list (checked out
# to some other merged, non-base branch) - unlike a dedicated per-task
# worktree, its current branch isn't a reliable record of what was checked
# out when a past session actually ran there
local base; base=$(git -C "$TEST_REPO" symbolic-ref --short HEAD)
cd "$TEST_REPO"
git checkout -qb main-drift

local stubbin; stubbin=$(mktemp -d)
cat > "$stubbin/claude" <<EOF
#!/usr/bin/env bash
cat <<JSON
[
{"id":"main111","cwd":"$TEST_REPO","name":"main repo session","state":"done"}
]
JSON
EOF
chmod +x "$stubbin/claude"
cat > "$stubbin/jq" <<EOF
#!/usr/bin/env bash
exec "$(command -v jq)" "\$@"
EOF
chmod +x "$stubbin/jq"

PATH="$stubbin:$PATH" run wt merged "$base" --claude
[ "$status" -eq 0 ]
[[ "$output" == *"main111"* ]]
[[ "$output" == *"main repo session"* ]]
[[ "$output" == *"(main, branch varies)"* ]]
[[ "$output" != *"main-drift"* ]]
rm -rf "$stubbin"
}

@test "wt merged --claude shows a placeholder row for worktrees with no sessions" {
local base; base=$(git -C "$TEST_REPO" symbolic-ref --short HEAD)
git -C "$TEST_REPO-feature" commit -q --allow-empty -m "feature commit"
cd "$TEST_REPO"
git merge -q feature

local stubbin; stubbin=$(mktemp -d)
cat > "$stubbin/claude" <<'EOF'
#!/usr/bin/env bash
echo '[]'
EOF
chmod +x "$stubbin/claude"
cat > "$stubbin/jq" <<EOF
#!/usr/bin/env bash
exec "$(command -v jq)" "\$@"
EOF
chmod +x "$stubbin/jq"

PATH="$stubbin:$PATH" run wt merged "$base" --claude
[ "$status" -eq 0 ]
[[ "$output" =~ feature.*-.*-.*- ]]
rm -rf "$stubbin"
}

@test "wt merged --claude errors when jq is missing" {
local base; base=$(git -C "$TEST_REPO" symbolic-ref --short HEAD)
git -C "$TEST_REPO-feature" commit -q --allow-empty -m "feature commit"
cd "$TEST_REPO"
git merge -q feature

local stubbin; stubbin=$(mktemp -d)
cat > "$stubbin/claude" <<'EOF'
#!/usr/bin/env bash
echo '[]'
EOF
chmod +x "$stubbin/claude"
# only symlink the external commands wt.sh actually needs (git, awk) -
# deliberately no jq, so `command -v jq` fails regardless of the host PATH
ln -s "$(command -v git)" "$stubbin/git"
ln -s "$(command -v awk)" "$stubbin/awk"

PATH="$stubbin" run wt merged "$base" --claude
[ "$status" -ne 0 ]
[[ "$output" == *"jq not found"* ]]
rm -rf "$stubbin"
}

@test "wt merged --claude degrades gracefully when claude agents returns malformed output" {
local base; base=$(git -C "$TEST_REPO" symbolic-ref --short HEAD)
git -C "$TEST_REPO-feature" commit -q --allow-empty -m "feature commit"
cd "$TEST_REPO"
git merge -q feature

local stubbin; stubbin=$(mktemp -d)
# fake claude returns garbage instead of JSON (e.g. a crash/error message)
cat > "$stubbin/claude" <<'EOF'
#!/usr/bin/env bash
echo 'not valid json at all'
EOF
chmod +x "$stubbin/claude"
cat > "$stubbin/jq" <<EOF
#!/usr/bin/env bash
exec "$(command -v jq)" "\$@"
EOF
chmod +x "$stubbin/jq"

PATH="$stubbin:$PATH" run wt merged "$base" --claude
[ "$status" -eq 0 ]
[[ "$output" == *"could not read Claude Code agent sessions"* ]]
[[ "$output" == *"feature"* ]]
[[ "$output" =~ feature.*-.*-.*- ]]
rm -rf "$stubbin"
}
24 changes: 24 additions & 0 deletions test/help.bats
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
load helpers

setup() { wt_common_setup; }
teardown() { wt_common_teardown; }

# --- help ---

@test "wt help prints usage" {
run wt help
[ "$status" -eq 0 ]
[[ "$output" == *"Usage:"* ]]
}

@test "wt --help prints usage" {
run wt --help
[ "$status" -eq 0 ]
[[ "$output" == *"Usage:"* ]]
}

@test "wt -h prints usage" {
run wt -h
[ "$status" -eq 0 ]
[[ "$output" == *"Usage:"* ]]
}
33 changes: 33 additions & 0 deletions test/helpers.bash
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# shared setup/teardown for all test files.
#
# usage, from a .bats file:
# load helpers # test/*.bats
# load ../helpers # test/claude/*.bats
#
# setup() { wt_common_setup; }
# teardown() { wt_common_teardown; }

wt_common_setup() {
# create a temp repo and two worktrees
TEST_REPO=$(mktemp -d)
cd "$TEST_REPO"
git init -q
git config user.email "test@test.com"
git config user.name "Test"
git commit -q --allow-empty -m "init"

git worktree add -q "$TEST_REPO-feature" -b feature
git worktree add -q "$TEST_REPO-other" -b other

# locate wt.sh by walking up from the test file's directory, so tests work
# at any nesting depth and don't require a git checkout
local dir="$BATS_TEST_DIRNAME"
while [ ! -f "$dir/wt.sh" ] && [ "$dir" != "/" ]; do
dir=$(dirname "$dir")
done
source "$dir/wt.sh"
}

wt_common_teardown() {
rm -rf "$TEST_REPO" "$TEST_REPO-feature" "$TEST_REPO-other"
}
38 changes: 38 additions & 0 deletions test/ls.bats
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
load helpers

setup() { wt_common_setup; }
teardown() { wt_common_teardown; }

# --- _wt_ls ---

@test "wt ls lists worktrees" {
run _wt_ls
[ "$status" -eq 0 ]
[[ "$output" == *"$TEST_REPO "* ]]
[[ "$output" == *"feature"* ]]
[[ "$output" == *"other"* ]]
}

@test "wt with no args lists worktrees" {
run wt
[ "$status" -eq 0 ]
[[ "$output" == *"feature"* ]]
}

@test "wt list alias works" {
run wt list
[ "$status" -eq 0 ]
[[ "$output" == *"feature"* ]]
}

@test "wt ls alias works" {
run wt ls
[ "$status" -eq 0 ]
[[ "$output" == *"feature"* ]]
}

@test "wt ls errors on unknown flag" {
run wt ls --bogus
[ "$status" -ne 0 ]
[[ "$output" == *"unknown flag"* ]]
}
Loading