Files
learnharness/tests/fast-mode-interaction.test.tsx

75 lines
4.7 KiB
TypeScript

import React, { act } from "react";
(globalThis as Record<string, unknown>).React = React;
import assert from "node:assert/strict";
import { afterEach, it } from "node:test";
import { createRoot, type Root } from "react-dom/client";
import { parseHTML } from "linkedom";
import { AgentSection } from "../components/settings/agent-section.tsx";
import { I18nProvider } from "../components/i18n.tsx";
import type { ModelCatalogEntry, ModelsSettings } from "../src/core/index.ts";
const dom = parseHTML("<html><body></body></html>");
Object.assign(globalThis, { window: dom.window, document: dom.document, Element: dom.window.Element, HTMLElement: dom.window.HTMLElement, CustomEvent: dom.window.CustomEvent, IS_REACT_ACT_ENVIRONMENT: true });
const originalFetch = globalThis.fetch;
const chatgpt = { provider: "chatgpt", id: "gpt-5" };
const api = { provider: "openai", id: "gpt-5" };
const fixture: ModelCatalogEntry = { ...chatgpt, name: "Offline model", reasoning: true, input: ["text", "image"], supportedLevels: ["low", "high"], cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, contextWindow: 8192, maxTokens: 2048 };
let root: Root | null = null;
let host: HTMLDivElement;
let writes: ModelsSettings[];
let saved: ModelsSettings;
let waitForSave: (() => Promise<void>) | undefined;
let failSave = false;
function button(slot: string, kind = "fast"): HTMLButtonElement {
const found = document.querySelector<HTMLButtonElement>(`[data-testid="slot-${kind}-${slot}"]`);
assert(found); return found;
}
async function click(target: HTMLButtonElement) { await act(async () => target.click()); }
async function mount() {
writes = []; waitForSave = undefined; failSave = false;
saved = { strong: { model: chatgpt, thinkingLevel: "high" }, cheap: { model: chatgpt, thinkingLevel: "low" }, vision: { model: api } };
const responses: Record<string, unknown> = {
"/api/models": { models: [fixture, { ...fixture, provider: "openai" }], slots: saved },
"/api/search": { available: false, engine: null, reason: "No search engine is configured." },
"/api/keys": { keys: [] }, "/api/settings/prompts": { prober: [], teacher: [] },
};
globalThis.fetch = async input => { const name = String(input); assert(name in responses); return Response.json(responses[name]); };
host = document.createElement("div"); document.body.appendChild(host); root = createRoot(host);
function Harness() {
const [models, setModels] = React.useState(saved);
return <I18nProvider initialGeneral={{ language: "ru" }}><AgentSection agent={undefined} models={models} graph={undefined} compaction={undefined} onSectionChange={() => {}} onCompactionChange={() => {}} onModelsChange={async next => {
writes.push(next); await waitForSave?.();
if (failSave) throw new Error("offline save failure");
saved = next; setModels(next);
}} /></I18nProvider>;
}
await act(async () => { root!.render(<Harness />); });
}
afterEach(async () => { await act(async () => root?.unmount()); root = null; host?.remove(); globalThis.fetch = originalFetch; });
it("Fast controls wait for stored truth, lock related edits while saving, and preserve other tiers", async () => {
await mount();
assert.equal(button("strong").getAttribute("aria-pressed"), "false");
assert.equal(button("vision").disabled, true);
assert.equal(button("strong").textContent, "Быстро");
let release!: () => void;
const pending = new Promise<void>(resolve => { release = resolve; }); waitForSave = () => pending;
await click(button("strong"));
assert.equal(writes.length, 1);
for (const slot of ["strong", "cheap"]) for (const kind of ["fast", "model", "thinking"]) assert.equal(button(slot, kind).disabled, true);
assert.equal(button("strong").getAttribute("aria-pressed"), "false", "unconfirmed billing choices must not appear saved");
await act(async () => { release(); }); waitForSave = undefined;
assert.equal(button("strong").getAttribute("aria-pressed"), "true");
await click(button("cheap"));
assert.equal(saved.strong?.fastMode, true); assert.equal(saved.cheap?.fastMode, true);
assert.equal(saved.strong?.thinkingLevel, "high"); assert.equal(saved.vision?.fastMode, undefined);
});
it("failed Fast saves retain the previous choice, show a Russian error, and allow retry", async () => {
await mount(); failSave = true;
await click(button("strong"));
assert.equal(button("strong").getAttribute("aria-pressed"), "false"); assert.equal(button("strong").disabled, false);
assert.equal(document.querySelector('[role="alert"]')?.textContent, "Не удалось сохранить настройки моделей");
failSave = false; await click(button("strong"));
assert.equal(button("strong").getAttribute("aria-pressed"), "true"); assert.equal(document.querySelector('[role="alert"]'), null);
});