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 });
});