chore: consolidate test files into __tests__/ dirs and clean stray engine artifacts

- Move all co-located *.test.* files into sibling __tests__/ directories so the
  layout is consistent across packages (159 renames + content-rewrite moves).
  Updates relative imports, vi.mock specifiers, and __dirname/import.meta.url
  path resolutions where tests read fixtures from disk.
- Drop tracked tsc-emit alongside engine .ts sources (auth-storage/logger/
  skill-resolver/context-limit-detector/pi.{js,d.ts,*.map}). These were
  accidentally committed in a merge and the stale pi.js was masking a real
  test-mock vs source mismatch (tests imported "../pi.js" and vite preferred
  the stale build over pi.ts).
- Add packages/engine/.gitignore to block future src/*.{js,d.ts,map}.
- Refactor plugin pi-module seams (openclaw/paperclip/hermes) to ESM-import
  createFnAgent / promptWithFallback / describeModel from @fusion/engine
  instead of require()-ing packages/engine/src/pi.js. Adds @fusion/engine to
  the two plugin package.jsons that were missing it; exports describeModel
  from the engine public API.
- Fix engine test mocks now that they run against current pi.ts: add
  ModelRegistry.create static to mocks in pi.test.ts and pi-create-fn-agent
  .test.ts; switch three boundary-result toEqual assertions to toMatchObject
  so the new content/isError fields don't trip exact-match comparison.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
gsxdsm
2026-04-24 18:45:10 -07:00
parent 5ca0df728f
commit bdcb048e20
232 changed files with 1311 additions and 26008 deletions

View File

@@ -1,715 +0,0 @@
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import type { CentralCore, Task } from "@fusion/core";
import { ChildProcessRuntime } from "./child-process-runtime.js";
import type {
ProjectRuntimeConfig,
RuntimeMetrics,
RuntimeStatus,
} from "../project-runtime.js";
import { runtimeLog } from "../logger.js";
import {
START_RUNTIME,
STOP_RUNTIME,
GET_METRICS,
TASK_CREATED,
TASK_MOVED,
TASK_UPDATED,
ERROR_EVENT,
HEALTH_CHANGED,
OK,
ERROR,
PONG,
} from "../ipc/ipc-protocol.js";
type Listener = (...args: any[]) => void;
type CommandMessage = {
type: string;
id: string;
payload: unknown;
};
type MockChildOptions = {
pingResults?: boolean[];
metricsResponse?: RuntimeMetrics;
sendCallbackErrors?: Partial<Record<string, Error>>;
markKilledOnSigterm?: boolean;
emitExitOnKill?: boolean;
};
type MockChildProcess = {
on: ReturnType<typeof vi.fn>;
send: ReturnType<typeof vi.fn>;
kill: ReturnType<typeof vi.fn>;
disconnect: ReturnType<typeof vi.fn>;
emit: (event: string, ...args: unknown[]) => void;
connected: boolean;
killed: boolean;
sentMessages: CommandMessage[];
};
const forkedChildren: MockChildProcess[] = [];
const queuedForkOptions: MockChildOptions[] = [];
function createMockChildProcess(options: MockChildOptions = {}): MockChildProcess {
const listeners = new Map<string, Listener[]>();
const pingResults = [...(options.pingResults ?? [])];
const child: MockChildProcess = {
on: vi.fn((event: string, handler: Listener) => {
const existing = listeners.get(event) ?? [];
existing.push(handler);
listeners.set(event, existing);
return child;
}),
send: vi.fn((message: CommandMessage, callback?: (error: Error | null) => void) => {
child.sentMessages.push(message);
const sendError = options.sendCallbackErrors?.[message.type];
if (sendError) {
callback?.(sendError);
return false;
}
callback?.(null);
const respond = (type: string, payload: unknown) => {
Promise.resolve().then(() => {
child.emit("message", {
type,
id: message.id,
payload,
});
});
};
if (message.type === START_RUNTIME) {
respond(OK, { data: { status: "active" } });
} else if (message.type === STOP_RUNTIME) {
respond(OK, { data: { stopped: true } });
} else if (message.type === GET_METRICS) {
respond(OK, {
data:
options.metricsResponse ??
{
inFlightTasks: 4,
activeAgents: 2,
lastActivityAt: "2026-04-08T00:00:00.000Z",
},
});
} else if (message.type === "PING") {
const pingOk = pingResults.shift() ?? true;
if (pingOk) {
respond(PONG, { timestamp: "2026-04-08T00:00:00.000Z" });
} else {
respond(ERROR, { message: "Ping failed", code: "PING_FAILED" });
}
}
return true;
}),
kill: vi.fn((signal?: string | number) => {
if (signal === "SIGKILL" || (signal === "SIGTERM" && options.markKilledOnSigterm !== false)) {
child.killed = true;
}
if (options.emitExitOnKill) {
child.emit("exit", signal === "SIGKILL" ? 137 : 0, typeof signal === "string" ? signal : null);
}
return true;
}),
disconnect: vi.fn(() => {
child.connected = false;
child.emit("disconnect");
}),
emit: (event: string, ...args: unknown[]) => {
for (const handler of listeners.get(event) ?? []) {
handler(...(args as any[]));
}
},
connected: true,
killed: false,
sentMessages: [],
};
return child;
}
const mockFork = vi.fn(() => {
const options = queuedForkOptions.shift() ?? {};
const child = createMockChildProcess(options);
forkedChildren.push(child);
return child;
});
vi.mock("node:child_process", () => ({
fork: (...args: unknown[]) => (mockFork as (...mockArgs: unknown[]) => unknown)(...args),
}));
function queueChild(options: MockChildOptions = {}): void {
queuedForkOptions.push(options);
}
function getLatestChild(): MockChildProcess {
const child = forkedChildren.at(-1);
if (!child) {
throw new Error("Expected a forked child process");
}
return child;
}
function getMessages(child: MockChildProcess, type: string): CommandMessage[] {
return child.sentMessages.filter((message) => message.type === type);
}
function createMockTask(id: string): Task {
return {
id,
title: `${id} title`,
description: `${id} description`,
column: "todo",
dependencies: [],
steps: [],
currentStep: 0,
createdAt: "2026-04-08T00:00:00.000Z",
updatedAt: "2026-04-08T00:00:00.000Z",
size: "M",
reviewLevel: 1,
log: [],
attachments: [],
} as Task;
}
describe("ChildProcessRuntime", () => {
let runtime: ChildProcessRuntime;
let runtimeAny: any;
const testConfig: ProjectRuntimeConfig = {
projectId: "proj_test123",
workingDirectory: "/tmp/test-project",
isolationMode: "child-process",
maxConcurrent: 2,
maxWorktrees: 4,
};
beforeEach(() => {
mockFork.mockClear();
forkedChildren.length = 0;
queuedForkOptions.length = 0;
const mockCentralCore = {
getGlobalConcurrencyState: vi.fn().mockResolvedValue({
globalMaxConcurrent: 4,
currentlyActive: 0,
queuedCount: 0,
projectsActive: {},
}),
} as unknown as CentralCore;
runtime = new ChildProcessRuntime(testConfig, mockCentralCore);
runtimeAny = runtime as any;
});
afterEach(async () => {
try {
await runtime.stop();
} catch {
// Ignore cleanup failures
}
vi.useRealTimers();
vi.restoreAllMocks();
});
describe("startup sequence", () => {
it("transitions stopped → starting → active, forks worker path, and sends START_RUNTIME config", async () => {
queueChild();
const transitions: RuntimeStatus[] = [];
runtime.on("health-changed", (data) => transitions.push(data.status));
await runtime.start();
const child = getLatestChild();
expect(transitions).toEqual(["starting", "active"]);
expect(runtime.getStatus()).toBe("active");
expect(mockFork).toHaveBeenCalledWith(
expect.stringMatching(/child-process-worker\.(ts|js)$/),
[],
expect.objectContaining({
silent: true,
execArgv: [],
})
);
const startMessages = getMessages(child, START_RUNTIME);
expect(startMessages).toHaveLength(1);
expect(startMessages[0]?.payload).toEqual({ config: testConfig });
});
it("sets status to errored and emits error when startup fails", async () => {
queueChild({
sendCallbackErrors: {
[START_RUNTIME]: new Error("start send failed"),
},
});
const errorSpy = vi.fn();
runtime.on("error", errorSpy);
await expect(runtime.start()).rejects.toThrow("Failed to send command: start send failed");
expect(runtime.getStatus()).toBe("errored");
expect(errorSpy).toHaveBeenCalledTimes(1);
expect(errorSpy.mock.calls[0]?.[0]).toBeInstanceOf(Error);
});
it("throws when start() is called in non-stopped states", async () => {
const blockedStates: RuntimeStatus[] = ["starting", "active", "stopping"];
for (const status of blockedStates) {
runtimeAny.status = status;
await expect(runtime.start()).rejects.toThrow(`Cannot start runtime: current status is ${status}`);
}
});
});
describe("shutdown sequence", () => {
it("transitions active → stopping → stopped and sends STOP_RUNTIME with timeout", async () => {
queueChild();
await runtime.start();
const child = getLatestChild();
const transitions: RuntimeStatus[] = [];
runtime.on("health-changed", (data) => transitions.push(data.status));
await runtime.stop();
expect(transitions).toEqual(["stopping", "stopped"]);
expect(runtime.getStatus()).toBe("stopped");
expect(getMessages(child, STOP_RUNTIME)).toHaveLength(1);
expect(getMessages(child, STOP_RUNTIME)[0]?.payload).toEqual({ timeoutMs: 30000 });
expect(child.kill).toHaveBeenCalledWith("SIGTERM");
});
it("is idempotent and does not send duplicate STOP_RUNTIME commands", async () => {
queueChild();
await runtime.start();
const child = getLatestChild();
await runtime.stop();
await runtime.stop();
expect(getMessages(child, STOP_RUNTIME)).toHaveLength(1);
expect(child.kill).toHaveBeenCalledTimes(1);
});
it("returns without error when stop() is called while already stopped", async () => {
await expect(runtime.stop()).resolves.toBeUndefined();
expect(runtime.getStatus()).toBe("stopped");
});
it("handles stop() gracefully when IPC is already disconnected", async () => {
queueChild();
runtime.on("error", () => {
// swallow asynchronous error events from disconnection path
});
await runtime.start();
const child = getLatestChild();
child.connected = false;
child.emit("disconnect");
await expect(runtime.stop()).resolves.toBeUndefined();
expect(runtime.getStatus()).toBe("stopped");
});
it("force-kills with SIGKILL after 5s timeout when child remains alive", async () => {
vi.useFakeTimers();
queueChild({ markKilledOnSigterm: false });
await runtime.start();
const child = getLatestChild();
await runtime.stop();
// Keep a live child reference so the delayed SIGKILL callback can execute the force-kill path.
runtimeAny.child = child;
child.killed = false;
await vi.advanceTimersByTimeAsync(5000);
expect(child.kill).toHaveBeenCalledWith("SIGTERM");
expect(child.kill).toHaveBeenCalledWith("SIGKILL");
});
});
describe("health monitoring and restart", () => {
it("starts health monitoring after start() and performs periodic pings", async () => {
vi.useFakeTimers();
queueChild({ pingResults: [true, true] });
await runtime.start();
const child = getLatestChild();
expect(getMessages(child, "PING")).toHaveLength(0);
await vi.advanceTimersByTimeAsync(5000);
expect(getMessages(child, "PING")).toHaveLength(1);
});
it("resets missed heartbeat count to 0 after a successful ping", async () => {
vi.useFakeTimers();
queueChild({ pingResults: [false, true] });
runtime.on("error", () => {
// swallow
});
await runtime.start();
await vi.advanceTimersByTimeAsync(5000);
expect(runtimeAny.healthMonitor.getMissedHeartbeats()).toBe(1);
await vi.advanceTimersByTimeAsync(5000);
expect(runtimeAny.healthMonitor.getMissedHeartbeats()).toBe(0);
});
it("triggers handleUnhealthy after three missed heartbeats", async () => {
vi.useFakeTimers();
queueChild({ pingResults: [false, false, false] });
const unhealthySpy = vi.spyOn(runtimeAny, "handleUnhealthy").mockImplementation(() => {});
await runtime.start();
await vi.advanceTimersByTimeAsync(15000);
expect(unhealthySpy).toHaveBeenCalledTimes(1);
});
it("uses exponential restart delays: 1000ms, 5000ms, 15000ms", () => {
vi.useFakeTimers();
runtimeAny.status = "active";
const timeoutSpy = vi.spyOn(globalThis, "setTimeout");
runtimeAny.handleUnhealthy();
runtimeAny.handleUnhealthy();
runtimeAny.handleUnhealthy();
const delays = timeoutSpy.mock.calls.map((call) => Number(call[1]));
expect(delays.slice(0, 3)).toEqual([1000, 5000, 15000]);
});
it("transitions to errored and emits error after max restart attempts", () => {
runtimeAny.status = "active";
const errorSpy = vi.fn();
runtime.on("error", errorSpy);
runtimeAny.handleUnhealthy();
runtimeAny.handleUnhealthy();
runtimeAny.handleUnhealthy();
runtimeAny.handleUnhealthy();
expect(runtime.getStatus()).toBe("errored");
expect(errorSpy).toHaveBeenCalledTimes(1);
expect(errorSpy.mock.calls[0]?.[0]).toBeInstanceOf(Error);
expect((errorSpy.mock.calls[0]?.[0] as Error).message).toContain("max restart attempts");
});
it("resets restart attempt counter after a successful health check", async () => {
vi.useFakeTimers();
queueChild({ pingResults: [true] });
await runtime.start();
runtimeAny.healthMonitor.incrementRestartAttempts();
runtimeAny.healthMonitor.incrementRestartAttempts();
expect(runtimeAny.healthMonitor.getRestartAttempts()).toBe(2);
await vi.advanceTimersByTimeAsync(5000);
expect(runtimeAny.healthMonitor.getRestartAttempts()).toBe(0);
});
it("stops health checks after stop()", async () => {
vi.useFakeTimers();
queueChild({ pingResults: [true, true, true] });
await runtime.start();
const child = getLatestChild();
await vi.advanceTimersByTimeAsync(5000);
const pingCountBeforeStop = getMessages(child, "PING").length;
await runtime.stop();
await vi.advanceTimersByTimeAsync(20000);
expect(getMessages(child, "PING").length).toBe(pingCountBeforeStop);
});
});
describe("child process exit and disconnect", () => {
it("unexpected child exit while active triggers restart handling", async () => {
queueChild();
await runtime.start();
const child = getLatestChild();
const unhealthySpy = vi.spyOn(runtimeAny, "handleUnhealthy").mockImplementation(() => {});
child.emit("exit", 1, null);
expect(unhealthySpy).toHaveBeenCalled();
});
it("child exit while stopping does not trigger restart", async () => {
queueChild();
await runtime.start();
const child = getLatestChild();
const unhealthySpy = vi.spyOn(runtimeAny, "handleUnhealthy").mockImplementation(() => {});
runtimeAny.status = "stopping";
child.emit("exit", 1, null);
expect(unhealthySpy).not.toHaveBeenCalled();
});
it("child exit while stopped does not trigger restart", async () => {
queueChild();
await runtime.start();
const child = getLatestChild();
const unhealthySpy = vi.spyOn(runtimeAny, "handleUnhealthy").mockImplementation(() => {});
runtimeAny.status = "stopped";
child.emit("exit", 1, null);
expect(unhealthySpy).not.toHaveBeenCalled();
});
it("IPC disconnect while active triggers restart handling", async () => {
queueChild();
await runtime.start();
const child = getLatestChild();
const unhealthySpy = vi.spyOn(runtimeAny, "handleUnhealthy").mockImplementation(() => {});
child.emit("disconnect");
expect(unhealthySpy).toHaveBeenCalled();
});
it("IPC disconnect while stopping does not trigger restart", async () => {
queueChild();
await runtime.start();
const child = getLatestChild();
const unhealthySpy = vi.spyOn(runtimeAny, "handleUnhealthy").mockImplementation(() => {});
runtimeAny.status = "stopping";
child.emit("disconnect");
expect(unhealthySpy).not.toHaveBeenCalled();
});
});
describe("event forwarding", () => {
it("forwards TASK_CREATED as task:created", async () => {
queueChild();
await runtime.start();
const child = getLatestChild();
const task = createMockTask("FN-1279-A");
const createdSpy = vi.fn();
runtime.on("task:created", createdSpy);
child.emit("message", {
type: TASK_CREATED,
id: "evt-created",
payload: { task },
});
expect(createdSpy).toHaveBeenCalledWith(task);
});
it("forwards TASK_MOVED as task:moved with { task, from, to } shape", async () => {
queueChild();
await runtime.start();
const child = getLatestChild();
const task = createMockTask("FN-1279-B");
const movedSpy = vi.fn();
runtime.on("task:moved", movedSpy);
child.emit("message", {
type: TASK_MOVED,
id: "evt-moved",
payload: { task, from: "todo", to: "in-progress" },
});
expect(movedSpy).toHaveBeenCalledWith({ task, from: "todo", to: "in-progress" });
});
it("forwards TASK_UPDATED as task:updated", async () => {
queueChild();
await runtime.start();
const child = getLatestChild();
const task = createMockTask("FN-1279-C");
const updatedSpy = vi.fn();
runtime.on("task:updated", updatedSpy);
child.emit("message", {
type: TASK_UPDATED,
id: "evt-updated",
payload: { task },
});
expect(updatedSpy).toHaveBeenCalledWith(task);
});
it("forwards ERROR_EVENT as Error instance and preserves error code", async () => {
queueChild();
await runtime.start();
const child = getLatestChild();
const errorSpy = vi.fn();
runtime.on("error", errorSpy);
child.emit("message", {
type: ERROR_EVENT,
id: "evt-error",
payload: { message: "worker failed", code: "WORKER_FAILURE" },
});
expect(errorSpy).toHaveBeenCalledTimes(1);
const forwardedError = errorSpy.mock.calls[0]?.[0] as Error & { code?: string };
expect(forwardedError).toBeInstanceOf(Error);
expect(forwardedError.message).toBe("worker failed");
expect(forwardedError.code).toBe("WORKER_FAILURE");
});
it("applies HEALTH_CHANGED payload to status and emits health-changed", async () => {
queueChild();
await runtime.start();
const child = getLatestChild();
const healthSpy = vi.fn();
runtime.on("health-changed", healthSpy);
healthSpy.mockClear();
child.emit("message", {
type: HEALTH_CHANGED,
id: "evt-health",
payload: { status: "paused", previous: "active" },
});
expect(runtime.getStatus()).toBe("paused");
expect(healthSpy).toHaveBeenCalledWith({ status: "paused", previous: "active" });
});
});
describe("metrics and inaccessible accessors", () => {
it("returns cached metrics when IPC is disconnected", () => {
runtimeAny.lastMetrics = {
inFlightTasks: 9,
activeAgents: 3,
lastActivityAt: "2026-04-08T01:00:00.000Z",
};
const metrics = runtime.getMetrics();
expect(metrics.inFlightTasks).toBe(9);
expect(metrics.activeAgents).toBe(3);
expect(typeof metrics.lastActivityAt).toBe("string");
});
it("updates cached metrics when GET_METRICS response is received", async () => {
queueChild({
metricsResponse: {
inFlightTasks: 12,
activeAgents: 5,
lastActivityAt: "2026-04-08T02:00:00.000Z",
},
});
await runtime.start();
runtime.getMetrics();
await vi.waitFor(() => {
expect(runtimeAny.lastMetrics).toEqual({
inFlightTasks: 12,
activeAgents: 5,
lastActivityAt: "2026-04-08T02:00:00.000Z",
});
});
});
it("ignores GET_METRICS IPC errors and returns the last known metrics", async () => {
queueChild({
sendCallbackErrors: {
[GET_METRICS]: new Error("metrics unavailable"),
},
});
await runtime.start();
runtimeAny.lastMetrics = {
inFlightTasks: 21,
activeAgents: 8,
lastActivityAt: "2026-04-08T03:00:00.000Z",
};
const metrics = runtime.getMetrics();
expect(metrics.inFlightTasks).toBe(21);
expect(metrics.activeAgents).toBe(8);
await Promise.resolve();
expect(runtimeAny.lastMetrics).toEqual({
inFlightTasks: 21,
activeAgents: 8,
lastActivityAt: "2026-04-08T03:00:00.000Z",
});
});
it("logs warning when GET_METRICS IPC query fails", async () => {
const warnSpy = vi.spyOn(runtimeLog, "warn").mockImplementation(() => {});
queueChild({
sendCallbackErrors: {
[GET_METRICS]: new Error("metrics unavailable"),
},
});
await runtime.start();
runtimeAny.lastMetrics = {
inFlightTasks: 1,
activeAgents: 0,
lastActivityAt: "2026-04-08T04:00:00.000Z",
};
const metrics = runtime.getMetrics();
expect(metrics.inFlightTasks).toBe(1);
expect(metrics.activeAgents).toBe(0);
await vi.waitFor(() => {
expect(warnSpy).toHaveBeenCalledWith(
expect.stringContaining("GET_METRICS IPC query failed, using cached value"),
);
});
expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining("metrics unavailable"));
warnSpy.mockRestore();
});
it("getTaskStore() always throws not accessible error", () => {
expect(() => runtime.getTaskStore()).toThrow("not accessible in ChildProcessRuntime");
});
it("getScheduler() always throws not accessible error", () => {
expect(() => runtime.getScheduler()).toThrow("not accessible in ChildProcessRuntime");
});
});
});

View File

@@ -1,390 +0,0 @@
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import type { RuntimeMetrics, RuntimeStatus, ProjectRuntimeConfig } from "../project-runtime.js";
import {
START_RUNTIME,
STOP_RUNTIME,
GET_STATUS,
GET_METRICS,
ERROR_EVENT,
} from "../ipc/ipc-protocol.js";
const mockState = vi.hoisted(() => ({
ipcWorkers: [] as any[],
runtimes: [] as any[],
}));
vi.mock("../logger.js", () => {
const mockLogger = { log: vi.fn(), warn: vi.fn(), error: vi.fn() };
return {
runtimeLog: mockLogger,
createLogger: () => mockLogger,
schedulerLog: mockLogger,
triageLog: mockLogger,
};
});
vi.mock("@fusion/core", () => ({
CentralCore: class MockCentralCore {},
}));
vi.mock("../ipc/ipc-worker.js", () => {
class MockIpcWorker {
handlers = new Map<string, (payload: unknown) => Promise<unknown> | unknown>();
onCommand = vi.fn((type: string, handler: (payload: unknown) => Promise<unknown> | unknown) => {
this.handlers.set(type, handler);
});
sendEvent = vi.fn();
shutdown = vi.fn();
constructor() {
mockState.ipcWorkers.push(this);
}
}
return { IpcWorker: MockIpcWorker };
});
vi.mock("./in-process-runtime.js", () => {
class MockInProcessRuntime {
status: RuntimeStatus = "stopped";
metrics: RuntimeMetrics = {
inFlightTasks: 1,
activeAgents: 1,
lastActivityAt: "2026-04-08T00:00:00.000Z",
};
listeners = new Map<string, Array<(...args: any[]) => void>>();
start = vi.fn(async () => {
this.status = "active";
});
stop = vi.fn(async () => {
this.status = "stopped";
});
getStatus = vi.fn(() => this.status);
getMetrics = vi.fn(() => this.metrics);
on = vi.fn((event: string, handler: (...args: any[]) => void) => {
const existing = this.listeners.get(event) ?? [];
existing.push(handler);
this.listeners.set(event, existing);
return this;
});
emit(event: string, ...args: any[]) {
for (const handler of this.listeners.get(event) ?? []) {
handler(...args);
}
}
constructor(
public config: ProjectRuntimeConfig,
public centralCore: unknown
) {
mockState.runtimes.push(this);
}
}
return { InProcessRuntime: MockInProcessRuntime };
});
vi.mock("../project-engine.js", async () => {
const { InProcessRuntime } = await import("./in-process-runtime.js");
class MockProjectEngine {
private runtime: any;
constructor(config: any, centralCore: any, _options?: any) {
this.runtime = new InProcessRuntime(config, centralCore);
}
start = vi.fn(async () => { await this.runtime.start(); });
stop = vi.fn(async () => { await this.runtime.stop(); });
getRuntime = vi.fn(() => this.runtime);
getTaskStore = vi.fn(() => null);
}
return { ProjectEngine: MockProjectEngine };
});
type MockWorker = {
handlers: Map<string, (payload: unknown) => Promise<unknown> | unknown>;
onCommand: ReturnType<typeof vi.fn>;
sendEvent: ReturnType<typeof vi.fn>;
shutdown: ReturnType<typeof vi.fn>;
};
type MockRuntime = {
config: ProjectRuntimeConfig;
centralCore: {
getGlobalConcurrencyState?: () => Promise<unknown>;
recordTaskCompletion?: () => Promise<void>;
};
status: RuntimeStatus;
metrics: RuntimeMetrics;
start: ReturnType<typeof vi.fn>;
stop: ReturnType<typeof vi.fn>;
getStatus: ReturnType<typeof vi.fn>;
getMetrics: ReturnType<typeof vi.fn>;
on: ReturnType<typeof vi.fn>;
emit: (event: string, ...args: unknown[]) => void;
};
const testConfig: ProjectRuntimeConfig = {
projectId: "proj_worker_test",
workingDirectory: "/tmp/test-worker",
isolationMode: "in-process",
maxConcurrent: 2,
maxWorktrees: 4,
};
async function loadWorkerModule(): Promise<MockWorker> {
await import("./child-process-worker.js");
const ipcWorker = mockState.ipcWorkers.at(-1) as MockWorker | undefined;
if (!ipcWorker) {
throw new Error("Expected child-process-worker to instantiate IpcWorker");
}
return ipcWorker;
}
function getHandler<T = unknown>(
worker: MockWorker,
type: string
): (payload: unknown) => Promise<T> {
const handler = worker.handlers.get(type);
if (!handler) {
throw new Error(`Missing handler for ${type}`);
}
return handler as (payload: unknown) => Promise<T>;
}
describe("child-process-worker", () => {
type SignalListener = (...args: unknown[]) => void;
const originalProcessSend = process.send;
let sigtermBaseline: SignalListener[] = [];
let sigintBaseline: SignalListener[] = [];
beforeEach(() => {
vi.resetModules();
vi.clearAllMocks();
mockState.ipcWorkers.length = 0;
mockState.runtimes.length = 0;
sigtermBaseline = process.listeners("SIGTERM") as unknown as SignalListener[];
sigintBaseline = process.listeners("SIGINT") as unknown as SignalListener[];
(process as NodeJS.Process & { send?: (...args: unknown[]) => unknown }).send = vi.fn(() => true);
vi.spyOn(process, "exit").mockImplementation((() => undefined) as never);
});
afterEach(() => {
for (const listener of process.listeners("SIGTERM")) {
if (!sigtermBaseline.some((l) => l === listener)) {
process.removeListener("SIGTERM", listener as unknown as SignalListener);
}
}
for (const listener of process.listeners("SIGINT")) {
if (!sigintBaseline.some((l) => l === listener)) {
process.removeListener("SIGINT", listener as unknown as SignalListener);
}
}
if (originalProcessSend) {
process.send = originalProcessSend;
} else {
delete (process as NodeJS.Process & { send?: unknown }).send;
}
vi.restoreAllMocks();
});
it("instantiates IpcWorker and registers START/STOP/GET_STATUS/GET_METRICS handlers", async () => {
const worker = await loadWorkerModule();
expect(mockState.ipcWorkers).toHaveLength(1);
expect(worker.onCommand).toHaveBeenCalledTimes(4);
expect(worker.onCommand).toHaveBeenCalledWith(START_RUNTIME, expect.any(Function));
expect(worker.onCommand).toHaveBeenCalledWith(STOP_RUNTIME, expect.any(Function));
expect(worker.onCommand).toHaveBeenCalledWith(GET_STATUS, expect.any(Function));
expect(worker.onCommand).toHaveBeenCalledWith(GET_METRICS, expect.any(Function));
expect(worker.handlers.size).toBe(4);
});
it("START_RUNTIME creates and starts InProcessRuntime, then returns status", async () => {
const worker = await loadWorkerModule();
const startHandler = getHandler<{ status: RuntimeStatus }>(worker, START_RUNTIME);
const result = await startHandler({ config: testConfig });
expect(result).toEqual({ status: "active" });
expect(mockState.runtimes).toHaveLength(1);
const runtime = mockState.runtimes[0] as MockRuntime;
expect(runtime.config).toEqual(testConfig);
expect(runtime.start).toHaveBeenCalledTimes(1);
expect(runtime.getStatus).toHaveBeenCalled();
expect(typeof runtime.centralCore.getGlobalConcurrencyState).toBe("function");
expect(typeof runtime.centralCore.recordTaskCompletion).toBe("function");
});
it("START_RUNTIME throws if runtime is already started", async () => {
const worker = await loadWorkerModule();
const startHandler = getHandler(worker, START_RUNTIME);
await startHandler({ config: testConfig });
await expect(startHandler({ config: testConfig })).rejects.toThrow("Runtime already started");
});
it("START_RUNTIME forwards runtime events via ipcWorker.sendEvent", async () => {
const worker = await loadWorkerModule();
const startHandler = getHandler(worker, START_RUNTIME);
await startHandler({ config: testConfig });
const runtime = mockState.runtimes[0] as MockRuntime;
const task = {
id: "FN-1279",
title: "task",
description: "desc",
column: "todo",
dependencies: [],
steps: [],
currentStep: 0,
createdAt: "2026-04-08T00:00:00.000Z",
updatedAt: "2026-04-08T00:00:00.000Z",
size: "M",
reviewLevel: 1,
log: [],
attachments: [],
};
runtime.emit("task:created", task);
runtime.emit("task:moved", { task, from: "todo", to: "in-progress" });
runtime.emit("task:updated", task);
const runtimeError = new Error("runtime boom") as Error & { code?: string };
runtimeError.code = "RUNTIME_ERR";
runtime.emit("error", runtimeError);
runtime.emit("health-changed", { status: "active", previous: "starting" });
expect(worker.sendEvent).toHaveBeenCalledWith("TASK_CREATED", { task });
expect(worker.sendEvent).toHaveBeenCalledWith("TASK_MOVED", {
task,
from: "todo",
to: "in-progress",
});
expect(worker.sendEvent).toHaveBeenCalledWith("TASK_UPDATED", { task });
expect(worker.sendEvent).toHaveBeenCalledWith(ERROR_EVENT, {
message: "runtime boom",
code: "RUNTIME_ERR",
});
expect(worker.sendEvent).toHaveBeenCalledWith("HEALTH_CHANGED", {
status: "active",
previous: "starting",
});
});
it("STOP_RUNTIME stops existing runtime and returns stopped true", async () => {
const worker = await loadWorkerModule();
const startHandler = getHandler(worker, START_RUNTIME);
const stopHandler = getHandler<{ stopped: boolean }>(worker, STOP_RUNTIME);
await startHandler({ config: testConfig });
const runtime = mockState.runtimes[0] as MockRuntime;
const result = await stopHandler({ timeoutMs: 12345 });
expect(result).toEqual({ stopped: true });
expect(runtime.stop).toHaveBeenCalledTimes(1);
});
it("STOP_RUNTIME throws when runtime has not been started", async () => {
const worker = await loadWorkerModule();
const stopHandler = getHandler(worker, STOP_RUNTIME);
await expect(stopHandler({ timeoutMs: 30000 })).rejects.toThrow("Runtime not started");
});
it("GET_STATUS returns stopped when runtime is null", async () => {
const worker = await loadWorkerModule();
const getStatusHandler = getHandler<{ status: RuntimeStatus }>(worker, GET_STATUS);
await expect(getStatusHandler({})).resolves.toEqual({ status: "stopped" });
});
it("GET_STATUS returns runtime status when runtime exists", async () => {
const worker = await loadWorkerModule();
const startHandler = getHandler(worker, START_RUNTIME);
const getStatusHandler = getHandler<{ status: RuntimeStatus }>(worker, GET_STATUS);
await startHandler({ config: testConfig });
const runtime = mockState.runtimes[0] as MockRuntime;
runtime.status = "paused";
await expect(getStatusHandler({})).resolves.toEqual({ status: "paused" });
});
it("GET_METRICS returns default metrics when runtime is null", async () => {
const worker = await loadWorkerModule();
const getMetricsHandler = getHandler<RuntimeMetrics>(worker, GET_METRICS);
const result = await getMetricsHandler({});
expect(result.inFlightTasks).toBe(0);
expect(result.activeAgents).toBe(0);
expect(typeof result.lastActivityAt).toBe("string");
});
it("GET_METRICS returns runtime metrics when runtime exists", async () => {
const worker = await loadWorkerModule();
const startHandler = getHandler(worker, START_RUNTIME);
const getMetricsHandler = getHandler<RuntimeMetrics>(worker, GET_METRICS);
await startHandler({ config: testConfig });
const runtime = mockState.runtimes[0] as MockRuntime;
runtime.metrics = {
inFlightTasks: 7,
activeAgents: 4,
lastActivityAt: "2026-04-08T05:00:00.000Z",
};
await expect(getMetricsHandler({})).resolves.toEqual(runtime.metrics);
expect(runtime.getMetrics).toHaveBeenCalledTimes(1);
});
it("SIGTERM stops runtime and shuts down IPC worker", async () => {
const worker = await loadWorkerModule();
const startHandler = getHandler(worker, START_RUNTIME);
await startHandler({ config: testConfig });
const runtime = mockState.runtimes[0] as MockRuntime;
process.emit("SIGTERM");
await vi.waitFor(() => {
expect(runtime.stop).toHaveBeenCalledTimes(1);
});
await vi.waitFor(() => {
expect(worker.shutdown).toHaveBeenCalledTimes(1);
});
});
it("SIGINT stops runtime and shuts down IPC worker", async () => {
const worker = await loadWorkerModule();
const startHandler = getHandler(worker, START_RUNTIME);
await startHandler({ config: testConfig });
const runtime = mockState.runtimes[0] as MockRuntime;
process.emit("SIGINT");
await vi.waitFor(() => {
expect(runtime.stop).toHaveBeenCalledTimes(1);
});
await vi.waitFor(() => {
expect(worker.shutdown).toHaveBeenCalledTimes(1);
});
});
});

File diff suppressed because it is too large Load Diff

View File

@@ -1,334 +0,0 @@
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import type { RuntimeMetrics } from "../project-runtime.js";
import { RemoteNodeClient } from "./remote-node-client.js";
const BASE_URL = "https://node.example.com";
const API_KEY = "secret-token";
describe("RemoteNodeClient", () => {
const originalFetch = globalThis.fetch;
beforeEach(() => {
vi.useRealTimers();
});
afterEach(() => {
globalThis.fetch = originalFetch;
vi.restoreAllMocks();
vi.useRealTimers();
});
it("health() parses successful response and sends auth header", async () => {
const fetchMock = vi.fn().mockResolvedValue(
new Response(JSON.stringify({ status: "ok", version: "1.0.0", uptime: 123 }), {
status: 200,
headers: { "content-type": "application/json" },
})
);
globalThis.fetch = fetchMock as unknown as typeof fetch;
const client = new RemoteNodeClient({ baseUrl: BASE_URL, apiKey: API_KEY });
const health = await client.health();
expect(health).toEqual({ status: "ok", version: "1.0.0", uptime: 123 });
expect(fetchMock).toHaveBeenCalledWith(`${BASE_URL}/api/health`, expect.objectContaining({
method: "GET",
headers: expect.objectContaining({
Authorization: `Bearer ${API_KEY}`,
}),
}));
});
it("getMetrics() parses runtime metrics", async () => {
const metrics: RuntimeMetrics = {
inFlightTasks: 4,
activeAgents: 2,
lastActivityAt: "2026-04-08T00:00:00.000Z",
};
globalThis.fetch = vi.fn().mockResolvedValue(
new Response(JSON.stringify(metrics), {
status: 200,
headers: { "content-type": "application/json" },
})
) as unknown as typeof fetch;
const client = new RemoteNodeClient({ baseUrl: BASE_URL, apiKey: API_KEY });
await expect(client.getMetrics()).resolves.toEqual(metrics);
});
it("createTask() sends POST with JSON body", async () => {
const createdTask = {
id: "KB-001",
description: "Create me",
column: "todo",
dependencies: [],
steps: [],
currentStep: 0,
status: "pending",
log: [],
attachments: [],
createdAt: "2026-04-08T00:00:00.000Z",
updatedAt: "2026-04-08T00:00:00.000Z",
size: "M",
reviewLevel: 1,
};
const fetchMock = vi.fn().mockResolvedValue(
new Response(JSON.stringify(createdTask), {
status: 200,
headers: { "content-type": "application/json" },
})
);
globalThis.fetch = fetchMock as unknown as typeof fetch;
const client = new RemoteNodeClient({ baseUrl: BASE_URL, apiKey: API_KEY });
await client.createTask({ description: "Create me" });
const options = fetchMock.mock.calls[0]?.[1] as RequestInit;
expect(fetchMock).toHaveBeenCalledWith(`${BASE_URL}/api/tasks`, expect.any(Object));
expect(options.method).toBe("POST");
expect(options.headers).toEqual(expect.objectContaining({
"Content-Type": "application/json",
Authorization: `Bearer ${API_KEY}`,
}));
expect(options.body).toBe(JSON.stringify({ description: "Create me" }));
});
it("listTasks() sends optional query params", async () => {
const fetchMock = vi.fn().mockResolvedValue(
new Response(JSON.stringify([]), {
status: 200,
headers: { "content-type": "application/json" },
})
);
globalThis.fetch = fetchMock as unknown as typeof fetch;
const client = new RemoteNodeClient({ baseUrl: BASE_URL, apiKey: API_KEY });
await client.listTasks({ column: "in-progress", limit: 10 });
expect(fetchMock).toHaveBeenCalledWith(
`${BASE_URL}/api/tasks?column=in-progress&limit=10`,
expect.objectContaining({ method: "GET" })
);
});
it("executeTask() posts to execute endpoint", async () => {
const fetchMock = vi.fn().mockResolvedValue(
new Response(JSON.stringify({ acknowledged: true }), {
status: 200,
headers: { "content-type": "application/json" },
})
);
globalThis.fetch = fetchMock as unknown as typeof fetch;
const client = new RemoteNodeClient({ baseUrl: BASE_URL, apiKey: API_KEY });
const result = await client.executeTask("KB-123");
expect(result).toEqual({ acknowledged: true });
expect(fetchMock).toHaveBeenCalledWith(
`${BASE_URL}/api/tasks/KB-123/execute`,
expect.objectContaining({ method: "POST" })
);
});
it("streamEvents() yields parsed events from SSE stream", async () => {
const sseBody = [
"event: task:created",
'data: {"type":"task:created","payload":{"id":"KB-1"},"timestamp":"2026-04-08T00:00:00.000Z"}',
"",
"event: task:updated",
'data: {"type":"task:updated","payload":{"id":"KB-1","column":"in-progress"},"timestamp":"2026-04-08T00:01:00.000Z"}',
"",
].join("\n");
globalThis.fetch = vi.fn().mockResolvedValue(
new Response(sseBody, {
status: 200,
headers: { "content-type": "text/event-stream" },
})
) as unknown as typeof fetch;
const client = new RemoteNodeClient({ baseUrl: BASE_URL, apiKey: API_KEY });
const events: unknown[] = [];
for await (const event of client.streamEvents()) {
events.push(event);
}
expect(events).toEqual([
{
type: "task:created",
payload: { id: "KB-1" },
timestamp: "2026-04-08T00:00:00.000Z",
},
{
type: "task:updated",
payload: { id: "KB-1", column: "in-progress" },
timestamp: "2026-04-08T00:01:00.000Z",
},
]);
});
it("retries on network errors", async () => {
vi.useFakeTimers();
const fetchMock = vi
.fn()
.mockRejectedValueOnce(new TypeError("network down"))
.mockResolvedValueOnce(
new Response(JSON.stringify({ status: "ok", version: "1.0.0", uptime: 123 }), {
status: 200,
headers: { "content-type": "application/json" },
})
);
globalThis.fetch = fetchMock as unknown as typeof fetch;
const client = new RemoteNodeClient({ baseUrl: BASE_URL, apiKey: API_KEY });
const request = client.health();
const expectation = expect(request).resolves.toEqual({
status: "ok",
version: "1.0.0",
uptime: 123,
});
await vi.advanceTimersByTimeAsync(1000);
await expectation;
expect(fetchMock).toHaveBeenCalledTimes(2);
});
it("does not retry on 4xx responses", async () => {
const fetchMock = vi.fn().mockResolvedValue(
new Response(JSON.stringify({ error: "unauthorized" }), {
status: 401,
statusText: "Unauthorized",
headers: { "content-type": "application/json" },
})
);
globalThis.fetch = fetchMock as unknown as typeof fetch;
const client = new RemoteNodeClient({ baseUrl: BASE_URL, apiKey: API_KEY });
await expect(client.health()).rejects.toThrow("401 Unauthorized");
expect(fetchMock).toHaveBeenCalledTimes(1);
});
it("retries on 5xx responses", async () => {
vi.useFakeTimers();
const fetchMock = vi
.fn()
.mockResolvedValueOnce(
new Response("server error", { status: 500, statusText: "Internal Server Error" })
)
.mockResolvedValueOnce(
new Response("server error", { status: 502, statusText: "Bad Gateway" })
)
.mockResolvedValueOnce(
new Response(JSON.stringify({ status: "ok", version: "1.0.0", uptime: 999 }), {
status: 200,
headers: { "content-type": "application/json" },
})
);
globalThis.fetch = fetchMock as unknown as typeof fetch;
const client = new RemoteNodeClient({ baseUrl: BASE_URL, apiKey: API_KEY });
const request = client.health();
const expectation = expect(request).resolves.toEqual({
status: "ok",
version: "1.0.0",
uptime: 999,
});
await vi.advanceTimersByTimeAsync(1000);
await vi.advanceTimersByTimeAsync(2000);
await expectation;
expect(fetchMock).toHaveBeenCalledTimes(3);
});
it("aborts requests after timeoutMs", async () => {
vi.useFakeTimers();
const fetchMock = vi.fn().mockImplementation((_: unknown, init?: RequestInit) => {
return new Promise((_resolve, reject) => {
const signal = init?.signal;
signal?.addEventListener("abort", () => {
const abortError = new Error("aborted");
abortError.name = "AbortError";
reject(abortError);
});
});
});
globalThis.fetch = fetchMock as unknown as typeof fetch;
const client = new RemoteNodeClient({
baseUrl: BASE_URL,
apiKey: API_KEY,
timeoutMs: 5,
});
const request = client.health();
const expectation = expect(request).rejects.toThrow("timed out");
await vi.runAllTimersAsync();
await expectation;
expect(fetchMock).toHaveBeenCalledTimes(4); // initial + 3 retries
});
it("sends auth header on all request methods", async () => {
const fetchMock = vi
.fn()
.mockResolvedValueOnce(
new Response(JSON.stringify({ status: "ok", version: "1.0.0", uptime: 1 }), {
status: 200,
headers: { "content-type": "application/json" },
})
)
.mockResolvedValueOnce(
new Response(JSON.stringify({ inFlightTasks: 0, activeAgents: 0, lastActivityAt: "now" }), {
status: 200,
headers: { "content-type": "application/json" },
})
)
.mockResolvedValueOnce(
new Response(JSON.stringify([]), {
status: 200,
headers: { "content-type": "application/json" },
})
)
.mockResolvedValueOnce(
new Response(JSON.stringify({ acknowledged: true }), {
status: 200,
headers: { "content-type": "application/json" },
})
)
.mockResolvedValueOnce(
new Response("event: ping\ndata: {}\n\n", {
status: 200,
headers: { "content-type": "text/event-stream" },
})
);
globalThis.fetch = fetchMock as unknown as typeof fetch;
const client = new RemoteNodeClient({ baseUrl: BASE_URL, apiKey: API_KEY });
await client.health();
await client.getMetrics();
await client.listTasks();
await client.executeTask("KB-777");
for await (const _event of client.streamEvents()) {
// Drain one-response event stream
}
for (const call of fetchMock.mock.calls) {
const options = call[1] as RequestInit;
expect(options.headers).toEqual(
expect.objectContaining({
Authorization: `Bearer ${API_KEY}`,
})
);
}
});
});

View File

@@ -1,268 +0,0 @@
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import type { NodeConfig } from "@fusion/core";
import type { RuntimeMetrics } from "../project-runtime.js";
import { RemoteNodeRuntime } from "./remote-node-runtime.js";
const mockClientConstructor = vi.hoisted(() => vi.fn());
const mockHealth = vi.hoisted(() => vi.fn());
const mockGetMetrics = vi.hoisted(() => vi.fn());
const mockStreamEvents = vi.hoisted(() => vi.fn());
vi.mock("./remote-node-client.js", () => ({
RemoteNodeClient: vi.fn().mockImplementation((options: unknown) => {
mockClientConstructor(options);
return {
health: mockHealth,
getMetrics: mockGetMetrics,
streamEvents: mockStreamEvents,
};
}),
}));
const NOW = "2026-04-08T00:00:00.000Z";
function createNode(overrides?: Partial<NodeConfig>): NodeConfig {
return {
id: "node_remote_1",
name: "Remote Node",
type: "remote",
url: "https://remote.example.com",
apiKey: "token-123",
status: "online",
maxConcurrent: 4,
createdAt: NOW,
updatedAt: NOW,
...overrides,
};
}
async function* idleStream(signal?: AbortSignal): AsyncIterable<unknown> {
while (!signal?.aborted) {
await new Promise((resolve) => setTimeout(resolve, 5));
}
// Yield to satisfy TypeScript/ESLint generator requirements
yield;
}
async function* eventStream(events: unknown[], signal?: AbortSignal): AsyncIterable<unknown> {
for (const event of events) {
yield event;
}
while (!signal?.aborted) {
await new Promise((resolve) => setTimeout(resolve, 5));
}
}
describe("RemoteNodeRuntime", () => {
beforeEach(() => {
mockClientConstructor.mockReset();
mockHealth.mockReset();
mockGetMetrics.mockReset();
mockStreamEvents.mockReset();
mockHealth.mockResolvedValue({ status: "ok", version: "1.0.0", uptime: 100 });
mockGetMetrics.mockResolvedValue({
inFlightTasks: 1,
activeAgents: 2,
lastActivityAt: NOW,
} satisfies RuntimeMetrics);
mockStreamEvents.mockImplementation(({ signal }: { signal?: AbortSignal } = {}) =>
idleStream(signal)
);
});
afterEach(async () => {
vi.clearAllMocks();
vi.useRealTimers();
});
it("start() transitions stopped -> starting -> active and starts stream", async () => {
const runtime = new RemoteNodeRuntime({
nodeConfig: createNode(),
projectId: "proj_1",
projectName: "Project 1",
});
const healthEvents: string[] = [];
runtime.on("health-changed", ({ status }) => {
healthEvents.push(status);
});
await runtime.start();
expect(runtime.getStatus()).toBe("active");
expect(healthEvents).toEqual(["starting", "active"]);
expect(mockHealth).toHaveBeenCalled();
expect(mockStreamEvents).toHaveBeenCalled();
expect(mockClientConstructor).toHaveBeenCalledWith({
baseUrl: "https://remote.example.com",
apiKey: "token-123",
});
await runtime.stop();
});
it("stop() transitions to stopped and is idempotent", async () => {
const runtime = new RemoteNodeRuntime({
nodeConfig: createNode(),
projectId: "proj_2",
projectName: "Project 2",
});
await runtime.start();
await runtime.stop();
expect(runtime.getStatus()).toBe("stopped");
await expect(runtime.stop()).resolves.toBeUndefined();
});
it("getTaskStore() throws descriptive error", () => {
const runtime = new RemoteNodeRuntime({
nodeConfig: createNode(),
projectId: "proj_3",
projectName: "Project 3",
});
expect(() => runtime.getTaskStore()).toThrow(
"TaskStore not accessible for remote node runtime"
);
});
it("getScheduler() throws descriptive error", () => {
const runtime = new RemoteNodeRuntime({
nodeConfig: createNode(),
projectId: "proj_4",
projectName: "Project 4",
});
expect(() => runtime.getScheduler()).toThrow("Scheduler not accessible for remote node runtime");
});
it("getMetrics() returns fetched metrics on success and fallback on failure", async () => {
const runtime = new RemoteNodeRuntime({
nodeConfig: createNode(),
projectId: "proj_5",
projectName: "Project 5",
});
await runtime.start();
expect(runtime.getMetrics()).toEqual({
inFlightTasks: 1,
activeAgents: 2,
lastActivityAt: NOW,
});
mockGetMetrics.mockRejectedValueOnce(new Error("metrics unavailable"));
runtime.getMetrics();
await Promise.resolve();
expect(runtime.getMetrics()).toEqual({
inFlightTasks: 0,
activeAgents: 0,
lastActivityAt: NOW,
});
await runtime.stop();
});
it("forwards remote task and error events", async () => {
const createdHandler = vi.fn();
const movedHandler = vi.fn();
const updatedHandler = vi.fn();
const errorHandler = vi.fn();
mockStreamEvents.mockImplementation(({ signal }: { signal?: AbortSignal } = {}) =>
eventStream(
[
{
type: "task:created",
payload: { id: "KB-1" },
timestamp: NOW,
},
{
type: "task:moved",
payload: { task: { id: "KB-1" }, from: "todo", to: "in-progress" },
timestamp: NOW,
},
{
type: "task:updated",
payload: { id: "KB-1", column: "done" },
timestamp: NOW,
},
{
type: "error",
payload: { message: "boom" },
timestamp: NOW,
},
],
signal
)
);
const runtime = new RemoteNodeRuntime({
nodeConfig: createNode(),
projectId: "proj_6",
projectName: "Project 6",
});
runtime.on("task:created", createdHandler);
runtime.on("task:moved", movedHandler);
runtime.on("task:updated", updatedHandler);
runtime.on("error", errorHandler);
await runtime.start();
await vi.waitFor(() => {
expect(createdHandler).toHaveBeenCalledWith({ id: "KB-1" });
expect(movedHandler).toHaveBeenCalledWith({
task: { id: "KB-1" },
from: "todo",
to: "in-progress",
});
expect(updatedHandler).toHaveBeenCalledWith({ id: "KB-1", column: "done" });
expect(errorHandler).toHaveBeenCalledWith(expect.any(Error));
});
await runtime.stop();
});
it("reconnects when stream ends unexpectedly and transitions to errored after max attempts", async () => {
mockStreamEvents.mockImplementation(async function* () {
// Immediate end to force reconnect loop.
});
const runtime = new RemoteNodeRuntime({
nodeConfig: createNode(),
projectId: "proj_7",
projectName: "Project 7",
});
(runtime as unknown as { reconnectBaseDelayMs: number }).reconnectBaseDelayMs = 1;
(runtime as unknown as { maxReconnectDelayMs: number }).maxReconnectDelayMs = 1;
(runtime as unknown as { maxReconnectAttempts: number }).maxReconnectAttempts = 3;
await runtime.start();
await vi.waitFor(() => {
expect(runtime.getStatus()).toBe("errored");
});
expect(mockStreamEvents.mock.calls.length).toBeGreaterThanOrEqual(3);
await runtime.stop();
});
it("validates remote node config on start", async () => {
const runtime = new RemoteNodeRuntime({
nodeConfig: createNode({ type: "local", url: undefined, apiKey: undefined }),
projectId: "proj_8",
projectName: "Project 8",
});
await expect(runtime.start()).rejects.toThrow("requires a remote node configuration");
});
});