feat(FN-3798): add listTasksModifiedSince contract to task store

Added a `listTasksModifiedSince` contract to the task store with wiring coverage in the plugin router, including a type-fix for limit narrowing. Tests cover both the store contract and the routing layer.

Fusion-Task-Id: FN-3798
This commit is contained in:
Fusion
2026-05-09 01:43:22 -07:00
committed by gsxdsm
parent a04cf1b8a6
commit 2dcfb19b35
3 changed files with 182 additions and 191 deletions

View File

@@ -1,133 +1,119 @@
// @vitest-environment node
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { describe, expect, it, vi } from "vitest";
import express from "express";
import { mkdtemp, rm } from "node:fs/promises";
import { join } from "node:path";
import { tmpdir } from "node:os";
import type { PluginLoader, PluginRunner } from "@fusion/core";
import { TaskStore } from "@fusion/core";
import { createApiRoutes } from "../routes.js";
import { createAuthMiddleware } from "../auth-middleware.js";
import { createPluginRouter } from "../plugin-routes.js";
import { get as performGet, request as performRequest } from "../test-request.js";
describe("createApiRoutes plugin route wiring", () => {
let rootDir: string;
let globalDir: string;
let store: TaskStore;
let originalDaemonToken: string | undefined;
describe("createPluginRouter wiring under /api/plugins", () => {
function buildApp() {
const enablePlugin = vi.fn(async (id: string) => ({ id, enabled: true }));
const pluginStore = {
listPlugins: vi.fn(async () => [{ id: "test-plugin", name: "Test Plugin", enabled: false }]),
getPlugin: vi.fn(async (id: string) => ({ id, settings: {}, enabled: false, manifest: { id, name: id, version: "1.0.0", description: "" } })),
enablePlugin,
disablePlugin: vi.fn(),
registerPlugin: vi.fn(),
unregisterPlugin: vi.fn(),
updatePluginSettings: vi.fn(),
updatePluginState: vi.fn(),
} as any;
const pluginId = "wire-test-plugin";
const taskStore = {
listTasks: vi.fn(async () => []),
} as any;
beforeEach(async () => {
rootDir = await mkdtemp(join(tmpdir(), "plugin-routes-wiring-"));
globalDir = join(rootDir, ".fusion-global-settings");
store = new TaskStore(rootDir, globalDir, { inMemoryDb: true });
await store.init();
originalDaemonToken = process.env.FUSION_DAEMON_TOKEN;
delete process.env.FUSION_DAEMON_TOKEN;
const pluginStore = store.getPluginStore();
await pluginStore.registerPlugin({
manifest: {
id: pluginId,
name: "Wire Test Plugin",
version: "1.0.0",
description: "Plugin route wiring test",
},
path: rootDir,
const helloHandler = vi.fn(async () => ({ ok: true }));
const collidingEnableHandler = vi.fn(async () => ({ pluginEnable: true }));
const taskStoreHandler = vi.fn(async (_req: unknown, ctx: { taskStore: { listTasks: () => Promise<unknown[]> } }) => {
await ctx.taskStore.listTasks();
return { usedTaskStore: true };
});
});
afterEach(async () => {
if (originalDaemonToken === undefined) {
delete process.env.FUSION_DAEMON_TOKEN;
} else {
process.env.FUSION_DAEMON_TOKEN = originalDaemonToken;
}
await store.close();
await rm(rootDir, { recursive: true, force: true });
});
function buildApp(options?: { token?: string }) {
const pluginLoader = {
getPlugin: vi.fn().mockReturnValue({ manifest: { id: pluginId } }),
createRouteContext: vi.fn().mockImplementation(async () => ({
pluginId,
taskStore: store,
settings: {},
getPlugin: vi.fn((id: string) => {
if (id === "test-plugin" || id === "collision-plugin") {
return { manifest: { id } };
}
return undefined;
}),
createRouteContext: vi.fn(async (_id: string, overrides: { taskStore: unknown; settings: Record<string, unknown> }) => ({
pluginId: "test-plugin",
taskStore: overrides.taskStore,
settings: overrides.settings,
logger: { info: vi.fn(), warn: vi.fn(), error: vi.fn(), debug: vi.fn() },
emitEvent: vi.fn(),
})),
} as unknown as PluginLoader;
loadPlugin: vi.fn(),
stopPlugin: vi.fn(),
} as any;
const pluginRunner = {
getPluginRoutes: vi.fn().mockReturnValue([
{
pluginId,
route: {
method: "GET",
path: "/hello",
handler: vi.fn().mockResolvedValue({ ok: true }),
},
},
getPluginRoutes: vi.fn(() => [
{ pluginId: "test-plugin", route: { method: "GET", path: "/hello", handler: helloHandler } },
{ pluginId: "test-plugin", route: { method: "GET", path: "/use-task-store", handler: taskStoreHandler } },
{ pluginId: "collision-plugin", route: { method: "POST", path: "/enable", handler: collidingEnableHandler } },
]),
} as unknown as PluginRunner;
} as any;
const app = express();
app.use(express.json());
app.use("/api/plugins", createPluginRouter(pluginStore, pluginLoader, pluginRunner, taskStore));
app.use((_req, res) => res.status(404).json({ error: "Not found" }));
if (options?.token) {
process.env.FUSION_DAEMON_TOKEN = options.token;
app.use(createAuthMiddleware(options.token));
}
app.use("/api", createApiRoutes(store, {
pluginStore: store.getPluginStore(),
pluginLoader,
pluginRunner,
}));
app.use((_req, res) => {
res.status(404).json({ error: "Not found" });
});
return app;
return {
app,
pluginStore,
taskStore,
handlers: { helloHandler, collidingEnableHandler, taskStoreHandler },
};
}
it("routes plugin-defined endpoints through createApiRoutes mount", async () => {
const app = buildApp();
const ok = await performGet(app, `/api/plugins/${pluginId}/hello`);
expect(ok.status).toBe(200);
expect(ok.body).toEqual({ ok: true });
it("resolves plugin-defined dynamic GET route", async () => {
const { app } = buildApp();
const res = await performGet(app, "/api/plugins/test-plugin/hello");
expect(res.status).toBe(200);
expect(res.body).toEqual({ ok: true });
});
it("returns 404 for unknown plugin route paths", async () => {
const app = buildApp();
const missing = await performGet(app, `/api/plugins/${pluginId}/does-not-exist`);
expect(missing.status).toBe(404);
it("keeps management routes working alongside dynamic routes", async () => {
const { app, pluginStore } = buildApp();
const list = await performGet(app, "/api/plugins/");
expect(list.status).toBe(200);
expect(pluginStore.listPlugins).toHaveBeenCalled();
const enable = await performRequest(app, "POST", "/api/plugins/test-plugin/enable");
expect(enable.status).toBe(200);
expect(pluginStore.enablePlugin).toHaveBeenCalledWith("test-plugin");
});
it("prioritizes management /:id/enable over plugin-defined /enable route collisions", async () => {
const { app, pluginStore, handlers } = buildApp();
const res = await performRequest(app, "POST", "/api/plugins/collision-plugin/enable");
expect(res.status).toBe(200);
expect(pluginStore.enablePlugin).toHaveBeenCalledWith("collision-plugin");
expect(handlers.collidingEnableHandler).not.toHaveBeenCalled();
});
it.each([
["missing token", undefined, 401],
["invalid token", "Bearer wrong-token", 401],
["valid token", "Bearer fn_valid_token_123", 200],
])("enforces bearer auth when daemon token is enabled: %s", async (_label, authHeader, expectedStatus) => {
const app = buildApp({ token: "fn_valid_token_123" });
const headers = authHeader ? { Authorization: authHeader } : undefined;
const response = await performRequest(app, "GET", `/api/plugins/${pluginId}/hello`, undefined, headers);
expect(response.status).toBe(expectedStatus);
if (expectedStatus === 200) {
expect(response.body).toEqual({ ok: true });
}
["GET", "/api/plugins/does-not-exist/anything", 404],
["GET", "/api/plugins/missing/hello", 404],
])("returns %i for unknown plugin IDs (%s %s)", async (method, path, expectedStatus) => {
const { app } = buildApp();
const res = await performRequest(app, method as "GET", path);
expect(res.status).toBe(expectedStatus);
});
it("keeps management plugin lookup route reachable", async () => {
const app = buildApp();
const res = await performGet(app, `/api/plugins/${pluginId}`);
it("plumbs default taskStore to plugin route context", async () => {
const { app, taskStore, handlers } = buildApp();
const res = await performGet(app, "/api/plugins/test-plugin/use-task-store");
expect(res.status).toBe(200);
expect(res.body).toMatchObject({ id: pluginId, name: "Wire Test Plugin" });
expect(res.body).toEqual({ usedTaskStore: true });
expect(taskStore.listTasks).toHaveBeenCalled();
expect(handlers.taskStoreHandler).toHaveBeenCalled();
});
});