import { chromium } from "playwright"; const b = await chromium.launch({ headless: true }); const p = await (await b.newContext()).newPage(); const errors=[]; const benign=s=>/40\d/.test(s); p.on("console",m=>{if(m.type()==="error"&&!benign(m.text()))errors.push("[c]"+m.text());}); p.on("pageerror",e=>errors.push("[pe]"+e.message)); const log=s=>process.stdout.write(s+"\n"); let pass=0,fail=0; const check=(n,c,x="")=>{log(`${c?" OK ":" FAIL"} ${n} ${x}`);c?pass++:fail++;}; await p.goto("http://127.0.0.1:8000/index.html",{waitUntil:"domcontentloaded",timeout:90000}); await p.waitForTimeout(2500); await p.click(`.lang-btn[data-lang="en"]`); await p.waitForTimeout(200); check("module loads, 0 errors", errors.length===0, `(${errors.length})`); await p.click('[data-mode-link="launch"]',{timeout:5000}); await p.waitForTimeout(400); check("section visible", await p.evaluate(()=>{const s=document.querySelector("#launch-section");return s&&getComputedStyle(s).display!=="none";})); check("GPU presets populated", await p.evaluate(()=>document.querySelector("#launch-gpu").options.length>5)); log("\n── Fetch geometry ──"); await p.fill("#launch-model","Qwen/Qwen2.5-7B-Instruct"); await p.keyboard.press("Escape"); await p.click("#launch-fetch-btn"); await p.waitForTimeout(3500); const st=await p.evaluate(()=>document.querySelector("#launch-status").innerText); check("geometry fetched (layers/GQA shown)", /layers|GQA|θ=/.test(st), st.slice(0,70)); check("ctx auto-filled", await p.evaluate(()=>!!document.querySelector("#launch-ctx").value)); async function gen({quant,gpu,vram,ctx,cache,fa}){ if(quant) await p.selectOption("#launch-quant",quant); if(gpu) await p.selectOption("#launch-gpu",gpu); await p.fill("#launch-vram",vram!=null?String(vram):""); if(ctx!=null) await p.fill("#launch-ctx",String(ctx)); if(cache) await p.selectOption("#launch-cache",cache); if(fa!=null){const c=await p.isChecked("#launch-fa"); if(c!==fa) await p.click("#launch-fa");} await p.click("#launch-gen-btn"); await p.waitForTimeout(300); return p.evaluate(()=>{const o=document.querySelector("#launch-output");return{ verdict:o.querySelector(".verdict-badge")?.innerText?.trim()||"", text:o.innerText};}); } log("\n── FITS case (7B Q4 on 24GB) ──"); let r=await gen({quant:"Q4_K_M",gpu:"24",vram:null,ctx:32768,cache:"fp16",fa:true}); check("verdict FITS", /FITS/.test(r.verdict), r.verdict); check("ngl = all layers", /all|28/.test(r.text)); check("llama-server cmd present", /llama-server/.test(r.text)); check("ollama cmd present", /ollama|num_ctx/.test(r.text)); check("--no-mmap added when all-on-GPU", /--no-mmap/.test(r.text)); check("-fa present", /-fa/.test(r.text)); check("VRAM breakdown (weights/KV)", /Weights|KV cache/.test(r.text)); log("\n── PARTIAL case (7B Q4 on tiny 3GB custom) ──"); r=await gen({quant:"Q4_K_M",vram:3,ctx:8192,fa:true}); check("verdict PARTIAL or TOO BIG", /PARTIAL|TOO BIG/.test(r.verdict), r.verdict); check("partial offload warning or cpu-only", /CPU|layers fit|smaller quant/i.test(r.text)); log("\n── cache quant changes KV flag ──"); r=await gen({quant:"Q4_K_M",gpu:"24",vram:null,ctx:32768,cache:"q8_0",fa:true}); check("KV cache q8_0 → -ctk/-ctv in cmd", /-ctk q8_0/.test(r.text)); log("\n── beyond-trained warning ──"); r=await gen({quant:"Q4_K_M",gpu:"80",vram:null,ctx:262144,cache:"fp16",fa:true}); check("L beyond trained → warning", /trained|RoPE|YaRN/i.test(r.text), "L=256K"); log("\n── error: generate before fetch (fresh) ──"); // can't easily un-fetch; just check error key exists by clearing geom via reload-free path is hard; skip log("\n── 4 languages ──"); for(const lang of ["es","fr","zh","en"]){ await p.click(`.lang-btn[data-lang="${lang}"]`); await p.waitForTimeout(250); const lbl=await p.evaluate(()=>document.querySelector('.mode-btn[data-mode="launch"]')?.textContent?.trim()); check(`${lang}: tab label`, lbl&&lbl.length>3, lbl); } check("copy button present", await p.evaluate(()=>!!document.querySelector("#launch-copy-llama"))); log(`\n=== ${pass} passed, ${fail} failed · JS errors: ${errors.length} ===`); errors.slice(0,10).forEach(e=>log(e)); await b.close(); process.exit(fail>0?1:0);