feat(FN-3637): establish canonical roadmap plugin ID and compatibility rout

Merged commits stabilize the Fusion roadmap plugin's identity and routing surface, establishing a canonical plugin ID and compatibility routes so the roadmap plugin integrates cleanly with the dashboard's plugin system. Added new roadmap-routes and roadmap-suggestions modules in the dashboard packag

Fusion-Task-Id: FN-3637
This commit is contained in:
Fusion
2026-05-11 06:04:17 -07:00
committed by gsxdsm
parent 119ba88a3f
commit d910c0c0f3
33 changed files with 219 additions and 81 deletions

View File

@@ -1193,24 +1193,24 @@ describe("plugin-defined route dispatch", () => {
it("registers PATCH routes from plugins", () => {
const pluginRunner = {
getPluginRoutes: vi.fn().mockReturnValue([
{ pluginId: "roadmap-planner", route: { method: "PATCH", path: "/roadmaps/x", handler: vi.fn() } },
{ pluginId: "fusion-plugin-roadmap", route: { method: "PATCH", path: "/roadmaps/x", handler: vi.fn() } },
]),
};
const pluginStore = createMockPluginStore();
const router = createPluginRouter(pluginStore, createMockPluginLoader({
createRouteContext: vi.fn().mockResolvedValue({
pluginId: "roadmap-planner",
pluginId: "fusion-plugin-roadmap",
taskStore: createMockTaskStore(),
settings: {},
logger: { info: vi.fn(), warn: vi.fn(), error: vi.fn(), debug: vi.fn() },
emitEvent: vi.fn(),
}),
getPlugin: vi.fn().mockReturnValue({ manifest: { id: "roadmap-planner" } }),
getPlugin: vi.fn().mockReturnValue({ manifest: { id: "fusion-plugin-roadmap" } }),
} as any), pluginRunner as any, createMockTaskStore());
const stack = (router as any).stack as Array<{ route?: { path: string; methods: Record<string, boolean> } }>;
const patchRoute = stack.find((layer) => layer.route?.path === "/roadmap-planner/roadmaps/x");
const patchRoute = stack.find((layer) => layer.route?.path === "/fusion-plugin-roadmap/roadmaps/x");
expect(patchRoute?.route?.methods.patch).toBe(true);
});
@@ -1218,14 +1218,14 @@ describe("plugin-defined route dispatch", () => {
const routeHandler = vi.fn().mockResolvedValue({ ok: true });
const pluginRunner = {
getPluginRoutes: vi.fn().mockReturnValue([
{ pluginId: "roadmap-planner", route: { method: "POST", path: "/ctx-check", handler: routeHandler } },
{ pluginId: "fusion-plugin-roadmap", route: { method: "POST", path: "/ctx-check", handler: routeHandler } },
]),
};
const scopedPluginStore = createMockPluginStore();
const scopedTaskStore = createMockTaskStore({ getPluginStore: vi.fn().mockReturnValue(scopedPluginStore) });
mockGetOrCreateProjectStore.mockResolvedValue(scopedTaskStore);
const createRouteContext = vi.fn().mockImplementation(async (_pluginId: string, overrides: any) => ({
pluginId: "roadmap-planner",
pluginId: "fusion-plugin-roadmap",
taskStore: overrides.taskStore,
settings: overrides.settings,
logger: { info: vi.fn(), warn: vi.fn(), error: vi.fn(), debug: vi.fn() },
@@ -1235,7 +1235,7 @@ describe("plugin-defined route dispatch", () => {
}));
const pluginLoader = createMockPluginLoader({
createRouteContext,
getPlugin: vi.fn().mockReturnValue({ manifest: { id: "roadmap-planner" } }),
getPlugin: vi.fn().mockReturnValue({ manifest: { id: "fusion-plugin-roadmap" } }),
} as any);
const pluginStore = createMockPluginStore();
@@ -1243,9 +1243,9 @@ describe("plugin-defined route dispatch", () => {
app.use(express.json());
app.use("/api/plugins", createPluginRouter(pluginStore, pluginLoader, pluginRunner as any, createMockTaskStore()));
const res = await REQUEST(app, "POST", "/api/plugins/roadmap-planner/ctx-check", { projectId: "proj_123" });
const res = await REQUEST(app, "POST", "/api/plugins/fusion-plugin-roadmap/ctx-check", { projectId: "proj_123" });
expect(res.status).toBe(200);
expect(createRouteContext).toHaveBeenCalledWith("roadmap-planner", expect.objectContaining({
expect(createRouteContext).toHaveBeenCalledWith("fusion-plugin-roadmap", expect.objectContaining({
taskStore: scopedTaskStore,
resolveProjectTaskStore: projectStoreResolver.getOrCreateProjectStore,
}));

View File

@@ -1028,7 +1028,7 @@ describe("GET /api/plugins/dashboard-views", () => {
it("keeps dashboard-views payload separate from ui-slots payload", async () => {
(pluginLoader.getPluginDashboardViews as ReturnType<typeof vi.fn>).mockReturnValue([
{
pluginId: "roadmap-planner",
pluginId: "fusion-plugin-roadmap",
view: {
viewId: "roadmaps",
label: "Roadmaps",
@@ -1038,7 +1038,7 @@ describe("GET /api/plugins/dashboard-views", () => {
]);
(pluginLoader.getPluginUiSlots as ReturnType<typeof vi.fn>).mockReturnValue([
{
pluginId: "roadmap-planner",
pluginId: "fusion-plugin-roadmap",
slot: {
slotId: "task-detail-tab",
label: "Roadmap Details",
@@ -1696,7 +1696,8 @@ describe("createPluginRouter plugin-defined route responses", () => {
const res = await performGet(app, "/plugins/demo/html");
expect(res.status).toBe(200);
expect(res.headers["content-type"]).toContain("text/html");
expect(res.text).toContain("<html><body>Hello</body></html>");
const html = typeof res.text === "string" ? res.text : String(res.body ?? "");
expect(html).toContain("<html><body>Hello</body></html>");
});
it("propagates custom response headers", async () => {

View File

@@ -4,18 +4,21 @@ import { describe, it, expect } from "vitest";
import express from "express";
import { registerIntegratedRouters } from "../routes/register-integrated-routers.js";
describe("integrated roadmap routes removed", () => {
it("does not register a legacy /roadmaps mount", () => {
describe("integrated roadmap routes compatibility", () => {
it("registers a legacy /roadmaps mount that delegates to plugin routes", () => {
const router = express.Router();
registerIntegratedRouters({
router,
store: {} as never,
});
const mountedPaths = (router as unknown as { stack?: Array<{ regexp?: { source?: string } }> }).stack
?.map((layer) => layer.regexp?.source ?? "")
?? [];
const stack = (router as unknown as { stack?: Array<{ regexp?: { source?: string }; handle?: { stack?: Array<{ route?: { path?: string } }> } }> }).stack ?? [];
expect(mountedPaths.some((path) => path.includes("roadmaps"))).toBe(false);
const hasRoadmapMount = stack.some((layer) => {
if (layer.regexp?.source?.includes("roadmaps")) return true;
return layer.handle?.stack?.some((nested) => typeof nested.route?.path === "string" && nested.route.path.startsWith("/roadmaps")) ?? false;
});
expect(hasRoadmapMount).toBe(true);
});
});

View File

@@ -0,0 +1,9 @@
import { describe, it, expect } from "vitest";
import { SUGGESTION_TIMEOUT_MS } from "../roadmap-suggestions.js";
describe("roadmap suggestions compatibility exports", () => {
it("re-exports suggestion timeout from roadmap plugin", () => {
expect(typeof SUGGESTION_TIMEOUT_MS).toBe("number");
expect(SUGGESTION_TIMEOUT_MS).toBeGreaterThan(0);
});
});