feat(FN-4775): add multi-node routing test coverage for hybrid executor

Adds test coverage for multi-node routing in the hybrid executor, specifically covering routing behavior across nodes (FN-4775 Step 3).

Fusion-Task-Id: FN-4775

Fusion-Task-Lineage: d75a91c9-0c6c-421c-8bc9-11586590d6fe
This commit is contained in:
Fusion (runfusion.ai)
2026-05-16 12:33:10 -07:00
committed by gsxdsm
parent 16ede9ca27
commit 91895a8b8c
2 changed files with 175 additions and 0 deletions

View File

@@ -12,6 +12,8 @@ files:
- node_modules/@fusion/core/**/*
- node_modules/@fusion/dashboard/**/*
- node_modules/better-sqlite3/**/*
- node_modules/ms/**/*
- node_modules/lodash.escaperegexp/**/*
asarUnpack:
- node_modules/better-sqlite3/**/*

View File

@@ -0,0 +1,173 @@
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { mkdirSync, mkdtempSync, rmSync } from "node:fs";
import { join } from "node:path";
import { tmpdir } from "node:os";
import { CentralCore } from "@fusion/core";
import { HybridExecutor } from "../hybrid-executor.js";
import { shouldUseHybridExecutor } from "../hybrid-executor-gate.js";
const projectManagerState = vi.hoisted(() => ({
projectIds: [] as string[],
stopAll: vi.fn().mockResolvedValue(undefined),
}));
const nodeHealthState = vi.hoisted(() => ({
nodes: new Map<string, string>(),
}));
vi.mock("../project-manager.js", () => ({
ProjectManager: vi.fn().mockImplementation(() => ({
on: vi.fn(),
addProject: vi.fn().mockImplementation(async ({ projectId }: { projectId: string }) => {
projectManagerState.projectIds.push(projectId);
return { getStatus: () => "running" };
}),
getRuntime: vi.fn(),
listRuntimes: vi.fn().mockReturnValue([]),
getProjectIds: vi.fn().mockImplementation(() => [...projectManagerState.projectIds]),
getGlobalMetrics: vi.fn().mockResolvedValue({
totalProjects: projectManagerState.projectIds.length,
activeProjects: projectManagerState.projectIds.length,
totalTasksInProgress: 0,
totalTasksQueued: 0,
globalConcurrencyUtilization: 0,
averageTaskLatencyMs: 0,
throughputPerMinute: 0,
errorRate: 0,
}),
acquireGlobalSlot: vi.fn().mockResolvedValue(true),
releaseGlobalSlot: vi.fn().mockResolvedValue(undefined),
removeProject: vi.fn().mockResolvedValue(undefined),
stopAll: projectManagerState.stopAll,
})),
}));
vi.mock("../node-health-monitor.js", () => ({
NodeHealthMonitor: vi.fn().mockImplementation((centralCore: CentralCore) => ({
start: vi.fn().mockImplementation(async () => {
const nodes = await centralCore.listNodes();
nodeHealthState.nodes.clear();
for (const node of nodes) {
nodeHealthState.nodes.set(node.id, node.status);
}
}),
stop: vi.fn().mockResolvedValue(undefined),
getNodeHealth: vi.fn().mockImplementation((nodeId: string) => nodeHealthState.nodes.get(nodeId)),
})),
}));
describe("HybridExecutor multi-node routing", () => {
const originalEnv = process.env.FUSION_HYBRID_EXECUTOR;
let tempDir: string;
beforeEach(() => {
projectManagerState.projectIds.length = 0;
projectManagerState.stopAll.mockClear();
nodeHealthState.nodes.clear();
delete process.env.FUSION_HYBRID_EXECUTOR;
tempDir = mkdtempSync(join(tmpdir(), "fn-4775-hybrid-routing-"));
});
afterEach(() => {
if (originalEnv === undefined) delete process.env.FUSION_HYBRID_EXECUTOR;
else process.env.FUSION_HYBRID_EXECUTOR = originalEnv;
rmSync(tempDir, { recursive: true, force: true });
vi.clearAllMocks();
});
async function createCentralWithProject(projectName: string): Promise<{ central: CentralCore; projectId: string }> {
const central = new CentralCore(tempDir);
await central.init();
const projectPath = join(tempDir, projectName);
mkdirSync(projectPath, { recursive: true });
const project = await central.registerProject({ name: projectName, path: projectPath });
await central.updateProject(project.id, { status: "active" });
return { central, projectId: project.id };
}
it("enables multi-node and initializes with node visibility", async () => {
const { central, projectId } = await createCentralWithProject("proj-multi-node");
try {
const remoteNode = await central.registerNode({
name: "remote-1",
type: "remote",
url: "http://127.0.0.1:5055",
apiKey: "test-key",
});
await expect(shouldUseHybridExecutor(central)).resolves.toEqual({ enabled: true, reason: "multi-node" });
const executor = new HybridExecutor(central);
await expect(executor.initialize()).resolves.toBeUndefined();
expect(executor.getProjectIds()).toEqual([projectId]);
expect(executor.getNodeHealthMonitor()).not.toBeNull();
const monitor = executor.getNodeHealthMonitor();
expect(monitor?.getNodeHealth(remoteNode.id)).toBe("offline");
await executor.shutdown();
} finally {
await central.close();
}
});
it("enables multi-project on a single node", async () => {
const central = new CentralCore(tempDir);
await central.init();
try {
const projectPathA = join(tempDir, "proj-a");
const projectPathB = join(tempDir, "proj-b");
mkdirSync(projectPathA, { recursive: true });
mkdirSync(projectPathB, { recursive: true });
const projectA = await central.registerProject({ name: "Project A", path: projectPathA });
const projectB = await central.registerProject({ name: "Project B", path: projectPathB });
await central.updateProject(projectA.id, { status: "active" });
await central.updateProject(projectB.id, { status: "initializing" });
await expect(shouldUseHybridExecutor(central)).resolves.toEqual({ enabled: true, reason: "multi-project" });
const executor = new HybridExecutor(central);
await executor.initialize();
expect(new Set(executor.getProjectIds())).toEqual(new Set([projectA.id, projectB.id]));
await executor.shutdown();
} finally {
await central.close();
}
});
it("degrades to central-unavailable when listNodes throws", async () => {
const { central } = await createCentralWithProject("proj-central-fail");
try {
vi.spyOn(central, "listNodes").mockRejectedValueOnce(new Error("central boom"));
const decision = await shouldUseHybridExecutor(central);
expect(decision).toEqual({ enabled: false, reason: "central-unavailable" });
const startup = async () => {
if (!decision.enabled) return;
const executor = new HybridExecutor(central);
await executor.initialize();
};
await expect(startup()).resolves.toBeUndefined();
} finally {
await central.close();
}
});
it("respects env override=0 over multi-node", async () => {
process.env.FUSION_HYBRID_EXECUTOR = "0";
const { central } = await createCentralWithProject("proj-env-off");
try {
await central.registerNode({
name: "remote-override",
type: "remote",
url: "http://127.0.0.1:5056",
apiKey: "test-key",
});
await expect(shouldUseHybridExecutor(central)).resolves.toEqual({ enabled: false, reason: "env-override" });
} finally {
await central.close();
}
});
});