feat(KB-501): implement multi-project runtime architecture with HybridExecutor
- Add ProjectRuntime interface with InProcessRuntime and ChildProcessRuntime implementations - Implement HybridExecutor for multi-project task orchestration - Add IPC protocol for host-worker communication in child-process mode - Implement health monitoring with automatic restart for child processes - Add comprehensive tests for hybrid-executor and project-runtime - Update AGENTS.md with multi-project runtime documentation - Add changeset for hybrid-executor-runtime release
This commit is contained in:
344
packages/engine/src/__tests__/hybrid-executor.test.ts
Normal file
344
packages/engine/src/__tests__/hybrid-executor.test.ts
Normal file
@@ -0,0 +1,344 @@
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
||||
import type { CentralCore, RegisteredProject } from "@fusion/core";
|
||||
import { HybridExecutor } from "../hybrid-executor.js";
|
||||
import type { ProjectRuntimeConfig } from "../project-runtime.js";
|
||||
|
||||
// Mock the ProjectManager
|
||||
const mockRuntimes = new Map();
|
||||
const mockProjectIds: string[] = [];
|
||||
|
||||
vi.mock("../project-manager.js", () => ({
|
||||
ProjectManager: vi.fn().mockImplementation(() => ({
|
||||
addProject: vi.fn().mockImplementation((config: ProjectRuntimeConfig) => {
|
||||
const runtime = {
|
||||
start: vi.fn().mockResolvedValue(undefined),
|
||||
stop: vi.fn().mockResolvedValue(undefined),
|
||||
getStatus: vi.fn().mockReturnValue("active"),
|
||||
getTaskStore: vi.fn(),
|
||||
getScheduler: vi.fn(),
|
||||
getMetrics: vi.fn().mockReturnValue({
|
||||
inFlightTasks: 0,
|
||||
activeAgents: 0,
|
||||
lastActivityAt: new Date().toISOString(),
|
||||
}),
|
||||
on: vi.fn().mockReturnThis(),
|
||||
};
|
||||
mockRuntimes.set(config.projectId, runtime);
|
||||
mockProjectIds.push(config.projectId);
|
||||
return Promise.resolve(runtime);
|
||||
}),
|
||||
removeProject: vi.fn().mockImplementation((projectId: string) => {
|
||||
mockRuntimes.delete(projectId);
|
||||
const index = mockProjectIds.indexOf(projectId);
|
||||
if (index > -1) mockProjectIds.splice(index, 1);
|
||||
return Promise.resolve(undefined);
|
||||
}),
|
||||
getRuntime: vi.fn().mockImplementation((projectId: string) => {
|
||||
return mockRuntimes.get(projectId);
|
||||
}),
|
||||
listRuntimes: vi.fn().mockImplementation(() => {
|
||||
return Array.from(mockRuntimes.values());
|
||||
}),
|
||||
getProjectIds: vi.fn().mockImplementation(() => {
|
||||
return [...mockProjectIds];
|
||||
}),
|
||||
getGlobalMetrics: vi.fn().mockResolvedValue({
|
||||
totalInFlightTasks: 0,
|
||||
totalActiveAgents: 0,
|
||||
runtimeCountByStatus: {
|
||||
active: 0,
|
||||
paused: 0,
|
||||
errored: 0,
|
||||
stopped: 0,
|
||||
starting: 0,
|
||||
stopping: 0,
|
||||
},
|
||||
totalRuntimes: 0,
|
||||
}),
|
||||
acquireGlobalSlot: vi.fn().mockResolvedValue(true),
|
||||
releaseGlobalSlot: vi.fn().mockResolvedValue(undefined),
|
||||
stopAll: vi.fn().mockImplementation(() => {
|
||||
mockRuntimes.clear();
|
||||
mockProjectIds.length = 0;
|
||||
return Promise.resolve(undefined);
|
||||
}),
|
||||
on: vi.fn().mockReturnThis(),
|
||||
})),
|
||||
}));
|
||||
|
||||
describe("HybridExecutor", () => {
|
||||
let executor: HybridExecutor;
|
||||
let mockCentralCore: CentralCore;
|
||||
const mockProject: RegisteredProject = {
|
||||
id: "proj_test123",
|
||||
name: "Test Project",
|
||||
path: "/tmp/test-project",
|
||||
status: "initializing",
|
||||
isolationMode: "in-process",
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
mockCentralCore = {
|
||||
listProjects: vi.fn().mockResolvedValue([mockProject]),
|
||||
getProject: vi.fn().mockResolvedValue(mockProject),
|
||||
registerProject: vi.fn().mockResolvedValue(mockProject),
|
||||
unregisterProject: vi.fn().mockResolvedValue(undefined),
|
||||
updateProject: vi.fn().mockResolvedValue(mockProject),
|
||||
getGlobalConcurrencyState: vi.fn().mockResolvedValue({
|
||||
globalMaxConcurrent: 4,
|
||||
currentlyActive: 0,
|
||||
queuedCount: 0,
|
||||
projectsActive: {},
|
||||
}),
|
||||
updateProjectHealth: vi.fn().mockResolvedValue(undefined),
|
||||
logActivity: vi.fn().mockResolvedValue(undefined),
|
||||
acquireGlobalSlot: vi.fn().mockResolvedValue(true),
|
||||
releaseGlobalSlot: vi.fn().mockResolvedValue(undefined),
|
||||
removeAllListeners: vi.fn(),
|
||||
on: vi.fn().mockReturnThis(),
|
||||
} as unknown as CentralCore;
|
||||
|
||||
executor = new HybridExecutor(mockCentralCore);
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
try {
|
||||
await executor.shutdown();
|
||||
} catch {
|
||||
// Ignore cleanup errors
|
||||
}
|
||||
vi.clearAllMocks();
|
||||
mockRuntimes.clear();
|
||||
mockProjectIds.length = 0;
|
||||
});
|
||||
|
||||
describe("initialization", () => {
|
||||
it("should start with initialized = false", () => {
|
||||
expect(executor.isInitialized()).toBe(false);
|
||||
});
|
||||
|
||||
it("should set initialized = true after initialize()", async () => {
|
||||
await executor.initialize();
|
||||
expect(executor.isInitialized()).toBe(true);
|
||||
});
|
||||
|
||||
it("should load existing projects on initialize", async () => {
|
||||
await executor.initialize();
|
||||
expect(mockCentralCore.listProjects).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should setup CentralCore listeners on initialize", async () => {
|
||||
await executor.initialize();
|
||||
expect(mockCentralCore.on).toHaveBeenCalledWith(
|
||||
"project:registered",
|
||||
expect.any(Function)
|
||||
);
|
||||
expect(mockCentralCore.on).toHaveBeenCalledWith(
|
||||
"project:unregistered",
|
||||
expect.any(Function)
|
||||
);
|
||||
expect(mockCentralCore.on).toHaveBeenCalledWith(
|
||||
"project:updated",
|
||||
expect.any(Function)
|
||||
);
|
||||
});
|
||||
|
||||
it("should be idempotent (initialize multiple times)", async () => {
|
||||
await executor.initialize();
|
||||
const callCount = (mockCentralCore.listProjects as ReturnType<typeof vi.fn>).mock.calls.length;
|
||||
|
||||
await executor.initialize();
|
||||
expect(mockCentralCore.listProjects).toHaveBeenCalledTimes(callCount);
|
||||
});
|
||||
});
|
||||
|
||||
describe("project lifecycle", () => {
|
||||
const testConfig: ProjectRuntimeConfig = {
|
||||
projectId: "proj_test123",
|
||||
workingDirectory: "/tmp/test-project",
|
||||
isolationMode: "in-process",
|
||||
maxConcurrent: 2,
|
||||
maxWorktrees: 4,
|
||||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
await executor.initialize();
|
||||
});
|
||||
|
||||
it("should add a project runtime", async () => {
|
||||
const runtime = await executor.addProject(testConfig);
|
||||
expect(runtime).toBeDefined();
|
||||
});
|
||||
|
||||
it("should emit project:added when adding project", async () => {
|
||||
const handler = vi.fn();
|
||||
executor.on("project:added", handler);
|
||||
|
||||
await executor.addProject(testConfig);
|
||||
|
||||
expect(handler).toHaveBeenCalledWith({
|
||||
projectId: "proj_test123",
|
||||
projectName: "Test Project",
|
||||
});
|
||||
});
|
||||
|
||||
it("should remove a project runtime", async () => {
|
||||
await executor.addProject(testConfig);
|
||||
await executor.removeProject("proj_test123");
|
||||
// Should not throw
|
||||
});
|
||||
|
||||
it("should emit project:removed when removing project", async () => {
|
||||
const handler = vi.fn();
|
||||
executor.on("project:removed", handler);
|
||||
|
||||
await executor.addProject(testConfig);
|
||||
await executor.removeProject("proj_test123");
|
||||
|
||||
expect(handler).toHaveBeenCalledWith({
|
||||
projectId: "proj_test123",
|
||||
projectName: "Test Project",
|
||||
});
|
||||
});
|
||||
|
||||
it("should get a runtime by project ID", async () => {
|
||||
await executor.addProject(testConfig);
|
||||
const runtime = executor.getRuntime("proj_test123");
|
||||
expect(runtime).toBeDefined();
|
||||
});
|
||||
|
||||
it("should list all runtimes", async () => {
|
||||
await executor.addProject(testConfig);
|
||||
const runtimes = executor.listRuntimes();
|
||||
expect(Array.isArray(runtimes)).toBe(true);
|
||||
});
|
||||
|
||||
it("should get all project IDs", async () => {
|
||||
await executor.addProject(testConfig);
|
||||
const ids = executor.getProjectIds();
|
||||
expect(ids).toContain("proj_test123");
|
||||
});
|
||||
});
|
||||
|
||||
describe("global metrics", () => {
|
||||
beforeEach(async () => {
|
||||
await executor.initialize();
|
||||
});
|
||||
|
||||
it("should get global metrics", async () => {
|
||||
const metrics = await executor.getGlobalMetrics();
|
||||
|
||||
expect(metrics).toHaveProperty("totalInFlightTasks");
|
||||
expect(metrics).toHaveProperty("totalActiveAgents");
|
||||
expect(metrics).toHaveProperty("runtimeCountByStatus");
|
||||
expect(metrics).toHaveProperty("totalRuntimes");
|
||||
});
|
||||
});
|
||||
|
||||
describe("concurrency slots", () => {
|
||||
beforeEach(async () => {
|
||||
await executor.initialize();
|
||||
});
|
||||
|
||||
it("should acquire global slot", async () => {
|
||||
const acquired = await executor.acquireGlobalSlot("proj_test123");
|
||||
expect(acquired).toBe(true);
|
||||
});
|
||||
|
||||
it("should release global slot", async () => {
|
||||
await executor.releaseGlobalSlot("proj_test123");
|
||||
// Should not throw
|
||||
});
|
||||
});
|
||||
|
||||
describe("event forwarding", () => {
|
||||
beforeEach(async () => {
|
||||
await executor.initialize();
|
||||
});
|
||||
|
||||
it("should support task:created event", () => {
|
||||
const handler = vi.fn();
|
||||
executor.on("task:created", handler);
|
||||
|
||||
// The event should be listenable
|
||||
expect(handler).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should support task:moved event", () => {
|
||||
const handler = vi.fn();
|
||||
executor.on("task:moved", handler);
|
||||
expect(handler).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should support task:updated event", () => {
|
||||
const handler = vi.fn();
|
||||
executor.on("task:updated", handler);
|
||||
expect(handler).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should support error event", () => {
|
||||
const handler = vi.fn();
|
||||
executor.on("error", handler);
|
||||
expect(handler).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should support health:changed event", () => {
|
||||
const handler = vi.fn();
|
||||
executor.on("health:changed", handler);
|
||||
expect(handler).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("shutdown", () => {
|
||||
beforeEach(async () => {
|
||||
await executor.initialize();
|
||||
});
|
||||
|
||||
it("should set initialized = false after shutdown", async () => {
|
||||
await executor.shutdown();
|
||||
expect(executor.isInitialized()).toBe(false);
|
||||
});
|
||||
|
||||
it("should be safe to call shutdown multiple times", async () => {
|
||||
await executor.shutdown();
|
||||
await executor.shutdown(); // Should not throw
|
||||
expect(executor.isInitialized()).toBe(false);
|
||||
});
|
||||
|
||||
it("should remove CentralCore listeners on shutdown", async () => {
|
||||
await executor.shutdown();
|
||||
expect(mockCentralCore.removeAllListeners).toHaveBeenCalledWith(
|
||||
"project:registered"
|
||||
);
|
||||
expect(mockCentralCore.removeAllListeners).toHaveBeenCalledWith(
|
||||
"project:unregistered"
|
||||
);
|
||||
expect(mockCentralCore.removeAllListeners).toHaveBeenCalledWith(
|
||||
"project:updated"
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("updateProject", () => {
|
||||
const testConfig: ProjectRuntimeConfig = {
|
||||
projectId: "proj_test123",
|
||||
workingDirectory: "/tmp/test-project",
|
||||
isolationMode: "in-process",
|
||||
maxConcurrent: 2,
|
||||
maxWorktrees: 4,
|
||||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
await executor.initialize();
|
||||
await executor.addProject(testConfig);
|
||||
});
|
||||
|
||||
it("should throw if runtime not found", async () => {
|
||||
await expect(
|
||||
executor.updateProject("non-existent", { maxConcurrent: 4 })
|
||||
).rejects.toThrow("Runtime not found");
|
||||
});
|
||||
});
|
||||
});
|
||||
362
packages/engine/src/__tests__/project-runtime.test.ts
Normal file
362
packages/engine/src/__tests__/project-runtime.test.ts
Normal file
@@ -0,0 +1,362 @@
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
||||
import { EventEmitter } from "node:events";
|
||||
import type { Task, TaskStore, CentralCore } from "@fusion/core";
|
||||
import type { Scheduler } from "../scheduler.js";
|
||||
import type {
|
||||
ProjectRuntime,
|
||||
ProjectRuntimeConfig,
|
||||
RuntimeStatus,
|
||||
RuntimeMetrics,
|
||||
ProjectRuntimeEvents,
|
||||
} from "../project-runtime.js";
|
||||
import { InProcessRuntime } from "../runtimes/in-process-runtime.js";
|
||||
import { ChildProcessRuntime } from "../runtimes/child-process-runtime.js";
|
||||
|
||||
/**
|
||||
* Mock implementation of ProjectRuntime for interface compliance testing.
|
||||
* This verifies the interface contract without relying on the full implementation.
|
||||
*/
|
||||
class MockProjectRuntime
|
||||
extends EventEmitter<ProjectRuntimeEvents>
|
||||
implements ProjectRuntime
|
||||
{
|
||||
private _status: RuntimeStatus = "stopped";
|
||||
private _metrics: RuntimeMetrics = {
|
||||
inFlightTasks: 0,
|
||||
activeAgents: 0,
|
||||
lastActivityAt: new Date().toISOString(),
|
||||
};
|
||||
|
||||
constructor(private config: ProjectRuntimeConfig) {
|
||||
super();
|
||||
this.setMaxListeners(100);
|
||||
}
|
||||
|
||||
async start(): Promise<void> {
|
||||
this._status = "starting";
|
||||
this.emit("health-changed", { status: "starting", previous: "stopped" });
|
||||
this._status = "active";
|
||||
this.emit("health-changed", { status: "active", previous: "starting" });
|
||||
}
|
||||
|
||||
async stop(): Promise<void> {
|
||||
this._status = "stopping";
|
||||
this.emit("health-changed", { status: "stopping", previous: "active" });
|
||||
this._status = "stopped";
|
||||
this.emit("health-changed", { status: "stopped", previous: "stopping" });
|
||||
}
|
||||
|
||||
getStatus(): RuntimeStatus {
|
||||
return this._status;
|
||||
}
|
||||
|
||||
getTaskStore(): TaskStore {
|
||||
throw new Error("Mock: getTaskStore not implemented");
|
||||
}
|
||||
|
||||
getScheduler(): Scheduler {
|
||||
throw new Error("Mock: getScheduler not implemented");
|
||||
}
|
||||
|
||||
getMetrics(): RuntimeMetrics {
|
||||
return { ...this._metrics };
|
||||
}
|
||||
|
||||
simulateTaskCreated(task: Task): void {
|
||||
this.emit("task:created", task);
|
||||
}
|
||||
|
||||
simulateTaskMoved(
|
||||
task: Task,
|
||||
from: string,
|
||||
to: string
|
||||
): void {
|
||||
this.emit("task:moved", { task, from, to });
|
||||
}
|
||||
|
||||
simulateTaskUpdated(task: Task): void {
|
||||
this.emit("task:updated", task);
|
||||
}
|
||||
|
||||
simulateError(error: Error): void {
|
||||
this.emit("error", error);
|
||||
}
|
||||
}
|
||||
|
||||
describe("ProjectRuntime Interface", () => {
|
||||
const testConfig: ProjectRuntimeConfig = {
|
||||
projectId: "proj_test123",
|
||||
workingDirectory: "/tmp/test-project",
|
||||
isolationMode: "in-process",
|
||||
maxConcurrent: 2,
|
||||
maxWorktrees: 4,
|
||||
};
|
||||
|
||||
describe("interface contract", () => {
|
||||
let runtime: MockProjectRuntime;
|
||||
|
||||
beforeEach(() => {
|
||||
runtime = new MockProjectRuntime(testConfig);
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
try {
|
||||
await runtime.stop();
|
||||
} catch {
|
||||
// Ignore cleanup errors
|
||||
}
|
||||
});
|
||||
|
||||
it("should extend EventEmitter", () => {
|
||||
expect(runtime).toBeInstanceOf(EventEmitter);
|
||||
});
|
||||
|
||||
it("should have required methods", () => {
|
||||
expect(typeof runtime.start).toBe("function");
|
||||
expect(typeof runtime.stop).toBe("function");
|
||||
expect(typeof runtime.getStatus).toBe("function");
|
||||
expect(typeof runtime.getTaskStore).toBe("function");
|
||||
expect(typeof runtime.getScheduler).toBe("function");
|
||||
expect(typeof runtime.getMetrics).toBe("function");
|
||||
});
|
||||
|
||||
it("should return RuntimeStatus from getStatus()", () => {
|
||||
const status = runtime.getStatus();
|
||||
expect(["active", "paused", "errored", "stopped", "starting", "stopping"]).toContain(status);
|
||||
});
|
||||
|
||||
it("should return RuntimeMetrics from getMetrics()", () => {
|
||||
const metrics = runtime.getMetrics();
|
||||
expect(metrics).toHaveProperty("inFlightTasks");
|
||||
expect(metrics).toHaveProperty("activeAgents");
|
||||
expect(metrics).toHaveProperty("lastActivityAt");
|
||||
expect(typeof metrics.inFlightTasks).toBe("number");
|
||||
expect(typeof metrics.activeAgents).toBe("number");
|
||||
expect(typeof metrics.lastActivityAt).toBe("string");
|
||||
});
|
||||
});
|
||||
|
||||
describe("status lifecycle", () => {
|
||||
let runtime: MockProjectRuntime;
|
||||
|
||||
beforeEach(() => {
|
||||
runtime = new MockProjectRuntime(testConfig);
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
try {
|
||||
await runtime.stop();
|
||||
} catch {
|
||||
// Ignore cleanup errors
|
||||
}
|
||||
});
|
||||
|
||||
it("should start with status 'stopped'", () => {
|
||||
expect(runtime.getStatus()).toBe("stopped");
|
||||
});
|
||||
|
||||
it("should transition through starting to active", async () => {
|
||||
const transitions: Array<{ status: RuntimeStatus; previous: RuntimeStatus }> = [];
|
||||
runtime.on("health-changed", (data) => {
|
||||
transitions.push(data);
|
||||
});
|
||||
|
||||
await runtime.start();
|
||||
|
||||
expect(runtime.getStatus()).toBe("active");
|
||||
expect(transitions).toContainEqual({ status: "starting", previous: "stopped" });
|
||||
expect(transitions).toContainEqual({ status: "active", previous: "starting" });
|
||||
});
|
||||
|
||||
it("should transition through stopping to stopped", async () => {
|
||||
await runtime.start();
|
||||
|
||||
const transitions: Array<{ status: RuntimeStatus; previous: RuntimeStatus }> = [];
|
||||
runtime.on("health-changed", (data) => {
|
||||
transitions.push(data);
|
||||
});
|
||||
|
||||
await runtime.stop();
|
||||
|
||||
expect(runtime.getStatus()).toBe("stopped");
|
||||
expect(transitions).toContainEqual({ status: "stopping", previous: "active" });
|
||||
expect(transitions).toContainEqual({ status: "stopped", previous: "stopping" });
|
||||
});
|
||||
});
|
||||
|
||||
describe("event emission", () => {
|
||||
let runtime: MockProjectRuntime;
|
||||
|
||||
beforeEach(() => {
|
||||
runtime = new MockProjectRuntime(testConfig);
|
||||
});
|
||||
|
||||
it("should emit task:created events", () => {
|
||||
const handler = vi.fn();
|
||||
runtime.on("task:created", handler);
|
||||
|
||||
const mockTask = { id: "KB-001", title: "Test Task" } as Task;
|
||||
runtime.simulateTaskCreated(mockTask);
|
||||
|
||||
expect(handler).toHaveBeenCalledWith(mockTask);
|
||||
});
|
||||
|
||||
it("should emit task:moved events", () => {
|
||||
const handler = vi.fn();
|
||||
runtime.on("task:moved", handler);
|
||||
|
||||
const mockTask = { id: "KB-001", title: "Test Task" } as Task;
|
||||
runtime.simulateTaskMoved(mockTask, "todo", "in-progress");
|
||||
|
||||
expect(handler).toHaveBeenCalledWith({
|
||||
task: mockTask,
|
||||
from: "todo",
|
||||
to: "in-progress",
|
||||
});
|
||||
});
|
||||
|
||||
it("should emit task:updated events", () => {
|
||||
const handler = vi.fn();
|
||||
runtime.on("task:updated", handler);
|
||||
|
||||
const mockTask = { id: "KB-001", title: "Updated Task" } as Task;
|
||||
runtime.simulateTaskUpdated(mockTask);
|
||||
|
||||
expect(handler).toHaveBeenCalledWith(mockTask);
|
||||
});
|
||||
|
||||
it("should emit error events", () => {
|
||||
const handler = vi.fn();
|
||||
runtime.on("error", handler);
|
||||
|
||||
const error = new Error("Test error");
|
||||
runtime.simulateError(error);
|
||||
|
||||
expect(handler).toHaveBeenCalledWith(error);
|
||||
});
|
||||
|
||||
it("should support removing event listeners with off()", () => {
|
||||
const handler = vi.fn();
|
||||
runtime.on("task:created", handler);
|
||||
runtime.off("task:created", handler);
|
||||
|
||||
const mockTask = { id: "KB-001", title: "Test Task" } as Task;
|
||||
runtime.simulateTaskCreated(mockTask);
|
||||
|
||||
expect(handler).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("real implementations", () => {
|
||||
let mockCentralCore: CentralCore;
|
||||
|
||||
beforeEach(() => {
|
||||
mockCentralCore = {
|
||||
getGlobalConcurrencyState: vi.fn().mockResolvedValue({
|
||||
globalMaxConcurrent: 4,
|
||||
currentlyActive: 0,
|
||||
queuedCount: 0,
|
||||
projectsActive: {},
|
||||
}),
|
||||
recordTaskCompletion: vi.fn().mockResolvedValue(undefined),
|
||||
} as unknown as CentralCore;
|
||||
});
|
||||
|
||||
describe("InProcessRuntime", () => {
|
||||
let runtime: InProcessRuntime;
|
||||
|
||||
beforeEach(() => {
|
||||
runtime = new InProcessRuntime(testConfig, mockCentralCore);
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
try {
|
||||
await runtime.stop();
|
||||
} catch {
|
||||
// Ignore cleanup errors
|
||||
}
|
||||
});
|
||||
|
||||
it("should implement ProjectRuntime interface", () => {
|
||||
expect(typeof runtime.start).toBe("function");
|
||||
expect(typeof runtime.stop).toBe("function");
|
||||
expect(typeof runtime.getStatus).toBe("function");
|
||||
expect(typeof runtime.getTaskStore).toBe("function");
|
||||
expect(typeof runtime.getScheduler).toBe("function");
|
||||
expect(typeof runtime.getMetrics).toBe("function");
|
||||
});
|
||||
|
||||
it("should be an EventEmitter", () => {
|
||||
expect(runtime).toBeInstanceOf(EventEmitter);
|
||||
});
|
||||
});
|
||||
|
||||
describe("ChildProcessRuntime", () => {
|
||||
let runtime: ChildProcessRuntime;
|
||||
|
||||
beforeEach(() => {
|
||||
runtime = new ChildProcessRuntime(testConfig, mockCentralCore);
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
try {
|
||||
await runtime.stop();
|
||||
} catch {
|
||||
// Ignore cleanup errors
|
||||
}
|
||||
});
|
||||
|
||||
it("should implement ProjectRuntime interface", () => {
|
||||
expect(typeof runtime.start).toBe("function");
|
||||
expect(typeof runtime.stop).toBe("function");
|
||||
expect(typeof runtime.getStatus).toBe("function");
|
||||
expect(typeof runtime.getTaskStore).toBe("function");
|
||||
expect(typeof runtime.getScheduler).toBe("function");
|
||||
expect(typeof runtime.getMetrics).toBe("function");
|
||||
});
|
||||
|
||||
it("should be an EventEmitter", () => {
|
||||
expect(runtime).toBeInstanceOf(EventEmitter);
|
||||
});
|
||||
|
||||
it("should throw for getTaskStore() (not accessible in child mode)", () => {
|
||||
expect(() => runtime.getTaskStore()).toThrow("not accessible in ChildProcessRuntime");
|
||||
});
|
||||
|
||||
it("should throw for getScheduler() (not accessible in child mode)", () => {
|
||||
expect(() => runtime.getScheduler()).toThrow("not accessible in ChildProcessRuntime");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("type guards", () => {
|
||||
it("should validate RuntimeStatus values", () => {
|
||||
const validStatuses: RuntimeStatus[] = [
|
||||
"active",
|
||||
"paused",
|
||||
"errored",
|
||||
"stopped",
|
||||
"starting",
|
||||
"stopping",
|
||||
];
|
||||
|
||||
for (const status of validStatuses) {
|
||||
expect(status).toBeDefined();
|
||||
}
|
||||
});
|
||||
|
||||
it("should validate RuntimeMetrics structure", () => {
|
||||
const metrics: RuntimeMetrics = {
|
||||
inFlightTasks: 0,
|
||||
activeAgents: 0,
|
||||
lastActivityAt: new Date().toISOString(),
|
||||
memoryBytes: 1024 * 1024,
|
||||
};
|
||||
|
||||
expect(metrics.inFlightTasks).toBe(0);
|
||||
expect(metrics.activeAgents).toBe(0);
|
||||
expect(typeof metrics.lastActivityAt).toBe("string");
|
||||
expect(typeof metrics.memoryBytes).toBe("number");
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user