fix(FUX-039): return init_error for found-but-uninitialized plugin runtime

Co-authored-by: Fusion <noreply@runfusion.ai>
This commit is contained in:
ddonaldson130
2026-07-08 12:29:14 -04:00
parent a8c018f7d4
commit c3c726cff4
3 changed files with 59 additions and 4 deletions

View File

@@ -6,7 +6,7 @@ import type { AddressInfo } from "node:net";
import { createRequire } from "node:module";
import { fileURLToPath } from "node:url";
import * as os from "node:os";
import { CentralCore, TaskStore } from "@fusion/core";
import { CentralCore, PluginLoader, TaskStore } from "@fusion/core";
import { createServer } from "@fusion/dashboard";
import { ProjectEngineManager } from "@fusion/engine";
import { ensureCwdProjectRegistered } from "./ensure-project-registered.js";
@@ -64,10 +64,25 @@ async function startDashboardRuntime(rootDir: string, paused: boolean, noAuth: b
})
: undefined;
/*
* FNXC:PluginSubsystem 2026-07-08-00:00:
* `fusion desktop` is a separate createServer(...) call site from the Electron
* app's packages/desktop/src local runtime (same "desktop" name, different
* package). It had the same gap: no PluginStore/PluginLoader passed in, so
* plugin install and Browse registry failed here too. Mirror
* packages/cli/src/commands/dashboard.ts's construction.
*/
const pluginStore = store.getPluginStore();
await pluginStore.init();
const pluginLoader = new PluginLoader({ pluginStore, taskStore: store });
const app = createServer(store, {
engine: cwdEngine,
engineManager,
centralCore,
pluginStore,
pluginLoader,
pluginRunner: pluginLoader,
/*
* FNXC:DesktopLauncher 2026-07-01-20:19:
* `fusion desktop --no-auth` is a compatibility flag for users who learned the dashboard launcher semantics. Propagate it to the embedded dashboard server explicitly so desktop routing never treats it as an unknown flag or falls back to source-workspace discovery.

View File

@@ -324,7 +324,10 @@ describe("runtime-resolution", () => {
expect(result.fallbackReason).toBe("factory_error");
});
it("should fall back to pi when createRuntimeContext returns null", async () => {
it("should fall back to pi with reason 'init_error' when createRuntimeContext returns null", async () => {
// Registration is found (getRuntimeById succeeds) but the plugin fails to
// produce a usable context -- this is an initialization failure, distinct
// from a registration that was never found in the first place.
mockPluginRunner.createRuntimeContext.mockResolvedValue(null);
const mockRuntime = createMockPluginRuntime("orphan", "Orphan Runtime");
mockPluginRunner.getRuntimeById.mockReturnValue({
@@ -337,7 +340,7 @@ describe("runtime-resolution", () => {
expect(result.runtimeId).toBe("pi");
expect(result.wasConfigured).toBe(false);
expect(result.fallbackReason).toBe("not_found");
expect(result.fallbackReason).toBe("init_error");
});
it("should report reason 'not_found' distinct from 'factory_error' across the two hint failure modes", async () => {
@@ -360,6 +363,40 @@ describe("runtime-resolution", () => {
expect(notFoundResult.fallbackReason).not.toBe(factoryErrorResult.fallbackReason);
});
it("should distinguish all three reachable FallbackReason values: not_found, init_error, factory_error", async () => {
// not_found: registration never existed for the requested runtimeId.
mockPluginRunner.getRuntimeById.mockReturnValueOnce(undefined);
const notFoundResult = await resolveRuntime(createContext("executor", "never-registered"));
expect(notFoundResult.fallbackReason).toBe("not_found");
// init_error: registration exists but the plugin fails to initialize a context.
mockPluginRunner.createRuntimeContext.mockResolvedValueOnce(null);
const initErrorRuntime = createMockPluginRuntime("uninitializable", "Uninitializable Runtime");
mockPluginRunner.getRuntimeById.mockReturnValueOnce({
pluginId: "uninitializable-plugin",
runtime: initErrorRuntime,
});
const initErrorResult = await resolveRuntime(createContext("executor", "uninitializable"));
expect(initErrorResult.fallbackReason).toBe("init_error");
// factory_error: registration exists, context initializes, but the factory itself fails.
const factoryErrorRuntime: PluginRuntimeRegistration = {
metadata: { runtimeId: "factory-broken", name: "Factory Broken Runtime" },
factory: vi.fn().mockRejectedValue(new Error("factory boom")),
};
mockPluginRunner.getRuntimeById.mockReturnValueOnce({
pluginId: "factory-broken-plugin",
runtime: factoryErrorRuntime,
});
const factoryErrorResult = await resolveRuntime(createContext("executor", "factory-broken"));
expect(factoryErrorResult.fallbackReason).toBe("factory_error");
// All three reasons must be pairwise distinct so a regression collapsing
// any two of them back together fails this assertion.
const reasons = [notFoundResult.fallbackReason, initErrorResult.fallbackReason, factoryErrorResult.fallbackReason];
expect(new Set(reasons).size).toBe(3);
});
});
});

View File

@@ -168,8 +168,11 @@ async function resolvePluginRuntime(
// Create plugin context for runtime factory
const pluginContext = await pluginRunner.createRuntimeContext(pluginId);
if (!pluginContext) {
// The registration exists (found above) but the plugin failed to produce a
// usable context, so this is an initialization failure, not a "never
// registered" miss -- must be distinguishable as "init_error".
runtimeLog.warn(`Plugin "${pluginId}" runtime factory context unavailable`);
return { ok: false, reason: "not_found" };
return { ok: false, reason: "init_error" };
}
// Instantiate the runtime via factory