import Fastify from "fastify"; import { afterEach, expect, test, vi } from "vitest"; import { memoryRoutes } from "../src/routes/memory.js"; import { DEFAULT_SEMANTIC_RUNTIME } from "../src/workspaces/runtime-renderer.js"; import type { ThtRunner } from "../src/tht/tht-runner.js"; import type { PrincipalContext } from "../src/auth/principal.js"; const apps: ReturnType[] = []; afterEach(async () => { await Promise.all(apps.splice(0).map(app => app.close())); }); const id = "mem-11111111-1111-4111-8111-111111111111"; const input = { family: "domain_clarification", subject: "Order", scope: "Sales" }; function setup(admin = true) { const app = Fastify(); apps.push(app); const principal: PrincipalContext = { issuer: "test", subject: "operator", roles: [admin ? "admin" : "user"], permissions: admin ? ["memory.manage"] : ["session.use"], isAdmin: admin }; app.addHook("preHandler", async request => { request.principal = principal; }); const run = vi.fn(async () => ({ code: 0, stdout: JSON.stringify({ items: [], total: 0 }), stderr: "" })); const bound = { runWithRuntimeSnapshot: run } as unknown as ThtRunner; const withPrincipal = vi.fn(() => bound); memoryRoutes(app, { runner: { withPrincipal }, runtime: DEFAULT_SEMANTIC_RUNTIME, registry: { list: async () => [{ id: "sales", commit: "a".repeat(40), blob: "b".repeat(40), snapshotPath: "/missing" }], read: vi.fn().mockResolvedValue({ workspace: { workspace: { language: "it" } } }), } }); return { app, run, withPrincipal, principal }; } test("list binds principal and workspace without requiring a DWH runtime", async () => { const { app, run, withPrincipal, principal } = setup(); const response = await app.inject("/workspaces/sales/memory?q=Order&page=2&column=id"); expect(response.statusCode).toBe(200); expect(withPrincipal).toHaveBeenCalledWith(principal); const [args, snapshot] = run.mock.calls[0] as unknown as [string[], string]; expect(args).toEqual(["memory", "admin", "--workspace", "sales"]); expect(JSON.parse(snapshot)).toMatchObject({ action: "list", request: { q: "Order", page: 2, column: "id" } }); expect(JSON.parse(snapshot).runtime.memoryLanguage).toBe("it"); }); test.each([ ["GET", ""], ["POST", ""], ["GET", "/pending"], ["GET", `/${id}`], ["PUT", `/${id}`], ["DELETE", `/${id}`], ["POST", `/${id}/retry`], ] as const)("non-admin cannot %s %s", async (method, suffix) => { const { app, run } = setup(false); const response = await app.inject({ method, url: `/workspaces/sales/memory${suffix}`, ...(method === "PUT" || method === "POST" && suffix === "" ? { payload: input } : {}) }); expect(response.statusCode).toBe(403); expect(run).not.toHaveBeenCalled(); }); test("unknown workspace and invalid input do not invoke the harness", async () => { const { app, run } = setup(); expect((await app.inject("/workspaces/missing/memory")).statusCode).toBe(404); expect((await app.inject("/workspaces/sales/memory?page_size=101")).statusCode).toBe(400); expect((await app.inject({ method: "POST", url: "/workspaces/sales/memory", payload: { ...input, workspace_id: "foreign" } })).statusCode).toBe(400); expect(run).not.toHaveBeenCalled(); }); test("create preserves saved-but-unindexed outcome, update carries complete related changes", async () => { const { app, run } = setup(); run.mockResolvedValue({ code: 0, stdout: JSON.stringify({ id, saved: true, indexed: false }), stderr: "" }); const result = await app.inject({ method: "POST", url: "/workspaces/sales/memory", payload: input }); expect(result.statusCode).toBe(201); expect(result.json()).toEqual({ id, saved: true, indexed: false }); await app.inject({ method: "PUT", url: `/workspaces/sales/memory/${id}`, payload: { ...input, links: [{ target_id: id, meaning: "Related" }], dependencies: [{ database: "dwh" }], } }); const [, snapshot] = run.mock.calls[1] as unknown as [string[], string]; expect(JSON.parse(snapshot)).toMatchObject({ action: "update", request: { id, card: { links: [{ target_id: id, meaning: "Related" }], dependencies: [{ database: "dwh" }], } } }); }); test.each([["memory_not_found", 404], ["memory_conflict", 409], ["memory_unavailable", 503]])( "maps %s without leaking stderr", async (code, status) => { const { app, run } = setup(); run.mockResolvedValue({ code: 1, stdout: JSON.stringify({ code, message: "sensitive detail" }), stderr: "secret" }); const result = await app.inject("/workspaces/sales/memory"); expect(result.statusCode).toBe(status); expect(result.json().code).toBe(code); expect(result.body).not.toMatch(/secret|sensitive/); }, );