Files
ThothII/backend/test/routes-workspaces.test.ts
T

263 lines
9.8 KiB
TypeScript

import { createHash } from "node:crypto";
import { once } from "node:events";
import { Buffer } from "node:buffer";
import { expect, test, vi } from "vitest";
import yazl from "yazl";
import { buildApp } from "../src/app.js";
import { loadConfig } from "../src/config.js";
import { WorkspaceRegistryError } from "../src/workspaces/git-repository.js";
import type { WorkspaceRegistry, WorkspaceRevision } from "../src/workspaces/registry.js";
import { renderWorkspaceDocs, serializeWorkspaceYaml, type CanonicalWorkspace } from "../src/workspaces/schema.js";
const workspace: CanonicalWorkspace = {
workspace: {
schema_version: 2,
id: "psd-clinical",
name: "Policlinico San Donato",
description: "Clinical analytics workspace",
language: "it",
},
dwh: {
engine: "postgres",
database: "warehouse",
schema: "datawarehouse",
supported_transports: ["postgres_direct"],
},
semantic_index: {
vector_store: {
engine: "pgvector",
database: "warehouse",
schema: "vectors",
collection: "clinical_documents",
dimensions: 768,
distance: "cosine",
supported_transports: ["pgvector_direct"],
},
embedding: {
provider: "ollama_compatible",
model: "nomic-embed-text-v2-moe",
dimensions: 768,
},
},
llm_policy: { allowed: ["zai/glm-5.2"] },
};
const revision: WorkspaceRevision = {
id: workspace.workspace.id,
commit: "a".repeat(40),
blob: "b".repeat(40),
snapshotPath: "/registry/snapshots/psd-clinical.yaml",
state: "operational",
};
type RegistryFake = Pick<WorkspaceRegistry, "bootstrap" | "pull" | "list" | "read" | "publish">;
function registryFake(overrides: Partial<RegistryFake> = {}): RegistryFake {
return {
bootstrap: vi.fn(async () => ({
branch: "main", head: revision.commit, ahead: 0, behind: 0, degraded: false,
})),
pull: vi.fn(async () => ({
branch: "main", head: revision.commit, ahead: 0, behind: 0, degraded: false,
})),
list: vi.fn(async () => [revision]),
read: vi.fn(async () => ({ workspace, revision })),
publish: vi.fn(async () => revision),
...overrides,
};
}
function appFor(registry: RegistryFake, diagnose = vi.fn(async () => ({ activatable: true, diagnostics: [] }))) {
return buildApp(loadConfig({
THT_HARNESS_DIR: "/missing-harness",
THT_WORKSPACE_REGISTRY_ROOT: "/tmp/thoth-route-test-registry",
}), {
thtRunner: {} as any,
workspaceRegistry: registry as WorkspaceRegistry,
workspaceDiagnoser: diagnose,
} as any);
}
function sha256(value: string): string {
return createHash("sha256").update(value).digest("hex");
}
async function zip(files: Record<string, string>): Promise<Buffer> {
const archive = new yazl.ZipFile();
const chunks: Buffer[] = [];
archive.outputStream.on("data", (chunk: Buffer) => chunks.push(chunk));
for (const [name, contents] of Object.entries(files)) archive.addBuffer(Buffer.from(contents), name);
archive.end();
await once(archive.outputStream, "end");
return Buffer.concat(chunks);
}
async function validBundle(): Promise<Buffer> {
const workspaceYaml = serializeWorkspaceYaml(workspace);
const docs = renderWorkspaceDocs(workspace);
const contractEnv = docs.envExample;
const readme = docs.markdown;
return await zip({
"manifest.json": JSON.stringify({
schema_version: 1,
workspace_id: workspace.workspace.id,
files: {
"workspace.yaml": sha256(workspaceYaml),
"contract.env.example": sha256(contractEnv),
"README.md": sha256(readme),
},
}),
"workspace.yaml": workspaceYaml,
"contract.env.example": contractEnv,
"README.md": readme,
});
}
function zipWithZipSlipEntry(): Promise<Buffer> {
return zip({ "aa/escape.yaml": "bad" }).then((archive) => {
const safeName = Buffer.from("aa/escape.yaml");
const unsafeName = Buffer.from("../escape.yaml");
for (let offset = archive.indexOf(safeName); offset !== -1; offset = archive.indexOf(safeName, offset + safeName.length)) {
unsafeName.copy(archive, offset);
}
return archive;
});
}
async function importBundle(app: ReturnType<typeof appFor>, archive: Buffer) {
const boundary = "----thoth-workspace-test-boundary";
const payload = Buffer.concat([
Buffer.from(`--${boundary}\r\ncontent-disposition: form-data; name="bundle"; filename="workspace.zip"\r\ncontent-type: application/zip\r\n\r\n`),
archive,
Buffer.from(`\r\n--${boundary}--\r\n`),
]);
return await app.inject({
method: "POST",
url: "/workspaces/import",
headers: { "content-type": `multipart/form-data; boundary=${boundary}` },
payload,
});
}
test("returns a redacted registry status and pulls without Git credential details", async () => {
const registry = registryFake({
bootstrap: vi.fn(async () => ({
branch: "main", head: revision.commit, ahead: 0, behind: 0, degraded: true, lastError: "git_auth_failed" as const,
})),
});
const app = appFor(registry);
const status = await app.inject({ method: "GET", url: "/workspace-registry/status" });
const pull = await app.inject({ method: "POST", url: "/workspace-registry/pull" });
expect(status.statusCode).toBe(200);
expect(status.json()).toEqual({
branch: "main", head: revision.commit, ahead: 0, behind: 0, degraded: true, lastError: "git_auth_failed",
});
expect(pull.statusCode).toBe(200);
expect(JSON.stringify([status.json(), pull.json()])).not.toMatch(/token|password|ssh:\/\//i);
});
test("lists compatible workspace summaries and reads a validated workspace", async () => {
const app = appFor(registryFake());
const list = await app.inject({ method: "GET", url: "/workspaces" });
const detail = await app.inject({ method: "GET", url: "/workspaces/psd-clinical" });
expect(list.statusCode).toBe(200);
expect(list.json()).toEqual([expect.objectContaining({
id: "psd-clinical", name: "psd-clinical", file: "psd-clinical.yaml", displayName: "Policlinico San Donato",
})]);
expect(detail.statusCode).toBe(200);
expect(detail.json()).toMatchObject({ workspace, revision });
});
test("validates a canonical workspace and runs the injected installation diagnostic", async () => {
const diagnose = vi.fn(async () => ({
activatable: false,
diagnostics: [{ level: "error" as const, code: "binding_missing" as const, field: "THT_WS_PSD_CLINICAL_DWH_PASSWORD_FILE", message: "Installation binding is missing or invalid." }],
}));
const app = appFor(registryFake(), diagnose);
const validate = await app.inject({ method: "POST", url: "/workspaces/validate", payload: { workspace } });
const testResult = await app.inject({ method: "POST", url: "/workspaces/psd-clinical/test", payload: {} });
expect(validate.statusCode).toBe(200);
expect(validate.json()).toMatchObject({ workspace });
expect(testResult.statusCode).toBe(200);
expect(testResult.json()).toMatchObject({ activatable: false, diagnostics: [{ code: "binding_missing" }] });
expect(diagnose).toHaveBeenCalledWith(workspace, expect.any(Object), { writeProbe: false });
});
test("returns a 409 field conflict instead of overwriting a changed workspace", async () => {
const conflict = Object.assign(
new WorkspaceRegistryError("workspace_conflict", "Workspace has changed"),
{
fields: ["semantic_index.embedding.model"],
expected: { commit: "c".repeat(40), blob: "d".repeat(40) },
actual: { commit: revision.commit, blob: revision.blob },
base: workspace,
local: { ...workspace, semantic_index: { ...workspace.semantic_index, embedding: { ...workspace.semantic_index.embedding, model: "local/model" } } },
remote: { ...workspace, semantic_index: { ...workspace.semantic_index, embedding: { ...workspace.semantic_index.embedding, model: "remote/model" } } },
},
);
const registry = registryFake({ publish: vi.fn(async () => { throw conflict; }) });
const app = appFor(registry);
const staleUpdate = {
action: "update",
workspace,
baseCommit: "c".repeat(40),
baseBlob: "d".repeat(40),
};
const res = await app.inject({ method: "POST", url: "/workspaces/publish", payload: staleUpdate });
expect(res.statusCode).toBe(409);
expect(res.json()).toMatchObject({
code: "workspace_conflict",
fields: ["semantic_index.embedding.model"],
expected: { commit: "c".repeat(40), blob: "d".repeat(40) },
actual: { commit: revision.commit, blob: revision.blob },
base: workspace,
remote: expect.objectContaining({
semantic_index: expect.objectContaining({
embedding: expect.objectContaining({ model: "remote/model" }),
}),
}),
});
});
test("exports generated public artifacts without secret values", async () => {
const app = appFor(registryFake());
const res = await app.inject({ method: "GET", url: "/workspaces/psd-clinical/export" });
expect(res.statusCode).toBe(200);
expect(res.headers["content-disposition"]).toMatch(/attachment; filename="psd-clinical\.zip"/);
expect(res.headers["content-type"]).toMatch(/application\/zip/);
expect(res.rawPayload.toString("utf8")).toContain("contract.env.example");
expect(res.rawPayload.toString("utf8")).not.toContain("secret-value");
});
test("rejects a zip-slip import without publishing or writing a checkout file", async () => {
const registry = registryFake();
const app = appFor(registry);
const res = await importBundle(app, await zipWithZipSlipEntry());
expect(res.statusCode).toBe(400);
expect(res.json()).toMatchObject({ code: "workspace_invalid" });
expect(registry.publish).not.toHaveBeenCalled();
});
test("imports an exact generated bundle only as a browser draft", async () => {
const registry = registryFake();
const app = appFor(registry);
const res = await importBundle(app, await validBundle());
expect(res.statusCode).toBe(200);
expect(res.json()).toMatchObject({ draft: { workspace } });
expect(registry.publish).not.toHaveBeenCalled();
});