feat(FN-3168): add plugin routes wiring and listTasksModifiedSince contract

This merge adds plugin routes wiring to the dashboard with a new `listTasksModifiedSince` task store contract (FN-3796/FN-3798), stabilizes flaky tests for initial-open scroll behavior and favorites rollback timing, and removes the roadmap backend in favor of a reports plugin scaffold (FN-3824); cov

Fusion-Task-Id: FN-3168
This commit is contained in:
Fusion
2026-05-09 01:51:13 -07:00
committed by gsxdsm
parent 2dcfb19b35
commit cd97d08372
7 changed files with 230 additions and 1 deletions

View File

@@ -71,6 +71,8 @@ import {
fetchAgentRunTimeline,
streamChatResponse,
fetchMemoryBackendStatus,
fetchPluginDashboardViews,
fetchPluginUiSlots,
type ProjectInfo,
type ProjectHealth,
type ActivityFeedEntry,
@@ -781,6 +783,51 @@ describe("task comments api", () => {
});
});
describe("plugin dashboard view API wrappers", () => {
const originalFetch = globalThis.fetch;
afterEach(() => {
globalThis.fetch = originalFetch;
});
it("fetchPluginDashboardViews calls /api/plugins/dashboard-views", async () => {
globalThis.fetch = vi.fn().mockReturnValue(mockFetchResponse(true, [
{
pluginId: "roadmap-planner",
view: { viewId: "roadmaps", label: "Roadmaps", componentPath: "./dashboard-view" },
},
]));
const result = await fetchPluginDashboardViews("project-a");
expect(result).toHaveLength(1);
expect(globalThis.fetch).toHaveBeenCalledWith("/api/plugins/dashboard-views?projectId=project-a", {
headers: { "Content-Type": "application/json" },
});
});
it("fetchPluginUiSlots calls /api/plugins/ui-slots and keeps slot shape", async () => {
globalThis.fetch = vi.fn().mockReturnValue(mockFetchResponse(true, [
{
pluginId: "roadmap-planner",
slot: {
slotId: "task-detail-tab",
label: "Roadmap Details",
componentPath: "./task-detail.js",
},
},
]));
const result = await fetchPluginUiSlots("project-a");
expect(result).toHaveLength(1);
expect(result[0]).toHaveProperty("slot");
expect(globalThis.fetch).toHaveBeenCalledWith("/api/plugins/ui-slots?projectId=project-a", {
headers: { "Content-Type": "application/json" },
});
});
});
describe("fetchModels", () => {
const originalFetch = globalThis.fetch;

View File

@@ -18,6 +18,7 @@ describe("pluginViewRegistry", () => {
it("builds plugin IDs", () => {
expect(getPluginViewId("plugin-a", "main")).toBe("plugin:plugin-a:main");
expect(getPluginViewId("roadmap-planner", "roadmaps")).toBe("plugin:roadmap-planner:roadmaps");
});
it("parses and validates plugin IDs", () => {
@@ -68,6 +69,16 @@ describe("pluginViewRegistry", () => {
expect(await screen.findByText("proj-1")).toBeInTheDocument();
});
it("resolves roadmap-planner registry entry and avoids unavailable fallback", async () => {
const RoadmapsView = lazy(async () => ({ default: () => <div>Roadmaps Plugin View</div> }));
registerPluginView("roadmap-planner", "roadmaps", RoadmapsView);
render(<>{PluginDashboardViewHost({ viewId: "plugin:roadmap-planner:roadmaps" })}</>);
expect(await screen.findByText("Roadmaps Plugin View")).toBeInTheDocument();
expect(screen.queryByTestId("plugin-view-unavailable")).toBeNull();
});
it("renders unavailable fallback for unregistered views", () => {
render(<>{PluginDashboardViewHost({ viewId: "plugin:plugin-a:missing" })}</>);
expect(screen.getByTestId("plugin-view-unavailable")).toBeInTheDocument();

View File

@@ -101,6 +101,20 @@ function createMockPluginLoader(overrides: Partial<PluginLoader> = {}): PluginLo
getPluginUiContributions: vi.fn().mockReturnValue([]),
getPluginRuntimes: vi.fn().mockReturnValue([]),
getPluginDashboardViews: vi.fn().mockReturnValue([]),
createRouteContext: vi.fn(async (pluginId: string, overrides?: { taskStore?: TaskStore; settings?: Record<string, unknown>; resolveProjectTaskStore?: (projectId: string) => Promise<TaskStore> }) => ({
pluginId,
taskStore: overrides?.taskStore ?? createMockTaskStore(),
settings: overrides?.settings ?? {},
logger: {
info: vi.fn(),
warn: vi.fn(),
error: vi.fn(),
debug: vi.fn(),
},
emitEvent: vi.fn(),
createAiSession: await fusionCore.getCreateAiSessionFactory(),
resolveProjectTaskStore: overrides?.resolveProjectTaskStore,
})),
loadAllPlugins: vi.fn().mockResolvedValue({ loaded: 0, errors: 0 }),
stopAllPlugins: vi.fn().mockResolvedValue(undefined),
invokeHook: vi.fn().mockResolvedValue(undefined),
@@ -935,6 +949,39 @@ describe("GET /api/plugins/dashboard-views", () => {
view: { viewId: "graph", label: "Graph", componentPath: "./Graph.js" },
});
});
it("keeps dashboard-views payload separate from ui-slots payload", async () => {
(pluginLoader.getPluginDashboardViews as ReturnType<typeof vi.fn>).mockReturnValue([
{
pluginId: "roadmap-planner",
view: {
viewId: "roadmaps",
label: "Roadmaps",
componentPath: "./dashboard-view",
},
},
]);
(pluginLoader.getPluginUiSlots as ReturnType<typeof vi.fn>).mockReturnValue([
{
pluginId: "roadmap-planner",
slot: {
slotId: "task-detail-tab",
label: "Roadmap Details",
componentPath: "./task-detail.js",
},
},
]);
const viewsRes = await performGet(buildApp(), "/api/plugins/dashboard-views");
const slotsRes = await performGet(buildApp(), "/api/plugins/ui-slots");
expect(viewsRes.status).toBe(200);
expect(slotsRes.status).toBe(200);
expect(viewsRes.body[0]).toHaveProperty("view");
expect(viewsRes.body[0]).not.toHaveProperty("slot");
expect(slotsRes.body[0]).toHaveProperty("slot");
expect(slotsRes.body[0]).not.toHaveProperty("view");
});
});
describe("GET /api/plugins/ui-slots", () => {