fix: close internal semantic review gaps

This commit is contained in:
2026-08-08 23:27:58 +02:00
parent 43d8063922
commit 2c4534d968
18 changed files with 806 additions and 77 deletions
+34 -8
View File
@@ -8,7 +8,7 @@ import type { PrincipalContext } from "../auth/principal.js";
import type { ReadinessManager } from "../runtime/readiness-manager.js";
import type { ListModelsFn } from "./meta.js";
import type { WorkspaceRegistry } from "../workspaces/registry.js";
import type { WorkspaceDescriptor } from "../workspaces/schema.js";
import { validateOperationalWorkspace, type WorkspaceDescriptor } from "../workspaces/schema.js";
import type { MaintenanceBarrier } from "../runtime/maintenance-gate.js";
const BOOTSTRAP_FAILURE_MESSAGE =
@@ -108,7 +108,11 @@ export function sessionRoutes(
const isNotFound = (error: unknown) =>
/not found|non trovata|inesistente|404/i.test(error instanceof Error ? error.message : String(error));
type LocatedSession = { manifest: any; workspaceConfigPath: string };
type LocatedSession = {
manifest: any;
workspaceConfigPath: string;
workspace?: WorkspaceDescriptor;
};
const workspaceRevisionUnavailable = () => Object.assign(
new Error("workspace revision unavailable"), { code: "workspace_revision_unavailable" },
@@ -168,7 +172,12 @@ export function sessionRoutes(
if (!saved.workspace_id || !saved.workspace_revision) return located;
try {
const pinned = await d.workspaceRegistry.readPinned(saved.workspace_id, saved.workspace_revision);
return { ...located, workspaceConfigPath: pinned.workspaceConfigPath ?? (pinned as any).revision?.snapshotPath };
const workspace = validateOperationalWorkspace(pinned.workspace);
return {
...located,
workspace,
workspaceConfigPath: pinned.workspaceConfigPath ?? (pinned as any).revision?.snapshotPath,
};
} catch {
throw workspaceRevisionUnavailable();
}
@@ -319,6 +328,7 @@ export function sessionRoutes(
let workspaceConfigPath: string | undefined;
let workspaceId: string | undefined;
let workspaceRevision: string | undefined;
let workspaceDescriptor: WorkspaceDescriptor | undefined;
let allowedModels: readonly string[] | undefined;
if (requestedWorkspaceId) {
try {
@@ -344,6 +354,7 @@ export function sessionRoutes(
workspaceConfigPath = resolved.revision.snapshotPath;
workspaceId = resolved.revision.id;
workspaceRevision = resolved.revision.commit;
workspaceDescriptor = resolved.workspace;
allowedModels = resolved.workspace.llm_policy.allowed;
} catch {
return reply.code(409).send({
@@ -362,8 +373,13 @@ export function sessionRoutes(
// runtime owned by this principal, while runtimes belonging to other users remain intact.
// Optional chaining preserves the deliberately narrow manager stubs used by route tests.
for (const id of d.mgr.teardownForPrincipal?.(principal) ?? []) boundRuntimes.delete(id);
const ensure = await d.readiness.ensure(workspaceConfigPath ?? "", principal);
if (!ensure.ok) return reply.code(503).send({ error: READINESS_FAILURE_MESSAGE });
const ensure = await d.readiness.ensure(
workspaceConfigPath ?? "", principal, workspaceDescriptor,
);
if (!ensure.ok) return reply.code(503).send({
error: READINESS_FAILURE_MESSAGE,
...(ensure.code ? { code: ensure.code } : {}),
});
// Local-only: verify the DWH is reachable BEFORE creating the session, so a dropped
// VPN surfaces as an up-front alert instead of a session that spawns Pi and then dies
// in bootstrap retrieval. `code` lets the client show a specific message.
@@ -546,7 +562,12 @@ export function sessionRoutes(
workspace_id?: string; workspace_revision?: string;
};
let workspaceConfigPath: string;
try { workspaceConfigPath = (await resolveSessionWorkspace(located)).workspaceConfigPath; }
let workspaceDescriptor: WorkspaceDescriptor | undefined;
try {
const resolved = await resolveSessionWorkspace(located);
workspaceConfigPath = resolved.workspaceConfigPath;
workspaceDescriptor = resolved.workspace;
}
catch { return unavailableWorkspaceReply(reply); }
try { settings = await d.getSettings(principal); } catch { return storageFailure(reply); }
// This check belongs inside the per-session lock: a preceding cold Resume may have
@@ -558,8 +579,13 @@ export function sessionRoutes(
return reply.code(200).send({ id, alreadyActive: true });
}
}
const ensure = await d.readiness.ensure(workspaceConfigPath ?? "", principal);
if (!ensure.ok) return reply.code(503).send({ error: READINESS_FAILURE_MESSAGE });
const ensure = await d.readiness.ensure(
workspaceConfigPath ?? "", principal, workspaceDescriptor,
);
if (!ensure.ok) return reply.code(503).send({
error: READINESS_FAILURE_MESSAGE,
...(ensure.code ? { code: ensure.code } : {}),
});
const options = {
provider: saved?.provider,
model: saved?.model,
+17 -3
View File
@@ -19,6 +19,7 @@ import {
parseWorkspaceYaml,
serializeWorkspaceYaml,
validateCanonicalWorkspace,
validateOperationalWorkspace,
type CanonicalWorkspace,
type WorkspaceDescriptor,
} from "../workspaces/schema.js";
@@ -327,9 +328,22 @@ export function workspaceRoutes(app: FastifyInstance, deps: WorkspaceRoutesDeps)
app.post("/workspaces/:id/test", async (request, reply) => {
try {
const { id } = z.object({ id: workspaceId }).parse(request.params);
const { workspace } = await deps.registry.read(id);
const bindings = resolveRuntimeBindings(workspace, process.env, deps.config.secretRoots);
return await deps.diagnose(workspace, bindings, { writeProbe: false });
const { workspace, revision } = await deps.registry.read(id);
if (revision.state !== "operational") {
throw new WorkspaceRegistryError(
"workspace_not_activatable", "Workspace requires explicit migration",
);
}
let operational: CanonicalWorkspace;
try {
operational = validateOperationalWorkspace(workspace);
} catch {
throw new WorkspaceRegistryError(
"workspace_not_activatable", "Workspace requires explicit migration",
);
}
const bindings = resolveRuntimeBindings(operational, process.env, deps.config.secretRoots);
return await deps.diagnose(operational, bindings, { writeProbe: false });
} catch (error) {
return errorReply(reply, error);
}
+29 -5
View File
@@ -1,9 +1,16 @@
import type { OllamaEnsureResult, ThtRunner } from "../tht/tht-runner.js";
import type {
OllamaEnsureResult,
SemanticReadinessCode,
ThtRunner,
} from "../tht/tht-runner.js";
import type { PrincipalContext } from "../auth/principal.js";
import type { WorkspaceDescriptor } from "../workspaces/schema.js";
export type ReadinessResult = OllamaEnsureResult & { code?: SemanticReadinessCode };
interface ReadyEntry {
expiresAt: number;
result: OllamaEnsureResult;
result: ReadinessResult;
}
/**
@@ -11,7 +18,7 @@ interface ReadyEntry {
* Failures are deliberately not cached so a submit can retry after a transient outage.
*/
export class ReadinessManager {
private inFlight = new Map<string, Promise<OllamaEnsureResult>>();
private inFlight = new Map<string, Promise<ReadinessResult>>();
private ready = new Map<string, ReadyEntry>();
constructor(
@@ -21,7 +28,11 @@ export class ReadinessManager {
private now: () => number = Date.now,
) {}
ensure(workspace = "", principal?: PrincipalContext): Promise<OllamaEnsureResult> {
ensure(
workspace = "",
principal?: PrincipalContext,
descriptor?: WorkspaceDescriptor,
): Promise<ReadinessResult> {
const key = `${principal?.issuer ?? ""}\0${principal?.subject ?? ""}\0${workspace}`;
const cached = this.ready.get(key);
if (cached && cached.expiresAt > this.now()) return Promise.resolve(cached.result);
@@ -32,7 +43,20 @@ export class ReadinessManager {
const runner = principal && typeof (this.tht as any).withPrincipal === "function"
? this.tht.withPrincipal(principal) : this.tht;
const pending = runner.ollamaEnsure(workspace, this.timeoutSec)
const pending = (async (): Promise<ReadinessResult> => {
try {
if (descriptor) {
const qdrant = await runner.qdrantEnsure(descriptor, this.timeoutSec);
if (!qdrant.ok) return qdrant;
}
const ollama = await runner.ollamaEnsure(workspace, this.timeoutSec);
return ollama.ok
? ollama
: { ...ollama, code: "workspace_not_activatable" };
} catch {
return { ok: false, code: "workspace_not_activatable" };
}
})()
.then((result) => {
if (result.ok) {
this.ready.set(key, { result, expiresAt: this.now() + this.ttlMs });
+73 -2
View File
@@ -15,7 +15,11 @@ import {
type RuntimePaths,
type SemanticRuntimeConfig,
} from "../workspaces/runtime-renderer.js";
import { parseWorkspaceYaml } from "../workspaces/schema.js";
import {
parseWorkspaceYaml,
validateOperationalWorkspace,
type WorkspaceDescriptor,
} from "../workspaces/schema.js";
export interface ThtConfig extends SecretBundleConfig {
thtBin: string;
@@ -25,6 +29,7 @@ export interface ThtConfig extends SecretBundleConfig {
runtimeSnapshotRoot?: string;
secretRoots?: readonly string[];
semanticRuntime: SemanticRuntimeConfig;
qdrantRequest?: typeof fetch;
}
export interface RuntimeConfigLease {
@@ -64,6 +69,24 @@ export interface OllamaEnsureResult {
model_name?: string;
}
export type SemanticReadinessCode = "workspace_not_activatable" | "semantic_index_incompatible";
export interface QdrantEnsureResult {
ok: boolean;
code?: SemanticReadinessCode;
}
const REQUIRED_QDRANT_PAYLOAD_INDEXES = [
"content_hash",
"document_id",
"kind",
"record_key",
"record_kind",
"vector_generation",
"workspace_id",
"workspace_revision",
] as const;
interface RuntimeSnapshot {
path: string;
dev: number;
@@ -136,7 +159,7 @@ export class ThtRunner {
if (before.dev !== after.dev || before.ino !== after.ino || before.size !== after.size) {
throw new Error("workspace snapshot changed while reading");
}
const workspace = parseWorkspaceYaml(source);
const workspace = validateOperationalWorkspace(parseWorkspaceYaml(source));
if (workspace.workspace.id !== identity.workspaceId) {
throw new Error("workspace snapshot identity does not match its path");
}
@@ -537,4 +560,52 @@ export class ThtRunner {
error: parsed?.error ?? (stderr.trim() || `tht ollama ensure exit ${code}`),
};
}
async qdrantEnsure(
workspace: WorkspaceDescriptor,
timeoutSec: number,
): Promise<QdrantEnsureResult> {
let descriptor;
try {
descriptor = validateOperationalWorkspace(workspace);
} catch {
return { ok: false, code: "workspace_not_activatable" };
}
const collection = descriptor.semantic_index.vector_store;
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), Math.max(1, timeoutSec) * 1000);
try {
const url = new URL(
`/collections/${encodeURIComponent(collection.collection)}`,
this.cfg.semanticRuntime.internalQdrantUrl,
);
const request = this.cfg.qdrantRequest ?? fetch;
const response = await request(url.toString(), { method: "GET", signal: controller.signal });
if (response.status === 404) {
return { ok: false, code: "semantic_index_incompatible" };
}
if (!response.ok) return { ok: false, code: "workspace_not_activatable" };
const body = await response.json() as any;
const result = body?.result;
const vectors = result?.config?.params?.vectors;
const payloadSchema = result?.payload_schema;
const configurationMatches = vectors
&& vectors.size === collection.dimensions
&& typeof vectors.distance === "string"
&& vectors.distance.toLowerCase() === collection.distance;
const indexesMatch = payloadSchema
&& typeof payloadSchema === "object"
&& REQUIRED_QDRANT_PAYLOAD_INDEXES.every(
(field) => payloadSchema[field]?.data_type === "keyword",
);
return configurationMatches && indexesMatch
? { ok: true }
: { ok: false, code: "semantic_index_incompatible" };
} catch {
return { ok: false, code: "workspace_not_activatable" };
} finally {
clearTimeout(timer);
controller.abort();
}
}
}
+8 -2
View File
@@ -13,6 +13,7 @@ import {
isCanonicalWorkspace,
parseWorkspaceYaml,
serializeWorkspaceYaml,
validateOperationalWorkspace,
type CanonicalWorkspace,
type WorkspaceDescriptor,
} from "./schema.js";
@@ -216,7 +217,9 @@ export class WorkspaceRegistry {
}
let workspace: WorkspaceDescriptor;
try {
workspace = parseWorkspaceYaml(await readFile(revision.snapshotPath, "utf8"));
workspace = validateOperationalWorkspace(
parseWorkspaceYaml(await readFile(revision.snapshotPath, "utf8")),
);
} catch (error) {
throw workspaceError(error);
}
@@ -259,7 +262,10 @@ export class WorkspaceRegistry {
const snapshotPath = this.snapshotPath(safeCommit(commit), id);
try {
const source = await readFile(snapshotPath, "utf8");
return { workspace: parseWorkspaceYaml(source), workspaceConfigPath: snapshotPath };
return {
workspace: validateOperationalWorkspace(parseWorkspaceYaml(source)),
workspaceConfigPath: snapshotPath,
};
} catch (error) {
throw workspaceError(error);
}
+43
View File
@@ -1,6 +1,21 @@
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" },
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>() {
let resolve!: (value: T) => void;
const promise = new Promise<T>((r) => { resolve = r; });
@@ -60,3 +75,31 @@ test("readiness does not cache failed results", async () => {
await expect(readiness.ensure("psd")).resolves.toMatchObject({ ok: true });
expect(calls).toBe(2);
});
test("readiness checks Qdrant before Ollama and skips Ollama on semantic incompatibility", async () => {
let ollamaCalls = 0;
const tht = {
qdrantEnsure: async () => ({ ok: false, code: "semantic_index_incompatible" }),
ollamaEnsure: async () => { ollamaCalls += 1; return { ok: true }; },
} as any;
const readiness = new ReadinessManager(tht, 60);
await expect(readiness.ensure("/registry/psd.yaml", undefined, workspace as any)).resolves.toEqual({
ok: false,
code: "semantic_index_incompatible",
});
expect(ollamaCalls).toBe(0);
});
test("readiness returns a sanitized activation code when a semantic probe throws", async () => {
const tht = {
qdrantEnsure: async () => { throw new Error("dial http://qdrant:6333/private"); },
ollamaEnsure: async () => ({ ok: true }),
} as any;
const readiness = new ReadinessManager(tht, 60);
await expect(readiness.ensure("/registry/psd.yaml", undefined, workspace as any)).resolves.toEqual({
ok: false,
code: "workspace_not_activatable",
});
});
+111 -7
View File
@@ -14,17 +14,34 @@ import { validateDeclarativePiConfig } from "../src/pi/managed-config.js";
const FAKE = path.resolve("../harness/tests/fake_pi/fake_pi_rpc.mjs");
const SCRIPT = path.resolve("../harness/tests/fake_pi/scripts/f1_disambiguation.json");
function operationalWorkspace(id = "default") {
return {
workspace: { schema_version: 3, 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;
}
const defaultWorkspaceRegistry = {
list: vi.fn(async () => [{
id: "default", commit: "e".repeat(40), blob: "f".repeat(40),
snapshotPath: `/data/workspace-registry/snapshots/${"e".repeat(40)}/default.yaml`, state: "operational",
}]),
read: vi.fn(async (id: string) => ({
workspace: {
llm_policy: {
allowed: ["zai/glm-5.2", "deepseek/deepseek-v4-pro", "local-qwen/qwen3.6-35b-a3b"],
},
},
workspace: operationalWorkspace(id),
revision: {
id, commit: "e".repeat(40), blob: "f".repeat(40),
snapshotPath: `/data/workspace-registry/snapshots/${"e".repeat(40)}/${id}.yaml`, state: "operational",
@@ -33,9 +50,13 @@ const defaultWorkspaceRegistry = {
};
function buildApp(config: Parameters<typeof buildRealApp>[0], deps: Record<string, unknown> = {}) {
const thtRunner = deps.thtRunner
? { qdrantEnsure: async () => ({ ok: true }), ...(deps.thtRunner as object) }
: undefined;
return buildRealApp(config, {
workspaceRuntimeSupport: () => true,
...deps,
...(thtRunner ? { thtRunner } : {}),
workspaceRegistry: { ...defaultWorkspaceRegistry, ...(deps.workspaceRegistry as object | undefined) },
} as any);
}
@@ -663,7 +684,7 @@ test("session lifecycle locates a B session when installation default is A", asy
],
readPinned: vi.fn(async (id: string, revision: string) => {
expect([id, revision]).toEqual(["b-workspace", "c".repeat(40)]);
return { workspace: { llm_policy: { allowed: ["zai/glm-5.2"] } }, workspaceConfigPath: bPinnedPath };
return { workspace: operationalWorkspace(id), workspaceConfigPath: bPinnedPath };
}),
} as any,
});
@@ -932,7 +953,7 @@ test("POST /sessions/:id/resume uses the manifest's retained workspace revision"
getSettings: () => ({ workspace: "legacy" }) as any,
workspaceRegistry: {
readPinned: vi.fn(async () => ({
workspace: { workspace: { id: "psd-clinical" } },
workspace: operationalWorkspace("psd-clinical"),
revision: {
id: "psd-clinical", commit: "a".repeat(40), blob: "b".repeat(40),
snapshotPath: "/data/workspace-registry/snapshots/aaaaaaaa/psd-clinical.yaml", state: "operational",
@@ -968,6 +989,61 @@ test("POST /sessions/:id/resume returns a sanitized error when its retained revi
expect(response.json()).toMatchObject({ code: "workspace_revision_unavailable" });
});
test("POST /sessions/:id/resume rejects a pinned schema-v2 workspace before readiness or runtime", async () => {
const readiness = vi.fn(async () => ({ ok: true }));
const reopenSession = vi.fn(async () => {});
const acquireWorkspaceRuntime = vi.fn();
const createFor = vi.fn();
const app = buildApp(loadConfig({ THT_HARNESS_DIR: "../harness" }), {
thtRunner: {
sessionShow: async () => ({
status: "open", archived: false,
workspace_id: "psd-clinical", workspace_revision: "a".repeat(40),
}),
reopenSession,
acquireWorkspaceRuntime,
} as any,
readiness: { ensure: readiness } as any,
mgr: { get: () => undefined, createFor } as any,
getSettings: () => ({ workspace: "legacy" }) as any,
workspaceRegistry: {
readPinned: vi.fn(async () => ({
workspace: {
workspace: { schema_version: 2, id: "psd-clinical", name: "PSD", language: "it" },
dwh: {
engine: "postgres", database: "warehouse", schema: "public",
supported_transports: ["rest_api"],
},
semantic_index: {
vector_store: {
engine: "pgvector", database: "warehouse", schema: "vectors",
collection: "documents", dimensions: 768, distance: "cosine",
supported_transports: ["rest_api"],
},
embedding: {
provider: "ollama_compatible", model: "nomic-embed-text", dimensions: 768,
},
},
llm_policy: { allowed: ["zai/glm-5.2"] },
},
workspaceConfigPath: `/data/workspace-registry/snapshots/${"a".repeat(40)}/psd-clinical.yaml`,
})),
} as any,
});
const response = await app.inject({ method: "POST", url: "/sessions/pinned-v2/resume" });
expect(response.statusCode).toBe(409);
expect(response.json()).toEqual({
code: "workspace_revision_unavailable",
error: "Session workspace configuration is unavailable. Check configuration and try again.",
});
expect(readiness).not.toHaveBeenCalled();
expect(reopenSession).not.toHaveBeenCalled();
expect(acquireWorkspaceRuntime).not.toHaveBeenCalled();
expect(createFor).not.toHaveBeenCalled();
});
test("a pruned pin blocks Resume but not active or mutation lifecycle routes", async () => {
const activePath = "/registry/snapshots/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/b-workspace.yaml";
const prunedError = "cannot read /registry/snapshots/secret-pruned-revision/b-workspace.yaml";
@@ -2340,11 +2416,35 @@ test("POST /sessions readiness failure returns one fixed public message without
expect(res.statusCode).toBe(503);
expect(res.json()).toEqual({
error: "Session services are not ready. Check configuration and connectivity, then try again.",
code: "workspace_not_activatable",
});
expect(res.body).not.toMatch(/secret\.invalid|DO_NOT_LEAK|\/srv\/private\/model-key/);
expect(createdCalled).toBe(false);
});
test.each(["semantic_index_incompatible", "workspace_not_activatable"] as const)(
"POST /sessions does not persist when Qdrant readiness returns %s",
async (code) => {
const sessionNew = vi.fn(async () => ({ id: "must-not-exist" }));
const app = buildApp(loadConfig({ THT_HARNESS_DIR: "../harness" }), {
thtRunner: { sessionNew } as any,
readiness: { ensure: async () => ({ ok: false, code }) } as any,
getSettings: () => ({ workspace: "psd" }) as any,
});
const response = await app.inject({
method: "POST", url: "/sessions", payload: { question: "q" },
});
expect(response.statusCode).toBe(503);
expect(response.json()).toEqual({
error: "Session services are not ready. Check configuration and connectivity, then try again.",
code,
});
expect(sessionNew).not.toHaveBeenCalled();
},
);
test("POST /sessions returns storage 503 before creating a Pi runtime when session persistence fails", async () => {
let piCreated = false;
const app = buildApp(loadConfig({ THT_HARNESS_DIR: "../harness" }), {
@@ -2365,8 +2465,10 @@ test("POST /sessions returns storage 503 before creating a Pi runtime when sessi
test("POST /sessions proceeds when ollamaEnsure succeeds", async () => {
let ensureWs: string | undefined;
const qdrantEnsure = vi.fn(async () => ({ ok: true }));
const app = buildApp(loadConfig({ THT_HARNESS_DIR: "../harness" }), {
thtRunner: {
qdrantEnsure,
ollamaEnsure: async (ws: string) => { ensureWs = ws; return { ok: true }; },
searchPack: async () => {},
sessionNew: async () => ({ id: "s1" }),
@@ -2376,6 +2478,7 @@ test("POST /sessions proceeds when ollamaEnsure succeeds", async () => {
});
const res = await app.inject({ method: "POST", url: "/sessions", payload: { question: "q" } });
expect(res.json()).toEqual({ id: "s1" });
expect(qdrantEnsure).toHaveBeenCalledWith(operationalWorkspace("psd"), 60);
expect(ensureWs).toContain(`/snapshots/${"e".repeat(40)}/psd.yaml`);
});
@@ -2574,6 +2677,7 @@ test("POST /sessions/:id/resume readiness failure returns the same fixed public
expect(res.statusCode).toBe(503);
expect(res.json()).toEqual({
error: "Session services are not ready. Check configuration and connectivity, then try again.",
code: "workspace_not_activatable",
});
expect(res.body).not.toMatch(/secret\.invalid|DO_NOT_LEAK|\/srv\/private\/resume-key/);
});
+7 -8
View File
@@ -238,7 +238,7 @@ test("validates a canonical workspace and runs the injected installation diagnos
expect(diagnose).not.toHaveBeenCalled();
});
test("runs the injected installation diagnostic for a migration-required v2 workspace when legacy bindings resolve", async () => {
test("rejects a migration-required v2 workspace before resolving semantic diagnostics", async () => {
const diagnose = vi.fn(async () => ({
activatable: false,
diagnostics: [{ level: "error" as const, code: "binding_missing" as const, field: "THT_WS_PSD_CLINICAL_VECTOR_BASE_URL", message: "Installation binding is missing or invalid." }],
@@ -257,13 +257,12 @@ test("runs the injected installation diagnostic for a migration-required v2 work
try {
const testResult = await app.inject({ method: "POST", url: "/workspaces/psd-clinical/test", payload: {} });
expect(testResult.statusCode).toBe(200);
expect(testResult.json()).toMatchObject({ activatable: false, diagnostics: [{ code: "binding_missing" }] });
expect(diagnose).toHaveBeenCalledWith(workspaceV2, expect.objectContaining({
dwh: expect.objectContaining({ transport: "rest_api", missing: [] }),
vector: expect.objectContaining({ transport: "rest_api", missing: [] }),
embedding: expect.objectContaining({ transport: "rest_api", missing: [] }),
}), { writeProbe: false });
expect(testResult.statusCode).toBe(400);
expect(testResult.json()).toEqual({
code: "workspace_not_activatable",
message: "Workspace cannot be activated on this installation.",
});
expect(diagnose).not.toHaveBeenCalled();
} finally {
process.env = originalEnv;
}
+100
View File
@@ -0,0 +1,100 @@
import { expect, test, vi } from "vitest";
import { ThtRunner } from "../src/tht/tht-runner.js";
import type { CanonicalWorkspace } from "../src/workspaces/schema.js";
const keywordIndexes = [
"content_hash", "document_id", "kind", "record_key", "record_kind",
"vector_generation", "workspace_id", "workspace_revision",
];
const workspace: CanonicalWorkspace = {
workspace: { schema_version: 3, 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>) {
return new ThtRunner({
thtBin: "tht",
harnessDir: "/harness",
configPath: "config/tht.yaml",
semanticRuntime: {
internalQdrantUrl: "http://qdrant:6333",
internalEmbeddingUrl: "http://embedding:11434",
internalEmbeddingModel: "qwen3-embedding:0.6b",
internalEmbeddingDimensions: 1024,
},
qdrantRequest: request,
});
}
function response(status: number, body: unknown) {
return {
ok: status >= 200 && status < 300,
status,
json: async () => body,
};
}
function collection(overrides: Record<string, unknown> = {}) {
return {
result: {
config: { params: { vectors: { size: 1024, distance: "Cosine" } } },
payload_schema: Object.fromEntries(keywordIndexes.map((field) => [field, { data_type: "keyword" }])),
...overrides,
},
};
}
test("Qdrant readiness uses only the internal URL and accepts the exact collection contract", async () => {
const request = vi.fn(async () => response(200, collection()));
await expect(runner(request).qdrantEnsure(workspace, 3)).resolves.toEqual({ ok: true });
expect(request).toHaveBeenCalledOnce();
expect(request.mock.calls[0][0]).toBe("http://qdrant:6333/collections/psd");
expect(request.mock.calls[0][1]).toMatchObject({ method: "GET", signal: expect.any(AbortSignal) });
});
test("Qdrant readiness classifies a missing collection as semantic incompatibility", async () => {
const request = vi.fn(async () => response(404, { status: "error", detail: "secret" }));
await expect(runner(request).qdrantEnsure(workspace, 3)).resolves.toEqual({
ok: false,
code: "semantic_index_incompatible",
});
});
test.each([
["dimensions", collection({
config: { params: { vectors: { size: 768, distance: "Cosine" } } },
})],
["distance", collection({
config: { params: { vectors: { size: 1024, distance: "Dot" } } },
})],
["payload indexes", collection({ payload_schema: { workspace_id: { data_type: "keyword" } } })],
])("Qdrant readiness rejects incompatible %s", async (_label, body) => {
const request = vi.fn(async () => response(200, body));
await expect(runner(request).qdrantEnsure(workspace, 3)).resolves.toEqual({
ok: false,
code: "semantic_index_incompatible",
});
});
test("Qdrant readiness sanitizes unreachable internal service failures", async () => {
const request = vi.fn(async () => { throw new Error("connect http://qdrant:6333/private"); });
await expect(runner(request).qdrantEnsure(workspace, 3)).resolves.toEqual({
ok: false,
code: "workspace_not_activatable",
});
});
+3
View File
@@ -662,6 +662,9 @@ test("lists a schema v2 descriptor as migration_required and refuses to acquire
await expect(registry.acquireSessionRevision("psd-clinical")).rejects.toMatchObject({
code: "workspace_invalid",
});
await expect(registry.readPinned("psd-clinical", remote.initialCommit)).rejects.toMatchObject({
code: "workspace_invalid",
});
});
test("lists operational descriptors retained after their workspace was removed from the active revision", async () => {
+23 -2
View File
@@ -42,6 +42,18 @@ llm_policy:
allowed: [zai/glm-5.2]
`;
const migrationRequiredWorkspace = canonicalWorkspace
.replace("schema_version: 3", "schema_version: 2")
.replace(
" engine: qdrant\n collection: psd-clinical",
" engine: pgvector\n database: analytics\n schema: vectors\n collection: documents",
)
.replace(" dimensions: 1024", " dimensions: 768")
.replace(" distance: cosine", " distance: cosine\n supported_transports: [rest_api]")
.replace(" provider: ollama_internal", " provider: ollama_compatible")
.replace(" model: qwen3-embedding:0.6b", " model: nomic-embed-text")
.replace(" dimensions: 1024", " dimensions: 768");
afterEach(() => {
vi.unstubAllEnvs();
roots.splice(0).forEach((root) => rmSync(root, { recursive: true, force: true }));
@@ -51,7 +63,7 @@ async function git(cwd: string, args: string[]): Promise<string> {
return (await runFile("git", args, { cwd })).stdout.trim();
}
async function fixture() {
async function fixture(workspaceSource = canonicalWorkspace) {
const root = mkdtempSync(join(tmpdir(), "tht-runtime-handoff-"));
roots.push(root);
const remote = join(root, "remote.git");
@@ -65,7 +77,7 @@ async function fixture() {
await git(source, ["config", "user.name", "Runtime Handoff Test"]);
await git(source, ["config", "user.email", "runtime-handoff@example.invalid"]);
mkdirSync(join(source, "workspaces"));
writeFileSync(join(source, "workspaces", "psd-clinical.yaml"), canonicalWorkspace);
writeFileSync(join(source, "workspaces", "psd-clinical.yaml"), workspaceSource);
await git(source, ["add", "workspaces/psd-clinical.yaml"]);
await git(source, ["commit", "-m", "Canonical workspace"]);
await git(source, ["remote", "add", "origin", remote]);
@@ -160,6 +172,15 @@ test("separate runtime leases hand off one stable logical workspace identity", a
}
});
test("ThtRunner refuses to render a migration-required registry snapshot", async () => {
const f = await fixture(migrationRequiredWorkspace);
const runner = runnerFor(f);
expect(() => runner.acquireWorkspaceRuntime(f.revision.snapshotPath)).toThrow(
"Workspace descriptor requires explicit migration to schema version 3",
);
});
test("local GET sessions mine uses the real canonical handoff and returns an empty inventory", async () => {
const f = await fixture();
const app = buildApp(loadConfig({