Files
fusion/packages/engine/src/__tests__/plugin-startup-single-load.test.ts
gsxdsm 746d33e7e6 FN-8468: prevent duplicate plugin startup loads
Ensure plugins share one process-wide startup lifecycle.

- Coalesce concurrent host and engine plugin loads into a single onLoad invocation.
- Synchronize reload and stop operations across participating loaders.
- Add regression coverage and document single-load lifecycle behavior.

Files changed:
 .changeset/fn-8468-plugin-single-onload.md         |   7 +
 docs/PLUGIN_AUTHORING.md                           |   1 +
 .../__tests__/plugin-loader-single-load.test.ts    | 126 +++++++++++++
 packages/core/src/plugin-loader.ts                 | 194 +++++++++++++++++++--
 .../__tests__/plugin-startup-single-load.test.ts   |  59 +++++++
 5 files changed, 370 insertions(+), 17 deletions(-)

Fusion-Task-Id: FN-8468

Fusion-Task-Lineage: a211f445-d236-44de-837b-28b9e15c7c52

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-07-21 19:28:35 -07:00

60 lines
2.8 KiB
TypeScript

import { afterEach, describe, expect, it, vi } from "vitest";
import { EventEmitter } from "node:events";
import { mkdtemp, rm, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { PluginLoader } from "@fusion/core";
import { PluginRunner } from "../plugin-runner.js";
function createStore(root: string, entry: string) {
const installation = {
id: "startup-single-load", name: "Startup single load", version: "1.0.0", description: "fixture",
path: entry, enabled: true, state: "installed", settings: {}, dependencies: [],
createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(),
};
const events = new EventEmitter();
return {
pluginStore: {
getPlugin: vi.fn(async () => ({ ...installation })),
listPlugins: vi.fn(async () => [{ ...installation }]),
updatePluginState: vi.fn(async () => ({ ...installation })),
on: events.on.bind(events), off: events.off.bind(events),
},
taskStore: {
getRootDir: () => root,
preflightPluginSchema: vi.fn(() => null), runPluginSchemaInits: vi.fn(async () => undefined),
on: vi.fn(), off: vi.fn(),
},
};
}
describe("PluginRunner startup lifecycle", () => {
const roots: string[] = [];
afterEach(async () => {
await Promise.all(roots.splice(0).map((root) => rm(root, { recursive: true, force: true })));
delete (globalThis as Record<string, unknown>).__fusionEngineStartupOnLoad;
});
it("does not repeat onLoad when host loadAll and engine runner init race", async () => {
const root = await mkdtemp(join(tmpdir(), "fusion-engine-plugin-startup-"));
roots.push(root);
const entry = join(root, "plugin.mjs");
await writeFile(entry, `export default {
manifest: { id: "startup-single-load", name: "fixture", version: "1.0.0", description: "fixture" },
state: "installed",
hooks: { onLoad: async () => { globalThis.__fusionEngineStartupOnLoad = (globalThis.__fusionEngineStartupOnLoad || 0) + 1; await new Promise(resolve => setTimeout(resolve, 20)); } },
};`);
const host = createStore(root, entry);
const engine = createStore(root, entry);
const hostLoader = new PluginLoader({ pluginStore: host.pluginStore as any, taskStore: host.taskStore as any });
const engineLoader = new PluginLoader({ pluginStore: engine.pluginStore as any, taskStore: engine.taskStore as any });
const runner = new PluginRunner({ pluginLoader: engineLoader, pluginStore: engine.pluginStore as any, taskStore: engine.taskStore as any, rootDir: root });
await Promise.all([hostLoader.loadAllPlugins(), runner.init()]);
expect((globalThis as Record<string, unknown>).__fusionEngineStartupOnLoad).toBe(1);
expect(engineLoader.isPluginLoaded("startup-single-load")).toBe(true);
});
});