Publish documentation / publish (push) Successful in 1m27s
Add PostgreSQL-backed memory, editable evidence with source review and activation, and human-approved archive repairs across the harness, API, and UI. Include migrations, deployment support, regression coverage, and validation documentation. Refresh permissions from validated session roles so existing administrator logins can access newly deployed archive management features.
82 lines
4.6 KiB
TypeScript
82 lines
4.6 KiB
TypeScript
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<typeof Fastify>[] = [];
|
|
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/);
|
|
},
|
|
);
|