mirror of
https://github.com/stablyai/orca.git
synced 2026-11-03 10:37:35 +00:00
* [comment audit] Shorten daemon, hook, and emulator comments [skip ci] * [comment audit] Shorten VM, conversation, SQLite, and theme-import comments [skip ci] * [comment audit] Tighten Git execution and source-control comment contracts [skip ci] * [comment audit] Clarify repository probes and maintenance comments [skip ci] * [comment audit] Shorten status, diff-cache, and checkout comments [skip ci] * [comment audit] Clarify worktree lifecycle and WSL routing comments [skip ci] * [comment audit] Shorten forge identity and review-refresh comments [skip ci] * [comment audit] Tighten hosted-review client comment contracts [skip ci] * [comment audit] Clarify GitLab compatibility and review comments [skip ci] * [comment audit] Tighten hook ownership and watchdog rationale [skip ci] * [comment audit] Condense host and integration compatibility comments [skip ci] * [comment audit] Clarify IPC authority and remote-routing rationale [skip ci] * [comment audit] Finalize file-import and containment comment cleanup * [comment audit] Fix stale Jira type imports blocking full CI --------- Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
652 lines
18 KiB
TypeScript
652 lines
18 KiB
TypeScript
import { existsSync } from 'node:fs'
|
|
import type * as FsPromises from 'node:fs/promises'
|
|
import { mkdir, mkdtemp, rm } from 'node:fs/promises'
|
|
import { tmpdir } from 'node:os'
|
|
import { join } from 'node:path'
|
|
import { afterEach, beforeEach, describe, expect, it, vi, type MockInstance } from 'vitest'
|
|
|
|
const {
|
|
gitExecFileAsyncMock,
|
|
gitExecFileSyncMock,
|
|
translateWslOutputPathsMock,
|
|
statMock,
|
|
readFileMock,
|
|
resolveGitDirMock
|
|
} = vi.hoisted(() => ({
|
|
gitExecFileAsyncMock: vi.fn(),
|
|
gitExecFileSyncMock: vi.fn(),
|
|
translateWslOutputPathsMock: vi.fn((output: string) => output),
|
|
statMock: vi.fn(),
|
|
readFileMock: vi.fn(),
|
|
resolveGitDirMock: vi.fn()
|
|
}))
|
|
|
|
vi.mock('./runner', () => ({
|
|
gitExecFileAsync: gitExecFileAsyncMock,
|
|
gitExecFileSync: gitExecFileSyncMock,
|
|
translateWslOutputPaths: translateWslOutputPathsMock
|
|
}))
|
|
|
|
vi.mock('./status', () => ({
|
|
resolveGitDir: resolveGitDirMock,
|
|
runWithGitReadCacheInvalidation: <T>(run: () => Promise<T>) => run()
|
|
}))
|
|
|
|
vi.mock('fs/promises', async () => {
|
|
const actual = await vi.importActual<typeof FsPromises>('fs/promises')
|
|
return { ...actual, stat: statMock, readFile: readFileMock }
|
|
})
|
|
|
|
import {
|
|
createGitCallReader,
|
|
createGitCommandMocker,
|
|
expectGitCallOrder,
|
|
resetWorktreeGitMocks,
|
|
resetWorktreeRemovalState
|
|
} from './remove-worktree-test-harness'
|
|
|
|
import { removeWorktree } from './worktree'
|
|
import {
|
|
__getSparseCheckoutStateCacheSizeForTests,
|
|
detectSparseCheckoutCached
|
|
} from './worktree-sparse-checkout-cache'
|
|
|
|
const mockGitCommands = createGitCommandMocker(gitExecFileAsyncMock)
|
|
const getGitCalls = createGitCallReader(gitExecFileAsyncMock)
|
|
|
|
// Pin non-Windows argv; Windows removal also sets core.longpaths.
|
|
let platformSpy: MockInstance<() => NodeJS.Platform>
|
|
|
|
beforeEach(() => {
|
|
resetWorktreeRemovalState()
|
|
platformSpy = vi.spyOn(process, 'platform', 'get').mockReturnValue('darwin')
|
|
})
|
|
|
|
afterEach(() => {
|
|
platformSpy.mockRestore()
|
|
})
|
|
|
|
describe('removeWorktree', () => {
|
|
beforeEach(() => {
|
|
resetWorktreeGitMocks({
|
|
gitExecFileAsyncMock,
|
|
gitExecFileSyncMock,
|
|
translateWslOutputPathsMock,
|
|
statMock,
|
|
readFileMock,
|
|
resolveGitDirMock
|
|
})
|
|
})
|
|
|
|
it('removes the worktree and deletes its local branch', async () => {
|
|
mockGitCommands({
|
|
'git worktree list --porcelain': {
|
|
stdout: `worktree /repo
|
|
HEAD abc123
|
|
branch refs/heads/main
|
|
|
|
worktree /repo-feature
|
|
HEAD def456
|
|
branch refs/heads/feature/test
|
|
`
|
|
},
|
|
'git worktree list --porcelain#2': {
|
|
stdout: `worktree /repo
|
|
HEAD abc123
|
|
branch refs/heads/main
|
|
`
|
|
}
|
|
})
|
|
|
|
await removeWorktree('/repo', '/repo-feature')
|
|
|
|
const calls = getGitCalls()
|
|
expect(calls).toEqual(
|
|
expect.arrayContaining(['git worktree remove /repo-feature', 'git branch -d -- feature/test'])
|
|
)
|
|
expect(calls).not.toContain('git worktree prune')
|
|
expectGitCallOrder(calls, 'git worktree remove /repo-feature', 'git branch -d -- feature/test')
|
|
})
|
|
|
|
it('preserves the branch when requested for a pre-existing local branch checkout', async () => {
|
|
mockGitCommands({
|
|
'git worktree list --porcelain': {
|
|
stdout: `worktree /repo
|
|
HEAD abc123
|
|
branch refs/heads/main
|
|
|
|
worktree /repo-feature
|
|
HEAD def456
|
|
branch refs/heads/feature/test
|
|
`
|
|
}
|
|
})
|
|
|
|
await removeWorktree('/repo', '/repo-feature', false, { deleteBranch: false })
|
|
|
|
const calls = getGitCalls()
|
|
expect(calls).toContain('git worktree remove /repo-feature')
|
|
expect(calls).not.toContain('git worktree prune')
|
|
expect(calls).not.toContain('git branch -d -- feature/test')
|
|
expect(calls).not.toContain('git branch -D -- feature/test')
|
|
})
|
|
|
|
it('skips branch deletion when another worktree still points at the branch', async () => {
|
|
mockGitCommands({
|
|
'git worktree list --porcelain': {
|
|
stdout: `worktree /repo
|
|
HEAD abc123
|
|
branch refs/heads/main
|
|
|
|
worktree /repo-feature
|
|
HEAD def456
|
|
branch refs/heads/feature/test
|
|
|
|
worktree /repo-feature-copy
|
|
HEAD def456
|
|
branch refs/heads/feature/test
|
|
`
|
|
},
|
|
'git worktree list --porcelain#2': {
|
|
stdout: `worktree /repo
|
|
HEAD abc123
|
|
branch refs/heads/main
|
|
|
|
worktree /repo-feature-copy
|
|
HEAD def456
|
|
branch refs/heads/feature/test
|
|
`
|
|
},
|
|
'git branch -d -- feature/test': {
|
|
error: new Error(
|
|
"cannot delete branch 'feature/test' used by worktree at '/repo-feature-copy'"
|
|
)
|
|
},
|
|
'git branch -d -- feature/test#2': {
|
|
error: new Error(
|
|
"cannot delete branch 'feature/test' used by worktree at '/repo-feature-copy'"
|
|
)
|
|
}
|
|
})
|
|
|
|
await removeWorktree('/repo', '/repo-feature')
|
|
|
|
const calls = getGitCalls()
|
|
expect(calls).toEqual(
|
|
expect.arrayContaining([
|
|
'git worktree remove /repo-feature',
|
|
'git branch -d -- feature/test',
|
|
'git worktree prune'
|
|
])
|
|
)
|
|
expect(calls.filter((call) => call === 'git branch -d -- feature/test')).toHaveLength(2)
|
|
expect(calls).not.toContain('git branch -D -- feature/test')
|
|
})
|
|
|
|
it('deletes the branch after prune removes stale sibling worktree entries', async () => {
|
|
mockGitCommands({
|
|
'git worktree list --porcelain': {
|
|
stdout: `worktree /repo
|
|
HEAD abc123
|
|
branch refs/heads/main
|
|
|
|
worktree /repo-feature
|
|
HEAD def456
|
|
branch refs/heads/feature/test
|
|
|
|
worktree /repo-stale
|
|
HEAD 0000000
|
|
branch refs/heads/feature/test
|
|
prunable gitdir file points to non-existent location
|
|
`
|
|
},
|
|
'git worktree list --porcelain#2': {
|
|
stdout: `worktree /repo
|
|
HEAD abc123
|
|
branch refs/heads/main
|
|
`
|
|
},
|
|
'git branch -d -- feature/test': {
|
|
error: new Error("cannot delete branch 'feature/test' used by worktree at '/repo-stale'")
|
|
},
|
|
'git branch -d -- feature/test#2': {
|
|
stdout: ''
|
|
}
|
|
})
|
|
|
|
await removeWorktree('/repo', '/repo-feature')
|
|
|
|
const calls = getGitCalls()
|
|
expect(calls).toEqual([
|
|
'git worktree list --porcelain -z',
|
|
'git worktree remove /repo-feature',
|
|
'git branch -d -- feature/test',
|
|
'git worktree prune',
|
|
'git branch -d -- feature/test'
|
|
])
|
|
})
|
|
|
|
it('lets Git delete the checkout inline with no deadline', async () => {
|
|
mockGitCommands({
|
|
'git worktree list --porcelain': {
|
|
stdout: `worktree /repo
|
|
HEAD abc123
|
|
branch refs/heads/main
|
|
|
|
worktree /repo-feature
|
|
HEAD def456
|
|
branch refs/heads/feature/test
|
|
`
|
|
},
|
|
'git worktree list --porcelain#2': {
|
|
stdout: `worktree /repo
|
|
HEAD abc123
|
|
branch refs/heads/main
|
|
`
|
|
}
|
|
})
|
|
|
|
await removeWorktree('/repo', '/repo-feature')
|
|
|
|
// Large deletions must finish without a deadline; their own queue bounds admission.
|
|
expect(gitExecFileAsyncMock).toHaveBeenCalledWith(['worktree', 'remove', '/repo-feature'], {
|
|
cwd: '/repo',
|
|
admissionExempt: true,
|
|
...(process.platform === 'darwin' ? { spawnOnWorkerThread: true } : {})
|
|
})
|
|
expect(getGitCalls()).not.toContain('git worktree prune')
|
|
expect(getGitCalls()).toContain('git branch -d -- feature/test')
|
|
})
|
|
|
|
it('lets a quit stop only the checkout delete, not the branch cleanup that holds ref locks', async () => {
|
|
mockGitCommands({
|
|
'git worktree list --porcelain': {
|
|
stdout: `worktree /repo
|
|
HEAD abc123
|
|
branch refs/heads/main
|
|
|
|
worktree /repo-feature
|
|
HEAD def456
|
|
branch refs/heads/feature/test
|
|
`
|
|
},
|
|
'git worktree list --porcelain#2': {
|
|
stdout: `worktree /repo
|
|
HEAD abc123
|
|
branch refs/heads/main
|
|
`
|
|
}
|
|
})
|
|
const stop = new AbortController()
|
|
|
|
await removeWorktree('/repo', '/repo-feature', false, {
|
|
checkoutDeleteSignal: stop.signal
|
|
})
|
|
|
|
const optionsOf = (command: string): { signal?: AbortSignal } | undefined =>
|
|
gitExecFileAsyncMock.mock.calls.find((call) => call[0].join(' ') === command)?.[1]
|
|
expect(optionsOf('worktree remove /repo-feature')?.signal).toBe(stop.signal)
|
|
expect(optionsOf('branch -d -- feature/test')).toBeDefined()
|
|
expect(optionsOf('branch -d -- feature/test')?.signal).toBeUndefined()
|
|
})
|
|
|
|
it('never lets Git run an inherited yes/no prompt program during the delete', async () => {
|
|
vi.stubEnv('GIT_ASK_YESNO', '/usr/local/bin/prompt')
|
|
try {
|
|
mockGitCommands({})
|
|
|
|
await removeWorktree('/repo', '/repo-feature', false, {
|
|
knownRemovedWorktree: { branch: '', head: '', locked: false }
|
|
})
|
|
|
|
const [, removeOptions] = gitExecFileAsyncMock.mock.calls[0]
|
|
expect(removeOptions.env).toBeDefined()
|
|
expect(removeOptions.env).not.toHaveProperty('GIT_ASK_YESNO')
|
|
} finally {
|
|
vi.unstubAllEnvs()
|
|
}
|
|
})
|
|
|
|
it('deletes what Git left of the checkout after a successful remove', async () => {
|
|
const scratch = await mkdtemp(join(tmpdir(), 'orca-remove-residue-'))
|
|
const worktreePath = join(scratch, 'feature')
|
|
// Git for Windows leaves junctions and the directories holding them behind yet exits 0.
|
|
await mkdir(join(worktreePath, 'node_modules', 'left-behind'), { recursive: true })
|
|
try {
|
|
mockGitCommands({})
|
|
|
|
await removeWorktree('/repo', worktreePath, false, {
|
|
knownRemovedWorktree: { branch: '', head: '', locked: false }
|
|
})
|
|
|
|
expect(getGitCalls()).toEqual([`git worktree remove ${worktreePath}`])
|
|
expect(existsSync(worktreePath)).toBe(false)
|
|
} finally {
|
|
await rm(scratch, { recursive: true, force: true })
|
|
}
|
|
})
|
|
|
|
it('leaves a WSL checkout that Git reported removed to the distro', async () => {
|
|
const scratch = await mkdtemp(join(tmpdir(), 'orca-remove-residue-wsl-'))
|
|
const worktreePath = join(scratch, 'feature')
|
|
await mkdir(worktreePath, { recursive: true })
|
|
try {
|
|
mockGitCommands({})
|
|
|
|
await removeWorktree('/repo', worktreePath, false, {
|
|
wslDistro: 'Ubuntu',
|
|
knownRemovedWorktree: { branch: '', head: '', locked: false }
|
|
})
|
|
|
|
expect(existsSync(worktreePath)).toBe(true)
|
|
} finally {
|
|
await rm(scratch, { recursive: true, force: true })
|
|
}
|
|
})
|
|
|
|
it('leaves a dirty checkout to Git to refuse', async () => {
|
|
mockGitCommands({
|
|
'git worktree list --porcelain': {
|
|
stdout: `worktree /repo
|
|
HEAD abc123
|
|
branch refs/heads/main
|
|
|
|
worktree /repo-feature
|
|
HEAD def456
|
|
branch refs/heads/feature/test
|
|
`
|
|
},
|
|
'git status --porcelain --untracked-files=all': { stdout: ' M src/app.ts\n' },
|
|
'git worktree remove /repo-feature': {
|
|
error: new Error('fatal: contains modified or untracked files, use --force to delete it')
|
|
}
|
|
})
|
|
|
|
await expect(removeWorktree('/repo', '/repo-feature')).rejects.toThrow(
|
|
'contains modified or untracked files'
|
|
)
|
|
expect(getGitCalls()).not.toContain('git worktree remove --force /repo-feature')
|
|
})
|
|
|
|
it('deletes WSL-hosted checkouts in place inside the distro', async () => {
|
|
mockGitCommands({
|
|
'git worktree list --porcelain': {
|
|
stdout: `worktree /repo
|
|
HEAD abc123
|
|
branch refs/heads/main
|
|
|
|
worktree /repo-feature
|
|
HEAD def456
|
|
branch refs/heads/feature/test
|
|
`
|
|
},
|
|
'git worktree list --porcelain#2': {
|
|
stdout: `worktree /repo
|
|
HEAD abc123
|
|
branch refs/heads/main
|
|
`
|
|
}
|
|
})
|
|
|
|
await removeWorktree('/repo', '/repo-feature', false, { wslDistro: 'Ubuntu' })
|
|
|
|
expect(getGitCalls()).not.toContain('git status --porcelain --untracked-files=all')
|
|
expect(getGitCalls()).toContain('git worktree remove /repo-feature')
|
|
})
|
|
|
|
it('removes a WSL checkout configured for a native Windows repo with Git alone', async () => {
|
|
const worktreePath = '\\\\wsl.localhost\\Ubuntu\\home\\dev\\feature'
|
|
platformSpy.mockReturnValue('win32')
|
|
mockGitCommands({})
|
|
|
|
await removeWorktree('C:\\repo', worktreePath, false, {
|
|
knownRemovedWorktree: { branch: '', head: '', locked: false }
|
|
})
|
|
|
|
expect(getGitCalls()).toEqual([`git -c core.longpaths=true worktree remove ${worktreePath}`])
|
|
})
|
|
|
|
it('passes one --force before the worktree path for dirty-file removal', async () => {
|
|
mockGitCommands({
|
|
'git worktree list --porcelain': {
|
|
stdout: `worktree /repo
|
|
HEAD abc123
|
|
branch refs/heads/main
|
|
|
|
worktree /repo-feature
|
|
HEAD def456
|
|
branch refs/heads/feature/test
|
|
`
|
|
},
|
|
'git worktree list --porcelain#2': {
|
|
stdout: `worktree /repo
|
|
HEAD abc123
|
|
branch refs/heads/main
|
|
`
|
|
}
|
|
})
|
|
|
|
await removeWorktree('/repo', '/repo-feature', true)
|
|
|
|
expect(getGitCalls()).toContain('git worktree remove --force /repo-feature')
|
|
})
|
|
|
|
it('preserves Git refusal even when parent status cannot reveal unpublished submodule commits', async () => {
|
|
mockGitCommands({
|
|
'git worktree list --porcelain': {
|
|
stdout: `worktree /repo
|
|
HEAD abc123
|
|
branch refs/heads/main
|
|
|
|
worktree /repo-feature
|
|
HEAD def456
|
|
branch refs/heads/feature/test
|
|
`
|
|
},
|
|
'git worktree list --porcelain#2': {
|
|
stdout: `worktree /repo
|
|
HEAD abc123
|
|
branch refs/heads/main
|
|
`
|
|
},
|
|
'git worktree remove /repo-feature': {
|
|
error: new Error('git worktree remove failed'),
|
|
stderr: 'fatal: working trees containing submodules cannot be moved or removed'
|
|
},
|
|
'git status --porcelain --untracked-files=all': { stdout: '' }
|
|
})
|
|
|
|
await expect(removeWorktree('/repo', '/repo-feature')).rejects.toThrow(
|
|
'git worktree remove failed'
|
|
)
|
|
expect(getGitCalls()).not.toContain('git worktree remove --force /repo-feature')
|
|
expect(getGitCalls()).not.toContain('git branch -d -- feature/test')
|
|
})
|
|
|
|
it('preserves Git refusal for a dirty submodule worktree', async () => {
|
|
mockGitCommands({
|
|
'git worktree list --porcelain': {
|
|
stdout: `worktree /repo
|
|
HEAD abc123
|
|
branch refs/heads/main
|
|
|
|
worktree /repo-feature
|
|
HEAD def456
|
|
branch refs/heads/feature/test
|
|
`
|
|
},
|
|
'git worktree remove /repo-feature': {
|
|
error: new Error('git worktree remove failed'),
|
|
stderr: 'fatal: working trees containing submodules cannot be moved or removed'
|
|
},
|
|
'git status --porcelain --untracked-files=all': { stdout: ' M sub\n' }
|
|
})
|
|
|
|
await expect(removeWorktree('/repo', '/repo-feature')).rejects.toThrow(
|
|
'git worktree remove failed'
|
|
)
|
|
expect(getGitCalls()).not.toContain('git worktree remove --force /repo-feature')
|
|
})
|
|
|
|
it('does not force-retry when the caller already forced removal', async () => {
|
|
mockGitCommands({
|
|
'git worktree list --porcelain': {
|
|
stdout: `worktree /repo
|
|
HEAD abc123
|
|
branch refs/heads/main
|
|
|
|
worktree /repo-feature
|
|
HEAD def456
|
|
branch refs/heads/feature/test
|
|
`
|
|
},
|
|
'git worktree remove --force /repo-feature': {
|
|
error: new Error('git worktree remove failed'),
|
|
stderr: 'fatal: working trees containing submodules cannot be moved or removed'
|
|
}
|
|
})
|
|
|
|
await expect(removeWorktree('/repo', '/repo-feature', true)).rejects.toThrow()
|
|
expect(
|
|
getGitCalls().filter((call) => call === 'git worktree remove --force /repo-feature')
|
|
).toHaveLength(1)
|
|
})
|
|
|
|
it('does not force-retry unrelated non-force remove failures', async () => {
|
|
mockGitCommands({
|
|
'git worktree list --porcelain': {
|
|
stdout: `worktree /repo
|
|
HEAD abc123
|
|
branch refs/heads/main
|
|
|
|
worktree /repo-feature
|
|
HEAD def456
|
|
branch refs/heads/feature/test
|
|
`
|
|
},
|
|
'git worktree remove /repo-feature': {
|
|
error: new Error('git worktree remove failed'),
|
|
stderr: 'fatal: contains modified or untracked files, use --force to delete it'
|
|
}
|
|
})
|
|
|
|
await expect(removeWorktree('/repo', '/repo-feature')).rejects.toThrow(
|
|
'git worktree remove failed'
|
|
)
|
|
expect(getGitCalls()).not.toContain('git worktree remove --force /repo-feature')
|
|
expect(getGitCalls()).not.toContain('git status --porcelain --untracked-files=all')
|
|
})
|
|
|
|
it('rejects a locked worktree with stable app-owned copy before invoking remove', async () => {
|
|
mockGitCommands({
|
|
'git worktree list --porcelain': {
|
|
stdout: `worktree /repo
|
|
HEAD abc123
|
|
branch refs/heads/main
|
|
|
|
worktree /repo-feature
|
|
HEAD def456
|
|
branch refs/heads/feature/test
|
|
locked active agent session
|
|
`
|
|
}
|
|
})
|
|
|
|
await expect(removeWorktree('/repo', '/repo-feature', true)).rejects.toThrow(
|
|
'Worktree is locked by Git. Lock reason: active agent session.'
|
|
)
|
|
expect(getGitCalls()).not.toContain('git worktree remove /repo-feature')
|
|
})
|
|
|
|
it('does not treat dirty-file force as permission to override a lock', async () => {
|
|
mockGitCommands({
|
|
'git worktree list --porcelain': {
|
|
stdout: `worktree /repo
|
|
HEAD abc123
|
|
branch refs/heads/main
|
|
|
|
worktree /repo-feature
|
|
HEAD def456
|
|
branch refs/heads/feature/test
|
|
locked active agent session
|
|
`
|
|
}
|
|
})
|
|
|
|
await expect(removeWorktree('/repo', '/repo-feature', true)).rejects.toThrow(
|
|
'Worktree is locked by Git. Lock reason: active agent session.'
|
|
)
|
|
expect(getGitCalls()).not.toContain('git worktree remove --force /repo-feature')
|
|
})
|
|
|
|
it('matches Windows worktree paths before deleting the branch', async () => {
|
|
mockGitCommands({
|
|
'git worktree list --porcelain': {
|
|
stdout: `worktree C:/repo
|
|
HEAD abc123
|
|
branch refs/heads/main
|
|
|
|
worktree C:/Workspaces/Delete-Branch-Ui-Test
|
|
HEAD def456
|
|
branch refs/heads/feature/test
|
|
`
|
|
},
|
|
'git worktree list --porcelain#2': {
|
|
stdout: `worktree C:/repo
|
|
HEAD abc123
|
|
branch refs/heads/main
|
|
`
|
|
}
|
|
})
|
|
|
|
await removeWorktree('C:\\repo', 'c:\\workspaces\\delete-branch-ui-test')
|
|
|
|
const calls = getGitCalls()
|
|
expect(calls).toEqual(
|
|
expect.arrayContaining([
|
|
'git worktree remove c:\\workspaces\\delete-branch-ui-test',
|
|
'git branch -d -- feature/test'
|
|
])
|
|
)
|
|
expect(calls).not.toContain('git worktree prune')
|
|
})
|
|
})
|
|
|
|
describe('removeWorktree sparse-checkout cache invalidation', () => {
|
|
beforeEach(() => {
|
|
resetWorktreeGitMocks({
|
|
gitExecFileAsyncMock,
|
|
gitExecFileSyncMock,
|
|
translateWslOutputPathsMock,
|
|
statMock,
|
|
readFileMock,
|
|
resolveGitDirMock
|
|
})
|
|
})
|
|
|
|
it('drops the removed worktree path so a re-created worktree at the same path is re-detected', async () => {
|
|
await detectSparseCheckoutCached('/repo', '/repo-feature')
|
|
expect(__getSparseCheckoutStateCacheSizeForTests()).toBe(1)
|
|
|
|
mockGitCommands({
|
|
'git worktree list --porcelain': {
|
|
stdout: `worktree /repo
|
|
HEAD abc123
|
|
branch refs/heads/main
|
|
|
|
worktree /repo-feature
|
|
HEAD def456
|
|
branch refs/heads/feature/test
|
|
`
|
|
}
|
|
})
|
|
|
|
await removeWorktree('/repo', '/repo-feature', false, { deleteBranch: false })
|
|
|
|
// Other rows may be re-cached during removal; only the removed path must require a fresh stat.
|
|
const statCallsBefore = statMock.mock.calls.length
|
|
expect(await detectSparseCheckoutCached('/repo', '/repo-feature')).toBe(false)
|
|
expect(statMock.mock.calls.length).toBeGreaterThan(statCallsBefore)
|
|
})
|
|
})
|