diff --git a/src/main/ipc/workspace-cleanup-snapshot-ipc.test.ts b/src/main/ipc/workspace-cleanup-snapshot-ipc.test.ts index 79636d25029..828e41bb2d1 100644 --- a/src/main/ipc/workspace-cleanup-snapshot-ipc.test.ts +++ b/src/main/ipc/workspace-cleanup-snapshot-ipc.test.ts @@ -2,10 +2,13 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' import { ipcMain } from 'electron' import type { Store } from '../persistence' -const { persistScanResultMock, readScanSnapshotMock } = vi.hoisted(() => ({ - persistScanResultMock: vi.fn(), - readScanSnapshotMock: vi.fn() -})) +const { persistScanResultMock, readScanSnapshotMock, scanWorkspaceCleanupMock } = vi.hoisted( + () => ({ + persistScanResultMock: vi.fn(), + readScanSnapshotMock: vi.fn(), + scanWorkspaceCleanupMock: vi.fn() + }) +) const { beginPruneBatchMock, finishPruneBatchMock, recordPruneMock } = vi.hoisted(() => ({ beginPruneBatchMock: vi.fn(), @@ -25,6 +28,10 @@ vi.mock('../workspace-cleanup-scan-snapshot', () => ({ readWorkspaceCleanupScanSnapshot: readScanSnapshotMock })) +vi.mock('./workspace-cleanup-scan', () => ({ + scanWorkspaceCleanup: scanWorkspaceCleanupMock +})) + vi.mock('../workspace-cleanup-removal-snapshot-prune', () => ({ beginWorkspaceCleanupRemovalSnapshotPruneBatch: beginPruneBatchMock, finishWorkspaceCleanupRemovalSnapshotPruneBatch: finishPruneBatchMock, @@ -50,6 +57,18 @@ describe('workspace cleanup snapshot IPC', () => { vi.mocked(ipcMain.handle).mockClear() persistScanResultMock.mockReset().mockResolvedValue(undefined) readScanSnapshotMock.mockReset() + scanWorkspaceCleanupMock.mockReset().mockImplementation(async (_store, args, options) => { + const result = { scannedAt: NOW, candidates: [], errors: [] } + options.onProgress?.({ + scanId: args.scanId, + scannedAt: NOW, + scannedWorktreeCount: 0, + totalWorktreeCount: 0, + candidates: [], + errors: [] + }) + return result + }) beginPruneBatchMock.mockReset() finishPruneBatchMock.mockReset().mockResolvedValue(undefined) recordPruneMock.mockReset() @@ -78,6 +97,21 @@ describe('workspace cleanup snapshot IPC', () => { expect(persistScanResultMock).not.toHaveBeenCalled() }) + it('stops streaming progress after the invoking renderer is destroyed', async () => { + registerWorkspaceCleanupHandlers(makeEmptyStore()) + const handler = vi + .mocked(ipcMain.handle) + .mock.calls.find(([channel]) => channel === 'workspaceCleanup:scan')?.[1] + const send = vi.fn(() => { + throw new Error('Object has been destroyed') + }) + + await expect( + handler?.({ sender: { isDestroyed: () => true, send } } as never, { scanId: 'scan-1' }) + ).resolves.toEqual({ scannedAt: NOW, candidates: [], errors: [] }) + expect(send).not.toHaveBeenCalled() + }) + it('serves the cached scan snapshot through getCachedScan', async () => { const snapshot = { scannedAt: NOW, candidates: [], errors: [] } readScanSnapshotMock.mockResolvedValue(snapshot) diff --git a/src/main/ipc/workspace-cleanup.ts b/src/main/ipc/workspace-cleanup.ts index 91b7189cb67..ecf78e0dc5b 100644 --- a/src/main/ipc/workspace-cleanup.ts +++ b/src/main/ipc/workspace-cleanup.ts @@ -53,7 +53,11 @@ export function registerWorkspaceCleanupHandlers( const scanArgs = args ?? {} const result = await scanWorkspaceCleanup(store, scanArgs, { onProgress: scanArgs.scanId - ? (progress) => event.sender.send('workspaceCleanup:scanProgress', progress) + ? (progress) => { + if (!event.sender.isDestroyed()) { + event.sender.send('workspaceCleanup:scanProgress', progress) + } + } : undefined }) // Focused scans are live-only; persisting each rewrites and fsyncs the fleet snapshot.