import { assertEquals, assertExists, assertStringIncludes } from "@std/assert"; import { withBerry } from "./testing.ts"; import { fill } from "@berry/util/dom.ts"; import { until } from "@berry/util"; const initial = { userName: "Charlotte", generalGuidelines: "Ask $USER first.", }; Deno.test("available providers start collapsed, expand for search, and remember manual expansion", async () => { await withBerry(async ({ scope, server, settle }) => { const available = [ { provider: "alpha", id: "a1", name: "Alpha One", input: ["text"], reasoning: false, thinkingLevels: ["off"], contextWindow: 1000, cost: { input: 0, output: 0 }, }, { provider: "beta", id: "b1", name: "Beta One", input: ["text"], reasoning: false, thinkingLevels: ["off"], contextWindow: 1000, cost: { input: 0, output: 0 }, }, ]; server.respond(({ path }) => path === "/api/models" ? available : []); const { app } = await import("./app.tsx"); document.body.append(app(scope)); server.snapshot("settings", { models: [{ provider: "alpha", modelId: "a1" }], systemPrompt: initial, }); server.snapshot("extensions", []); await settle(); const groups = [ ...document.querySelectorAll(".model-list details.provider"), ]; assertEquals(groups.length, 2); assertEquals( groups.map((group) => group.open), [false, false], ); groups[0].open = true; await settle(); server.snapshot("settings", { models: [], systemPrompt: initial }); await settle(); assertEquals(document.querySelector(".model-list details")!.open, true); const filter = document.querySelector(".settings input.filter")!; filter.focus(); fill(filter, "beta"); await settle(); const shown = [...document.querySelectorAll(".model-list details.provider")]; assertEquals(shown.length, 1); assertEquals((shown[0] as HTMLDetailsElement).open, true); assertStringIncludes(shown[0].textContent, "Beta One"); fill(filter, ""); await settle(); assertEquals( [...document.querySelectorAll(".model-list details")].map((group) => group.open ), [true, false], ); }, { url: "http://berry.test/#/settings" }); }); Deno.test("global prompt drafts survive remote updates, save only changed fields, and recover from failure", async () => { await withBerry(async ({ scope, server, settle }) => { server.respond(() => []); const { app } = await import("./app.tsx"); document.body.append(app(scope)); server.snapshot("settings", { models: [], systemPrompt: initial }); server.snapshot("extensions", []); await settle(); const editor = document.querySelector(".system-prompt"); assertExists(editor); const name = editor.querySelector("input")!; const general = editor.querySelector("textarea")!; assertEquals([name.value, general.value], Object.values(initial)); general.focus(); fill(general, ""); const remote = { ...initial, userName: "Sam" }; server.snapshot("settings", { models: [], systemPrompt: remote }); await settle(); assertEquals([name.value, general.value], ["Sam", ""]); server.respond(() => new Response(JSON.stringify({ error: "disk full" }), { status: 500 })); general.blur(); await settle(); assertEquals(general.value, ""); assertEquals(editor.querySelector('[role="status"]')!.textContent, "disk full"); assertEquals(editor.disabled, false); server.respond(() => ({ models: [], systemPrompt: { ...remote, generalGuidelines: "" } })); general.focus(); general.blur(); await settle(); assertEquals(server.changes().at(-1), { method: "PATCH", path: "/api/settings", body: { systemPrompt: { generalGuidelines: "" } }, }); server.snapshot("settings", { models: [], systemPrompt: { ...remote, generalGuidelines: "New global rule." }, }); await settle(); assertEquals(general.value, "New global rule."); }, { url: "http://berry.test/#/settings" }); }); Deno.test("blur saves are queued without blocking editing or overwriting newer drafts", async () => { await withBerry(async ({ scope, server, settle }) => { server.respond(() => []); const { app } = await import("./app.tsx"); document.body.append(app(scope)); server.snapshot("settings", { models: [], systemPrompt: initial }); server.snapshot("extensions", []); await settle(); const editor = document.querySelector(".system-prompt")!; const name = editor.querySelector("input")!; const general = editor.querySelector("textarea")!; const entered = Promise.withResolvers(); const first = Promise.withResolvers(); let saved = { ...initial }; server.respond(({ method, body }) => { if (method !== "PATCH") return []; if (server.changes().length === 1) { entered.resolve(); return first.promise; } saved = { ...saved, ...(body as { systemPrompt: Partial }).systemPrompt }; return { systemPrompt: saved }; }); general.focus(); fill(general, "First edit"); assertEquals(server.changes().length, 0); name.focus(); await entered.promise; assertEquals(editor.disabled, false); general.focus(); fill(general, "Second edit"); name.focus(); fill(name, "Alex"); assertEquals(server.changes().length, 1); saved = { ...saved, generalGuidelines: "First edit" }; server.snapshot("settings", { models: [], systemPrompt: saved }); first.resolve({ systemPrompt: saved }); await until(() => server.changes().length === 2); await settle(); assertEquals(general.value, "Second edit"); assertEquals(name.value, "Alex"); assertEquals(server.changes().map(({ body }) => body), [ { systemPrompt: { generalGuidelines: "First edit" } }, { systemPrompt: { generalGuidelines: "Second edit" } }, ]); name.blur(); await settle(); assertEquals(server.changes().at(-1)?.body, { systemPrompt: { userName: "Alex" } }); const count = server.changes().length; general.focus(); general.blur(); await settle(); assertEquals(server.changes().length, count); }, { url: "http://berry.test/#/settings" }); }); Deno.test("extension prose saves per extension and keeps dirty drafts across remote updates", async () => { await withBerry(async ({ scope, server, settle }) => { server.respond(() => []); const { app } = await import("./app.tsx"); document.body.append(app(scope)); const extensions = [{ name: "code", directory: "/ext/code", description: "edit files", status: "loaded", error: null, running: true, builtin: false, alwaysOn: false, defaultEnabled: false, web: null, formerly: [], configuration: null, promptProse: { default: "Default prose." }, startup: null, }]; server.snapshot("extensions", extensions); await settle(); const row = document.querySelector(".extension")!; const area = row.querySelector("textarea")!; const fieldset = row.querySelector("fieldset")!; assertExists(area); assertEquals(fieldset.disabled, true); assertEquals(area.value, ""); server.snapshot("settings", { models: [], systemPrompt: { ...initial, promptProse: {} } }); await settle(); assertEquals(fieldset.disabled, false); assertEquals(area.value, "Default prose."); area.focus(); fill(area, "Edited prose."); server.snapshot("settings", { models: [], systemPrompt: { ...initial, userName: "Sam", promptProse: {} }, }); await settle(); assertEquals(area.value, "Edited prose."); server.respond(() => ({ systemPrompt: { ...initial, promptProse: { code: "Edited prose." } }, })); area.blur(); await settle(); assertEquals(server.changes().at(-1), { method: "PATCH", path: "/api/settings", body: { systemPrompt: { promptProse: { code: "Edited prose." } } }, }); server.snapshot("settings", { models: [], systemPrompt: { ...initial, promptProse: { code: "Remote prose." } }, }); await settle(); assertEquals(area.value, "Remote prose."); // a reloaded default reaches an untouched draft, but never overwrites an edited one server.snapshot("settings", { models: [], systemPrompt: { ...initial, promptProse: {} }, }); await settle(); server.snapshot("extensions", [{ ...extensions[0], promptProse: { default: "Reloaded prose." }, }]); await settle(); const freshRow = document.querySelector(".extension")!; const freshArea = freshRow.querySelector("textarea")!; assertEquals(freshArea.value, "Reloaded prose."); assertEquals(freshArea, area); const reloaded = freshArea; reloaded.focus(); fill(reloaded, "Draft prose."); assertEquals(document.activeElement, reloaded); server.snapshot("extensions", [{ ...extensions[0], description: "new description", promptProse: { default: "Newer prose." }, }]); await settle(); assertEquals(reloaded.value, "Draft prose."); assertEquals(document.activeElement, reloaded); assertEquals(document.querySelector(".extension")!, freshRow); assertEquals(document.querySelector(".extension textarea"), reloaded); assertEquals(freshRow.querySelector(".description")!.textContent, "new description"); const entered = Promise.withResolvers(); const first = Promise.withResolvers(); let kept = "Draft prose."; server.respond(({ method, body }) => { if (method !== "PATCH") return []; if (server.changes().length === 2) { entered.resolve(); return first.promise; } kept = (body as { systemPrompt: { promptProse: { code: string } } }).systemPrompt.promptProse .code; return { systemPrompt: { ...initial, promptProse: { code: kept } } }; }); reloaded.focus(); reloaded.blur(); await entered.promise; reloaded.focus(); fill(reloaded, "Second queued."); reloaded.blur(); await settle(); assertEquals(reloaded.value, "Second queued."); first.resolve({ systemPrompt: { ...initial, promptProse: { code: "Draft prose." } } }); await until(() => server.changes().length === 3); await settle(); assertEquals(reloaded.value, "Second queued."); assertEquals(server.changes().slice(1).map(({ body }) => body), [ { systemPrompt: { promptProse: { code: "Draft prose." } } }, { systemPrompt: { promptProse: { code: "Second queued." } } }, ]); server.respond(() => new Response(JSON.stringify({ error: "disk full" }), { status: 500 })); reloaded.focus(); fill(reloaded, "Failing edit."); reloaded.blur(); await settle(); assertEquals(reloaded.value, "Failing edit."); assertEquals( document.querySelector(".extension")!.querySelector('[role="status"]')!.textContent, "disk full", ); }, { url: "http://berry.test/#/settings" }); }); Deno.test("the local environment toggle follows global settings and restores its value after a failed save", async () => { await withBerry(async ({ scope, server, settle }) => { server.respond(() => []); const { app } = await import("./app.tsx"); document.body.append(app(scope)); server.snapshot("settings", { models: [] }); server.snapshot("extensions", []); await settle(); const local = document.querySelector('.environments input[type="checkbox"]'); assertExists(local); assertEquals(local.checked, true); server.respond(() => ({ allowLocalEnvironment: false })); local.click(); await settle(); assertEquals(server.changes().at(-1), { method: "PATCH", path: "/api/settings", body: { allowLocalEnvironment: false }, }); assertEquals(local.checked, false); server.respond(() => new Response(JSON.stringify({ error: "disk full" }), { status: 500 })); local.click(); await settle(); assertEquals(local.checked, false); assertEquals(local.disabled, false); assertEquals(document.querySelector(".environments .error")?.textContent, "disk full"); server.snapshot("settings", { models: [], allowLocalEnvironment: true }); await settle(); assertEquals(local.checked, true); }, { url: "http://berry.test/#/settings" }); }); Deno.test("the notification switch explains a page that cannot ask, and stores only a granted choice", async () => { await withBerry(async ({ scope, server, settle }) => { server.respond(() => []); const { app } = await import("./app.tsx"); document.body.append(app(scope)); server.snapshot("settings", { models: [] }); server.snapshot("extensions", []); await settle(); const input = document.querySelector(".notify input[type=checkbox]"); assertExists(input); assertEquals(input.disabled, true); assertEquals(input.checked, false); assertStringIncludes( document.querySelector(".notify [role=status]")?.textContent ?? "", "secure", ); }, { url: "http://berry.test/#/settings" }); class Granted { static permission: NotificationPermission = "granted"; static requestPermission = () => Promise.resolve("granted" as const); constructor() {} close() {} } await withBerry(async ({ scope, server, settle }) => { server.respond(() => []); const { app } = await import("./app.tsx"); document.body.append(app(scope)); server.snapshot("settings", { models: [] }); server.snapshot("extensions", []); await settle(); const input = document.querySelector(".notify input[type=checkbox]"); assertExists(input); assertEquals(input.disabled, false); input.click(); await settle(); assertEquals(input.checked, true); assertEquals(localStorage.getItem("berry.notifications"), "on"); input.click(); await settle(); assertEquals(input.checked, false); assertEquals(localStorage.getItem("berry.notifications"), null); }, { url: "http://berry.test/#/settings", globals: { Notification: Granted } }); });