feat(FN-2341): merge fusion/fn-2341

This commit is contained in:
gsxdsm
2026-04-23 14:40:52 -07:00
parent c8b46bfeb2
commit 94473c864a
3 changed files with 127 additions and 3 deletions

View File

@@ -2329,6 +2329,116 @@ describe("runDashboard — lifecycle listener cleanup", () => {
});
});
describe("runDashboard — CentralCore cleanup diagnostics", () => {
function getNewSignalHandler(
signal: "SIGINT" | "SIGTERM",
baseline: Array<(...args: any[]) => unknown>,
): () => void {
const added = process.listeners(signal).find((listener) => !baseline.includes(listener as (...args: any[]) => unknown));
expect(added).toBeDefined();
return added as () => void;
}
async function configureCentralCoreCloseFailure(errorMessage: string): Promise<{
close: ReturnType<typeof vi.fn>;
mockStore: ReturnType<typeof makeMockStore>;
}> {
const { TaskStore, CentralCore } = await import("@fusion/core");
const mockStore = makeMockStore();
const close = vi.fn().mockRejectedValue(new Error(errorMessage));
(TaskStore as unknown as ReturnType<typeof vi.fn>).mockImplementation(() => mockStore);
(CentralCore as unknown as ReturnType<typeof vi.fn>).mockImplementation(() => ({
init: vi.fn().mockResolvedValue(undefined),
close,
getProjectByPath: vi.fn().mockResolvedValue({ id: "project-1" }),
listProjects: vi.fn().mockResolvedValue([{ id: "project-1", path: process.cwd() }]),
startDiscovery: vi.fn().mockResolvedValue(undefined),
stopDiscovery: vi.fn(),
listNodes: vi.fn().mockResolvedValue([]),
updateNode: vi.fn().mockResolvedValue(undefined),
}));
return { close, mockStore };
}
it("logs non-fatal diagnostics when CentralCore.close fails in dispose cleanup", async () => {
const { close } = await configureCentralCoreCloseFailure("dispose close failed");
const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
const unhandledRejectionSpy = vi.fn();
process.on("unhandledRejection", unhandledRejectionSpy);
try {
const { dispose } = await runDashboard(0, { open: false });
dispose();
await new Promise((resolve) => setImmediate(resolve));
await new Promise((resolve) => setImmediate(resolve));
expect(close).toHaveBeenCalled();
expect(warnSpy).toHaveBeenCalledWith(
expect.stringContaining("[dashboard] CentralCore.close() failed during dispose cleanup: dispose close failed"),
);
expect(unhandledRejectionSpy).not.toHaveBeenCalled();
} finally {
process.off("unhandledRejection", unhandledRejectionSpy);
warnSpy.mockRestore();
}
});
it("logs shutdown diagnostics and still closes store + exits when CentralCore.close fails", async () => {
const { close, mockStore } = await configureCentralCoreCloseFailure("normal shutdown close failed");
const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
const exitSpy = vi.spyOn(process, "exit").mockImplementation((() => undefined) as never);
const baselineSigintHandlers = process.listeners("SIGINT");
try {
await runDashboard(0, { open: false });
const sigintHandler = getNewSignalHandler("SIGINT", baselineSigintHandlers);
sigintHandler();
await new Promise((resolve) => setImmediate(resolve));
await new Promise((resolve) => setImmediate(resolve));
expect(close).toHaveBeenCalled();
expect(warnSpy).toHaveBeenCalledWith(
expect.stringContaining("[dashboard] CentralCore.close() failed during shutdown (SIGINT): normal shutdown close failed"),
);
expect(mockStore.close).toHaveBeenCalled();
expect(exitSpy).toHaveBeenCalledWith(0);
} finally {
warnSpy.mockRestore();
exitSpy.mockRestore();
}
});
it("logs dev shutdown diagnostics and still exits when mesh CentralCore.close fails", async () => {
const { close, mockStore } = await configureCentralCoreCloseFailure("dev shutdown close failed");
const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
const exitSpy = vi.spyOn(process, "exit").mockImplementation((() => undefined) as never);
const baselineSigtermHandlers = process.listeners("SIGTERM");
try {
await runDashboard(0, { open: false, dev: true });
const sigtermHandler = getNewSignalHandler("SIGTERM", baselineSigtermHandlers);
sigtermHandler();
await new Promise((resolve) => setImmediate(resolve));
await new Promise((resolve) => setImmediate(resolve));
expect(close).toHaveBeenCalled();
expect(warnSpy).toHaveBeenCalledWith(
expect.stringContaining("[dashboard] CentralCore.close() failed during dev shutdown (SIGTERM): dev shutdown close failed"),
);
expect(mockStore.close).toHaveBeenCalled();
expect(exitSpy).toHaveBeenCalledWith(0);
} finally {
warnSpy.mockRestore();
exitSpy.mockRestore();
}
});
});
// ── promptForPort tests ───────────────────────────────────────────────
import { promptForPort } from "./dashboard.js";

View File

@@ -575,6 +575,15 @@ export async function runDashboard(port: number, opts: { paused?: boolean; dev?:
);
}
async function closeCentralCoreBestEffort(core: CentralCore, context: string): Promise<void> {
try {
await core.close();
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
logSink.warn(`CentralCore.close() failed during ${context}: ${message}`, "dashboard");
}
}
function registerHandler(
target: NodeJS.EventEmitter,
event: string | symbol,
@@ -949,7 +958,7 @@ export async function runDashboard(port: number, opts: { paused?: boolean; dev?:
disposeCallbacks.push(async () => {
await engineManager.stopAll();
await centralCoreForEngine.close().catch(() => {});
await closeCentralCoreBestEffort(centralCoreForEngine, "dispose cleanup");
});
app = createServer(store, {
@@ -1025,7 +1034,7 @@ export async function runDashboard(port: number, opts: { paused?: boolean; dev?:
}
}
await centralCoreForEngine.close().catch(() => {});
await closeCentralCoreBestEffort(centralCoreForEngine, `shutdown (${signal})`);
store.close();
process.exit(0);
@@ -1216,7 +1225,7 @@ export async function runDashboard(port: number, opts: { paused?: boolean; dev?:
}
if (centralCoreForMesh) {
await centralCoreForMesh.close().catch(() => {});
await closeCentralCoreBestEffort(centralCoreForMesh, `dev shutdown (${signal})`);
}
store.close();