fix(wizard): include request_id in apply-batch payload
Backend EA2 apply-batch requires a request_id (idempotency key). Without it, every node/edge write after Intake was failing with HTTP 422 and the wizard stayed stuck on the Analyze phase. Verified via qa/audit_wizard_writes.mjs against the live URL: previously 1 successful POST (draft creation) + 6x 422, now full multi-phase write path enabled.
This commit is contained in:
@@ -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();
|
||||
Reference in New Issue
Block a user