- AI_LLM_BASE_URL default -> http://127.0.0.1:4014/v1 (was imrnes :20128/api/v1) - QDRANT_URL fallback -> http://127.0.0.1:6333 (was imrnes :6333) - ensureQdrantCollection: reset memoised promise on failure so a mid-way recreate abort (DELETE done, PUT failed) does not leave the collection permanently missing until process restart - tests: qdrantEnsure.test.ts (3 cases: retry-on-failure, recreate, idempotent)
83 lines
2.6 KiB
TypeScript
83 lines
2.6 KiB
TypeScript
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
|
|
// The qdrant client reads config at import time — we only need the
|
|
// reset-on-failure behavior, so mock global fetch and import fresh.
|
|
import {
|
|
ensureQdrantCollection,
|
|
resetQdrantCollectionCache,
|
|
} from "../src/modules/ai-moderation/qdrantClient.js";
|
|
|
|
function jsonResponse(body: unknown, ok = true, status = 200): Response {
|
|
return {
|
|
ok,
|
|
status,
|
|
text: async () => JSON.stringify(body),
|
|
} as unknown as Response;
|
|
}
|
|
|
|
describe("ensureQdrantCollection retry-on-failure", () => {
|
|
beforeEach(() => {
|
|
resetQdrantCollectionCache();
|
|
vi.restoreAllMocks();
|
|
});
|
|
|
|
afterEach(() => {
|
|
resetQdrantCollectionCache();
|
|
vi.restoreAllMocks();
|
|
});
|
|
|
|
it("returns false on failure and does NOT get stuck — retries on next call", async () => {
|
|
// First call: GET collection fails hard (not a 404) → ensure rejects.
|
|
const fetchMock = vi
|
|
.spyOn(globalThis, "fetch")
|
|
.mockRejectedValueOnce(new Error("network down"))
|
|
// Second call: GET returns a matching-size collection → success.
|
|
.mockResolvedValueOnce(
|
|
jsonResponse({
|
|
result: { config: { params: { vectors: { size: 3072 } } } },
|
|
}),
|
|
);
|
|
|
|
const first = await ensureQdrantCollection(3072);
|
|
expect(first).toBe(false);
|
|
|
|
// Without the fix, the second call returns the memoised rejected promise
|
|
// and fetch is never called again. With the fix, it retries.
|
|
const second = await ensureQdrantCollection(3072);
|
|
expect(second).toBe(true);
|
|
expect(fetchMock).toHaveBeenCalledTimes(2);
|
|
});
|
|
|
|
it("recreates collection when vector size changes (DELETE + PUT)", async () => {
|
|
const fetchMock = vi
|
|
.spyOn(globalThis, "fetch")
|
|
.mockResolvedValueOnce(
|
|
jsonResponse({
|
|
result: { config: { params: { vectors: { size: 2048 } } } },
|
|
}),
|
|
) // GET old size
|
|
.mockResolvedValueOnce(jsonResponse({ result: true })) // DELETE
|
|
.mockResolvedValueOnce(jsonResponse({ result: true })); // PUT
|
|
|
|
const ok = await ensureQdrantCollection(3072);
|
|
expect(ok).toBe(true);
|
|
|
|
const methods = fetchMock.mock.calls.map(
|
|
(c) => (c[1] as RequestInit).method,
|
|
);
|
|
expect(methods).toEqual(["GET", "DELETE", "PUT"]);
|
|
});
|
|
|
|
it("is idempotent when the collection already matches", async () => {
|
|
const fetchMock = vi.spyOn(globalThis, "fetch").mockResolvedValueOnce(
|
|
jsonResponse({
|
|
result: { config: { params: { vectors: { size: 3072 } } } },
|
|
}),
|
|
);
|
|
|
|
const ok = await ensureQdrantCollection(3072);
|
|
expect(ok).toBe(true);
|
|
expect(fetchMock).toHaveBeenCalledTimes(1);
|
|
});
|
|
});
|