fix: preserve workspace evidence in browser drafts

This commit is contained in:
2026-08-09 20:27:20 +02:00
parent ba7596c2dd
commit a580c4ca8a
6 changed files with 637 additions and 12 deletions
+138 -1
View File
@@ -1,7 +1,10 @@
import { expect, test } from "vitest";
import { http, HttpResponse } from "msw";
import { server } from "../test/msw";
import { asWorkspaceConflict, importWorkspace, publishWorkspace, type CanonicalWorkspace } from "./workspaces";
import {
asWorkspaceConflict, getWorkspace, importWorkspace, publishWorkspace, validateWorkspace,
type CanonicalWorkspace,
} from "./workspaces";
const workspace: CanonicalWorkspace = {
workspace: { schema_version: 3, id: "psd-clinical", name: "PSD Clinical", language: "en" },
@@ -13,6 +16,27 @@ const workspace: CanonicalWorkspace = {
llm_policy: { allowed: ["zai/glm-5.2"] },
};
const evidenceWorkspace = {
...workspace,
evidence: {
source: {
type: "filesystem",
uri: "workspace-content/psd-clinical/evidence",
patterns: ["**/*.md"],
max_bytes: 10 * 1024 * 1024,
},
policy: { max_chunk_chars: 4_000, retain_published_generations: 3 },
},
} satisfies CanonicalWorkspace;
const revision = {
id: "psd-clinical",
commit: "a".repeat(40),
blob: "b".repeat(40),
snapshotPath: "workspaces/psd-clinical.yaml",
state: "operational" as const,
};
test("uploads a workspace bundle without JSON content type", async () => {
let contentType: string | null = null;
server.use(http.post("/api/workspaces/import", ({ request }) => {
@@ -107,3 +131,116 @@ test("rejects a conflict payload that attempts to surface removed vector transpo
expect(asWorkspaceConflict(error)).toBeUndefined();
});
test("sanitizes read and validate responses while preserving 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("rejects malformed workspace API responses instead of exposing unknown Evidence fields", async () => {
const malformed = {
...evidenceWorkspace,
evidence: { ...evidenceWorkspace.evidence, signed_urls_file: "/run/secrets/urls" },
};
server.use(
http.get("/api/workspaces/psd-clinical", () => HttpResponse.json({ workspace: malformed, revision })),
http.post("/api/workspaces/validate", () => HttpResponse.json({ workspace: malformed, contract: {} })),
);
await expect(getWorkspace("psd-clinical")).rejects.toThrow();
await expect(validateWorkspace(evidenceWorkspace)).rejects.toThrow();
});
test("publishes Evidence without mutating or dropping it from the request", async () => {
let sent: unknown;
server.use(http.post("/api/workspaces/publish", async ({ request }) => {
sent = await request.json();
return HttpResponse.json({ revision });
}));
await publishWorkspace({
action: "update", workspace: evidenceWorkspace,
baseCommit: revision.commit, baseBlob: revision.blob,
});
expect(sent).toMatchObject({ workspace: { evidence: evidenceWorkspace.evidence } });
expect(evidenceWorkspace.evidence.source.patterns).toEqual(["**/*.md"]);
});
const evidenceConflictFields = [
"evidence",
"evidence.source",
"evidence.source.type",
"evidence.source.uri",
"evidence.source.patterns",
"evidence.source.max_bytes",
"evidence.source.uris",
"evidence.source.authentication",
"evidence.source.connect_timeout_ms",
"evidence.source.read_timeout_ms",
"evidence.source.max_redirects",
"evidence.source.allow_private_hosts",
"evidence.source.max_cache_bytes",
"evidence.source.endpoint_url",
"evidence.source.region",
"evidence.source.credentials",
"evidence.source.trusted_endpoint",
"evidence.source.allow_private_endpoint",
"evidence.source.allow_insecure_endpoint",
"evidence.source.max_objects",
"evidence.source.max_pages",
"evidence.source.page_size",
"evidence.policy",
"evidence.policy.max_chunk_chars",
"evidence.policy.retain_published_generations",
] as const;
test.each(evidenceConflictFields)("accepts canonical Evidence conflict field %s", async (field) => {
server.use(http.post("/api/workspaces/publish", () => HttpResponse.json({
code: "workspace_conflict",
message: "Workspace changed in the registry.",
fields: [field],
expected: { commit: "a".repeat(40), blob: "b".repeat(40) },
actual: { commit: "c".repeat(40), blob: "d".repeat(40) },
base: evidenceWorkspace,
local: evidenceWorkspace,
remote: evidenceWorkspace,
}, { status: 409 })));
const error = await publishWorkspace({
action: "update", workspace: evidenceWorkspace,
baseCommit: "a".repeat(40), baseBlob: "b".repeat(40),
}).catch((cause: unknown) => cause);
expect(asWorkspaceConflict(error)).toMatchObject({ fields: [field] });
});
test("rejects unknown Evidence conflict paths", async () => {
server.use(http.post("/api/workspaces/publish", () => HttpResponse.json({
code: "workspace_conflict",
message: "Workspace changed in the registry.",
fields: ["evidence.source.signed_url"],
expected: { commit: "a".repeat(40), blob: "b".repeat(40) },
actual: { commit: "c".repeat(40), blob: "d".repeat(40) },
base: evidenceWorkspace,
local: evidenceWorkspace,
remote: evidenceWorkspace,
}, { status: 409 })));
const error = await publishWorkspace({
action: "update", workspace: evidenceWorkspace,
baseCommit: "a".repeat(40), baseBlob: "b".repeat(40),
}).catch((cause: unknown) => cause);
expect(asWorkspaceConflict(error)).toBeUndefined();
});