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.
70 lines
5.1 KiB
TypeScript
70 lines
5.1 KiB
TypeScript
import Fastify from "fastify";
|
|
import { afterEach, expect, test, vi } from "vitest";
|
|
import { evidenceRoutes } from "../src/routes/evidence.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())); });
|
|
function setup(admin = true) {
|
|
const app = Fastify(); apps.push(app);
|
|
const principal: PrincipalContext = { issuer: "test", subject: "curator", roles: [admin ? "admin" : "user"],
|
|
permissions: admin ? ["evidence.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 withPrincipal = vi.fn(() => ({ runWithRuntimeSnapshot: run }) as unknown as ThtRunner);
|
|
const consolidateEvidence = vi.fn();
|
|
const evidenceSources = vi.fn().mockResolvedValue({status: "succeeded"});
|
|
evidenceRoutes(app, { runner: { withPrincipal }, registryRoot: "/data/registry", hostRegistryRoot: "/srv/Thoth workspaces",
|
|
registry: { list: async () => [{ id: "sales", commit: "a".repeat(40), blob: "b".repeat(40), snapshotPath: "/missing" }] },
|
|
service: { consolidateEvidence, evidenceSources } });
|
|
return { app, run, withPrincipal, principal, consolidateEvidence, evidenceSources };
|
|
}
|
|
test("browses complete local files with host paths, without a database runtime", async () => {
|
|
const { app, run, withPrincipal, principal } = setup();
|
|
const response = await app.inject("/workspaces/sales/evidence?kind=domain&concept=orders&page=2");
|
|
expect(response.statusCode).toBe(200);
|
|
expect(withPrincipal).toHaveBeenCalledWith(principal);
|
|
const [argv, snapshot] = run.mock.calls[0] as unknown as [string[], string];
|
|
expect(argv).toEqual(["evidence", "admin", "--workspace", "sales"]);
|
|
expect(JSON.parse(snapshot)).toEqual({ root: "/data/registry/repo/sales", query: { kind: "domain", concept: "orders", page: 2 } });
|
|
expect(response.json().location.workspace).toBe("/srv/Thoth workspaces/repo/sales");
|
|
expect(response.json().location.git_commands[0]).toContain("git -C '/srv/Thoth workspaces/repo'");
|
|
});
|
|
|
|
test("source actions require admin, bind the actor, and reject cross-workspace or arbitrary inputs", async () => {
|
|
const denied = setup(false);
|
|
expect((await denied.app.inject({method: "POST", url: "/workspaces/sales/evidence/sources", payload: {action: "refresh"}})).statusCode).toBe(403);
|
|
expect(denied.evidenceSources).not.toHaveBeenCalled();
|
|
const allowed = setup();
|
|
expect((await allowed.app.inject({method: "POST", url: "/workspaces/sales/evidence/sources", payload: {action: "refresh"}})).statusCode).toBe(200);
|
|
expect(allowed.evidenceSources).toHaveBeenCalledWith({workspaceId: "sales", action: "refresh", actor: "curator"});
|
|
const choice = {action: "decide", sourceId: "a".repeat(64), revision: "b".repeat(64), decision: "keep"};
|
|
expect((await allowed.app.inject({method: "POST", url: "/workspaces/sales/evidence/sources", payload: choice})).statusCode).toBe(200);
|
|
expect(allowed.evidenceSources).toHaveBeenLastCalledWith({...choice, workspaceId: "sales", actor: "curator"});
|
|
for (const payload of [{action: "refresh", url: "http://localhost"}, {...choice, actor: "forged"}, {...choice, revision: "../other"}]) {
|
|
expect((await allowed.app.inject({method: "POST", url: "/workspaces/sales/evidence/sources", payload})).statusCode).toBe(400);
|
|
}
|
|
expect((await allowed.app.inject({method: "POST", url: "/workspaces/other/evidence/sources", payload: choice})).statusCode).toBe(404);
|
|
expect(allowed.evidenceSources).toHaveBeenCalledTimes(2);
|
|
});
|
|
test.each(["/", "/evidence:rule", "/consolidate"])("refuses non-admin access %s", async suffix => {
|
|
const { app, run, consolidateEvidence } = setup(false);
|
|
const response = await app.inject({ method: suffix === "/consolidate" ? "POST" : "GET", url: `/workspaces/sales/evidence${suffix === "/" ? "" : suffix}` });
|
|
expect(response.statusCode).toBe(403); expect(run).not.toHaveBeenCalled(); expect(consolidateEvidence).not.toHaveBeenCalled();
|
|
});
|
|
test("rejects workspace escape, unknown workspace and unsupported filters", async () => {
|
|
const { app, run } = setup();
|
|
expect((await app.inject("/workspaces/foreign/evidence")).statusCode).toBe(404);
|
|
expect((await app.inject("/workspaces/sales/evidence?root=/private")).statusCode).toBe(400);
|
|
expect((await app.inject("/workspaces/sales/evidence?page_size=101")).statusCode).toBe(400);
|
|
expect(run).not.toHaveBeenCalled();
|
|
});
|
|
test("a missing unit is 404 and consolidation preserves the partial outcome", async () => {
|
|
const { app, consolidateEvidence } = setup();
|
|
expect((await app.inject("/workspaces/sales/evidence/evidence:missing")).statusCode).toBe(404);
|
|
consolidateEvidence.mockResolvedValue({ status: "failed", warnings: ["Saved; retry indexing."] });
|
|
const response = await app.inject({ method: "POST", url: "/workspaces/sales/evidence/consolidate" });
|
|
expect(response.json()).toEqual({ status: "failed", warnings: ["Saved; retry indexing."] });
|
|
});
|