152 lines
4.7 KiB
TypeScript
152 lines
4.7 KiB
TypeScript
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
|
|
const hoisted = vi.hoisted(() => {
|
|
const skipResponsesForOps = new Set<string>();
|
|
const workers: Array<{
|
|
messages: unknown[];
|
|
handlers: Map<string, (value: unknown) => void>;
|
|
postMessage: (message: unknown) => void;
|
|
on: (event: string, handler: (value: unknown) => void) => void;
|
|
terminate: ReturnType<typeof vi.fn>;
|
|
}> = [];
|
|
const createMockWorker = vi.fn().mockImplementation(() => {
|
|
const worker = {
|
|
messages: [] as unknown[],
|
|
handlers: new Map<string, (value: unknown) => 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<typeof import('node:fs')>('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 },
|
|
});
|
|
});
|
|
});
|