diff --git a/src/app/(dashboard)/dashboard/memory/components/QdrantConfigCard.tsx b/src/app/(dashboard)/dashboard/memory/components/QdrantConfigCard.tsx index 5fc31527fd..b0a83dc92a 100644 --- a/src/app/(dashboard)/dashboard/memory/components/QdrantConfigCard.tsx +++ b/src/app/(dashboard)/dashboard/memory/components/QdrantConfigCard.tsx @@ -123,6 +123,16 @@ export default function QdrantConfigCard() { } }, []); + // Auto-check on mount once settings load: without this the status badge + // renders red after a page refresh because `health` starts as null and the + // old code treated "not checked yet" the same as "failed". The Test + // connection button still drives the same check manually. + useEffect(() => { + if (!loading && qdrant.enabled && health === null) { + void checkHealth(); + } + }, [loading, qdrant.enabled, health, checkHealth]); + const runSearch = useCallback(async () => { const q = searchQuery.trim(); if (!q) return; @@ -185,18 +195,32 @@ export default function QdrantConfigCard() { {qdrant.enabled - ? health?.ok - ? t("qdrant.statusActive") - : t("qdrant.statusError") + ? health === null + ? t("qdrant.testing") + : health.ok + ? t("qdrant.statusActive") + : t("qdrant.statusError") : t("qdrant.statusDisabled")} diff --git a/tests/unit/ui/qdrant-config-card.test.tsx b/tests/unit/ui/qdrant-config-card.test.tsx index 2f61d68a83..7ac859d514 100644 --- a/tests/unit/ui/qdrant-config-card.test.tsx +++ b/tests/unit/ui/qdrant-config-card.test.tsx @@ -37,8 +37,9 @@ function makeContainer(): HTMLElement { describe("QdrantConfigCard", () => { beforeEach(() => { - (globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = - true; + ( + globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean } + ).IS_REACT_ACT_ENVIRONMENT = true; globalThis.fetch = vi.fn().mockImplementation((url: string) => { if (url === "/api/settings/qdrant") { return Promise.resolve({ @@ -68,9 +69,8 @@ describe("QdrantConfigCard", () => { }); it("renders after loading qdrant settings", async () => { - const { default: QdrantConfigCard } = await import( - "../../../src/app/(dashboard)/dashboard/memory/components/QdrantConfigCard" - ); + const { default: QdrantConfigCard } = + await import("../../../src/app/(dashboard)/dashboard/memory/components/QdrantConfigCard"); const container = makeContainer(); const root = createRoot(container); await act(async () => { @@ -110,9 +110,8 @@ describe("QdrantConfigCard", () => { }); globalThis.fetch = fetchMock; - const { default: QdrantConfigCard } = await import( - "../../../src/app/(dashboard)/dashboard/memory/components/QdrantConfigCard" - ); + const { default: QdrantConfigCard } = + await import("../../../src/app/(dashboard)/dashboard/memory/components/QdrantConfigCard"); const container = makeContainer(); const root = createRoot(container); await act(async () => { @@ -123,7 +122,7 @@ describe("QdrantConfigCard", () => { }); const toggleBtn = container.querySelector( - "[data-testid='qdrant-enabled-switch']", + "[data-testid='qdrant-enabled-switch']" ) as HTMLButtonElement | null; expect(toggleBtn).toBeTruthy(); await act(async () => { @@ -135,9 +134,7 @@ describe("QdrantConfigCard", () => { const putCalls = fetchMock.mock.calls.filter( (c: [string, { method?: string }]) => - typeof c[0] === "string" && - c[0] === "/api/settings/qdrant" && - c[1]?.method === "PUT", + typeof c[0] === "string" && c[0] === "/api/settings/qdrant" && c[1]?.method === "PUT" ); expect(putCalls.length).toBeGreaterThan(0); }); @@ -166,9 +163,8 @@ describe("QdrantConfigCard", () => { }); globalThis.fetch = fetchMock; - const { default: QdrantConfigCard } = await import( - "../../../src/app/(dashboard)/dashboard/memory/components/QdrantConfigCard" - ); + const { default: QdrantConfigCard } = + await import("../../../src/app/(dashboard)/dashboard/memory/components/QdrantConfigCard"); const container = makeContainer(); const root = createRoot(container); await act(async () => { @@ -179,7 +175,7 @@ describe("QdrantConfigCard", () => { }); const testBtn = container.querySelector( - "[data-testid='qdrant-test-connection']", + "[data-testid='qdrant-test-connection']" ) as HTMLButtonElement | null; expect(testBtn).toBeTruthy(); await act(async () => { @@ -190,7 +186,7 @@ describe("QdrantConfigCard", () => { }); const healthCalls = fetchMock.mock.calls.filter( - (c: [string]) => typeof c[0] === "string" && c[0] === "/api/settings/qdrant/health", + (c: [string]) => typeof c[0] === "string" && c[0] === "/api/settings/qdrant/health" ); expect(healthCalls.length).toBeGreaterThan(0); // Health OK result should be shown @@ -198,37 +194,36 @@ describe("QdrantConfigCard", () => { }); it("search test button calls /api/settings/qdrant/search and renders results", async () => { - const fetchMock = vi.fn().mockImplementation((url: string, opts?: { method?: string; body?: string }) => { - if (url === "/api/settings/qdrant") { - return Promise.resolve({ - ok: true, - json: async () => MOCK_QDRANT_SETTINGS, - }); - } - if (url === "/api/settings/qdrant/embedding-models") { - return Promise.resolve({ - ok: true, - json: async () => ({ models: [] }), - }); - } - if (url === "/api/settings/qdrant/search" && opts?.method === "POST") { - return Promise.resolve({ - ok: true, - json: async () => ({ + const fetchMock = vi + .fn() + .mockImplementation((url: string, opts?: { method?: string; body?: string }) => { + if (url === "/api/settings/qdrant") { + return Promise.resolve({ ok: true, - results: [ - { id: "r1", score: 0.9876, payload: { content: "test content" } }, - ], - }), - }); - } - return Promise.resolve({ ok: true, json: async () => ({}) }); - }); + json: async () => MOCK_QDRANT_SETTINGS, + }); + } + if (url === "/api/settings/qdrant/embedding-models") { + return Promise.resolve({ + ok: true, + json: async () => ({ models: [] }), + }); + } + if (url === "/api/settings/qdrant/search" && opts?.method === "POST") { + return Promise.resolve({ + ok: true, + json: async () => ({ + ok: true, + results: [{ id: "r1", score: 0.9876, payload: { content: "test content" } }], + }), + }); + } + return Promise.resolve({ ok: true, json: async () => ({}) }); + }); globalThis.fetch = fetchMock; - const { default: QdrantConfigCard } = await import( - "../../../src/app/(dashboard)/dashboard/memory/components/QdrantConfigCard" - ); + const { default: QdrantConfigCard } = + await import("../../../src/app/(dashboard)/dashboard/memory/components/QdrantConfigCard"); const container = makeContainer(); const root = createRoot(container); await act(async () => { @@ -240,7 +235,7 @@ describe("QdrantConfigCard", () => { // Set search query by manipulating the input const searchInputs = Array.from(container.querySelectorAll("input")).filter( - (i) => i.type !== "password" && i.type !== "number", + (i) => i.type !== "password" && i.type !== "number" ); // The search query input is the one with the search placeholder const searchInput = searchInputs[searchInputs.length - 1] as HTMLInputElement | null; @@ -250,7 +245,7 @@ describe("QdrantConfigCard", () => { if (searchInput) { const nativeSetter = Object.getOwnPropertyDescriptor( window.HTMLInputElement.prototype, - "value", + "value" )?.set; nativeSetter?.call(searchInput, "test query"); searchInput.dispatchEvent(new Event("change", { bubbles: true })); @@ -258,7 +253,7 @@ describe("QdrantConfigCard", () => { }); const searchTestBtn = container.querySelector( - "[data-testid='qdrant-search-test']", + "[data-testid='qdrant-search-test']" ) as HTMLButtonElement | null; expect(searchTestBtn).toBeTruthy(); await act(async () => { @@ -272,7 +267,7 @@ describe("QdrantConfigCard", () => { (c: [string, { method?: string }]) => typeof c[0] === "string" && c[0] === "/api/settings/qdrant/search" && - c[1]?.method === "POST", + c[1]?.method === "POST" ); expect(searchCalls.length).toBeGreaterThan(0); }); @@ -301,9 +296,8 @@ describe("QdrantConfigCard", () => { }); globalThis.fetch = fetchMock; - const { default: QdrantConfigCard } = await import( - "../../../src/app/(dashboard)/dashboard/memory/components/QdrantConfigCard" - ); + const { default: QdrantConfigCard } = + await import("../../../src/app/(dashboard)/dashboard/memory/components/QdrantConfigCard"); const container = makeContainer(); const root = createRoot(container); await act(async () => { @@ -314,7 +308,7 @@ describe("QdrantConfigCard", () => { }); const cleanupBtn = container.querySelector( - "[data-testid='qdrant-cleanup']", + "[data-testid='qdrant-cleanup']" ) as HTMLButtonElement | null; expect(cleanupBtn).toBeTruthy(); await act(async () => { @@ -328,10 +322,58 @@ describe("QdrantConfigCard", () => { (c: [string, { method?: string }]) => typeof c[0] === "string" && c[0] === "/api/settings/qdrant/cleanup" && - c[1]?.method === "POST", + c[1]?.method === "POST" ); expect(cleanupCalls.length).toBeGreaterThan(0); // Shows cleanup success message expect(container.textContent).toContain("qdrant.cleanupSuccess"); }); + + it("auto-checks health on mount when enabled (no red error after refresh)", async () => { + const fetchMock = vi.fn().mockImplementation((url: string) => { + if (url === "/api/settings/qdrant") { + return Promise.resolve({ + ok: true, + json: async () => ({ ...MOCK_QDRANT_SETTINGS, enabled: true }), + }); + } + if (url === "/api/settings/qdrant/embedding-models") { + return Promise.resolve({ + ok: true, + json: async () => ({ models: [] }), + }); + } + if (url === "/api/settings/qdrant/health") { + return Promise.resolve({ + ok: true, + json: async () => ({ ok: true, latencyMs: 2 }), + }); + } + return Promise.resolve({ ok: true, json: async () => ({}) }); + }); + globalThis.fetch = fetchMock; + + const { default: QdrantConfigCard } = + await import("../../../src/app/(dashboard)/dashboard/memory/components/QdrantConfigCard"); + const container = makeContainer(); + const root = createRoot(container); + await act(async () => { + root.render(); + }); + await act(async () => { + await new Promise((r) => setTimeout(r, 50)); + }); + + // The health endpoint must be hit automatically on mount — no manual + // "Test connection" click required. Regression: the badge used to render + // red after a page refresh because health started as null and the mount + // effect never checked it. + const healthCalls = fetchMock.mock.calls.filter( + (c: [string]) => typeof c[0] === "string" && c[0] === "/api/settings/qdrant/health" + ); + expect(healthCalls.length).toBeGreaterThan(0); + // Badge shows the real healthy state, not a red error. + expect(container.textContent).toContain("qdrant.statusActive"); + expect(container.textContent).not.toContain("qdrant.statusError"); + }); });