fix: gate workspace diagnostic migration
This commit is contained in:
@@ -11,7 +11,7 @@ import { WorkspaceRegistry } from "../src/workspaces/registry.js";
|
||||
import type { WorkspaceRegistryConfig } from "../src/workspaces/types.js";
|
||||
|
||||
const validYaml = `workspace:
|
||||
schema_version: 1
|
||||
schema_version: 2
|
||||
id: psd-clinical
|
||||
name: Policlinico San Donato
|
||||
language: it
|
||||
@@ -48,7 +48,7 @@ async function git(cwd: string, args: string[]): Promise<void> {
|
||||
await runFile("git", args, { cwd });
|
||||
}
|
||||
|
||||
async function fixture(): Promise<{
|
||||
async function fixture(workspaceSource = validYaml): Promise<{
|
||||
root: string; remote: string; source: string; initialCommit: string;
|
||||
}> {
|
||||
const root = mkdtempSync(join(tmpdir(), "thoth-workspace-registry-"));
|
||||
@@ -61,7 +61,7 @@ async function fixture(): Promise<{
|
||||
await git(source, ["config", "user.name", "Workspace Registry Test"]);
|
||||
await git(source, ["config", "user.email", "workspace-registry@example.invalid"]);
|
||||
mkdirSync(join(source, "workspaces"));
|
||||
writeFileSync(join(source, "workspaces", "psd-clinical.yaml"), validYaml);
|
||||
writeFileSync(join(source, "workspaces", "psd-clinical.yaml"), workspaceSource);
|
||||
await git(source, ["add", "workspaces/psd-clinical.yaml"]);
|
||||
await git(source, ["commit", "-m", "Initial workspace"]);
|
||||
await git(source, ["remote", "add", "origin", remote]);
|
||||
@@ -104,6 +104,25 @@ test("bootstraps a checkout and activates a validated immutable snapshot", async
|
||||
});
|
||||
});
|
||||
|
||||
test("lists a v1 descriptor in migration-required state without rendering operational artifacts", async () => {
|
||||
const legacyYaml = validYaml.replace(
|
||||
" database: postgres\n schema: vectors\n",
|
||||
"",
|
||||
).replace("schema_version: 2", "schema_version: 1");
|
||||
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);
|
||||
});
|
||||
|
||||
test("keeps the last valid snapshot when a pulled commit has invalid YAML", async () => {
|
||||
const remote = await fixture();
|
||||
const registry = new WorkspaceRegistry(config(join(remote.root, "registry"), remote.remote));
|
||||
|
||||
@@ -4,7 +4,7 @@ import { renderRuntimeConfig, type RuntimeBindings, type RuntimePaths } from "..
|
||||
import { parseWorkspaceYaml } from "../src/workspaces/schema.js";
|
||||
|
||||
const workspace = parseWorkspaceYaml(`workspace:
|
||||
schema_version: 1
|
||||
schema_version: 2
|
||||
id: psd-clinical
|
||||
name: Policlinico San Donato
|
||||
language: it
|
||||
@@ -34,6 +34,30 @@ const paths: RuntimePaths = {
|
||||
artifacts: "/data/workspaces/psd-clinical/artifacts",
|
||||
indexes: "/data/workspaces/psd-clinical/indexes",
|
||||
};
|
||||
const legacyWorkspace = parseWorkspaceYaml(`workspace:
|
||||
schema_version: 1
|
||||
id: psd-clinical
|
||||
name: Policlinico San Donato
|
||||
language: it
|
||||
dwh:
|
||||
engine: postgres
|
||||
database: postgres
|
||||
schema: datawarehouse
|
||||
supported_transports: [postgres_direct]
|
||||
semantic_index:
|
||||
vector_store:
|
||||
engine: pgvector
|
||||
collection: clinical_documents
|
||||
dimensions: 768
|
||||
distance: cosine
|
||||
supported_transports: [pgvector_direct]
|
||||
embedding:
|
||||
provider: ollama_compatible
|
||||
model: nomic-embed-text-v2-moe
|
||||
dimensions: 768
|
||||
llm_policy:
|
||||
allowed: [zai/glm-5.2]
|
||||
`);
|
||||
|
||||
const directBindings: RuntimeBindings = {
|
||||
dwh: {
|
||||
@@ -99,6 +123,10 @@ test("renders a direct PostgreSQL binding to the legacy harness shape", () => {
|
||||
expect(yaml).toContain("schema: datawarehouse");
|
||||
});
|
||||
|
||||
test("refuses to render a v1 descriptor until an explicit migration creates v2", () => {
|
||||
expect(() => renderRuntimeConfig(legacyWorkspace, directBindings, paths)).toThrow(/migrat/i);
|
||||
});
|
||||
|
||||
test("omits direct TLS fields when binding validation did not retain a file path", () => {
|
||||
const dwhValues = { ...directBindings.dwh.values };
|
||||
const vectorValues = { ...directBindings.vector.values };
|
||||
|
||||
@@ -6,7 +6,7 @@ import { resolveBinding } from "../src/workspaces/bindings.js";
|
||||
import { parseWorkspaceYaml } from "../src/workspaces/schema.js";
|
||||
|
||||
const workspace = parseWorkspaceYaml(`workspace:
|
||||
schema_version: 1
|
||||
schema_version: 2
|
||||
id: psd-clinical
|
||||
name: Policlinico San Donato
|
||||
language: it
|
||||
@@ -121,3 +121,20 @@ test("rejects a selected transport that the canonical workspace does not support
|
||||
missing: ["THT_WS_PSD_CLINICAL_DWH_TRANSPORT"],
|
||||
});
|
||||
});
|
||||
|
||||
test("never treats a vector reader credential as the optional writer binding", () => {
|
||||
const readerKey = secretPath("vector-reader-key");
|
||||
const writerWorkspace = {
|
||||
...workspace,
|
||||
semantic_index: { ...workspace.semantic_index, vector_writer: {} },
|
||||
};
|
||||
const resolveWriter = () => resolveBinding(writerWorkspace, "VECTOR_WRITER" as never, {
|
||||
THT_WS_PSD_CLINICAL_VECTOR_API_KEY_FILE: readerKey.path,
|
||||
}, [readerKey.root]);
|
||||
|
||||
expect(resolveWriter).not.toThrow();
|
||||
expect(resolveWriter()).toMatchObject({
|
||||
missing: ["THT_WS_PSD_CLINICAL_VECTOR_WRITER_API_KEY_FILE"],
|
||||
values: {},
|
||||
});
|
||||
});
|
||||
|
||||
@@ -5,7 +5,7 @@ import { type CanonicalWorkspace, parseWorkspaceYaml } from "../src/workspaces/s
|
||||
import { renderRuntimeConfig, type RuntimeBindings } from "../src/workspaces/runtime-renderer.js";
|
||||
|
||||
const validWorkspace = parseWorkspaceYaml(`workspace:
|
||||
schema_version: 1
|
||||
schema_version: 2
|
||||
id: psd-clinical
|
||||
name: Policlinico San Donato
|
||||
language: it
|
||||
@@ -74,7 +74,7 @@ test("renders English UI headings and workspace-language Italian prose", () => {
|
||||
|
||||
test("renders the vector store identity and creates writer credentials only when declared", () => {
|
||||
const writerWorkspace = parseWorkspaceYaml(`workspace:
|
||||
schema_version: 1
|
||||
schema_version: 2
|
||||
id: psd-clinical
|
||||
name: Policlinico San Donato
|
||||
language: it
|
||||
@@ -82,7 +82,7 @@ dwh:
|
||||
engine: postgres
|
||||
database: warehouse
|
||||
schema: datawarehouse
|
||||
supported_transports: [postgres_direct]
|
||||
supported_transports: [postgres_direct, rest_api]
|
||||
semantic_index:
|
||||
vector_store:
|
||||
engine: pgvector
|
||||
@@ -91,7 +91,7 @@ semantic_index:
|
||||
collection: clinical_documents
|
||||
dimensions: 768
|
||||
distance: cosine
|
||||
supported_transports: [pgvector_direct]
|
||||
supported_transports: [pgvector_direct, rest_api]
|
||||
vector_writer: {}
|
||||
embedding:
|
||||
provider: ollama_compatible
|
||||
|
||||
@@ -8,7 +8,7 @@ import type { RuntimeBindings } from "../src/workspaces/runtime-renderer.js";
|
||||
import { parseWorkspaceYaml } from "../src/workspaces/schema.js";
|
||||
|
||||
const workspace = parseWorkspaceYaml(`workspace:
|
||||
schema_version: 1
|
||||
schema_version: 2
|
||||
id: psd-clinical
|
||||
name: Policlinico San Donato
|
||||
language: it
|
||||
|
||||
@@ -8,7 +8,7 @@ import { GitWorkspaceRepository, WorkspaceRepositoryLock } from "../src/workspac
|
||||
import type { WorkspaceRegistryConfig } from "../src/workspaces/types.js";
|
||||
|
||||
const validYaml = `workspace:
|
||||
schema_version: 1
|
||||
schema_version: 2
|
||||
id: psd-clinical
|
||||
name: Policlinico San Donato
|
||||
language: it
|
||||
|
||||
@@ -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", () => {
|
||||
|
||||
Reference in New Issue
Block a user