From 488d4dcd4b06ca0ea20fe42590cf2d824d0f5443 Mon Sep 17 00:00:00 2001 From: Gorka Date: Fri, 3 Jul 2026 16:18:29 -0300 Subject: [PATCH 1/2] =?UTF-8?q?test(playwright):=20stabilize=20full-flow?= =?UTF-8?q?=20E2E=20=E2=80=94=20root-cause=203=20races=20(ClickUp=2086caj0?= =?UTF-8?q?zzq)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Make the full-flow browser suite a reliable gate by replacing timing-fragile waits/setup with condition-based waits on the real signals. No app-code changes. - Step 13 (async settlement): replace fixed waitForTimeout(5000) + single-shot fetchCompletedInboundStroops read with a bounded expect.poll on the real projection (>0, 150s/2s). /pay/instant/execute writes the merchant's IN/COMPLETED row only after on-chain settlement, well after Step 12's mid-flight POS status — the old read raced it. - Step 5 (nav handoff): Step 4's Next registers the PP then navigates to /setup/join; wait for the join view's #council-url to render instead of a flaky networkidle, so Step 5 starts deterministically on the join view rather than racing the in-flight nav onto the fragile /home fallback. - Step 4 (fee-payer funding): fund the PP root account (bundle fee-payer) with 500 XLM instead of 10. provider-platform's pre-flight requires the fee-payer hold >= NETWORK_FEE (100 XLM); 10 XLM deterministically failed every bundle with InsufficientFees (masked before by the old loose Step 13 assertion). The green e2e/pos-instant suites fund the PP via friendbot (~10k XLM). Verified: 10/10 consecutive green runs of ./test.sh playwright, 0 flakes. --- playwright/tests/full-flow.spec.ts | 91 +++++++++++++++++++----------- 1 file changed, 59 insertions(+), 32 deletions(-) diff --git a/playwright/tests/full-flow.spec.ts b/playwright/tests/full-flow.spec.ts index 72a04c8..601f199 100644 --- a/playwright/tests/full-flow.spec.ts +++ b/playwright/tests/full-flow.spec.ts @@ -352,9 +352,20 @@ test.describe("Full UC Flow", () => { await providerPage.fill("#pp-email", "playwright-pp@moonlight.test"); await providerPage.click("#next-btn"); - // Fund Account — wallet sends XLM to the PP's derived address + // Fund Account — wallet sends XLM to the PP's derived address. + // The PP root account is the fee-payer for every bundle provider-platform + // submits (executor.process.ts: feePayerPubkey = ppPublicKey). Its + // pre-flight OpEx check (preflight-opex-balance.ts) requires the fee-payer + // to HOLD at least the inclusion fee (NETWORK_FEE = 1_000_000_000 stroops = + // 100 XLM, canonical across every docker suite) plus the Soroban resource + // fee, after reserves — otherwise the bundle terminal-FAILs with + // "Insufficient fees on fee-payer account" and the payment never settles. + // The old "10" under-funded it, so Step 12's send silently failed and + // Step 13 saw 0 stroops. The green e2e/pos-instant suites fund the PP via + // friendbot (~10_000 XLM); mirror that headroom here with a UI send well + // above the 100 XLM floor. await providerPage.waitForSelector("#fund-amount", { timeout: 15_000 }); - await providerPage.fill("#fund-amount", "10"); + await providerPage.fill("#fund-amount", "500"); await withWalletApproval(providerCtx.context, providerPage, async () => { await providerPage.click("#fund-btn"); @@ -364,9 +375,19 @@ test.describe("Full UC Flow", () => { timeout: 60_000, }); - // Next registers the PP via API (no wallet popup), then auto-navigates to /setup/join + // Next registers the PP via API (no wallet popup), then auto-navigates to + // /setup/join. Wait for the join view's Council-URL input to actually + // render rather than a flaky networkidle: registerPp is a network call + // whose latency under docker load can outrun Step 5's checks, so + // networkidle can resolve while still mid-navigation — leaving Step 5 to + // race the in-flight nav, miss #council-url, and fall back to the fragile + // /home path (observed: trace shows fund → join → home, timeout). Landing + // deterministically on the join view is the real post-condition here. await providerPage.click("#next-btn"); - await providerPage.waitForLoadState("networkidle"); + await providerPage.waitForSelector("#council-url", { + state: "visible", + timeout: 30_000, + }); }); // ── Step 5: Provider requests joining council ───────────────────── @@ -833,37 +854,43 @@ test.describe("Full UC Flow", () => { // ── Step 13: Merchant verifies received payment ─────────────────── test("Step 13: Merchant verifies received payment", async () => { - await merchantPage.goto(`${urls.moonlightPay}/#/`); - await merchantPage.waitForLoadState("networkidle"); - await merchantPage.waitForTimeout(5000); - await merchantPage.reload(); - await merchantPage.waitForLoadState("networkidle"); - - // UI smoke — merchant home renders. - const hasBalance = await merchantPage - .locator("#balance-display") - .isVisible() - .catch(() => false); - const hasTxList = await merchantPage - .locator("#tx-list") - .isVisible() - .catch(() => false); - expect(hasBalance || hasTxList).toBeTruthy(); - // Protocol-level proof: pay-platform recorded at least one COMPLETED - // inbound transaction for the merchant. The previous assertion - // (txContent?.length > 0) accepted "No transactions yet" placeholder - // text, so a payment that never settled looked the same as one that - // did. The DB query is authoritative. + // inbound transaction for the merchant. The DB query is authoritative + // (the old UI-text check accepted the "No transactions yet" placeholder). + // + // This settles ASYNCHRONOUSLY: the POS calls /pay/instant/execute, which + // writes the merchant's IN/COMPLETED row only AFTER the bundle settles on + // chain (provider-platform's executor 5s + verifier 10s ticks; the + // endpoint itself polls up to 120s). Step 12 returns early — it reads the + // mid-flight "Processing payment…" status, not the final "Payment + // complete!" — so the row lands well after Step 12. The old fixed + // waitForTimeout(5000) + single read raced that settlement and saw 0 + // stroops. Poll the real signal until it settles instead of guessing a + // sleep. (No retry/skip — this is a condition-based wait on the actual + // projection, bounded just above the endpoint's own 120s settle cap.) if (getTarget() === "local") { - const completed = await fetchCompletedInboundStroops( - profiles.merchant.publicKey, - ); - expect( - completed > 0n, - `Expected at least one COMPLETED inbound tx for merchant ${profiles.merchant.publicKey}, got total ${completed} stroops`, - ).toBe(true); + await expect + .poll( + async () => + Number( + await fetchCompletedInboundStroops(profiles.merchant.publicKey), + ), + { + timeout: 150_000, + intervals: [2_000], + message: + `No COMPLETED inbound tx for merchant ${profiles.merchant.publicKey} settled within 150s`, + }, + ) + .toBeGreaterThan(0); } + + // UI smoke — merchant home renders the (now-settled) state. + await merchantPage.goto(`${urls.moonlightPay}/#/`); + await merchantPage.waitForLoadState("networkidle"); + await expect( + merchantPage.locator("#balance-display, #tx-list").first(), + ).toBeVisible({ timeout: 15_000 }); }); // ── Step 13b: Merchant deletes their pay-platform account ───────── From 002ac861c198fed58c845ec41e1868ad113000aa Mon Sep 17 00:00:00 2001 From: Gorka Date: Fri, 3 Jul 2026 16:24:50 -0300 Subject: [PATCH 2/2] chore(playwright): bump e2e harness version 0.1.0 -> 0.1.1 --- playwright/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/playwright/package.json b/playwright/package.json index 397a757..4358ed7 100644 --- a/playwright/package.json +++ b/playwright/package.json @@ -1,6 +1,6 @@ { "name": "moonlight-playwright-e2e", - "version": "0.1.0", + "version": "0.1.1", "private": true, "scripts": { "test": "npx playwright test --project=chromium",