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:
@@ -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`
|
||||||
|
|||||||
@@ -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", () => {
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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 });
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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(() => {
|
||||||
|
|||||||
@@ -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({
|
||||||
|
|||||||
Reference in New Issue
Block a user