// React UI against the fake OpenAI-compatible model (tests/mock_llm.py): model badge + chat mode text, fallback badge // texts, quota badge, rate-limit toast, and a model-sent set_theme that the server sanitised. No real model calls. // Usage: NODE_PATH= node tests/fake_model_browser.js const puppeteer = require("puppeteer-core"); const [OK, BUSY, DAILY, RL] = process.argv.slice(2, 6); const results = []; const check = (name, ok, d = "") => { results.push(!!ok); console.log((ok ? "PASS " : "FAIL ") + name + (d ? ` [${d}]` : "")); }; const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); (async () => { const browser = await puppeteer.launch({ executablePath: process.env.CHROME || "/usr/bin/google-chrome", headless: "new", args: ["--no-sandbox", "--disable-gpu", "--user-data-dir=/tmp/am-fake-model-" + Date.now()] }); const errors = []; const open = async (base) => { const ctx = await browser.createBrowserContext(); const page = await ctx.newPage(); await page.setViewport({ width: 1280, height: 860 }); page.on("pageerror", (e) => errors.push(e.message)); await page.goto(base + "/", { waitUntil: "networkidle0" }); await page.waitForSelector(".card", { timeout: 15000 }); return page; }; const ask = async (page, text) => { const n = (await page.$$(".msg.bot")).length; if (await page.$eval("#chat", (c) => c.classList.contains("hidden"))) await page.click("#chatLauncher"); await page.type("#chatInput", text); await page.keyboard.press("Enter"); await page.waitForFunction((n) => document.querySelectorAll(".msg.bot").length > n, { timeout: 30000 }, n); await sleep(300); return page.$$eval(".msg.bot", (ns) => ns[ns.length - 1].textContent); }; const text = (page, sel) => page.$eval(sel, (n) => n.textContent); // 1) model answers: badge + chat mode show the model; tool-hydrated cards render; unknown component ignored let p = await open(OK); check("badge shows configured model on load", (await text(p, "#agentBadge")) === "AI: mock-model", await text(p, "#agentBadge")); await ask(p, "show me bikes"); const mode = await text(p, "#chatMode"), badge = await text(p, "#agentBadge"), title = await p.$eval("#agentBadge", (n) => n.title); check("model reply: badge + chat mode", badge === "AI: mock-model" && /^AI agent · mock-model · \d+\.\ds$/.test(mode) && title === "mock-model via OpenRouter (free tier)", `${badge} | ${mode} | ${title}`); const titles = await p.$$eval("#stage .card h3", (ns) => ns.map((n) => n.textContent)); check("model-chosen cards rendered through the registry", titles.length > 0 && titles.every((t) => /bike/i.test(t)) && (await p.$$("#stage > section")).length === 1, titles.join(" | ")); // 2) model restyle: server-sanitised set_theme applied; HTML in copy rendered as text; customCss scoped const reply = await ask(p, "make the background a sunset gradient"); const st = await p.evaluate(() => ({ bg: getComputedStyle(document.body).backgroundImage, img: !!document.querySelector("#headline img"), h: document.getElementById("headline").textContent, css: document.getElementById("user-css").textContent, toast: [...document.querySelectorAll("#toasts .toast")].map((t) => t.textContent).join("|"), ls: JSON.parse(localStorage.getItem("am_theme_v1") || "{}") })); check("model set_theme applied (gradient, toast, saved)", /gradient/.test(st.bg) && /Look updated/.test(st.toast) && st.ls.background, st.bg.slice(0, 60)); check("model copy/CSS sanitised in the page", !st.img && /Sunset/.test(st.h) && !/ built-in agent answers, fallback badge text p = await open(BUSY); await ask(p, "show me bikes"); check("fallback badge + chat mode text", (await text(p, "#agentBadge")) === "agent: built-in (AI unavailable)" && (await text(p, "#chatMode")) === "built-in agent answered (AI model unavailable)" && (await p.$$("#stage .card")).length > 0, `${await text(p, "#agentBadge")} | ${await text(p, "#chatMode")}`); // 4) daily quota used up -> after reload the badge says so (from /api/status) p = await open(DAILY); await ask(p, "show me bikes"); await p.reload({ waitUntil: "networkidle0" }); await sleep(300); check("quota badge after reload", (await text(p, "#agentBadge")) === "agent: built-in (AI quota used up)" && /resets/.test(await p.$eval("#agentBadge", (n) => n.title)), await text(p, "#agentBadge")); // 5) per-IP rate limit -> warning toast p = await open(RL); await ask(p, "show me bikes"); await ask(p, "show me bikes"); const toasts = await p.$$eval("#toasts .toast.warn", (ns) => ns.map((n) => n.textContent)); check("rate-limit warning toast", toasts.some((t) => /quickly/.test(t)), toasts.join("|")); check("no page JS errors", errors.length === 0, errors.join("; ")); await browser.close(); const passed = results.filter(Boolean).length; console.log(`\n${passed}/${results.length} passed`); process.exit(passed === results.length ? 0 : 1); })().catch((e) => { console.error(e); process.exit(2); });