// Buyer-script behavioural QA. Walks every visible CTA the way a non-engineer // buyer would, then sweeps the rendered DOM text on each scene for embarrassing // strings ("not configured", "backend can't", "runtime-values", "provider key", // raw 32-hex IDs). Anything that surfaces on first glance is a buyer-facing // failure even if the underlying scaffolding is correct. import { chromium } from "playwright"; const URL = "https://canvas.flow-master.ai/"; const args = ["--host-resolver-rules=MAP canvas.flow-master.ai 65.21.71.186", "--ignore-certificate-errors"]; const FORBIDDEN_PATTERNS = [ { name: "raw 32-hex IDs", re: /[a-f0-9]{32}/i }, { name: "runtime-values raw mention", re: /runtime[_\s-]?values/i }, { name: "action_execution_failed raw", re: /action_execution_failed/i }, { name: "OPENAI_API_KEY raw", re: /OPENAI_API_KEY/ }, { name: "ANTHROPIC_API_KEY raw", re: /ANTHROPIC_API_KEY/ }, { name: "kubectl", re: /\bkubectl\b/ }, { name: "404 raw", re: /\bHTTP 404\b/i }, { name: "500 raw", re: /\bHTTP 500\b/i }, { name: "stack trace", re: /at\s+\w+\s+\(.*\.ts:\d+/ }, ]; const results = []; function record(name, ok, detail = "") { results.push({ name, ok, detail }); console.log(`${ok ? "PASS" : "FAIL"} ${name}${detail ? " — " + detail : ""}`); } const b = await chromium.launch({ headless: true, args }); const ctx = await b.newContext({ viewport: { width: 1440, height: 900 } }); const p = await ctx.newPage(); async function inspectScene(label) { await p.waitForTimeout(1200); const body = (await p.locator("body").innerText()).toLowerCase(); const findings = FORBIDDEN_PATTERNS .filter((f) => f.re.test(body)) .map((f) => `${f.name}: "${(body.match(f.re) || [""])[0]}"`) .slice(0, 4); record(`buyer_${label}_no_engineering_copy`, findings.length === 0, findings.length ? findings.join(" | ") : "clean"); } await p.goto(URL, { waitUntil: "networkidle" }); await p.waitForTimeout(800); await inspectScene("login"); await p.locator(".persona-chip", { hasText: /Mariana/ }).click(); await p.locator(".dev-login-btn").click(); await p.waitForSelector(".hero-actions", { timeout: 15000 }); await p.waitForFunction(() => document.querySelectorAll(".hub-chip").length >= 8, undefined, { timeout: 15000 }).catch(() => {}); await inspectScene("landing"); const scenes = [ { chip: /Approvals queue/, label: "approvals" }, { chip: /^Documents$/, label: "documents" }, { chip: /Procurement Hub/, label: "procurement_hub" }, { chip: /People Hub/, label: "people_hub" }, { chip: /IT Hub/, label: "it_hub" }, { chip: /Attendance Map/, label: "geo" }, { chip: /Command Assistant/, label: "assistant" }, { chip: /Team Chat/, label: "chat" }, { chip: /What is FlowMaster/, label: "explainer" }, ]; for (const s of scenes) { await p.goto(URL, { waitUntil: "networkidle" }).catch(() => {}); await p.waitForSelector(".hub-chip", { timeout: 15000 }).catch(() => {}); await p.waitForTimeout(600); await p.locator(".hub-chip", { hasText: s.chip }).click().catch(() => {}); await inspectScene(s.label); } // Approvals: click the first action button — if the runtime is up the button // is enabled; if it's down it should be disabled (we should NEVER allow a // click through to an error). Either way no embarrassing copy may surface. await p.goto(URL, { waitUntil: "networkidle" }).catch(() => {}); await p.waitForSelector(".hub-chip", { timeout: 15000 }).catch(() => {}); await p.locator(".hub-chip", { hasText: /Approvals queue/ }).click(); await p.waitForSelector(".approvals-row", { timeout: 15000 }).catch(() => {}); await p.locator(".approvals-row").first().click(); await p.waitForSelector(".approvals-card", { timeout: 8000 }).catch(() => {}); await p.waitForTimeout(2500); const btn = p.locator(".approvals-actions .btn").first(); if (await btn.count() > 0) { const isDisabled = await btn.isDisabled(); if (!isDisabled) { await btn.click(); await p.waitForTimeout(3000); } const tail = (await p.locator("body").innerText()).toLowerCase(); const tailFindings = FORBIDDEN_PATTERNS.filter((f) => f.re.test(tail)).map((f) => f.name).slice(0, 3); record( "buyer_approvals_action_click_no_embarrassing_copy", tailFindings.length === 0, tailFindings.length ? `LEAK: ${tailFindings.join(", ")} (was disabled? ${isDisabled})` : `clean (disabled? ${isDisabled})` ); } const fails = results.filter((r) => !r.ok); console.log(`\n=== buyer script: ${results.length - fails.length}/${results.length} passed ===`); if (fails.length) { console.log("FAILED:"); fails.forEach((f) => console.log(` - ${f.name}: ${f.detail}`)); } await b.close(); process.exit(fails.length === 0 ? 0 : 1);