mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-07-09 17:19:02 +00:00
feat(worktree): Phase C — session persistence, hooksPath, Footer + WorktreeExitDialog, three-mode --resume restore (#4174)
* docs(worktree): update design doc — split Phase C/D, add Future section
- Phase C: session persistence + hooksPath + StatusLine + WorktreeExitDialog
- Phase D: --worktree CLI flag + symlinkDirectories
- Future: sparse checkout, .worktreeinclude, tmux, PR reference parsing
- Feature comparison table updated with Phase A/B completion status
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* docs(worktree): add Phase C implementation plan
8 tasks: WorktreeSession sidecar storage, hooksPath setup,
EnterWorktree/ExitWorktree session wiring, useWorktreeSession hook,
Footer display, --resume context injection, WorktreeExitDialog.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* docs(worktree): update Phase C plan after claude-code comparison
- WorktreeSession: add originalHeadCommit field
- hooksPath: add .husky/ detection + skip-if-already-set logic
- StatusLine payload: expand worktree field to match claude-code schema
- WorktreeExitDialog: load dirty state on mount, display counts in dialog
- UIState.activeWorktree: add originalCwd, originalBranch, originalHeadCommit
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat(worktree): add WorktreeSession sidecar storage
New worktreeSessionService.ts exposes read/write/clear functions for the
sidecar JSON file at <chatsDir>/<sessionId>.worktree.json. SessionService
gains getWorktreeSessionPath() so callers don't need to know the layout.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(worktree): configure core.hooksPath after worktree creation
createUserWorktree() now sets `core.hooksPath` inside the new worktree to
the main repo's hooks directory (.husky preferred, .git/hooks fallback) so
commits inside the worktree run the same pre-commit checks as the main
repo. Mirrors claude-code's performPostCreationSetup logic — skips the
subprocess when the value already matches to avoid ~14ms spawn overhead.
Failures are non-fatal: the worktree is still usable without hooks.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(worktree): persist WorktreeSession sidecar in EnterWorktreeTool
After creating a worktree, EnterWorktreeTool now writes a sidecar JSON
file at <chatsDir>/<sessionId>.worktree.json with the full session state
(slug, paths, branches, original HEAD SHA). --resume reads this in Phase
C task 7 to restore worktree context. Best-effort: write failures don't
abort the creation.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(worktree): clear WorktreeSession sidecar in ExitWorktreeTool
After successful keep or remove, ExitWorktreeTool now clears the sidecar
JSON file iff its slug matches the worktree being exited. The slug check
prevents wiping the sidecar when the user exits a worktree that isn't
currently tracked (multiple worktrees on disk, sidecar tracks one).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(worktree): expose active worktree via useWorktreeSession + UIState
New useWorktreeSession hook watches the sidecar JSON file (created by
EnterWorktreeTool, deleted by ExitWorktreeTool) and returns the current
WorktreeSession or null. AppContainer wires it into a new
UIState.activeWorktree field consumed by Footer (Task 6) and
WorktreeExitDialog (Task 8).
A showWorktreeExitDialog state placeholder is added too, hardcoded false
until Task 8 wires the dialog trigger.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(worktree): show active worktree in Footer + StatusLine payload
Footer renders `⎇ <branch> (<slug>)` when activeWorktree != null, but
only when the user has no custom statusline (their script likely
handles it from the stdin payload itself).
useStatusLine's StatusLineCommandInput gains a `worktree` field with
{name, path, branch, original_cwd, original_branch} — matches claude-code's
schema so statusline scripts can be shared across both CLIs.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(worktree): inject context hint on --resume when worktree is active
On --resume, if the session has a WorktreeSession sidecar, append an
INFO history item pointing the model at the worktree path so it
continues using it for file operations. Stale sidecars (worktree dir
deleted out-of-band) are cleaned up so the Footer indicator doesn't
go stale.
qwen-code can't process.chdir() the way claude-code does because
Config.targetDir is immutable; the context hint is the equivalent
behavioral cue.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(worktree): add WorktreeExitDialog with dirty-state inspection
WorktreeExitDialog renders when the user double-presses Ctrl+C inside a
worktree. On mount it runs `git status --porcelain` and
`git rev-list --count <originalHeadCommit>..HEAD` to show how many
uncommitted files and new commits the user would discard by choosing
"Remove". The dialog never auto-removes — every exit goes through
explicit user confirmation per requirements.
handleExit in AppContainer intercepts the second-press quit when
activeWorktree is set and shows the dialog instead. A new UIAction
handleWorktreeExit(choice) routes the user's choice through removal
(via GitWorktreeService.removeUserWorktree) + sidecar cleanup + /quit.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* docs(worktree): add Phase C E2E test plan
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* docs(worktree): fix E2E test plan sidecar path + jq selector
- sidecar lives at ~/.qwen/projects/<sanitized-cwd>/chats/, not ~/.qwen/tmp/<hash>/
- qwen --output-format json emits a JSON array, not NDJSON — jq needs .[]
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(worktree): add showWorktreeExitDialog to dialogsVisible
Phase C task 8 introduced showWorktreeExitDialog state and the dialog
render in DialogManager, but missed adding the flag to the dialogsVisible
OR expression. DefaultAppLayout only renders DialogManager when
dialogsVisible is true, so the dialog was never shown — second Ctrl+C
in a worktree silently absorbed instead of triggering the prompt.
Caught by Group E E2E tests.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(worktree): extend --resume context restore to headless + ACP modes
Phase C task 7 originally placed the worktree-restore logic in
AppContainer.tsx (TUI only). E2E Group C exposed that headless and ACP
modes never run AppContainer, so stale sidecars accumulate and the model
loses worktree context after --resume.
Refactor to a shared `restoreWorktreeContext` helper in core, then wire
the three entry points:
- TUI (AppContainer): keep historyManager.addItem(INFO) UX, route via
the helper.
- Headless (nonInteractiveCli): prepend the notice as a system-reminder
block on the user prompt; emit a `worktree_restored` system message to
the JSON adapter so SDK consumers can react.
- ACP (Session.pendingWorktreeNotice): set by acpAgent.loadSession on
resume, consumed and cleared exactly once on the next #executePrompt.
All three modes call the same helper, so stale-sidecar cleanup is
consistent. Helper covers: missing sidecar, live worktree dir,
deleted worktree dir, regular file at worktreePath, malformed JSON.
5 new unit tests for restoreWorktreeContext (13/13 pass total).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* test(worktree): add ACP-mode integration tests for --resume context
Covers:
- acpAgent.worktree.test.ts (3 tests): loadSession sets
pendingWorktreeNotice only when worktree dir is live, clears
stale sidecar otherwise, swallows restoreWorktreeContext errors.
- Session.worktree.test.ts (4 tests): #executePrompt prepends the
system-reminder block exactly once on first prompt, clears the
pending notice, second prompt sees no leakage, no-op when nothing
was set.
E2E via real ACP protocol is impractical without a Zed client; these
tests cover the integration boundaries directly.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* docs(worktree): clarify hooksPath comment + pendingWorktreeNotice one-shot rationale
Two doc-only fixes from PR #4174 review:
- gitWorktreeService.ts: previous hooksPath comment overstated the
optimization (claimed claude-code's ~14ms saving but we still do a
read subprocess). Rewrite to be explicit: write-skip only, read
retained, parseGitConfigValue's full optimization deliberately not
ported because the read happens once per worktree creation.
- Session.ts: pendingWorktreeNotice doc now explains why it's one-shot
(after the first prompt the worktree path is already in conversation
context; re-injecting would clutter history without adding signal).
No behavior change.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(test): add getResumedSessionData to nonInteractiveCli mock Config
CI surfaced TypeError: config.getResumedSessionData is not a function
across 12 tests in nonInteractiveCli.test.ts. The Phase C ada0837e2
commit added a worktree-restore call in the headless path that probes
config.getResumedSessionData(); the mock Config never had that method.
Return undefined to short-circuit the restore block — these tests
don't exercise --resume.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(worktree): address PR #4174 reviewer findings
Bundled response to the two review rounds. Per-thread replies follow.
CORE — worktree sidecar robustness (Findings 3252368644, 3252368651, 3255171690):
- atomicWriteJSON instead of fs.writeFile (no more half-written sidecar after a crash)
- readWorktreeSession now schema-validates the parsed object and returns null
on missing/wrong-type fields instead of propagating undefined into consumers
- restoreWorktreeContext clears the sidecar on JSON parse failure / read I/O
error so a corrupted file doesn't block every subsequent --resume
CORE — hooksPath setup (Finding 3252368645):
- configureHooksPath distinguishes ENOENT (benign "candidate not present")
from real stat errors (EACCES/EIO/ENOTDIR); the latter are warn-logged
so a silently-degraded hooksPath is visible to operators
CLI — handleWorktreeExit Remove path (Findings 3252368637, 3252368640 a+b):
- Anchor GitWorktreeService at activeWorktree.originalCwd (the captured
repo root), not config.getTargetDir() — fixes monorepo-subdirectory
launches where the worktree lives under the repo root but getTargetDir
points at a subpackage
- Check removeUserWorktree return value; on failure, leave the sidecar
intact so --resume can recover (previous code cleared it regardless)
- Pass forceDeleteBranch:true to honour the dialog's "discards N commits"
label — without it `git branch -d` refused unmerged commits and the
branch was silently preserved
CLI — useWorktreeSession watcher (Finding 3252368648):
- Normalize fs.watch filename via toString() so the Linux-Buffer code
path triggers reloads (previous comparison silently never matched)
- Treat null filename as "unknown, reload to be safe" (recursive watchers
on some platforms emit events without a payload)
CLI — WorktreeExitDialog (Findings 3252368650, 3255171694):
- execGit now correctly reads numeric exit codes from .code/.status
(NodeJS.ErrnoException.code is a string for spawn errors, number for
subprocess exits); previous typeof === 'number' check always missed
- Dialog body shows an "⚠ Could not measure worktree state (...)" banner
when git status / rev-list failed, so the user doesn't see a misleading
"0 files, 0 commits" before choosing Remove
CLI — closeAnyOpenDialog (Round 2 review body):
- Wire WorktreeExitDialog into the standard dialog-dismissal path so
Ctrl+C dismisses it the same way it dismisses every other dialog
TEST FIXES — vitest timeouts:
- Real git invocations + user-global hooks (e.g. trustup post-commit
webhooks) can take 10–20s per setUp on CI. Bump testTimeout +
hookTimeout to 30s for the three integ test suites that spawn git
(Phase B/C worktree integ tests) so the suite isn't flaky.
NEW TESTS:
- worktreeSessionService.test: 3 new cases covering malformed JSON,
missing required fields, wrong-type fields, malformed sidecar cleanup,
partial sidecar cleanup (16 total, up from 13).
- useWorktreeSession.test.tsx: 4 new cases — null when no sidecar,
parsed sidecar at mount, reacts to delete, reacts to creation.
- WorktreeExitDialog.test.tsx: 1 new case — loading frame renders before
git probes resolve. (Async dialog states tested via E2E — vi.mock of
execFile in ink-testing-library doesn't fire mock impl reliably.)
- nonInteractiveCli.test: 3 new "Phase C --resume" cases — system-reminder
injection on live worktree, no injection when sidecar absent, stale
sidecar cleanup when worktree dir is gone.
DECLINED FINDINGS (replied on threads):
- 3252368642 (Dialog Keep clears sidecar) — declined-design. Dialog
Keep = "exit app, keep worktree for next --resume"; tool Keep =
"I'm done with this worktree". Intentionally different semantics.
- 3252368643 (originalHeadCommit base branch) — false-positive. There
is no base_branch parameter; getCurrentCommitHash() returns HEAD which
equals the tip of the current branch (== baseBranch in createUserWorktree).
- 3252368640 part c (bypass safety guards) — declined-design. The
dialog IS the safety affordance for this path — it shows dirty-state
counts and asks for explicit user confirmation before removal.
- 3255171696 (DialogManager async fire-and-forget) — false-positive.
handleSlashCommand('/quit') is inside the await chain in
handleWorktreeExit, so the described race ("process.exit before remove
completes") cannot occur.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(test): correct linter-mangled imports in useWorktreeSession.test
Pre-commit hook auto-fixed imports collapsed value imports
(writeWorktreeSession, clearWorktreeSession) into an `import type`
block, breaking runtime resolution. Split back into value + type imports.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(test): normalize path separators for Windows in worktree session integ
Windows CI failure: `repoRoot` from Node's `fs.mkdtemp` returns
backslash-separated paths (`C:\Users\runneradmin\…`), but
`originalCwd` in the sidecar comes from `getRepoTopLevel()` which
delegates to `git rev-parse --show-toplevel` — git on Windows
returns forward slashes (`C:/Users/runneradmin/…`).
The Windows-only assertion `expect(originalCwd).toBe(repoRoot)` was
comparing two different representations of the same canonical path
and rightly failed on `Object.is` equality. Compare via path.normalize
on both sides so the assertion holds across platforms without
changing the runtime path (originalCwd still records git's output
verbatim, which is what consumers expect since other places in the
codebase that read `getRepoTopLevel()` also work with that shape).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(worktree): address PR #4174 round 4 findings
Finding #3256237933 (Critical, follow-up to #3252368640 part 1):
handleWorktreeExit silently /quit'd when removeUserWorktree returned
{success:false}, contradicting the user's intent after they clicked
"Remove worktree and branch (discards N commits, M files)". Now
surfaces an ERROR history item with the underlying error message
and STAYS in the session so the user can decide what to do
(retry via exit_worktree, fix the lock/permission/corruption issue,
or quit anyway). Same treatment applied to the hard-failure catch
block — previously it caught the throw and proceeded to /quit with
no log; now it emits the error and stays alive.
Finding #3256236050 (Nit): originalCwd field name implies "user's
launch cwd" but actually stores `getRepoTopLevel()` (different in
monorepo subdir launches — the gap closed by #3252368637). Renaming
the field would force on-disk migration of every existing sidecar
(every active --resume breaks until users wipe the old file).
Doc-only fix: WorktreeSession.originalCwd now carries an explicit
JSDoc explaining the semantics and warning consumers expecting
process.cwd() to NOT use this field.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(worktree): address PR #4174 round 5 findings
Finding #3256241831 (Nit, but awareness UX): the built-in `⎇`
indicator used to disappear whenever `statusLineLines.length > 0`,
on the assumption that the user's custom statusline rendered worktree
itself. That assumption is unsafe — scripts written before Phase C
don't know about `payload.worktree`, scripts can deliberately ignore
the field, and partial scripts may render some fields but not
worktree. In any of those cases the user sees no worktree UI while
having an active worktree, risking destructive operations in the
wrong cwd. New behavior: indicator shows by default regardless of
statusline. Added an opt-out setting `ui.hideBuiltinWorktreeIndicator`
(default false) for users whose custom statusline already renders
worktree and want to avoid duplication.
Finding #3256239608 (Nit): `fs.watch` in useWorktreeSession holds
an inode handle to `chatsDir` at mount time. If the directory is
deleted out-of-band (manual cleanup, antivirus quarantine, reset
scripts) and recreated, the watcher does NOT re-attach to the new
inode and the Footer indicator stops reacting to sidecar changes.
Reviewer explicitly accepted this as a documented limitation rather
than adding polling-fallback or error-event-handler complexity for
an edge case that doesn't arise in normal use. Added a JSDoc block
on the hook explaining the limitation and pointing to the future
fix shapes.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* chore(worktree): regenerate settings.schema.json for hideBuiltinWorktreeIndicator
CI Lint step caught that the JSON schema mirror in
packages/vscode-ide-companion was out of date after adding the new
ui.hideBuiltinWorktreeIndicator setting in 80f9cb495. Regenerated
via `npm run generate:settings-schema`.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(worktree): address PR #4174 round 6 findings
Critical fixes:
- #3259975247: TUI dialog Remove now reads the in-worktree session
marker and refuses to delete a worktree owned by a different
session — same ownership guard ExitWorktreeTool already applies.
Stale/copied sidecars can no longer destroy another session's work.
- #3259975249: TUI --resume queues a one-shot pendingWorktreeNotice
ref consumed by handleFinalSubmit; the user's first prompt is
prefixed with the same <system-reminder> block headless/ACP use.
Previously only the INFO history item showed in the transcript
(UI-only), so resumed models could silently edit the parent
checkout.
- #3259975245: exit_worktree action='keep' no longer clears the
sidecar. `keep` means "preserve the worktree for later"; clearing
the persisted binding broke --resume / Footer / WorktreeExitDialog
for kept worktrees. Now matches the Dialog keep semantics. Test
updated to assert preservation instead of clearing.
- ACP unstable_resumeSession parity: factored the worktree restore
block into #restoreWorktreeOnResume() and called from both
loadSession() and unstable_resumeSession(). ACP clients using
resume no longer miss the worktree context.
Suggestion-level fixes:
- #3259975237: configureHooksPath now resolves the canonical hooks
dir via `git rev-parse --git-common-dir` instead of constructing
`<sourceRepoPath>/.git/hooks`. The construction assumed .git is a
directory, but when Qwen runs from a linked worktree it's a file
pointing at the real gitdir → ENOTDIR → silent no-hooks worktree.
- #3259975242: only writes core.hooksPath when the key is unset.
A non-empty inherited or user-configured value is preserved
instead of being silently replaced.
- #3256839787: restoreWorktreeContext adds a structural invariant
check — worktreePath must live under <originalCwd>/.qwen/worktrees/.
A tampered/copied sidecar pointing at an arbitrary existing dir
is rejected and cleared so the model can't be redirected.
Tests:
- worktreeSessionService.test: 17/17 (added prefix-escape rejection
case + restructured the existing live-worktree case to satisfy
the new structural invariant).
- exit-worktree.session.integ.test: rewrote keep test to assert
preservation (matches new behavior).
- nonInteractiveCli.test: updated fixture worktreeDir to live
under <originalCwd>/.qwen/worktrees/ for the prefix invariant.
- All other suites pass without modification.
Test coverage gap acknowledgement (no comment_id reply): per-handler
unit tests for handleWorktreeExit + dialog post-load states remain
covered by the E2E Group E suite in docs/e2e-tests/worktree-phase-c.md.
The execFile mock path in ink-testing-library still doesn't deliver
async useEffect state transitions reliably, so unit testing those
states adds more harness than signal; deferring.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
b0ea9f4849
commit
a7e05302e6
33 changed files with 4718 additions and 39 deletions
594
docs/e2e-tests/worktree-phase-c.md
Normal file
594
docs/e2e-tests/worktree-phase-c.md
Normal file
|
|
@ -0,0 +1,594 @@
|
|||
# Worktree Phase C E2E Test Plan
|
||||
|
||||
## Scope
|
||||
|
||||
End-to-end verification of Phase C features against the local build at
|
||||
`/Users/mochi/code/qwen-code/.claude/worktrees/romantic-burnell-b6e48c/dist/cli.js`.
|
||||
|
||||
Phase C delivers:
|
||||
|
||||
- **Task 1, 3, 4** — `WorktreeSession` sidecar JSON file at
|
||||
`~/.qwen/tmp/<projectHash>/chats/<sessionId>.worktree.json`
|
||||
- **Task 2** — `core.hooksPath` configured inside new worktrees
|
||||
- **Task 5–6** — `useWorktreeSession` hook, `UIState.activeWorktree`, Footer
|
||||
worktree indicator, `StatusLineCommandInput.worktree` field
|
||||
- **Task 7** — `--resume` injects an INFO history item when active worktree
|
||||
still exists; cleans up stale sidecar otherwise
|
||||
- **Task 8** — `WorktreeExitDialog` with dirty-state inspection, intercepts
|
||||
second Ctrl+C in active worktree
|
||||
|
||||
## Binaries
|
||||
|
||||
- **Local build**: `node /Users/mochi/code/qwen-code/.claude/worktrees/romantic-burnell-b6e48c/dist/cli.js`
|
||||
- **Baseline (for pre-impl comparison if needed)**: globally installed `qwen`
|
||||
|
||||
## Test environment template
|
||||
|
||||
Each group runs in its own temp git repo and tmux session:
|
||||
|
||||
```bash
|
||||
TEST_DIR=$(mktemp -d -t qwen-wt-phc-XXXXXX)
|
||||
TEST_DIR=$(cd "$TEST_DIR" && pwd -P) # resolve symlinks (macOS /var → /private/var)
|
||||
cd "$TEST_DIR"
|
||||
git init -q -b main
|
||||
git config user.email t@e.com
|
||||
git config user.name t
|
||||
git config commit.gpgsign false
|
||||
echo "hello" > README.md
|
||||
git add README.md
|
||||
git commit -q -m "initial" --no-verify
|
||||
```
|
||||
|
||||
`QWEN=/Users/mochi/code/qwen-code/.claude/worktrees/romantic-burnell-b6e48c/dist/cli.js`
|
||||
|
||||
---
|
||||
|
||||
## Group A: WorktreeSession sidecar (headless)
|
||||
|
||||
**Mode:** headless, `--approval-mode yolo`, `--output-format json`
|
||||
|
||||
### A1: enter_worktree writes sidecar with all fields
|
||||
|
||||
**Steps:**
|
||||
|
||||
```bash
|
||||
SESSION=$(node $QWEN "use the enter_worktree tool with name='a1-test' to create a worktree" \
|
||||
--approval-mode yolo --output-format json 2>/dev/null \
|
||||
| jq -r '.[] | select(.type=="system") | .session_id' | head -1)
|
||||
|
||||
PROJECT_ID=$(node -e "console.log(process.argv[1].replace(/[^a-zA-Z0-9]/g,'-'))" "$TEST_DIR")
|
||||
SIDECAR=~/.qwen/projects/$PROJECT_ID/chats/$SESSION.worktree.json
|
||||
|
||||
# Verify all fields present
|
||||
cat "$SIDECAR" | jq '.slug, .worktreePath, .worktreeBranch, .originalCwd, .originalBranch, .originalHeadCommit'
|
||||
```
|
||||
|
||||
**Expected:**
|
||||
|
||||
- `slug` = "a1-test"
|
||||
- `worktreePath` ends with `.qwen/worktrees/a1-test`
|
||||
- `worktreeBranch` = "worktree-a1-test"
|
||||
- `originalCwd` = `$TEST_DIR` (resolved)
|
||||
- `originalBranch` = "main"
|
||||
- `originalHeadCommit` matches `[0-9a-f]{40}`
|
||||
|
||||
### A2: exit_worktree (keep) clears sidecar
|
||||
|
||||
**Steps:**
|
||||
|
||||
```bash
|
||||
SESSION=$(node $QWEN "create a worktree named 'a2-test' using enter_worktree, then immediately exit it with action='keep' using exit_worktree" \
|
||||
--approval-mode yolo --output-format json 2>/dev/null \
|
||||
| jq -r '.[] | select(.type=="system") | .session_id' | head -1)
|
||||
|
||||
SIDECAR=~/.qwen/projects/$PROJECT_ID/chats/$SESSION.worktree.json
|
||||
test ! -f "$SIDECAR" && echo "PASS: sidecar removed" || echo "FAIL: sidecar still exists"
|
||||
```
|
||||
|
||||
**Expected:** sidecar file does not exist after the exit_worktree call.
|
||||
|
||||
### A3: exit_worktree (remove) clears sidecar
|
||||
|
||||
**Steps:**
|
||||
|
||||
```bash
|
||||
SESSION=$(node $QWEN "create a worktree named 'a3-test' using enter_worktree, then immediately exit it with action='remove' and discard_changes=true using exit_worktree" \
|
||||
--approval-mode yolo --output-format json 2>/dev/null \
|
||||
| jq -r '.[] | select(.type=="system") | .session_id' | head -1)
|
||||
|
||||
SIDECAR=~/.qwen/projects/$PROJECT_ID/chats/$SESSION.worktree.json
|
||||
test ! -f "$SIDECAR" && echo "PASS: sidecar removed" || echo "FAIL: sidecar still exists"
|
||||
# Also verify the worktree dir is gone
|
||||
test ! -d "$TEST_DIR/.qwen/worktrees/a3-test" && echo "PASS: worktree dir removed"
|
||||
```
|
||||
|
||||
**Expected:** both the sidecar AND the worktree directory are gone.
|
||||
|
||||
---
|
||||
|
||||
## Group B: hooksPath configuration (headless)
|
||||
|
||||
### B1: Without `.husky/`, hooksPath = `<repo>/.git/hooks`
|
||||
|
||||
**Steps:**
|
||||
|
||||
```bash
|
||||
node $QWEN "use enter_worktree with name='b1-test' to create a worktree" \
|
||||
--approval-mode yolo --output-format json 2>/dev/null > /dev/null
|
||||
|
||||
HOOKS_PATH=$(git -C "$TEST_DIR/.qwen/worktrees/b1-test" config --local core.hooksPath)
|
||||
echo "Got hooksPath: $HOOKS_PATH"
|
||||
test "$HOOKS_PATH" = "$TEST_DIR/.git/hooks" && echo "PASS" || echo "FAIL"
|
||||
```
|
||||
|
||||
**Expected:** `$TEST_DIR/.git/hooks`
|
||||
|
||||
### B2: With `.husky/`, hooksPath = `<repo>/.husky`
|
||||
|
||||
**Steps:**
|
||||
|
||||
```bash
|
||||
mkdir -p "$TEST_DIR/.husky"
|
||||
echo '#!/bin/sh' > "$TEST_DIR/.husky/pre-commit"
|
||||
chmod +x "$TEST_DIR/.husky/pre-commit"
|
||||
|
||||
node $QWEN "use enter_worktree with name='b2-test' to create a worktree" \
|
||||
--approval-mode yolo --output-format json 2>/dev/null > /dev/null
|
||||
|
||||
HOOKS_PATH=$(git -C "$TEST_DIR/.qwen/worktrees/b2-test" config --local core.hooksPath)
|
||||
test "$HOOKS_PATH" = "$TEST_DIR/.husky" && echo "PASS" || echo "FAIL got=$HOOKS_PATH"
|
||||
```
|
||||
|
||||
**Expected:** `$TEST_DIR/.husky`
|
||||
|
||||
### B3: Hooks in main repo actually fire from inside worktree
|
||||
|
||||
**Steps:**
|
||||
|
||||
```bash
|
||||
# Set up a hook that writes a marker file
|
||||
mkdir -p "$TEST_DIR/.git/hooks"
|
||||
cat > "$TEST_DIR/.git/hooks/pre-commit" <<'EOF'
|
||||
#!/bin/sh
|
||||
echo "hook-fired" > /tmp/qwen-wt-hook-marker
|
||||
EOF
|
||||
chmod +x "$TEST_DIR/.git/hooks/pre-commit"
|
||||
|
||||
node $QWEN "use enter_worktree with name='b3-test' to create a worktree" \
|
||||
--approval-mode yolo --output-format json 2>/dev/null > /dev/null
|
||||
|
||||
# Commit something inside the worktree
|
||||
WT="$TEST_DIR/.qwen/worktrees/b3-test"
|
||||
echo "x" > "$WT/file.txt"
|
||||
git -C "$WT" add file.txt
|
||||
rm -f /tmp/qwen-wt-hook-marker
|
||||
git -C "$WT" commit -m "trigger hook" 2>&1
|
||||
test -f /tmp/qwen-wt-hook-marker && echo "PASS: hook fired" || echo "FAIL: hook did not fire"
|
||||
rm -f /tmp/qwen-wt-hook-marker
|
||||
```
|
||||
|
||||
**Expected:** `/tmp/qwen-wt-hook-marker` exists after the commit.
|
||||
|
||||
---
|
||||
|
||||
## Group C: --resume worktree restoration (headless)
|
||||
|
||||
### C1: --resume injects worktree context when sidecar present and dir alive
|
||||
|
||||
**Steps:**
|
||||
|
||||
```bash
|
||||
# Create initial session with worktree
|
||||
INIT_OUT=$(node $QWEN "use enter_worktree with name='c1-test' to create a worktree" \
|
||||
--approval-mode yolo --output-format json 2>/dev/null)
|
||||
SESSION=$(echo "$INIT_OUT" | jq -r '.[] | select(.type=="system") | .session_id' | head -1)
|
||||
|
||||
# Resume the session and ask "what's my context?"
|
||||
RESUMED=$(node $QWEN --resume "$SESSION" "say SIDECAR-CONFIRM" \
|
||||
--approval-mode yolo --output-format json 2>/dev/null)
|
||||
|
||||
# Look for the injected INFO message text in the conversation
|
||||
echo "$RESUMED" | grep -q "Resumed.*Active worktree.*c1-test" && echo "PASS" || echo "FAIL: no context injection"
|
||||
```
|
||||
|
||||
**Expected:** the JSON stream contains an INFO message referencing `c1-test`.
|
||||
|
||||
### C2: --resume cleans up stale sidecar when worktree dir is gone
|
||||
|
||||
**Steps:**
|
||||
|
||||
```bash
|
||||
INIT_OUT=$(node $QWEN "use enter_worktree with name='c2-test' to create a worktree" \
|
||||
--approval-mode yolo --output-format json 2>/dev/null)
|
||||
SESSION=$(echo "$INIT_OUT" | jq -r '.[] | select(.type=="system") | .session_id' | head -1)
|
||||
SIDECAR=~/.qwen/projects/$PROJECT_ID/chats/$SESSION.worktree.json
|
||||
|
||||
# Delete the worktree directory out-of-band
|
||||
rm -rf "$TEST_DIR/.qwen/worktrees/c2-test"
|
||||
test -f "$SIDECAR" || { echo "SKIP: sidecar was already gone"; exit 0; }
|
||||
|
||||
# Resume — should clean up the stale sidecar
|
||||
node $QWEN --resume "$SESSION" "hello" --approval-mode yolo --output-format json 2>/dev/null > /dev/null
|
||||
test ! -f "$SIDECAR" && echo "PASS: stale sidecar cleaned" || echo "FAIL: stale sidecar still present"
|
||||
```
|
||||
|
||||
**Expected:** sidecar file is removed.
|
||||
|
||||
---
|
||||
|
||||
## Group D: Footer worktree indicator (interactive tmux)
|
||||
|
||||
### D1: Footer shows worktree indicator after enter_worktree
|
||||
|
||||
**Steps:**
|
||||
|
||||
```bash
|
||||
tmux new-session -d -s wt-d1 -x 200 -y 50 \
|
||||
"cd $TEST_DIR && node $QWEN --approval-mode yolo"
|
||||
sleep 3
|
||||
|
||||
tmux send-keys -t wt-d1 "use enter_worktree with name='d1-test'"
|
||||
sleep 0.5
|
||||
tmux send-keys -t wt-d1 Enter
|
||||
|
||||
for i in $(seq 1 30); do
|
||||
sleep 2
|
||||
tmux capture-pane -t wt-d1 -p | grep -q "Type your message" && break
|
||||
done
|
||||
|
||||
# Capture and look for the worktree indicator line in Footer area
|
||||
tmux capture-pane -t wt-d1 -p -S -100 > /tmp/wt-d1.out
|
||||
grep -E "⎇.*worktree-d1-test.*\(d1-test\)" /tmp/wt-d1.out && echo "PASS" || \
|
||||
{ echo "FAIL — captured output:"; cat /tmp/wt-d1.out; }
|
||||
tmux kill-session -t wt-d1
|
||||
```
|
||||
|
||||
**Expected:** Footer contains a line like `⎇ worktree-d1-test (d1-test)`.
|
||||
|
||||
### D2: Footer indicator disappears after exit_worktree (keep)
|
||||
|
||||
**Steps:**
|
||||
|
||||
```bash
|
||||
tmux new-session -d -s wt-d2 -x 200 -y 50 \
|
||||
"cd $TEST_DIR && node $QWEN --approval-mode yolo"
|
||||
sleep 3
|
||||
|
||||
tmux send-keys -t wt-d2 "use enter_worktree with name='d2-test'"
|
||||
sleep 0.5
|
||||
tmux send-keys -t wt-d2 Enter
|
||||
for i in $(seq 1 30); do sleep 2; tmux capture-pane -t wt-d2 -p | grep -q "Type your message" && break; done
|
||||
|
||||
# Verify indicator showed
|
||||
tmux capture-pane -t wt-d2 -p -S -100 | grep -q "⎇.*d2-test" || { echo "FAIL: indicator missing before exit"; tmux kill-session -t wt-d2; exit 1; }
|
||||
|
||||
# Exit the worktree (keep)
|
||||
tmux send-keys -t wt-d2 "use exit_worktree with name='d2-test' action='keep'"
|
||||
sleep 0.5
|
||||
tmux send-keys -t wt-d2 Enter
|
||||
for i in $(seq 1 30); do sleep 2; tmux capture-pane -t wt-d2 -p | grep -q "Kept worktree" && break; done
|
||||
|
||||
sleep 2 # give Footer a tick to refresh after sidecar removal
|
||||
tmux capture-pane -t wt-d2 -p -S -100 > /tmp/wt-d2-after.out
|
||||
# After exit, the indicator should be gone from the bottom panel area
|
||||
tail -5 /tmp/wt-d2-after.out | grep -q "⎇.*d2-test" && \
|
||||
echo "FAIL: indicator still showing" || echo "PASS"
|
||||
tmux kill-session -t wt-d2
|
||||
```
|
||||
|
||||
**Expected:** worktree indicator disappears from Footer within ~2s of `exit_worktree`.
|
||||
|
||||
---
|
||||
|
||||
## Group E: WorktreeExitDialog (interactive tmux)
|
||||
|
||||
### E1: Second Ctrl+C in worktree shows dialog instead of quitting
|
||||
|
||||
**Steps:**
|
||||
|
||||
```bash
|
||||
tmux new-session -d -s wt-e1 -x 200 -y 50 \
|
||||
"cd $TEST_DIR && node $QWEN --approval-mode yolo"
|
||||
sleep 3
|
||||
|
||||
tmux send-keys -t wt-e1 "use enter_worktree with name='e1-test'"
|
||||
sleep 0.5
|
||||
tmux send-keys -t wt-e1 Enter
|
||||
for i in $(seq 1 30); do sleep 2; tmux capture-pane -t wt-e1 -p | grep -q "Type your message" && break; done
|
||||
|
||||
# First Ctrl+C (cleanup; should show "Press Ctrl+C again to exit")
|
||||
tmux send-keys -t wt-e1 C-c
|
||||
sleep 0.3
|
||||
tmux capture-pane -t wt-e1 -p | grep -q "Press Ctrl+C again" || \
|
||||
{ echo "FAIL: first Ctrl+C didn't show warning"; tmux kill-session -t wt-e1; exit 1; }
|
||||
|
||||
# Second Ctrl+C — should show the WorktreeExitDialog, NOT quit
|
||||
tmux send-keys -t wt-e1 C-c
|
||||
sleep 2
|
||||
|
||||
# Verify the dialog rendered
|
||||
tmux capture-pane -t wt-e1 -p -S -50 > /tmp/wt-e1.out
|
||||
grep -q "Active worktree.*e1-test" /tmp/wt-e1.out && \
|
||||
grep -q "Keep worktree" /tmp/wt-e1.out && \
|
||||
grep -q "Remove worktree" /tmp/wt-e1.out && \
|
||||
echo "PASS" || { echo "FAIL — captured:"; cat /tmp/wt-e1.out; }
|
||||
tmux kill-session -t wt-e1
|
||||
```
|
||||
|
||||
**Expected:** dialog shows three options (Keep / Remove / Cancel) and process is still alive.
|
||||
|
||||
### E2: Dialog shows dirty-state counts (commits + files)
|
||||
|
||||
**Steps:**
|
||||
|
||||
```bash
|
||||
tmux new-session -d -s wt-e2 -x 200 -y 50 \
|
||||
"cd $TEST_DIR && node $QWEN --approval-mode yolo"
|
||||
sleep 3
|
||||
|
||||
tmux send-keys -t wt-e2 "use enter_worktree with name='e2-test'"
|
||||
sleep 0.5
|
||||
tmux send-keys -t wt-e2 Enter
|
||||
for i in $(seq 1 30); do sleep 2; tmux capture-pane -t wt-e2 -p | grep -q "Type your message" && break; done
|
||||
|
||||
# Make the worktree dirty: 1 new commit + 1 uncommitted file
|
||||
WT="$TEST_DIR/.qwen/worktrees/e2-test"
|
||||
echo "new" > "$WT/new.txt"
|
||||
git -C "$WT" add new.txt
|
||||
git -C "$WT" commit -q -m "test commit" --no-verify
|
||||
echo "dirty" > "$WT/uncommitted.txt"
|
||||
|
||||
# Trigger exit dialog via Ctrl+C double-press
|
||||
tmux send-keys -t wt-e2 C-c
|
||||
sleep 0.3
|
||||
tmux send-keys -t wt-e2 C-c
|
||||
sleep 3 # allow time for git status / rev-list
|
||||
|
||||
tmux capture-pane -t wt-e2 -p -S -50 > /tmp/wt-e2.out
|
||||
grep -qE "new commit|uncommitted file" /tmp/wt-e2.out && echo "PASS" || \
|
||||
{ echo "FAIL — captured:"; cat /tmp/wt-e2.out; }
|
||||
tmux kill-session -t wt-e2
|
||||
```
|
||||
|
||||
**Expected:** dialog body contains both "X new commit(s)" and "Y uncommitted file(s)".
|
||||
|
||||
### E3: Cancel option dismisses dialog without exiting
|
||||
|
||||
**Steps:**
|
||||
|
||||
```bash
|
||||
tmux new-session -d -s wt-e3 -x 200 -y 50 \
|
||||
"cd $TEST_DIR && node $QWEN --approval-mode yolo"
|
||||
sleep 3
|
||||
|
||||
tmux send-keys -t wt-e3 "use enter_worktree with name='e3-test'"
|
||||
sleep 0.5
|
||||
tmux send-keys -t wt-e3 Enter
|
||||
for i in $(seq 1 30); do sleep 2; tmux capture-pane -t wt-e3 -p | grep -q "Type your message" && break; done
|
||||
|
||||
# Trigger dialog
|
||||
tmux send-keys -t wt-e3 C-c
|
||||
sleep 0.3
|
||||
tmux send-keys -t wt-e3 C-c
|
||||
sleep 3
|
||||
|
||||
# Navigate to Cancel (DOWN DOWN) and press Enter
|
||||
tmux send-keys -t wt-e3 Down
|
||||
sleep 0.2
|
||||
tmux send-keys -t wt-e3 Down
|
||||
sleep 0.2
|
||||
tmux send-keys -t wt-e3 Enter
|
||||
sleep 2
|
||||
|
||||
# Dialog should be gone; input prompt should be back
|
||||
tmux capture-pane -t wt-e3 -p | grep -q "Type your message" && echo "PASS" || \
|
||||
{ echo "FAIL — captured:"; tmux capture-pane -t wt-e3 -p; }
|
||||
|
||||
# Verify the worktree was NOT removed
|
||||
test -d "$TEST_DIR/.qwen/worktrees/e3-test" && echo "worktree intact" || echo "FAIL: worktree gone"
|
||||
tmux kill-session -t wt-e3
|
||||
```
|
||||
|
||||
**Expected:** dialog closes, input prompt returns, worktree directory still exists.
|
||||
|
||||
### E4: Keep option exits session but preserves worktree
|
||||
|
||||
**Steps:**
|
||||
|
||||
```bash
|
||||
tmux new-session -d -s wt-e4 -x 200 -y 50 \
|
||||
"cd $TEST_DIR && node $QWEN --approval-mode yolo"
|
||||
sleep 3
|
||||
|
||||
tmux send-keys -t wt-e4 "use enter_worktree with name='e4-test'"
|
||||
sleep 0.5
|
||||
tmux send-keys -t wt-e4 Enter
|
||||
for i in $(seq 1 30); do sleep 2; tmux capture-pane -t wt-e4 -p | grep -q "Type your message" && break; done
|
||||
|
||||
# Trigger dialog and pick Keep (first option, already selected)
|
||||
tmux send-keys -t wt-e4 C-c
|
||||
sleep 0.3
|
||||
tmux send-keys -t wt-e4 C-c
|
||||
sleep 3
|
||||
tmux send-keys -t wt-e4 Enter
|
||||
|
||||
# Wait for process to exit
|
||||
for i in $(seq 1 20); do
|
||||
sleep 1
|
||||
tmux has-session -t wt-e4 2>/dev/null || break
|
||||
tmux capture-pane -t wt-e4 -p | grep -q "\$ " && break # shell prompt back
|
||||
done
|
||||
|
||||
# Worktree directory should still exist
|
||||
test -d "$TEST_DIR/.qwen/worktrees/e4-test" && echo "PASS: worktree preserved" || \
|
||||
echo "FAIL: worktree was removed"
|
||||
tmux kill-session -t wt-e4 2>/dev/null || true
|
||||
```
|
||||
|
||||
**Expected:** process exits, worktree directory remains on disk.
|
||||
|
||||
### E5: Remove option exits session and deletes worktree
|
||||
|
||||
**Steps:**
|
||||
|
||||
```bash
|
||||
tmux new-session -d -s wt-e5 -x 200 -y 50 \
|
||||
"cd $TEST_DIR && node $QWEN --approval-mode yolo"
|
||||
sleep 3
|
||||
|
||||
tmux send-keys -t wt-e5 "use enter_worktree with name='e5-test'"
|
||||
sleep 0.5
|
||||
tmux send-keys -t wt-e5 Enter
|
||||
for i in $(seq 1 30); do sleep 2; tmux capture-pane -t wt-e5 -p | grep -q "Type your message" && break; done
|
||||
|
||||
# Trigger dialog and pick Remove (DOWN, Enter)
|
||||
tmux send-keys -t wt-e5 C-c
|
||||
sleep 0.3
|
||||
tmux send-keys -t wt-e5 C-c
|
||||
sleep 3
|
||||
tmux send-keys -t wt-e5 Down
|
||||
sleep 0.2
|
||||
tmux send-keys -t wt-e5 Enter
|
||||
|
||||
# Wait for exit
|
||||
for i in $(seq 1 20); do
|
||||
sleep 1
|
||||
tmux has-session -t wt-e5 2>/dev/null || break
|
||||
tmux capture-pane -t wt-e5 -p | grep -q "\$ " && break
|
||||
done
|
||||
|
||||
# Worktree directory should be GONE
|
||||
test ! -d "$TEST_DIR/.qwen/worktrees/e5-test" && echo "PASS: worktree removed" || \
|
||||
echo "FAIL: worktree still on disk"
|
||||
# Branch should also be deleted
|
||||
git -C "$TEST_DIR" branch --list | grep -q "worktree-e5-test" && \
|
||||
echo "FAIL: branch still present" || echo "PASS: branch removed"
|
||||
tmux kill-session -t wt-e5 2>/dev/null || true
|
||||
```
|
||||
|
||||
**Expected:** process exits, worktree directory deleted, branch `worktree-e5-test` deleted.
|
||||
|
||||
---
|
||||
|
||||
## Group F: Real-user workflow simulation (interactive tmux)
|
||||
|
||||
### F1: Full enter → edit → commit → resume → exit (keep) flow
|
||||
|
||||
**Steps:**
|
||||
|
||||
```bash
|
||||
tmux new-session -d -s wt-f1 -x 200 -y 50 \
|
||||
"cd $TEST_DIR && node $QWEN --approval-mode yolo"
|
||||
sleep 3
|
||||
|
||||
# Step 1: enter worktree
|
||||
tmux send-keys -t wt-f1 "use enter_worktree with name='f1-feature' to create a worktree"
|
||||
sleep 0.5
|
||||
tmux send-keys -t wt-f1 Enter
|
||||
for i in $(seq 1 30); do sleep 2; tmux capture-pane -t wt-f1 -p | grep -q "Type your message" && break; done
|
||||
|
||||
# Step 2: read the absolute worktree path so the model knows where to write
|
||||
WT="$TEST_DIR/.qwen/worktrees/f1-feature"
|
||||
tmux send-keys -t wt-f1 "write the file $WT/hello.txt with content 'hi from worktree'"
|
||||
sleep 0.5
|
||||
tmux send-keys -t wt-f1 Enter
|
||||
for i in $(seq 1 60); do sleep 2; tmux capture-pane -t wt-f1 -p | grep -q "Type your message" && break; done
|
||||
|
||||
# Verify the file was actually written INSIDE the worktree
|
||||
test -f "$WT/hello.txt" && grep -q "hi from worktree" "$WT/hello.txt" && \
|
||||
echo "PASS: file written inside worktree" || echo "FAIL: file not in worktree"
|
||||
|
||||
# Step 3: Exit with keep via the tool
|
||||
tmux send-keys -t wt-f1 "use exit_worktree with name='f1-feature' action='keep'"
|
||||
sleep 0.5
|
||||
tmux send-keys -t wt-f1 Enter
|
||||
for i in $(seq 1 30); do sleep 2; tmux capture-pane -t wt-f1 -p | grep -q "Kept worktree" && break; done
|
||||
|
||||
# Step 4: Verify worktree still on disk after exit
|
||||
test -d "$WT" && echo "PASS: worktree kept" || echo "FAIL: worktree removed"
|
||||
test -f "$WT/hello.txt" && echo "PASS: file persists" || echo "FAIL"
|
||||
|
||||
tmux kill-session -t wt-f1
|
||||
```
|
||||
|
||||
**Expected:**
|
||||
|
||||
- File written to worktree directory (not main repo)
|
||||
- After exit `keep`, both the worktree directory and the file remain
|
||||
|
||||
### F2: Custom statusline receives `worktree` payload
|
||||
|
||||
**Steps:**
|
||||
|
||||
```bash
|
||||
# Create a statusline script that prints the JSON it receives via stdin
|
||||
SETTINGS_DIR=~/.qwen
|
||||
SETTINGS_FILE=$SETTINGS_DIR/settings.json
|
||||
cp -f "$SETTINGS_FILE" /tmp/qwen-settings-backup.json 2>/dev/null || true
|
||||
mkdir -p "$SETTINGS_DIR"
|
||||
SL_SCRIPT=/tmp/qwen-wt-statusline.sh
|
||||
cat > $SL_SCRIPT <<'EOF'
|
||||
#!/bin/sh
|
||||
INPUT=$(cat)
|
||||
echo "$INPUT" > /tmp/qwen-wt-statusline-input.json
|
||||
WT_NAME=$(echo "$INPUT" | jq -r '.worktree.name // "no-worktree"')
|
||||
echo "WT=$WT_NAME"
|
||||
EOF
|
||||
chmod +x $SL_SCRIPT
|
||||
|
||||
cat > "$SETTINGS_FILE" <<EOF
|
||||
{"ui":{"statusLine":{"type":"command","command":"$SL_SCRIPT"}}}
|
||||
EOF
|
||||
|
||||
tmux new-session -d -s wt-f2 -x 200 -y 50 \
|
||||
"cd $TEST_DIR && node $QWEN --approval-mode yolo"
|
||||
sleep 5 # statusline needs an extra tick
|
||||
|
||||
tmux send-keys -t wt-f2 "use enter_worktree with name='f2-test'"
|
||||
sleep 0.5
|
||||
tmux send-keys -t wt-f2 Enter
|
||||
for i in $(seq 1 30); do sleep 2; tmux capture-pane -t wt-f2 -p | grep -q "Type your message" && break; done
|
||||
|
||||
sleep 3 # let statusline refresh after sidecar change
|
||||
|
||||
# Inspect the captured payload
|
||||
cat /tmp/qwen-wt-statusline-input.json | jq '.worktree.name, .worktree.path, .worktree.branch'
|
||||
|
||||
# Verify built-in Footer indicator is HIDDEN when custom statusline is active
|
||||
tmux capture-pane -t wt-f2 -p -S -100 > /tmp/wt-f2.out
|
||||
grep -q "WT=f2-test" /tmp/wt-f2.out && echo "PASS: custom statusline rendered" || echo "FAIL"
|
||||
tmux kill-session -t wt-f2
|
||||
|
||||
# Restore settings
|
||||
cp -f /tmp/qwen-settings-backup.json "$SETTINGS_FILE" 2>/dev/null || rm -f "$SETTINGS_FILE"
|
||||
```
|
||||
|
||||
**Expected:**
|
||||
|
||||
- `/tmp/qwen-wt-statusline-input.json` has `.worktree.name == "f2-test"`, `.path`, `.branch` set
|
||||
- Custom statusline output `WT=f2-test` appears in Footer
|
||||
- Built-in `⎇ worktree-...` row is NOT rendered (suppressed by custom statusline)
|
||||
|
||||
---
|
||||
|
||||
## Pass criteria summary
|
||||
|
||||
| Group | Test | Expected |
|
||||
| ----- | -------------------------- | -------------------------------------------- |
|
||||
| A | A1 enter writes sidecar | all 6 fields populated |
|
||||
| A | A2 keep clears sidecar | file removed |
|
||||
| A | A3 remove clears sidecar | file + dir removed |
|
||||
| B | B1 hooksPath fallback | `<repo>/.git/hooks` |
|
||||
| B | B2 hooksPath husky | `<repo>/.husky` |
|
||||
| B | B3 hook fires in wt | marker file written |
|
||||
| C | C1 resume injects context | INFO message present |
|
||||
| C | C2 stale sidecar cleanup | sidecar removed |
|
||||
| D | D1 footer shows wt | `⎇ worktree-...` rendered |
|
||||
| D | D2 footer hides after exit | indicator disappears |
|
||||
| E | E1 dialog on 2nd Ctrl+C | dialog visible, alive |
|
||||
| E | E2 dirty-state counts | commits + files shown |
|
||||
| E | E3 Cancel | dialog gone, session alive |
|
||||
| E | E4 Keep | session exits, wt preserved |
|
||||
| E | E5 Remove | session exits, wt deleted |
|
||||
| F | F1 full workflow | file in wt, persists after keep |
|
||||
| F | F2 custom statusline | worktree payload received, footer suppressed |
|
||||
Loading…
Add table
Add a link
Reference in a new issue