From c3c726cff472b502aa4e6865874990607a428b70 Mon Sep 17 00:00:00 2001 From: ddonaldson130 Date: Wed, 8 Jul 2026 12:29:14 -0400 Subject: [PATCH] fix(FUX-039): return init_error for found-but-uninitialized plugin runtime Co-authored-by: Fusion --- packages/cli/src/commands/desktop.ts | 17 +++++++- .../src/__tests__/runtime-resolution.test.ts | 41 ++++++++++++++++++- packages/engine/src/runtime-resolution.ts | 5 ++- 3 files changed, 59 insertions(+), 4 deletions(-) diff --git a/packages/cli/src/commands/desktop.ts b/packages/cli/src/commands/desktop.ts index d3a75f9ee9..10592303bd 100644 --- a/packages/cli/src/commands/desktop.ts +++ b/packages/cli/src/commands/desktop.ts @@ -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. diff --git a/packages/engine/src/__tests__/runtime-resolution.test.ts b/packages/engine/src/__tests__/runtime-resolution.test.ts index 4bcd318026..f1592ebc05 100644 --- a/packages/engine/src/__tests__/runtime-resolution.test.ts +++ b/packages/engine/src/__tests__/runtime-resolution.test.ts @@ -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); + }); }); }); diff --git a/packages/engine/src/runtime-resolution.ts b/packages/engine/src/runtime-resolution.ts index 9f22fa3bc6..748a88a40a 100644 --- a/packages/engine/src/runtime-resolution.ts +++ b/packages/engine/src/runtime-resolution.ts @@ -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