/** * L14 / lot C2 — embedder settings feature wired to the stateful * `MockEmbedderGateway` via the real `DIProvider` (same harness as * `memory.test.tsx`). * * Covers: * - the strategy list renders on an equal footing (None, ONNX, Ollama, API, * Custom) with the "Recommended" badge on ONNX * - conditional fields per strategy (ONNX model picker + auto dimension; * localServer endpoint; api endpoint + apiKeyEnv label; none → no fields) * - a strategy whose build flag is false is shown disabled with * "not available in this build" (None stays available) * - Save calls the gateway with the right profile * - "Back to None" deletes the configured profiles * * Plus the active-tier transparency line in MemoryPanel (Naïve vs Vector). */ import { describe, it, expect, vi } from "vitest"; import { render, screen, waitFor, fireEvent } from "@testing-library/react"; import { MockEmbedderGateway, MockMemoryGateway } from "@/adapters/mock"; import type { EmbedderEngines, EmbedderProfile } from "@/domain"; import type { Gateways } from "@/ports"; import { DIProvider } from "@/app/di"; import { MemoryPanel } from "@/features/memory"; import { EmbedderSettings } from "./EmbedderSettings"; function renderEmbedder( engines?: Partial, seed: EmbedderProfile[] = [], ) { const embedder = new MockEmbedderGateway(engines, seed); const gateways = { embedder } as unknown as Gateways; return { embedder, ...render( , ), }; } async function waitForEmbedderIdle() { await waitFor(() => { expect( (screen.getByRole("button", { name: "back to none" }) as HTMLButtonElement) .disabled, ).toBe(false); }); } describe("EmbedderSettings (with MockEmbedderGateway)", () => { it("renders the strategy list on an equal footing with a Recommended badge", async () => { renderEmbedder(); await waitForEmbedderIdle(); // Every strategy is offered. expect(screen.getByTestId("embedder-strategy-none")).toBeTruthy(); expect(screen.getByTestId("embedder-strategy-localOnnx")).toBeTruthy(); expect(screen.getByTestId("embedder-strategy-localServer")).toBeTruthy(); expect(screen.getByTestId("embedder-strategy-api")).toBeTruthy(); expect(screen.getByTestId("embedder-strategy-custom")).toBeTruthy(); // ONNX carries the "Recommended" badge. expect(screen.getByText("Recommended")).toBeTruthy(); // The change-takes-effect notice is visible. expect(screen.getByTestId("embedder-restart-note").textContent).toMatch( /next app start/i, ); }); it("pre-selects ONNX with the recommended model and auto-fills its dimension (384)", async () => { renderEmbedder(); await waitForEmbedderIdle(); // ONNX is the default cursor → its model picker is shown. const modelSelect = (await screen.findByLabelText( "onnx model", )) as HTMLSelectElement; expect(modelSelect.value).toBe("e5-small"); // Dimension auto-filled from the model. expect((screen.getByLabelText("dimension") as HTMLInputElement).value).toBe( "384", ); }); it("shows endpoint for localServer and endpoint + apiKeyEnv (named, not the key) for api", async () => { renderEmbedder(); await waitForEmbedderIdle(); // localServer → endpoint, no apiKeyEnv. fireEvent.click( screen.getByRole("radio", { name: "Ollama (local server)" }), ); await waitFor(() => expect(screen.getByLabelText("endpoint")).toBeTruthy()); expect(screen.queryByLabelText("api key env var")).toBeNull(); // api → endpoint + apiKeyEnv with an explicit "env var" label. fireEvent.click(screen.getByRole("radio", { name: "API" })); await waitFor(() => expect(screen.getByLabelText("api key env var")).toBeTruthy(), ); expect(screen.getByText(/environment variable/i)).toBeTruthy(); expect(screen.getByText(/never the key itself/i)).toBeTruthy(); }); it("renders no conditional fields for None", async () => { renderEmbedder(); await waitForEmbedderIdle(); fireEvent.click(screen.getByRole("radio", { name: "None (naïve recall)" })); await waitFor(() => expect(screen.queryByLabelText("dimension")).toBeNull(), ); expect(screen.queryByLabelText("onnx model")).toBeNull(); // No Save button in None mode (only "Back to None"). expect(screen.queryByRole("button", { name: "save embedder" })).toBeNull(); }); it("disables strategies whose build flag is false with a 'not available' note", async () => { renderEmbedder({ vectorOnnxEnabled: false, vectorHttpEnabled: false }); await waitForEmbedderIdle(); const onnxRadio = screen.getByRole("radio", { name: "Local ONNX", }) as HTMLInputElement; const ollamaRadio = screen.getByRole("radio", { name: "Ollama (local server)", }) as HTMLInputElement; const apiRadio = screen.getByRole("radio", { name: "API" }) as HTMLInputElement; const noneRadio = screen.getByRole("radio", { name: "None (naïve recall)", }) as HTMLInputElement; expect(onnxRadio.disabled).toBe(true); expect(ollamaRadio.disabled).toBe(true); expect(apiRadio.disabled).toBe(true); // None is always available. expect(noneRadio.disabled).toBe(false); // The honesty note appears. expect( screen.getAllByText("not available in this build").length, ).toBeGreaterThan(0); }); it("Save sends the recommended ONNX profile (e5-small, dim 384)", async () => { const { embedder } = renderEmbedder(); const spy = vi.spyOn(embedder, "saveEmbedderProfile"); await waitForEmbedderIdle(); fireEvent.click(screen.getByRole("button", { name: "save embedder" })); await waitFor(() => expect(spy).toHaveBeenCalledTimes(1)); const saved = spy.mock.calls[0][0]; expect(saved.strategy).toBe("localOnnx"); expect(saved.model).toBe("e5-small"); expect(saved.dimension).toBe(384); expect(saved.id.length).toBeGreaterThan(0); expect(saved.name.length).toBeGreaterThan(0); // ONNX never carries an endpoint/apiKeyEnv. expect(saved.endpoint).toBeUndefined(); expect(saved.apiKeyEnv).toBeUndefined(); }); it("Save for api sends endpoint + apiKeyEnv (the env var NAME)", async () => { const { embedder } = renderEmbedder(); const spy = vi.spyOn(embedder, "saveEmbedderProfile"); await waitForEmbedderIdle(); fireEvent.click(screen.getByRole("radio", { name: "API" })); await waitFor(() => expect(screen.getByLabelText("api key env var")).toBeTruthy(), ); fireEvent.change(screen.getByLabelText("api key env var"), { target: { value: "MY_KEY_VAR" }, }); fireEvent.click(screen.getByRole("button", { name: "save embedder" })); await waitFor(() => expect(spy).toHaveBeenCalled()); const saved = spy.mock.calls.at(-1)![0]; expect(saved.strategy).toBe("api"); expect(saved.apiKeyEnv).toBe("MY_KEY_VAR"); expect(saved.endpoint).toBeTruthy(); }); it("'Back to None' deletes the configured profiles", async () => { const seed: EmbedderProfile[] = [ { id: "onnx-local", name: "Local ONNX", strategy: "localOnnx", model: "e5-small", dimension: 384 }, ]; const { embedder } = renderEmbedder(undefined, seed); const spy = vi.spyOn(embedder, "deleteEmbedderProfile"); await waitForEmbedderIdle(); fireEvent.click(screen.getByRole("button", { name: "back to none" })); await waitFor(() => expect(spy).toHaveBeenCalledWith("onnx-local")); expect(await embedder.listEmbedderProfiles()).toHaveLength(0); }); }); // --------------------------------------------------------------------------- // MemoryPanel — active recall tier transparency // --------------------------------------------------------------------------- describe("MemoryPanel active tier (transparency)", () => { function renderPanel(embedder: MockEmbedderGateway) { const gateways = { memory: new MockMemoryGateway(), embedder, } as unknown as Gateways; return render( , ); } it("shows 'Naïve (None)' when no embedder is configured", async () => { renderPanel(new MockEmbedderGateway()); const tier = await screen.findByTestId("memory-tier"); await waitFor(() => expect(tier.textContent).toMatch(/Naïve \(None\)/)); }); it("shows 'Vector — ' when an embedder is configured", async () => { const embedder = new MockEmbedderGateway(undefined, [ { id: "onnx-local", name: "Local ONNX", strategy: "localOnnx", model: "e5-small", dimension: 384 }, ]); renderPanel(embedder); const tier = await screen.findByTestId("memory-tier"); await waitFor(() => expect(tier.textContent).toMatch(/Vector — localOnnx e5-small/)); }); });