import { afterAll, afterEach, beforeAll, beforeEach, expect, mock, test } from "bun:test"; import { GlobalRegistrator } from "@happy-dom/global-registrator"; import { act, useState } from "react"; import { createRoot, type Root } from "react-dom/client"; import { OpenGeniApiError } from "@opengeni/sdk"; import type { AgentLearningContext, AgentLearningOverrides, SaveAgentLearningSettingsRequest, } from "@opengeni/sdk"; const defaults: AgentLearningOverrides = { knowledge: "automatic", instructions: "review_first", skills: "off", }; const getSettings = mock( async (_workspace: string, scope: string, source?: AgentLearningContext) => ({ ownerKey: scope, contextKey: source ? `${source.kind}:${source.id}` : "default", version: 1, settings: source ? {} : defaults, }), ); const saveSettings = mock(async (_workspace: string, input: SaveAgentLearningSettingsRequest) => ({ ownerKey: input.scope, contextKey: "chat:test", version: 2, settings: Object.fromEntries( Object.entries(input.settings).filter(([, mode]) => mode !== "inherit"), ), })); const context = { client: { getAgentLearningSettings: getSettings, saveAgentLearningSettings: saveSettings }, captureWorkspaceInvocation: () => ({}), ownsWorkspaceInvocation: () => true, }; mock.module("../../app-context", () => ({ useAppContext: () => context })); const { AgentLearningSettingsEditor, AgentLearningDraftEditor } = await import("./agent-learning-settings"); const { apiErrorAdvice } = await import("../../../ui/api-error"); let container: HTMLDivElement; let root: Root; beforeAll(() => { GlobalRegistrator.register(); ( globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT: boolean } ).IS_REACT_ACT_ENVIRONMENT = true; }); beforeEach(() => { getSettings.mockClear(); saveSettings.mockClear(); container = document.createElement("div"); document.body.append(container); root = createRoot(container); }); afterEach(async () => { await act(async () => root.unmount()); container.remove(); }); afterAll(() => { mock.restore(); GlobalRegistrator.unregister(); }); async function render(canEdit = true) { await act(async () => root.render( , ), ); } function field(label: string) { const node = [...container.querySelectorAll("label")].find( (candidate) => candidate.textContent === label, ); if (!node) throw new Error(`Missing ${label}`); return document.getElementById(node.htmlFor) as HTMLSelectElement; } async function change(select: HTMLSelectElement, value: string) { await act(async () => { select.value = value; select.dispatchEvent(new Event("change", { bubbles: true })); }); } test("compact settings use the one vocabulary and say when a value is the default", async () => { await render(); const select = field("Knowledge"); expect(select.value).toBe("inherit"); expect(select.parentElement?.querySelector("[aria-hidden]")?.textContent).toBe( "Default (Automatic)", ); expect([...select.options].map((option) => option.textContent?.trim())).toEqual([ "Default (Automatic)", "Automatic", "Review first", "Off", ]); expect(field("Instructions").parentElement?.querySelector("[aria-hidden]")?.textContent).toBe( "Default (Review first)", ); expect(field("Skills").parentElement?.querySelector("[aria-hidden]")?.textContent).toBe( "Default (Off)", ); expect(container.textContent).not.toContain("Retained sources"); expect(container.textContent).not.toContain("Automatic saves become"); expect(container.textContent).toContain( "Off stops agent changes. Agents still use what's already there.", ); }); test("missing category defaults use automatic without persisting a choice", async () => { const empty = async () => ({ ownerKey: "workspace", contextKey: "defaults", version: 0, settings: {}, }); getSettings.mockImplementationOnce(empty).mockImplementationOnce(empty); await render(); for (const label of ["Knowledge", "Instructions", "Skills"]) { expect(field(label).value).toBe("inherit"); expect(field(label).parentElement?.querySelector("[aria-hidden]")?.textContent).toBe( "Default (Automatic)", ); } expect(saveSettings).not.toHaveBeenCalled(); }); test("compact override and reset keep the sparse API semantics", async () => { await render(); await change(field("Knowledge"), "off"); expect(saveSettings.mock.calls[0]?.[1]).toMatchObject({ source: { kind: "chat", id: "test" }, settings: { knowledge: "off" }, expectedVersion: 1, }); await change(field("Knowledge"), "inherit"); expect(saveSettings.mock.calls[1]?.[1]).toMatchObject({ settings: { knowledge: "inherit" }, expectedVersion: 2, }); expect(field("Knowledge").value).toBe("inherit"); expect(field("Knowledge").parentElement?.querySelector("[aria-hidden]")?.textContent).toBe( "Default (Automatic)", ); }); test("read-only compact settings cannot save even if a change event is dispatched", async () => { await render(false); expect(container.querySelector("fieldset")?.disabled).toBe(true); await change(field("Knowledge"), "off"); expect(saveSettings).not.toHaveBeenCalled(); }); test("new-chat compact draft does not write settings and removes only the reset override", async () => { let current: AgentLearningOverrides = { skills: "automatic" }; function Draft() { const [value, setValue] = useState(current); return ( { current = next; setValue(next); }} /> ); } await act(async () => root.render()); await change(field("Knowledge"), "off"); expect(current).toEqual({ skills: "automatic", knowledge: "off" }); await change(field("Knowledge"), "inherit"); expect(current).toEqual({ skills: "automatic" }); expect(saveSettings).not.toHaveBeenCalled(); }); /** Opens the editor again on a fresh root, so it starts from the cached rows. */ async function reopen() { await render(); await act(async () => root.unmount()); root = createRoot(container); } test("a failed refresh over cached rows says it couldn't refresh, with Try again", async () => { await reopen(); const failure = new OpenGeniApiError(503, ""); getSettings.mockImplementationOnce(async () => { throw failure; }); await render(); // The cached rows stay. expect(field("Knowledge").value).toBe("inherit"); expect(container.textContent).toContain("Couldn't refresh Agent learning."); expect(container.textContent).toContain(apiErrorAdvice(failure)); expect(container.textContent).not.toContain("Couldn't save that."); const retry = [...container.querySelectorAll("button")].find( (each) => each.textContent === "Try again", ); const reads = getSettings.mock.calls.length; await act(async () => retry!.click()); expect(getSettings.mock.calls.length).toBeGreaterThan(reads); expect(container.textContent).not.toContain("Couldn't refresh Agent learning."); }); test("cached rows can't be saved until the refresh has read the current version", async () => { await reopen(); let release!: () => void; const gate = new Promise((resolve) => { release = resolve; }); const slow = async (_workspace: string, scope: string, source?: AgentLearningContext) => { await gate; return { ownerKey: scope, contextKey: source ? `${source.kind}:${source.id}` : "default", version: 5, settings: source ? {} : defaults, }; }; getSettings.mockImplementationOnce(slow).mockImplementationOnce(slow); await render(); expect(container.querySelector("fieldset")?.disabled).toBe(true); await change(field("Knowledge"), "off"); expect(saveSettings).not.toHaveBeenCalled(); await act(async () => release()); expect(container.querySelector("fieldset")?.disabled).toBe(false); await change(field("Knowledge"), "off"); expect(saveSettings.mock.calls[0]?.[1]).toMatchObject({ expectedVersion: 5 }); });