feat(FN-3160): move roadmap route context into fusion-plugin-roadmap plugin
The merge completes FN-3160 by making the roadmap route context plugin-owned, moving `roadmap-routes` and `roadmap-suggestions` logic from the dashboard into `fusion-plugin-roadmap` with updated plugin-loader integration. It also includes FN-3755's shared state snapshot support for mesh sync hardeni Fusion-Task-Id: FN-3160
This commit is contained in:
@@ -0,0 +1,22 @@
|
||||
import { describe, it, expect, vi } from "vitest";
|
||||
import { PluginLoader } from "../plugin-loader.js";
|
||||
|
||||
describe("PluginLoader.createRouteContext", () => {
|
||||
it("applies overrides including resolveProjectTaskStore", async () => {
|
||||
const pluginStore = {
|
||||
getPlugin: vi.fn().mockResolvedValue({ settings: { x: 1 } }),
|
||||
} as any;
|
||||
const baseStore = { getRootDir: () => "/tmp" } as any;
|
||||
const loader = new PluginLoader({ pluginStore, taskStore: baseStore });
|
||||
const resolveProjectTaskStore = vi.fn();
|
||||
const ctx = await loader.createRouteContext("roadmap-planner", {
|
||||
taskStore: baseStore,
|
||||
settings: { ok: true },
|
||||
resolveProjectTaskStore,
|
||||
});
|
||||
|
||||
expect(ctx.pluginId).toBe("roadmap-planner");
|
||||
expect(ctx.settings).toEqual({ ok: true });
|
||||
expect(ctx.resolveProjectTaskStore).toBe(resolveProjectTaskStore);
|
||||
});
|
||||
});
|
||||
@@ -114,25 +114,32 @@ export class PluginLoader extends EventEmitter<{
|
||||
// ── Context Creation ───────────────────────────────────────────────
|
||||
|
||||
private async createContext(plugin: FusionPlugin): Promise<PluginContext> {
|
||||
return this.createRouteContext(plugin.manifest.id);
|
||||
}
|
||||
|
||||
async createRouteContext(
|
||||
pluginId: string,
|
||||
overrides?: Partial<Pick<PluginContext, "taskStore" | "settings" | "resolveProjectTaskStore">>,
|
||||
): Promise<PluginContext> {
|
||||
const createAiSession = await getCreateAiSessionFactory();
|
||||
if (process.env.DEBUG?.includes("plugins")) {
|
||||
this.log.log(
|
||||
createAiSession
|
||||
? `[plugin:${plugin.manifest.id}] createAiSession available`
|
||||
: `[plugin:${plugin.manifest.id}] createAiSession unavailable`,
|
||||
? `[plugin:${pluginId}] createAiSession available`
|
||||
: `[plugin:${pluginId}] createAiSession unavailable`,
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
pluginId: plugin.manifest.id,
|
||||
taskStore: this.options.taskStore,
|
||||
settings: await this.getPluginSettings(plugin.manifest.id),
|
||||
logger: this.createLogger(plugin.manifest.id),
|
||||
pluginId,
|
||||
taskStore: overrides?.taskStore ?? this.options.taskStore,
|
||||
settings: overrides?.settings ?? await this.getPluginSettings(pluginId),
|
||||
logger: this.createLogger(pluginId),
|
||||
createAiSession,
|
||||
resolveProjectTaskStore: overrides?.resolveProjectTaskStore,
|
||||
emitEvent: (event: string, data: unknown) => {
|
||||
this.emit("plugin:error", { pluginId: plugin.manifest.id, error: new Error(`Custom event: ${event}`) });
|
||||
// Custom events are logged but not surfaced as errors
|
||||
this.log.log(`[plugin:${plugin.manifest.id}] Custom event: ${event}`, data);
|
||||
this.emit("plugin:error", { pluginId, error: new Error(`Custom event: ${event}`) });
|
||||
this.log.log(`[plugin:${pluginId}] Custom event: ${event}`, data);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -137,6 +137,8 @@ export interface PluginContext {
|
||||
emitEvent: (event: string, data: unknown) => void;
|
||||
/** Engine-injected AI session factory (undefined when engine is not loaded) */
|
||||
createAiSession?: CreateAiSessionFactory;
|
||||
/** Optional host capability to resolve a project-scoped TaskStore by projectId. */
|
||||
resolveProjectTaskStore?: (projectId: string) => Promise<TaskStore>;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user