Spaces:
Runtime error
Runtime error
| /** | |
| * Tests for the Qdrant wiring contract used by createMemory. | |
| * | |
| * createMemory in store.ts performs a best-effort fire-and-forget call to | |
| * upsertSemanticMemoryPoint after every successful SQLite write. This test | |
| * pins the behaviour the wiring relies on: | |
| * 1. normalizeQdrantConfig handles the disabled / unconfigured case | |
| * (which makes upsertSemanticMemoryPoint short-circuit with | |
| * { ok: false, error: "not_configured" } instead of throwing). | |
| * 2. normalizeQdrantConfig applies the documented defaults when keys | |
| * are missing or malformed. | |
| * | |
| * These pure-logic checks avoid the need for a live DB / Qdrant server in CI. | |
| */ | |
| import { describe, test, expect, beforeAll, afterEach } from "vitest"; | |
| import { | |
| normalizeQdrantConfig, | |
| buildQuantizationConfig, | |
| searchQuantizationParams, | |
| } from "../qdrant"; | |
| describe("normalizeQdrantConfig — defaults & disabled state", () => { | |
| test("returns disabled config when settings are empty (no Qdrant configured)", () => { | |
| const cfg = normalizeQdrantConfig({}); | |
| expect(cfg.enabled).toBe(false); | |
| expect(cfg.host).toBe(""); | |
| expect(cfg.apiKey).toBeNull(); | |
| // Defaults still applied for non-toggle fields: | |
| expect(cfg.port).toBe(6333); | |
| expect(cfg.collection).toBe("omniroute_memory"); | |
| expect(cfg.embeddingModel).toBe("openai/text-embedding-3-small"); | |
| expect(cfg.vectorSize).toBe(1536); | |
| expect(cfg.hnswEfConstruct).toBe(128); | |
| }); | |
| test("disabled flag wins even when host is set", () => { | |
| const cfg = normalizeQdrantConfig({ | |
| qdrantHost: "qdrant.example.com", | |
| qdrantEnabled: false, | |
| }); | |
| expect(cfg.enabled).toBe(false); | |
| expect(cfg.host).toBe("qdrant.example.com"); | |
| }); | |
| test("enabled=true + host set yields an active config", () => { | |
| const cfg = normalizeQdrantConfig({ | |
| qdrantEnabled: true, | |
| qdrantHost: "qdrant.example.com", | |
| qdrantPort: 6334, | |
| qdrantApiKey: "secret-key", | |
| qdrantCollection: "my_memory", | |
| qdrantEmbeddingModel: "voyage/voyage-3", | |
| }); | |
| expect(cfg.enabled).toBe(true); | |
| expect(cfg.host).toBe("qdrant.example.com"); | |
| expect(cfg.port).toBe(6334); | |
| expect(cfg.apiKey).toBe("secret-key"); | |
| expect(cfg.collection).toBe("my_memory"); | |
| expect(cfg.embeddingModel).toBe("voyage/voyage-3"); | |
| }); | |
| test("string port is coerced; whitespace-only apiKey is treated as missing", () => { | |
| const cfg = normalizeQdrantConfig({ | |
| qdrantEnabled: true, | |
| qdrantHost: "host", | |
| qdrantPort: "6335", | |
| qdrantApiKey: " ", | |
| }); | |
| expect(cfg.port).toBe(6335); | |
| expect(cfg.apiKey).toBeNull(); | |
| }); | |
| test("falsy / non-true qdrantEnabled values leave the config disabled", () => { | |
| for (const value of [undefined, null, 0, "", "true", 1]) { | |
| const cfg = normalizeQdrantConfig({ | |
| qdrantEnabled: value as unknown, | |
| qdrantHost: "host", | |
| }); | |
| expect(cfg.enabled).toBe(false); | |
| } | |
| }); | |
| }); | |
| describe("Qdrant scalar quantization wiring (Q1 / F4.4)", () => { | |
| test("defaults quantization to 'none' when the setting is missing", () => { | |
| expect(normalizeQdrantConfig({}).quantization).toBe("none"); | |
| }); | |
| test("reads valid int8 / binary modes; invalid or non-string values fall back to none", () => { | |
| expect(normalizeQdrantConfig({ qdrantQuantization: "int8" }).quantization).toBe("int8"); | |
| expect(normalizeQdrantConfig({ qdrantQuantization: "binary" }).quantization).toBe("binary"); | |
| expect(normalizeQdrantConfig({ qdrantQuantization: "none" }).quantization).toBe("none"); | |
| expect(normalizeQdrantConfig({ qdrantQuantization: "bogus" }).quantization).toBe("none"); | |
| expect(normalizeQdrantConfig({ qdrantQuantization: 5 as unknown }).quantization).toBe("none"); | |
| }); | |
| test("buildQuantizationConfig: none → undefined (body unchanged), int8 → scalar, binary → binary", () => { | |
| // none must stay undefined so the create body is byte-identical to today (no behavioral change). | |
| expect(buildQuantizationConfig("none")).toBeUndefined(); | |
| expect(buildQuantizationConfig("int8")).toEqual({ | |
| scalar: { type: "int8", always_ram: true, quantile: 0.99 }, | |
| }); | |
| expect(buildQuantizationConfig("binary")).toEqual({ binary: { always_ram: true } }); | |
| }); | |
| test("searchQuantizationParams: rescore enabled only for a quantized collection", () => { | |
| expect(searchQuantizationParams("none")).toBeUndefined(); | |
| expect(searchQuantizationParams("int8")).toEqual({ quantization: { rescore: true } }); | |
| expect(searchQuantizationParams("binary")).toEqual({ quantization: { rescore: true } }); | |
| }); | |
| }); | |
| describe("normalizeQdrantConfig — env-var fallbacks (cluster profile: --profile memory)", () => { | |
| const KEYS = [ | |
| "QDRANT_HOST", | |
| "QDRANT_PORT", | |
| "QDRANT_API_KEY", | |
| "QDRANT_COLLECTION", | |
| ] as const; | |
| const savedEnv: Record<string, string | undefined> = {}; | |
| beforeAll(() => { | |
| for (const k of KEYS) savedEnv[k] = process.env[k]; | |
| }); | |
| afterEach(() => { | |
| for (const k of KEYS) { | |
| if (savedEnv[k] === undefined) delete process.env[k]; | |
| else process.env[k] = savedEnv[k]; | |
| } | |
| }); | |
| test("settings.qdrantHost takes precedence over QDRANT_HOST env", () => { | |
| process.env.QDRANT_HOST = "env-host"; | |
| const cfg = normalizeQdrantConfig({ qdrantHost: "settings-host" }); | |
| expect(cfg.host).toBe("settings-host"); | |
| }); | |
| test("QDRANT_HOST fills host when settings.qdrantHost is missing", () => { | |
| process.env.QDRANT_HOST = "qdrant"; | |
| const cfg = normalizeQdrantConfig({}); | |
| expect(cfg.host).toBe("qdrant"); | |
| }); | |
| test("QDRANT_PORT is parsed when no port in settings", () => { | |
| process.env.QDRANT_PORT = "6334"; | |
| const cfg = normalizeQdrantConfig({}); | |
| expect(cfg.port).toBe(6334); | |
| }); | |
| test("QDRANT_API_KEY is read when settings.qdrantApiKey is whitespace", () => { | |
| process.env.QDRANT_API_KEY = "secret"; | |
| const cfg = normalizeQdrantConfig({ qdrantApiKey: " " }); | |
| expect(cfg.apiKey).toBe("secret"); | |
| }); | |
| test("QDRANT_COLLECTION falls back to env when default would otherwise be used", () => { | |
| process.env.QDRANT_COLLECTION = "external-mem"; | |
| const cfg = normalizeQdrantConfig({}); | |
| expect(cfg.collection).toBe("external-mem"); | |
| }); | |
| test("all 4 env vars consumed correctly together", () => { | |
| process.env.QDRANT_HOST = "qdrant.example.com"; | |
| process.env.QDRANT_PORT = "7777"; | |
| process.env.QDRANT_API_KEY = "env-key"; | |
| process.env.QDRANT_COLLECTION = "env-coll"; | |
| process.env.QDRANT_VECTOR_SIZE = "768"; | |
| process.env.QDRANT_HNSW_EF_CONSTRUCT = "256"; | |
| const cfg = normalizeQdrantConfig({}); | |
| expect(cfg.host).toBe("qdrant.example.com"); | |
| expect(cfg.port).toBe(7777); | |
| expect(cfg.apiKey).toBe("env-key"); | |
| expect(cfg.collection).toBe("env-coll"); | |
| expect(cfg.vectorSize).toBe(768); | |
| expect(cfg.hnswEfConstruct).toBe(256); | |
| }); | |
| test("settings.qdrantVectorSize / hnswEfConstruct take precedence over env", () => { | |
| process.env.QDRANT_VECTOR_SIZE = "768"; | |
| process.env.QDRANT_HNSW_EF_CONSTRUCT = "256"; | |
| const cfg = normalizeQdrantConfig({ | |
| qdrantVectorSize: 1024, | |
| qdrantHnswEfConstruct: 200, | |
| }); | |
| expect(cfg.vectorSize).toBe(1024); | |
| expect(cfg.hnswEfConstruct).toBe(200); | |
| }); | |
| test("invalid QDRANT_VECTOR_SIZE / HNSW_EF_CONSTRUCT fall back to defaults", () => { | |
| process.env.QDRANT_VECTOR_SIZE = "not-a-number"; | |
| process.env.QDRANT_HNSW_EF_CONSTRUCT = "0"; | |
| const cfg = normalizeQdrantConfig({}); | |
| expect(cfg.vectorSize).toBe(1536); | |
| expect(cfg.hnswEfConstruct).toBe(128); | |
| }); | |
| }); | |