import { expect, test } from "vitest"; import { http, HttpResponse } from "msw"; import { server } from "../test/msw"; import { canonicalWorkspaceFixture, workspaceRevisionFixture, workspaceSummaryFixture } from "../test/workspace-fixtures"; import { asWorkspaceApiError, getWorkspace, importWorkspace, listWorkspaces, publishWorkspace, validateWorkspace, type CanonicalWorkspace, } from "./workspaces"; const workspace = canonicalWorkspaceFixture("psd-clinical"); const revision = workspaceRevisionFixture("psd-clinical"); const readySummary = workspaceSummaryFixture("psd-clinical", { displayName: "PSD Clinical", description: "Clinical workspace", revision, }); const evidenceWorkspace = { ...workspace, evidence: { source: { type: "filesystem", uri: "psd-clinical/evidence", patterns: ["**/*.md"], max_bytes: 10 * 1024 * 1024, }, policy: { max_chunk_chars: 4_000, retain_published_generations: 3 }, }, } satisfies CanonicalWorkspace; test("decodes catalog-driven workspace summaries with exact root descriptor paths", async () => { server.use(http.get("/api/workspaces", () => HttpResponse.json([ readySummary, workspaceSummaryFixture("bootstrap-slot", { displayName: "Bootstrap slot", description: "Needs configuration", configurationState: "configuration_required", }), ]))); await expect(listWorkspaces()).resolves.toEqual([ readySummary, workspaceSummaryFixture("bootstrap-slot", { displayName: "Bootstrap slot", description: "Needs configuration", configurationState: "configuration_required", }), ]); }); test.each([ ["a summary with the removed language field", { ...readySummary, language: "en" }], ["a non-canonical descriptor path", { ...readySummary, file: "psd-clinical.yaml" }], ["a ready summary without a revision", { ...readySummary, revision: undefined }], ["a configuration_required summary with a revision", { ...workspaceSummaryFixture("bootstrap-slot", { displayName: "Bootstrap slot", configurationState: "configuration_required", }), revision, }], ])("rejects %s", async (_case, malformedSummary) => { server.use(http.get("/api/workspaces", () => HttpResponse.json([malformedSummary]))); await expect(listWorkspaces()).rejects.toThrow("invalid workspace summary"); }); test("uploads a workspace bundle without JSON content type", async () => { let contentType: string | null = null; server.use(http.post("/api/workspaces/import", ({ request }) => { contentType = request.headers.get("content-type"); return HttpResponse.json({ draft: { workspace } }); })); await importWorkspace(new File(["zip"], "clinical.thoth-workspace.zip", { type: "application/zip" })); expect(contentType ?? "").not.toMatch(/application\/json/i); }); test("sanitizes imported filesystem Evidence only when it uses the workspace directory root", async () => { server.use(http.post("/api/workspaces/import", () => HttpResponse.json({ draft: { workspace: evidenceWorkspace, contract: { variables: [] } }, }))); const result = await importWorkspace(new File(["zip"], "clinical.thoth-workspace.zip")); expect(result.draft.workspace.evidence).toEqual(evidenceWorkspace.evidence); expect(result.draft.workspace).not.toBe(evidenceWorkspace); }); test("rejects imported filesystem Evidence that still points at workspace-content", async () => { server.use(http.post("/api/workspaces/import", () => HttpResponse.json({ draft: { workspace: { ...evidenceWorkspace, evidence: { ...evidenceWorkspace.evidence, source: { ...evidenceWorkspace.evidence.source, uri: "workspace-content/psd-clinical/evidence", }, }, }, contract: { variables: [] }, }, }))); await expect(importWorkspace(new File(["zip"], "clinical.thoth-workspace.zip"))) .rejects.toThrow("invalid imported workspace draft"); }); test("accepts the atomic schema-v3 workspace revision contract without historical state", async () => { server.use(http.get("/api/workspaces/psd-clinical", () => HttpResponse.json({ workspace, revision }))); await expect(getWorkspace("psd-clinical")).resolves.toEqual({ workspace, revision }); }); test("sanitizes read and validate responses while preserving directory-based Evidence", async () => { server.use( http.get("/api/workspaces/psd-clinical", () => HttpResponse.json({ workspace: evidenceWorkspace, revision })), http.post("/api/workspaces/validate", () => HttpResponse.json({ workspace: evidenceWorkspace, contract: {} })), ); const read = await getWorkspace("psd-clinical"); const validated = await validateWorkspace(evidenceWorkspace); expect(read.workspace.evidence).toEqual(evidenceWorkspace.evidence); expect(read.workspace).not.toBe(evidenceWorkspace); expect(validated.workspace.evidence).toEqual(evidenceWorkspace.evidence); }); test("publishes only bootstrap create requests", async () => { let sent: unknown; server.use(http.post("/api/workspaces/publish", async ({ request }) => { sent = await request.json(); return HttpResponse.json({ revision }); })); await expect(publishWorkspace({ action: "create", workspace: evidenceWorkspace, baseCommit: revision.commit, })).resolves.toEqual({ revision }); expect(sent).toEqual({ action: "create", workspace: { ...evidenceWorkspace }, baseCommit: revision.commit, }); expect(sent).not.toHaveProperty("baseBlob"); }); test("rejects a publish response with a malformed revision", async () => { server.use(http.post("/api/workspaces/publish", () => HttpResponse.json({ revision: { ...revision, commit: "not-a-commit" }, }))); await expect(publishWorkspace({ action: "create", workspace: evidenceWorkspace, baseCommit: revision.commit, })).rejects.toThrow("invalid workspace revision"); }); test("decodes workspace_curator_owned safely without conflict fields", async () => { server.use(http.post("/api/workspaces/publish", () => HttpResponse.json({ code: "workspace_curator_owned", message: "Existing descriptors are curator-owned.", }, { status: 409 }))); const error = await publishWorkspace({ action: "create", workspace, baseCommit: revision.commit, }).catch((cause: unknown) => cause); expect(asWorkspaceApiError(error)).toEqual({ status: 409, code: "workspace_curator_owned", message: "Existing descriptors are curator-owned.", }); });