From b090b601a5d0e23d518b77b56279db42ed600f11 Mon Sep 17 00:00:00 2001 From: diegosouzapw Date: Sat, 8 Aug 2026 00:08:57 -0300 Subject: [PATCH 1/5] fix(deps): bump nanoid, dompurify for 2 new Dependabot alerts (#189, #190) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps: nanoid ^3.3.17 (was transitive, now overridden), dompurify ^3.4.13 (with monaco-editor scoped override). Closes Dependabot #189, #190. Remaining #182-#188 (js-yaml + mermaid) already closed by #9651 merge — awaiting Dependabot re-scan. npm audit → 0 vulnerabilities. --- package-lock.json | 14 +++++++------- package.json | 7 +++++-- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/package-lock.json b/package-lock.json index 71c0d85d8fca..c061366549aa 100644 --- a/package-lock.json +++ b/package-lock.json @@ -31,7 +31,7 @@ "clsx": "^2.1.1", "commander": "^15.0.0", "csv-stringify": "^6.7.0", - "dompurify": "^3.4.12", + "dompurify": "^3.4.13", "express": "^5.2.1", "fetch-socks": "^1.3.3", "fflate": "^0.8.3", @@ -17064,9 +17064,9 @@ "license": "MIT" }, "node_modules/dompurify": { - "version": "3.4.12", - "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.12.tgz", - "integrity": "sha512-zQvGet8Z2sWbQhCmfFz/T5QWH2oBmjnqK3qvOjaqaNLrLEF912WamU+ohnTp0TCep/MFVHpdJuCZEdFOdTnEFg==", + "version": "3.4.13", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.13.tgz", + "integrity": "sha512-2vmYIoqjze2d+kakP8S/nS5shfsl587kzwEjcGlTdiksUVgFHnFCsLYDVj/JNqJVOQZGSYBTmuycv0PodwmnMQ==", "license": "(MPL-2.0 OR Apache-2.0)", "optionalDependencies": { "@types/trusted-types": "^2.0.7" @@ -27576,9 +27576,9 @@ "optional": true }, "node_modules/nanoid": { - "version": "3.3.16", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz", - "integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==", + "version": "3.3.18", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz", + "integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==", "funding": [ { "type": "github", diff --git a/package.json b/package.json index a9904548e035..6709d2734799 100644 --- a/package.json +++ b/package.json @@ -256,7 +256,7 @@ "clsx": "^2.1.1", "commander": "^15.0.0", "csv-stringify": "^6.7.0", - "dompurify": "^3.4.12", + "dompurify": "^3.4.13", "express": "^5.2.1", "fetch-socks": "^1.3.3", "fflate": "^0.8.3", @@ -395,7 +395,6 @@ ] }, "overrides": { - "dompurify": "^3.4.12", "fast-xml-parser": "^5.10.1", "sharp": "^0.35.0", "postcss": "^8.5.18", @@ -454,6 +453,10 @@ }, "xmlbuilder2": { "js-yaml": "^4.3.1" + }, + "nanoid": "^3.3.17", + "monaco-editor": { + "dompurify": "^3.4.13" } } } From 918fba5e392ce8b137976349f035597196edc440 Mon Sep 17 00:00:00 2001 From: diegosouzapw Date: Sat, 8 Aug 2026 01:18:49 -0300 Subject: [PATCH 2/5] fix(repo): harden .gitignore to also ignore a _tasks symlink (/_tasks) _tasks is a SEPARATE nested git repo (gitignored). The pattern _tasks/ (trailing slash) ignores only a directory, not a SYMLINK named _tasks. A self-referential _tasks symlink can slip in via git add -A and, once pulled, checkout materializes it over the real _tasks repo (destroying plans/specs/hands-off). Anchored /_tasks ignores the symlink too, preventing re-capture. --- .gitignore | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.gitignore b/.gitignore index 4a0b192e9cb6..f2738f3aa70b 100644 --- a/.gitignore +++ b/.gitignore @@ -250,3 +250,8 @@ tests/homolog/.auth/ tests/homolog/ui/.auth/ homolog-report/ docker-compose.yml.bak + +# _tasks e um repo git SEPARADO (ver AGENTS.md). A linha _tasks/ (com barra) NAO +# ignora um SYMLINK chamado _tasks; /_tasks (ancorado) cobre arquivo/symlink/dir na raiz +# e impede que um git add -A recapture o symlink (incidente 2026-08-08). +/_tasks From 514cf85fef4072c7e8d2bbd766842eb4c74ed67f Mon Sep 17 00:00:00 2001 From: rafacpti23 Date: Sun, 23 Aug 2026 02:21:44 -0300 Subject: [PATCH 3/5] docs: specify Qdrant configuration guidance --- ...23-qdrant-configuration-guidance-design.md | 64 +++++++++++++++++++ 1 file changed, 64 insertions(+) create mode 100644 docs/superpowers/specs/2026-08-23-qdrant-configuration-guidance-design.md diff --git a/docs/superpowers/specs/2026-08-23-qdrant-configuration-guidance-design.md b/docs/superpowers/specs/2026-08-23-qdrant-configuration-guidance-design.md new file mode 100644 index 000000000000..d78c52812bf7 --- /dev/null +++ b/docs/superpowers/specs/2026-08-23-qdrant-configuration-guidance-design.md @@ -0,0 +1,64 @@ +# Qdrant Configuration Guidance Design + +## Goal + +Make the Memory > Engine > Qdrant experience explain what Qdrant does, guide users through a safe configuration, and verify that the selected Qdrant collection accepts embeddings produced by the configured OmniRoute model before Qdrant is enabled. + +## Scope + +- Add a concise, localized explanation that Qdrant stores semantic-memory vectors for relevant-context retrieval. It is not a token compressor; token savings are indirect and depend on less irrelevant context being injected. +- Add a configuration checklist covering a protected Qdrant endpoint, host/port, collection, embedding provider/model, matching vector dimensions, connection test, and search test. +- Extend the authenticated Qdrant health route to inspect the configured collection without creating, updating, searching, or deleting points. Return the collection vector dimension and a clear state when the collection is absent or uses named vectors. +- Show a pre-enable compatibility result in the Qdrant card. If the endpoint is reachable but the vector dimension cannot be determined from the selected embedding model, the UI must explain that the search test is the authoritative end-to-end validation. If dimensions differ, the UI must block enabling and explain how to create a compatible collection. +- Keep the existing behavior that initial writes create a missing collection using the embedding dimension detected from the first successful embedding. + +## User Flow + +1. The user opens Dashboard > Memory > Engine and reads the purpose and prerequisites. +2. The user enters Qdrant host, port, collection, optional API key, and an embedding provider/model with a configured provider credential. +3. The user saves settings and clicks Test connection. +4. The health result reports endpoint status and, for an existing collection, its vector dimensions and named-vector configuration. +5. The user runs Test search. This generates an embedding through OmniRoute and proves that the model dimension matches the collection and that retrieval works. +6. The Enable control remains unavailable after a known incompatibility; otherwise it follows the existing setting update path, which sets `memoryVectorStore` to `qdrant`. + +## Collection Creation Guidance + +The UI will provide copyable Qdrant REST guidance, using a placeholder dimension rather than assuming one for every model: + +```json +PUT /collections/ +{ + "vectors": { "size": , "distance": "Cosine" } +} +``` + +For the audited server, the existing `omniroute_memory` collection has a named 2048-dimensional vector. It must be paired with the same 2048-dimensional embedding model that created it. The default `openai/text-embedding-3-small` emits 1536-dimensional vectors and therefore requires a separate 1536-dimensional collection. + +## API Contract + +`GET /api/settings/qdrant/health` will retain `{ ok, latencyMs, error? }` and add optional read-only metadata: + +```ts +{ + collection?: { + exists: boolean; + vectorSize?: number; + vectorName?: string | null; + }; +} +``` + +The route must never expose Qdrant API keys. It must sanitize upstream error text before returning it. + +## Error Handling + +- A disconnected endpoint remains an error result, without changing settings. +- A missing collection is guidance, not an error: OmniRoute creates it on the first successful Qdrant write. +- A known dimension mismatch blocks enabling and tells the user to choose a matching model or a separate collection. +- A model whose dimension cannot be determined does not claim compatibility; the user must run Test search. + +## Testing + +- Route tests cover health metadata for single-vector, named-vector, missing-collection, and sanitized upstream-error responses. +- Component tests cover the purpose explanation, checklist, compatible/mismatch/missing collection states, and disabled enable action on a mismatch. +- Existing Qdrant route and card tests remain green. From 680641ccc5ab66111ff8ab038b704971a18fae12 Mon Sep 17 00:00:00 2001 From: rafacpti23 Date: Sun, 23 Aug 2026 02:25:40 -0300 Subject: [PATCH 4/5] docs: plan Qdrant configuration guidance --- ...026-08-23-qdrant-configuration-guidance.md | 53 +++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 docs/superpowers/plans/2026-08-23-qdrant-configuration-guidance.md diff --git a/docs/superpowers/plans/2026-08-23-qdrant-configuration-guidance.md b/docs/superpowers/plans/2026-08-23-qdrant-configuration-guidance.md new file mode 100644 index 000000000000..d99670f92f8d --- /dev/null +++ b/docs/superpowers/plans/2026-08-23-qdrant-configuration-guidance.md @@ -0,0 +1,53 @@ +# Qdrant Configuration Guidance Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Explain Qdrant configuration and prevent activation until a real embedding-to-Qdrant search verifies the selected model and collection work together. + +**Architecture:** The health route remains read-only but exposes collection vector metadata. The card provides a localized mini tutorial and requires a successful search test before activation; that test produces an actual embedding, so it detects mismatched dimensions without guessing a model's size. + +**Tech Stack:** Next.js App Router, React, TypeScript, Zod, next-intl, Node test runner, Vitest. + +--- + +### Task 1: Read collection metadata in health checks + +**Files:** + +- Modify: `src/lib/memory/qdrant.ts` +- Modify: `tests/integration/qdrant-routes.test.ts` + +- [ ] Add a failing integration test that mocks `/readyz` and `GET /collections/omniroute_memory`, then expects `collection: { exists: true, vectorSize: 2048, vectorName: "omniao" }` from the health route. +- [ ] Run `node --import tsx/esm --test tests/integration/qdrant-routes.test.ts` and observe the expected failure because health lacks collection metadata. +- [ ] Add `getQdrantCollectionMetadata()` to `src/lib/memory/qdrant.ts`. It may only read `GET /collections/` and returns `{ exists: false }` or `{ exists: true, vectorSize, vectorName }`. It handles unnamed `vectors.size` and named-vector maps; it never returns API keys or changes Qdrant state. +- [ ] Extend `checkQdrantHealth()` to return this metadata after a successful `/readyz` probe. +- [ ] Re-run `node --import tsx/esm --test tests/integration/qdrant-routes.test.ts` and confirm it passes. + +### Task 2: Tutorial and search-validation gate + +**Files:** + +- Modify: `src/app/(dashboard)/dashboard/memory/components/QdrantConfigCard.tsx` +- Modify: `tests/unit/ui/qdrant-config-card.test.tsx` + +- [ ] Add failing component tests for a `data-testid="qdrant-setup-tutorial"` trigger, tutorial credit, disabled enable action before validation, and enabled action after a successful `/api/settings/qdrant/search` result. +- [ ] Run `npx vitest run tests/unit/ui/qdrant-config-card.test.tsx` and observe the expected failure. +- [ ] Add `tutorialOpen` and `searchValidated` state. Reset `searchValidated` when configuration is saved or search fails; set it only after `{ ok: true }` from the search endpoint. +- [ ] Disable only the transition that enables Qdrant while `searchValidated` is false; allow disabling normally. +- [ ] Render a compact modal opened from the tutorial trigger. It explains vector-memory retrieval, indirect token savings, HTTPS/API-key protection, matching dimensions, collection creation, and Save → Test connection → Test search. Add credit text through i18n: `Rafa Martins — rafacpti@gmail.com`. +- [ ] Display the health-route collection state: missing collection, unnamed vector size, or named vector plus size. +- [ ] Re-run `npx vitest run tests/unit/ui/qdrant-config-card.test.tsx` and confirm it passes. + +### Task 3: Localization and verification + +**Files:** + +- Modify: `src/i18n/messages/en.json` +- Modify: `src/i18n/messages/pt-BR.json` + +- [ ] Add matching English and Portuguese `memory.qdrant` strings for tutorial content, collection states, validation requirement, and credit. +- [ ] Format changed code with `npx prettier --write`. +- [ ] Run `node --import tsx/esm --test tests/integration/qdrant-routes.test.ts`. +- [ ] Run `npx vitest run src/lib/memory/__tests__/qdrant-wiring.test.ts tests/unit/ui/qdrant-config-card.test.tsx`. +- [ ] Run `npm run typecheck:core`. +- [ ] Commit with `feat: guide Qdrant memory configuration`, push `rafacpti23/qdrant-configuration-guidance` to `origin`, and open a draft PR to `diegosouzapw/OmniRoute`. From 3f34a2bcf366a3b61a714fb10bc22c9add01bb78 Mon Sep 17 00:00:00 2001 From: rafacpti23 Date: Sun, 23 Aug 2026 02:39:18 -0300 Subject: [PATCH 5/5] feat: guide Qdrant memory configuration --- .../memory/components/QdrantConfigCard.tsx | 111 +++++++++++++++--- src/lib/memory/qdrant.ts | 43 ++++++- tests/integration/qdrant-routes.test.ts | 62 +++++++--- tests/unit/ui/qdrant-config-card.test.tsx | 23 ++-- 4 files changed, 190 insertions(+), 49 deletions(-) diff --git a/src/app/(dashboard)/dashboard/memory/components/QdrantConfigCard.tsx b/src/app/(dashboard)/dashboard/memory/components/QdrantConfigCard.tsx index 68ab2231d113..62eee097e7be 100644 --- a/src/app/(dashboard)/dashboard/memory/components/QdrantConfigCard.tsx +++ b/src/app/(dashboard)/dashboard/memory/components/QdrantConfigCard.tsx @@ -33,9 +33,14 @@ export default function QdrantConfigCard() { const [apiKeyInput, setApiKeyInput] = useState(""); const [saving, setSaving] = useState(false); const [saveStatus, setSaveStatus] = useState<"" | "saved" | "error">(""); - const [health, setHealth] = useState<{ ok: boolean; latencyMs: number; error?: string } | null>( - null, - ); + const [health, setHealth] = useState<{ + ok: boolean; + latencyMs: number; + error?: string; + collection?: { exists: boolean; vectorSize?: number; vectorName?: string | null }; + } | null>(null); + const [searchValidated, setSearchValidated] = useState(false); + const [tutorialOpen, setTutorialOpen] = useState(false); const [checking, setChecking] = useState(false); const [searchQuery, setSearchQuery] = useState(""); const [searching, setSearching] = useState(false); @@ -69,6 +74,7 @@ export default function QdrantConfigCard() { async (updates: Partial & { apiKey?: string }) => { const prev = qdrant; const next = { ...qdrant, ...updates }; + setSearchValidated(false); setQdrant(next); setSaving(true); setSaveStatus(""); @@ -103,7 +109,7 @@ export default function QdrantConfigCard() { setSaving(false); } }, - [qdrant], + [qdrant] ); const checkHealth = useCallback(async () => { @@ -137,9 +143,13 @@ export default function QdrantConfigCard() { const data = await res.json().catch(() => null); if (res.ok && data?.ok) { setSearchResults(Array.isArray(data.results) ? data.results : []); + setSearchValidated(true); + } else { + setSearchValidated(false); } } catch { setSearchResults([]); + setSearchValidated(false); } finally { setSearching(false); } @@ -182,14 +192,18 @@ export default function QdrantConfigCard() {

{t("qdrant.title")}

{t("qdrant.description")}

+
save({ enabled: !qdrant.enabled })} - disabled={saving} + disabled={saving || (!qdrant.enabled && !searchValidated)} role="switch" aria-checked={qdrant.enabled} className={`relative w-11 h-6 rounded-full transition-colors ${ @@ -244,6 +258,13 @@ export default function QdrantConfigCard() { + {!qdrant.enabled && !searchValidated && ( +

+ Execute um teste de busca bem-sucedido antes de ativar. Ele valida o modelo de embedding e + a dimensão da coleção. +

+ )} + {health && (
)} + {health?.collection && ( +
+ {health.collection.exists ? ( + <> + Coleção compatível com vetores de dimensão{" "} + {health.collection.vectorSize} + {health.collection.vectorName ? ` (vetor: ${health.collection.vectorName})` : ""}. O + modelo configurado deve gerar a mesma dimensão. + + ) : ( + <> + A coleção ainda não existe. Ela será criada na primeira gravação com o modelo + validado. + + )} +
+ )} {saveStatus === "saved" && (
@@ -284,7 +322,10 @@ export default function QdrantConfigCard() { value={qdrant.port} type="number" onChange={(e) => - setQdrant((s) => ({ ...s, port: Math.max(1, Math.min(65535, Number(e.target.value) || 1)) })) + setQdrant((s) => ({ + ...s, + port: Math.max(1, Math.min(65535, Number(e.target.value) || 1)), + })) } placeholder="6333" className="w-full px-3 py-2 rounded-lg bg-background border border-border text-sm font-mono focus:outline-none focus:ring-1 focus:ring-emerald-500" @@ -355,9 +396,7 @@ export default function QdrantConfigCard() { )}
{cleanupMsg &&

{cleanupMsg}

}
+ {tutorialOpen && ( +
+
+
+
+

Tutorial rápido: memória com Qdrant

+

+ O Qdrant guarda vetores e metadados das memórias para recuperar contexto + relevante. Ele não comprime tokens diretamente; a economia é indireta, ao evitar + contexto sem relação com a solicitação. +

+
+ +
+
    +
  1. Proteja o servidor com HTTPS e API key antes de uso produtivo.
  2. +
  3. + Informe host, porta, coleção e um modelo no formato provider/model com credencial + configurada. +
  4. +
  5. + A dimensão da coleção precisa ser igual à dimensão produzida pelo modelo. Uma + coleção existente de 2048 dimensões não aceita embeddings de 1536 dimensões. +
  6. +
  7. Salve, teste a conexão e execute o teste de busca. Só então ative o Qdrant.
  8. +
+
{`PUT /collections/minha_memoria\n{\n  "vectors": { "size": , "distance": "Cosine" }\n}`}
+

+ Créditos: Rafa Martins — rafacpti@gmail.com +

+
+
+ )} ); } diff --git a/src/lib/memory/qdrant.ts b/src/lib/memory/qdrant.ts index 8178bfb5709e..a35c566b415b 100644 --- a/src/lib/memory/qdrant.ts +++ b/src/lib/memory/qdrant.ts @@ -72,7 +72,8 @@ export function normalizeQdrantConfig(settings: Record): Qdrant ? process.env.QDRANT_API_KEY.trim() : undefined; const envCollection = - typeof process.env.QDRANT_COLLECTION === "string" && process.env.QDRANT_COLLECTION.trim().length > 0 + typeof process.env.QDRANT_COLLECTION === "string" && + process.env.QDRANT_COLLECTION.trim().length > 0 ? process.env.QDRANT_COLLECTION.trim() : undefined; @@ -84,15 +85,19 @@ export function normalizeQdrantConfig(settings: Record): Qdrant ? Math.round(portRaw) : typeof portRaw === "string" ? Math.round(Number(portRaw) || 6333) - : envPort ?? 6333; + : (envPort ?? 6333); const apiKey = (typeof settings.qdrantApiKey === "string" && settings.qdrantApiKey.trim().length > 0 ? settings.qdrantApiKey.trim() - : null) ?? envApiKey ?? null; + : null) ?? + envApiKey ?? + null; const collection = (typeof settings.qdrantCollection === "string" && settings.qdrantCollection.trim().length > 0 ? settings.qdrantCollection.trim() - : null) ?? envCollection ?? "omniroute_memory"; + : null) ?? + envCollection ?? + "omniroute_memory"; const embeddingModel = (typeof settings.qdrantEmbeddingModel === "string" && settings.qdrantEmbeddingModel.trim().length > 0 @@ -175,10 +180,37 @@ async function qdrantFetch(cfg: QdrantConfig, path: string, init?: RequestInit): }); } +export type QdrantCollectionMetadata = + { exists: false } | { exists: true; vectorSize: number; vectorName: string | null }; + +export async function getQdrantCollectionMetadata(): Promise { + const cfg = await getQdrantConfig(); + if (!cfg.enabled || !cfg.host) return null; + + const res = await qdrantFetch(cfg, `/collections/${encodeURIComponent(cfg.collection)}`, { + method: "GET", + }); + if (res.status === 404) return { exists: false }; + if (!res.ok) return null; + + const data = (await res.json().catch(() => null)) as any; + const vectors = data?.result?.config?.params?.vectors; + if (!vectors || typeof vectors !== "object" || Array.isArray(vectors)) return null; + if (typeof vectors.size === "number") { + return { exists: true, vectorSize: vectors.size, vectorName: null }; + } + + const vectorName = Object.keys(vectors)[0]; + const vectorSize = vectorName ? vectors[vectorName]?.size : null; + if (typeof vectorSize !== "number") return null; + return { exists: true, vectorSize, vectorName }; +} + export async function checkQdrantHealth(): Promise<{ ok: boolean; latencyMs: number; error?: string; + collection?: QdrantCollectionMetadata; }> { const cfg = await getQdrantConfig(); const start = Date.now(); @@ -193,7 +225,8 @@ export async function checkQdrantHealth(): Promise<{ const text = await res.text().catch(() => ""); return { ok: false, latencyMs, error: text.slice(0, 200) || `HTTP ${res.status}` }; } - return { ok: true, latencyMs }; + const collection = await getQdrantCollectionMetadata(); + return { ok: true, latencyMs, ...(collection ? { collection } : {}) }; } catch (err) { return { ok: false, diff --git a/tests/integration/qdrant-routes.test.ts b/tests/integration/qdrant-routes.test.ts index 8028b9a94a68..38de422c19c7 100644 --- a/tests/integration/qdrant-routes.test.ts +++ b/tests/integration/qdrant-routes.test.ts @@ -31,18 +31,11 @@ const memorySettings = await import("../../src/lib/memory/settings.ts"); // ── Route imports ── const qdrantSettingsRoute = await import("../../src/app/api/settings/qdrant/route.ts"); -const qdrantHealthRoute = await import( - "../../src/app/api/settings/qdrant/health/route.ts" -); -const qdrantSearchRoute = await import( - "../../src/app/api/settings/qdrant/search/route.ts" -); -const qdrantCleanupRoute = await import( - "../../src/app/api/settings/qdrant/cleanup/route.ts" -); -const qdrantEmbeddingModelsRoute = await import( - "../../src/app/api/settings/qdrant/embedding-models/route.ts" -); +const qdrantHealthRoute = await import("../../src/app/api/settings/qdrant/health/route.ts"); +const qdrantSearchRoute = await import("../../src/app/api/settings/qdrant/search/route.ts"); +const qdrantCleanupRoute = await import("../../src/app/api/settings/qdrant/cleanup/route.ts"); +const qdrantEmbeddingModelsRoute = + await import("../../src/app/api/settings/qdrant/embedding-models/route.ts"); // ── Helpers ── @@ -55,11 +48,7 @@ async function resetStorage() { memorySettings.invalidateMemorySettingsCache(); } -async function makeAuthRequest( - method: "GET" | "POST" | "PUT", - url: string, - body?: unknown -) { +async function makeAuthRequest(method: "GET" | "POST" | "PUT", url: string, body?: unknown) { return makeManagementSessionRequest(url, { method, body }); } @@ -278,6 +267,45 @@ test("GET /api/settings/qdrant/health — returns health result shape (qdrant di assert.strictEqual(body.ok, false, "ok should be false when qdrant not configured"); }); +test("GET /api/settings/qdrant/health — reports named collection vector metadata", async () => { + await localDb.updateSettings({ + qdrantEnabled: true, + qdrantHost: "http://qdrant.test", + qdrantCollection: "omniroute_memory", + }); + const originalFetch = globalThis.fetch; + globalThis.fetch = async (url) => { + if (String(url).endsWith("/readyz")) return new Response("ready", { status: 200 }); + if (String(url).endsWith("/collections/omniroute_memory")) { + return Response.json({ + result: { + config: { + params: { + vectors: { omniao: { size: 2048, distance: "Cosine" } }, + }, + }, + }, + }); + } + return new Response("not found", { status: 404 }); + }; + + try { + const req = await makeAuthRequest("GET", "http://localhost/api/settings/qdrant/health"); + const res = await qdrantHealthRoute.GET(req as any); + const body = await res.json(); + + assert.strictEqual(res.status, 200); + assert.deepStrictEqual(body.collection, { + exists: true, + vectorSize: 2048, + vectorName: "omniao", + }); + } finally { + globalThis.fetch = originalFetch; + } +}); + test("GET /api/settings/qdrant/health — 401 without auth", async () => { await setRequireLogin(true); const req = makeUnauthRequest("GET", "http://localhost/api/settings/qdrant/health"); diff --git a/tests/unit/ui/qdrant-config-card.test.tsx b/tests/unit/ui/qdrant-config-card.test.tsx index 2f61d68a83ee..38c49d4349c9 100644 --- a/tests/unit/ui/qdrant-config-card.test.tsx +++ b/tests/unit/ui/qdrant-config-card.test.tsx @@ -86,7 +86,7 @@ describe("QdrantConfigCard", () => { expect(container.querySelector("[data-testid='qdrant-cleanup']")).toBeTruthy(); }); - it("toggle enabled switch calls PUT /api/settings/qdrant", async () => { + it("requires a search validation before enabling and exposes the setup tutorial", async () => { const fetchMock = vi.fn().mockImplementation((url: string, opts?: { method?: string }) => { if (url === "/api/settings/qdrant" && opts?.method === "PUT") { return Promise.resolve({ @@ -126,20 +126,17 @@ describe("QdrantConfigCard", () => { "[data-testid='qdrant-enabled-switch']", ) as HTMLButtonElement | null; expect(toggleBtn).toBeTruthy(); + expect(toggleBtn?.disabled).toBe(true); + + const tutorial = container.querySelector( + "[data-testid='qdrant-setup-tutorial']", + ) as HTMLButtonElement | null; + expect(tutorial).toBeTruthy(); await act(async () => { - toggleBtn?.click(); - }); - await act(async () => { - await new Promise((r) => setTimeout(r, 50)); + tutorial?.click(); }); - - const putCalls = fetchMock.mock.calls.filter( - (c: [string, { method?: string }]) => - typeof c[0] === "string" && - c[0] === "/api/settings/qdrant" && - c[1]?.method === "PUT", - ); - expect(putCalls.length).toBeGreaterThan(0); + expect(container.querySelector("[role='dialog']")).toBeTruthy(); + expect(container.textContent).toContain("Rafa Martins"); }); it("test connection button calls /api/settings/qdrant/health", async () => {