refactor: make workspace descriptors schema v3 only
This commit is contained in:
@@ -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: {
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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", () => {
|
||||
|
||||
Reference in New Issue
Block a user