Files
ThothII/backend/test/qdrant-collection.test.ts

173 lines
7.2 KiB
TypeScript

import { expect, test } from "vitest";
import { QDRANT_REQUIRED_INDEXES, reconcileCollection } from "../src/workspaces/qdrant-collection.js";
function fakeRequest(info: any | undefined, { create = true, index = true } = {}) {
let current = info;
let created = false;
return async (url: string, init?: any) => {
if (init?.method === "PUT" && /\/index$/.test(url)) {
if (!index) return { status: 409, ok: false, json: async () => ({}) } as any;
// Simulate the index being created: the collection becomes fully compatible.
current = compatible();
return { status: 200, ok: true, json: async () => ({}) } as any;
}
if (init?.method === "PUT") {
if (!create) return { status: 409, ok: false, json: async () => ({}) } as any;
created = true;
current = compatible();
return { status: 200, ok: true, json: async () => ({}) } as any;
}
if (current === undefined) return { status: 404, ok: false, json: async () => ({}) } as any;
return { status: 200, ok: true, json: async () => ({ result: current }) } as any;
};
}
const payloadSchema = Object.fromEntries(QDRANT_REQUIRED_INDEXES.map((f) => [f, { data_type: "keyword" }]));
const compatible = (size = 1024, distance = "Cosine", schema = payloadSchema) => ({
config: { params: { vectors: { size, distance } } },
payload_schema: schema,
});
test("Evidence maintenance adds an absent BM25 vector without changing the dense contract", async () => {
const info = compatible();
const requests: Array<{ url: string; init?: any }> = [];
const request = async (url: string, init?: any) => {
requests.push({ url, init });
if (init?.method === "PUT" && /\/vectors\/bm25$/.test(url)) {
expect(JSON.parse(String(init.body))).toEqual({ sparse: { modifier: "idf" } });
(info.config.params as any).sparse_vectors = { bm25: { modifier: "idf" } };
return { status: 200, ok: true, json: async () => ({}) } as any;
}
return { status: 200, ok: true, json: async () => ({ result: info }) } as any;
};
const result = await reconcileCollection({
baseUrl: "http://qdrant:6333", collection: "c", dimensions: 1024, distance: "cosine",
mode: "evidence_maintenance", request,
});
expect(result).toEqual({ ok: true, state: "upgraded" });
expect(info.config.params.vectors).toEqual({ size: 1024, distance: "Cosine" });
expect(requests.filter(({ init }) => init?.method === "PUT")).toHaveLength(1);
expect(requests[1]?.url).toBe("http://qdrant:6333/collections/c/vectors/bm25");
});
test("Evidence maintenance creates a missing collection with both required vector contracts", async () => {
let info: any;
let createdBody: any;
const request = async (url: string, init?: any) => {
if (init?.method === "PUT") {
createdBody = JSON.parse(String(init.body));
info = { config: { params: createdBody }, payload_schema: payloadSchema };
return { status: 200, ok: true, json: async () => ({}) } as any;
}
if (info === undefined) return { status: 404, ok: false, json: async () => ({}) } as any;
return { status: 200, ok: true, json: async () => ({ result: info }) } as any;
};
const result = await reconcileCollection({
baseUrl: "http://qdrant:6333", collection: "c", dimensions: 1024, distance: "cosine",
mode: "evidence_maintenance", request,
});
expect(result).toEqual({ ok: true, state: "ready" });
expect(createdBody).toEqual({
vectors: { size: 1024, distance: "Cosine" },
sparse_vectors: { bm25: { modifier: "idf" } },
});
});
test("Evidence maintenance refuses an incompatible BM25 definition without mutating", async () => {
const info = compatible();
(info.config.params as any).sparse_vectors = { bm25: { modifier: "none" } };
const requests: Array<{ url: string; init?: any }> = [];
const request = async (url: string, init?: any) => {
requests.push({ url, init });
return { status: 200, ok: true, json: async () => ({ result: info }) } as any;
};
const result = await reconcileCollection({
baseUrl: "http://qdrant:6333", collection: "c", dimensions: 1024, distance: "cosine",
mode: "evidence_maintenance", request,
});
expect(result).toEqual({ ok: false, code: "semantic_index_incompatible" });
expect(requests.filter(({ init }) => init?.method === "PUT")).toEqual([]);
});
test("ordinary session reconciliation does not add BM25", async () => {
const info = compatible();
const requests: Array<{ url: string; init?: any }> = [];
const request = async (url: string, init?: any) => {
requests.push({ url, init });
return { status: 200, ok: true, json: async () => ({ result: info }) } as any;
};
const result = await reconcileCollection({
baseUrl: "http://qdrant:6333", collection: "c", dimensions: 1024, distance: "cosine",
mode: "self_heal", request,
});
expect(result).toEqual({ ok: true, state: "ready" });
expect(requests.filter(({ url }) => /\/vectors\/bm25$/.test(url))).toEqual([]);
});
test("self-heal creates a missing compatible collection", async () => {
const r = await reconcileCollection({
baseUrl: "http://qdrant:6333", collection: "c", dimensions: 1024, distance: "cosine",
mode: "self_heal", request: fakeRequest(undefined),
});
expect(r.ok).toBe(true);
});
test("require_existing refuses a missing collection", async () => {
const r = await reconcileCollection({
baseUrl: "http://qdrant:6333", collection: "c", dimensions: 1024, distance: "cosine",
mode: "require_existing", request: fakeRequest(undefined),
});
expect(r).toEqual({ ok: false, code: "semantic_index_incompatible" });
});
test("self-heal adds missing keyword indexes", async () => {
const schema = { ...payloadSchema };
delete schema["workspace_revision"];
const r = await reconcileCollection({
baseUrl: "http://qdrant:6333", collection: "c", dimensions: 1024, distance: "cosine",
mode: "self_heal", request: fakeRequest(compatible(1024, "Cosine", schema)),
});
expect(r).toMatchObject({ ok: true, state: "repaired" });
});
test("refuses incompatible dimensions or distance without mutating", async () => {
for (const info of [compatible(768, "Cosine"), compatible(1024, "Dot")]) {
const r = await reconcileCollection({
baseUrl: "http://qdrant:6333", collection: "c", dimensions: 1024, distance: "cosine",
mode: "self_heal", request: fakeRequest(info),
});
expect(r).toEqual({ ok: false, code: "semantic_index_incompatible" });
}
});
test("self-heal creates with the Qdrant-valid distance enum", async () => {
let createdBody: any;
const base = fakeRequest(undefined);
const request = async (url: string, init?: any) => {
if (init?.method === "PUT" && !/\/index$/.test(url)) createdBody = JSON.parse(String(init.body));
return base(url, init);
};
const r = await reconcileCollection({
baseUrl: "http://qdrant:6333", collection: "c", dimensions: 1024, distance: "cosine",
mode: "self_heal", request,
});
expect(r.ok).toBe(true);
expect(createdBody.vectors.distance).toBe("Cosine");
});
test("ready compatible collection passes", async () => {
const r = await reconcileCollection({
baseUrl: "http://qdrant:6333", collection: "c", dimensions: 1024, distance: "cosine",
mode: "require_existing", request: fakeRequest(compatible()),
});
expect(r).toMatchObject({ ok: true, state: "ready" });
});