feat: multi-project engine runtime improvements
- Update project manager and runtime interfaces for multi-project coordination - Add project engine configuration to core types and settings schema - Enhance child-process worker tests for signal handling coverage - Extend in-process runtime with per-project engine lifecycle hooks Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
This commit is contained in:
@@ -13,13 +13,15 @@ const mockState = vi.hoisted(() => ({
|
||||
runtimes: [] as any[],
|
||||
}));
|
||||
|
||||
vi.mock("../logger.js", () => ({
|
||||
runtimeLog: {
|
||||
log: vi.fn(),
|
||||
warn: vi.fn(),
|
||||
error: vi.fn(),
|
||||
},
|
||||
}));
|
||||
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 {},
|
||||
@@ -88,6 +90,21 @@ vi.mock("./in-process-runtime.js", () => {
|
||||
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>;
|
||||
@@ -348,7 +365,9 @@ describe("child-process-worker", () => {
|
||||
expect(runtime.stop).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
expect(worker.shutdown).toHaveBeenCalledTimes(1);
|
||||
await vi.waitFor(() => {
|
||||
expect(worker.shutdown).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
it("SIGINT stops runtime and shuts down IPC worker", async () => {
|
||||
@@ -363,6 +382,8 @@ describe("child-process-worker", () => {
|
||||
expect(runtime.stop).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
expect(worker.shutdown).toHaveBeenCalledTimes(1);
|
||||
await vi.waitFor(() => {
|
||||
expect(worker.shutdown).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -89,6 +89,7 @@ export class InProcessRuntime
|
||||
private routineRunner?: RoutineRunner;
|
||||
private routineScheduler?: RoutineScheduler;
|
||||
private missionExecutionLoop?: MissionExecutionLoop;
|
||||
private missionAutopilot?: MissionAutopilot;
|
||||
private triageProcessor?: TriageProcessor;
|
||||
|
||||
/**
|
||||
@@ -124,11 +125,16 @@ export class InProcessRuntime
|
||||
runtimeLog.log(`Starting InProcessRuntime for project ${this.config.projectId}`);
|
||||
|
||||
try {
|
||||
// 1. Initialize TaskStore
|
||||
// 1. Initialize TaskStore (use external if provided, otherwise create new)
|
||||
const { TaskStore, PluginStore: PluginStoreClass, PluginLoader: PluginLoaderClass } = await import("@fusion/core");
|
||||
this.taskStore = new TaskStore(this.config.workingDirectory);
|
||||
await this.taskStore.init();
|
||||
runtimeLog.log(`TaskStore initialized for project ${this.config.projectId}`);
|
||||
if (this.config.externalTaskStore) {
|
||||
this.taskStore = this.config.externalTaskStore;
|
||||
runtimeLog.log(`TaskStore provided externally for project ${this.config.projectId}`);
|
||||
} else {
|
||||
this.taskStore = new TaskStore(this.config.workingDirectory);
|
||||
await this.taskStore.init();
|
||||
runtimeLog.log(`TaskStore initialized for project ${this.config.projectId}`);
|
||||
}
|
||||
|
||||
// 2. Initialize Plugin system (PluginStore + PluginLoader + PluginRunner)
|
||||
this.pluginStore = new PluginStoreClass(this.taskStore.getFusionDir());
|
||||
@@ -164,15 +170,21 @@ export class InProcessRuntime
|
||||
);
|
||||
}
|
||||
|
||||
// 4. Initialize global semaphore from CentralCore
|
||||
const globalLimit = await this.getGlobalConcurrencyLimit();
|
||||
this.globalSemaphore = new AgentSemaphore(() => globalLimit);
|
||||
// 4. Initialize global semaphore — use shared one from ProjectManager if provided,
|
||||
// otherwise create a local one from CentralCore (single-project mode).
|
||||
if (this.config.globalSemaphore) {
|
||||
this.globalSemaphore = this.config.globalSemaphore;
|
||||
} else {
|
||||
const globalLimit = await this.getGlobalConcurrencyLimit();
|
||||
this.globalSemaphore = new AgentSemaphore(() => globalLimit);
|
||||
}
|
||||
|
||||
// 5. Initialize Scheduler
|
||||
const missionStore = this.taskStore.getMissionStore();
|
||||
const missionAutopilot = missionStore
|
||||
this.missionAutopilot = missionStore
|
||||
? new MissionAutopilot(this.taskStore, missionStore)
|
||||
: undefined;
|
||||
const missionAutopilot = this.missionAutopilot;
|
||||
|
||||
// Initialize MissionExecutionLoop for validation cycle handling
|
||||
const missionExecutionLoop = missionStore
|
||||
@@ -482,6 +494,9 @@ export class InProcessRuntime
|
||||
void this.scheduler.reconcileAllMissionFeatures();
|
||||
}
|
||||
|
||||
// 14. Start MissionAutopilot background polling
|
||||
this.missionAutopilot?.start();
|
||||
|
||||
this.setStatus("active");
|
||||
runtimeLog.log(`InProcessRuntime started for project ${this.config.projectId}`);
|
||||
} catch (error) {
|
||||
@@ -563,6 +578,12 @@ export class InProcessRuntime
|
||||
runtimeLog.log("Scheduler stopped");
|
||||
}
|
||||
|
||||
// 7. Stop mission autopilot background polling
|
||||
if (this.missionAutopilot) {
|
||||
this.missionAutopilot.stop();
|
||||
runtimeLog.log("MissionAutopilot stopped");
|
||||
}
|
||||
|
||||
// 7. Stop mission execution loop
|
||||
if (this.missionExecutionLoop) {
|
||||
this.missionExecutionLoop.stop();
|
||||
@@ -709,6 +730,22 @@ export class InProcessRuntime
|
||||
return this.triageProcessor;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the MissionAutopilot instance (if initialized).
|
||||
* Returns undefined when no MissionStore is available.
|
||||
*/
|
||||
getMissionAutopilot(): MissionAutopilot | undefined {
|
||||
return this.missionAutopilot;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the MissionExecutionLoop instance (if initialized).
|
||||
* Returns undefined when no MissionStore is available.
|
||||
*/
|
||||
getMissionExecutionLoop(): MissionExecutionLoop | undefined {
|
||||
return this.missionExecutionLoop;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute a heartbeat run for an agent.
|
||||
*
|
||||
|
||||
Reference in New Issue
Block a user