import { afterEach, describe, expect, it, vi } from 'vitest'; const hoisted = vi.hoisted(() => { const skipResponsesForOps = new Set(); const workers: Array<{ messages: unknown[]; handlers: Map void>; postMessage: (message: unknown) => void; on: (event: string, handler: (value: unknown) => void) => void; terminate: ReturnType; }> = []; const createMockWorker = vi.fn().mockImplementation(() => { const worker = { messages: [] as unknown[], handlers: new Map void>(), postMessage(message: unknown) { worker.messages.push(message); const request = message as { id: string; op: string }; if (skipResponsesForOps.has(request.op)) return; queueMicrotask(() => { const handler = worker.handlers.get('message'); if (!handler) return; handler({ id: request.id, ok: true, result: request.op === 'listTeams' || request.op === 'getAllTasks' ? [] : null, diag: { op: request.op, totalMs: 0 }, }); }); }, on(event: string, handler: (value: unknown) => void) { worker.handlers.set(event, handler); }, terminate: vi.fn(async () => undefined), }; workers.push(worker); return worker; }); return { workers, createMockWorker, skipResponsesForOps, }; }); vi.mock('node:fs', async () => { const actual = await vi.importActual('node:fs'); return { ...actual, existsSync: vi.fn(() => true), }; }); vi.mock('node:worker_threads', () => ({ Worker: hoisted.createMockWorker, default: { Worker: hoisted.createMockWorker, }, })); describe('TeamFsWorkerClient', () => { afterEach(() => { vi.resetModules(); vi.clearAllMocks(); vi.useRealTimers(); hoisted.workers.length = 0; hoisted.skipResponsesForOps.clear(); }); it('prewarms the worker without running a scan', async () => { const { TeamFsWorkerClient } = await import( '../../../../src/main/services/team/TeamFsWorkerClient' ); const client = new TeamFsWorkerClient(); await client.prewarm(); expect(hoisted.workers).toHaveLength(1); expect(hoisted.workers[0].messages).toHaveLength(1); expect(hoisted.workers[0].messages[0]).toMatchObject({ op: 'warmup', payload: {}, }); }); it('does not queue warmup behind an already running worker', async () => { const { TeamFsWorkerClient } = await import( '../../../../src/main/services/team/TeamFsWorkerClient' ); const client = new TeamFsWorkerClient(); await client.listTeams({ largeConfigBytes: 8 * 1024, configHeadBytes: 4 * 1024, maxConfigBytes: 256 * 1024, maxMembersMetaBytes: 256 * 1024, maxSessionHistoryInSummary: 10, maxProjectPathHistoryInSummary: 10, }); await client.prewarm(); expect(hoisted.workers).toHaveLength(1); expect(hoisted.workers[0].messages).toHaveLength(1); expect(hoisted.workers[0].messages[0]).toMatchObject({ op: 'listTeams', }); }); it('ignores stale worker exit after timeout when a replacement worker owns pending work', async () => { vi.useFakeTimers(); hoisted.skipResponsesForOps.add('warmup'); hoisted.skipResponsesForOps.add('listTeams'); const { TeamFsWorkerClient } = await import( '../../../../src/main/services/team/TeamFsWorkerClient' ); const client = new TeamFsWorkerClient(); const prewarmResult = client.prewarm().catch((error: unknown) => error); await vi.advanceTimersByTimeAsync(20_001); const prewarmError = await prewarmResult; expect(prewarmError).toBeInstanceOf(Error); expect((prewarmError as Error).message).toContain('Worker call timeout'); expect(hoisted.workers).toHaveLength(1); const listPromise = client.listTeams({ largeConfigBytes: 8 * 1024, configHeadBytes: 4 * 1024, maxConfigBytes: 256 * 1024, maxMembersMetaBytes: 256 * 1024, maxSessionHistoryInSummary: 10, maxProjectPathHistoryInSummary: 10, }); expect(hoisted.workers).toHaveLength(2); const staleWorker = hoisted.workers[0]; const replacementWorker = hoisted.workers[1]; const listRequest = replacementWorker.messages[0] as { id: string }; staleWorker.handlers.get('exit')?.(1); replacementWorker.handlers.get('message')?.({ id: listRequest.id, ok: true, result: [{ teamName: 'fresh-team', displayName: 'Fresh Team' }], diag: { op: 'listTeams', totalMs: 1 }, }); await expect(listPromise).resolves.toEqual({ teams: [{ teamName: 'fresh-team', displayName: 'Fresh Team' }], diag: { op: 'listTeams', totalMs: 1 }, }); }); });