feat: diagnose workspace connector bindings
This commit is contained in:
@@ -23,9 +23,12 @@ export interface AppConfig {
|
||||
* pay the probe; the local dev launcher (run-stack.sh) opts in via THT_DWH_PRECHECK.
|
||||
*/
|
||||
dwhPrecheck: boolean;
|
||||
workspaceDiagnosticTimeoutMs: number;
|
||||
workspaceRegistry: WorkspaceRegistryConfig;
|
||||
}
|
||||
|
||||
export const MAX_WORKSPACE_DIAGNOSTIC_TIMEOUT_MS = 10_000;
|
||||
|
||||
function requiredRegistryValue(value: string, label: string): string {
|
||||
if (value.length === 0 || value.trim() !== value || value.includes("\0")) {
|
||||
throw new Error(`workspace registry ${label} configuration is invalid`);
|
||||
@@ -77,6 +80,14 @@ function positiveImportLimit(value: string | undefined, fallback: number): numbe
|
||||
return limit;
|
||||
}
|
||||
|
||||
function diagnosticTimeout(value: string | undefined): number {
|
||||
const timeout = Number(value ?? 5_000);
|
||||
if (!Number.isSafeInteger(timeout) || timeout < 1 || timeout > MAX_WORKSPACE_DIAGNOSTIC_TIMEOUT_MS) {
|
||||
throw new Error("workspace diagnostic timeout configuration is invalid");
|
||||
}
|
||||
return timeout;
|
||||
}
|
||||
|
||||
export function loadConfig(env: Record<string, string | undefined>): AppConfig {
|
||||
const authMode = env.AUTH_MODE ?? "none";
|
||||
if (!(["none", "mock", "upstream"] as const).includes(authMode as AppConfig["authMode"])) {
|
||||
@@ -192,6 +203,7 @@ export function loadConfig(env: Record<string, string | undefined>): AppConfig {
|
||||
secretFiles,
|
||||
modelApiKeyFile,
|
||||
dwhPrecheck: env.THT_DWH_PRECHECK === "true" || env.THT_DWH_PRECHECK === "1",
|
||||
workspaceDiagnosticTimeoutMs: diagnosticTimeout(env.THT_WORKSPACE_DIAGNOSTIC_TIMEOUT_MS),
|
||||
workspaceRegistry,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,408 @@
|
||||
import { randomUUID } from "node:crypto";
|
||||
import { MAX_WORKSPACE_DIAGNOSTIC_TIMEOUT_MS } from "../config.js";
|
||||
import { buildInstallationContract } from "./contracts.js";
|
||||
import type { RuntimeBindings } from "./runtime-renderer.js";
|
||||
import { validateCanonicalWorkspace, type CanonicalWorkspace } from "./schema.js";
|
||||
import type { WorkspaceErrorCode } from "./types.js";
|
||||
|
||||
export interface Diagnostic {
|
||||
level: "error" | "warning" | "info";
|
||||
code: WorkspaceErrorCode | "binding_ok";
|
||||
field?: string;
|
||||
message: string;
|
||||
}
|
||||
|
||||
export interface WorkspaceDiagnostics {
|
||||
activatable: boolean;
|
||||
diagnostics: Diagnostic[];
|
||||
}
|
||||
|
||||
type ConnectorRole = "dwh" | "vector";
|
||||
|
||||
interface DiagnosticResource {
|
||||
database?: string;
|
||||
schema?: string;
|
||||
collection?: string;
|
||||
}
|
||||
|
||||
export interface ConnectorDiagnosticRequest {
|
||||
role: ConnectorRole;
|
||||
transport: "postgres_direct" | "pgvector_direct" | "rest_api" | "ssh_tunnel";
|
||||
host?: string;
|
||||
port?: number;
|
||||
baseUrl?: string;
|
||||
user?: string;
|
||||
credentialFile: string;
|
||||
tlsCaFile?: string;
|
||||
resource: DiagnosticResource;
|
||||
timeoutMs: number;
|
||||
signal: AbortSignal;
|
||||
}
|
||||
|
||||
export interface ConnectorDiagnosticResult {
|
||||
resolved: boolean;
|
||||
tlsVerified: boolean;
|
||||
authenticated: boolean;
|
||||
resource: DiagnosticResource;
|
||||
}
|
||||
|
||||
export interface SshTunnelRequest {
|
||||
sshHost: string;
|
||||
sshPort: number;
|
||||
sshUser: string;
|
||||
privateKeyFile: string;
|
||||
knownHostsFile: string;
|
||||
targetHost: string;
|
||||
targetPort: number;
|
||||
localHost: "127.0.0.1";
|
||||
localPort: 0;
|
||||
timeoutMs: number;
|
||||
signal: AbortSignal;
|
||||
}
|
||||
|
||||
export interface LoopbackTunnel {
|
||||
host: "127.0.0.1";
|
||||
port: number;
|
||||
}
|
||||
|
||||
export interface VectorDiagnosticRequest {
|
||||
collection: string;
|
||||
timeoutMs: number;
|
||||
signal: AbortSignal;
|
||||
}
|
||||
|
||||
export interface VectorDiagnosticResult {
|
||||
collection?: string;
|
||||
dimensions?: number;
|
||||
distance?: "cosine" | "l2" | "inner_product";
|
||||
}
|
||||
|
||||
export interface EmbeddingDiagnosticRequest {
|
||||
baseUrl: string;
|
||||
credentialFile?: string;
|
||||
tlsCaFile?: string;
|
||||
model: string;
|
||||
timeoutMs: number;
|
||||
signal: AbortSignal;
|
||||
}
|
||||
|
||||
export interface EmbeddingDiagnosticResult {
|
||||
available: boolean;
|
||||
dimensions?: number;
|
||||
}
|
||||
|
||||
export interface WriteDiagnosticRecordRequest {
|
||||
collection: string;
|
||||
id: string;
|
||||
dimensions: number;
|
||||
timeoutMs: number;
|
||||
signal: AbortSignal;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adapters own protocol-specific I/O. They receive only binding file paths, never secret
|
||||
* contents, and return metadata only; response bodies must stay inside the adapter.
|
||||
*/
|
||||
export interface DiagnosticAdapters {
|
||||
probeConnector(request: ConnectorDiagnosticRequest): Promise<ConnectorDiagnosticResult>;
|
||||
withSshTunnel<T>(
|
||||
request: SshTunnelRequest,
|
||||
probe: (tunnel: LoopbackTunnel) => Promise<T>,
|
||||
): Promise<T>;
|
||||
inspectVector(request: VectorDiagnosticRequest): Promise<VectorDiagnosticResult>;
|
||||
probeEmbedding(request: EmbeddingDiagnosticRequest): Promise<EmbeddingDiagnosticResult>;
|
||||
writeDiagnosticRecord(request: WriteDiagnosticRecordRequest): Promise<void>;
|
||||
removeDiagnosticRecord(request: WriteDiagnosticRecordRequest): Promise<void>;
|
||||
}
|
||||
|
||||
export const DEFAULT_WORKSPACE_DIAGNOSTIC_TIMEOUT_MS = 5_000;
|
||||
|
||||
function unavailableAdapters(): DiagnosticAdapters {
|
||||
const unavailable = async (): Promise<never> => {
|
||||
throw new Error("diagnostic adapter unavailable");
|
||||
};
|
||||
return {
|
||||
probeConnector: unavailable,
|
||||
withSshTunnel: unavailable,
|
||||
inspectVector: unavailable,
|
||||
probeEmbedding: unavailable,
|
||||
writeDiagnosticRecord: unavailable,
|
||||
removeDiagnosticRecord: unavailable,
|
||||
};
|
||||
}
|
||||
|
||||
function boundedTimeout(value: number | undefined, fallback: number): number {
|
||||
const selected = value ?? fallback;
|
||||
return Math.min(Math.max(1, selected), fallback, MAX_WORKSPACE_DIAGNOSTIC_TIMEOUT_MS);
|
||||
}
|
||||
|
||||
async function withTimeout<T>(timeoutMs: number, operation: (signal: AbortSignal) => Promise<T>): Promise<T> {
|
||||
const controller = new AbortController();
|
||||
let timer: NodeJS.Timeout | undefined;
|
||||
try {
|
||||
return await new Promise<T>((resolve, reject) => {
|
||||
timer = setTimeout(() => {
|
||||
controller.abort();
|
||||
reject(new Error("diagnostic timed out"));
|
||||
}, timeoutMs);
|
||||
void operation(controller.signal).then(resolve, reject);
|
||||
});
|
||||
} finally {
|
||||
if (timer !== undefined) clearTimeout(timer);
|
||||
controller.abort();
|
||||
}
|
||||
}
|
||||
|
||||
function sameResource(expected: DiagnosticResource, actual: DiagnosticResource): boolean {
|
||||
return Object.entries(expected).every(([key, value]) => actual[key as keyof DiagnosticResource] === value);
|
||||
}
|
||||
|
||||
function hasRequiredConnectorChecks(result: ConnectorDiagnosticResult, resource: DiagnosticResource): boolean {
|
||||
return result.resolved && result.tlsVerified && result.authenticated && sameResource(resource, result.resource);
|
||||
}
|
||||
|
||||
function diagnosticError(code: WorkspaceErrorCode, field?: string): Diagnostic {
|
||||
return {
|
||||
level: "error",
|
||||
code,
|
||||
...(field ? { field } : {}),
|
||||
message: code === "binding_missing"
|
||||
? "Installation binding is missing or invalid."
|
||||
: code === "semantic_index_incompatible"
|
||||
? "Semantic index metadata is incompatible with this workspace."
|
||||
: "Connector diagnostic failed.",
|
||||
};
|
||||
}
|
||||
|
||||
function bindingName(
|
||||
workspace: CanonicalWorkspace,
|
||||
role: "DWH" | "VECTOR" | "EMBEDDING",
|
||||
suffix: string,
|
||||
): string {
|
||||
const entry = buildInstallationContract(workspace).variables.find((variable) => (
|
||||
variable.role === role && variable.suffix === suffix
|
||||
));
|
||||
if (!entry) throw new Error(`workspace contract is missing ${role}_${suffix}`);
|
||||
return entry.name;
|
||||
}
|
||||
|
||||
function numericBinding(binding: Record<string, string>, name: string): number | undefined {
|
||||
const value = Number(binding[name]);
|
||||
return Number.isInteger(value) && value > 0 && value <= 65_535 ? value : undefined;
|
||||
}
|
||||
|
||||
function diagnosticsForMissingBindings(
|
||||
workspace: CanonicalWorkspace,
|
||||
bindings: RuntimeBindings,
|
||||
): Diagnostic[] {
|
||||
const missing = new Set([
|
||||
...bindings.dwh.missing,
|
||||
...bindings.vector.missing,
|
||||
...bindings.embedding.missing,
|
||||
]);
|
||||
const knownHosts = [
|
||||
bindings.dwh.transport === "ssh_tunnel" ? bindingName(workspace, "DWH", "SSH_KNOWN_HOSTS_FILE") : undefined,
|
||||
bindings.vector.transport === "ssh_tunnel" ? bindingName(workspace, "VECTOR", "SSH_KNOWN_HOSTS_FILE") : undefined,
|
||||
].filter((field): field is string => field !== undefined);
|
||||
const ordered = [...new Set([...knownHosts.filter((field) => missing.has(field)), ...[...missing].sort()])];
|
||||
return ordered.map((field) => diagnosticError("binding_missing", field));
|
||||
}
|
||||
|
||||
function connectorRequest(
|
||||
workspace: CanonicalWorkspace,
|
||||
role: ConnectorRole,
|
||||
bindings: RuntimeBindings,
|
||||
timeoutMs: number,
|
||||
): ConnectorDiagnosticRequest | SshTunnelRequest | undefined {
|
||||
const binding = role === "dwh" ? bindings.dwh : bindings.vector;
|
||||
const contractRole = role === "dwh" ? "DWH" : "VECTOR";
|
||||
const values = binding.values;
|
||||
const resource: DiagnosticResource = role === "dwh"
|
||||
? { database: workspace.dwh.database, schema: workspace.dwh.schema }
|
||||
: { collection: workspace.semantic_index.vector_store.collection };
|
||||
const field = (suffix: string) => bindingName(workspace, contractRole, suffix);
|
||||
const credentialFile = values[field(binding.transport === "rest_api" ? "API_KEY_FILE" : "PASSWORD_FILE")];
|
||||
if (credentialFile === undefined) return undefined;
|
||||
|
||||
if (binding.transport === "rest_api") {
|
||||
const baseUrl = values[field("BASE_URL")];
|
||||
if (baseUrl === undefined) return undefined;
|
||||
return {
|
||||
role,
|
||||
transport: "rest_api",
|
||||
baseUrl,
|
||||
credentialFile,
|
||||
tlsCaFile: values[field("TLS_CA_FILE")],
|
||||
resource,
|
||||
timeoutMs,
|
||||
signal: new AbortController().signal,
|
||||
};
|
||||
}
|
||||
|
||||
if (binding.transport === "ssh_tunnel") {
|
||||
const sshHost = values[field("SSH_HOST")];
|
||||
const sshPort = numericBinding(values, field("SSH_PORT"));
|
||||
const sshUser = values[field("SSH_USER")];
|
||||
const privateKeyFile = values[field("SSH_PRIVATE_KEY_FILE")];
|
||||
const knownHostsFile = values[field("SSH_KNOWN_HOSTS_FILE")];
|
||||
const targetHost = values[field("SSH_TARGET_HOST")];
|
||||
const targetPort = numericBinding(values, field("SSH_TARGET_PORT"));
|
||||
if (!sshHost || !sshPort || !sshUser || !privateKeyFile || !knownHostsFile || !targetHost || !targetPort) return undefined;
|
||||
return {
|
||||
sshHost, sshPort, sshUser, privateKeyFile, knownHostsFile, targetHost, targetPort,
|
||||
localHost: "127.0.0.1", localPort: 0, timeoutMs, signal: new AbortController().signal,
|
||||
};
|
||||
}
|
||||
|
||||
const host = values[field("HOST")];
|
||||
const port = numericBinding(values, field("PORT"));
|
||||
const user = values[field("USER")];
|
||||
if (!host || !port || !user) return undefined;
|
||||
return {
|
||||
role,
|
||||
transport: binding.transport,
|
||||
host,
|
||||
port,
|
||||
user,
|
||||
credentialFile,
|
||||
tlsCaFile: values[field("TLS_CA_FILE")],
|
||||
resource,
|
||||
timeoutMs,
|
||||
signal: new AbortController().signal,
|
||||
};
|
||||
}
|
||||
|
||||
function tunnelProbeRequest(
|
||||
workspace: CanonicalWorkspace,
|
||||
role: ConnectorRole,
|
||||
bindings: RuntimeBindings,
|
||||
timeoutMs: number,
|
||||
tunnel: LoopbackTunnel,
|
||||
signal: AbortSignal,
|
||||
): ConnectorDiagnosticRequest {
|
||||
const binding = role === "dwh" ? bindings.dwh : bindings.vector;
|
||||
const contractRole = role === "dwh" ? "DWH" : "VECTOR";
|
||||
const password = binding.values[bindingName(workspace, contractRole, "PASSWORD_FILE")];
|
||||
const user = binding.values[bindingName(workspace, contractRole, "USER")];
|
||||
if (!password || !user) throw new Error("missing SSH connector credentials");
|
||||
return {
|
||||
role,
|
||||
transport: "ssh_tunnel",
|
||||
host: tunnel.host,
|
||||
port: tunnel.port,
|
||||
user,
|
||||
credentialFile: password,
|
||||
tlsCaFile: binding.values[bindingName(workspace, contractRole, "TLS_CA_FILE")],
|
||||
resource: role === "dwh"
|
||||
? { database: workspace.dwh.database, schema: workspace.dwh.schema }
|
||||
: { collection: workspace.semantic_index.vector_store.collection },
|
||||
timeoutMs,
|
||||
signal,
|
||||
};
|
||||
}
|
||||
|
||||
export function createWorkspaceDiagnoser(
|
||||
adapters: DiagnosticAdapters,
|
||||
options: { timeoutMs?: number } = {},
|
||||
) {
|
||||
const fallbackTimeout = boundedTimeout(options.timeoutMs, DEFAULT_WORKSPACE_DIAGNOSTIC_TIMEOUT_MS);
|
||||
|
||||
return async function diagnoseWorkspace(
|
||||
workspace: CanonicalWorkspace,
|
||||
bindings: RuntimeBindings,
|
||||
options: { writeProbe: boolean },
|
||||
): Promise<WorkspaceDiagnostics> {
|
||||
const canonical = validateCanonicalWorkspace(workspace);
|
||||
const diagnostics = diagnosticsForMissingBindings(canonical, bindings);
|
||||
if (diagnostics.length > 0) return { activatable: false, diagnostics };
|
||||
|
||||
const dwhTimeout = boundedTimeout(canonical.dwh.timeout_ms, fallbackTimeout);
|
||||
const vectorTimeout = boundedTimeout(canonical.semantic_index.vector_store.timeout_ms, fallbackTimeout);
|
||||
const embeddingTimeout = boundedTimeout(canonical.semantic_index.embedding.timeout_ms, fallbackTimeout);
|
||||
|
||||
for (const role of ["dwh", "vector"] as const) {
|
||||
const timeoutMs = role === "dwh" ? dwhTimeout : vectorTimeout;
|
||||
const request = connectorRequest(canonical, role, bindings, timeoutMs);
|
||||
if (!request) {
|
||||
diagnostics.push(diagnosticError("binding_missing"));
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
const result = "sshHost" in request
|
||||
? await withTimeout(timeoutMs, (signal) => adapters.withSshTunnel(
|
||||
{ ...request, signal },
|
||||
(tunnel) => adapters.probeConnector(tunnelProbeRequest(
|
||||
canonical, role, bindings, timeoutMs, tunnel, signal,
|
||||
)),
|
||||
))
|
||||
: await withTimeout(timeoutMs, (signal) => adapters.probeConnector({ ...request, signal }));
|
||||
const resource = role === "dwh"
|
||||
? { database: canonical.dwh.database, schema: canonical.dwh.schema }
|
||||
: { collection: canonical.semantic_index.vector_store.collection };
|
||||
if (!hasRequiredConnectorChecks(result, resource)) diagnostics.push(diagnosticError("connector_unavailable"));
|
||||
else diagnostics.push({ level: "info", code: "binding_ok", message: `${role === "dwh" ? "DWH" : "Vector"} binding diagnostic passed.` });
|
||||
} catch {
|
||||
diagnostics.push(diagnosticError("connector_unavailable"));
|
||||
}
|
||||
}
|
||||
|
||||
if (!diagnostics.some((diagnostic) => diagnostic.level === "error")) {
|
||||
try {
|
||||
const vector = await withTimeout(vectorTimeout, (signal) => adapters.inspectVector({
|
||||
collection: canonical.semantic_index.vector_store.collection,
|
||||
timeoutMs: vectorTimeout,
|
||||
signal,
|
||||
}));
|
||||
const expected = canonical.semantic_index.vector_store;
|
||||
if (
|
||||
vector.collection !== expected.collection
|
||||
|| vector.dimensions !== expected.dimensions
|
||||
|| vector.distance !== expected.distance
|
||||
) diagnostics.push(diagnosticError("semantic_index_incompatible"));
|
||||
} catch {
|
||||
diagnostics.push(diagnosticError("connector_unavailable"));
|
||||
}
|
||||
}
|
||||
|
||||
if (!diagnostics.some((diagnostic) => diagnostic.level === "error")) {
|
||||
try {
|
||||
const embedding = await withTimeout(embeddingTimeout, (signal) => adapters.probeEmbedding({
|
||||
baseUrl: bindings.embedding.values[bindingName(canonical, "EMBEDDING", "BASE_URL")] ?? "",
|
||||
credentialFile: bindings.embedding.values[bindingName(canonical, "EMBEDDING", "API_KEY_FILE")],
|
||||
tlsCaFile: bindings.embedding.values[bindingName(canonical, "EMBEDDING", "TLS_CA_FILE")],
|
||||
model: canonical.semantic_index.embedding.model,
|
||||
timeoutMs: embeddingTimeout,
|
||||
signal,
|
||||
}));
|
||||
if (!embedding.available || embedding.dimensions !== canonical.semantic_index.embedding.dimensions) {
|
||||
diagnostics.push(diagnosticError("semantic_index_incompatible"));
|
||||
}
|
||||
} catch {
|
||||
diagnostics.push(diagnosticError("connector_unavailable"));
|
||||
}
|
||||
}
|
||||
|
||||
if (options.writeProbe && !diagnostics.some((diagnostic) => diagnostic.level === "error")) {
|
||||
const request: WriteDiagnosticRecordRequest = {
|
||||
collection: canonical.semantic_index.vector_store.collection,
|
||||
id: `diagnostic:${randomUUID()}`,
|
||||
dimensions: canonical.semantic_index.vector_store.dimensions,
|
||||
timeoutMs: vectorTimeout,
|
||||
signal: new AbortController().signal,
|
||||
};
|
||||
try {
|
||||
await withTimeout(vectorTimeout, (signal) => adapters.writeDiagnosticRecord({ ...request, signal }));
|
||||
await withTimeout(vectorTimeout, (signal) => adapters.removeDiagnosticRecord({ ...request, signal }));
|
||||
} catch {
|
||||
diagnostics.push(diagnosticError("connector_unavailable"));
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
activatable: !diagnostics.some((diagnostic) => diagnostic.level === "error"),
|
||||
diagnostics,
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export const diagnoseWorkspace = createWorkspaceDiagnoser(unavailableAdapters());
|
||||
@@ -0,0 +1,235 @@
|
||||
import { expect, test, vi } from "vitest";
|
||||
import {
|
||||
createWorkspaceDiagnoser,
|
||||
type DiagnosticAdapters,
|
||||
} from "../src/workspaces/diagnostics.js";
|
||||
import type { RuntimeBindings } from "../src/workspaces/runtime-renderer.js";
|
||||
import { parseWorkspaceYaml } from "../src/workspaces/schema.js";
|
||||
|
||||
const workspace = parseWorkspaceYaml(`workspace:
|
||||
schema_version: 1
|
||||
id: psd-clinical
|
||||
name: Policlinico San Donato
|
||||
language: it
|
||||
dwh:
|
||||
engine: postgres
|
||||
database: warehouse
|
||||
schema: datawarehouse
|
||||
timeout_ms: 8000
|
||||
supported_transports: [postgres_direct, rest_api, ssh_tunnel]
|
||||
semantic_index:
|
||||
vector_store:
|
||||
engine: pgvector
|
||||
collection: clinical_documents
|
||||
dimensions: 768
|
||||
distance: cosine
|
||||
timeout_ms: 8000
|
||||
supported_transports: [pgvector_direct, rest_api, ssh_tunnel]
|
||||
embedding:
|
||||
provider: ollama_compatible
|
||||
model: nomic-embed-text-v2-moe
|
||||
dimensions: 768
|
||||
timeout_ms: 8000
|
||||
llm_policy:
|
||||
allowed: [zai/glm-5.2]
|
||||
`);
|
||||
|
||||
const bindings: RuntimeBindings = {
|
||||
dwh: {
|
||||
transport: "postgres_direct",
|
||||
missing: [],
|
||||
values: {
|
||||
THT_WS_PSD_CLINICAL_DWH_HOST: "dwh.example.test",
|
||||
THT_WS_PSD_CLINICAL_DWH_PORT: "5432",
|
||||
THT_WS_PSD_CLINICAL_DWH_USER: "reader",
|
||||
THT_WS_PSD_CLINICAL_DWH_PASSWORD_FILE: "/run/secrets/dwh-password",
|
||||
THT_WS_PSD_CLINICAL_DWH_TLS_CA_FILE: "/run/secrets/dwh-ca",
|
||||
},
|
||||
},
|
||||
vector: {
|
||||
transport: "pgvector_direct",
|
||||
missing: [],
|
||||
values: {
|
||||
THT_WS_PSD_CLINICAL_VECTOR_HOST: "vector.example.test",
|
||||
THT_WS_PSD_CLINICAL_VECTOR_PORT: "5432",
|
||||
THT_WS_PSD_CLINICAL_VECTOR_USER: "vector-reader",
|
||||
THT_WS_PSD_CLINICAL_VECTOR_PASSWORD_FILE: "/run/secrets/vector-password",
|
||||
THT_WS_PSD_CLINICAL_VECTOR_TLS_CA_FILE: "/run/secrets/vector-ca",
|
||||
},
|
||||
},
|
||||
embedding: {
|
||||
transport: "rest_api",
|
||||
missing: [],
|
||||
values: {
|
||||
THT_WS_PSD_CLINICAL_EMBEDDING_BASE_URL: "https://embedding.example.test",
|
||||
THT_WS_PSD_CLINICAL_EMBEDDING_API_KEY_FILE: "/run/secrets/embedding-key",
|
||||
THT_WS_PSD_CLINICAL_EMBEDDING_TLS_CA_FILE: "/run/secrets/embedding-ca",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
function successfulAdapters(overrides: Partial<DiagnosticAdapters> = {}): DiagnosticAdapters {
|
||||
return {
|
||||
probeConnector: vi.fn(async (request) => ({
|
||||
resolved: true,
|
||||
tlsVerified: true,
|
||||
authenticated: true,
|
||||
resource: request.resource,
|
||||
})),
|
||||
withSshTunnel: vi.fn(async (_request, probe) => probe({ host: "127.0.0.1", port: 45678 })),
|
||||
inspectVector: vi.fn(async () => ({
|
||||
collection: "clinical_documents",
|
||||
dimensions: 768,
|
||||
distance: "cosine",
|
||||
})),
|
||||
probeEmbedding: vi.fn(async () => ({ available: true, dimensions: 768 })),
|
||||
writeDiagnosticRecord: vi.fn(async () => undefined),
|
||||
removeDiagnosticRecord: vi.fn(async () => undefined),
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
function diagnose(adapters = successfulAdapters()) {
|
||||
return createWorkspaceDiagnoser(adapters, { timeoutMs: 5000 });
|
||||
}
|
||||
|
||||
test("reports the missing vector collection dimensions as semantic-index incompatibility", async () => {
|
||||
const result = await diagnose(successfulAdapters({
|
||||
inspectVector: vi.fn(async () => ({
|
||||
collection: "clinical_documents",
|
||||
dimensions: undefined,
|
||||
distance: "cosine",
|
||||
})),
|
||||
}))(workspace, bindings, { writeProbe: false });
|
||||
|
||||
expect(result.diagnostics).toContainEqual(expect.objectContaining({
|
||||
code: "semantic_index_incompatible",
|
||||
}));
|
||||
expect(result.activatable).toBe(false);
|
||||
});
|
||||
|
||||
test("refuses an SSH tunnel when known-hosts is missing", async () => {
|
||||
const sshBindingsWithoutKnownHosts: RuntimeBindings = {
|
||||
...bindings,
|
||||
dwh: {
|
||||
transport: "ssh_tunnel",
|
||||
missing: ["THT_WS_PSD_CLINICAL_DWH_SSH_KNOWN_HOSTS_FILE"],
|
||||
values: {
|
||||
THT_WS_PSD_CLINICAL_DWH_USER: "reader",
|
||||
THT_WS_PSD_CLINICAL_DWH_PASSWORD_FILE: "/run/secrets/dwh-password",
|
||||
THT_WS_PSD_CLINICAL_DWH_SSH_HOST: "bastion.example.test",
|
||||
THT_WS_PSD_CLINICAL_DWH_SSH_PORT: "22",
|
||||
THT_WS_PSD_CLINICAL_DWH_SSH_USER: "tunnel",
|
||||
THT_WS_PSD_CLINICAL_DWH_SSH_PRIVATE_KEY_FILE: "/run/secrets/ssh-key",
|
||||
THT_WS_PSD_CLINICAL_DWH_SSH_TARGET_HOST: "dwh.internal",
|
||||
THT_WS_PSD_CLINICAL_DWH_SSH_TARGET_PORT: "5432",
|
||||
},
|
||||
},
|
||||
};
|
||||
const adapters = successfulAdapters();
|
||||
|
||||
const result = await diagnose(adapters)(workspace, sshBindingsWithoutKnownHosts, { writeProbe: false });
|
||||
|
||||
expect(result.activatable).toBe(false);
|
||||
expect(result.diagnostics[0]).toMatchObject({
|
||||
code: "binding_missing",
|
||||
field: expect.stringContaining("SSH_KNOWN_HOSTS_FILE"),
|
||||
});
|
||||
expect(adapters.withSshTunnel).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("checks direct and REST resolution, TLS, authentication, and resource metadata without exposing failures", async () => {
|
||||
const adapters = successfulAdapters({
|
||||
probeConnector: vi.fn(async (request) => ({
|
||||
resolved: true,
|
||||
tlsVerified: true,
|
||||
authenticated: true,
|
||||
resource: request.role === "dwh"
|
||||
? { database: "warehouse", schema: "wrong_schema" }
|
||||
: request.resource,
|
||||
})),
|
||||
});
|
||||
const restBindings: RuntimeBindings = {
|
||||
...bindings,
|
||||
dwh: {
|
||||
transport: "rest_api",
|
||||
missing: [],
|
||||
values: {
|
||||
THT_WS_PSD_CLINICAL_DWH_BASE_URL: "https://dwh.example.test",
|
||||
THT_WS_PSD_CLINICAL_DWH_API_KEY_FILE: "/run/secrets/dwh-api-key",
|
||||
THT_WS_PSD_CLINICAL_DWH_TLS_CA_FILE: "/run/secrets/dwh-ca",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const result = await diagnose(adapters)(workspace, restBindings, { writeProbe: false });
|
||||
|
||||
expect(adapters.probeConnector).toHaveBeenCalledWith(expect.objectContaining({
|
||||
transport: "rest_api",
|
||||
timeoutMs: 5000,
|
||||
tlsCaFile: "/run/secrets/dwh-ca",
|
||||
credentialFile: "/run/secrets/dwh-api-key",
|
||||
resource: { database: "warehouse", schema: "datawarehouse" },
|
||||
}));
|
||||
expect(result).toMatchObject({ activatable: false });
|
||||
expect(JSON.stringify(result)).not.toContain("wrong_schema");
|
||||
expect(JSON.stringify(result)).not.toContain("/run/secrets/dwh-api-key");
|
||||
});
|
||||
|
||||
test("uses a loopback-only SSH tunnel for the bounded connector probe", async () => {
|
||||
const adapters = successfulAdapters();
|
||||
const sshBindings: RuntimeBindings = {
|
||||
...bindings,
|
||||
dwh: {
|
||||
transport: "ssh_tunnel",
|
||||
missing: [],
|
||||
values: {
|
||||
THT_WS_PSD_CLINICAL_DWH_USER: "reader",
|
||||
THT_WS_PSD_CLINICAL_DWH_PASSWORD_FILE: "/run/secrets/dwh-password",
|
||||
THT_WS_PSD_CLINICAL_DWH_TLS_CA_FILE: "/run/secrets/dwh-ca",
|
||||
THT_WS_PSD_CLINICAL_DWH_SSH_HOST: "bastion.example.test",
|
||||
THT_WS_PSD_CLINICAL_DWH_SSH_PORT: "22",
|
||||
THT_WS_PSD_CLINICAL_DWH_SSH_USER: "tunnel",
|
||||
THT_WS_PSD_CLINICAL_DWH_SSH_PRIVATE_KEY_FILE: "/run/secrets/ssh-key",
|
||||
THT_WS_PSD_CLINICAL_DWH_SSH_KNOWN_HOSTS_FILE: "/run/secrets/known-hosts",
|
||||
THT_WS_PSD_CLINICAL_DWH_SSH_TARGET_HOST: "dwh.internal",
|
||||
THT_WS_PSD_CLINICAL_DWH_SSH_TARGET_PORT: "5432",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const result = await diagnose(adapters)(workspace, sshBindings, { writeProbe: false });
|
||||
|
||||
expect(result.activatable).toBe(true);
|
||||
expect(adapters.withSshTunnel).toHaveBeenCalledWith(expect.objectContaining({
|
||||
localHost: "127.0.0.1",
|
||||
localPort: 0,
|
||||
knownHostsFile: "/run/secrets/known-hosts",
|
||||
timeoutMs: 5000,
|
||||
}), expect.any(Function));
|
||||
expect(adapters.probeConnector).toHaveBeenCalledWith(expect.objectContaining({
|
||||
host: "127.0.0.1",
|
||||
port: 45678,
|
||||
}));
|
||||
});
|
||||
|
||||
test("requires a matching embedding model vector and removes its unique write probe", async () => {
|
||||
const adapters = successfulAdapters();
|
||||
|
||||
const result = await diagnose(adapters)(workspace, bindings, { writeProbe: true });
|
||||
|
||||
expect(result.activatable).toBe(true);
|
||||
expect(adapters.probeEmbedding).toHaveBeenCalledWith(expect.objectContaining({
|
||||
model: "nomic-embed-text-v2-moe",
|
||||
timeoutMs: 5000,
|
||||
}));
|
||||
expect(adapters.writeDiagnosticRecord).toHaveBeenCalledWith(expect.objectContaining({
|
||||
collection: "clinical_documents",
|
||||
id: expect.stringMatching(/^diagnostic:/),
|
||||
dimensions: 768,
|
||||
}));
|
||||
expect(adapters.removeDiagnosticRecord).toHaveBeenCalledWith(expect.objectContaining({
|
||||
collection: "clinical_documents",
|
||||
id: expect.stringMatching(/^diagnostic:/),
|
||||
}));
|
||||
});
|
||||
@@ -17,7 +17,7 @@ ARG PI_VERSION
|
||||
|
||||
# Runtime tools
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
curl ca-certificates ripgrep fd-find tini \
|
||||
curl ca-certificates ripgrep fd-find tini git openssh-client \
|
||||
&& rm -rf /var/lib/apt/lists/* \
|
||||
&& ln -s /usr/bin/fdfind /usr/local/bin/fd
|
||||
|
||||
|
||||
Reference in New Issue
Block a user