fix(FUX-039): return init_error for found-but-uninitialized plugin runtime
Co-authored-by: Fusion <noreply@runfusion.ai>
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user