189 lines
6.4 KiB
TypeScript
189 lines
6.4 KiB
TypeScript
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.",
|
|
});
|
|
});
|