diff --git a/qa/audit_wizard_writes.mjs b/qa/audit_wizard_writes.mjs new file mode 100644 index 0000000..3f0aca7 --- /dev/null +++ b/qa/audit_wizard_writes.mjs @@ -0,0 +1,120 @@ +// EA2 step-write audit: drive the live wizard like a human, capture +// every /api/ea2/* + /api/runtime/* POST/PUT, and report which Wizard +// phase actually hit the backend. +import { chromium } from "playwright"; + +const URL = "https://canvas.flow-master.ai/"; +const b = await chromium.launch({ + headless: true, + args: ["--host-resolver-rules=MAP canvas.flow-master.ai 65.21.71.186", "--ignore-certificate-errors"], +}); +const ctx = await b.newContext({ viewport: { width: 1440, height: 900 } }); +const p = await ctx.newPage(); + +const writes = []; +p.on("request", (req) => { + const u = req.url(); + const m = req.method(); + if ((m === "POST" || m === "PUT") && /\/api\/(ea2|runtime)\//.test(u)) { + writes.push({ method: m, path: u.replace("https://canvas.flow-master.ai", ""), at: Date.now() }); + } +}); +p.on("response", async (res) => { + const u = res.url(); + if (/\/api\/(ea2|runtime)\//.test(u) && (res.request().method() === "POST" || res.request().method() === "PUT")) { + let body = ""; + try { body = (await res.text()).slice(0, 200); } catch {} + console.log(`[resp ${res.status()}] ${res.request().method()} ${u.replace("https://canvas.flow-master.ai", "")} → ${body}`); + } +}); + +await p.goto(URL, { waitUntil: "networkidle" }); +await p.waitForTimeout(1500); +await p.screenshot({ path: "qa/screenshots/audit/01-landing.png" }); + +await p.locator("input[type='email']").fill("dev@flow-master.ai"); +await p.locator("button", { hasText: /developer|dev-login/i }).first().click(); +await p.waitForTimeout(2500); +await p.screenshot({ path: "qa/screenshots/audit/02-after-login.png" }); +console.log("[after login] visible buttons:"); +for (const b of await p.locator("button").all()) { + const t = (await b.textContent())?.trim(); + if (t) console.log(" -", t.slice(0, 60)); +} + +// Flip to LIVE mode (button labelled "Go live" on landing) +const liveBtn = p.locator("button", { hasText: /^Go live$/ }).first(); +if (await liveBtn.count() > 0) { + await liveBtn.click(); + await p.waitForTimeout(1200); + console.log("[step] clicked Go live"); +} +console.log("[after Go live] visible buttons:"); +for (const b of await p.locator("button").all()) { + const t = (await b.textContent())?.trim(); + if (t) console.log(" -", t.slice(0, 70)); +} + +// Open the wizard +const designBtn = p.locator("button", { hasText: /Design a process/ }).first(); +if (await designBtn.count() > 0) { + await designBtn.click(); + await p.waitForTimeout(1500); + console.log("[step] clicked Design a process"); +} else { + console.log("[warn] no 'Design a process' button found after Go live"); +} +await p.screenshot({ path: "qa/screenshots/audit/03-wizard.png" }); +console.log("[after Design] body excerpt:"); +const body = await p.locator("body").innerText(); +console.log(body.slice(0, 500)); + +console.log("=== writes during Wizard navigation only ==="); +const baselineWrites = writes.length; +writes.slice().forEach((w) => console.log(` ${w.method} ${w.path}`)); + +// Type into the textarea (description), which is what gates Start Drafting +const desc = p.locator("textarea").first(); +if (await desc.count() > 0) { + await desc.fill("When a store manager requests a new laptop, run a procurement approval"); + console.log("\n[step] typed description into textarea"); +} else { + console.log("\n[warn] no textarea found"); +} +const nameInput = p.locator("input[type='text']").first(); +if (await nameInput.count() > 0) { + await nameInput.fill("Laptop Procurement"); +} +await p.waitForTimeout(400); +const continueBtn = p.locator("button:not([disabled])", { hasText: /start drafting|continue|next|generate|analyze|confirm|publish/i }).first(); +if (await continueBtn.count() > 0) { + console.log(`[step] clicking "${(await continueBtn.textContent())?.trim()}"`); + await continueBtn.click(); + await p.waitForTimeout(3500); +} + +console.log(`\n=== writes after first 'Continue' (${writes.length - baselineWrites} new) ===`); +writes.slice(baselineWrites).forEach((w) => console.log(` ${w.method} ${w.path}`)); + +// Try to click through more phases +for (let i = 0; i < 6; i++) { + const next = p.locator("button:not([disabled])", { hasText: /confirm|continue|next|publish|save|accept|review/i }).first(); + if (await next.count() === 0) break; + console.log(`[loop ${i}] clicking "${(await next.textContent())?.trim()}"`); + const before = writes.length; + await next.click().catch(() => {}); + await p.waitForTimeout(2500); + if (writes.length === before) break; +} + +console.log(`\n=== TOTAL ${writes.length} writes across full wizard run ===`); +writes.forEach((w) => console.log(` ${w.method} ${w.path}`)); + +console.log("\n=== SUMMARY ==="); +const summary = writes.reduce((acc, w) => { + const key = `${w.method} ${w.path.split("?")[0].replace(/\/[a-f0-9]{8,}/g, "/{id}")}`; + acc[key] = (acc[key] || 0) + 1; + return acc; +}, {}); +console.log(JSON.stringify(summary, null, 2)); +await b.close(); diff --git a/src/lib/wizardApi.ts b/src/lib/wizardApi.ts index f4bba3c..b7e7a77 100644 --- a/src/lib/wizardApi.ts +++ b/src/lib/wizardApi.ts @@ -61,16 +61,23 @@ export const wizardApi = { }, async applyBatch(flow_key: string, ops: BatchOperation[], actor: Actor, signal?: AbortSignal) { + const request_id = + typeof crypto !== "undefined" && "randomUUID" in crypto + ? crypto.randomUUID() + : `req-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`; const res = await fetch(`${api.config.baseUrl}/api/ea2/apply-batch`, { method: "POST", headers: { Authorization: `Bearer ${sessionStorage.getItem("fm.mc.token.v1")}`, "Content-Type": "application/json", }, - body: JSON.stringify({ flow_key, ops, actor }), + body: JSON.stringify({ flow_key, ops, actor, request_id }), signal, }); - if (!res.ok) throw new Error(`applyBatch failed: ${res.status}`); + if (!res.ok) { + const detail = await res.text().catch(() => ""); + throw new Error(`applyBatch ${res.status}: ${detail.slice(0, 200)}`); + } return res.json(); },