feat(dogfood-wave1): Studio retries, Chat error, LLM gateway, kill snapshot, topbar cleanup (#14)
build-and-publish / test (push) Has been cancelled
build-and-publish / image (push) Has been cancelled

This commit was merged in pull request #14.
This commit is contained in:
2026-06-15 14:30:04 +00:00
parent 601429ea89
commit d5a9c81b9c
17 changed files with 513 additions and 185 deletions
+45
View File
@@ -0,0 +1,45 @@
// Real human dogfood — persona chip click should sign in directly.
import { chromium } from "playwright";
const URL = "https://canvas.flow-master.ai/";
const b = await chromium.launch({ headless: true });
const ctx = await b.newContext({ viewport: { width: 1440, height: 900 } });
const p = await ctx.newPage();
const calls = [];
p.on("response", (r) => {
const u = r.url();
if (u.includes("/api/")) calls.push({ status: r.status(), url: u.replace("https://canvas.flow-master.ai", "") });
});
console.log("[1] Visit canvas.flow-master.ai");
await p.goto(URL, { waitUntil: "domcontentloaded" });
await p.waitForTimeout(2000);
console.log("[2] Click CEO persona chip");
await p.locator(".persona-chip", { hasText: /Mariana/ }).click();
await p.waitForTimeout(4000);
console.log("[3] Check scene + url");
const onLogin = (await p.locator(".login-page").count()) > 0;
const onLanding = (await p.locator(".hero-eyebrow").count()) > 0;
const sceneClass = await p.evaluate(() => document.querySelector("[class*='shell-']")?.className);
console.log(` onLogin=${onLogin} onLanding=${onLanding} shell=${sceneClass}`);
console.log("[4] Try Mission");
await p.locator(".tab", { hasText: /Mission/ }).click().catch(() => {});
await p.waitForTimeout(3000);
const onMission = (await p.locator(".bp-node-body, .empty").count()) > 0;
console.log(` onMission=${onMission}`);
await p.screenshot({ path: "/tmp/canvas-evidence/05-LIVE-after-fix.png", fullPage: false });
console.log("\n=== API CALLS ===");
const status = {};
for (const c of calls) status[c.status] = (status[c.status] || 0) + 1;
console.log("status counts:", JSON.stringify(status));
console.log("first 10:");
for (const c of calls.slice(0, 10)) console.log(` ${c.status} ${c.url.slice(0, 80)}`);
await b.close();
+166
View File
@@ -0,0 +1,166 @@
// 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}`);