feat: publish and synchronize workspace drafts
This commit is contained in:
@@ -1,7 +1,17 @@
|
||||
import { expect, test } from "vitest";
|
||||
import { http, HttpResponse } from "msw";
|
||||
import { server } from "../test/msw";
|
||||
import { importWorkspace } from "./workspaces";
|
||||
import { asWorkspaceConflict, importWorkspace, publishWorkspace, type CanonicalWorkspace } from "./workspaces";
|
||||
|
||||
const workspace: CanonicalWorkspace = {
|
||||
workspace: { schema_version: 2, id: "psd-clinical", name: "PSD Clinical", language: "en" },
|
||||
dwh: { engine: "postgres", database: "clinical", schema: "datawarehouse", supported_transports: ["postgres_direct"] },
|
||||
semantic_index: {
|
||||
vector_store: { engine: "pgvector", database: "vectors", schema: "public", collection: "clinical", dimensions: 768, distance: "cosine", supported_transports: ["pgvector_direct"] },
|
||||
embedding: { provider: "ollama_compatible", model: "nomic-embed-text", dimensions: 768 },
|
||||
},
|
||||
llm_policy: { allowed: ["zai/glm-5.2"] },
|
||||
};
|
||||
|
||||
test("uploads a workspace bundle without JSON content type", async () => {
|
||||
let contentType: string | null = null;
|
||||
@@ -17,3 +27,14 @@ test("uploads a workspace bundle without JSON content type", async () => {
|
||||
// that header untouched; a real browser adds multipart/form-data + boundary.
|
||||
expect(contentType ?? "").not.toMatch(/application\/json/i);
|
||||
});
|
||||
|
||||
test("rejects a conflict payload that attempts to surface a secret field", async () => {
|
||||
server.use(http.post("http://localhost:8787/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();
|
||||
});
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { ApiError, apiFetch, apiFetchBlob } from "./client";
|
||||
import { sanitizeCanonicalWorkspace } from "../workspaces/drafts";
|
||||
|
||||
export type WorkspaceErrorCode =
|
||||
| "workspace_invalid" | "binding_missing" | "workspace_not_activatable"
|
||||
@@ -126,6 +127,16 @@ export interface WorkspaceApiError {
|
||||
fields?: string[];
|
||||
}
|
||||
|
||||
const conflictFields = new Set([
|
||||
"workspace.name", "workspace.description", "workspace.language",
|
||||
"dwh.database", "dwh.schema", "dwh.port", "dwh.timeout_ms", "dwh.supported_transports",
|
||||
"semantic_index.vector_store.database", "semantic_index.vector_store.schema", "semantic_index.vector_store.collection",
|
||||
"semantic_index.vector_store.dimensions", "semantic_index.vector_store.distance", "semantic_index.vector_store.port",
|
||||
"semantic_index.vector_store.timeout_ms", "semantic_index.vector_store.supported_transports",
|
||||
"semantic_index.embedding.provider", "semantic_index.embedding.model", "semantic_index.embedding.dimensions",
|
||||
"semantic_index.embedding.timeout_ms", "semantic_index.vector_writer", "llm_policy.default", "llm_policy.allowed",
|
||||
]);
|
||||
|
||||
const workspaceErrorCodes = new Set<WorkspaceErrorCode>([
|
||||
"workspace_invalid", "binding_missing", "workspace_not_activatable", "workspace_stale",
|
||||
"workspace_conflict", "git_unavailable", "git_auth_failed", "git_non_fast_forward",
|
||||
@@ -159,13 +170,16 @@ export function asWorkspaceConflict(error: unknown): WorkspaceConflict | undefin
|
||||
if (safe?.code !== "workspace_conflict") return undefined;
|
||||
const payload = object((error as ApiError).payload);
|
||||
const fields = safe.fields;
|
||||
if (!payload || !fields || !object(payload.base) || !object(payload.local) || !object(payload.remote)) return undefined;
|
||||
const base = payload && sanitizeCanonicalWorkspace(payload.base);
|
||||
const local = payload && sanitizeCanonicalWorkspace(payload.local);
|
||||
const remote = payload && sanitizeCanonicalWorkspace(payload.remote);
|
||||
if (!payload || !fields || !fields.every((field) => conflictFields.has(field)) || !base || !local || !remote) return undefined;
|
||||
return {
|
||||
code: "workspace_conflict",
|
||||
fields,
|
||||
base: payload.base as CanonicalWorkspace,
|
||||
local: payload.local as CanonicalWorkspace,
|
||||
remote: payload.remote as CanonicalWorkspace,
|
||||
base,
|
||||
local,
|
||||
remote,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user