fix: gate workspace diagnostic migration

This commit is contained in:
2026-08-03 23:30:15 +02:00
parent c5685f4962
commit 450d7ab07f
14 changed files with 430 additions and 145 deletions
+79 -2
View File
@@ -1,8 +1,9 @@
import { expect, test } from "vitest";
import { parseWorkspaceYaml, serializeWorkspaceYaml } from "../src/workspaces/schema.js";
import * as workspaceSchema from "../src/workspaces/schema.js";
import { parseWorkspaceYaml, serializeWorkspaceYaml, validateCanonicalWorkspace } from "../src/workspaces/schema.js";
export const validYaml = `workspace:
schema_version: 1
schema_version: 2
id: psd-clinical
name: Policlinico San Donato
description: Clinical data warehouse workspace
@@ -119,6 +120,82 @@ test("requires explicit vector database and schema identities with strict diagno
.toThrow(/method/i);
expect(() => parseWorkspaceYaml(diagnosticWorkspace.replace(" distance: distance", " distance: distance\n extra: ignored")))
.toThrow(/unrecognized key/i);
for (const unsafePath of [
"//diagnostic.invalid/rpc", "'/\\\\diagnostic'", "'/rpc\\\\diagnostic'", "'/rpc/%5Cdiagnostic'", "'/rpc/\u0001'",
]) {
expect(() => parseWorkspaceYaml(diagnosticWorkspace.replace("path: /rpc/ping", `path: ${unsafePath}`)))
.toThrow(/origin-relative|path/i);
}
expect(() => parseWorkspaceYaml(diagnosticWorkspace.replace("auth: bearer", "auth: basic")))
.toThrow(/auth/i);
expect(() => parseWorkspaceYaml(diagnosticWorkspace.replace(" schema: schema", " schema: schema\n status: status")))
.toThrow(/unrecognized key/i);
expect(() => parseWorkspaceYaml(diagnosticWorkspace.replace(" schema: schema", " schema: bad field")))
.toThrow(/response field/i);
});
test("keeps v1 descriptors readable but requires explicit migration before v2 operations", () => {
const v1WithoutVectorIdentity = validYaml.replace("schema_version: 2", "schema_version: 1").replace(
" database: postgres\n schema: vectors\n", "",
);
expect(() => parseWorkspaceYaml(v1WithoutVectorIdentity)).not.toThrow();
expect(() => parseWorkspaceYaml(validYaml)).not.toThrow();
const v1 = parseWorkspaceYaml(v1WithoutVectorIdentity);
const v2 = parseWorkspaceYaml(validYaml);
expect(v1.workspace.schema_version).toBe(1);
expect(() => validateCanonicalWorkspace(v1)).toThrow(/migrat/i);
expect(v2.workspace.schema_version).toBe(2);
const migrate = (workspaceSchema as { migrateWorkspaceV1ToV2?: unknown }).migrateWorkspaceV1ToV2;
expect(migrate).toBeTypeOf("function");
const migrated = (migrate as (workspace: typeof v1, identity: { database: string; schema: string }) => unknown)(v1, {
database: "vector_database",
schema: "vectors",
});
expect(validateCanonicalWorkspace(migrated)).toMatchObject({
workspace: { schema_version: 2 },
semantic_index: { vector_store: { database: "vector_database", schema: "vectors" } },
});
});
test("constructs diagnostic URLs only when the resolved URL remains on the service origin", () => {
const resolveDiagnosticUrl = (workspaceSchema as { resolveDiagnosticUrl?: unknown }).resolveDiagnosticUrl;
expect(resolveDiagnosticUrl).toBeTypeOf("function");
expect((resolveDiagnosticUrl as (baseUrl: string, path: string) => URL)("https://service.example/base", "/rpc/ping"))
.toMatchObject({ href: "https://service.example/rpc/ping" });
expect(() => (resolveDiagnosticUrl as (baseUrl: string, path: string) => URL)(
"https://service.example/base", "//diagnostic.invalid/rpc",
)).toThrow(/origin/i);
});
test("rejects REST diagnostic declarations without their matching connector transport", () => {
const diagnostics = `diagnostics:
dwh_rest:
method: POST
path: /rpc/ping
auth: bearer
response:
database: database
schema: schema
vector_rest:
metadata:
method: GET
path: /metadata
auth: bearer
response:
collection: collection
dimensions: dimensions
distance: distance
llm_policy:
`;
const declared = validYaml.replace("llm_policy:\n", diagnostics);
expect(() => parseWorkspaceYaml(declared.replace(" - rest_api\n", ""))).toThrow(/dwh_rest/i);
expect(() => parseWorkspaceYaml(declared.replace(" - rest_api\n", " - rest_api\n", 1).replace(
" - rest_api\n", "",
))).toThrow(/vector_rest/i);
});
test("serializes canonical YAML that parses back to the same workspace", () => {