refactor: make browser workspace writes bootstrap-only
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import { http, HttpResponse } from "msw";
|
||||
import { server } from "../test/msw";
|
||||
import { canonicalWorkspaceFixture } from "../test/workspace-fixtures";
|
||||
import { canonicalWorkspaceFixture, workspaceRevisionFixture, workspaceSummaryFixture } from "../test/workspace-fixtures";
|
||||
import { createSession, getMe, listSessions, prewarmRuntime, resumeSession } from "./sessions";
|
||||
import {
|
||||
renameSession, setSessionGroup, archiveSession, unarchiveSession,
|
||||
@@ -15,8 +15,10 @@ test("createSession migrates legacy selections and POSTs browser preferences", a
|
||||
workspace: "psd-clinical", provider: "zai", model: "glm-5.2", thinking: "low",
|
||||
})),
|
||||
http.get("/api/workspaces", () => HttpResponse.json([{
|
||||
id: "psd-clinical", name: "psd-clinical", file: "psd-clinical.yaml", displayName: "PSD Clinical", language: "en",
|
||||
revision: { id: "psd-clinical", commit: "a".repeat(40), blob: "b".repeat(40), snapshotPath: "/snapshot" },
|
||||
...workspaceSummaryFixture("psd-clinical", {
|
||||
displayName: "PSD Clinical",
|
||||
revision: workspaceRevisionFixture("psd-clinical"),
|
||||
}),
|
||||
}])),
|
||||
http.get("/api/workspaces/psd-clinical", () => HttpResponse.json({
|
||||
workspace: canonicalWorkspaceFixture("psd-clinical"),
|
||||
@@ -43,11 +45,11 @@ test("createSession does not POST when a selected summary aliases another worksp
|
||||
let posted = false;
|
||||
server.use(
|
||||
http.get("/api/workspaces", () => HttpResponse.json([{
|
||||
id: "psd-clinical", name: "other-workspace", file: "psd-clinical.yaml",
|
||||
displayName: "PSD Clinical", language: "en",
|
||||
revision: {
|
||||
id: "psd-clinical", commit: "a".repeat(40), blob: "b".repeat(40), snapshotPath: "/snapshot",
|
||||
},
|
||||
...workspaceSummaryFixture("psd-clinical", {
|
||||
displayName: "PSD Clinical",
|
||||
revision: workspaceRevisionFixture("psd-clinical"),
|
||||
}),
|
||||
name: "other-workspace",
|
||||
}])),
|
||||
http.get("/api/workspaces/psd-clinical", () => HttpResponse.json({
|
||||
workspace: canonicalWorkspaceFixture("psd-clinical"),
|
||||
@@ -82,8 +84,10 @@ test.each([
|
||||
let posted = false;
|
||||
server.use(
|
||||
http.get("/api/workspaces", () => HttpResponse.json([{
|
||||
id: "psd-clinical", name: "psd-clinical", file: "psd-clinical.yaml",
|
||||
displayName: "PSD Clinical", language: "en", revision,
|
||||
...workspaceSummaryFixture("psd-clinical", {
|
||||
displayName: "PSD Clinical",
|
||||
revision: revision as any,
|
||||
}),
|
||||
}])),
|
||||
http.get("/api/workspaces/psd-clinical", () => HttpResponse.json({
|
||||
workspace: canonicalWorkspaceFixture("psd-clinical"),
|
||||
@@ -131,7 +135,10 @@ test("createSession rejects a workspace summary that omits the canonical revisio
|
||||
workspace: "psd-clinical", provider: "zai", model: "glm-5.2", thinking: "low",
|
||||
})),
|
||||
http.get("/api/workspaces", () => HttpResponse.json([{
|
||||
id: "psd-clinical", name: "psd-clinical", file: "psd-clinical.yaml", displayName: "PSD Clinical", language: "en",
|
||||
...workspaceSummaryFixture("psd-clinical", {
|
||||
displayName: "PSD Clinical",
|
||||
configurationState: "configuration_required",
|
||||
}),
|
||||
}])),
|
||||
http.post("/api/sessions", async () => {
|
||||
posted = true;
|
||||
|
||||
@@ -1,27 +1,32 @@
|
||||
import { expect, test } from "vitest";
|
||||
import { http, HttpResponse } from "msw";
|
||||
import { server } from "../test/msw";
|
||||
import { canonicalWorkspaceFixture, workspaceRevisionFixture, workspaceSummaryFixture } from "../test/workspace-fixtures";
|
||||
import {
|
||||
asWorkspaceConflict, getWorkspace, importWorkspace, listWorkspaces, publishWorkspace, validateWorkspace,
|
||||
asWorkspaceApiError,
|
||||
getWorkspace,
|
||||
importWorkspace,
|
||||
listWorkspaces,
|
||||
publishWorkspace,
|
||||
validateWorkspace,
|
||||
type CanonicalWorkspace,
|
||||
} from "./workspaces";
|
||||
|
||||
const workspace: CanonicalWorkspace = {
|
||||
workspace: { schema_version: 3, id: "psd-clinical", name: "PSD Clinical", language: "en" },
|
||||
dwh: { engine: "postgres", database: "clinical", schema: "datawarehouse", supported_transports: ["postgres_direct"] },
|
||||
semantic_index: {
|
||||
vector_store: { engine: "qdrant", collection: "clinical", dimensions: 1024, distance: "cosine" },
|
||||
embedding: { provider: "ollama_internal", model: "qwen3-embedding:0.6b", dimensions: 1024 },
|
||||
},
|
||||
llm_policy: { allowed: ["zai/glm-5.2"] },
|
||||
};
|
||||
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: "workspace-content/psd-clinical/evidence",
|
||||
uri: "psd-clinical/evidence",
|
||||
patterns: ["**/*.md"],
|
||||
max_bytes: 10 * 1024 * 1024,
|
||||
},
|
||||
@@ -29,79 +34,43 @@ const evidenceWorkspace = {
|
||||
},
|
||||
} satisfies CanonicalWorkspace;
|
||||
|
||||
const revision = {
|
||||
id: "psd-clinical",
|
||||
commit: "a".repeat(40),
|
||||
blob: "b".repeat(40),
|
||||
snapshotPath: "workspaces/psd-clinical.yaml",
|
||||
};
|
||||
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",
|
||||
}),
|
||||
])));
|
||||
|
||||
const summary = {
|
||||
id: "psd-clinical",
|
||||
name: "psd-clinical",
|
||||
file: "psd-clinical.yaml",
|
||||
displayName: "PSD Clinical",
|
||||
description: "Clinical workspace",
|
||||
language: "en" as const,
|
||||
revision,
|
||||
};
|
||||
|
||||
test("decodes and normalizes state-free workspace summaries without passing through backend-only fields", async () => {
|
||||
server.use(http.get("/api/workspaces", () => HttpResponse.json([{
|
||||
...summary, workspace, backendOnly: "ignored",
|
||||
}])));
|
||||
|
||||
await expect(listWorkspaces()).resolves.toEqual([summary]);
|
||||
});
|
||||
|
||||
test("accepts internally multiline display names and descriptions using backend trim semantics", async () => {
|
||||
const multiline = {
|
||||
...summary,
|
||||
displayName: "PSD\nClinical",
|
||||
description: "First line\n\tSecond line",
|
||||
};
|
||||
server.use(http.get("/api/workspaces", () => HttpResponse.json([multiline])));
|
||||
|
||||
await expect(listWorkspaces()).resolves.toEqual([multiline]);
|
||||
await expect(listWorkspaces()).resolves.toEqual([
|
||||
readySummary,
|
||||
workspaceSummaryFixture("bootstrap-slot", {
|
||||
displayName: "Bootstrap slot",
|
||||
description: "Needs configuration",
|
||||
configurationState: "configuration_required",
|
||||
}),
|
||||
]);
|
||||
});
|
||||
|
||||
test.each([
|
||||
["an id/name alias", { ...summary, name: "other-workspace" }],
|
||||
["a non-canonical selector file", { ...summary, file: "workspaces/psd-clinical.yaml" }],
|
||||
])("rejects workspace summaries with %s", async (_case, malformedSummary) => {
|
||||
["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.each([
|
||||
["historical state", { ...revision, state: "operational" }],
|
||||
["unknown revision field", { ...revision, generation: 1 }],
|
||||
["malformed revision", { ...revision, commit: "not-a-commit" }],
|
||||
])("rejects workspace summaries with %s", async (_case, malformedRevision) => {
|
||||
server.use(http.get("/api/workspaces", () => HttpResponse.json([{
|
||||
...summary, revision: malformedRevision,
|
||||
}])));
|
||||
|
||||
await expect(listWorkspaces()).rejects.toThrow("invalid workspace summary");
|
||||
});
|
||||
|
||||
test.each([
|
||||
["a non-array response", { ...summary }],
|
||||
["a malformed selector field", [{ ...summary, language: "fr" }]],
|
||||
])("rejects %s from the workspace summary API", async (_case, response) => {
|
||||
server.use(http.get("/api/workspaces", () => HttpResponse.json(response)));
|
||||
|
||||
await expect(listWorkspaces()).rejects.toThrow("invalid workspace summary");
|
||||
});
|
||||
|
||||
test("preserves a present summary without revision so callers can distinguish it from an absent workspace", async () => {
|
||||
const { revision: _revision, ...incomplete } = summary;
|
||||
server.use(http.get("/api/workspaces", () => HttpResponse.json([incomplete])));
|
||||
|
||||
await expect(listWorkspaces()).resolves.toEqual([incomplete]);
|
||||
});
|
||||
|
||||
test("uploads a workspace bundle without JSON content type", async () => {
|
||||
let contentType: string | null = null;
|
||||
server.use(http.post("/api/workspaces/import", ({ request }) => {
|
||||
@@ -111,13 +80,10 @@ test("uploads a workspace bundle without JSON content type", async () => {
|
||||
|
||||
await importWorkspace(new File(["zip"], "clinical.thoth-workspace.zip", { type: "application/zip" }));
|
||||
|
||||
// jsdom's FormData is not the same implementation as Node's fetch FormData,
|
||||
// so it cannot expose a browser-generated boundary here. The client must leave
|
||||
// that header untouched; a real browser adds multipart/form-data + boundary.
|
||||
expect(contentType ?? "").not.toMatch(/application\/json/i);
|
||||
});
|
||||
|
||||
test("sanitizes imported Evidence before returning a browser draft", async () => {
|
||||
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: [] } },
|
||||
})));
|
||||
@@ -128,13 +94,21 @@ test("sanitizes imported Evidence before returning a browser draft", async () =>
|
||||
expect(result.draft.workspace).not.toBe(evidenceWorkspace);
|
||||
});
|
||||
|
||||
test("rejects imported Evidence with a secret-shaped field", async () => {
|
||||
const malformed = {
|
||||
...evidenceWorkspace,
|
||||
evidence: { ...evidenceWorkspace.evidence, signed_urls_file: "/run/secrets/urls" },
|
||||
};
|
||||
test("rejects imported filesystem Evidence that still points at workspace-content", async () => {
|
||||
server.use(http.post("/api/workspaces/import", () => HttpResponse.json({
|
||||
draft: { workspace: malformed, contract: {} },
|
||||
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")))
|
||||
@@ -142,140 +116,12 @@ test("rejects imported Evidence with a secret-shaped field", async () => {
|
||||
});
|
||||
|
||||
test("accepts the atomic schema-v3 workspace revision contract without historical state", async () => {
|
||||
const stateFreeRevision = {
|
||||
id: "psd-clinical",
|
||||
commit: "a".repeat(40),
|
||||
blob: "b".repeat(40),
|
||||
snapshotPath: "workspaces/psd-clinical.yaml",
|
||||
};
|
||||
server.use(http.get("/api/workspaces/psd-clinical", () => HttpResponse.json({
|
||||
workspace, revision: stateFreeRevision,
|
||||
})));
|
||||
server.use(http.get("/api/workspaces/psd-clinical", () => HttpResponse.json({ workspace, revision })));
|
||||
|
||||
await expect(getWorkspace("psd-clinical")).resolves.toEqual({ workspace, revision: stateFreeRevision });
|
||||
await expect(getWorkspace("psd-clinical")).resolves.toEqual({ workspace, revision });
|
||||
});
|
||||
|
||||
test("rejects the removed historical workspace revision state as an extra API key", async () => {
|
||||
server.use(http.get("/api/workspaces/psd-clinical", () => HttpResponse.json({
|
||||
workspace,
|
||||
revision: {
|
||||
id: "psd-clinical",
|
||||
commit: "a".repeat(40),
|
||||
blob: "b".repeat(40),
|
||||
snapshotPath: "workspaces/psd-clinical.yaml",
|
||||
[["st", "ate"].join("")]: "operational",
|
||||
},
|
||||
})));
|
||||
|
||||
await expect(getWorkspace("psd-clinical")).rejects.toThrow("invalid workspace revision");
|
||||
});
|
||||
|
||||
test("rejects read responses with a missing or inconsistent revision", async () => {
|
||||
server.use(http.get("/api/workspaces/psd-clinical", () => HttpResponse.json({
|
||||
workspace: evidenceWorkspace,
|
||||
revision: { ...revision, id: "other-workspace" },
|
||||
})));
|
||||
await expect(getWorkspace("psd-clinical")).rejects.toThrow("invalid workspace revision");
|
||||
|
||||
server.use(http.get("/api/workspaces/psd-clinical", () => HttpResponse.json({
|
||||
workspace: evidenceWorkspace, revision: null,
|
||||
})));
|
||||
await expect(getWorkspace("psd-clinical")).rejects.toThrow("invalid workspace revision");
|
||||
});
|
||||
|
||||
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: "update", workspace: evidenceWorkspace,
|
||||
baseCommit: revision.commit, baseBlob: revision.blob,
|
||||
})).rejects.toThrow("invalid workspace revision");
|
||||
});
|
||||
|
||||
test("rejects a conflict payload that attempts to surface a secret field", async () => {
|
||||
server.use(http.post("/api/workspaces/publish", () => HttpResponse.json({
|
||||
code: "workspace_conflict", message: "Workspace changed in the registry.", fields: ["dwh.password"],
|
||||
base: { ...workspace, dwh: { ...workspace.dwh, password: "secret" } }, local: workspace, remote: workspace,
|
||||
}, { status: 409 })));
|
||||
|
||||
const error = await publishWorkspace({ action: "update", workspace, baseCommit: "a".repeat(40), baseBlob: "b".repeat(40) }).catch((cause: unknown) => cause);
|
||||
|
||||
expect(asWorkspaceConflict(error)).toBeUndefined();
|
||||
});
|
||||
|
||||
const diagnosticConflictFields = [
|
||||
"diagnostics.dwh_rest.method", "diagnostics.dwh_rest.path", "diagnostics.dwh_rest.auth",
|
||||
"diagnostics.dwh_rest.response.database", "diagnostics.dwh_rest.response.schema",
|
||||
] as const;
|
||||
|
||||
const optionalDiagnosticsConflictFields = [
|
||||
"diagnostics",
|
||||
"diagnostics.dwh_rest",
|
||||
] as const;
|
||||
|
||||
const diagnosticsWorkspace: CanonicalWorkspace = {
|
||||
...workspace,
|
||||
dwh: { ...workspace.dwh, supported_transports: ["postgres_direct", "rest_api"] },
|
||||
diagnostics: {
|
||||
dwh_rest: { method: "POST", path: "/rpc/ping", auth: "bearer", response: { database: "database", schema: "schema" } },
|
||||
},
|
||||
};
|
||||
|
||||
test.each(optionalDiagnosticsConflictFields)("accepts optional diagnostics conflict branch %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: workspace, local: diagnosticsWorkspace, remote: diagnosticsWorkspace,
|
||||
}, { status: 409 })));
|
||||
|
||||
const error = await publishWorkspace({ action: "update", workspace: diagnosticsWorkspace, baseCommit: "a".repeat(40), baseBlob: "b".repeat(40) }).catch((cause: unknown) => cause);
|
||||
|
||||
expect(asWorkspaceConflict(error)).toMatchObject({ fields: [field] });
|
||||
});
|
||||
|
||||
test.each(diagnosticConflictFields)("accepts canonical diagnostic conflict leaf %s with its remote revision", async (field) => {
|
||||
const diagnosticsWorkspace: CanonicalWorkspace = {
|
||||
...workspace,
|
||||
dwh: { ...workspace.dwh, supported_transports: ["postgres_direct", "rest_api"] },
|
||||
diagnostics: {
|
||||
dwh_rest: { method: "POST", path: "/rpc/ping", auth: "bearer", response: { database: "database", schema: "schema" } },
|
||||
},
|
||||
};
|
||||
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: diagnosticsWorkspace, local: diagnosticsWorkspace, remote: diagnosticsWorkspace,
|
||||
}, { status: 409 })));
|
||||
|
||||
const error = await publishWorkspace({ action: "update", workspace: diagnosticsWorkspace, baseCommit: "a".repeat(40), baseBlob: "b".repeat(40) }).catch((cause: unknown) => cause);
|
||||
|
||||
expect(asWorkspaceConflict(error)).toMatchObject({ actual: { commit: "c".repeat(40), blob: "d".repeat(40) }, fields: [field] });
|
||||
});
|
||||
|
||||
test("rejects a conflict payload that attempts to surface removed vector transport and credential branches", async () => {
|
||||
server.use(http.post("/api/workspaces/publish", () => HttpResponse.json({
|
||||
code: "workspace_conflict",
|
||||
message: "Workspace changed in the registry.",
|
||||
fields: ["diagnostics.vector_rest.reversible_probe.auth"],
|
||||
expected: { commit: "a".repeat(40), blob: "b".repeat(40) },
|
||||
actual: { commit: "c".repeat(40), blob: "d".repeat(40) },
|
||||
base: workspace,
|
||||
local: workspace,
|
||||
remote: workspace,
|
||||
}, { status: 409 })));
|
||||
|
||||
const error = await publishWorkspace({ action: "update", workspace, baseCommit: "a".repeat(40), baseBlob: "b".repeat(40) }).catch((cause: unknown) => cause);
|
||||
|
||||
expect(asWorkspaceConflict(error)).toBeUndefined();
|
||||
});
|
||||
|
||||
|
||||
test("sanitizes read and validate responses while preserving Evidence", async () => {
|
||||
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: {} })),
|
||||
@@ -289,100 +135,54 @@ test("sanitizes read and validate responses while preserving Evidence", async ()
|
||||
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 () => {
|
||||
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 publishWorkspace({
|
||||
action: "update", workspace: evidenceWorkspace,
|
||||
baseCommit: revision.commit, baseBlob: revision.blob,
|
||||
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).toMatchObject({ workspace: { evidence: evidenceWorkspace.evidence } });
|
||||
expect(evidenceWorkspace.evidence.source.patterns).toEqual(["**/*.md"]);
|
||||
expect(sent).not.toHaveProperty("baseBlob");
|
||||
});
|
||||
|
||||
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) => {
|
||||
test("rejects a publish response with a malformed revision", async () => {
|
||||
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,
|
||||
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: "update", workspace: evidenceWorkspace,
|
||||
baseCommit: "a".repeat(40), baseBlob: "b".repeat(40),
|
||||
action: "create",
|
||||
workspace,
|
||||
baseCommit: revision.commit,
|
||||
}).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();
|
||||
expect(asWorkspaceApiError(error)).toEqual({
|
||||
status: 409,
|
||||
code: "workspace_curator_owned",
|
||||
message: "Existing descriptors are curator-owned.",
|
||||
});
|
||||
});
|
||||
|
||||
Binary file not shown.
Reference in New Issue
Block a user