* fix(workspace-worktree): restore re-attaches existing branch instead of recreating with -b
Restoring a session whose worktree directory was cleaned up but whose
branch still existed locally would 422 with `fatal: a branch named <X>
already exists`. The recovery path in `restore()` unconditionally fell
through to `git worktree add -b`, even though `destroy()` deliberately
preserves session branches so the user's commits aren't lost.
When the local branch already exists, restore now clears any stale
worktree registration at the target path and retries `git worktree add
<path> <branch>` (no -b/-B). The existing -b fallback is preserved
verbatim for the case where the local branch is genuinely missing
(only the remote ref exists). -B is intentionally not used — it would
force-reset the branch back to the base ref and silently discard the
session's commits, which is the opposite of restore's intent.
Test coverage:
- 6 new unit tests covering the recovery path, cleanup tolerance,
error propagation, and "no -b/-B" invariants
- 2 updated existing unit tests (now mock the new refExists check)
- 2 new integration tests exercising real git: branch preservation
on clean restore, and recovery from a dirty teardown that left a
stale registry entry
Closes#1741
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* test(workspace-worktree): explicitly create main branch in restore integration tests
CI runs git with a different `init.defaultBranch` than the local dev
environment, so the bare clone has no `main` branch when the test
attempts to push. Mirror the existing tests in this file (which also
call `git switch -c <branch>` before the first commit).
Fixes integration-test failures on PR #1742.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(workspace-worktree): rmSync stale workspace dir before retry
Follow-up to the previous commit on this branch. The recovery path
cleared the git worktree registry but didn't touch the filesystem, so
when restore was triggered by a stale junk directory at the workspace
path (workspace.exists() returns false because rev-parse fails on a
non-working-tree dir), the retry would fail with the same error:
fatal: '<workspacePath>' already exists
Replace the inline `worktree prune` cleanup with a call to the existing
`clearStaleWorktreePath()` helper, which handles all three states:
- dir gone → no-op
- dir present and not registered → rmSync
- dir present and still registered after prune → throws (safety:
never delete a registered worktree)
This mirrors how create() already handles the same stale-state cases
upfront via clearStaleWorktreePath at the top of its flow.
Test coverage:
- 1 new unit test: rmSyncs a stale workspace directory before retry
- 1 new unit test: refuses to rmSync a still-registered worktree dir
(data safety — error must propagate, not be swallowed)
- 1 new integration test on real git: dir physically present as
non-working-tree leftover, restore must rmSync it before retry,
and the session commit must survive
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* refactor(workspace-worktree): extract restore helpers, drop redundant prune
Addresses review feedback on PR #1742:
1. Extract two named helpers, reattachExistingBranch and
createBranchFromBase, so restore()'s catch block reads as the
bifurcation it actually is — 2 lines per branch, no nested
try/catch hierarchy. Behavior is unchanged.
2. Drop the redundant `worktree prune` that ran inside the recovery
path (via clearStaleWorktreePath). The entry-point prune in
restore() is sufficient. reattachExistingBranch now inlines the
existsSync + isRegisteredWorktree + rmSync sequence directly,
keeping the data-safety guard ("refuse to rmSync a registered
worktree") intact but skipping the second prune call.
The catch block shrinks from ~46 lines to 12 (the rest moves into
the helpers, where the docstrings can explain WHY each branch exists
without cluttering the call site).
Tests: same 64 unit + 13 integration tests still pass — the mock
sequences in the recovery-path tests no longer expect a second prune
call, since the new code doesn't make one.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(workspace-worktree): address Copilot review on PR #1742
Two real concerns flagged in inline review:
1. isRegisteredWorktree did strict string equality on paths. If
`workspacePath` was passed in a non-canonical form (trailing slash,
".." segments) and git reported the canonical path, the check would
false-negative — and the subsequent rmSync in cleanupStaleWorkspacePath
would silently delete a still-registered worktree (data loss). Fix
by resolve()-normalizing both sides before comparison.
2. createBranchFromBase (the "branch missing locally" recovery path)
skipped the stale-path cleanup that reattachExistingBranch did. So
if `workspacePath` had a stale dir AND the branch was missing,
`git worktree add -b ...` would fail with the same "<path> already
exists" error this PR was fixing for the re-attach case. Fix by
factoring the cleanup into cleanupStaleWorkspacePath, called from
both helpers.
Test coverage:
- Unit: path normalization safety — workspacePath with trailing slash
vs canonical registered path must still throw "still registered"
(proves rmSync is NOT called)
- Unit: createBranchFromBase clears stale dir before -b add
- Integration: branch missing locally + stale dir at workspacePath →
restore must clean dir AND recreate branch from origin (commit
preserved end-to-end on real git)
Existing 2 "branch missing" tests updated to mock the new cleanup
calls in createBranchFromBase.
66 unit tests + 14 integration tests pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>