refactor: make workspace descriptors schema v3 only

This commit is contained in:
2026-08-10 20:21:20 +02:00
parent 5d016ce635
commit 512b0261b1
12 changed files with 187 additions and 295 deletions
+43 -15
View File
@@ -15,8 +15,8 @@ import { createProductionWorkspaceDiagnoser } from "../src/workspaces/diagnostic
import { WorkspaceRegistryError } from "../src/workspaces/git-repository.js";
import { WorkspaceRegistry, type WorkspaceRevision } from "../src/workspaces/registry.js";
import {
parseWorkspaceYaml, renderWorkspaceDocs, serializeWorkspaceYaml, validateCanonicalWorkspace,
type CanonicalWorkspace, type WorkspaceV2,
parseWorkspaceYaml, renderWorkspaceDocs, serializeWorkspaceYaml, validateWorkspaceDescriptor,
type CanonicalWorkspace,
} from "../src/workspaces/schema.js";
const workspace: CanonicalWorkspace = {
@@ -49,7 +49,7 @@ const workspace: CanonicalWorkspace = {
llm_policy: { allowed: ["zai/glm-5.2"] },
};
const workspaceV2: WorkspaceV2 = {
const workspaceV2 = {
workspace: {
schema_version: 2,
id: "psd-clinical",
@@ -104,6 +104,11 @@ const workspaceV2: WorkspaceV2 = {
llm_policy: { allowed: ["zai/glm-5.2"] },
};
const workspaceV1 = {
...workspaceV2,
workspace: { ...workspaceV2.workspace, schema_version: 1 as const },
};
const revision: WorkspaceRevision = {
id: workspace.workspace.id,
commit: "a".repeat(40),
@@ -248,6 +253,31 @@ test("validates a canonical workspace and runs the injected installation diagnos
expect(diagnose).not.toHaveBeenCalled();
});
test.each([
["v1", workspaceV1],
["v2", workspaceV2],
])("rejects schema %s at validate and publish boundaries with a sanitized error", async (_version, legacy) => {
const registry = registryFake();
const app = appFor(registry);
for (const request of [
{ url: "/workspaces/validate", payload: { workspace: legacy } },
{
url: "/workspaces/publish",
payload: { action: "create", workspace: legacy, baseCommit: revision.commit },
},
]) {
const response = await app.inject({ method: "POST", ...request });
expect(response.statusCode).toBe(400);
expect(response.json()).toEqual({
code: "workspace_invalid",
message: "Workspace request or bundle is invalid.",
});
expect(response.body).not.toMatch(/migration_required|schema version/i);
}
expect(registry.publish).not.toHaveBeenCalled();
});
test("rejects a migration-required v2 workspace before resolving semantic diagnostics", async () => {
const diagnose = vi.fn(async () => ({
activatable: false,
@@ -342,12 +372,12 @@ test("returns a 409 field conflict instead of overwriting a changed workspace",
const conflict = Object.assign(
new WorkspaceRegistryError("workspace_conflict", "Workspace has changed"),
{
fields: ["semantic_index.embedding.model"],
fields: ["workspace.description"],
expected: { commit: "c".repeat(40), blob: "d".repeat(40) },
actual: { commit: revision.commit, blob: revision.blob },
base: workspace,
local: { ...workspace, semantic_index: { ...workspace.semantic_index, embedding: { ...workspace.semantic_index.embedding, model: "local/model" } } },
remote: { ...workspace, semantic_index: { ...workspace.semantic_index, embedding: { ...workspace.semantic_index.embedding, model: "remote/model" } } },
local: { ...workspace, workspace: { ...workspace.workspace, description: "Local description" } },
remote: { ...workspace, workspace: { ...workspace.workspace, description: "Remote description" } },
},
);
const registry = registryFake({ publish: vi.fn(async () => { throw conflict; }) });
@@ -364,14 +394,12 @@ test("returns a 409 field conflict instead of overwriting a changed workspace",
expect(res.statusCode).toBe(409);
expect(res.json()).toMatchObject({
code: "workspace_conflict",
fields: ["semantic_index.embedding.model"],
fields: ["workspace.description"],
expected: { commit: "c".repeat(40), blob: "d".repeat(40) },
actual: { commit: revision.commit, blob: revision.blob },
base: workspace,
remote: expect.objectContaining({
semantic_index: expect.objectContaining({
embedding: expect.objectContaining({ model: "remote/model" }),
}),
workspace: expect.objectContaining({ description: "Remote description" }),
}),
});
});
@@ -450,7 +478,7 @@ function withEvidence(
source: Partial<CanonicalWorkspace["evidence"]["source"]> & { type: "filesystem" | "http" | "s3" },
changes: Partial<CanonicalWorkspace["evidence"]["policy"]> = {},
): CanonicalWorkspace {
return validateCanonicalWorkspace({
return validateWorkspaceDescriptor({
...workspace,
evidence: { source, policy: changes },
});
@@ -587,7 +615,7 @@ test.each([
test("real publish create/update, pull, list, and read preserve a complete Evidence descriptor", async () => {
const fixture = await createRealRouteFixture(httpEvidenceWorkspace);
const status = await fixture.app.inject({ method: "GET", url: "/workspace-registry/status" });
const created = validateCanonicalWorkspace({
const created = validateWorkspaceDescriptor({
...httpEvidenceWorkspace,
workspace: { ...httpEvidenceWorkspace.workspace, id: "research-clinical", name: "Research Clinical" },
semantic_index: {
@@ -600,7 +628,7 @@ test("real publish create/update, pull, list, and read preserve a complete Evide
payload: { action: "create", workspace: created, baseCommit: status.json().head },
});
const createdRevision = create.json().revision as WorkspaceRevision;
const updated = validateCanonicalWorkspace({
const updated = validateWorkspaceDescriptor({
...created,
evidence: {
...created.evidence,
@@ -617,7 +645,7 @@ test("real publish create/update, pull, list, and read preserve a complete Evide
});
expect(update.statusCode).toBe(200);
await realGit(fixture.author, ["pull", "--ff-only", "origin", "main"]);
const remotelyEdited = validateCanonicalWorkspace({
const remotelyEdited = validateWorkspaceDescriptor({
...updated,
evidence: {
...updated.evidence,
@@ -742,7 +770,7 @@ test("real publish and pull fail safely when the contextual Evidence Git tree is
const fixture = await createRealRouteFixture();
await fixture.registry.bootstrap();
const current = await fixture.registry.read("psd-clinical");
const missing = validateCanonicalWorkspace({
const missing = validateWorkspaceDescriptor({
...workspace,
workspace: { ...workspace.workspace, id: "missing-evidence", name: "Missing Evidence" },
semantic_index: {