feat(FN-3163): test suggestion route error contract in roadmap routes

Adds an assertion for the suggestion route error contract in the roadmap routes test file, completing the test coverage for that endpoint.

Fusion-Task-Id: FN-3163
This commit is contained in:
Fusion
2026-05-08 18:32:23 -07:00
committed by gsxdsm
parent a087aa4e57
commit c0d462a12d
6 changed files with 63 additions and 4 deletions

View File

@@ -456,7 +456,8 @@ const plugin: FusionPlugin = {
### Route Mounting ### Route Mounting
Routes are mounted at `/api/plugins/{pluginId}/{path}`: Routes are mounted at `/api/plugins/{pluginId}/{path}`.
Route handlers receive the same loader-built `PluginContext` used by hooks/tools, including real `taskStore`, plugin `settings`, `logger`, `emitEvent`, and engine-injected `createAiSession` (when available):
- Plugin ID: `fusion-plugin-notification` - Plugin ID: `fusion-plugin-notification`
- Route path: `/status` - Route path: `/status`

View File

@@ -1204,6 +1204,47 @@ describe("plugin-defined route dispatch", () => {
const patchRoute = stack.find((layer) => layer.route?.path === "/roadmap-planner/roadmaps/x"); const patchRoute = stack.find((layer) => layer.route?.path === "/roadmap-planner/roadmaps/x");
expect(patchRoute?.route?.methods.patch).toBe(true); expect(patchRoute?.route?.methods.patch).toBe(true);
}); });
it("passes scoped taskStore and createAiSession through pluginLoader.createRouteContext", async () => {
const routeHandler = vi.fn().mockResolvedValue({ ok: true });
const pluginRunner = {
getPluginRoutes: vi.fn().mockReturnValue([
{ pluginId: "roadmap-planner", 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",
taskStore: overrides.taskStore,
settings: overrides.settings,
logger: { info: vi.fn(), warn: vi.fn(), error: vi.fn(), debug: vi.fn() },
emitEvent: vi.fn(),
createAiSession: vi.fn(),
resolveProjectTaskStore: overrides.resolveProjectTaskStore,
}));
const pluginLoader = createMockPluginLoader({
createRouteContext,
getPlugin: vi.fn().mockReturnValue({ manifest: { id: "roadmap-planner" } }),
} as any);
const pluginStore = createMockPluginStore();
const app = express();
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" });
expect(res.status).toBe(200);
expect(createRouteContext).toHaveBeenCalledWith("roadmap-planner", expect.objectContaining({
taskStore: scopedTaskStore,
resolveProjectTaskStore: projectStoreResolver.getOrCreateProjectStore,
}));
expect(routeHandler).toHaveBeenCalledWith(
expect.anything(),
expect.objectContaining({ taskStore: scopedTaskStore, createAiSession: expect.any(Function) }),
);
});
}); });
describe("Project scoping", () => { describe("Project scoping", () => {

View File

@@ -31,4 +31,6 @@
Roadmap tables are plugin-owned and created via `hooks.onSchemaInit` in `src/index.ts`, which delegates to `src/roadmap-schema.ts`. Core database bootstrap no longer creates roadmap tables/indexes. Roadmap tables are plugin-owned and created via `hooks.onSchemaInit` in `src/index.ts`, which delegates to `src/roadmap-schema.ts`. Core database bootstrap no longer creates roadmap tables/indexes.
Roadmap AI suggestion generation is plugin-owned (`src/roadmap-suggestions.ts` / `src/roadmap-routes.ts`) and uses `PluginContext.createAiSession()` when available. The plugin must not import `@fusion/engine` directly for suggestion generation.
The plugin keeps a single canonical dashboard entrypoint (`./dashboard-view`) and accepts host-supplied dashboard context (`projectId`, optional `addToast`). Do not deep-import dashboard internals from this plugin. The plugin keeps a single canonical dashboard entrypoint (`./dashboard-view`) and accepts host-supplied dashboard context (`projectId`, optional `addToast`). Do not deep-import dashboard internals from this plugin.

View File

@@ -1,5 +1,5 @@
import { describe, it, expect, vi } from "vitest"; import { describe, it, expect, vi } from "vitest";
import { createRoadmapPluginRoutes } from "../routes/roadmap-routes.js"; import { createRoadmapPluginRoutes } from "../roadmap-routes.js";
function createCtx() { function createCtx() {
return { return {
@@ -30,4 +30,19 @@ describe("createRoadmapPluginRoutes", () => {
const result = await route!.handler({ params: { roadmapId: "RM-1" }, body: {} }, createCtx()); const result = await route!.handler({ params: { roadmapId: "RM-1" }, body: {} }, createCtx());
expect(result).toMatchObject({ status: 400 }); expect(result).toMatchObject({ status: 400 });
}); });
it("returns 404 for milestone suggestions when roadmap is missing", async () => {
const route = createRoadmapPluginRoutes().find((r) => r.path === "/roadmaps/:roadmapId/suggestions/milestones");
const ctx = createCtx();
const store = ctx.taskStore.getRoadmapStore();
ctx.taskStore.getRoadmapStore = () => ({ ...store, getRoadmap: vi.fn(() => null) });
const result = await route!.handler({ params: { roadmapId: "RM-404" }, body: { goalPrompt: "goal" } }, ctx);
expect(result).toMatchObject({ status: 404 });
});
it("returns 503 for suggestions when createAiSession is unavailable", async () => {
const route = createRoadmapPluginRoutes().find((r) => r.path === "/roadmaps/:roadmapId/suggestions/milestones");
const result = await route!.handler({ params: { roadmapId: "RM-1" }, body: { goalPrompt: "goal" } }, createCtx());
expect(result).toMatchObject({ status: 503 });
});
}); });

View File

@@ -4,7 +4,7 @@ import {
__setCreateAiSessionFactory, __setCreateAiSessionFactory,
generateMilestoneSuggestions, generateMilestoneSuggestions,
ServiceUnavailableError, ServiceUnavailableError,
} from "../routes/roadmap-suggestions.js"; } from "../roadmap-suggestions.js";
describe("roadmap suggestion service", () => { describe("roadmap suggestion service", () => {
beforeEach(() => { beforeEach(() => {

View File

@@ -1,5 +1,5 @@
import { definePlugin } from "@fusion/plugin-sdk"; import { definePlugin } from "@fusion/plugin-sdk";
import { createRoadmapPluginRoutes } from "./routes/roadmap-routes.js"; import { createRoadmapPluginRoutes } from "./roadmap-routes.js";
import { ensureRoadmapSchema } from "./roadmap-schema.js"; import { ensureRoadmapSchema } from "./roadmap-schema.js";
const plugin = definePlugin({ const plugin = definePlugin({