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: {
+21 -52
View File
@@ -327,6 +327,8 @@ function persistPreStateManifest(root: string, commit: string): void {
const envExample = "# Legacy registry artifact\n";
const markdown = "# Legacy registry artifact\n";
chmodSync(join(snapshotDirectory, envName), 0o600);
chmodSync(join(snapshotDirectory, docsName), 0o600);
writeFileSync(join(snapshotDirectory, envName), envExample);
writeFileSync(join(snapshotDirectory, docsName), markdown);
active.revisions = active.revisions.map(({ state: _state, ...revision }: Record<string, unknown>) => revision);
@@ -693,25 +695,19 @@ test("resets an ahead checkout after a rejected push and retries publication", a
await expect(registry.publish(request)).resolves.toMatchObject({ id: "push-recovery" });
});
test("lists a v1 descriptor in migration-required state without rendering operational artifacts", async () => {
const legacyYaml = legacyV1Yaml();
test.each([
["v1", legacyV1Yaml()],
["v2", legacyV2Yaml()],
])("rejects a schema %s descriptor instead of activating it", async (_version, legacyYaml) => {
const remote = await fixture(legacyYaml);
const registry = new WorkspaceRegistry(config(join(remote.root, "registry"), remote.remote));
const status = await registry.bootstrap();
const [revision] = await registry.list();
expect(revision).toMatchObject({ state: "migration_required" });
await expect(registry.read("psd-clinical")).resolves.toMatchObject({
workspace: { workspace: { schema_version: 1 } },
});
expect(existsSync(join(remote.root, "registry", "snapshots", status.head!, "psd-clinical.env.example"))).toBe(false);
expect(existsSync(join(remote.root, "registry", "snapshots", status.head!, "psd-clinical.md"))).toBe(false);
await expect(registry.bootstrap()).rejects.toMatchObject({ code: "workspace_invalid" });
expect(existsSync(join(remote.root, "registry", "state", "active.json"))).toBe(false);
});
test("migrates a validated pre-state manifest and keeps its v1 workspace migration-gated", async () => {
const legacyYaml = legacyV1Yaml();
const remote = await fixture(legacyYaml);
test("migrates a validated pre-state manifest while preserving its v3 operational state", async () => {
const remote = await fixture();
const root = join(remote.root, "registry");
const firstRegistry = new WorkspaceRegistry(config(root, remote.remote));
await firstRegistry.bootstrap();
@@ -723,19 +719,20 @@ test("migrates a validated pre-state manifest and keeps its v1 workspace migrati
degraded: false,
});
await expect(restoredRegistry.list()).resolves.toMatchObject([
{ id: "psd-clinical", state: "migration_required" },
{ id: "psd-clinical", state: "operational" },
]);
const active = JSON.parse(readFileSync(join(root, "state", "active.json"), "utf8"));
const manifest = JSON.parse(readFileSync(join(root, "snapshots", remote.initialCommit, "snapshot.json"), "utf8"));
expect(active.revisions[0].state).toBe("migration_required");
expect(manifest.revisions[0].state).toBe("migration_required");
expect(Object.keys(manifest.files)).toEqual(["psd-clinical.yaml"]);
expect(active.revisions[0].state).toBe("operational");
expect(manifest.revisions[0].state).toBe("operational");
expect(Object.keys(manifest.files).sort()).toEqual([
"psd-clinical.env.example", "psd-clinical.md", "psd-clinical.yaml",
]);
});
test("finishes a pre-state active manifest migration after its snapshot was atomically updated", async () => {
const legacyYaml = legacyV1Yaml();
const remote = await fixture(legacyYaml);
test("finishes a pre-state active manifest migration after its v3 snapshot was atomically updated", async () => {
const remote = await fixture();
const root = join(remote.root, "registry");
const registry = new WorkspaceRegistry(config(root, remote.remote));
await registry.bootstrap();
@@ -743,19 +740,17 @@ test("finishes a pre-state active manifest migration after its snapshot was atom
const snapshotPath = join(root, "snapshots", remote.initialCommit, "snapshot.json");
const manifest = JSON.parse(readFileSync(snapshotPath, "utf8"));
manifest.revisions[0].state = "migration_required";
manifest.files = { "psd-clinical.yaml": manifest.files["psd-clinical.yaml"] };
manifest.revisions[0].state = "operational";
writeFileSync(snapshotPath, JSON.stringify(manifest));
const restoredRegistry = new WorkspaceRegistry(config(root, remote.remote));
await expect(restoredRegistry.list()).resolves.toMatchObject([
{ id: "psd-clinical", state: "migration_required" },
{ id: "psd-clinical", state: "operational" },
]);
});
test("rejects a corrupt pre-state manifest rather than accepting it during migration", async () => {
const legacyYaml = legacyV1Yaml();
const remote = await fixture(legacyYaml);
const remote = await fixture();
const root = join(remote.root, "registry");
const registry = new WorkspaceRegistry(config(root, remote.remote));
await registry.bootstrap();
@@ -872,32 +867,6 @@ test("a session revision lease survives stale retention scans until its manifest
expect(existsSync(registry.snapshotPath(remote.initialCommit, "psd-clinical"))).toBe(false);
});
test("does not acquire a session revision lease for a migration_required workspace", async () => {
const remote = await fixture(legacyV1Yaml());
const registry = new WorkspaceRegistry(config(join(remote.root, "registry"), remote.remote));
await registry.bootstrap();
await expect(registry.acquireSessionRevision("psd-clinical")).rejects.toMatchObject({
code: "workspace_invalid",
});
});
test("lists a schema v2 descriptor as migration_required and refuses to acquire it", async () => {
const remote = await fixture(legacyV2Yaml());
const registry = new WorkspaceRegistry(config(join(remote.root, "registry"), remote.remote));
await registry.bootstrap();
await expect(registry.list()).resolves.toMatchObject([
{ id: "psd-clinical", state: "migration_required" },
]);
await expect(registry.acquireSessionRevision("psd-clinical")).rejects.toMatchObject({
code: "workspace_invalid",
});
await expect(registry.readPinned("psd-clinical", remote.initialCommit)).rejects.toMatchObject({
code: "workspace_invalid",
});
});
test("lists operational descriptors retained after their workspace was removed from the active revision", async () => {
const remote = await fixture();
const root = join(remote.root, "registry");
+14 -20
View File
@@ -1,13 +1,12 @@
import { readFileSync } from "node:fs";
import { resolve } from "node:path";
import { parse } from "yaml";
import { expect, test } from "vitest";
import * as workspaceSchema from "../src/workspaces/schema.js";
import {
parseWorkspaceYaml,
serializeWorkspaceYaml,
validateCanonicalWorkspace,
validateWorkspaceDescriptor,
type WorkspaceDescriptor,
} from "../src/workspaces/schema.js";
export const validYaml = `workspace:
@@ -165,8 +164,8 @@ test("rejects legacy semantic connector fields and diagnostics in schema v3", ()
))).toThrow(/unrecognized key|vector_rest/i);
});
test("keeps v1 and v2 descriptors parseable but non-operational", () => {
const v1Yaml = `workspace:
test.each([
["v1", `workspace:
schema_version: 1
id: psd-clinical
name: Policlinico San Donato
@@ -192,8 +191,8 @@ semantic_index:
llm_policy:
allowed:
- zai/glm-5.2
`;
const v2Yaml = `workspace:
`],
["v2", `workspace:
schema_version: 2
id: psd-clinical
name: Policlinico San Donato
@@ -221,22 +220,17 @@ semantic_index:
llm_policy:
allowed:
- zai/glm-5.2
`;
`],
])("rejects schema %s descriptors at parser and object-validator boundaries", (_version, yaml) => {
expect(() => parseWorkspaceYaml(yaml)).toThrow(/schema_version|invalid literal|3/i);
expect(() => validateWorkspaceDescriptor(parse(yaml))).toThrow(/schema_version|invalid literal|3/i);
});
const v1 = parseWorkspaceYaml(v1Yaml);
const v2 = parseWorkspaceYaml(v2Yaml);
const isOperationalWorkspace = (workspaceSchema as { isOperationalWorkspace?: unknown }).isOperationalWorkspace;
test("does not expose the redundant canonical validator or v1 migration", () => {
const legacyExports = workspaceSchema as Record<string, unknown>;
expect(v1.workspace.schema_version).toBe(1);
expect(v2.workspace.schema_version).toBe(2);
expect(validateCanonicalWorkspace(parseWorkspaceYaml(validYaml))).toMatchObject({
workspace: { schema_version: 3 },
});
expect(() => validateCanonicalWorkspace(v1)).toThrow(/schema version 3|migration/i);
expect(() => validateCanonicalWorkspace(v2)).toThrow(/schema version 3|migration/i);
expect(isOperationalWorkspace).toBeTypeOf("function");
expect((isOperationalWorkspace as (workspace: WorkspaceDescriptor) => boolean)(v1)).toBe(false);
expect((isOperationalWorkspace as (workspace: WorkspaceDescriptor) => boolean)(v2)).toBe(false);
expect(legacyExports.validateCanonicalWorkspace).toBeUndefined();
expect(legacyExports.migrateWorkspaceV1ToV2).toBeUndefined();
});
test("constructs diagnostic URLs only when the resolved URL remains on the service origin", () => {