// Resilient dogfood — never aborts on individual scene failure. import { chromium } from "playwright"; import { mkdirSync, writeFileSync } from "node:fs"; const URL = "https://canvas.flow-master.ai/"; const OUT = "/tmp/canvas-evidence/ulw-dogfood"; mkdirSync(OUT, { recursive: true }); const calls = []; const consoleMessages = []; const pageErrors = []; process.on("uncaughtException", async (e) => { console.error("UNCAUGHT:", e.message); await writeArtifacts(); process.exit(2); }); function writeArtifacts() { writeFileSync(`${OUT}/network.json`, JSON.stringify(calls, null, 2)); writeFileSync(`${OUT}/console.json`, JSON.stringify(consoleMessages, null, 2)); writeFileSync(`${OUT}/pageerrors.json`, JSON.stringify(pageErrors, null, 2)); } const b = await chromium.launch({ headless: true }); const ctx = await b.newContext({ viewport: { width: 1440, height: 900 } }); const p = await ctx.newPage(); p.on("response", (r) => { const u = r.url(); if (u.includes("/api/") || u.includes("/internal/")) { calls.push({ ts: Date.now(), method: r.request().method(), status: r.status(), url: u.replace("https://canvas.flow-master.ai", ""), }); } }); p.on("console", (m) => consoleMessages.push({ ts: Date.now(), type: m.type(), text: m.text().slice(0, 300) })); p.on("pageerror", (e) => pageErrors.push({ ts: Date.now(), message: e.message })); const t0 = Date.now(); const log = (s) => console.log(`[${((Date.now() - t0) / 1000).toFixed(1)}s] ${s}`); async function snap(name) { try { await p.screenshot({ path: `${OUT}/${name}.png`, fullPage: false }); } catch {} } async function safely(label, fn) { try { await fn(); } catch (e) { log(` FAIL ${label}: ${e.message.slice(0, 150)}`); } } log("1. landing"); await safely("goto", async () => { await p.goto(URL, { waitUntil: "domcontentloaded", timeout: 20000 }); await p.waitForTimeout(2500); }); await snap("01-landing"); log("2. click CEO persona"); await safely("ceo click", async () => { await p.locator(".persona-chip", { hasText: /Mariana/ }).click({ timeout: 8000 }); await p.waitForTimeout(6000); }); await snap("02-after-ceo-click"); log(` url=${p.url()} shell=${await p.evaluate(() => document.querySelector("[class*='shell-']")?.className).catch(() => "?")}`); const headerExists = await p.locator(".topbar").count(); log(` topbar visible: ${headerExists > 0}`); if (headerExists === 0) { log(" still on login — trying explicit dev-login button"); await safely("dev-login fallback", async () => { const email = p.locator("input[type='email']").first(); if (await email.count()) { await email.fill("ceo-head@flow-master.ai"); const btn = p.locator(".dev-login-btn").first(); if (await btn.count()) { const enabled = await btn.evaluate((el) => !el.hasAttribute("disabled")); log(` dev-login-btn enabled=${enabled}`); if (enabled) await btn.click({ timeout: 5000 }); } await p.waitForTimeout(5000); } }); await snap("03-after-devlogin-fallback"); log(` url=${p.url()} shell=${await p.evaluate(() => document.querySelector("[class*='shell-']")?.className).catch(() => "?")}`); } const tabs = ["Mission", "Approvals", "Studio", "Hubs", "Chat", "Assistant", "Settings"]; for (const tab of tabs) { await safely(`tab ${tab}`, async () => { const t = p.locator(".tab", { hasText: new RegExp(`^${tab}\\b`) }).first(); if (await t.count() === 0) { log(` tab missing: ${tab}`); return; } await t.click({ timeout: 5000 }); await p.waitForTimeout(3000); await snap(`tab-${tab.toLowerCase()}`); }); } log("3. STUDIO dogfood"); await safely("studio", async () => { await p.locator(".tab", { hasText: /Studio/ }).click({ timeout: 5000 }); await p.waitForTimeout(2000); const desc = p.locator("textarea").first(); if (await desc.count()) { await desc.fill("When a store manager requests a new laptop, run a procurement approval."); await snap("studio-intake-filled"); const intakeBtn = p.locator("button.btn-primary").filter({ hasText: /draft|→/i }).first(); if (await intakeBtn.count()) { await intakeBtn.click({ timeout: 5000 }); await p.waitForTimeout(7000); await snap("studio-analyze"); const confirmBtn = p.locator("button.btn-primary").filter({ hasText: /Confirm structure|Confirm Structure/i }).first(); if (await confirmBtn.count()) { await confirmBtn.click({ timeout: 5000 }); await p.waitForTimeout(7000); await snap("studio-after-confirm"); const toasts = await p.locator(".toast, .toaster, .toast-msg, .login-error").allTextContents(); log(` toasts: ${toasts.join(" | ").slice(0, 400)}`); } else { log(" no Confirm Structure button"); } } else { log(" no Start Drafting button"); } } else { log(" no textarea"); } }); log("4. ASSISTANT 'list processes'"); await safely("assistant", async () => { await p.locator(".tab", { hasText: /Assistant/ }).click({ timeout: 5000 }); await p.waitForTimeout(3000); await snap("assistant-empty"); const input = p.locator(".agent-input, input[placeholder*='Ask']").first(); if (await input.count()) { await input.fill("list processes"); await input.press("Enter"); await p.waitForTimeout(8000); await snap("assistant-after-list"); const msgs = await p.locator(".agent-msg-text, .agent-msg-body").allTextContents(); log(` last reply: ${msgs.slice(-1).join(" ").slice(0, 400)}`); } else { log(" no input"); } }); log("5. CHAT"); await safely("chat", async () => { await p.locator(".tab", { hasText: /Chat/ }).click({ timeout: 5000 }); await p.waitForTimeout(4000); await snap("chat"); const innerText = await p.evaluate(() => document.querySelector(".scene")?.innerText?.slice(0, 400) ?? ""); log(` chat scene text: ${innerText.slice(0, 300)}`); }); await b.close(); writeArtifacts(); const byStatus = {}; for (const c of calls) byStatus[c.status] = (byStatus[c.status] || 0) + 1; const fails = calls.filter(c => c.status >= 400); const errors = consoleMessages.filter(m => m.type === "error"); console.log(`\n=== DOGFOOD SUMMARY ===`); console.log(`api calls: ${calls.length} by status: ${JSON.stringify(byStatus)}`); console.log(`page errors: ${pageErrors.length}`); console.log(`console errors: ${errors.length}`); console.log(`\nFailing API calls:`); for (const f of fails.slice(0, 40)) console.log(` ${f.status} ${f.method} ${f.url.slice(0, 80)}`); console.log(`\nConsole errors:`); for (const e of errors.slice(0, 20)) console.log(` ${e.text.slice(0, 200)}`); console.log(`\nPage errors:`); for (const e of pageErrors.slice(0, 10)) console.log(` ${e.message.slice(0, 200)}`); console.log(`\nArtifacts: ${OUT}`);