Skip to content
Closed
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
39 changes: 36 additions & 3 deletions src/main/runtime/worktree-teardown.test.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,20 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'

const { listRegisteredPtysMock } = vi.hoisted(() => ({
listRegisteredPtysMock: vi.fn()
}))
const { listRegisteredPtysMock, terminateWindowsSetupRunnersForWorktreeIdMock } = vi.hoisted(
() => ({
listRegisteredPtysMock: vi.fn(),
terminateWindowsSetupRunnersForWorktreeIdMock: vi.fn()
})
)

vi.mock('../memory/pty-registry', () => ({
listRegisteredPtys: listRegisteredPtysMock
}))

vi.mock('../windows-worktree-setup-runner-kill', () => ({
terminateWindowsSetupRunnersForWorktreeId: terminateWindowsSetupRunnersForWorktreeIdMock
}))

import { killAllProcessesForWorktree, WORKTREE_PROCESS_SWEEP_TIMEOUT_MS } from './worktree-teardown'
import type { IPtyProvider, PtyProcessInfo } from '../providers/types'
import { DaemonPtyAdapter } from '../daemon/daemon-pty-adapter'
Expand Down Expand Up @@ -40,6 +47,8 @@ function createProviderStub(listProcesses: () => Promise<PtyProcessInfo[]>): IPt
describe('killAllProcessesForWorktree', () => {
beforeEach(() => {
listRegisteredPtysMock.mockReset()
terminateWindowsSetupRunnersForWorktreeIdMock.mockReset()
terminateWindowsSetupRunnersForWorktreeIdMock.mockResolvedValue(0)
})

it('reaches daemon sessions and registry entries without a runtime', async () => {
Expand Down Expand Up @@ -887,4 +896,28 @@ describe('killAllProcessesForWorktree', () => {
vi.useRealTimers()
}
})

it('sweeps Windows setup-runner.cmd trees after PTY stop (#10629)', async () => {
const worktreeId = 'repo-1::D:\\orca\\workspaces\\repo\\feature'
const localProvider = createProviderStub(async () => [
{ id: `${worktreeId}@@pty-1`, cwd: 'D:\\orca\\workspaces\\repo\\feature', title: 'shell' }
])
listRegisteredPtysMock.mockReturnValue([])

await killAllProcessesForWorktree(worktreeId, { localProvider, timeoutMs: 5_000 })

expect(localProvider.shutdown).toHaveBeenCalledWith(
`${worktreeId}@@pty-1`,
expect.objectContaining({ immediate: true })
)
expect(terminateWindowsSetupRunnersForWorktreeIdMock).toHaveBeenCalledWith(
worktreeId,
expect.objectContaining({ deadlineMs: expect.any(Number) })
)
const shutdownCall = (localProvider.shutdown as unknown as ReturnType<typeof vi.fn>).mock
.invocationCallOrder[0]
const setupRunnerSweepCall =
terminateWindowsSetupRunnersForWorktreeIdMock.mock.invocationCallOrder[0]
expect(setupRunnerSweepCall).toBeGreaterThan(shutdownCall)
})
})
9 changes: 9 additions & 0 deletions src/main/runtime/worktree-teardown.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
verifyUnstoppedPtys,
type UnstoppedPtyVerdict
} from './unstopped-pty-verification'
import { terminateWindowsSetupRunnersForWorktreeId } from '../windows-worktree-setup-runner-kill'

// Why: normal inventories still coalesce into one process scan, while a stale
// or pathological inventory cannot fan out unbounded provider/RPC shutdowns.
Expand Down Expand Up @@ -252,6 +253,14 @@ export async function killAllProcessesForWorktree(
}
}

// Why: reparented setup-runner.cmd keeps Windows worktree dirs locked after PTY stop (#10629).
// Bound the sweep so a stalled process enumeration cannot escape the teardown deadline.
await settleBeforeDeadline(
() => terminateWindowsSetupRunnersForWorktreeId(worktreeId, { deadlineMs: deadline }),
0,
deadline
)

return { runtimeStopped: runtimeResult.stopped, providerStopped, registryStopped }
}

Expand Down
Loading