220 lines
6.4 KiB
TypeScript
220 lines
6.4 KiB
TypeScript
import { EventEmitter } from "node:events";
|
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
|
|
const mocks = vi.hoisted(() => ({
|
|
forkMock: vi.fn(),
|
|
}));
|
|
|
|
vi.mock("electron", () => ({
|
|
app: { isPackaged: false },
|
|
utilityProcess: { fork: mocks.forkMock },
|
|
}));
|
|
|
|
import { createWorkerService } from "../../src/main/worker-service";
|
|
|
|
function createWorker() {
|
|
const worker = new EventEmitter() as EventEmitter & {
|
|
stdout: EventEmitter;
|
|
stderr: EventEmitter;
|
|
postMessage: ReturnType<typeof vi.fn>;
|
|
};
|
|
worker.stdout = new EventEmitter();
|
|
worker.stderr = new EventEmitter();
|
|
worker.postMessage = vi.fn();
|
|
return worker;
|
|
}
|
|
|
|
describe("worker service shutdown", () => {
|
|
beforeEach(() => {
|
|
mocks.forkMock.mockReset();
|
|
});
|
|
|
|
afterEach(() => {
|
|
vi.clearAllTimers();
|
|
vi.useRealTimers();
|
|
});
|
|
|
|
it("keeps completed run outcomes when cleaning shutdown runs", async () => {
|
|
const worker = createWorker();
|
|
mocks.forkMock.mockReturnValueOnce(worker);
|
|
const finishRun = vi.fn();
|
|
const db = {
|
|
projection: {
|
|
snapshot: {
|
|
listSnapshot: vi.fn(() => ({
|
|
runs: [
|
|
{ id: "run-succeeded", status: "running", taskId: null, workspaceId: null },
|
|
{ id: "run-queued", status: "queued", taskId: null, workspaceId: null },
|
|
],
|
|
})),
|
|
},
|
|
},
|
|
core: {
|
|
runs: {
|
|
getRun: vi.fn((id: string) => ({
|
|
id,
|
|
status: id === "run-succeeded" ? "succeeded" : "queued",
|
|
})),
|
|
finishRun,
|
|
},
|
|
workspaces: { releaseWorkspaceLock: vi.fn() },
|
|
tasks: { releaseTaskLock: vi.fn() },
|
|
},
|
|
};
|
|
const service = createWorkerService({
|
|
db,
|
|
logger: { debug: vi.fn(), info: vi.fn(), warn: vi.fn(), error: vi.fn() },
|
|
workerPath: "/tmp/worker.js",
|
|
isQuitting: () => false,
|
|
publishDomainChanged: vi.fn(),
|
|
emitEvent: vi.fn(),
|
|
wakeAgentIfPossible: vi.fn(() => null),
|
|
dispatchRunToWorker: vi.fn(),
|
|
processRunCompletion: vi.fn(),
|
|
onRunStarted: vi.fn(),
|
|
onRunCompleted: vi.fn(),
|
|
notify: vi.fn(),
|
|
budgetService: undefined,
|
|
sessionService: undefined,
|
|
getRuntimeBridge: vi.fn(() => null),
|
|
getRuntimeMessageInjector: vi.fn(() => null),
|
|
} as never);
|
|
|
|
service.createWorker();
|
|
const shutdown = service.shutdown();
|
|
worker.emit("exit", 0);
|
|
await shutdown;
|
|
|
|
expect(worker.postMessage).toHaveBeenCalledWith({ type: "shutdown" });
|
|
expect(finishRun).toHaveBeenCalledTimes(1);
|
|
expect(finishRun).toHaveBeenCalledWith(expect.objectContaining({
|
|
runId: "run-queued",
|
|
status: "interrupted",
|
|
}));
|
|
});
|
|
|
|
it("clears the crashed worker reference before recovery runs", () => {
|
|
vi.useFakeTimers();
|
|
const worker = createWorker();
|
|
mocks.forkMock.mockReturnValueOnce(worker);
|
|
const publishDomainChanged = vi.fn();
|
|
const service = createWorkerService({
|
|
db: {
|
|
projection: {
|
|
snapshot: {
|
|
listSnapshot: vi.fn(() => ({ runs: [] })),
|
|
},
|
|
},
|
|
},
|
|
logger: { debug: vi.fn(), info: vi.fn(), warn: vi.fn(), error: vi.fn() },
|
|
workerPath: "/tmp/worker.js",
|
|
isQuitting: () => false,
|
|
publishDomainChanged,
|
|
emitEvent: vi.fn(),
|
|
wakeAgentIfPossible: vi.fn(() => null),
|
|
dispatchRunToWorker: vi.fn(),
|
|
processRunCompletion: vi.fn(),
|
|
onRunStarted: vi.fn(),
|
|
onRunCompleted: vi.fn(),
|
|
notify: vi.fn(),
|
|
budgetService: undefined,
|
|
sessionService: undefined,
|
|
getRuntimeBridge: vi.fn(() => null),
|
|
getRuntimeMessageInjector: vi.fn(() => null),
|
|
} as never);
|
|
|
|
service.createWorker();
|
|
expect(service.getWorker()).toBe(worker);
|
|
worker.emit("exit", 1);
|
|
|
|
expect(service.getWorker()).toBeNull();
|
|
expect(publishDomainChanged).toHaveBeenCalledOnce();
|
|
});
|
|
|
|
it("requeues queued crash runs after the replacement worker is created", () => {
|
|
vi.useFakeTimers();
|
|
const crashedWorker = createWorker();
|
|
const replacementWorker = createWorker();
|
|
mocks.forkMock
|
|
.mockReturnValueOnce(crashedWorker)
|
|
.mockReturnValueOnce(replacementWorker);
|
|
const queuedRun = {
|
|
id: "run-queued",
|
|
status: "queued",
|
|
taskId: "task-1",
|
|
agentId: "agent-1",
|
|
companyId: "company-1",
|
|
workspaceId: null,
|
|
connectorId: "openai-codex",
|
|
};
|
|
const dispatchRunToWorker = vi.fn();
|
|
const service = createWorkerService({
|
|
db: {
|
|
projection: {
|
|
snapshot: {
|
|
listSnapshot: vi.fn(() => ({
|
|
runs: [queuedRun],
|
|
companies: [{ id: "company-1", name: "Company", description: "Recovered company" }],
|
|
})),
|
|
},
|
|
},
|
|
core: {
|
|
runs: {
|
|
getRun: vi.fn(() => queuedRun),
|
|
},
|
|
entity: {
|
|
getTask: vi.fn(() => ({
|
|
id: "task-1",
|
|
companyId: "company-1",
|
|
workspaceId: null,
|
|
projectId: null,
|
|
requiresFilesystem: false,
|
|
})),
|
|
getAgent: vi.fn(() => ({
|
|
id: "agent-1",
|
|
companyId: "company-1",
|
|
connectorId: "openai-codex",
|
|
workspaceId: null,
|
|
})),
|
|
},
|
|
connectors: {
|
|
getConnector: vi.fn(() => ({ id: "openai-codex" })),
|
|
},
|
|
workspaces: {
|
|
releaseWorkspaceLock: vi.fn(),
|
|
getCompanyObservationWorkspace: vi.fn(() => null),
|
|
},
|
|
},
|
|
},
|
|
logger: { debug: vi.fn(), info: vi.fn(), warn: vi.fn(), error: vi.fn() },
|
|
workerPath: "/tmp/worker.js",
|
|
isQuitting: () => false,
|
|
publishDomainChanged: vi.fn(),
|
|
emitEvent: vi.fn(),
|
|
wakeAgentIfPossible: vi.fn(() => null),
|
|
dispatchRunToWorker,
|
|
processRunCompletion: vi.fn(),
|
|
onRunStarted: vi.fn(),
|
|
onRunCompleted: vi.fn(),
|
|
notify: vi.fn(),
|
|
budgetService: undefined,
|
|
sessionService: undefined,
|
|
getRuntimeBridge: vi.fn(() => null),
|
|
getRuntimeMessageInjector: vi.fn(() => null),
|
|
} as never);
|
|
|
|
service.createWorker();
|
|
crashedWorker.emit("exit", 1);
|
|
expect(dispatchRunToWorker).not.toHaveBeenCalled();
|
|
|
|
vi.advanceTimersByTime(1_000);
|
|
|
|
expect(service.getWorker()).toBe(replacementWorker);
|
|
expect(dispatchRunToWorker).toHaveBeenCalledWith(expect.objectContaining({
|
|
runId: "run-queued",
|
|
companyId: "company-1",
|
|
trigger: "recovery",
|
|
}));
|
|
});
|
|
});
|