Files
canvas-frontend/qa/_wave3_neg_path.mjs
T
canvas-bot 727b8af463
build-and-publish / test (push) Has been cancelled
build-and-publish / image (push) Has been cancelled
fix(wizard): retry config attach on Confirm if initial PUT failed
Oracle wave-3 caveat: 'A toast alone is insufficient if the app marks
the wizard state as attached locally and never retries.'

Track draft.wizardConfigAttached. Set true only after the initial PUT
resolves. In handleStructureSave (Confirm Structure), if not yet
attached, retry updateDraftConfig before applying the structure batch.
Failure of the retry is logged but doesn't block save — structure
still lands and downstream config sync can pick up later.
2026-06-15 21:54:28 +04:00

105 lines
5.0 KiB
JavaScript

// Oracle's wave-3 negative path: force the wizard config PUT to fail
// after the draft POST succeeds, then verify:
// 1. The soft toast appears with the exact copy
// 2. The draft remains usable (no hard crash)
// 3. Confirm/handleStructureSave retries the attach
import { chromium } from "playwright";
const URL = "https://canvas.flow-master.ai/";
const r = [];
const pass = (n, d = "") => { r.push({ n, ok: true, d }); console.log(`PASS ${n}${d ? " " + d : ""}`); };
const fail = (n, d = "") => { r.push({ n, ok: false, d }); console.log(`FAIL ${n}${d ? " " + d : ""}`); };
const b = await chromium.launch({ headless: true });
const ctx = await b.newContext({ viewport: { width: 1440, height: 900 } });
const p = await ctx.newPage();
const calls = [];
const consoleMsgs = [];
p.on("console", m => consoleMsgs.push({ type: m.type(), text: m.text().slice(0, 220) }));
p.on("response", res => {
if (res.url().includes("/api/")) calls.push({ status: res.status(), method: res.request().method(), url: res.url().replace("https://canvas.flow-master.ai", "") });
});
await ctx.addInitScript(() => localStorage.setItem("fm.canvas.tour.seen", String(Date.now())));
console.log("\n--- Force the first wizard PUT to fail with 500 ---");
let injected = 0;
const targetKeys = new Set();
await p.route("**/api/ea2/flow/*", async (route, request) => {
const u = request.url();
if (request.method() === "PUT" && injected === 0) {
const m = u.match(/\/api\/ea2\/flow\/([a-f0-9]{16,})$/);
if (m) {
targetKeys.add(m[1]);
injected++;
console.log(` injecting 500 on first PUT to ${m[1]}`);
return route.fulfill({ status: 500, contentType: "application/json", body: JSON.stringify({ error: "Injected failure for negative-path test" }) });
}
}
return route.continue();
});
await p.goto(URL, { waitUntil: "domcontentloaded" });
await p.waitForTimeout(2500);
await p.locator(".persona-chip").filter({ hasText: /Mariana/ }).click();
await p.waitForSelector(".topbar", { timeout: 30000 });
await p.waitForTimeout(2000);
await p.evaluate(() => localStorage.removeItem("fm.mc.wizard.draft"));
await p.reload({ waitUntil: "domcontentloaded" });
await p.waitForTimeout(2500);
await p.locator(".tab").filter({ hasText: /Studio/i }).first().click();
await p.waitForSelector("textarea", { timeout: 10000 });
await p.locator("textarea").first().fill("Negative path: force PUT failure on attach.");
await p.locator("button.btn-primary").filter({ hasText: /draft/i }).first().click();
await p.waitForTimeout(6000);
const posts = calls.filter(c => c.method === "POST" && c.url === "/api/ea2/flow");
const puts = calls.filter(c => c.method === "PUT" && /\/api\/ea2\/flow\/[a-f0-9]+/.test(c.url));
console.log(` POSTs:`, posts.map(c => c.status));
console.log(` PUTs:`, puts.map(c => c.status));
if (posts.some(c => c.status === 201)) pass("neg_draft_post_succeeded");
else fail("neg_draft_post_succeeded", `posts=${JSON.stringify(posts)}`);
if (puts.some(c => c.status === 500)) pass("neg_first_put_intercepted_500");
else fail("neg_first_put_intercepted_500", `puts=${JSON.stringify(puts)}`);
const toasts = await p.locator(".toast").allTextContents();
console.log(` toasts seen:`, toasts);
const softToast = toasts.find(t => /didn't attach.*Confirm/i.test(t));
if (softToast) pass("neg_soft_toast_with_exact_copy", softToast.slice(0, 80));
else fail("neg_soft_toast_with_exact_copy", `toasts=${JSON.stringify(toasts)}`);
const warnSeen = consoleMsgs.some(m => m.text.includes("[wizard] config attach failed"));
if (warnSeen) pass("neg_console_warn_logged");
else fail("neg_console_warn_logged");
// Draft still usable: should have advanced to Analyze step
const analyzeVisible = await p.locator(".scene").innerText().then(t => /analyze/i.test(t));
if (analyzeVisible) pass("neg_draft_still_usable_after_failure");
else fail("neg_draft_still_usable_after_failure");
console.log("\n--- Click Confirm Structure, verify retry attach fires ---");
const putsBeforeConfirm = puts.length;
await p.locator("button.btn-primary").filter({ hasText: /Confirm/i }).first().click();
await p.waitForTimeout(8000);
const putsAfter = calls.filter(c => c.method === "PUT" && /\/api\/ea2\/flow\/[a-f0-9]+/.test(c.url));
const newPuts = putsAfter.slice(putsBeforeConfirm);
console.log(` PUTs during Confirm:`, newPuts.map(c => `${c.status} ${c.url.slice(-30)}`));
const retryPut = newPuts.find(c => Array.from(targetKeys).some(k => c.url.includes(k)));
if (retryPut && retryPut.status === 200) pass("neg_confirm_retries_attach_successfully", `PUT ${retryPut.status}`);
else if (retryPut) pass("neg_confirm_retries_attach_fired", `PUT ${retryPut.status} (attempt made)`);
else fail("neg_confirm_retries_attach", `no PUT to original draft key during Confirm`);
const total5xx = calls.filter(c => c.status >= 500).length;
console.log(`\n5xx count: ${total5xx} (expected 1 from injection)`);
await p.screenshot({ path: "/tmp/canvas-evidence/wave3-neg-path.png" });
await b.close();
const passed = r.filter(x => x.ok).length;
console.log(`\n=== ${passed}/${r.length} negative-path checks passed ===`);
process.exit(passed === r.length ? 0 : 1);