94 lines
3.2 KiB
TypeScript
94 lines
3.2 KiB
TypeScript
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: 4, id: "psd", name: "PSD", language: "it" },
|
|
dwh: {
|
|
engine: "postgres", database: "warehouse", schema: "public",
|
|
supported_transports: ["postgres_direct"],
|
|
},
|
|
};
|
|
|
|
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, state: "ready" });
|
|
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",
|
|
});
|
|
});
|