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:
@@ -29,83 +29,87 @@ describe("TaskStore.listTasksModifiedSince", () => {
|
||||
);
|
||||
}
|
||||
|
||||
it("returns an empty array for an empty store", async () => {
|
||||
const changes = await store.listTasksModifiedSince("2026-01-01T00:00:00.000Z", 50);
|
||||
expect(changes).toEqual([]);
|
||||
it("returns empty tasks and hasMore false when nothing matches", async () => {
|
||||
const result = await store.listTasksModifiedSince("2026-01-01T00:00:00.000Z", 50);
|
||||
expect(result).toEqual({ tasks: [], hasMore: false });
|
||||
});
|
||||
|
||||
it("returns no rows when all updatedAt are <= since", async () => {
|
||||
it("returns rows in updatedAt ASC order using strict greater-than cursor", async () => {
|
||||
await createTaskWithUpdatedAt("FN-1", "2026-01-01T00:00:00.000Z");
|
||||
await createTaskWithUpdatedAt("FN-2", "2026-01-01T00:00:00.500Z");
|
||||
await createTaskWithUpdatedAt("FN-2", "2026-01-01T00:00:00.002Z");
|
||||
await createTaskWithUpdatedAt("FN-3", "2026-01-01T00:00:00.001Z");
|
||||
|
||||
const changes = await store.listTasksModifiedSince("2026-01-01T00:00:00.500Z", 50);
|
||||
expect(changes).toEqual([]);
|
||||
});
|
||||
|
||||
it("uses a strict greater-than cursor boundary", async () => {
|
||||
const since = "2026-01-01T00:00:00.000Z";
|
||||
await createTaskWithUpdatedAt("FN-1", since);
|
||||
await createTaskWithUpdatedAt("FN-2", "2026-01-01T00:00:00.001Z");
|
||||
|
||||
const changes = await store.listTasksModifiedSince(since, 50);
|
||||
expect(changes.map((task) => task.id)).toEqual(["FN-2"]);
|
||||
});
|
||||
|
||||
it("returns tasks in updatedAt ascending order", async () => {
|
||||
await createTaskWithUpdatedAt("FN-1", "2026-01-01T00:00:00.003Z");
|
||||
await createTaskWithUpdatedAt("FN-2", "2026-01-01T00:00:00.001Z");
|
||||
await createTaskWithUpdatedAt("FN-3", "2026-01-01T00:00:00.002Z");
|
||||
|
||||
const changes = await store.listTasksModifiedSince("2026-01-01T00:00:00.000Z", 50);
|
||||
expect(changes.map((task) => task.id)).toEqual(["FN-2", "FN-3", "FN-1"]);
|
||||
expect(changes.map((task) => task.updatedAt)).toEqual([
|
||||
const result = await store.listTasksModifiedSince("2026-01-01T00:00:00.000Z");
|
||||
expect(result.hasMore).toBe(false);
|
||||
expect(result.tasks.map((task) => task.id)).toEqual(["FN-3", "FN-2"]);
|
||||
expect(result.tasks.map((task) => task.updatedAt)).toEqual([
|
||||
"2026-01-01T00:00:00.001Z",
|
||||
"2026-01-01T00:00:00.002Z",
|
||||
"2026-01-01T00:00:00.003Z",
|
||||
]);
|
||||
});
|
||||
|
||||
it("applies the limit cap to earliest modified tasks", async () => {
|
||||
it("sets hasMore true when trimmed and false when exactly limit rows match", async () => {
|
||||
for (let i = 1; i <= 5; i += 1) {
|
||||
await createTaskWithUpdatedAt(`FN-${i}`, `2026-01-01T00:00:00.00${i}Z`);
|
||||
}
|
||||
|
||||
const changes = await store.listTasksModifiedSince("2026-01-01T00:00:00.000Z", 2);
|
||||
expect(changes.map((task) => task.id)).toEqual(["FN-1", "FN-2"]);
|
||||
const trimmed = await store.listTasksModifiedSince("2026-01-01T00:00:00.000Z", 2);
|
||||
expect(trimmed.tasks.map((task) => task.id)).toEqual(["FN-1", "FN-2"]);
|
||||
expect(trimmed.hasMore).toBe(true);
|
||||
|
||||
const exact = await store.listTasksModifiedSince("2026-01-01T00:00:00.000Z", 5);
|
||||
expect(exact.tasks).toHaveLength(5);
|
||||
expect(exact.hasMore).toBe(false);
|
||||
});
|
||||
|
||||
it.each([
|
||||
["limit=0", "2026-01-01T00:00:00.000Z", 0, "finite positive integer"],
|
||||
["limit=-1", "2026-01-01T00:00:00.000Z", -1, "finite positive integer"],
|
||||
["limit=201", "2026-01-01T00:00:00.000Z", 201, "less than or equal to 200"],
|
||||
["limit non-numeric", "2026-01-01T00:00:00.000Z", Number.NaN, "finite positive integer"],
|
||||
["since non-string", 123 as unknown as string, 50, "since must be a non-empty string"],
|
||||
["since empty", "", 50, "since must be a non-empty string"],
|
||||
])("validates inputs: %s", async (_name, since, limit, message) => {
|
||||
await expect(store.listTasksModifiedSince(since, limit as number)).rejects.toThrow(TypeError);
|
||||
await expect(store.listTasksModifiedSince(since, limit as number)).rejects.toThrow(message);
|
||||
it("uses default limit 50 and clamps above max to 200", async () => {
|
||||
for (let i = 1; i <= 220; i += 1) {
|
||||
const padded = i.toString().padStart(3, "0");
|
||||
await createTaskWithUpdatedAt(`FN-${i}`, `2026-01-01T00:00:00.${padded}Z`);
|
||||
}
|
||||
|
||||
const defaultLimited = await store.listTasksModifiedSince("2026-01-01T00:00:00.000Z", Number.NaN);
|
||||
expect(defaultLimited.tasks).toHaveLength(50);
|
||||
expect(defaultLimited.hasMore).toBe(true);
|
||||
|
||||
const maxLimited = await store.listTasksModifiedSince("2026-01-01T00:00:00.000Z", 1000);
|
||||
expect(maxLimited.tasks).toHaveLength(200);
|
||||
expect(maxLimited.hasMore).toBe(true);
|
||||
});
|
||||
|
||||
it("applies slim mode log stripping and timedExecution aggregation", async () => {
|
||||
await createTaskWithUpdatedAt("FN-1", "2026-01-01T00:00:00.000Z");
|
||||
await store.logEntry("FN-1", "[timing] Step finished in 123ms");
|
||||
|
||||
const full = await store.listTasksModifiedSince("2026-01-01T00:00:00.000Z", 50);
|
||||
expect(full).toHaveLength(1);
|
||||
expect(full[0]?.log.length).toBeGreaterThan(0);
|
||||
|
||||
const slim = await store.listTasksModifiedSince("2026-01-01T00:00:00.000Z", 50, { slim: true });
|
||||
expect(slim).toHaveLength(1);
|
||||
expect(slim[0]?.log).toEqual([]);
|
||||
expect(typeof slim[0]?.timedExecutionMs).toBe("number");
|
||||
expect(slim[0]?.timedExecutionMs).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it("excludes archived-column tasks", async () => {
|
||||
it.each([0, -5])("clamps limit below 1 to 1 (limit=%s)", async (limit) => {
|
||||
await createTaskWithUpdatedAt("FN-1", "2026-01-01T00:00:00.001Z");
|
||||
await createTaskWithUpdatedAt("FN-2", "2026-01-01T00:00:00.002Z");
|
||||
|
||||
const result = await store.listTasksModifiedSince("2026-01-01T00:00:00.000Z", limit);
|
||||
expect(result.tasks).toHaveLength(1);
|
||||
expect(result.tasks[0]?.id).toBe("FN-1");
|
||||
expect(result.hasMore).toBe(true);
|
||||
});
|
||||
|
||||
it.each(["", "not-a-date", "yesterday"])("throws on invalid since cursor: %s", async (since) => {
|
||||
await expect(store.listTasksModifiedSince(since, 50)).rejects.toThrow(TypeError);
|
||||
await expect(store.listTasksModifiedSince(since, 50)).rejects.toThrow("listTasksModifiedSince: invalid since cursor");
|
||||
});
|
||||
|
||||
it("excludes archived tasks by default and includes them when requested", async () => {
|
||||
await createTaskWithUpdatedAt("FN-1", "2026-01-01T00:00:00.001Z", "todo");
|
||||
await createTaskWithUpdatedAt("FN-2", "2026-01-01T00:00:00.002Z", "archived");
|
||||
|
||||
const changes = await store.listTasksModifiedSince("2026-01-01T00:00:00.000Z", 50);
|
||||
expect(changes.map((task) => task.id)).toEqual(["FN-1"]);
|
||||
const excluded = await store.listTasksModifiedSince("2026-01-01T00:00:00.000Z");
|
||||
expect(excluded.tasks.map((task) => task.id)).toEqual(["FN-1"]);
|
||||
|
||||
const included = await store.listTasksModifiedSince("2026-01-01T00:00:00.000Z", 50, { includeArchived: true });
|
||||
expect(included.tasks.map((task) => task.id)).toEqual(["FN-1", "FN-2"]);
|
||||
});
|
||||
|
||||
it("returns slim tasks with no prompt body and empty log", async () => {
|
||||
await createTaskWithUpdatedAt("FN-1", "2026-01-01T00:00:00.001Z");
|
||||
await store.logEntry("FN-1", "timing marker");
|
||||
|
||||
const result = await store.listTasksModifiedSince("2026-01-01T00:00:00.000Z", 50);
|
||||
expect(result.tasks).toHaveLength(1);
|
||||
expect(result.tasks[0]?.prompt).toBeUndefined();
|
||||
expect(result.tasks[0]?.log).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2710,48 +2710,49 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
|
||||
return sorted.slice(offset, offset + Math.max(0, limit));
|
||||
}
|
||||
|
||||
/** List only live-board tasks modified after `since`; archived snapshots in archiveDb are excluded. */
|
||||
/**
|
||||
* List slim task rows with `updatedAt` strictly greater than the cursor.
|
||||
*
|
||||
* Uses strict `>` cursor semantics (rows where `updatedAt === since` are excluded),
|
||||
* returns rows ordered by `updatedAt ASC`, defaults limit to 50, and caps at 200.
|
||||
* Archived tasks are excluded by default unless `opts.includeArchived` is true.
|
||||
*
|
||||
* Callers should re-invoke this method with the last returned task's `updatedAt`
|
||||
* as the next `since` cursor.
|
||||
*/
|
||||
async listTasksModifiedSince(
|
||||
since: string,
|
||||
limit: number,
|
||||
opts?: { projectId?: string; slim?: boolean },
|
||||
): Promise<Task[]> {
|
||||
if (typeof since !== "string" || since.trim().length === 0) {
|
||||
throw new TypeError("since must be a non-empty string");
|
||||
limit?: number,
|
||||
opts?: { includeArchived?: boolean },
|
||||
): Promise<{ tasks: Task[]; hasMore: boolean }> {
|
||||
if (Number.isNaN(Date.parse(since))) {
|
||||
throw new TypeError("listTasksModifiedSince: invalid since cursor");
|
||||
}
|
||||
|
||||
const resolvedLimit = limit === undefined ? 50 : limit;
|
||||
if (!Number.isFinite(resolvedLimit) || !Number.isInteger(resolvedLimit) || resolvedLimit <= 0) {
|
||||
throw new TypeError("limit must be a finite positive integer");
|
||||
}
|
||||
if (resolvedLimit > 200) {
|
||||
throw new TypeError("limit must be less than or equal to 200");
|
||||
}
|
||||
const defaultLimit = 50;
|
||||
const resolvedLimit = typeof limit !== "number" || !Number.isFinite(limit)
|
||||
? defaultLimit
|
||||
: Math.max(1, Math.min(200, Math.floor(limit)));
|
||||
const includeArchived = opts?.includeArchived ?? false;
|
||||
const selectClause = this.getTaskSelectClause(true);
|
||||
|
||||
// projectId is reserved for future cross-project queries.
|
||||
void opts?.projectId;
|
||||
const rows = includeArchived
|
||||
? (this.db.prepare(
|
||||
`SELECT ${selectClause} FROM tasks WHERE updatedAt > ? ORDER BY updatedAt ASC LIMIT ?`,
|
||||
).all(since, resolvedLimit + 1) as TaskRow[])
|
||||
: (this.db.prepare(
|
||||
`SELECT ${selectClause} FROM tasks WHERE updatedAt > ? AND "column" != 'archived' ORDER BY updatedAt ASC LIMIT ?`,
|
||||
).all(since, resolvedLimit + 1) as TaskRow[]);
|
||||
|
||||
const slim = opts?.slim === true;
|
||||
const selectClause = this.getTaskSelectClause(slim);
|
||||
const rows = this.db.prepare(
|
||||
`SELECT ${selectClause} FROM tasks WHERE updatedAt > ? AND "column" != 'archived' ORDER BY updatedAt ASC LIMIT ?`,
|
||||
).all(since, resolvedLimit) as TaskRow[];
|
||||
|
||||
return Promise.all(rows.map(async (row) => {
|
||||
const hasMore = rows.length > resolvedLimit;
|
||||
const tasks = rows.slice(0, resolvedLimit).map((row) => {
|
||||
const task = this.rowToTask(row);
|
||||
task.timedExecutionMs = this.computeTimedExecutionMs(task.log);
|
||||
task.log = [];
|
||||
return task;
|
||||
});
|
||||
|
||||
if (slim) {
|
||||
task.timedExecutionMs = this.computeTimedExecutionMs(task.log);
|
||||
task.log = [];
|
||||
}
|
||||
|
||||
if (!slim || task.steps.length > 0) {
|
||||
return task;
|
||||
}
|
||||
|
||||
const steps = await this.parseStepsFromPrompt(task.id);
|
||||
return steps.length > 0 ? { ...task, steps } : task;
|
||||
}));
|
||||
return { tasks, hasMore };
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user