feat: unify installation model catalog
This commit is contained in:
@@ -5,14 +5,12 @@ import { createLocalAuthFixture } from "./auth-test-fixtures.js";
|
||||
function fakeService(): PiManagementService {
|
||||
return {
|
||||
status: vi.fn(async () => ({ ready: true })),
|
||||
options: vi.fn(async () => ({ providers: [], models: [], reasoning: [], checkedAt: "2026-08-17T00:00:00.000Z" })),
|
||||
configure: vi.fn(async (value) => ({ ...value, updatedAt: "2026-08-17T00:00:00.000Z" })),
|
||||
test: vi.fn(async () => ({ ready: true, checkedAt: "2026-08-17T00:00:00.000Z" })),
|
||||
logs: vi.fn(async () => ({ lines: [] })),
|
||||
};
|
||||
}
|
||||
|
||||
test("a local HTTPS cookie session authorizes Pi writes through an untrusted internal HTTP hop", async () => {
|
||||
test("a local HTTPS cookie session authorizes the Pi smoke check through an untrusted internal HTTP hop", async () => {
|
||||
const service = fakeService();
|
||||
const fixture = await createLocalAuthFixture(
|
||||
{ piManagement: service },
|
||||
@@ -25,31 +23,21 @@ test("a local HTTPS cookie session authorizes Pi writes through an untrusted int
|
||||
expect(fixture.publicUrl).toBe("HTTPS://thothii.example.test");
|
||||
|
||||
const proxyHeaders = fixture.sessionHeaders({ host: "127.0.0.1:8080" });
|
||||
const configured = await fixture.app.inject({
|
||||
method: "PUT",
|
||||
url: "/pi-management/config",
|
||||
headers: proxyHeaders,
|
||||
payload: { provider: "zai", model: "glm-5.2", reasoning: "high" },
|
||||
});
|
||||
const smoke = await fixture.app.inject({
|
||||
method: "POST",
|
||||
url: "/pi-management/test",
|
||||
headers: proxyHeaders,
|
||||
});
|
||||
|
||||
expect(configured.statusCode).toBe(200);
|
||||
expect(smoke.statusCode).toBe(200);
|
||||
expect(service.configure).toHaveBeenCalledTimes(1);
|
||||
expect(service.test).toHaveBeenCalledTimes(1);
|
||||
|
||||
fixture.resetDownstreamHits();
|
||||
vi.mocked(service.configure).mockClear();
|
||||
vi.mocked(service.test).mockClear();
|
||||
const wrongOrigin = await fixture.app.inject({
|
||||
method: "PUT",
|
||||
url: "/pi-management/config",
|
||||
method: "POST",
|
||||
url: "/pi-management/test",
|
||||
headers: fixture.sessionHeaders({ host: "127.0.0.1:8080", origin: "https://evil.example" }),
|
||||
payload: { provider: "zai", model: "glm-5.2", reasoning: "high" },
|
||||
});
|
||||
const wrongCsrf = await fixture.app.inject({
|
||||
method: "POST",
|
||||
@@ -62,7 +50,6 @@ test("a local HTTPS cookie session authorizes Pi writes through an untrusted int
|
||||
expect(response.json()).toEqual({ code: "csrf_failed", error: "Request origin validation failed" });
|
||||
}
|
||||
expect(fixture.downstreamHits()).toBe(0);
|
||||
expect(service.configure).not.toHaveBeenCalled();
|
||||
expect(service.test).not.toHaveBeenCalled();
|
||||
} finally {
|
||||
await fixture.close();
|
||||
|
||||
@@ -19,16 +19,11 @@ afterEach(() => {
|
||||
});
|
||||
|
||||
const workspace: WorkspaceDescriptor = {
|
||||
workspace: { schema_version: 3, id: "psd-clinical", name: "Policlinico San Donato", language: "it" },
|
||||
workspace: { schema_version: 4, id: "psd-clinical", name: "Policlinico San Donato", language: "it" },
|
||||
dwh: {
|
||||
engine: "postgres", database: "warehouse", schema: "datawarehouse", port: 5432,
|
||||
supported_transports: ["postgres_direct", "rest_api"],
|
||||
},
|
||||
semantic_index: {
|
||||
vector_store: { engine: "qdrant", collection: "psd", dimensions: 1024, distance: "cosine" },
|
||||
embedding: { provider: "ollama_internal", model: "qwen3-embedding:0.6b", dimensions: 1024 },
|
||||
},
|
||||
llm_policy: { allowed: ["zai/glm-5.2"] },
|
||||
diagnostics: { dwh_rest: { method: "GET", path: "/health", auth: "bearer", response: { database: "database", schema: "schema" } } },
|
||||
};
|
||||
const revision: WorkspaceRevision = { id: "psd-clinical", commit: "a".repeat(40), blob: "b".repeat(40), snapshotPath: "/tmp/psd.yaml" };
|
||||
|
||||
@@ -24,7 +24,7 @@ import type { WorkspaceDescriptor } from "../src/workspaces/schema.js";
|
||||
|
||||
const workspace: WorkspaceDescriptor = {
|
||||
workspace: {
|
||||
schema_version: 3,
|
||||
schema_version: 4,
|
||||
id: "psd-clinical",
|
||||
name: "Policlinico San Donato",
|
||||
language: "it",
|
||||
@@ -36,11 +36,6 @@ const workspace: WorkspaceDescriptor = {
|
||||
port: 5432,
|
||||
supported_transports: ["postgres_direct"],
|
||||
},
|
||||
semantic_index: {
|
||||
vector_store: { engine: "qdrant", collection: "psd", dimensions: 1024, distance: "cosine" },
|
||||
embedding: { provider: "ollama_internal", model: "qwen3-embedding:0.6b", dimensions: 1024 },
|
||||
},
|
||||
llm_policy: { allowed: ["zai/glm-5.2"] },
|
||||
};
|
||||
const revision: WorkspaceRevision = {
|
||||
id: "psd-clinical",
|
||||
@@ -49,7 +44,7 @@ const revision: WorkspaceRevision = {
|
||||
snapshotPath: "/tmp/psd.yaml",
|
||||
};
|
||||
const configuredModel: ResolvedMetadataGenerationModel = {
|
||||
id: "openai-mini",
|
||||
id: "openai/gpt-4.1-mini",
|
||||
provider: "openai",
|
||||
model: "gpt-4.1-mini",
|
||||
apiKeyEnv: "OPENAI_API_KEY",
|
||||
@@ -705,7 +700,7 @@ test("generates one selected Catalog Column from a single JSON code fence", asyn
|
||||
expect(completionRequest.messages[0]?.content).toContain('{"results":[');
|
||||
expect(completionRequest.messages[1]?.content).toContain(`"targetId":"${column.id}"`);
|
||||
expect(completionRequest.messages[1]?.content).not.toMatch(/source rows|samples|example values/i);
|
||||
expect(start.body).not.toMatch(/test-provider-secret|gpt-4\.1|openai\/gpt|Catalog metadata/);
|
||||
expect(start.body).not.toMatch(/test-provider-secret|Catalog metadata/);
|
||||
|
||||
resolveCompletion(`\`\`\`json\n${JSON.stringify({
|
||||
results: [{
|
||||
@@ -2909,7 +2904,7 @@ test("validates selected targets and resolves every requested target before laun
|
||||
const unknownModel = await app.inject({
|
||||
method: "POST",
|
||||
url: `/catalog/databases/${database.id}/description-generation-runs`,
|
||||
payload: { modelId: "unknown-model", scope: "selected_columns", targetIds: [column.id] },
|
||||
payload: { modelId: "openai/unknown-model", scope: "selected_columns", targetIds: [column.id] },
|
||||
});
|
||||
expect(unknownModel.statusCode).toBe(409);
|
||||
expect(unknownModel.json().code).toBe("metadata_generation_model_unavailable");
|
||||
|
||||
@@ -14,6 +14,7 @@ import { up as upDescriptionGeneration } from "../src/catalog/migrations/005_des
|
||||
import { up as upSensitiveDataFlag } from "../src/catalog/migrations/006_sensitive_data_flag.js";
|
||||
import { up as upSensitiveSuggestionRuns } from "../src/catalog/migrations/007_sensitive_data_suggestion_runs.js";
|
||||
import { up as upAiTokenUsage } from "../src/catalog/migrations/009_ai_token_usage.js";
|
||||
import { up as upCanonicalModelIds } from "../src/catalog/migrations/010_canonical_model_ids.js";
|
||||
import { KyselyCatalogRepository, type CatalogDatabase } from "../src/catalog/repository.js";
|
||||
import { loadConfig } from "../src/config.js";
|
||||
import type { WorkspaceRegistry } from "../src/workspaces/registry.js";
|
||||
@@ -50,6 +51,7 @@ test.skipIf(!dockerAvailable)("Fastify persists Description Generation success a
|
||||
await upDescriptionGeneration(db);
|
||||
await upSensitiveSuggestionRuns(db);
|
||||
await upAiTokenUsage(db);
|
||||
await upCanonicalModelIds(db);
|
||||
const repository = new KyselyCatalogRepository(db);
|
||||
const database = await repository.create({
|
||||
workspaceId: "psd-clinical",
|
||||
@@ -158,9 +160,9 @@ test.skipIf(!dockerAvailable)("Fastify persists Description Generation success a
|
||||
}),
|
||||
};
|
||||
const models: MetadataGenerationModels = {
|
||||
catalog: () => ({ models: [{ id: "openai-mini", label: "OpenAI Mini" }], default: "openai-mini" }),
|
||||
catalog: () => ({ models: [{ id: "openai/gpt-4.1-mini", label: "OpenAI Mini" }], default: "openai/gpt-4.1-mini" }),
|
||||
resolve: () => ({
|
||||
id: "openai-mini",
|
||||
id: "openai/gpt-4.1-mini",
|
||||
provider: "openai",
|
||||
model: "gpt-4.1-mini",
|
||||
apiKeyEnv: "OPENAI_API_KEY",
|
||||
@@ -205,12 +207,12 @@ test.skipIf(!dockerAvailable)("Fastify persists Description Generation success a
|
||||
method: "POST",
|
||||
url: `/catalog/databases/${database.id}/description-generation-runs`,
|
||||
payload: {
|
||||
modelId: "openai-mini",
|
||||
modelId: "openai/gpt-4.1-mini",
|
||||
scope: "selected_columns",
|
||||
targetIds: [status.id, birthDate.id],
|
||||
},
|
||||
});
|
||||
expect(successfulStart.statusCode).toBe(202);
|
||||
expect(successfulStart.statusCode, successfulStart.body).toBe(202);
|
||||
expect(await terminalRun(app, successfulStart.json().id)).toMatchObject({
|
||||
status: "completed",
|
||||
total: 2,
|
||||
@@ -233,7 +235,7 @@ test.skipIf(!dockerAvailable)("Fastify persists Description Generation success a
|
||||
const tableStart = await app.inject({
|
||||
method: "POST",
|
||||
url: `/catalog/databases/${database.id}/description-generation-runs`,
|
||||
payload: { modelId: "openai-mini", scope: "selected_tables", targetIds: [table.id] },
|
||||
payload: { modelId: "openai/gpt-4.1-mini", scope: "selected_tables", targetIds: [table.id] },
|
||||
});
|
||||
expect(tableStart.statusCode).toBe(202);
|
||||
expect(await terminalRun(app, tableStart.json().id)).toMatchObject({
|
||||
@@ -253,7 +255,7 @@ test.skipIf(!dockerAvailable)("Fastify persists Description Generation success a
|
||||
const failedStart = await app.inject({
|
||||
method: "POST",
|
||||
url: `/catalog/databases/${database.id}/description-generation-runs`,
|
||||
payload: { modelId: "openai-mini", scope: "selected_columns", targetIds: [status.id] },
|
||||
payload: { modelId: "openai/gpt-4.1-mini", scope: "selected_columns", targetIds: [status.id] },
|
||||
});
|
||||
expect(failedStart.statusCode).toBe(202);
|
||||
const failedRun = await terminalRun(app, failedStart.json().id);
|
||||
@@ -283,7 +285,7 @@ test.skipIf(!dockerAvailable)("Fastify persists Description Generation success a
|
||||
const allStart = await app.inject({
|
||||
method: "POST",
|
||||
url: `/catalog/databases/${database.id}/description-generation-runs`,
|
||||
payload: { modelId: "openai-mini", scope: "all" },
|
||||
payload: { modelId: "openai/gpt-4.1-mini", scope: "all" },
|
||||
});
|
||||
expect(allStart.statusCode).toBe(202);
|
||||
const allRun = await terminalRun(app, allStart.json().id);
|
||||
@@ -327,7 +329,7 @@ test.skipIf(!dockerAvailable)("Fastify persists Description Generation success a
|
||||
const missingStart = await app.inject({
|
||||
method: "POST",
|
||||
url: `/catalog/databases/${database.id}/description-generation-runs`,
|
||||
payload: { modelId: "openai-mini", scope: "missing" },
|
||||
payload: { modelId: "openai/gpt-4.1-mini", scope: "missing" },
|
||||
});
|
||||
expect(missingStart.statusCode).toBe(202);
|
||||
expect(await terminalRun(app, missingStart.json().id)).toMatchObject({
|
||||
|
||||
@@ -14,6 +14,7 @@ import { up as upSensitiveDataFlag } from "../src/catalog/migrations/006_sensiti
|
||||
import { up as upSensitiveSuggestionRuns } from "../src/catalog/migrations/007_sensitive_data_suggestion_runs.js";
|
||||
import { up as upLogicalRelationships } from "../src/catalog/migrations/008_catalog_logical_relationships.js";
|
||||
import { up as upAiTokenUsage } from "../src/catalog/migrations/009_ai_token_usage.js";
|
||||
import { up as upCanonicalModelIds } from "../src/catalog/migrations/010_canonical_model_ids.js";
|
||||
|
||||
const dockerAvailable = spawnSync("docker", ["info"], { stdio: "ignore" }).status === 0;
|
||||
|
||||
@@ -32,6 +33,7 @@ test.skipIf(!dockerAvailable)("PostgreSQL migration enforces one database per wo
|
||||
await upDescriptionGeneration(db);
|
||||
await upSensitiveSuggestionRuns(db);
|
||||
await upAiTokenUsage(db);
|
||||
await upCanonicalModelIds(db);
|
||||
await sql`CREATE ROLE thothii_catalog_runtime`.execute(db);
|
||||
await upRuntimeSequencePrivileges(db);
|
||||
const sequencePrivilege = await sql<{ allowed: boolean }>`
|
||||
@@ -391,6 +393,7 @@ test.skipIf(!dockerAvailable)("PostgreSQL repository persists description and se
|
||||
await upDescriptionGeneration(db);
|
||||
await upSensitiveSuggestionRuns(db);
|
||||
await upAiTokenUsage(db);
|
||||
await upCanonicalModelIds(db);
|
||||
const repository = new KyselyCatalogRepository(db);
|
||||
const firstDatabase = await repository.create({
|
||||
workspaceId: "generation-one",
|
||||
@@ -428,14 +431,14 @@ test.skipIf(!dockerAvailable)("PostgreSQL repository persists description and se
|
||||
const run = await repository.createDescriptionGenerationRun(
|
||||
firstDatabase.id,
|
||||
"selected_columns",
|
||||
"openai-mini",
|
||||
"openai/gpt-4.1-mini",
|
||||
"it",
|
||||
1,
|
||||
);
|
||||
expect(run).toMatchObject({
|
||||
databaseId: firstDatabase.id,
|
||||
scope: "selected_columns",
|
||||
modelId: "openai-mini",
|
||||
modelId: "openai/gpt-4.1-mini",
|
||||
language: "it",
|
||||
status: "queued",
|
||||
total: 1,
|
||||
@@ -450,7 +453,7 @@ test.skipIf(!dockerAvailable)("PostgreSQL repository persists description and se
|
||||
await expect(repository.createDescriptionGenerationRun(
|
||||
secondDatabase.id,
|
||||
"selected_columns",
|
||||
"openai-mini",
|
||||
"openai/gpt-4.1-mini",
|
||||
"en",
|
||||
1,
|
||||
)).rejects.toThrow("A description generation run is already active");
|
||||
@@ -461,7 +464,7 @@ test.skipIf(!dockerAvailable)("PostgreSQL repository persists description and se
|
||||
await expect(repository.createDescriptionGenerationRun(
|
||||
secondDatabase.id,
|
||||
"selected_columns",
|
||||
"openai-mini",
|
||||
"openai/gpt-4.1-mini",
|
||||
"en",
|
||||
1,
|
||||
)).rejects.toThrow("A description generation run is already active");
|
||||
@@ -505,7 +508,7 @@ test.skipIf(!dockerAvailable)("PostgreSQL repository persists description and se
|
||||
const next = await repository.createDescriptionGenerationRun(
|
||||
secondDatabase.id,
|
||||
"missing",
|
||||
"openai-mini",
|
||||
"openai/gpt-4.1-mini",
|
||||
"en",
|
||||
1,
|
||||
);
|
||||
@@ -533,7 +536,7 @@ test.skipIf(!dockerAvailable)("PostgreSQL repository persists description and se
|
||||
const allRun = await repository.createDescriptionGenerationRun(
|
||||
firstDatabase.id,
|
||||
"all",
|
||||
"openai-mini",
|
||||
"openai/gpt-4.1-mini",
|
||||
"it",
|
||||
2,
|
||||
);
|
||||
@@ -562,7 +565,7 @@ test.skipIf(!dockerAvailable)("PostgreSQL repository persists description and se
|
||||
const suggestionRun = await repository.createSensitiveDataSuggestionRun(
|
||||
firstDatabase.id,
|
||||
"selected_columns",
|
||||
"openai-mini",
|
||||
"openai/gpt-4.1-mini",
|
||||
);
|
||||
expect(suggestionRun).toMatchObject({
|
||||
databaseId: firstDatabase.id,
|
||||
@@ -604,7 +607,7 @@ test.skipIf(!dockerAvailable)("PostgreSQL repository persists description and se
|
||||
const interruptedSuggestionRun = await repository.createSensitiveDataSuggestionRun(
|
||||
secondDatabase.id,
|
||||
"all",
|
||||
"openai-mini",
|
||||
"openai/gpt-4.1-mini",
|
||||
);
|
||||
expect(await repository.interruptActiveSensitiveDataSuggestionRuns(
|
||||
"Sensitive-field suggestion generation was interrupted by backend restart.",
|
||||
|
||||
@@ -16,13 +16,8 @@ const roots: string[] = [];
|
||||
afterEach(() => { for (const root of roots.splice(0)) rmSync(root, { recursive: true, force: true }); });
|
||||
|
||||
const workspace: WorkspaceDescriptor = {
|
||||
workspace: { schema_version: 3, id: "psd-clinical", name: "Policlinico San Donato", language: "it" },
|
||||
workspace: { schema_version: 4, id: "psd-clinical", name: "Policlinico San Donato", language: "it" },
|
||||
dwh: { engine: "postgres", database: "warehouse", schema: "datawarehouse", port: 5432, supported_transports: ["postgres_direct"] },
|
||||
semantic_index: {
|
||||
vector_store: { engine: "qdrant", collection: "psd", dimensions: 1024, distance: "cosine" },
|
||||
embedding: { provider: "ollama_internal", model: "qwen3-embedding:0.6b", dimensions: 1024 },
|
||||
},
|
||||
llm_policy: { allowed: ["zai/glm-5.2"] },
|
||||
};
|
||||
const revision: WorkspaceRevision = { id: "psd-clinical", commit: "a".repeat(40), blob: "b".repeat(40), snapshotPath: "/tmp/psd.yaml" };
|
||||
|
||||
|
||||
@@ -13,16 +13,11 @@ const roots: string[] = [];
|
||||
afterEach(() => { for (const root of roots.splice(0)) rmSync(root, { recursive: true, force: true }); });
|
||||
|
||||
const workspace: WorkspaceDescriptor = {
|
||||
workspace: { schema_version: 3, id: "psd-clinical", name: "Policlinico San Donato", language: "it" },
|
||||
workspace: { schema_version: 4, id: "psd-clinical", name: "Policlinico San Donato", language: "it" },
|
||||
dwh: {
|
||||
engine: "postgres", database: "warehouse", schema: "datawarehouse", port: 5432,
|
||||
supported_transports: ["postgres_direct", "rest_api"],
|
||||
},
|
||||
semantic_index: {
|
||||
vector_store: { engine: "qdrant", collection: "psd", dimensions: 1024, distance: "cosine" },
|
||||
embedding: { provider: "ollama_internal", model: "qwen3-embedding:0.6b", dimensions: 1024 },
|
||||
},
|
||||
llm_policy: { allowed: ["zai/glm-5.2"] },
|
||||
diagnostics: { dwh_rest: { method: "GET", path: "/health", auth: "bearer", response: { database: "database", schema: "schema" } } },
|
||||
};
|
||||
const revision: WorkspaceRevision = {
|
||||
|
||||
@@ -71,6 +71,7 @@ test("loadConfig keeps local development defaults", () => {
|
||||
workspaceSecretRuntimeRoot: "/tmp/thothii-workspace-secrets",
|
||||
internalQdrantUrl: "http://qdrant:6333",
|
||||
internalEmbeddingUrl: "http://embedding:11434",
|
||||
internalEmbeddingId: "ollama/qwen3-embedding:0.6b",
|
||||
internalEmbeddingModel: "qwen3-embedding:0.6b",
|
||||
internalEmbeddingDimensions: 1024,
|
||||
authMode: "none",
|
||||
@@ -198,6 +199,22 @@ test("loadConfig accepts only the allowed internal semantic runtime hosts", () =
|
||||
.toThrow(/internal.*embedding|invalid/i);
|
||||
});
|
||||
|
||||
test("loadConfig derives the embedding runtime model from its canonical catalog identity", () => {
|
||||
expect(loadConfig({
|
||||
THT_INTERNAL_EMBEDDING_ID: "ollama/nomic-embed-text",
|
||||
THT_INTERNAL_EMBEDDING_MODEL: "nomic-embed-text",
|
||||
})).toMatchObject({
|
||||
internalEmbeddingId: "ollama/nomic-embed-text",
|
||||
internalEmbeddingModel: "nomic-embed-text",
|
||||
});
|
||||
expect(() => loadConfig({
|
||||
THT_INTERNAL_EMBEDDING_ID: "ollama/nomic-embed-text",
|
||||
THT_INTERNAL_EMBEDDING_MODEL: "different-model",
|
||||
})).toThrow("does not match its canonical identity");
|
||||
expect(() => loadConfig({ THT_INTERNAL_EMBEDDING_ID: "not-canonical" }))
|
||||
.toThrow("embedding identity configuration is invalid");
|
||||
});
|
||||
|
||||
test("loadConfig enables the legacy workspace request only through explicit local mode", () => {
|
||||
expect(loadConfig({ THT_LEGACY_WORKSPACE_MODE: "local" }).legacyWorkspaceMode).toBe(true);
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
const semanticRuntime = {
|
||||
internalQdrantUrl: "http://qdrant:6333",
|
||||
internalEmbeddingUrl: "http://embedding:11434",
|
||||
internalEmbeddingId: "ollama/qwen3-embedding:0.6b",
|
||||
internalEmbeddingModel: "qwen3-embedding:0.6b",
|
||||
internalEmbeddingDimensions: 1024,
|
||||
};
|
||||
@@ -102,11 +103,11 @@ function restRendered(): Record<string, unknown> {
|
||||
}
|
||||
|
||||
const directCanonical =
|
||||
`{"schemaVersion":1,"dwh":{` +
|
||||
`{"schemaVersion":2,"dwh":{` +
|
||||
`"engine":"postgres","database":"postgres","schema":"datawarehouse",` +
|
||||
`"transport":"postgres_direct","host":"dwh.internal","port":5432,"user":"thoth_reader"},` +
|
||||
`"vector":{"collection":"psd-clinical","dimensions":1024,"distance":"cosine"},` +
|
||||
`"embedding":{"model":"qwen3-embedding:0.6b","dimensions":1024},` +
|
||||
`"embedding":{"id":"ollama/qwen3-embedding:0.6b","model":"qwen3-embedding:0.6b","dimensions":1024},` +
|
||||
`"roots":{"artifacts":"/data/sessions/psd-clinical/artifacts",` +
|
||||
`"indexes":"/data/sessions/psd-clinical/indexes"}}`;
|
||||
|
||||
@@ -192,6 +193,9 @@ test("DWH-affecting changes alter the effective config identity", () => {
|
||||
const changedCollection = { ...base, resources: { ...base.resources, vector: { ...(base.resources as Record<string, any>).vector, collection: "other" } } };
|
||||
expect(effectiveConfigIdentity("psd-clinical", changedCollection)).not.toBe(identityBefore);
|
||||
|
||||
const changedEmbeddingIdentity = { ...base, resources: { ...base.resources, embeddings: { ...(base.resources as Record<string, any>).embeddings, model: "other-embedding" } } };
|
||||
expect(effectiveConfigIdentity("psd-clinical", changedEmbeddingIdentity)).not.toBe(identityBefore);
|
||||
|
||||
const changedTransport = restRendered();
|
||||
expect(effectiveConfigIdentity("psd-clinical", changedTransport)).not.toBe(identityBefore);
|
||||
});
|
||||
|
||||
@@ -1,15 +1,12 @@
|
||||
import { chmodSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { afterEach, expect, test, vi } from "vitest";
|
||||
import { buildApp } from "../src/app.js";
|
||||
import { MemoryCatalogRepository } from "../src/catalog/memory-repository.js";
|
||||
import { afterEach, expect, test } from "vitest";
|
||||
import {
|
||||
loadMetadataGenerationModels,
|
||||
MetadataGenerationModelUnavailableError,
|
||||
} from "../src/catalog/metadata-generation-models.js";
|
||||
import { loadConfig } from "../src/config.js";
|
||||
import type { WorkspaceRegistry } from "../src/workspaces/registry.js";
|
||||
import { loadRuntimeModelCatalog, splitCanonicalModelId } from "../src/models/runtime-model-catalog.js";
|
||||
|
||||
const roots: string[] = [];
|
||||
|
||||
@@ -17,260 +14,101 @@ afterEach(() => {
|
||||
for (const root of roots.splice(0)) rmSync(root, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
function metadataConfiguration(
|
||||
metadataGeneration: string,
|
||||
secrets = "OPENAI_API_KEY=raw-provider-secret\n",
|
||||
) {
|
||||
const root = mkdtempSync(join(tmpdir(), "thothii-metadata-models-"));
|
||||
function runtimeCatalog(overrides: Record<string, unknown> = {}, secrets = "OPENAI_API_KEY=raw-provider-secret\n") {
|
||||
const root = mkdtempSync(join(tmpdir(), "thothii-runtime-models-"));
|
||||
roots.push(root);
|
||||
const installationFile = join(root, "thothii-installation.yaml");
|
||||
const catalogFile = join(root, "catalog.json");
|
||||
const secretsFile = join(root, "thothii.secrets");
|
||||
writeFileSync(installationFile, metadataGeneration, { mode: 0o600 });
|
||||
writeFileSync(secretsFile, secrets, { mode: 0o600 });
|
||||
chmodSync(installationFile, 0o600);
|
||||
chmodSync(secretsFile, 0o600);
|
||||
return { installationFile, secretsFile };
|
||||
}
|
||||
|
||||
function appFor(installationFile: string, secretsFile: string) {
|
||||
const config = loadConfig({
|
||||
NODE_ENV: "test",
|
||||
THT_HARNESS_DIR: "/missing",
|
||||
THT_INSTALLATION_CONFIG_FILE: installationFile,
|
||||
THT_SECRETS_FILE: secretsFile,
|
||||
PI_PROVIDER: "unrelated-pi-provider",
|
||||
PI_MODEL: "unrelated-pi-model",
|
||||
});
|
||||
return buildApp(config, {
|
||||
thtRunner: {} as never,
|
||||
workspaceRegistry: { list: vi.fn(async () => []) } as unknown as WorkspaceRegistry,
|
||||
workspaceDiagnoser: vi.fn(),
|
||||
catalogRepository: new MemoryCatalogRepository(),
|
||||
});
|
||||
}
|
||||
|
||||
test("exposes only safe metadata-generation choices and their configured default", async () => {
|
||||
const { installationFile, secretsFile } = metadataConfiguration(`metadataGeneration:
|
||||
default: openai-mini
|
||||
models:
|
||||
- id: openai-mini
|
||||
label: OpenAI Mini
|
||||
litellm:
|
||||
provider: openai
|
||||
model: gpt-4.1-mini
|
||||
endpoint:
|
||||
baseUrl: https://api.openai.example/v1
|
||||
apiVersion: "2026-08-01"
|
||||
apiKeyEnv: OPENAI_API_KEY
|
||||
`);
|
||||
const app = appFor(installationFile, secretsFile);
|
||||
|
||||
const response = await app.inject({ method: "GET", url: "/catalog/metadata-generation/models" });
|
||||
|
||||
expect(response.statusCode).toBe(200);
|
||||
expect(response.json()).toEqual({
|
||||
models: [{ id: "openai-mini", label: "OpenAI Mini" }],
|
||||
default: "openai-mini",
|
||||
});
|
||||
expect(response.body).not.toMatch(/openai\/gpt|gpt-4\.1|api\.openai|OPENAI_API_KEY|raw-provider-secret/);
|
||||
await app.close();
|
||||
});
|
||||
|
||||
test("rejects an unprotected installation descriptor", () => {
|
||||
const { installationFile, secretsFile } = metadataConfiguration(`metadataGeneration:
|
||||
default: openai-mini
|
||||
models:
|
||||
- id: openai-mini
|
||||
label: OpenAI Mini
|
||||
litellm: {provider: openai, model: gpt-4.1-mini}
|
||||
apiKeyEnv: OPENAI_API_KEY
|
||||
`);
|
||||
chmodSync(installationFile, 0o644);
|
||||
|
||||
expect(() => loadMetadataGenerationModels({ installationFile, secretsFile }))
|
||||
.toThrow("metadata-generation installation is unavailable");
|
||||
});
|
||||
|
||||
test("returns an empty safe catalog when no metadata-generation model is configured", async () => {
|
||||
const { installationFile, secretsFile } = metadataConfiguration("profile: local\n");
|
||||
const app = appFor(installationFile, secretsFile);
|
||||
|
||||
const response = await app.inject({ method: "GET", url: "/catalog/metadata-generation/models" });
|
||||
|
||||
expect(response.statusCode).toBe(200);
|
||||
expect(response.json()).toEqual({ models: [], default: null });
|
||||
await app.close();
|
||||
});
|
||||
|
||||
test("resolves only a configured selection for the later generation boundary", () => {
|
||||
const { installationFile, secretsFile } = metadataConfiguration(`metadataGeneration:
|
||||
default: openai-mini
|
||||
models:
|
||||
- id: openai-mini
|
||||
label: OpenAI Mini
|
||||
litellm:
|
||||
provider: openai
|
||||
model: gpt-4.1-mini
|
||||
endpoint: {baseUrl: https://api.openai.example/v1, apiVersion: "2026-08-01"}
|
||||
apiKeyEnv: OPENAI_API_KEY
|
||||
`);
|
||||
const models = loadMetadataGenerationModels({ installationFile, secretsFile });
|
||||
|
||||
expect(models.resolve("openai-mini")).toEqual({
|
||||
id: "openai-mini",
|
||||
provider: "openai",
|
||||
model: "gpt-4.1-mini",
|
||||
endpoint: { baseUrl: "https://api.openai.example/v1", apiVersion: "2026-08-01" },
|
||||
apiKeyEnv: "OPENAI_API_KEY",
|
||||
apiKey: "raw-provider-secret",
|
||||
});
|
||||
expect(() => models.resolve("unknown-model")).toThrow(MetadataGenerationModelUnavailableError);
|
||||
});
|
||||
|
||||
test("loads DeepSeek models, GLM, and an explicit keyless Qwen endpoint from installation setup", () => {
|
||||
const { installationFile, secretsFile } = metadataConfiguration(`metadataGeneration:
|
||||
default: glm-53
|
||||
models:
|
||||
- id: deepseek-v4-pro
|
||||
label: DeepSeek V4 Pro
|
||||
litellm: {provider: deepseek, model: deepseek-v4-pro}
|
||||
apiKeyEnv: DEEPSEEK_API_KEY
|
||||
- id: deepseek-v4-flash
|
||||
label: DeepSeek V4 Flash
|
||||
litellm: {provider: deepseek, model: deepseek-v4-flash}
|
||||
apiKeyEnv: DEEPSEEK_API_KEY
|
||||
- id: glm-53
|
||||
label: GLM 5.3
|
||||
litellm:
|
||||
provider: openai
|
||||
model: glm-5.3
|
||||
endpoint: {baseUrl: https://api.z.ai/api/coding/paas/v4}
|
||||
apiKeyEnv: ZAI_API_KEY
|
||||
- id: qwen-36
|
||||
label: Qwen 3.6
|
||||
litellm:
|
||||
provider: openai
|
||||
model: qwen3.6-35b-a3b
|
||||
disableThinking: true
|
||||
endpoint: {baseUrl: https://models.internal.example/v1}
|
||||
`, "DEEPSEEK_API_KEY=deepseek-secret\nZAI_API_KEY=zai-secret\n");
|
||||
|
||||
const models = loadMetadataGenerationModels({ installationFile, secretsFile });
|
||||
|
||||
expect(models.catalog()).toEqual({
|
||||
const catalog = {
|
||||
schemaVersion: 1,
|
||||
defaultSession: "zai/glm-5.3",
|
||||
defaultMetadataGeneration: "zai/glm-5.3",
|
||||
embedding: { id: "ollama/qwen3-embedding:0.6b", dimensions: 1024 },
|
||||
models: [
|
||||
{ id: "deepseek-v4-pro", label: "DeepSeek V4 Pro" },
|
||||
{ id: "deepseek-v4-flash", label: "DeepSeek V4 Flash" },
|
||||
{ id: "glm-53", label: "GLM 5.3" },
|
||||
{ id: "qwen-36", label: "Qwen 3.6" },
|
||||
{
|
||||
id: "zai/glm-5.3", provider: "zai", model: "glm-5.3", label: "GLM 5.3",
|
||||
upstreamModel: "glm-5.3", endpoint: { baseUrl: "https://api.z.ai/v1" },
|
||||
authentication: { mode: "secret_env", apiKeyEnv: "OPENAI_API_KEY" },
|
||||
sessionAdapter: { mode: "openai_compatible" },
|
||||
metadataAdapter: { litellmProvider: "openai" },
|
||||
session: { reasoning: true, contextWindow: 200000, maxTokens: 131072 },
|
||||
metadataGeneration: { disableThinking: false },
|
||||
},
|
||||
{
|
||||
id: "deepseek/deepseek-v4-pro", provider: "deepseek", model: "deepseek-v4-pro",
|
||||
label: "DeepSeek V4 Pro", upstreamModel: "deepseek-v4-pro",
|
||||
authentication: { mode: "pi_auth" }, sessionAdapter: { mode: "pi_builtin" },
|
||||
session: { reasoning: false },
|
||||
},
|
||||
],
|
||||
default: "glm-53",
|
||||
...overrides,
|
||||
};
|
||||
writeFileSync(catalogFile, JSON.stringify(catalog), { mode: 0o600 });
|
||||
writeFileSync(secretsFile, secrets, { mode: 0o600 });
|
||||
chmodSync(catalogFile, 0o600);
|
||||
chmodSync(secretsFile, 0o600);
|
||||
return { catalogFile, secretsFile };
|
||||
}
|
||||
|
||||
test("loads session default and safe metadata choices from the normalized runtime catalog", () => {
|
||||
const { catalogFile, secretsFile } = runtimeCatalog();
|
||||
const runtime = loadRuntimeModelCatalog(catalogFile);
|
||||
const metadata = loadMetadataGenerationModels({ catalogFile, secretsFile });
|
||||
|
||||
expect(runtime.defaultSession).toBe("zai/glm-5.3");
|
||||
expect(runtime.hasSession("deepseek/deepseek-v4-pro")).toBe(true);
|
||||
expect(metadata.catalog()).toEqual({
|
||||
models: [{ id: "zai/glm-5.3", label: "GLM 5.3" }],
|
||||
default: "zai/glm-5.3",
|
||||
});
|
||||
expect(models.resolve("deepseek-v4-pro")).toMatchObject({
|
||||
apiKeyEnv: "DEEPSEEK_API_KEY",
|
||||
apiKey: "deepseek-secret",
|
||||
});
|
||||
expect(models.resolve("qwen-36")).toEqual({
|
||||
id: "qwen-36",
|
||||
provider: "openai",
|
||||
model: "qwen3.6-35b-a3b",
|
||||
disableThinking: true,
|
||||
endpoint: { baseUrl: "https://models.internal.example/v1" },
|
||||
expect(metadata.resolve("zai/glm-5.3")).toEqual({
|
||||
id: "zai/glm-5.3", provider: "openai", model: "glm-5.3",
|
||||
endpoint: { baseUrl: "https://api.z.ai/v1" },
|
||||
apiKeyEnv: "OPENAI_API_KEY", apiKey: "raw-provider-secret",
|
||||
});
|
||||
expect(() => metadata.resolve("zai/missing")).toThrow(MetadataGenerationModelUnavailableError);
|
||||
});
|
||||
|
||||
test("loads an explicit keyless endpoint without a secret bundle", () => {
|
||||
const { installationFile } = metadataConfiguration(`metadataGeneration:
|
||||
default: qwen-36
|
||||
models:
|
||||
- id: qwen-36
|
||||
label: Qwen 3.6
|
||||
litellm:
|
||||
provider: openai
|
||||
model: qwen3.6-35b-a3b
|
||||
disableThinking: true
|
||||
endpoint: {baseUrl: https://models.internal.example/v1}
|
||||
`);
|
||||
test("returns empty catalogs when no runtime projection is configured", () => {
|
||||
expect(loadRuntimeModelCatalog().defaultSession).toBeNull();
|
||||
expect(loadMetadataGenerationModels({}).catalog()).toEqual({ models: [], default: null });
|
||||
});
|
||||
|
||||
expect(loadMetadataGenerationModels({ installationFile }).resolve("qwen-36")).toEqual({
|
||||
id: "qwen-36",
|
||||
provider: "openai",
|
||||
model: "qwen3.6-35b-a3b",
|
||||
disableThinking: true,
|
||||
endpoint: { baseUrl: "https://models.internal.example/v1" },
|
||||
test("rejects a drifted default and an unprotected projection", () => {
|
||||
const drifted = runtimeCatalog({ defaultSession: "zai/missing" });
|
||||
expect(() => loadRuntimeModelCatalog(drifted.catalogFile)).toThrow("session default is invalid");
|
||||
|
||||
const unprotected = runtimeCatalog();
|
||||
chmodSync(unprotected.catalogFile, 0o666);
|
||||
expect(() => loadRuntimeModelCatalog(unprotected.catalogFile)).toThrow("runtime model catalog is unavailable");
|
||||
});
|
||||
|
||||
test("rejects authentication semantics that cannot come from the installation catalog", () => {
|
||||
const invalid = runtimeCatalog({
|
||||
defaultMetadataGeneration: undefined,
|
||||
models: [{
|
||||
id: "zai/glm-5.3",
|
||||
provider: "zai",
|
||||
model: "glm-5.3",
|
||||
label: "GLM 5.3",
|
||||
upstreamModel: "glm-5.3",
|
||||
authentication: { mode: "secret_env" },
|
||||
sessionAdapter: { mode: "pi_builtin" },
|
||||
session: { reasoning: true },
|
||||
}],
|
||||
});
|
||||
expect(() => loadRuntimeModelCatalog(invalid.catalogFile)).toThrow("runtime model catalog is invalid");
|
||||
});
|
||||
|
||||
test.each([
|
||||
["invalid YAML", "metadataGeneration: [\n", "OPENAI_API_KEY=secret\n", /invalid YAML/],
|
||||
["duplicate ids", `metadataGeneration:
|
||||
default: openai-mini
|
||||
models:
|
||||
- {id: openai-mini, label: One, litellm: {provider: openai, model: gpt-4.1-mini}, apiKeyEnv: OPENAI_API_KEY}
|
||||
- {id: openai-mini, label: Two, litellm: {provider: openai, model: gpt-4.1}, apiKeyEnv: OPENAI_API_KEY}
|
||||
`, "OPENAI_API_KEY=secret\n", /model id "openai-mini" is duplicated/],
|
||||
["missing default", `metadataGeneration:
|
||||
models:
|
||||
- {id: openai-mini, label: One, litellm: {provider: openai, model: gpt-4.1-mini}, apiKeyEnv: OPENAI_API_KEY}
|
||||
`, "OPENAI_API_KEY=secret\n", /default is required/],
|
||||
["unknown default", `metadataGeneration:
|
||||
default: absent
|
||||
models:
|
||||
- {id: openai-mini, label: One, litellm: {provider: openai, model: gpt-4.1-mini}, apiKeyEnv: OPENAI_API_KEY}
|
||||
`, "OPENAI_API_KEY=secret\n", /default "absent" is not configured/],
|
||||
["malformed settings", `metadataGeneration:
|
||||
default: openai-mini
|
||||
models:
|
||||
- {id: openai-mini, label: One, litellm: {provider: "open ai", model: gpt-4.1-mini}, apiKeyEnv: OPENAI_API_KEY}
|
||||
`, "OPENAI_API_KEY=secret\n", /configuration is invalid/],
|
||||
["malformed endpoint", `metadataGeneration:
|
||||
default: openai-mini
|
||||
models:
|
||||
- id: openai-mini
|
||||
label: One
|
||||
litellm: {provider: openai, model: gpt-4.1-mini, endpoint: {baseUrl: not-a-url}}
|
||||
apiKeyEnv: OPENAI_API_KEY
|
||||
`, "OPENAI_API_KEY=secret\n", /configuration is invalid/],
|
||||
["keyless hosted model without endpoint", `metadataGeneration:
|
||||
default: openai-mini
|
||||
models:
|
||||
- {id: openai-mini, label: One, litellm: {provider: openai, model: gpt-4.1-mini}}
|
||||
`, "", /configuration is invalid/],
|
||||
["disable thinking without endpoint", `metadataGeneration:
|
||||
default: openai-mini
|
||||
models:
|
||||
- id: openai-mini
|
||||
label: One
|
||||
litellm: {provider: openai, model: gpt-4.1-mini, disableThinking: true}
|
||||
apiKeyEnv: OPENAI_API_KEY
|
||||
`, "OPENAI_API_KEY=secret\n", /configuration is invalid/],
|
||||
["unallowed secret reference", `metadataGeneration:
|
||||
default: openai-mini
|
||||
models:
|
||||
- {id: openai-mini, label: One, litellm: {provider: openai, model: gpt-4.1-mini}, apiKeyEnv: THT_DWH_API_KEY}
|
||||
`, "THT_DWH_API_KEY=secret\n", /configuration is invalid/],
|
||||
["missing referenced secret", `metadataGeneration:
|
||||
default: openai-mini
|
||||
models:
|
||||
- {id: openai-mini, label: One, litellm: {provider: openai, model: gpt-4.1-mini}, apiKeyEnv: OPENAI_API_KEY}
|
||||
`, "THT_DWH_API_KEY=secret\n", /secret "OPENAI_API_KEY" is missing/],
|
||||
["unusable referenced secret", `metadataGeneration:
|
||||
default: openai-mini
|
||||
models:
|
||||
- {id: openai-mini, label: One, litellm: {provider: openai, model: gpt-4.1-mini}, apiKeyEnv: OPENAI_API_KEY}
|
||||
`, "OPENAI_API_KEY=secret with whitespace\n", /secret "OPENAI_API_KEY" is unusable/],
|
||||
] as const)("rejects %s metadata-generation configuration", (_name, yaml, secrets, expected) => {
|
||||
const { installationFile, secretsFile } = metadataConfiguration(yaml, secrets);
|
||||
expect(() => loadMetadataGenerationModels({ installationFile, secretsFile })).toThrow(expected);
|
||||
test("fails closed for missing or unusable provider secrets", () => {
|
||||
const missing = runtimeCatalog({}, "THT_DWH_API_KEY=other\n");
|
||||
expect(() => loadMetadataGenerationModels(missing)).toThrow('secret "OPENAI_API_KEY" is missing');
|
||||
|
||||
const unusable = runtimeCatalog({}, "OPENAI_API_KEY=contains whitespace\n");
|
||||
expect(() => loadMetadataGenerationModels(unusable)).toThrow('secret "OPENAI_API_KEY" is unusable');
|
||||
});
|
||||
|
||||
test("rejects a missing secret-bundle declaration for configured models", () => {
|
||||
const { installationFile } = metadataConfiguration(`metadataGeneration:
|
||||
default: openai-mini
|
||||
models:
|
||||
- {id: openai-mini, label: One, litellm: {provider: openai, model: gpt-4.1-mini}, apiKeyEnv: OPENAI_API_KEY}
|
||||
`);
|
||||
|
||||
expect(() => loadMetadataGenerationModels({ installationFile }))
|
||||
.toThrow("metadata-generation keyed models require THT_SECRETS_FILE");
|
||||
test("splits canonical session identities without provider aliases", () => {
|
||||
expect(splitCanonicalModelId("zai/glm-5.3")).toEqual({ provider: "zai", model: "glm-5.3" });
|
||||
expect(() => splitCanonicalModelId("glm-5.3")).toThrow("model identity is invalid");
|
||||
});
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { mkdtempSync, readFileSync, readdirSync, rmSync } from "node:fs";
|
||||
import { mkdtempSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { expect, test, vi } from "vitest";
|
||||
import { loadConfig } from "../src/config.js";
|
||||
import {
|
||||
PiManagementError,
|
||||
createPiManagement,
|
||||
type PiExecFile,
|
||||
} from "../src/pi/management.js";
|
||||
import type { RuntimeModelCatalog } from "../src/models/runtime-model-catalog.js";
|
||||
|
||||
function configFor(settingsFile = join(mkdtempSync(join(tmpdir(), "tht-pi-management-")), "settings.json")) {
|
||||
return loadConfig({
|
||||
@@ -18,10 +18,14 @@ function configFor(settingsFile = join(mkdtempSync(join(tmpdir(), "tht-pi-manage
|
||||
});
|
||||
}
|
||||
|
||||
const supportedModels = [
|
||||
{ provider: "zai", id: "glm-5.2", name: "GLM 5.2", reasoning: true },
|
||||
{ provider: "deepseek", id: "deepseek-v4", name: "DeepSeek V4", reasoning: true },
|
||||
];
|
||||
const modelCatalog: RuntimeModelCatalog = {
|
||||
defaultSession: "zai/glm-5.2",
|
||||
defaultMetadataGeneration: null,
|
||||
embedding: { id: "ollama/qwen3-embedding:0.6b", dimensions: 1024 },
|
||||
sessionModels: () => [],
|
||||
metadataModels: () => [],
|
||||
hasSession: (id) => id === "zai/glm-5.2",
|
||||
};
|
||||
|
||||
function successfulExec(calls: Array<{ command: string; args: string[]; timeout: number }>): PiExecFile {
|
||||
return async (command, args, options) => {
|
||||
@@ -36,7 +40,7 @@ test("status parses only a Pi version from a fixed execFile argument array", asy
|
||||
const calls: Array<{ command: string; args: string[]; timeout: number }> = [];
|
||||
const service = createPiManagement(configFor(), {
|
||||
execute: successfulExec(calls),
|
||||
listModels: async () => supportedModels,
|
||||
modelCatalog,
|
||||
readSettings: () => ({ provider: "zai", model: "glm-5.2", thinking: "medium" }),
|
||||
credentialStatus: () => "missing",
|
||||
now: () => new Date("2026-08-05T10:00:00.000Z"),
|
||||
@@ -63,7 +67,7 @@ test.each(["present", "missing"] as const)(
|
||||
const checkedProviders: Array<string | undefined> = [];
|
||||
const service = createPiManagement(configFor(), {
|
||||
execute: successfulExec([]),
|
||||
listModels: async () => supportedModels,
|
||||
modelCatalog,
|
||||
readSettings: () => ({ provider: "zai", model: "glm-5.2", thinking: "medium" }),
|
||||
credentialStatus: (provider) => {
|
||||
checkedProviders.push(provider);
|
||||
@@ -85,100 +89,6 @@ test.each(["present", "missing"] as const)(
|
||||
},
|
||||
);
|
||||
|
||||
// Catches an options response that leaks provider metadata or lets callers choose model IDs that
|
||||
// Pi did not explicitly enable for this installation.
|
||||
test("options expose only closed provider, model, and reasoning choices", async () => {
|
||||
const service = createPiManagement(configFor(), {
|
||||
execute: successfulExec([]),
|
||||
listModels: async () => supportedModels,
|
||||
now: () => new Date("2026-08-05T10:00:00.000Z"),
|
||||
});
|
||||
|
||||
await expect(service.options()).resolves.toEqual({
|
||||
providers: ["zai", "deepseek"],
|
||||
models: [
|
||||
{ provider: "zai", id: "glm-5.2" },
|
||||
{ provider: "deepseek", id: "deepseek-v4" },
|
||||
],
|
||||
reasoning: ["low", "medium", "high"],
|
||||
checkedAt: "2026-08-05T10:00:00.000Z",
|
||||
});
|
||||
});
|
||||
|
||||
// Catches raw managed models.json validation details being collapsed into an ambiguous model-list
|
||||
// failure or escaping through the Pi Management options API.
|
||||
test("options report invalid managed model configuration with a stable sanitized error", async () => {
|
||||
const service = createPiManagement(configFor(), {
|
||||
execute: successfulExec([]),
|
||||
listModels: async () => {
|
||||
throw Object.assign(
|
||||
new Error("!sensitive-command /private/models.json raw-secret"),
|
||||
{ code: "PI_MANAGED_CONFIG_INVALID" },
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
let caught: unknown;
|
||||
try {
|
||||
await service.options();
|
||||
} catch (error) {
|
||||
caught = error;
|
||||
}
|
||||
expect(caught).toMatchObject<PiManagementError>({
|
||||
code: "pi_management_unavailable",
|
||||
message: "Pi provider/model configuration is invalid",
|
||||
});
|
||||
expect(String(caught)).not.toMatch(/sensitive|private|models\.json|secret/i);
|
||||
});
|
||||
|
||||
// Catches configuration writes that accept whitespace, unknown choices, or extra free-form fields
|
||||
// before reaching the durable installation settings file.
|
||||
test("config rejects invalid free-form values before writing settings", async () => {
|
||||
const directory = mkdtempSync(join(tmpdir(), "tht-pi-management-invalid-"));
|
||||
try {
|
||||
let writes = 0;
|
||||
const service = createPiManagement(configFor(join(directory, "settings.json")), {
|
||||
execute: successfulExec([]),
|
||||
listModels: async () => supportedModels,
|
||||
readSettings: () => ({}),
|
||||
saveSettings: () => { writes += 1; return {}; },
|
||||
});
|
||||
|
||||
await expect(service.configure({
|
||||
provider: "zai ", model: "glm-5.2", reasoning: "medium", unexpected: "value",
|
||||
} as any)).rejects.toMatchObject<PiManagementError>({ code: "pi_management_invalid_config" });
|
||||
expect(writes).toBe(0);
|
||||
} finally {
|
||||
rmSync(directory, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
// Catches a non-atomic implementation that can leave partial settings or temporary files after a
|
||||
// normal installation-default update.
|
||||
test("config validates closed choices and atomically persists non-secret defaults", async () => {
|
||||
const directory = mkdtempSync(join(tmpdir(), "tht-pi-management-write-"));
|
||||
const settingsFile = join(directory, "settings.json");
|
||||
try {
|
||||
const service = createPiManagement(configFor(settingsFile), {
|
||||
execute: successfulExec([]),
|
||||
listModels: async () => supportedModels,
|
||||
now: () => new Date("2026-08-05T10:00:00.000Z"),
|
||||
});
|
||||
|
||||
await expect(service.configure({
|
||||
provider: "zai", model: "glm-5.2", reasoning: "high",
|
||||
})).resolves.toEqual({
|
||||
provider: "zai", model: "glm-5.2", reasoning: "high", updatedAt: "2026-08-05T10:00:00.000Z",
|
||||
});
|
||||
expect(JSON.parse(readFileSync(settingsFile, "utf8"))).toEqual({
|
||||
provider: "zai", model: "glm-5.2", thinking: "high",
|
||||
});
|
||||
expect(readdirSync(directory)).toEqual(["settings.json"]);
|
||||
} finally {
|
||||
rmSync(directory, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
// Catches a hung Pi smoke check that leaves an operator waiting indefinitely or returns raw child
|
||||
// diagnostics containing provider credentials.
|
||||
test("smoke uses the configured timeout and reports a sanitized timeout", async () => {
|
||||
@@ -188,7 +98,7 @@ test("smoke uses the configured timeout and reports a sanitized timeout", async
|
||||
calls.push({ command, args, timeout: options.timeout });
|
||||
throw Object.assign(new Error("provider token=raw-provider-token"), { code: "ETIMEDOUT" });
|
||||
},
|
||||
listModels: async () => supportedModels,
|
||||
modelCatalog,
|
||||
readSettings: () => ({ provider: "zai", model: "glm-5.2", thinking: "medium" }),
|
||||
now: () => new Date("2026-08-05T10:00:00.000Z"),
|
||||
});
|
||||
@@ -210,7 +120,7 @@ test("smoke exercises the configured provider and model", async () => {
|
||||
const providerChecks: unknown[] = [];
|
||||
const service = createPiManagement(configFor(), {
|
||||
execute: successfulExec([]),
|
||||
listModels: async () => supportedModels,
|
||||
modelCatalog,
|
||||
readSettings: () => ({ provider: "zai", model: "glm-5.2", thinking: "medium" }),
|
||||
smokeProvider: async (request) => { providerChecks.push(request); },
|
||||
now: () => new Date("2026-08-05T10:00:00.000Z"),
|
||||
@@ -230,7 +140,7 @@ test("smoke exercises the configured provider and model", async () => {
|
||||
test("smoke fails closed and sanitizes configured-provider authentication errors", async () => {
|
||||
const service = createPiManagement(configFor(), {
|
||||
execute: successfulExec([]),
|
||||
listModels: async () => supportedModels,
|
||||
modelCatalog,
|
||||
readSettings: () => ({ provider: "zai", model: "glm-5.2", thinking: "medium" }),
|
||||
smokeProvider: async () => {
|
||||
throw new Error('401 {"token":"raw-expired-token","output":"raw-provider-output"}');
|
||||
@@ -252,7 +162,7 @@ test("smoke fails closed and sanitizes configured-provider authentication errors
|
||||
test("smoke reports invalid managed provider configuration with a stable sanitized error", async () => {
|
||||
const service = createPiManagement(configFor(), {
|
||||
execute: successfulExec([]),
|
||||
listModels: async () => supportedModels,
|
||||
modelCatalog,
|
||||
readSettings: () => ({ provider: "zai", model: "glm-5.2", thinking: "medium" }),
|
||||
smokeProvider: async () => {
|
||||
throw Object.assign(
|
||||
@@ -284,7 +194,7 @@ test("smoke applies one deadline across version and a hung provider turn", async
|
||||
() => resolve({ stdout: "pi 0.80.3\n", stderr: "" }),
|
||||
500,
|
||||
)),
|
||||
listModels: async () => supportedModels,
|
||||
modelCatalog,
|
||||
readSettings: () => ({ provider: "zai", model: "glm-5.2", thinking: "medium" }),
|
||||
smokeProvider: async ({ timeoutMs }) => {
|
||||
providerTimeouts.push(timeoutMs);
|
||||
@@ -320,7 +230,7 @@ test("logs keep only the latest 200 redacted lines", async () => {
|
||||
source[201] = "THT_MODEL_API_KEY=raw-env-secret";
|
||||
const service = createPiManagement(configFor(), {
|
||||
execute: successfulExec([]),
|
||||
listModels: async () => supportedModels,
|
||||
modelCatalog,
|
||||
readLogs: () => source.join("\n"),
|
||||
now: () => new Date("2026-08-05T10:00:00.000Z"),
|
||||
});
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
} from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { PiProcessManager } from "../src/pi/pi-process-manager.js";
|
||||
import type { RuntimeModelCatalog, RuntimeModel } from "../src/models/runtime-model-catalog.js";
|
||||
import { loadConfig } from "../src/config.js";
|
||||
import {
|
||||
PI_MANAGED_CONFIG_ERROR_MESSAGE,
|
||||
@@ -641,6 +642,53 @@ test("session Pi spawn reads the single secret bundle and scrubs its path", asyn
|
||||
}
|
||||
});
|
||||
|
||||
test("session Pi spawn resolves the selected catalog credential from the secret bundle", () => {
|
||||
const root = mkdtempSync(path.join(tmpdir(), "thothii-catalog-credential-"));
|
||||
const agentDir = path.join(root, "agent");
|
||||
mkdirSync(agentDir, { mode: 0o700 });
|
||||
writeFileSync(path.join(agentDir, "auth.json"), "{}\n", { mode: 0o600 });
|
||||
writeFileSync(path.join(agentDir, "models.json"), '{"providers":{}}\n', { mode: 0o600 });
|
||||
const secret = path.join(root, "thothii.secrets");
|
||||
writeFileSync(secret, "ZAI_API_KEY=catalog-secret\nTHT_MODEL_API_KEY=legacy-secret\n", { mode: 0o600 });
|
||||
const model: RuntimeModel = {
|
||||
id: "openai/test-model",
|
||||
provider: "openai",
|
||||
model: "test-model",
|
||||
label: "Test model",
|
||||
upstreamModel: "test-model",
|
||||
authentication: { mode: "secret_env", apiKeyEnv: "ZAI_API_KEY" },
|
||||
sessionAdapter: { mode: "pi_builtin" },
|
||||
session: { reasoning: false },
|
||||
};
|
||||
const modelCatalog: RuntimeModelCatalog = {
|
||||
defaultSession: model.id,
|
||||
defaultMetadataGeneration: null,
|
||||
embedding: null,
|
||||
sessionModels: () => [model],
|
||||
metadataModels: () => [],
|
||||
hasSession: (id) => id === model.id,
|
||||
};
|
||||
const calls: any[][] = [];
|
||||
const child = recordingChild();
|
||||
child.stderr.resume = () => {};
|
||||
vi.stubEnv("PI_CODING_AGENT_DIR", agentDir);
|
||||
const mgr = new PiProcessManager(loadConfig({ THT_SECRETS_FILE: secret }), {
|
||||
modelCatalog,
|
||||
authProviders: () => new Set(),
|
||||
spawnFn: (...args: any[]) => { calls.push(args); return child as any; },
|
||||
});
|
||||
try {
|
||||
mgr.createFor("catalog-credential", { provider: "openai", model: "test-model" });
|
||||
expect(calls[0][2].env.ZAI_API_KEY).toBe("catalog-secret");
|
||||
expect(calls[0][2].env).not.toHaveProperty("OPENAI_API_KEY");
|
||||
expect(calls[0][2].env).not.toHaveProperty("THT_MODEL_API_KEY");
|
||||
} finally {
|
||||
mgr.teardown("catalog-credential");
|
||||
vi.unstubAllEnvs();
|
||||
rmSync(root, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test.each([["OpenAI", "openai"], ["gemini", "google"]])(
|
||||
"set_model uses canonical packaged provider ID for %s", async (provider, canonical) => {
|
||||
const secret = path.resolve(__dirname, `.canonical-key-${process.pid}-${provider}`);
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import { EventEmitter } from "node:events";
|
||||
import { existsSync, readFileSync, readdirSync } from "node:fs";
|
||||
import { dirname } from "node:path";
|
||||
import { existsSync, mkdtempSync, readFileSync, readdirSync, rmSync, writeFileSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { dirname, join } from "node:path";
|
||||
import { afterEach, expect, test, vi } from "vitest";
|
||||
import { loadConfig } from "../src/config.js";
|
||||
import { createPiProviderSmoke } from "../src/pi/provider-smoke.js";
|
||||
import type { RuntimeModel, RuntimeModelCatalog } from "../src/models/runtime-model-catalog.js";
|
||||
|
||||
afterEach(() => vi.unstubAllEnvs());
|
||||
|
||||
@@ -44,6 +46,50 @@ function successfulProviderChild() {
|
||||
|
||||
const MANAGED_CONFIG_ERROR = "Pi provider/model configuration is invalid";
|
||||
|
||||
test("provider smoke resolves the selected catalog credential from the secret bundle", async () => {
|
||||
const root = mkdtempSync(join(tmpdir(), "thothii-smoke-catalog-credential-"));
|
||||
const secret = join(root, "thothii.secrets");
|
||||
writeFileSync(secret, "ZAI_API_KEY=catalog-secret\nTHT_MODEL_API_KEY=legacy-secret\n", { mode: 0o600 });
|
||||
const model: RuntimeModel = {
|
||||
id: "openai/test-model",
|
||||
provider: "openai",
|
||||
model: "test-model",
|
||||
label: "Test model",
|
||||
upstreamModel: "test-model",
|
||||
authentication: { mode: "secret_env", apiKeyEnv: "ZAI_API_KEY" },
|
||||
sessionAdapter: { mode: "pi_builtin" },
|
||||
session: { reasoning: false },
|
||||
};
|
||||
const modelCatalog: RuntimeModelCatalog = {
|
||||
defaultSession: model.id,
|
||||
defaultMetadataGeneration: null,
|
||||
embedding: null,
|
||||
sessionModels: () => [model],
|
||||
metadataModels: () => [],
|
||||
hasSession: (id) => id === model.id,
|
||||
};
|
||||
let spawnEnv: NodeJS.ProcessEnv | undefined;
|
||||
const smoke = createPiProviderSmoke(loadConfig({ THT_SECRETS_FILE: secret }), {
|
||||
modelCatalog,
|
||||
authProviders: () => new Set(),
|
||||
readModelsStore: () => undefined,
|
||||
spawnFn: (_command, _args, options) => {
|
||||
spawnEnv = options.env;
|
||||
return successfulProviderChild();
|
||||
},
|
||||
});
|
||||
try {
|
||||
await expect(smoke({
|
||||
provider: "openai", model: "test-model", reasoning: "medium", timeoutMs: 750,
|
||||
})).resolves.toBeUndefined();
|
||||
expect(spawnEnv?.ZAI_API_KEY).toBe("catalog-secret");
|
||||
expect(spawnEnv).not.toHaveProperty("OPENAI_API_KEY");
|
||||
expect(spawnEnv).not.toHaveProperty("THT_MODEL_API_KEY");
|
||||
} finally {
|
||||
rmSync(root, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
// Catches an isolated smoke agent that copies auth.json but drops the selected custom
|
||||
// provider/model from models.json, causing set_model to fail before the real request.
|
||||
test("provider smoke reaches the selected custom provider from an isolated models.json", async () => {
|
||||
|
||||
@@ -2,18 +2,11 @@ import { expect, test } from "vitest";
|
||||
import { ReadinessManager } from "../src/runtime/readiness-manager.js";
|
||||
|
||||
const workspace = {
|
||||
workspace: { schema_version: 3, id: "psd", name: "PSD", language: "it" },
|
||||
workspace: { schema_version: 4, id: "psd", name: "PSD", language: "it" },
|
||||
dwh: {
|
||||
engine: "postgres", database: "warehouse", schema: "public",
|
||||
supported_transports: ["postgres_direct"],
|
||||
},
|
||||
semantic_index: {
|
||||
vector_store: { engine: "qdrant", collection: "psd", dimensions: 1024, distance: "cosine" },
|
||||
embedding: {
|
||||
provider: "ollama_internal", model: "qwen3-embedding:0.6b", dimensions: 1024,
|
||||
},
|
||||
},
|
||||
llm_policy: { allowed: ["zai/glm-5.2"] },
|
||||
} as const;
|
||||
|
||||
function deferred<T>() {
|
||||
|
||||
@@ -15,7 +15,7 @@ afterEach(() => {
|
||||
});
|
||||
|
||||
const validYaml = `workspace:
|
||||
schema_version: 3
|
||||
schema_version: 4
|
||||
id: psd-clinical
|
||||
name: Policlinico San Donato
|
||||
language: it
|
||||
@@ -24,18 +24,6 @@ dwh:
|
||||
database: postgres
|
||||
schema: datawarehouse
|
||||
supported_transports: [postgres_direct]
|
||||
semantic_index:
|
||||
vector_store:
|
||||
engine: qdrant
|
||||
collection: psd-clinical
|
||||
dimensions: 1024
|
||||
distance: cosine
|
||||
embedding:
|
||||
provider: ollama_internal
|
||||
model: qwen3-embedding:0.6b
|
||||
dimensions: 1024
|
||||
llm_policy:
|
||||
allowed: [zai/glm-5.2]
|
||||
`;
|
||||
|
||||
async function git(cwd: string, args: string[]): Promise<string> {
|
||||
|
||||
@@ -20,7 +20,7 @@ async function git(cwd: string, args: string[]): Promise<string> {
|
||||
}
|
||||
|
||||
const descriptor = `workspace:
|
||||
schema_version: 3
|
||||
schema_version: 4
|
||||
id: research
|
||||
name: Research
|
||||
language: en
|
||||
@@ -29,18 +29,6 @@ dwh:
|
||||
database: analytics
|
||||
schema: mart
|
||||
supported_transports: [postgres_direct]
|
||||
semantic_index:
|
||||
vector_store:
|
||||
engine: qdrant
|
||||
collection: research
|
||||
dimensions: 1024
|
||||
distance: cosine
|
||||
embedding:
|
||||
provider: ollama_internal
|
||||
model: qwen3-embedding:0.6b
|
||||
dimensions: 1024
|
||||
llm_policy:
|
||||
allowed: [zai/glm-5.2]
|
||||
evidence:
|
||||
source:
|
||||
type: filesystem
|
||||
|
||||
@@ -14,11 +14,6 @@ function fakeService(): PiManagementService {
|
||||
config: { provider: "zai", model: "glm-5.2", reasoning: "medium" },
|
||||
checkedAt: "2026-08-05T10:00:00.000Z",
|
||||
})),
|
||||
options: vi.fn(async () => ({
|
||||
providers: ["zai"], models: [{ provider: "zai", id: "glm-5.2" }],
|
||||
reasoning: ["low", "medium", "high"], checkedAt: "2026-08-05T10:00:00.000Z",
|
||||
})),
|
||||
configure: vi.fn(async (value) => ({ ...value, updatedAt: "2026-08-05T10:00:00.000Z" })),
|
||||
test: vi.fn(async () => ({ ready: true, checkedAt: "2026-08-05T10:00:00.000Z" })),
|
||||
logs: vi.fn(async () => ({ lines: ["Pi smoke check succeeded"], checkedAt: "2026-08-05T10:00:00.000Z" })),
|
||||
};
|
||||
@@ -106,24 +101,17 @@ test("loopback-only AUTH_MODE=none may read the sanitized Pi status", async () =
|
||||
});
|
||||
|
||||
// A local implicit administrator has pi.manage, but a browser origin still cannot borrow that
|
||||
// authority to mutate local configuration or trigger provider work.
|
||||
// authority to trigger provider work.
|
||||
test("loopback-only management rejects cross-origin writes for its local administrator", async () => {
|
||||
const service = fakeService();
|
||||
const app = appWith(service);
|
||||
try {
|
||||
const configured = await app.inject({
|
||||
method: "PUT", url: "/pi-management/config",
|
||||
headers: { host: "127.0.0.1:8080", origin: "https://evil.example" },
|
||||
payload: { provider: "zai", model: "glm-5.2", reasoning: "high" },
|
||||
});
|
||||
const smoke = await app.inject({
|
||||
method: "POST", url: "/pi-management/test",
|
||||
headers: { host: "127.0.0.1:8080", origin: "https://evil.example" },
|
||||
});
|
||||
|
||||
expect(configured.statusCode).toBe(403);
|
||||
expect(smoke.statusCode).toBe(403);
|
||||
expect(service.configure).not.toHaveBeenCalled();
|
||||
expect(service.test).not.toHaveBeenCalled();
|
||||
} finally {
|
||||
await app.close();
|
||||
@@ -132,14 +120,13 @@ test("loopback-only management rejects cross-origin writes for its local adminis
|
||||
|
||||
// Catches an origin guard that also blocks the same-origin Docker frontend or non-browser local
|
||||
// lifecycle clients that do not send Origin.
|
||||
test("loopback-only management preserves same-origin frontend and origin-less local writes", async () => {
|
||||
test("loopback-only management preserves same-origin and origin-less smoke checks", async () => {
|
||||
const service = fakeService();
|
||||
const app = appWith(service);
|
||||
try {
|
||||
const sameOrigin = await app.inject({
|
||||
method: "PUT", url: "/pi-management/config",
|
||||
method: "POST", url: "/pi-management/test",
|
||||
headers: { host: "127.0.0.1:8080", origin: "http://127.0.0.1:8080" },
|
||||
payload: { provider: "zai", model: "glm-5.2", reasoning: "high" },
|
||||
});
|
||||
const lifecycleClient = await app.inject({ method: "POST", url: "/pi-management/test" });
|
||||
|
||||
@@ -150,25 +137,20 @@ test("loopback-only management preserves same-origin frontend and origin-less lo
|
||||
}
|
||||
});
|
||||
|
||||
// Catches route wiring that bypasses closed service validation or gives the browser a Docker/image
|
||||
// lifecycle endpoint rather than only installation-default configuration and diagnostics.
|
||||
test("trusted admins receive only configuration, smoke, options, and log endpoints", async () => {
|
||||
// Catches a regression that reintroduces a browser-writable provider/model source.
|
||||
test("trusted admins receive only status, smoke, and log endpoints", async () => {
|
||||
const service = fakeService();
|
||||
const app = appWith(service, exposedServerEnv);
|
||||
try {
|
||||
const options = await app.inject({ method: "GET", url: "/pi-management/options", headers: adminHeaders });
|
||||
const configured = await app.inject({
|
||||
method: "PUT", url: "/pi-management/config", headers: adminHeaders,
|
||||
payload: { provider: "zai", model: "glm-5.2", reasoning: "high" },
|
||||
});
|
||||
const status = await app.inject({ method: "GET", url: "/pi-management/status", headers: adminHeaders });
|
||||
const smoke = await app.inject({ method: "POST", url: "/pi-management/test", headers: adminHeaders });
|
||||
const logs = await app.inject({ method: "GET", url: "/pi-management/logs", headers: adminHeaders });
|
||||
|
||||
expect(options.statusCode).toBe(200);
|
||||
expect(configured.statusCode).toBe(200);
|
||||
expect(configured.json()).toMatchObject({ provider: "zai", model: "glm-5.2", reasoning: "high" });
|
||||
expect(status.statusCode).toBe(200);
|
||||
expect(smoke.statusCode).toBe(200);
|
||||
expect(logs.statusCode).toBe(200);
|
||||
expect((await app.inject({ method: "GET", url: "/pi-management/options", headers: adminHeaders })).statusCode).toBe(404);
|
||||
expect((await app.inject({ method: "PUT", url: "/pi-management/config", headers: adminHeaders })).statusCode).toBe(404);
|
||||
expect(app.printRoutes()).not.toContain("update");
|
||||
expect(app.printRoutes()).not.toContain("rollback");
|
||||
} finally {
|
||||
|
||||
@@ -18,25 +18,25 @@ const SCRIPT = path.resolve("../harness/tests/fake_pi/scripts/f1_disambiguation.
|
||||
|
||||
function operationalWorkspace(id = "default") {
|
||||
return {
|
||||
workspace: { schema_version: 3, id, name: id, language: "en" },
|
||||
workspace: { schema_version: 4, id, name: id, language: "en" },
|
||||
dwh: {
|
||||
engine: "postgres", database: "warehouse", schema: "public",
|
||||
supported_transports: ["postgres_direct"],
|
||||
},
|
||||
semantic_index: {
|
||||
vector_store: {
|
||||
engine: "qdrant", collection: id, dimensions: 1024, distance: "cosine",
|
||||
},
|
||||
embedding: {
|
||||
provider: "ollama_internal", model: "qwen3-embedding:0.6b", dimensions: 1024,
|
||||
},
|
||||
},
|
||||
llm_policy: {
|
||||
allowed: ["zai/glm-5.2", "deepseek/deepseek-v4-pro", "local-qwen/qwen3.6-35b-a3b"],
|
||||
},
|
||||
} as const;
|
||||
}
|
||||
|
||||
function sessionCatalog(defaultSession = "zai/glm-5.2", available = [defaultSession]) {
|
||||
return {
|
||||
defaultSession,
|
||||
defaultMetadataGeneration: null,
|
||||
embedding: { id: "ollama/qwen3-embedding:0.6b", dimensions: 1024 },
|
||||
sessionModels: () => [],
|
||||
metadataModels: () => [],
|
||||
hasSession: (id: string) => available.includes(id),
|
||||
} as any;
|
||||
}
|
||||
|
||||
const defaultWorkspaceRegistry = {
|
||||
list: vi.fn(async () => [{
|
||||
id: "default", commit: "e".repeat(40), blob: "f".repeat(40),
|
||||
@@ -495,8 +495,8 @@ test("creates a session from the active immutable workspace revision", async ()
|
||||
workspaceRegistry: {
|
||||
read: vi.fn(async () => ({
|
||||
workspace: {
|
||||
workspace: { schema_version: 2, id: "psd-clinical", name: "PSD", language: "it" },
|
||||
dwh: {}, semantic_index: {}, llm_policy: { allowed: ["zai/glm-5.2"] },
|
||||
workspace: { schema_version: 4, id: "psd-clinical", name: "PSD", language: "it" },
|
||||
dwh: {},
|
||||
},
|
||||
revision: {
|
||||
id: "psd-clinical", commit: "a".repeat(40), blob: "b".repeat(40),
|
||||
@@ -589,22 +589,11 @@ test("rejects an SSH-only workspace before persisting or starting a session", as
|
||||
workspaceRegistry: {
|
||||
acquireSessionRevision: vi.fn(async () => ({
|
||||
workspace: {
|
||||
workspace: { schema_version: 2, id: "ssh-workspace", name: "SSH", language: "en" },
|
||||
workspace: { schema_version: 4, id: "ssh-workspace", name: "SSH", language: "en" },
|
||||
dwh: {
|
||||
engine: "postgres", database: "postgres", schema: "public",
|
||||
supported_transports: ["ssh_tunnel"],
|
||||
},
|
||||
semantic_index: {
|
||||
vector_store: {
|
||||
engine: "pgvector", database: "postgres", schema: "vectors",
|
||||
collection: "documents", dimensions: 768, distance: "cosine",
|
||||
supported_transports: ["ssh_tunnel"],
|
||||
},
|
||||
embedding: {
|
||||
provider: "ollama_compatible", model: "nomic-embed-text", dimensions: 768,
|
||||
},
|
||||
},
|
||||
llm_policy: { allowed: ["zai/glm-5.2"] },
|
||||
},
|
||||
revision: {
|
||||
id: "ssh-workspace", commit: "a".repeat(40), blob: "b".repeat(40),
|
||||
@@ -632,7 +621,7 @@ test("hands a revision lease to retention only after the session manifest is dur
|
||||
const markPersisted = vi.fn(async () => {});
|
||||
const abort = vi.fn(async () => {});
|
||||
const acquireSessionRevision = vi.fn(async () => ({
|
||||
workspace: { llm_policy: { allowed: ["zai/glm-5.2"] } },
|
||||
workspace: operationalWorkspace("leased"),
|
||||
revision: {
|
||||
id: "leased", commit: "a".repeat(40), blob: "b".repeat(40),
|
||||
snapshotPath: `/data/workspace-registry/snapshots/${"a".repeat(40)}/leased.yaml`,
|
||||
@@ -670,7 +659,7 @@ test("creates a session from the configured default workspace revision when work
|
||||
const sessionNew = vi.fn(async () => ({ id: "default-pinned" }));
|
||||
const registry = {
|
||||
read: vi.fn(async (id: string) => ({
|
||||
workspace: { llm_policy: { allowed: ["zai/glm-5.2"] } },
|
||||
workspace: operationalWorkspace(id),
|
||||
revision: {
|
||||
id, commit: "c".repeat(40), blob: "d".repeat(40),
|
||||
snapshotPath: `/data/workspace-registry/snapshots/${"c".repeat(40)}/${id}.yaml`,
|
||||
@@ -758,7 +747,7 @@ test("session lifecycle locates a B session when installation default is A", asy
|
||||
listModels: async () => [{ provider: "zai", id: "glm-5.2", name: "GLM 5.2", reasoning: true }],
|
||||
workspaceRegistry: {
|
||||
read: async (id: string) => ({
|
||||
workspace: { llm_policy: { allowed: ["zai/glm-5.2"] } },
|
||||
workspace: operationalWorkspace(id),
|
||||
revision: { id, commit: "b".repeat(40), blob: "d".repeat(40), snapshotPath: bPath },
|
||||
}),
|
||||
list: async () => [
|
||||
@@ -793,7 +782,7 @@ test("session lifecycle locates a B session when installation default is A", asy
|
||||
expect(runtimeOptions).toEqual(["/runtime/1.yaml", "/runtime/2.yaml"]);
|
||||
});
|
||||
|
||||
test("POST /sessions usa i settings (workspace/provider/model/thinking) e crea+avvia", async () => {
|
||||
test("POST /sessions uses the catalog default with workspace/thinking settings and starts", async () => {
|
||||
const modelKey = path.join(os.tmpdir(), `thoth-model-key-${process.pid}`);
|
||||
writeFileSync(modelKey, "test-model-key", { mode: 0o600 });
|
||||
chmodSync(modelKey, 0o600);
|
||||
@@ -808,7 +797,8 @@ test("POST /sessions usa i settings (workspace/provider/model/thinking) e crea+a
|
||||
sessionNew: async (o: any) => { sessionNewArg = o; return { id: "s1" }; },
|
||||
sessionList: async () => [{ id: "s1" }],
|
||||
} as any,
|
||||
getSettings: () => ({ workspace: "w", provider: "zai", model: "glm-5.2", thinking: "high" }),
|
||||
getSettings: () => ({ workspace: "w", thinking: "high" }),
|
||||
runtimeModelCatalog: sessionCatalog(),
|
||||
listModels: async () => [
|
||||
{ provider: "zai", id: "glm-5.2", name: "GLM 5.2", reasoning: true },
|
||||
],
|
||||
@@ -2565,32 +2555,43 @@ test("POST /sessions proceeds when ollamaEnsure succeeds", async () => {
|
||||
expect(ensureWs).toContain(`/snapshots/${"e".repeat(40)}/psd.yaml`);
|
||||
});
|
||||
|
||||
test("POST /sessions rejects an unavailable saved model before persisting a session", async () => {
|
||||
test("POST /sessions falls back from a stale requested model to the catalog default", async () => {
|
||||
let created = 0;
|
||||
let persisted: any;
|
||||
const runtime = { bridge: { onClientEvent: () => {} } };
|
||||
const app = buildApp(loadConfig({ THT_HARNESS_DIR: "../harness" }), {
|
||||
thtRunner: {
|
||||
sessionNew: async () => { created += 1; return { id: "must-not-exist" }; },
|
||||
sessionNew: async (options: any) => { created += 1; persisted = options; return { id: "fallback" }; },
|
||||
searchPack: async () => {},
|
||||
} as any,
|
||||
readiness: { ensure: async () => ({ ok: true }) } as any,
|
||||
getSettings: () => ({
|
||||
workspace: "psd",
|
||||
provider: "deepseek",
|
||||
model: "deepseek-v4-pro",
|
||||
thinking: "medium",
|
||||
}) as any,
|
||||
getSettings: () => ({ workspace: "psd", thinking: "medium" }) as any,
|
||||
runtimeModelCatalog: sessionCatalog(),
|
||||
listModels: async () => [
|
||||
{ provider: "zai", id: "glm-5.2", name: "GLM 5.2", reasoning: true },
|
||||
],
|
||||
mgr: {
|
||||
teardownForPrincipal: () => [],
|
||||
createFor: () => runtime,
|
||||
get: () => runtime,
|
||||
configure: async () => {},
|
||||
start: () => {},
|
||||
} as any,
|
||||
});
|
||||
|
||||
const res = await app.inject({ method: "POST", url: "/sessions", payload: { question: "q" } });
|
||||
const res = await app.inject({
|
||||
method: "POST",
|
||||
url: "/sessions",
|
||||
payload: { question: "q", provider: "deepseek", model: "deepseek-v4-pro" },
|
||||
});
|
||||
|
||||
expect(res.statusCode).toBe(503);
|
||||
expect(res.statusCode).toBe(200);
|
||||
expect(res.json()).toEqual({
|
||||
error: "Selected model is unavailable. Check Pi authentication and model settings, then try again.",
|
||||
code: "model_unavailable",
|
||||
id: "fallback",
|
||||
warning: "Configured model deepseek/deepseek-v4-pro is unavailable; using zai/glm-5.2.",
|
||||
});
|
||||
expect(created).toBe(0);
|
||||
expect(persisted).toMatchObject({ provider: "zai", model: "glm-5.2" });
|
||||
expect(created).toBe(1);
|
||||
});
|
||||
|
||||
test("POST /sessions marks a persisted session failed when runtime construction throws", async () => {
|
||||
|
||||
@@ -21,7 +21,7 @@ function appWithTmpSettings(extraEnv: Record<string, string> = {}, deps = {}) {
|
||||
return { app, dir };
|
||||
}
|
||||
|
||||
test("GET /settings returns effective defaults (env provider/model/thinking, first workspace)", async () => {
|
||||
test("GET /settings returns thinking and the first workspace without legacy model defaults", async () => {
|
||||
const { app, dir } = appWithTmpSettings({ PI_PROVIDER: "zai", PI_MODEL: "glm-5.2", PI_THINKING: "medium" }, {
|
||||
listModels: async () => [{ provider: "zai", id: "glm-5.2", name: "GLM 5.2", reasoning: true }],
|
||||
});
|
||||
@@ -29,8 +29,8 @@ test("GET /settings returns effective defaults (env provider/model/thinking, fir
|
||||
const res = await app.inject({ method: "GET", url: "/settings" });
|
||||
expect(res.statusCode).toBe(200);
|
||||
const body = res.json();
|
||||
expect(body.provider).toBe("zai");
|
||||
expect(body.model).toBe("glm-5.2");
|
||||
expect(body).not.toHaveProperty("provider");
|
||||
expect(body).not.toHaveProperty("model");
|
||||
expect(body.thinking).toBe("medium");
|
||||
expect(typeof body.workspace).toBe("string"); // first workspace from ../harness/workspaces
|
||||
} finally {
|
||||
@@ -62,7 +62,9 @@ test("PUT /settings does not persist personal workspace or LLM choices", async (
|
||||
});
|
||||
expect(put.statusCode).toBe(200);
|
||||
const got = await app.inject({ method: "GET", url: "/settings" });
|
||||
expect(got.json()).toMatchObject({ provider: "zai", model: "glm-5.2", thinking: "medium" });
|
||||
expect(got.json()).toMatchObject({ thinking: "medium" });
|
||||
expect(got.json()).not.toHaveProperty("provider");
|
||||
expect(got.json()).not.toHaveProperty("model");
|
||||
expect(got.json()).not.toMatchObject({ workspace: "psd", thinking: "high" });
|
||||
} finally {
|
||||
rmSync(dir, { recursive: true, force: true });
|
||||
@@ -114,7 +116,7 @@ test("settings no longer read or write principal-specific preferences", async ()
|
||||
expect(preferences.size).toBe(0);
|
||||
});
|
||||
|
||||
test("GET /settings retains complete legacy installation defaults without seeding a private profile", async () => {
|
||||
test("GET /settings drops legacy installation model fields without seeding a private profile", async () => {
|
||||
let preferences: Record<string, unknown> = {};
|
||||
const writes: Record<string, unknown>[] = [];
|
||||
const runner = {
|
||||
@@ -135,9 +137,7 @@ test("GET /settings retains complete legacy installation defaults without seedin
|
||||
const first = await app.inject({ method: "GET", url: "/settings" });
|
||||
const second = await app.inject({ method: "GET", url: "/settings" });
|
||||
|
||||
const expected = {
|
||||
workspace: "local", provider: "local-qwen", model: "qwen3.6-35b-a3b", thinking: "low",
|
||||
};
|
||||
const expected = { workspace: "local", thinking: "low" };
|
||||
expect(first.statusCode).toBe(200);
|
||||
expect(first.json()).toEqual(expected);
|
||||
expect(second.json()).toEqual(expected);
|
||||
@@ -167,15 +167,13 @@ test("GET /settings ignores stale private preferences in favor of installation d
|
||||
const response = await app.inject({ method: "GET", url: "/settings" });
|
||||
|
||||
expect(response.statusCode).toBe(200);
|
||||
expect(response.json()).toEqual({
|
||||
workspace: "local", provider: "local-qwen", model: "qwen3.6-35b-a3b", thinking: "low",
|
||||
});
|
||||
expect(response.json()).toEqual({ workspace: "local", thinking: "low" });
|
||||
} finally {
|
||||
rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("PUT /settings rejects an unknown model when a model list is available", async () => {
|
||||
test("PUT /settings ignores a legacy unknown model because the catalog owns model validity", async () => {
|
||||
const { app, dir } = appWithTmpSettings({}, {
|
||||
listModels: async () => [{ provider: "zai", id: "glm-5.2", name: "GLM 5.2", reasoning: true }],
|
||||
});
|
||||
@@ -184,14 +182,14 @@ test("PUT /settings rejects an unknown model when a model list is available", as
|
||||
method: "PUT", url: "/settings",
|
||||
payload: { workspace: "psd", provider: "zai", model: "does-not-exist", thinking: "low" },
|
||||
});
|
||||
expect(put.statusCode).toBe(400);
|
||||
expect(put.json()).toMatchObject({ error: expect.stringMatching(/model/i) });
|
||||
expect(put.statusCode).toBe(200);
|
||||
expect(put.json()).not.toHaveProperty("model");
|
||||
} finally {
|
||||
rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("PUT /settings validates provider and model as one composite identifier", async () => {
|
||||
test("PUT /settings ignores legacy provider/model pairs", async () => {
|
||||
const { app, dir } = appWithTmpSettings({}, {
|
||||
listModels: async () => [
|
||||
{ provider: "provider-a", id: "shared-id", name: "A", reasoning: false },
|
||||
@@ -204,7 +202,7 @@ test("PUT /settings validates provider and model as one composite identifier", a
|
||||
workspace: "psd", provider: "provider-b", model: "shared-id", thinking: "low",
|
||||
},
|
||||
});
|
||||
expect(wrongProvider.statusCode).toBe(400);
|
||||
expect(wrongProvider.statusCode).toBe(200);
|
||||
|
||||
const exactPair = await app.inject({
|
||||
method: "PUT", url: "/settings",
|
||||
@@ -213,6 +211,8 @@ test("PUT /settings validates provider and model as one composite identifier", a
|
||||
},
|
||||
});
|
||||
expect(exactPair.statusCode).toBe(200);
|
||||
expect(exactPair.json()).not.toHaveProperty("provider");
|
||||
expect(exactPair.json()).not.toHaveProperty("model");
|
||||
} finally {
|
||||
rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { test, expect, vi } from "vitest";
|
||||
import { test, expect } from "vitest";
|
||||
import Fastify from "fastify";
|
||||
import { buildApp } from "../src/app.js";
|
||||
import { loadConfig } from "../src/config.js";
|
||||
@@ -156,12 +156,23 @@ test("registry-backed SQL preview resolves and uses the session's pinned runtime
|
||||
// Workspace registry route coverage lives in routes-workspaces.test.ts. `/workspaces` no longer
|
||||
// reads legacy harness files: the Git registry is the single shared source of truth.
|
||||
|
||||
test("GET /models returns {models:[...]} from injected listModels stub", async () => {
|
||||
test("GET /models returns session choices from the installation model catalog", async () => {
|
||||
const app = buildApp(loadConfig({ THT_HARNESS_DIR: "../harness" }), {
|
||||
thtRunner: {} as any,
|
||||
listModels: async () => [
|
||||
{ provider: "zai", id: "glm-5.2", name: "GLM 5.2", reasoning: true },
|
||||
],
|
||||
runtimeModelCatalog: {
|
||||
defaultSession: "zai/glm-5.2",
|
||||
defaultMetadataGeneration: null,
|
||||
embedding: { id: "ollama/qwen3-embedding:0.6b", dimensions: 1024 },
|
||||
sessionModels: () => [{
|
||||
id: "zai/glm-5.2", provider: "zai", model: "glm-5.2", label: "GLM 5.2",
|
||||
upstreamModel: "glm-5.2", authentication: { mode: "pi_auth" },
|
||||
sessionAdapter: { mode: "pi_builtin" }, session: { reasoning: true },
|
||||
}],
|
||||
metadataModels: () => [],
|
||||
hasSession: (id: string) => id === "zai/glm-5.2",
|
||||
},
|
||||
// Runtime introspection is a health gate for starting a session, not a second catalog.
|
||||
listModels: async () => { throw new Error("Pi is unavailable"); },
|
||||
});
|
||||
|
||||
const res = await app.inject({ method: "GET", url: "/models" });
|
||||
@@ -172,36 +183,17 @@ test("GET /models returns {models:[...]} from injected listModels stub", async (
|
||||
});
|
||||
});
|
||||
|
||||
test("GET /models returns {models:[]} when listModels throws (graceful fallback)", async () => {
|
||||
test("GET /models returns an empty list when the catalog has no session models", async () => {
|
||||
const app = buildApp(loadConfig({ THT_HARNESS_DIR: "../harness" }), {
|
||||
thtRunner: {} as any,
|
||||
listModels: async () => { throw new Error("Pi not running"); },
|
||||
});
|
||||
|
||||
const res = await app.inject({ method: "GET", url: "/models" });
|
||||
|
||||
expect(res.statusCode).toBe(200);
|
||||
expect(res.json()).toEqual({ models: [] });
|
||||
});
|
||||
|
||||
test("GET /models logs a sanitized warning when listing fails", async () => {
|
||||
const app = buildApp(loadConfig({ THT_HARNESS_DIR: "../harness" }), {
|
||||
thtRunner: {} as any,
|
||||
listModels: async () => { throw new Error("credential-value-must-not-appear"); },
|
||||
});
|
||||
const warn = vi.spyOn(app.log, "warn");
|
||||
|
||||
const res = await app.inject({ method: "GET", url: "/models" });
|
||||
|
||||
expect(res.json()).toEqual({ models: [] });
|
||||
expect(JSON.stringify(warn.mock.calls)).not.toContain("credential-value-must-not-appear");
|
||||
expect(warn).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("GET /models with empty listModels stub returns empty array", async () => {
|
||||
const app = buildApp(loadConfig({ THT_HARNESS_DIR: "../harness" }), {
|
||||
thtRunner: {} as any,
|
||||
listModels: async () => [],
|
||||
runtimeModelCatalog: {
|
||||
defaultSession: null,
|
||||
defaultMetadataGeneration: null,
|
||||
embedding: null,
|
||||
sessionModels: () => [],
|
||||
metadataModels: () => [],
|
||||
hasSession: () => false,
|
||||
},
|
||||
});
|
||||
|
||||
const res = await app.inject({ method: "GET", url: "/models" });
|
||||
|
||||
@@ -14,7 +14,7 @@ import type { AuthDiagnoser, AuthDiagnostics } from "../src/auth/diagnostics.js"
|
||||
|
||||
const workspace: CanonicalWorkspace = {
|
||||
workspace: {
|
||||
schema_version: 3,
|
||||
schema_version: 4,
|
||||
id: "psd-clinical",
|
||||
name: "Policlinico San Donato",
|
||||
description: "Clinical analytics workspace",
|
||||
@@ -26,20 +26,6 @@ const workspace: CanonicalWorkspace = {
|
||||
schema: "datawarehouse",
|
||||
supported_transports: ["postgres_direct"],
|
||||
},
|
||||
semantic_index: {
|
||||
vector_store: {
|
||||
engine: "qdrant",
|
||||
collection: "psd-clinical",
|
||||
dimensions: 1024,
|
||||
distance: "cosine",
|
||||
},
|
||||
embedding: {
|
||||
provider: "ollama_internal",
|
||||
model: "qwen3-embedding:0.6b",
|
||||
dimensions: 1024,
|
||||
},
|
||||
},
|
||||
llm_policy: { allowed: ["zai/glm-5.2"] },
|
||||
};
|
||||
|
||||
const revision: WorkspaceRevision = {
|
||||
@@ -194,7 +180,7 @@ test("lists workspace summaries and reads a validated immutable workspace", asyn
|
||||
expect(read.json()).toEqual({ workspace, revision });
|
||||
});
|
||||
|
||||
test("validates a schema v3 workspace without mutating the repository", async () => {
|
||||
test("validates a schema v4 workspace without mutating the repository", async () => {
|
||||
const app = appFor(registryFake());
|
||||
|
||||
const response = await app.inject({
|
||||
@@ -284,7 +270,7 @@ test.each([1, 2])("rejects schema v%s at the validation boundary with a sanitize
|
||||
expect(response.body).not.toMatch(/migration_required|schema version/i);
|
||||
});
|
||||
|
||||
test("runs diagnostics for a schema v3 workspace", async () => {
|
||||
test("runs diagnostics for a schema v4 workspace", async () => {
|
||||
const diagnose = vi.fn(async () => ({ activatable: true, diagnostics: [] }));
|
||||
const app = appFor(registryFake(), diagnose);
|
||||
|
||||
|
||||
@@ -27,9 +27,9 @@ test("saveSettings writes the file and loadSettings reads it back", () => {
|
||||
const dir = mkdtempSync(join(tmpdir(), "tht-set-"));
|
||||
try {
|
||||
const cfg = cfgWith(join(dir, "nested", "settings.json"));
|
||||
const saved = saveSettings(cfg, { workspace: "psd", provider: "zai", model: "glm-5.2", thinking: "medium" });
|
||||
expect(saved.model).toBe("glm-5.2");
|
||||
expect(loadSettings(cfg)).toEqual({ workspace: "psd", provider: "zai", model: "glm-5.2", thinking: "medium" });
|
||||
const saved = saveSettings(cfg, { workspace: "psd", thinking: "medium" });
|
||||
expect(saved.thinking).toBe("medium");
|
||||
expect(loadSettings(cfg)).toEqual({ workspace: "psd", thinking: "medium" });
|
||||
} finally {
|
||||
rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
@@ -55,11 +55,11 @@ test("saveSettings restores the previous file when post-rename directory durabil
|
||||
const dir = mkdtempSync(join(tmpdir(), "tht-set-transaction-"));
|
||||
try {
|
||||
const cfg = cfgWith(join(dir, "settings.json"));
|
||||
saveSettings(cfg, { provider: "old", model: "old-model", thinking: "low" });
|
||||
saveSettings(cfg, { thinking: "low" });
|
||||
let syncs = 0;
|
||||
expect(() => saveSettings(
|
||||
cfg,
|
||||
{ provider: "new", model: "new-model", thinking: "high" },
|
||||
{ thinking: "high" },
|
||||
{
|
||||
syncDirectory(directory: string) {
|
||||
syncs += 1;
|
||||
@@ -69,7 +69,7 @@ test("saveSettings restores the previous file when post-rename directory durabil
|
||||
},
|
||||
},
|
||||
)).toThrow(/directory fsync failure/);
|
||||
expect(loadSettings(cfg)).toEqual({ provider: "old", model: "old-model", thinking: "low" });
|
||||
expect(loadSettings(cfg)).toEqual({ thinking: "low" });
|
||||
expect(syncs).toBeGreaterThanOrEqual(2);
|
||||
} finally {
|
||||
rmSync(dir, { recursive: true, force: true });
|
||||
|
||||
@@ -8,18 +8,11 @@ const keywordIndexes = [
|
||||
];
|
||||
|
||||
const workspace: CanonicalWorkspace = {
|
||||
workspace: { schema_version: 3, id: "psd", name: "PSD", language: "it" },
|
||||
workspace: { schema_version: 4, id: "psd", name: "PSD", language: "it" },
|
||||
dwh: {
|
||||
engine: "postgres", database: "warehouse", schema: "public",
|
||||
supported_transports: ["postgres_direct"],
|
||||
},
|
||||
semantic_index: {
|
||||
vector_store: { engine: "qdrant", collection: "psd", dimensions: 1024, distance: "cosine" },
|
||||
embedding: {
|
||||
provider: "ollama_internal", model: "qwen3-embedding:0.6b", dimensions: 1024,
|
||||
},
|
||||
},
|
||||
llm_policy: { allowed: ["zai/glm-5.2"] },
|
||||
};
|
||||
|
||||
function runner(request: (...args: any[]) => Promise<any>) {
|
||||
|
||||
@@ -19,12 +19,13 @@ afterEach(() => {
|
||||
const semanticRuntime = {
|
||||
internalQdrantUrl: "http://qdrant:6333",
|
||||
internalEmbeddingUrl: "http://embedding:11434",
|
||||
internalEmbeddingId: "ollama/qwen3-embedding:0.6b",
|
||||
internalEmbeddingModel: "qwen3-embedding:0.6b",
|
||||
internalEmbeddingDimensions: 1024,
|
||||
};
|
||||
|
||||
const baseWorkspace = parseWorkspaceYaml(`workspace:
|
||||
schema_version: 3
|
||||
schema_version: 4
|
||||
id: psd-clinical
|
||||
name: Runtime Lease
|
||||
language: en
|
||||
@@ -33,21 +34,9 @@ dwh:
|
||||
database: analytics
|
||||
schema: mart
|
||||
supported_transports: [postgres_direct]
|
||||
semantic_index:
|
||||
vector_store:
|
||||
engine: qdrant
|
||||
collection: psd-clinical
|
||||
dimensions: 1024
|
||||
distance: cosine
|
||||
embedding:
|
||||
provider: ollama_internal
|
||||
model: qwen3-embedding:0.6b
|
||||
dimensions: 1024
|
||||
llm_policy:
|
||||
allowed: [zai/glm-5.2]
|
||||
`);
|
||||
const filesystemWorkspace = parseWorkspaceYaml(`${baseWorkspace ? '' : ''}workspace:
|
||||
schema_version: 3
|
||||
schema_version: 4
|
||||
id: fs-workspace
|
||||
name: Filesystem
|
||||
language: en
|
||||
@@ -56,25 +45,13 @@ dwh:
|
||||
database: analytics
|
||||
schema: mart
|
||||
supported_transports: [postgres_direct]
|
||||
semantic_index:
|
||||
vector_store:
|
||||
engine: qdrant
|
||||
collection: fs-workspace
|
||||
dimensions: 1024
|
||||
distance: cosine
|
||||
embedding:
|
||||
provider: ollama_internal
|
||||
model: qwen3-embedding:0.6b
|
||||
dimensions: 1024
|
||||
llm_policy:
|
||||
allowed: [zai/glm-5.2]
|
||||
evidence:
|
||||
source:
|
||||
type: filesystem
|
||||
uri: fs-workspace/evidence
|
||||
`);
|
||||
const privateHttpWorkspace = parseWorkspaceYaml(`workspace:
|
||||
schema_version: 3
|
||||
schema_version: 4
|
||||
id: http-workspace
|
||||
name: Http
|
||||
language: en
|
||||
@@ -83,18 +60,6 @@ dwh:
|
||||
database: analytics
|
||||
schema: mart
|
||||
supported_transports: [postgres_direct]
|
||||
semantic_index:
|
||||
vector_store:
|
||||
engine: qdrant
|
||||
collection: http-workspace
|
||||
dimensions: 1024
|
||||
distance: cosine
|
||||
embedding:
|
||||
provider: ollama_internal
|
||||
model: qwen3-embedding:0.6b
|
||||
dimensions: 1024
|
||||
llm_policy:
|
||||
allowed: [zai/glm-5.2]
|
||||
evidence:
|
||||
source:
|
||||
type: http
|
||||
@@ -125,7 +90,7 @@ function runtime(workspace = baseWorkspace, workspaceId = workspace.workspace.id
|
||||
bindingDigest: "sha256:bindings",
|
||||
semanticQdrantUrl: "http://qdrant:6333",
|
||||
effectiveConfig: {
|
||||
schemaVersion: 1,
|
||||
schemaVersion: 2,
|
||||
dwh: {
|
||||
engine: "postgres",
|
||||
database: "analytics",
|
||||
@@ -136,7 +101,7 @@ function runtime(workspace = baseWorkspace, workspaceId = workspace.workspace.id
|
||||
user: "reader",
|
||||
},
|
||||
vector: { collection: workspaceId, dimensions: 1024, distance: "cosine" },
|
||||
embedding: { model: "qwen3-embedding:0.6b", dimensions: 1024 },
|
||||
embedding: { id: "ollama/qwen3-embedding:0.6b", model: "qwen3-embedding:0.6b", dimensions: 1024 },
|
||||
roots: { artifacts: "/data/artifacts", indexes: "/data/indexes" },
|
||||
},
|
||||
effectiveConfigIdentity: "workspace://psd-clinical@v1:" + "d".repeat(64),
|
||||
|
||||
@@ -28,13 +28,15 @@ test("job state creates durable 0600 JSON and enforces same-revision resume", as
|
||||
catalogBlob: "c".repeat(40),
|
||||
configDigest: "sha256:config",
|
||||
bindingDigest: "sha256:bindings",
|
||||
embeddingId: "ollama/qwen3-embedding:0.6b",
|
||||
embeddingDimensions: 1024,
|
||||
});
|
||||
|
||||
const path = store.jobPath(job.runId);
|
||||
expect(existsSync(path)).toBe(true);
|
||||
expect(statSync(path).mode & 0o777).toBe(0o600);
|
||||
expect(JSON.parse(readFileSync(path, "utf8"))).toMatchObject({
|
||||
schemaVersion: 1,
|
||||
schemaVersion: 2,
|
||||
operation: "preprocess dwh",
|
||||
workspaceId: "psd-clinical",
|
||||
workspaceRevision: "a".repeat(40),
|
||||
@@ -42,6 +44,8 @@ test("job state creates durable 0600 JSON and enforces same-revision resume", as
|
||||
catalogBlob: "c".repeat(40),
|
||||
configDigest: "sha256:config",
|
||||
bindingDigest: "sha256:bindings",
|
||||
embeddingId: "ollama/qwen3-embedding:0.6b",
|
||||
embeddingDimensions: 1024,
|
||||
});
|
||||
|
||||
await expect(store.beginJob({
|
||||
@@ -52,6 +56,20 @@ test("job state creates durable 0600 JSON and enforces same-revision resume", as
|
||||
catalogBlob: "c".repeat(40),
|
||||
configDigest: "sha256:config",
|
||||
bindingDigest: "sha256:bindings",
|
||||
embeddingId: "ollama/qwen3-embedding:0.6b",
|
||||
embeddingDimensions: 1024,
|
||||
})).rejects.toMatchObject({ code: "preprocessing_resume_mismatch" });
|
||||
|
||||
await expect(store.beginJob({
|
||||
operation: "preprocess dwh",
|
||||
runId: job.runId,
|
||||
workspaceRevision: "a".repeat(40),
|
||||
descriptorBlob: "b".repeat(40),
|
||||
catalogBlob: "c".repeat(40),
|
||||
configDigest: "sha256:config",
|
||||
bindingDigest: "sha256:bindings",
|
||||
embeddingId: "ollama/replacement-embedding",
|
||||
embeddingDimensions: 1024,
|
||||
})).rejects.toMatchObject({ code: "preprocessing_resume_mismatch" });
|
||||
|
||||
const resumed = await store.beginJob({
|
||||
@@ -62,6 +80,8 @@ test("job state creates durable 0600 JSON and enforces same-revision resume", as
|
||||
catalogBlob: "c".repeat(40),
|
||||
configDigest: "sha256:config",
|
||||
bindingDigest: "sha256:bindings",
|
||||
embeddingId: "ollama/qwen3-embedding:0.6b",
|
||||
embeddingDimensions: 1024,
|
||||
});
|
||||
expect(resumed.runId).toBe(job.runId);
|
||||
});
|
||||
|
||||
@@ -75,10 +75,6 @@ function workspaceVariant(
|
||||
return {
|
||||
...workspace,
|
||||
workspace: { ...workspace.workspace, ...changes, id },
|
||||
semantic_index: {
|
||||
...workspace.semantic_index,
|
||||
vector_store: { ...workspace.semantic_index.vector_store, collection: id },
|
||||
},
|
||||
...(workspace.evidence?.source.type === "filesystem"
|
||||
? {
|
||||
evidence: {
|
||||
@@ -182,7 +178,7 @@ test("shared deployment fixtures remain valid standalone descriptors with canoni
|
||||
|
||||
expect(smoke).toMatchObject({
|
||||
workspace: {
|
||||
schema_version: 3,
|
||||
schema_version: 4,
|
||||
id: "local",
|
||||
name: "Local",
|
||||
description: "Isolated workspace registry smoke fixture.",
|
||||
@@ -193,14 +189,14 @@ test("shared deployment fixtures remain valid standalone descriptors with canoni
|
||||
},
|
||||
});
|
||||
expect(task13).toMatchObject({
|
||||
workspace: { schema_version: 3, id: "task13-smoke", name: "Task 13 Smoke" },
|
||||
workspace: { schema_version: 4, id: "task13-smoke", name: "Task 13 Smoke" },
|
||||
evidence: {
|
||||
source: { type: "filesystem", uri: "task13-smoke/evidence", patterns: ["**/*.md"] },
|
||||
policy: { max_chunk_chars: 4000, retain_published_generations: 3 },
|
||||
},
|
||||
});
|
||||
expect(windows).toMatchObject({
|
||||
workspace: { schema_version: 3, id: "task13-windows", name: "Task 13 Windows" },
|
||||
workspace: { schema_version: 4, id: "task13-windows", name: "Task 13 Windows" },
|
||||
evidence: {
|
||||
source: { type: "filesystem", uri: "task13-windows/evidence", patterns: ["**/*.md"] },
|
||||
policy: { max_chunk_chars: 4000, retain_published_generations: 3 },
|
||||
@@ -282,7 +278,7 @@ test("registry rejects orphan descriptors, metadata mismatches, and the retired
|
||||
});
|
||||
});
|
||||
|
||||
test("Windows clone contract copies the shared complete schema v3 descriptor into the nested registry layout", () => {
|
||||
test("Windows clone contract copies the shared complete schema v4 descriptor into the nested registry layout", () => {
|
||||
const descriptor = parseWorkspaceYaml(readFixture("workspace-registry-windows.yaml"));
|
||||
const windows = readFileSync(
|
||||
new URL("../../scripts/test-windows-clone-contract.ps1", import.meta.url),
|
||||
@@ -299,7 +295,7 @@ test("Windows clone contract copies the shared complete schema v3 descriptor int
|
||||
expect(windows).not.toContain('schema_version: 3');
|
||||
expect(descriptor).toMatchObject({
|
||||
workspace: {
|
||||
schema_version: 3,
|
||||
schema_version: 4,
|
||||
id: "task13-windows",
|
||||
name: "Task 13 Windows",
|
||||
language: "en",
|
||||
|
||||
@@ -15,7 +15,7 @@ import {
|
||||
import type { WorkspaceRegistryConfig } from "../src/workspaces/types.js";
|
||||
|
||||
const validYaml = `workspace:
|
||||
schema_version: 3
|
||||
schema_version: 4
|
||||
id: psd-clinical
|
||||
name: Policlinico San Donato
|
||||
language: it
|
||||
@@ -24,18 +24,6 @@ dwh:
|
||||
database: postgres
|
||||
schema: datawarehouse
|
||||
supported_transports: [postgres_direct]
|
||||
semantic_index:
|
||||
vector_store:
|
||||
engine: qdrant
|
||||
collection: psd-clinical
|
||||
dimensions: 1024
|
||||
distance: cosine
|
||||
embedding:
|
||||
provider: ollama_internal
|
||||
model: qwen3-embedding:0.6b
|
||||
dimensions: 1024
|
||||
llm_policy:
|
||||
allowed: [zai/glm-5.2]
|
||||
`;
|
||||
|
||||
function withFilesystemEvidence(source: string, id = "psd-clinical"): string {
|
||||
@@ -146,7 +134,7 @@ function legacyV1Yaml(source = validYaml): string {
|
||||
.replace(" model: qwen3-embedding:0.6b", " model: nomic-embed-text-v2-moe")
|
||||
.replace(" dimensions: 1024", " dimensions: 768")
|
||||
.replace("distance: cosine\n", "distance: cosine\n supported_transports: [pgvector_direct]\n")
|
||||
.replace("schema_version: 3", "schema_version: 1");
|
||||
.replace("schema_version: 4", "schema_version: 1");
|
||||
}
|
||||
|
||||
function legacyV2Yaml(source = validYaml): string {
|
||||
@@ -158,7 +146,7 @@ function legacyV2Yaml(source = validYaml): string {
|
||||
.replace(" model: qwen3-embedding:0.6b", " model: nomic-embed-text-v2-moe")
|
||||
.replace(" dimensions: 1024", " dimensions: 768")
|
||||
.replace("distance: cosine\n", "distance: cosine\n supported_transports: [pgvector_direct]\n")
|
||||
.replace("schema_version: 3", "schema_version: 2");
|
||||
.replace("schema_version: 4", "schema_version: 2");
|
||||
}
|
||||
|
||||
const runFile = promisify(execFile);
|
||||
@@ -197,7 +185,7 @@ async function fixture(workspaceSource = validYaml): Promise<{
|
||||
await git(source, ["init", "--initial-branch=main"]);
|
||||
await git(source, ["config", "user.name", "Workspace Registry Test"]);
|
||||
await git(source, ["config", "user.email", "workspace-registry@example.invalid"]);
|
||||
const workspace = workspaceSource.includes("schema_version: 3")
|
||||
const workspace = workspaceSource.includes("schema_version: 4")
|
||||
? parseWorkspaceYaml(workspaceSource) : undefined;
|
||||
mkdirSync(join(source, "psd-clinical"), { recursive: true });
|
||||
writeFileSync(join(source, "thoth-workspaces.yaml"), catalogYaml([
|
||||
@@ -256,7 +244,7 @@ async function multiWorkspaceFixture(workspaces: Record<string, string>): Promis
|
||||
await git(source, ["config", "user.email", "workspace-registry@example.invalid"]);
|
||||
const entries = [];
|
||||
for (const [id, workspaceSource] of Object.entries(workspaces)) {
|
||||
const workspace = workspaceSource.includes("schema_version: 3") ? parseWorkspaceYaml(workspaceSource) : undefined;
|
||||
const workspace = workspaceSource.includes("schema_version: 4") ? parseWorkspaceYaml(workspaceSource) : undefined;
|
||||
entries.push({ id, name: workspace.workspace.name, ...(workspace.workspace.description ? { description: workspace.workspace.description } : {}) });
|
||||
mkdirSync(join(source, id), { recursive: true });
|
||||
writeFileSync(join(source, id, "workspace.yaml"), workspaceSource);
|
||||
@@ -788,7 +776,7 @@ test("normalizes historical operational state during offline fallback after rest
|
||||
expect(read.revision).not.toHaveProperty("state");
|
||||
});
|
||||
|
||||
test("fails closed when a retained snapshot descriptor is not schema v3", async () => {
|
||||
test("fails closed when a retained snapshot descriptor is not schema v4", async () => {
|
||||
const remote = await fixture();
|
||||
const root = join(remote.root, "registry");
|
||||
await new WorkspaceRegistry(config(root, remote.remote)).bootstrap();
|
||||
@@ -820,45 +808,6 @@ test("keeps the last valid snapshot when a pulled commit has invalid YAML", asyn
|
||||
});
|
||||
});
|
||||
|
||||
test("rejects duplicate schema v3 collection ownership and keeps the previous active snapshot", async () => {
|
||||
const v3Yaml = validYaml;
|
||||
const remote = await multiWorkspaceFixture({
|
||||
"psd-clinical": v3Yaml,
|
||||
"research-clinical": v3Yaml
|
||||
.replace("id: psd-clinical", "id: research-clinical")
|
||||
.replace("name: Policlinico San Donato", "name: Research Clinical")
|
||||
.replace("collection: psd-clinical", "collection: research-clinical"),
|
||||
});
|
||||
const registry = new WorkspaceRegistry(config(join(remote.root, "registry"), remote.remote));
|
||||
await registry.bootstrap();
|
||||
|
||||
writeFileSync(
|
||||
join(remote.source, "research-clinical", "workspace.yaml"),
|
||||
v3Yaml
|
||||
.replace("id: psd-clinical", "id: research-clinical")
|
||||
.replace("name: Policlinico San Donato", "name: Research Clinical")
|
||||
.replace("collection: psd-clinical", "collection: shared"),
|
||||
);
|
||||
writeFileSync(
|
||||
join(remote.source, "psd-clinical", "workspace.yaml"),
|
||||
v3Yaml.replace("collection: psd-clinical", "collection: shared"),
|
||||
);
|
||||
await git(remote.source, ["add", "-A"]);
|
||||
await git(remote.source, ["commit", "-m", "Duplicate collection ownership"]);
|
||||
await git(remote.source, ["push", "origin", "main"]);
|
||||
|
||||
await expect(registry.pull()).rejects.toMatchObject({
|
||||
code: "workspace_invalid",
|
||||
message: "Workspace repository content is invalid",
|
||||
});
|
||||
await expect(registry.read("psd-clinical")).resolves.toMatchObject({
|
||||
revision: { commit: remote.initialCommit },
|
||||
});
|
||||
await expect(registry.read("research-clinical")).resolves.toMatchObject({
|
||||
revision: { commit: remote.initialCommit },
|
||||
});
|
||||
});
|
||||
|
||||
test("retains a historical snapshot while a resumable manifest still references its revision", async () => {
|
||||
const remote = await fixture();
|
||||
const root = join(remote.root, "registry");
|
||||
|
||||
@@ -66,7 +66,7 @@ workspaces: [{id: psd-clinical, name: Runtime Lease}]
|
||||
`);
|
||||
mkdirSync(join(source, "psd-clinical", "evidence"), { recursive: true });
|
||||
writeFileSync(join(source, "psd-clinical", "workspace.yaml"), `workspace:
|
||||
schema_version: 3
|
||||
schema_version: 4
|
||||
id: psd-clinical
|
||||
name: Runtime Lease
|
||||
language: en
|
||||
@@ -75,18 +75,6 @@ dwh:
|
||||
database: analytics
|
||||
schema: mart
|
||||
supported_transports: [postgres_direct]
|
||||
semantic_index:
|
||||
vector_store:
|
||||
engine: qdrant
|
||||
collection: psd-clinical
|
||||
dimensions: 1024
|
||||
distance: cosine
|
||||
embedding:
|
||||
provider: ollama_internal
|
||||
model: qwen3-embedding:0.6b
|
||||
dimensions: 1024
|
||||
llm_policy:
|
||||
allowed: [zai/glm-5.2]
|
||||
evidence:
|
||||
source:
|
||||
type: filesystem
|
||||
|
||||
@@ -21,7 +21,7 @@ const thtBin = join(harnessDir, ".venv", "bin", "tht");
|
||||
const roots: string[] = [];
|
||||
|
||||
const canonicalWorkspace = `workspace:
|
||||
schema_version: 3
|
||||
schema_version: 4
|
||||
id: psd-clinical
|
||||
name: Runtime handoff
|
||||
language: en
|
||||
@@ -30,18 +30,6 @@ dwh:
|
||||
database: analytics
|
||||
schema: mart
|
||||
supported_transports: [postgres_direct]
|
||||
semantic_index:
|
||||
vector_store:
|
||||
engine: qdrant
|
||||
collection: psd-clinical
|
||||
dimensions: 1024
|
||||
distance: cosine
|
||||
embedding:
|
||||
provider: ollama_internal
|
||||
model: qwen3-embedding:0.6b
|
||||
dimensions: 1024
|
||||
llm_policy:
|
||||
allowed: [zai/glm-5.2]
|
||||
`;
|
||||
|
||||
const filesystemWorkspace = `${canonicalWorkspace}evidence:
|
||||
@@ -145,7 +133,7 @@ function runnerFor(f: Awaited<ReturnType<typeof fixture>>): ThtRunner {
|
||||
} as any);
|
||||
}
|
||||
|
||||
test("real schema-v3 registry revision loads through ThtRunner and the harness contract", async () => {
|
||||
test("real schema-v4 registry revision loads through ThtRunner and the harness contract", async () => {
|
||||
const f = await fixture();
|
||||
const runner = runnerFor(f);
|
||||
|
||||
|
||||
@@ -12,8 +12,8 @@ import {
|
||||
import { supportsSessionRuntime } from "../src/workspaces/bindings.js";
|
||||
import { parseWorkspaceYaml } from "../src/workspaces/schema.js";
|
||||
|
||||
const workspaceV3 = parseWorkspaceYaml(`workspace:
|
||||
schema_version: 3
|
||||
const workspaceV4 = parseWorkspaceYaml(`workspace:
|
||||
schema_version: 4
|
||||
id: psd-clinical
|
||||
name: Policlinico San Donato
|
||||
language: it
|
||||
@@ -22,19 +22,6 @@ dwh:
|
||||
database: postgres
|
||||
schema: datawarehouse
|
||||
supported_transports: [postgres_direct, rest_api, ssh_tunnel]
|
||||
semantic_index:
|
||||
vector_store:
|
||||
engine: qdrant
|
||||
collection: psd-clinical
|
||||
dimensions: 1024
|
||||
distance: cosine
|
||||
embedding:
|
||||
provider: ollama_internal
|
||||
model: qwen3-embedding:0.6b
|
||||
dimensions: 1024
|
||||
llm_policy:
|
||||
default: zai/glm-5.2
|
||||
allowed: [zai/glm-5.2]
|
||||
`);
|
||||
const paths: RuntimePaths = {
|
||||
sessions: "/data/workspaces/psd-clinical/sessions",
|
||||
@@ -45,6 +32,7 @@ const paths: RuntimePaths = {
|
||||
const semanticRuntime: SemanticRuntimeConfig = {
|
||||
internalQdrantUrl: "http://qdrant:6333",
|
||||
internalEmbeddingUrl: "http://embedding:11434",
|
||||
internalEmbeddingId: "ollama/qwen3-embedding:0.6b",
|
||||
internalEmbeddingModel: "qwen3-embedding:0.6b",
|
||||
internalEmbeddingDimensions: 1024,
|
||||
};
|
||||
@@ -63,8 +51,8 @@ const directBindings: RuntimeBindings = {
|
||||
evidence: { missing: [], values: {} },
|
||||
};
|
||||
|
||||
test("renders only the schema-v3 internal Qdrant and Ollama runtime shape", () => {
|
||||
const rendered = parse(renderRuntimeConfig(workspaceV3, directBindings, paths, {
|
||||
test("derives the internal Qdrant and Ollama runtime shape from workspace v4 plus installation config", () => {
|
||||
const rendered = parse(renderRuntimeConfig(workspaceV4, directBindings, paths, {
|
||||
workspaceId: "psd-clinical", workspaceRevision: "a".repeat(40),
|
||||
}, {}, semanticRuntime));
|
||||
|
||||
@@ -95,8 +83,8 @@ test("renders only the schema-v3 internal Qdrant and Ollama runtime shape", () =
|
||||
expect(rendered).not.toHaveProperty("vector_rest");
|
||||
});
|
||||
|
||||
test("renders schema-v3 DWH REST without exposing secret contents", () => {
|
||||
const rendered = parse(renderRuntimeConfig(workspaceV3, {
|
||||
test("renders workspace-v4 DWH REST without exposing secret contents", () => {
|
||||
const rendered = parse(renderRuntimeConfig(workspaceV4, {
|
||||
dwh: {
|
||||
transport: "rest_api", missing: [], values: {
|
||||
THT_WS_PSD_CLINICAL_DWH_BASE_URL: "https://dwh.example.test",
|
||||
@@ -152,7 +140,7 @@ function evidenceWorkspace(
|
||||
}
|
||||
|
||||
const canonicalEvidenceWorkspace = `workspace:
|
||||
schema_version: 3
|
||||
schema_version: 4
|
||||
id: psd-clinical
|
||||
name: Runtime Evidence
|
||||
language: en
|
||||
@@ -161,18 +149,6 @@ dwh:
|
||||
database: analytics
|
||||
schema: mart
|
||||
supported_transports: [postgres_direct]
|
||||
semantic_index:
|
||||
vector_store:
|
||||
engine: qdrant
|
||||
collection: psd-clinical
|
||||
dimensions: 1024
|
||||
distance: cosine
|
||||
embedding:
|
||||
provider: ollama_internal
|
||||
model: qwen3-embedding:0.6b
|
||||
dimensions: 1024
|
||||
llm_policy:
|
||||
allowed: [zai/glm-5.2]
|
||||
`;
|
||||
|
||||
const evidenceRevision = "1".repeat(40);
|
||||
@@ -374,7 +350,7 @@ test("renders static S3 Evidence with endpoint policy, limits, and file paths bu
|
||||
|
||||
test("omits Evidence configuration and policy when the descriptor has no Evidence", () => {
|
||||
const rendered = parse(renderRuntimeConfig(
|
||||
workspaceV3,
|
||||
workspaceV4,
|
||||
directBindings,
|
||||
paths,
|
||||
evidenceContext,
|
||||
|
||||
@@ -14,7 +14,7 @@ const roots: string[] = [];
|
||||
|
||||
function workspace(extra = "") {
|
||||
return parseWorkspaceYaml(`workspace:
|
||||
schema_version: 3
|
||||
schema_version: 4
|
||||
id: psd-clinical
|
||||
name: Policlinico San Donato
|
||||
language: en
|
||||
@@ -23,10 +23,6 @@ dwh:
|
||||
database: postgres
|
||||
schema: datawarehouse
|
||||
supported_transports: [postgres_direct, rest_api, ssh_tunnel]
|
||||
semantic_index:
|
||||
vector_store: { engine: qdrant, collection: psd-clinical, dimensions: 1024, distance: cosine }
|
||||
embedding: { provider: ollama_internal, model: qwen3-embedding:0.6b, dimensions: 1024 }
|
||||
llm_policy: { allowed: [zai/glm-5.2] }
|
||||
${extra}`);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
import { expect, test } from "vitest";
|
||||
import { migrateWorkspaceV3Yaml, parseWorkspaceYaml } from "../src/workspaces/schema.js";
|
||||
|
||||
const legacy = `workspace:
|
||||
schema_version: 3
|
||||
id: abc
|
||||
name: Example
|
||||
language: en
|
||||
dwh:
|
||||
engine: postgres
|
||||
database: warehouse
|
||||
schema: public
|
||||
supported_transports: [postgres_direct]
|
||||
semantic_index:
|
||||
vector_store: {engine: qdrant, collection: abc, dimensions: 1024, distance: cosine}
|
||||
embedding: {provider: ollama_internal, model: qwen3-embedding:0.6b, dimensions: 1024}
|
||||
llm_policy:
|
||||
default: zai/glm-5.3
|
||||
allowed: [zai/glm-5.3]
|
||||
`;
|
||||
|
||||
test("strict workspace v4 rejects model-bearing v3 descriptors", () => {
|
||||
expect(() => parseWorkspaceYaml(legacy)).toThrow();
|
||||
});
|
||||
|
||||
test("v3 to v4 migration removes only model/vector policy and bumps the version", () => {
|
||||
const migrated = migrateWorkspaceV3Yaml(legacy);
|
||||
const workspace = parseWorkspaceYaml(migrated);
|
||||
|
||||
expect(workspace.workspace).toMatchObject({ schema_version: 4, id: "abc" });
|
||||
expect(migrated).not.toMatch(/semantic_index|llm_policy/);
|
||||
expect(workspace.dwh).toEqual({
|
||||
engine: "postgres", database: "warehouse", schema: "public",
|
||||
supported_transports: ["postgres_direct"],
|
||||
});
|
||||
});
|
||||
@@ -9,8 +9,8 @@ import {
|
||||
} from "../src/workspaces/bindings.js";
|
||||
import { parseWorkspaceYaml } from "../src/workspaces/schema.js";
|
||||
|
||||
const workspaceV3 = parseWorkspaceYaml(`workspace:
|
||||
schema_version: 3
|
||||
const workspaceV4 = parseWorkspaceYaml(`workspace:
|
||||
schema_version: 4
|
||||
id: psd-clinical
|
||||
name: Policlinico San Donato
|
||||
language: it
|
||||
@@ -19,10 +19,6 @@ dwh:
|
||||
database: postgres
|
||||
schema: datawarehouse
|
||||
supported_transports: [postgres_direct, rest_api, ssh_tunnel]
|
||||
semantic_index:
|
||||
vector_store: { engine: qdrant, collection: psd-clinical, dimensions: 1024, distance: cosine }
|
||||
embedding: { provider: ollama_internal, model: qwen3-embedding:0.6b, dimensions: 1024 }
|
||||
llm_policy: { allowed: [zai/glm-5.2] }
|
||||
`);
|
||||
const temporaryRoots: string[] = [];
|
||||
|
||||
@@ -40,9 +36,9 @@ function secretPath(name: string): { root: string; path: string } {
|
||||
return { root: secrets, path };
|
||||
}
|
||||
|
||||
test("resolves schema-v3 direct DWH bindings from the stable namespace", () => {
|
||||
test("resolves workspace-v4 direct DWH bindings from the stable namespace", () => {
|
||||
const password = secretPath("dwh-password");
|
||||
const result = resolveBinding(workspaceV3, "DWH", {
|
||||
const result = resolveBinding(workspaceV4, "DWH", {
|
||||
THT_WS_PSD_CLINICAL_DWH_TRANSPORT: "postgres_direct",
|
||||
THT_WS_PSD_CLINICAL_DWH_HOST: "dwh.internal",
|
||||
THT_WS_PSD_CLINICAL_DWH_PORT: "5432",
|
||||
@@ -61,14 +57,14 @@ test("resolves schema-v3 direct DWH bindings from the stable namespace", () => {
|
||||
});
|
||||
});
|
||||
|
||||
test("requires schema-v3 REST credentials unless the DWH diagnostic declares auth none", () => {
|
||||
expect(resolveBinding(workspaceV3, "DWH", {
|
||||
test("requires workspace-v4 REST credentials unless the DWH diagnostic declares auth none", () => {
|
||||
expect(resolveBinding(workspaceV4, "DWH", {
|
||||
THT_WS_PSD_CLINICAL_DWH_TRANSPORT: "rest_api",
|
||||
THT_WS_PSD_CLINICAL_DWH_BASE_URL: "https://dwh.example.test",
|
||||
}, []).missing).toContain("THT_WS_PSD_CLINICAL_DWH_API_KEY_FILE");
|
||||
|
||||
const noAuth = parseWorkspaceYaml(`workspace:
|
||||
schema_version: 3
|
||||
schema_version: 4
|
||||
id: psd-clinical
|
||||
name: No auth
|
||||
language: en
|
||||
@@ -77,16 +73,12 @@ dwh:
|
||||
database: postgres
|
||||
schema: public
|
||||
supported_transports: [rest_api]
|
||||
semantic_index:
|
||||
vector_store: { engine: qdrant, collection: psd-clinical, dimensions: 1024, distance: cosine }
|
||||
embedding: { provider: ollama_internal, model: qwen3-embedding:0.6b, dimensions: 1024 }
|
||||
diagnostics:
|
||||
dwh_rest:
|
||||
method: GET
|
||||
path: /health
|
||||
auth: none
|
||||
response: { database: database, schema: schema }
|
||||
llm_policy: { allowed: [zai/glm-5.2] }
|
||||
`);
|
||||
expect(resolveBinding(noAuth, "DWH", {
|
||||
THT_WS_PSD_CLINICAL_DWH_TRANSPORT: "rest_api",
|
||||
@@ -98,7 +90,7 @@ test("rejects unsupported transports and secret paths outside configured roots",
|
||||
const outside = secretPath("outside-password");
|
||||
const allowed = secretPath("allowed-password");
|
||||
const directOnly = parseWorkspaceYaml(`workspace:
|
||||
schema_version: 3
|
||||
schema_version: 4
|
||||
id: psd-clinical
|
||||
name: Direct only
|
||||
language: en
|
||||
@@ -107,15 +99,11 @@ dwh:
|
||||
database: postgres
|
||||
schema: public
|
||||
supported_transports: [postgres_direct]
|
||||
semantic_index:
|
||||
vector_store: { engine: qdrant, collection: psd-clinical, dimensions: 1024, distance: cosine }
|
||||
embedding: { provider: ollama_internal, model: qwen3-embedding:0.6b, dimensions: 1024 }
|
||||
llm_policy: { allowed: [zai/glm-5.2] }
|
||||
`);
|
||||
expect(resolveBinding(directOnly, "DWH", {
|
||||
THT_WS_PSD_CLINICAL_DWH_TRANSPORT: "rest_api",
|
||||
}, []).missing).toContain("THT_WS_PSD_CLINICAL_DWH_TRANSPORT");
|
||||
const result = resolveBinding(workspaceV3, "DWH", {
|
||||
const result = resolveBinding(workspaceV4, "DWH", {
|
||||
THT_WS_PSD_CLINICAL_DWH_TRANSPORT: "postgres_direct",
|
||||
THT_WS_PSD_CLINICAL_DWH_HOST: "dwh.internal",
|
||||
THT_WS_PSD_CLINICAL_DWH_PORT: "5432",
|
||||
@@ -128,7 +116,7 @@ llm_policy: { allowed: [zai/glm-5.2] }
|
||||
|
||||
test("runtime bindings contain only DWH and Evidence roles", () => {
|
||||
const password = secretPath("dwh-password");
|
||||
const bindings = resolveRuntimeBindings(workspaceV3, {
|
||||
const bindings = resolveRuntimeBindings(workspaceV4, {
|
||||
THT_WS_PSD_CLINICAL_DWH_TRANSPORT: "postgres_direct",
|
||||
THT_WS_PSD_CLINICAL_DWH_HOST: "dwh.internal",
|
||||
THT_WS_PSD_CLINICAL_DWH_PORT: "5432",
|
||||
@@ -143,7 +131,7 @@ test("runtime bindings contain only DWH and Evidence roles", () => {
|
||||
|
||||
function withEvidence(source: Record<string, unknown>) {
|
||||
return parseWorkspaceYaml(`workspace:
|
||||
schema_version: 3
|
||||
schema_version: 4
|
||||
id: psd-clinical
|
||||
name: Policlinico San Donato
|
||||
language: it
|
||||
@@ -152,10 +140,6 @@ dwh:
|
||||
database: postgres
|
||||
schema: datawarehouse
|
||||
supported_transports: [postgres_direct]
|
||||
semantic_index:
|
||||
vector_store: { engine: qdrant, collection: psd-clinical, dimensions: 1024, distance: cosine }
|
||||
embedding: { provider: ollama_internal, model: qwen3-embedding:0.6b, dimensions: 1024 }
|
||||
llm_policy: { allowed: [zai/glm-5.2] }
|
||||
evidence:
|
||||
source: ${JSON.stringify(source)}
|
||||
`);
|
||||
@@ -260,7 +244,7 @@ test("rejects relative, missing, directory, unreadable, and escaping symlink Evi
|
||||
});
|
||||
|
||||
test("includes Evidence binding completeness in session runtime support without changing v3 compatibility", () => {
|
||||
const unsigned = resolveRuntimeBindings(workspaceV3, {}, ["/run/secrets"]);
|
||||
const unsigned = resolveRuntimeBindings(workspaceV4, {}, ["/run/secrets"]);
|
||||
expect(unsigned.evidence).toEqual({ values: {}, missing: [] });
|
||||
expect(supportsSessionRuntime(unsigned)).toBe(true);
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
} from "../src/workspaces/catalog.js";
|
||||
|
||||
const descriptor = parseWorkspaceYaml(`workspace:
|
||||
schema_version: 3
|
||||
schema_version: 4
|
||||
id: psd
|
||||
name: Policlinico San Donato
|
||||
description: Clinical warehouse
|
||||
@@ -17,10 +17,6 @@ dwh:
|
||||
database: postgres
|
||||
schema: datawarehouse
|
||||
supported_transports: [rest_api]
|
||||
semantic_index:
|
||||
vector_store: { engine: qdrant, collection: psd, dimensions: 1024, distance: cosine }
|
||||
embedding: { provider: ollama_internal, model: qwen3-embedding:0.6b, dimensions: 1024 }
|
||||
llm_policy: { allowed: [zai/glm-5.2] }
|
||||
`);
|
||||
|
||||
test("parses the strict ordered root catalog", () => {
|
||||
|
||||
@@ -5,8 +5,8 @@ import { join } from "node:path";
|
||||
import { buildInstallationContract, renderWorkspaceDocs } from "../src/workspaces/contracts.js";
|
||||
import { type CanonicalWorkspace, parseWorkspaceYaml } from "../src/workspaces/schema.js";
|
||||
|
||||
const workspaceV3 = parseWorkspaceYaml(`workspace:
|
||||
schema_version: 3
|
||||
const workspaceV4 = parseWorkspaceYaml(`workspace:
|
||||
schema_version: 4
|
||||
id: psd-clinical
|
||||
name: Policlinico San Donato
|
||||
language: it
|
||||
@@ -15,17 +15,12 @@ dwh:
|
||||
database: postgres
|
||||
schema: datawarehouse
|
||||
supported_transports: [postgres_direct, rest_api, ssh_tunnel]
|
||||
semantic_index:
|
||||
vector_store: { engine: qdrant, collection: psd-clinical, dimensions: 1024, distance: cosine }
|
||||
embedding: { provider: ollama_internal, model: qwen3-embedding:0.6b, dimensions: 1024 }
|
||||
llm_policy:
|
||||
allowed: [zai/glm-5.2]
|
||||
`);
|
||||
|
||||
test("schema-v3 installation contracts expose only DWH bindings and no semantic variables", () => {
|
||||
const contract = buildInstallationContract(workspaceV3);
|
||||
test("workspace-v4 installation contracts expose only DWH bindings and no semantic variables", () => {
|
||||
const contract = buildInstallationContract(workspaceV4);
|
||||
const names = contract.variables.map((variable) => variable.name);
|
||||
const docs = renderWorkspaceDocs(workspaceV3);
|
||||
const docs = renderWorkspaceDocs(workspaceV4);
|
||||
|
||||
expect(contract.workspaceId).toBe("psd-clinical");
|
||||
expect(contract.namespace).toBe("PSD_CLINICAL");
|
||||
@@ -54,22 +49,22 @@ test.each([
|
||||
|
||||
test("validates public contract and documentation inputs at runtime", () => {
|
||||
const unsafeWorkspace = {
|
||||
...workspaceV3,
|
||||
workspace: { ...workspaceV3.workspace, id: "psd\nclinical" },
|
||||
...workspaceV4,
|
||||
workspace: { ...workspaceV4.workspace, id: "psd\nclinical" },
|
||||
} as CanonicalWorkspace;
|
||||
|
||||
expect(() => buildInstallationContract(unsafeWorkspace)).toThrow(/id/i);
|
||||
expect(() => renderWorkspaceDocs(unsafeWorkspace)).toThrow(/id/i);
|
||||
});
|
||||
|
||||
test("v3 installation contract omits external vector and embedding bindings", () => {
|
||||
const contract = buildInstallationContract(workspaceV3);
|
||||
test("v4 installation contract omits external vector and embedding bindings", () => {
|
||||
const contract = buildInstallationContract(workspaceV4);
|
||||
const names = contract.variables.map((variable) => variable.name);
|
||||
|
||||
expect(names).toContain("THT_WS_PSD_CLINICAL_DWH_TRANSPORT");
|
||||
expect(names.some((name) => name.includes("_VECTOR_"))).toBe(false);
|
||||
expect(names.some((name) => name.includes("_EMBEDDING_"))).toBe(false);
|
||||
expect(renderWorkspaceDocs(workspaceV3).markdown).not.toContain("Embedding service");
|
||||
expect(renderWorkspaceDocs(workspaceV4).markdown).not.toContain("Embedding service");
|
||||
});
|
||||
|
||||
|
||||
@@ -114,7 +109,7 @@ test.each([
|
||||
|
||||
function renderWorkspaceWithoutEvidence(): string {
|
||||
return `workspace:
|
||||
schema_version: 3
|
||||
schema_version: 4
|
||||
id: psd-clinical
|
||||
name: Policlinico San Donato
|
||||
language: it
|
||||
@@ -123,10 +118,6 @@ dwh:
|
||||
database: postgres
|
||||
schema: datawarehouse
|
||||
supported_transports: [postgres_direct]
|
||||
semantic_index:
|
||||
vector_store: { engine: qdrant, collection: psd-clinical, dimensions: 1024, distance: cosine }
|
||||
embedding: { provider: ollama_internal, model: qwen3-embedding:0.6b, dimensions: 1024 }
|
||||
llm_policy: { allowed: [zai/glm-5.2] }
|
||||
`;
|
||||
}
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ import type { RuntimeBindings } from "../src/workspaces/runtime-renderer.js";
|
||||
import { parseWorkspaceYaml, resolveDiagnosticUrl } from "../src/workspaces/schema.js";
|
||||
|
||||
const workspace = parseWorkspaceYaml(`workspace:
|
||||
schema_version: 3
|
||||
schema_version: 4
|
||||
id: psd-clinical
|
||||
name: Policlinico San Donato
|
||||
language: it
|
||||
@@ -22,18 +22,6 @@ dwh:
|
||||
schema: datawarehouse
|
||||
timeout_ms: 8000
|
||||
supported_transports: [postgres_direct, rest_api, ssh_tunnel]
|
||||
semantic_index:
|
||||
vector_store:
|
||||
engine: qdrant
|
||||
collection: psd-clinical
|
||||
dimensions: 1024
|
||||
distance: cosine
|
||||
embedding:
|
||||
provider: ollama_internal
|
||||
model: qwen3-embedding:0.6b
|
||||
dimensions: 1024
|
||||
llm_policy:
|
||||
allowed: [zai/glm-5.2]
|
||||
`);
|
||||
|
||||
const bindings: RuntimeBindings = {
|
||||
@@ -78,7 +66,7 @@ afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
test("diagnoses schema-v3 DWH, internal Qdrant, and internal Ollama without semantic bindings", async () => {
|
||||
test("diagnoses workspace-v4 DWH plus installation-derived Qdrant and Ollama", async () => {
|
||||
const adapters = successfulAdapters();
|
||||
const result = await diagnose(adapters)(workspace, bindings, { writeProbe: false });
|
||||
|
||||
@@ -139,7 +127,7 @@ test("reports only sanitized DWH and Evidence binding names before network diagn
|
||||
expect(adapters.inspectQdrant).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("keeps schema-v3 DWH SSH diagnostic-only and runtime-inactive", async () => {
|
||||
test("keeps workspace-v4 DWH SSH diagnostic-only and runtime-inactive", async () => {
|
||||
const adapters = successfulAdapters();
|
||||
const result = await diagnose(adapters)(workspace, {
|
||||
...bindings,
|
||||
@@ -152,9 +140,9 @@ test("keeps schema-v3 DWH SSH diagnostic-only and runtime-inactive", async () =>
|
||||
expect(adapters.probeConnector).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("uses the schema-v3 declared DWH REST diagnostic and auth policy", async () => {
|
||||
test("uses the workspace-v4 declared DWH REST diagnostic and auth policy", async () => {
|
||||
const restWorkspace = parseWorkspaceYaml(`workspace:
|
||||
schema_version: 3
|
||||
schema_version: 4
|
||||
id: psd-clinical
|
||||
name: REST workspace
|
||||
language: en
|
||||
@@ -163,16 +151,12 @@ dwh:
|
||||
database: warehouse
|
||||
schema: datawarehouse
|
||||
supported_transports: [rest_api]
|
||||
semantic_index:
|
||||
vector_store: { engine: qdrant, collection: psd-clinical, dimensions: 1024, distance: cosine }
|
||||
embedding: { provider: ollama_internal, model: qwen3-embedding:0.6b, dimensions: 1024 }
|
||||
diagnostics:
|
||||
dwh_rest:
|
||||
method: POST
|
||||
path: /rpc/ping
|
||||
auth: bearer
|
||||
response: { database: database, schema: schema }
|
||||
llm_policy: { allowed: [zai/glm-5.2] }
|
||||
`);
|
||||
const adapters = successfulAdapters();
|
||||
const result = await diagnose(adapters)(restWorkspace, {
|
||||
@@ -423,7 +407,7 @@ test("uses a REST secret only as a header and redacts it from failed diagnostics
|
||||
const canary = "CANARY-REST-AUTH-SECRET";
|
||||
await writeFile(credentialFile, canary);
|
||||
const restDescriptor = parseWorkspaceYaml(`workspace:
|
||||
schema_version: 3
|
||||
schema_version: 4
|
||||
id: psd-clinical
|
||||
name: REST auth
|
||||
language: en
|
||||
@@ -432,16 +416,12 @@ dwh:
|
||||
database: warehouse
|
||||
schema: datawarehouse
|
||||
supported_transports: [rest_api]
|
||||
semantic_index:
|
||||
vector_store: { engine: qdrant, collection: psd-clinical, dimensions: 1024, distance: cosine }
|
||||
embedding: { provider: ollama_internal, model: qwen3-embedding:0.6b, dimensions: 1024 }
|
||||
diagnostics:
|
||||
dwh_rest:
|
||||
method: GET
|
||||
path: /health
|
||||
auth: bearer
|
||||
response: { database: database, schema: schema }
|
||||
llm_policy: { allowed: [zai/glm-5.2] }
|
||||
`);
|
||||
const fetchMock = vi.fn()
|
||||
.mockResolvedValueOnce(new Response("upstream CANARY-REST-AUTH-SECRET", { status: 503 }))
|
||||
|
||||
@@ -53,7 +53,7 @@ async function makeRepo(id: string, annotations: string | Buffer | "dir" | "syml
|
||||
writeFileSync(join(source, "thoth-workspaces.yaml"),
|
||||
`schema_version: 1\nworkspaces: [{id: ${id}, name: Workspace}]\n`);
|
||||
mkdirSync(join(source, id, "schema"), { recursive: true });
|
||||
writeFileSync(join(source, id, "workspace.yaml"), `workspace:\n schema_version: 3\n id: ${id}\n`);
|
||||
writeFileSync(join(source, id, "workspace.yaml"), `workspace:\n schema_version: 4\n id: ${id}\n`);
|
||||
const annotationsPath = join(source, id, "schema", "annotations.yaml");
|
||||
if (annotations === "dir") {
|
||||
mkdirSync(annotationsPath, { recursive: true });
|
||||
|
||||
@@ -47,7 +47,7 @@ async function fixture(layout: EvidenceLayout): Promise<{ root: string; remote:
|
||||
await git(source, ["config", "user.email", "evidence@example.invalid"]);
|
||||
writeFileSync(join(source, "thoth-workspaces.yaml"), "schema_version: 1\nworkspaces: [{id: research, name: Research}]\n");
|
||||
mkdirSync(join(source, "research"), { recursive: true });
|
||||
writeFileSync(join(source, "research", "workspace.yaml"), "workspace:\n schema_version: 3\n id: research\n");
|
||||
writeFileSync(join(source, "research", "workspace.yaml"), "workspace:\n schema_version: 4\n id: research\n");
|
||||
const evidence = join(source, "research", "evidence");
|
||||
if (layout === "tree") {
|
||||
mkdirSync(join(evidence, "nested"), { recursive: true });
|
||||
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
import type { WorkspaceRegistryConfig } from "../src/workspaces/types.js";
|
||||
|
||||
const validYaml = `workspace:
|
||||
schema_version: 2
|
||||
schema_version: 4
|
||||
id: psd-clinical
|
||||
name: Policlinico San Donato
|
||||
language: it
|
||||
@@ -21,21 +21,6 @@ dwh:
|
||||
database: postgres
|
||||
schema: datawarehouse
|
||||
supported_transports: [postgres_direct]
|
||||
semantic_index:
|
||||
vector_store:
|
||||
engine: pgvector
|
||||
database: postgres
|
||||
schema: vectors
|
||||
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 runFile = promisify(execFile);
|
||||
|
||||
+12
-21
@@ -13,20 +13,11 @@ import {
|
||||
import { renderRuntimeConfig, type RuntimeBindings } from "../src/workspaces/runtime-renderer.js";
|
||||
|
||||
const unsupportedWorkspace = {
|
||||
workspace: { schema_version: 2, id: "legacy-workspace", name: "Legacy", language: "en" },
|
||||
workspace: { schema_version: 3, id: "legacy-workspace", name: "Legacy", language: "en" },
|
||||
dwh: {
|
||||
engine: "postgres", database: "warehouse", schema: "public",
|
||||
supported_transports: ["postgres_direct"],
|
||||
},
|
||||
semantic_index: {
|
||||
vector_store: {
|
||||
engine: "pgvector", database: "warehouse", schema: "vectors",
|
||||
collection: "documents", dimensions: 768, distance: "cosine",
|
||||
supported_transports: ["pgvector_direct"],
|
||||
},
|
||||
embedding: { provider: "ollama_compatible", model: "legacy", dimensions: 768 },
|
||||
},
|
||||
llm_policy: { allowed: ["zai/glm-5.2"] },
|
||||
};
|
||||
|
||||
const bindings: RuntimeBindings = {
|
||||
@@ -40,31 +31,31 @@ const adapters: DiagnosticAdapters = {
|
||||
probeEmbedding: vi.fn(),
|
||||
};
|
||||
|
||||
test("renderer rejects callers that bypass the schema-v3 type contract", () => {
|
||||
test("renderer rejects callers that bypass the schema-v4 type contract", () => {
|
||||
expect(() => renderRuntimeConfig(unsupportedWorkspace as never, bindings, {
|
||||
sessions: "/data/sessions", artifacts: "/data/artifacts", indexes: "/data/indexes",
|
||||
})).toThrow("Runtime renderer supports only workspace schema version 3");
|
||||
})).toThrow("Runtime renderer supports only workspace schema version 4");
|
||||
});
|
||||
|
||||
test("installation contract rejects callers that bypass the schema-v3 type contract", () => {
|
||||
test("installation contract rejects callers that bypass the schema-v4 type contract", () => {
|
||||
expect(() => buildInstallationContract(unsupportedWorkspace as never))
|
||||
.toThrow("Installation contract supports only workspace schema version 3");
|
||||
.toThrow("Installation contract supports only workspace schema version 4");
|
||||
});
|
||||
|
||||
test("binding entry points reject callers that bypass the schema-v3 type contract", () => {
|
||||
test("binding entry points reject callers that bypass the schema-v4 type contract", () => {
|
||||
expect(() => resolveBinding(unsupportedWorkspace as never, "DWH", {}, []))
|
||||
.toThrow("Workspace bindings support only workspace schema version 3");
|
||||
.toThrow("Workspace bindings support only workspace schema version 4");
|
||||
expect(() => resolveEvidenceBinding(unsupportedWorkspace as never, {}, []))
|
||||
.toThrow("Workspace bindings support only workspace schema version 3");
|
||||
.toThrow("Workspace bindings support only workspace schema version 4");
|
||||
expect(() => resolveRuntimeBindings(unsupportedWorkspace as never, {}, []))
|
||||
.toThrow("Workspace bindings support only workspace schema version 3");
|
||||
.toThrow("Workspace bindings support only workspace schema version 4");
|
||||
});
|
||||
|
||||
test("diagnoser factories reject callers that bypass the schema-v3 type contract", async () => {
|
||||
test("diagnoser factories reject callers that bypass the schema-v4 type contract", async () => {
|
||||
await expect(createWorkspaceDiagnoser(adapters)(unsupportedWorkspace as never, bindings, {
|
||||
writeProbe: false,
|
||||
})).rejects.toThrow("Workspace diagnoser supports only workspace schema version 3");
|
||||
})).rejects.toThrow("Workspace diagnoser supports only workspace schema version 4");
|
||||
await expect(createProductionWorkspaceDiagnoser(5_000, adapters)(
|
||||
unsupportedWorkspace as never, bindings, { writeProbe: false },
|
||||
)).rejects.toThrow("Workspace diagnoser supports only workspace schema version 3");
|
||||
)).rejects.toThrow("Workspace diagnoser supports only workspace schema version 4");
|
||||
});
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
} from "../src/workspaces/schema.js";
|
||||
|
||||
export const validYaml = `workspace:
|
||||
schema_version: 3
|
||||
schema_version: 4
|
||||
id: psd-clinical
|
||||
name: Policlinico San Donato
|
||||
description: Clinical data warehouse workspace
|
||||
@@ -25,33 +25,8 @@ dwh:
|
||||
- postgres_direct
|
||||
- rest_api
|
||||
- ssh_tunnel
|
||||
semantic_index:
|
||||
vector_store:
|
||||
engine: qdrant
|
||||
collection: psd-clinical
|
||||
dimensions: 1024
|
||||
distance: cosine
|
||||
embedding:
|
||||
provider: ollama_internal
|
||||
model: qwen3-embedding:0.6b
|
||||
dimensions: 1024
|
||||
llm_policy:
|
||||
default: zai/glm-5.2
|
||||
allowed:
|
||||
- zai/glm-5.2
|
||||
- openai/gpt-5
|
||||
`;
|
||||
|
||||
test("rejects a workspace whose embedding dimensions differ from its collection", () => {
|
||||
expect(() => parseWorkspaceYaml(validYaml.replace("dimensions: 1024", "dimensions: 1536")))
|
||||
.toThrow(/dimensions/i);
|
||||
});
|
||||
|
||||
test("rejects an LLM default outside its allowlist", () => {
|
||||
expect(() => parseWorkspaceYaml(validYaml.replace("- zai/glm-5.2", "- openai/gpt-5")))
|
||||
.toThrow(/allowlist/i);
|
||||
});
|
||||
|
||||
test("rejects unknown keys and invalid immutable IDs", () => {
|
||||
expect(() => parseWorkspaceYaml(validYaml.replace(" language: it", " language: it\n label: PSD")))
|
||||
.toThrow(/unrecognized key/i);
|
||||
@@ -74,94 +49,34 @@ test("accepts optional connection ports and timeouts but rejects unsafe values",
|
||||
.toThrow(/port/i);
|
||||
expect(() => parseWorkspaceYaml(validYaml.replace("timeout_ms: 5000", "timeout_ms: 0")))
|
||||
.toThrow(/timeout/i);
|
||||
expect(() => parseWorkspaceYaml(validYaml.replace("dimensions: 1024", "dimensions: 2048")))
|
||||
.toThrow(/1024|dimensions/i);
|
||||
});
|
||||
|
||||
test("accepts only the schema v3 internal qdrant semantic shape", () => {
|
||||
test("accepts a model-free schema v4 workspace", () => {
|
||||
expect(parseWorkspaceYaml(validYaml)).toMatchObject({
|
||||
workspace: { schema_version: 3, id: "psd-clinical" },
|
||||
semantic_index: {
|
||||
vector_store: {
|
||||
engine: "qdrant",
|
||||
collection: "psd-clinical",
|
||||
dimensions: 1024,
|
||||
distance: "cosine",
|
||||
},
|
||||
embedding: {
|
||||
provider: "ollama_internal",
|
||||
model: "qwen3-embedding:0.6b",
|
||||
dimensions: 1024,
|
||||
},
|
||||
},
|
||||
workspace: { schema_version: 4, id: "psd-clinical" },
|
||||
dwh: { database: "postgres", schema: "datawarehouse" },
|
||||
});
|
||||
});
|
||||
|
||||
test("committed example descriptors parse as exact schema v3 workspaces", () => {
|
||||
test("committed example descriptors parse as exact schema v4 workspaces", () => {
|
||||
const example = readFileSync(resolve(process.cwd(), "../deploy/workspaces/example.yaml"), "utf8");
|
||||
const psdExample = readFileSync(resolve(process.cwd(), "../deploy/workspaces/psd.yaml.example"), "utf8");
|
||||
|
||||
expect(() => parseWorkspaceYaml(example)).not.toThrow();
|
||||
expect(() => parseWorkspaceYaml(psdExample)).not.toThrow();
|
||||
expect(parseWorkspaceYaml(example)).toMatchObject({
|
||||
workspace: { schema_version: 3 },
|
||||
semantic_index: {
|
||||
vector_store: { engine: "qdrant", distance: "cosine", dimensions: 1024 },
|
||||
embedding: { provider: "ollama_internal", model: "qwen3-embedding:0.6b", dimensions: 1024 },
|
||||
},
|
||||
workspace: { schema_version: 4 },
|
||||
});
|
||||
expect(parseWorkspaceYaml(psdExample)).toMatchObject({
|
||||
workspace: { schema_version: 3 },
|
||||
semantic_index: {
|
||||
vector_store: { engine: "qdrant", distance: "cosine", dimensions: 1024 },
|
||||
embedding: { provider: "ollama_internal", model: "qwen3-embedding:0.6b", dimensions: 1024 },
|
||||
},
|
||||
workspace: { schema_version: 4 },
|
||||
});
|
||||
});
|
||||
|
||||
test("rejects pgvector semantic stores in schema v3", () => {
|
||||
expect(() => parseWorkspaceYaml(validYaml.replace("engine: qdrant", "engine: pgvector")))
|
||||
.toThrow(/qdrant|pgvector/i);
|
||||
});
|
||||
|
||||
test("rejects supported_transports inside schema v3 semantic identity", () => {
|
||||
const withTransport = validYaml.replace(
|
||||
" distance: cosine\n",
|
||||
" distance: cosine\n supported_transports:\n - rest_api\n",
|
||||
);
|
||||
|
||||
expect(() => parseWorkspaceYaml(withTransport)).toThrow(/unrecognized key|supported_transports/i);
|
||||
});
|
||||
|
||||
test("rejects external embedding providers in schema v3", () => {
|
||||
expect(() => parseWorkspaceYaml(validYaml.replace("provider: ollama_internal", "provider: openai_compatible")))
|
||||
.toThrow(/ollama_internal|provider/i);
|
||||
});
|
||||
|
||||
test("rejects non-cosine distance in schema v3", () => {
|
||||
expect(() => parseWorkspaceYaml(validYaml.replace("distance: cosine", "distance: l2")))
|
||||
.toThrow(/cosine|distance/i);
|
||||
});
|
||||
|
||||
test("rejects unknown fields in schema v3 semantic identity", () => {
|
||||
const withUnknownField = validYaml.replace(
|
||||
" collection: psd-clinical\n",
|
||||
" collection: psd-clinical\n namespace: psd\n",
|
||||
);
|
||||
|
||||
expect(() => parseWorkspaceYaml(withUnknownField)).toThrow(/unrecognized key/i);
|
||||
});
|
||||
|
||||
test("rejects legacy semantic connector fields and diagnostics in schema v3", () => {
|
||||
expect(() => parseWorkspaceYaml(validYaml.replace(
|
||||
" collection: psd-clinical\n",
|
||||
" collection: psd-clinical\n database: postgres\n",
|
||||
))).toThrow(/unrecognized key|database/i);
|
||||
|
||||
expect(() => parseWorkspaceYaml(validYaml.replace(
|
||||
"llm_policy:\n",
|
||||
"diagnostics:\n vector_rest:\n metadata:\n method: GET\n path: /metadata\n auth: bearer\n response:\n collection: collection\n dimensions: dimensions\n distance: distance\nllm_policy:\n",
|
||||
))).toThrow(/unrecognized key|vector_rest/i);
|
||||
test("rejects installation-owned model and vector fields", () => {
|
||||
expect(() => parseWorkspaceYaml(`${validYaml}llm_policy:\n allowed: [zai/glm-5.3]\n`))
|
||||
.toThrow(/unrecognized key|llm_policy/i);
|
||||
expect(() => parseWorkspaceYaml(`${validYaml}semantic_index: {}\n`))
|
||||
.toThrow(/unrecognized key|semantic_index/i);
|
||||
});
|
||||
|
||||
test.each([
|
||||
@@ -222,8 +137,8 @@ llm_policy:
|
||||
- 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);
|
||||
expect(() => parseWorkspaceYaml(yaml)).toThrow(/schema_version|invalid literal|4/i);
|
||||
expect(() => validateWorkspaceDescriptor(parse(yaml))).toThrow(/schema_version|invalid literal|4/i);
|
||||
});
|
||||
|
||||
test("does not expose the redundant canonical validator or v1 migration", () => {
|
||||
@@ -253,9 +168,8 @@ test("rejects REST diagnostic declarations without their matching connector tran
|
||||
response:
|
||||
database: database
|
||||
schema: schema
|
||||
llm_policy:
|
||||
`;
|
||||
const declared = validYaml.replace("llm_policy:\n", diagnostics);
|
||||
const declared = `${validYaml}${diagnostics}`;
|
||||
|
||||
expect(() => parseWorkspaceYaml(declared.replace(" - rest_api\n", ""))).toThrow(/dwh_rest/i);
|
||||
});
|
||||
@@ -462,7 +376,7 @@ test.each(["curated/**/*.yaml", "curated/**"])(
|
||||
},
|
||||
);
|
||||
|
||||
test("keeps evidence optional on schema v3", () => {
|
||||
test("keeps evidence optional on schema v4", () => {
|
||||
expect(validateWorkspaceDescriptor(validWorkspaceObject())).not.toHaveProperty("evidence");
|
||||
});
|
||||
|
||||
@@ -471,7 +385,7 @@ test("serializes defaulted evidence canonically and parses it without loss", ()
|
||||
type: "filesystem",
|
||||
uri: "psd-clinical/evidence",
|
||||
}));
|
||||
if (canonical.workspace.schema_version !== 3) throw new Error("expected schema v3");
|
||||
if (canonical.workspace.schema_version !== 4) throw new Error("expected schema v4");
|
||||
|
||||
expect(parseWorkspaceYaml(serializeWorkspaceYaml(canonical))).toEqual(canonical);
|
||||
});
|
||||
|
||||
@@ -46,7 +46,7 @@ async function fixture(): Promise<{ root: string; remote: string; commit: string
|
||||
await git(source, ["config", "user.email", "evidence-materializer@example.invalid"]);
|
||||
writeFileSync(join(source, "thoth-workspaces.yaml"), "schema_version: 1\nworkspaces: [{id: research, name: Research}]\n");
|
||||
mkdirSync(join(source, "research", "evidence", "nested"), { recursive: true });
|
||||
writeFileSync(join(source, "research", "workspace.yaml"), "workspace:\n schema_version: 3\n id: research\n");
|
||||
writeFileSync(join(source, "research", "workspace.yaml"), "workspace:\n schema_version: 4\n id: research\n");
|
||||
writeFileSync(join(source, "research", "evidence", "guide.md"), "# guide\n");
|
||||
writeFileSync(join(source, "research", "evidence", "nested", "deep.md"), "# deep\n");
|
||||
await git(source, ["add", "-A"]);
|
||||
@@ -102,7 +102,7 @@ test("refuses symlink-containing trees and bound violations without publishing",
|
||||
await git(source, ["config", "user.email", "e@e.invalid"]);
|
||||
writeFileSync(join(source, "thoth-workspaces.yaml"), "schema_version: 1\nworkspaces: [{id: research, name: Research}]\n");
|
||||
mkdirSync(join(source, "research", "evidence"), { recursive: true });
|
||||
writeFileSync(join(source, "research", "workspace.yaml"), "workspace:\n schema_version: 3\n id: research\n");
|
||||
writeFileSync(join(source, "research", "workspace.yaml"), "workspace:\n schema_version: 4\n id: research\n");
|
||||
writeFileSync(join(source, "research", "outside.md"), "# outside\n");
|
||||
symlinkSync("../outside.md", join(source, "research", "evidence", "link.md"));
|
||||
await git(source, ["add", "-A"]);
|
||||
|
||||
Reference in New Issue
Block a user