Skip to content
21 changes: 21 additions & 0 deletions e2e/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,27 @@ scenarios:
| `output_contains` | Output includes string |
| `output_not_contains` | Output excludes string |

### Scenario Variables

POSIX scenarios (bash/zsh/fish) can reference these, set up by the generator:

| Variable | Form | Use |
|----------|------|-----|
| `$WT_BIN` | POSIX | The `wt` binary, invocable by the interpreter |
| `$TEST_DIR`, `$REPO_DIR` | POSIX | Paths to hand to `test`, `cat`, `mkdir`, … |
| `$WORKTREE_ROOT` | native | Exported; what `wt` itself reads |
| `$WORKTREE_ROOT_POSIX` | POSIX | Assert on worktree paths from the shell |
| `$TEST_DIR_NATIVE`, `$REPO_DIR_NATIVE` | native | Paths to hand to `wt` (e.g. `WT_CONFIG=`) |

The `_NATIVE`/`_POSIX` pairs only differ under Git Bash, where the interpreter
speaks `/c/...` and the native `wt.exe` speaks `C:\...`. Bash converts
POSIX-looking *arguments* when it spawns a native binary, but not *environment
variables* — so anything passed to `wt` through the environment needs the
native form. Everywhere else both forms are the same string, which is what
keeps a scenario using them portable.

These are POSIX-generator only; the PowerShell generator does not define them.

### Skip Conditions

```yaml
Expand Down
18 changes: 18 additions & 0 deletions e2e/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -444,11 +444,24 @@ func generatePosixScript(wtBinary, shell string, scenario Scenario, verbose, sho
sb.WriteString("TEST_DIR=$(mktemp -d)\n")
sb.WriteString("REPO_DIR=\"$TEST_DIR/test-repo\"\n")
sb.WriteString("REPO_NAME=\"test-repo\"\n")
// A scenario running under Git Bash straddles two path worlds: the
// interpreter and its coreutils understand POSIX paths (/c/...), the native
// wt.exe understands Windows ones (C:\...). Every base path is therefore
// available in both forms — $..._NATIVE to hand to wt (as an argument value
// bash won't convert, e.g. an env var), $..._POSIX to assert on with
// test/grep. On every other OS the two forms are the same string, so a
// scenario written with them stays portable.
if runtime.GOOS == "windows" {
sb.WriteString("mkdir -p \"$TEST_DIR/worktrees\"\n")
sb.WriteString("export WORKTREE_ROOT=$(cygpath -w \"$TEST_DIR/worktrees\")\n")
sb.WriteString("WORKTREE_ROOT_POSIX=\"$TEST_DIR/worktrees\"\n")
sb.WriteString("TEST_DIR_NATIVE=$(cygpath -w \"$TEST_DIR\")\n")
sb.WriteString("REPO_DIR_NATIVE=$(cygpath -w \"$REPO_DIR\")\n")
} else {
sb.WriteString("export WORKTREE_ROOT=\"$TEST_DIR/worktrees\"\n")
sb.WriteString("WORKTREE_ROOT_POSIX=\"$WORKTREE_ROOT\"\n")
sb.WriteString("TEST_DIR_NATIVE=\"$TEST_DIR\"\n")
sb.WriteString("REPO_DIR_NATIVE=\"$REPO_DIR\"\n")
}
sb.WriteString("mkdir -p \"$REPO_DIR\"\n")
sb.WriteString("cd \"$REPO_DIR\"\n")
Expand Down Expand Up @@ -611,6 +624,11 @@ func generateFishScript(wtBinary string, scenario Scenario, verbose, showOutput,
sb.WriteString("set REPO_DIR \"$TEST_DIR/test-repo\"\n")
sb.WriteString("set REPO_NAME \"test-repo\"\n")
sb.WriteString("set -x WORKTREE_ROOT \"$TEST_DIR/worktrees\"\n")
// Fish never runs on Windows, so the native and POSIX forms coincide.
// They exist here only so scenarios can name them unconditionally.
sb.WriteString("set WORKTREE_ROOT_POSIX \"$WORKTREE_ROOT\"\n")
sb.WriteString("set TEST_DIR_NATIVE \"$TEST_DIR\"\n")
sb.WriteString("set REPO_DIR_NATIVE \"$REPO_DIR\"\n")
sb.WriteString("mkdir -p \"$REPO_DIR\"; or exit 1\n")
sb.WriteString("cd \"$REPO_DIR\"; or exit 1\n")
sb.WriteString("git init --quiet; or exit 1\n")
Expand Down
4 changes: 2 additions & 2 deletions e2e/scenarios/cleanup.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ scenarios:

- name: cleanup_cleans_directory
description: Cleanup removes the worktree directory from filesystem
skip_os: [windows] # Uses Unix test -d command
skip_shells: [powershell, pwsh] # Uses Unix test -d command
steps:
# Create and merge a branch
- run: git checkout -b cleanup-dir-branch
Expand All @@ -90,6 +90,6 @@ scenarios:
- run: wt cleanup --force
expect:
exit_code: 0
- run: test -d "$WORKTREE_ROOT/$REPO_NAME/cleanup-dir-branch" && echo "EXISTS" || echo "REMOVED"
- run: test -d "$WORKTREE_ROOT_POSIX/$REPO_NAME/cleanup-dir-branch" && echo "EXISTS" || echo "REMOVED"
expect:
output_contains: REMOVED
10 changes: 6 additions & 4 deletions e2e/scenarios/format-json.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -65,18 +65,20 @@ scenarios:
- name: migrate_json_summary
description: migrate command returns structured JSON summary
skip_shellenv: true
skip_os: [windows]
# $TEST_DIR is a POSIX-generator variable; the PowerShell generator names
# it $TestDir and does not translate it inside a step.
skip_shells: [powershell, pwsh]
setup:
- create_branch: migrate-json
steps:
- run: git worktree add "$REPO_DIR/../legacy/migrate-json" migrate-json
- run: git worktree add "$TEST_DIR/legacy/migrate-json" migrate-json
expect:
exit_code: 0
- run: WORKTREE_ROOT="$REPO_DIR/../worktrees" $WT_BIN --format json migrate
- run: $WT_BIN --format json migrate
expect:
exit_code: 0
output_contains: '"command":"wt migrate"'
- run: WORKTREE_ROOT="$REPO_DIR/../worktrees" $WT_BIN --format json migrate
- run: $WT_BIN --format json migrate
expect:
output_contains: '"migrated"'

Expand Down
41 changes: 28 additions & 13 deletions e2e/scenarios/hooks.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,14 @@ description: Test pre/post command hook functionality
scenarios:
- name: post_create_hook_runs
description: Post-create hook executes after worktree creation
# wt runs hook commands through `cmd /c` on Windows (see runHooks), which
# neither expands $WT_PATH nor provides the POSIX utilities this hook body
# uses. That holds under Git Bash too, since it is wt that spawns the hook.
skip_os: [windows]
skip_shellenv: true
steps:
# Write a config file with a post_create hook, then create worktree
- run: mkdir -p "$TEST_DIR/config" && printf '[hooks]\npost_create = ["touch $WT_PATH/.hook-ran"]\n' > "$TEST_DIR/config/config.toml" && WT_CONFIG="$TEST_DIR/config/config.toml" $WT_BIN create hook-test
- run: mkdir -p "$TEST_DIR/config" && printf '[hooks]\npost_create = ["touch $WT_PATH/.hook-ran"]\n' > "$TEST_DIR/config/config.toml" && WT_CONFIG="$TEST_DIR_NATIVE/config/config.toml" $WT_BIN create hook-test
expect:
exit_code: 0
- run: test -f "$WORKTREE_ROOT/test-repo/hook-test/.hook-ran" && echo "HOOK_RAN" || echo "HOOK_MISSING"
Expand All @@ -19,9 +22,9 @@ scenarios:
- name: pre_create_hook_aborts
description: Pre-create hook failure prevents worktree creation
skip_shellenv: true
skip_os: [windows]
skip_shells: [powershell, pwsh] # steps use POSIX shell syntax
steps:
- run: mkdir -p "$TEST_DIR/config" && printf '[hooks]\npre_create = ["false"]\n' > "$TEST_DIR/config/config.toml" && WT_CONFIG="$TEST_DIR/config/config.toml" $WT_BIN create blocked-branch
- run: mkdir -p "$TEST_DIR/config" && printf '[hooks]\npre_create = ["false"]\n' > "$TEST_DIR/config/config.toml" && WT_CONFIG="$TEST_DIR_NATIVE/config/config.toml" $WT_BIN create blocked-branch
expect:
exit_code: 1
- run: $WT_BIN list
Expand All @@ -30,21 +33,24 @@ scenarios:

- name: hooks_disabled_env
description: WT_HOOKS_DISABLED=1 skips all hooks
skip_os: [windows]
skip_shells: [powershell, pwsh] # steps use POSIX shell syntax
skip_shellenv: true
steps:
- run: mkdir -p "$TEST_DIR/config" && printf '[hooks]\npre_create = ["false"]\n' > "$TEST_DIR/config/config.toml" && WT_CONFIG="$TEST_DIR/config/config.toml" WT_HOOKS_DISABLED=1 $WT_BIN create should-succeed
- run: mkdir -p "$TEST_DIR/config" && printf '[hooks]\npre_create = ["false"]\n' > "$TEST_DIR/config/config.toml" && WT_CONFIG="$TEST_DIR_NATIVE/config/config.toml" WT_HOOKS_DISABLED=1 $WT_BIN create should-succeed
expect:
exit_code: 0

- name: post_checkout_hook_runs
description: Post-checkout hook executes after worktree checkout
# wt runs hook commands through `cmd /c` on Windows (see runHooks), which
# neither expands $WT_PATH nor provides the POSIX utilities this hook body
# uses. That holds under Git Bash too, since it is wt that spawns the hook.
skip_os: [windows]
skip_shellenv: true
setup:
- create_branch: hook-co-branch
steps:
- run: mkdir -p "$TEST_DIR/config" && printf '[hooks]\npost_checkout = ["touch $WT_PATH/.checkout-hook-ran"]\n' > "$TEST_DIR/config/config.toml" && WT_CONFIG="$TEST_DIR/config/config.toml" $WT_BIN checkout hook-co-branch
- run: mkdir -p "$TEST_DIR/config" && printf '[hooks]\npost_checkout = ["touch $WT_PATH/.checkout-hook-ran"]\n' > "$TEST_DIR/config/config.toml" && WT_CONFIG="$TEST_DIR_NATIVE/config/config.toml" $WT_BIN checkout hook-co-branch
expect:
exit_code: 0
- run: test -f "$WORKTREE_ROOT/test-repo/hook-co-branch/.checkout-hook-ran" && echo "HOOK_RAN" || echo "HOOK_MISSING"
Expand All @@ -53,6 +59,9 @@ scenarios:

- name: checkout_hooks_run_on_existing_worktree
description: Pre/post checkout hooks execute even when worktree already exists
# wt runs hook commands through `cmd /c` on Windows (see runHooks), which
# neither expands $WT_PATH nor provides the POSIX utilities this hook body
# uses. That holds under Git Bash too, since it is wt that spawns the hook.
skip_os: [windows]
skip_shellenv: true
setup:
Expand All @@ -64,7 +73,7 @@ scenarios:
exit_code: 0
- cd: $REPO_DIR
# Second checkout with hooks configured — hooks should still run
- run: mkdir -p "$TEST_DIR/config" && printf '[hooks]\npost_checkout = ["touch $WT_PATH/.existing-hook-ran"]\n' > "$TEST_DIR/config/config.toml" && WT_CONFIG="$TEST_DIR/config/config.toml" $WT_BIN checkout existing-co-branch
- run: mkdir -p "$TEST_DIR/config" && printf '[hooks]\npost_checkout = ["touch $WT_PATH/.existing-hook-ran"]\n' > "$TEST_DIR/config/config.toml" && WT_CONFIG="$TEST_DIR_NATIVE/config/config.toml" $WT_BIN checkout existing-co-branch
expect:
exit_code: 0
output_contains: "already exists"
Expand All @@ -74,7 +83,7 @@ scenarios:

- name: pre_checkout_hook_aborts_existing_worktree
description: Pre-checkout hook failure prevents checkout of existing worktree
skip_os: [windows]
skip_shells: [powershell, pwsh] # steps use POSIX shell syntax
skip_shellenv: true
setup:
- create_branch: abort-co-branch
Expand All @@ -85,13 +94,13 @@ scenarios:
exit_code: 0
- cd: $REPO_DIR
# Second checkout with failing pre_checkout hook — should abort
- run: mkdir -p "$TEST_DIR/config" && printf '[hooks]\npre_checkout = ["false"]\n' > "$TEST_DIR/config/config.toml" && WT_CONFIG="$TEST_DIR/config/config.toml" $WT_BIN checkout abort-co-branch
- run: mkdir -p "$TEST_DIR/config" && printf '[hooks]\npre_checkout = ["false"]\n' > "$TEST_DIR/config/config.toml" && WT_CONFIG="$TEST_DIR_NATIVE/config/config.toml" $WT_BIN checkout abort-co-branch
expect:
exit_code: 1

- name: pre_remove_hook_runs
description: Pre-remove hook executes before worktree removal
skip_os: [windows]
skip_shells: [powershell, pwsh] # steps use POSIX shell syntax
skip_shellenv: true
setup:
- create_branch: rm-hook-branch
Expand All @@ -100,13 +109,16 @@ scenarios:
expect:
exit_code: 0
- cd: $REPO_DIR
- run: mkdir -p "$TEST_DIR/config" && printf '[hooks]\npre_remove = ["echo HOOK_PRE_REMOVE"]\n' > "$TEST_DIR/config/config.toml" && WT_CONFIG="$TEST_DIR/config/config.toml" $WT_BIN remove rm-hook-branch
- run: mkdir -p "$TEST_DIR/config" && printf '[hooks]\npre_remove = ["echo HOOK_PRE_REMOVE"]\n' > "$TEST_DIR/config/config.toml" && WT_CONFIG="$TEST_DIR_NATIVE/config/config.toml" $WT_BIN remove rm-hook-branch
expect:
exit_code: 0
output_contains: "HOOK_PRE_REMOVE"

- name: copy_env_file_when_exists
description: Post-create hook copies .env from main to worktree when it exists
# wt runs hook commands through `cmd /c` on Windows (see runHooks), which
# neither expands $WT_PATH nor provides the POSIX utilities this hook body
# uses. That holds under Git Bash too, since it is wt that spawns the hook.
skip_os: [windows]
skip_shellenv: true
setup:
Expand All @@ -116,7 +128,7 @@ scenarios:
- git_add: .env
- git_commit: "add .env file"
steps:
- run: mkdir -p "$TEST_DIR/config" && printf '[hooks]\npost_create = ["test -f $WT_MAIN/.env && cp $WT_MAIN/.env $WT_PATH/.env || true"]\n' > "$TEST_DIR/config/config.toml" && WT_CONFIG="$TEST_DIR/config/config.toml" $WT_BIN create env-branch
- run: mkdir -p "$TEST_DIR/config" && printf '[hooks]\npost_create = ["test -f $WT_MAIN/.env && cp $WT_MAIN/.env $WT_PATH/.env || true"]\n' > "$TEST_DIR/config/config.toml" && WT_CONFIG="$TEST_DIR_NATIVE/config/config.toml" $WT_BIN create env-branch
expect:
exit_code: 0
- run: cat "$WORKTREE_ROOT/test-repo/env-branch/.env"
Expand All @@ -125,10 +137,13 @@ scenarios:

- name: copy_env_file_skipped_when_missing
description: Post-create hook silently skips when .env does not exist in main
# wt runs hook commands through `cmd /c` on Windows (see runHooks), which
# neither expands $WT_PATH nor provides the POSIX utilities this hook body
# uses. That holds under Git Bash too, since it is wt that spawns the hook.
skip_os: [windows]
skip_shellenv: true
steps:
- run: mkdir -p "$TEST_DIR/config" && printf '[hooks]\npost_create = ["test -f $WT_MAIN/.env && cp $WT_MAIN/.env $WT_PATH/.env || true"]\n' > "$TEST_DIR/config/config.toml" && WT_CONFIG="$TEST_DIR/config/config.toml" $WT_BIN create no-env-branch
- run: mkdir -p "$TEST_DIR/config" && printf '[hooks]\npost_create = ["test -f $WT_MAIN/.env && cp $WT_MAIN/.env $WT_PATH/.env || true"]\n' > "$TEST_DIR/config/config.toml" && WT_CONFIG="$TEST_DIR_NATIVE/config/config.toml" $WT_BIN create no-env-branch
expect:
exit_code: 0
- run: test -f "$WORKTREE_ROOT/test-repo/no-env-branch/.env" && echo "ENV_EXISTS" || echo "ENV_MISSING"
Expand Down
22 changes: 11 additions & 11 deletions e2e/scenarios/init.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -63,13 +63,16 @@ scenarios:
- name: init_creates_config
description: Init creates shell config with markers
skip_shellenv: true
# steps use bash-only inline env syntax (HOME="..." cmd), so fish can't run them
# These four scenarios redirect wt's home lookup at TEST_DIR so they never
# touch the real ~/.bashrc. That needs bash-only inline env syntax
# (HOME="..." cmd), which rules out fish and the PowerShell shells — a
# shell constraint, not an OS one, so they do run under Git Bash. wt reads
# $HOME on Unix and %USERPROFILE% on Windows, hence both are set.
skip_shells: [powershell, pwsh, fish]
skip_os: [windows]
steps:
- run: |
touch "$TEST_DIR/.bashrc"
HOME="$TEST_DIR" $WT_BIN init bash --no-prompt
HOME="$TEST_DIR" USERPROFILE="$TEST_DIR_NATIVE" $WT_BIN init bash --no-prompt
cat "$TEST_DIR/.bashrc"
expect:
exit_code: 0
Expand All @@ -79,12 +82,11 @@ scenarios:
description: Running init twice does not duplicate config
skip_shellenv: true
skip_shells: [powershell, pwsh, fish]
skip_os: [windows]
steps:
- run: |
touch "$TEST_DIR/.bashrc"
HOME="$TEST_DIR" $WT_BIN init bash --no-prompt
HOME="$TEST_DIR" $WT_BIN init bash --no-prompt
HOME="$TEST_DIR" USERPROFILE="$TEST_DIR_NATIVE" $WT_BIN init bash --no-prompt
HOME="$TEST_DIR" USERPROFILE="$TEST_DIR_NATIVE" $WT_BIN init bash --no-prompt
grep -c "wt initialize >>>" "$TEST_DIR/.bashrc"
expect:
exit_code: 0
Expand All @@ -94,12 +96,11 @@ scenarios:
description: Init --uninstall removes the config
skip_shellenv: true
skip_shells: [powershell, pwsh, fish]
skip_os: [windows]
steps:
- run: |
touch "$TEST_DIR/.bashrc"
HOME="$TEST_DIR" $WT_BIN init bash --no-prompt
HOME="$TEST_DIR" $WT_BIN init bash --uninstall --no-prompt
HOME="$TEST_DIR" USERPROFILE="$TEST_DIR_NATIVE" $WT_BIN init bash --no-prompt
HOME="$TEST_DIR" USERPROFILE="$TEST_DIR_NATIVE" $WT_BIN init bash --uninstall --no-prompt
grep -c "wt initialize" "$TEST_DIR/.bashrc" 2>/dev/null || echo "0"
expect:
output_contains: "0"
Expand All @@ -108,12 +109,11 @@ scenarios:
description: Init preserves existing shell config content
skip_shellenv: true
skip_shells: [powershell, pwsh, fish]
skip_os: [windows]
steps:
- run: |
echo "# My existing config" > "$TEST_DIR/.bashrc"
echo "export MY_VAR=hello" >> "$TEST_DIR/.bashrc"
HOME="$TEST_DIR" $WT_BIN init bash --no-prompt
HOME="$TEST_DIR" USERPROFILE="$TEST_DIR_NATIVE" $WT_BIN init bash --no-prompt
cat "$TEST_DIR/.bashrc"
expect:
exit_code: 0
Expand Down
Loading