From 1329c64bb674ef82f66d687fc231e37f58469ee7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Diamond?= <32074058+Andre-Diamond@users.noreply.github.com> Date: Mon, 13 Jul 2026 12:09:46 +0200 Subject: [PATCH] refactor: remove redundant waitForLoadState calls and improve retry logic in CI scenarios --- .github/workflows/pr-playwright-browser.yml | 9 -- Dockerfile.playwright | 3 - e2e/tests/create-wallet-ui.spec.ts | 1 - e2e/tests/rejected-signing.spec.ts | 1 - e2e/tests/responsive-smoke.spec.ts | 1 - e2e/tests/ring-transfer.spec.ts | 3 - scripts/ci/README.md | 4 +- scripts/ci/scenarios/flows/utxoShapeFlow.ts | 4 +- scripts/ci/scenarios/steps/certificates.ts | 109 +++++++++++--------- scripts/ci/scenarios/steps/proxyBot.ts | 4 +- src/__tests__/ciScenarioManifest.test.ts | 28 +++++ 11 files changed, 92 insertions(+), 75 deletions(-) diff --git a/.github/workflows/pr-playwright-browser.yml b/.github/workflows/pr-playwright-browser.yml index fbf41f1b..95510888 100644 --- a/.github/workflows/pr-playwright-browser.yml +++ b/.github/workflows/pr-playwright-browser.yml @@ -61,15 +61,6 @@ jobs: exit 1 fi - - name: Pull base images (with retry) - shell: bash - run: | - for i in 1 2 3; do - docker pull node:20-alpine && break - echo "Pull attempt $i failed, retrying in 30s..." - sleep 30 - done - - name: Build CI containers shell: bash run: docker compose -f docker-compose.playwright.yml build app bootstrap-runner playwright-runner diff --git a/Dockerfile.playwright b/Dockerfile.playwright index 9265204b..5e9fcd36 100644 --- a/Dockerfile.playwright +++ b/Dockerfile.playwright @@ -7,9 +7,6 @@ COPY package.json package-lock.json* ./ COPY prisma ./prisma RUN npm ci --fetch-retries=5 --fetch-retry-mintimeout=20000 --fetch-retry-maxtimeout=120000 -# Install Chromium and its system dependencies. -RUN npx playwright install chromium --with-deps - # Copy e2e suite and the CI framework types it imports. COPY e2e/ ./e2e/ COPY scripts/ci/framework/ ./scripts/ci/framework/ diff --git a/e2e/tests/create-wallet-ui.spec.ts b/e2e/tests/create-wallet-ui.spec.ts index 948136e7..362bce18 100644 --- a/e2e/tests/create-wallet-ui.spec.ts +++ b/e2e/tests/create-wallet-ui.spec.ts @@ -80,7 +80,6 @@ async function createWalletThroughUi( const clearStakeKey = mode === "legacy"; await page.goto("/wallets/new-wallet-flow/save"); - await page.waitForLoadState("networkidle", { timeout: 30_000 }).catch(() => {}); await page.getByLabel("Name", { exact: true }).fill(walletName); await page.getByLabel(/Description/i).fill(`Playwright ${mode} wallet creation coverage`); diff --git a/e2e/tests/rejected-signing.spec.ts b/e2e/tests/rejected-signing.spec.ts index 1e04d210..bd9f4375 100644 --- a/e2e/tests/rejected-signing.spec.ts +++ b/e2e/tests/rejected-signing.spec.ts @@ -141,7 +141,6 @@ test.describe("rejected wallet signing", () => { ).toEqual([]); await page.goto(`/wallets/${wallet.walletId}/transactions`); - await page.waitForLoadState("networkidle", { timeout: 30_000 }).catch(() => {}); await expect(page.locator('[data-testid^="tx-card-"]')).toHaveCount(0); }); diff --git a/e2e/tests/responsive-smoke.spec.ts b/e2e/tests/responsive-smoke.spec.ts index eb4199d1..ff87a8bd 100644 --- a/e2e/tests/responsive-smoke.spec.ts +++ b/e2e/tests/responsive-smoke.spec.ts @@ -67,7 +67,6 @@ for (const viewport of VIEWPORTS) { await expect( page.getByRole("link", { name: "New Wallet" }), ).toBeVisible({ timeout: 60_000 }); - await page.waitForLoadState("networkidle", { timeout: 30_000 }).catch(() => {}); await expectNoHorizontalOverflow(page, "wallet list"); }); diff --git a/e2e/tests/ring-transfer.spec.ts b/e2e/tests/ring-transfer.spec.ts index 943fddbb..264b7084 100644 --- a/e2e/tests/ring-transfer.spec.ts +++ b/e2e/tests/ring-transfer.spec.ts @@ -162,7 +162,6 @@ async function waitForUtxoSelectorLoaded( for (let attempt = 1; attempt <= 3; attempt += 1) { diagnostics.push(`loading new transaction page attempt ${attempt}`); await page.goto(newTransactionUrl); - await page.waitForLoadState("networkidle", { timeout: 60_000 }).catch(() => {}); const loaded = await page .waitForSelector('[data-testid="utxo-selector"][data-loaded="true"]', { @@ -414,7 +413,6 @@ test.describe("ring transfer", () => { const transactionsUrl = `/wallets/${srcWallet.walletId}/transactions`; await page.goto(transactionsUrl); await expect(page).toHaveURL(new RegExp(`/wallets/${srcWallet.walletId}/transactions$`)); - await page.waitForLoadState("networkidle", { timeout: 30_000 }).catch(() => {}); // Extract the newly created transaction's ID from the first pending tx card const txCard = page.locator('[data-testid^="tx-card-"]').first(); @@ -448,7 +446,6 @@ test.describe("ring transfer", () => { await authenticateAs(page, 1); await page.goto(`/wallets/${srcWallet.walletId}/transactions`); - await page.waitForLoadState("networkidle", { timeout: 30_000 }).catch(() => {}); // Confirm the tx card is still pending (proposer signed, threshold not yet met) await page.waitForSelector(`[data-testid="tx-card-${transactionId}"]`, { diff --git a/scripts/ci/README.md b/scripts/ci/README.md index 3b50311a..9489da50 100644 --- a/scripts/ci/README.md +++ b/scripts/ci/README.md @@ -197,7 +197,7 @@ Runs after the early discovery and ADA route-health checks, before request-heavy Runs when both `legacy` and `sdk` wallets are in context. Requires `CI_DREP_ANCHOR_URL`. -For each wallet type the scenario runs a pre-hygiene step followed by two sequential phases — register then retire — leaving the wallet in its pre-test DRep state: +For each wallet type the scenario runs a pre-hygiene step followed by two sequential phases — register then retire — leaving the wallet in its pre-test DRep state. The legacy and SDK wallet branches run in parallel because they spend from distinct multisig wallets, while each branch keeps its register-before-retire ordering: **Pre-hygiene step** — checks on-chain DRep state via `GET /api/v1/drepInfo`. If the DRep is already registered (e.g. from a previous incomplete run), it proposes a `retire` tx, signs with both signers, and waits for on-chain confirmation. If the broadcast is rejected with `DRepNotRegistered` or similar errors, the credential is treated as already clean (stale Blockfrost cache false-positive) and the step succeeds silently. @@ -208,7 +208,7 @@ For each wallet type the scenario runs a pre-hygiene step followed by two sequen 3. Signer 1 (`CI_MNEMONIC_2`, index 1) adds a payment-key witness, no broadcast. 4. Signer 2 (`CI_MNEMONIC_3`, index 2) adds a payment-key witness and broadcasts. 5. Assert the transaction is cleared from pending. -6. Poll `freeUtxos?fresh=true` until the spent inputs are no longer unspent on-chain (confirms block inclusion before the next phase). Up to 30 retries × 8 s = 4 minutes. +6. Poll `freeUtxos?fresh=true` until the spent inputs are no longer unspent on-chain (confirms block inclusion before the next phase). Up to 48 retries × 5 s = 4 minutes. 7. Repeat steps 1–6 with `action: "retire"`. **Why payment-key witnesses are sufficient for DRep cert:** diff --git a/scripts/ci/scenarios/flows/utxoShapeFlow.ts b/scripts/ci/scenarios/flows/utxoShapeFlow.ts index 6fa0d5c6..45cc7760 100644 --- a/scripts/ci/scenarios/flows/utxoShapeFlow.ts +++ b/scripts/ci/scenarios/flows/utxoShapeFlow.ts @@ -94,8 +94,8 @@ async function pollUntilUtxosConsumed(args: { maxRetries?: number; retryDelayMs?: number; }): Promise<{ attempts: number }> { - const maxRetries = args.maxRetries ?? 30; - const retryDelayMs = args.retryDelayMs ?? 8000; + const maxRetries = args.maxRetries ?? 48; + const retryDelayMs = args.retryDelayMs ?? 5000; const spent = new Set(args.spentUtxoRefs.map(key)); for (let attempt = 0; attempt < maxRetries; attempt++) { if (attempt > 0) { diff --git a/scripts/ci/scenarios/steps/certificates.ts b/scripts/ci/scenarios/steps/certificates.ts index b86f1d6a..bf051e4a 100644 --- a/scripts/ci/scenarios/steps/certificates.ts +++ b/scripts/ci/scenarios/steps/certificates.ts @@ -42,7 +42,7 @@ async function fetchUtxoRefs(args: { * the result. This confirms the cert tx has been included in a block and its * inputs are no longer unspent on-chain. * - * Preprod block time is ~20 s. We retry every 8 s for up to 4 minutes. + * Preprod block time is ~20 s. We retry every 5 s for up to 4 minutes. */ async function pollUntilUtxosConsumed(args: { ctx: CIBootstrapContext; @@ -54,8 +54,8 @@ async function pollUntilUtxosConsumed(args: { retryDelayMs?: number; }): Promise<{ attempts: number }> { const { ctx, walletId, token, botAddress, spentUtxoRefs } = args; - const maxRetries = args.maxRetries ?? 30; - const retryDelayMs = args.retryDelayMs ?? 8000; + const maxRetries = args.maxRetries ?? 48; + const retryDelayMs = args.retryDelayMs ?? 5000; const spentKeys = new Set(spentUtxoRefs.map((r) => `${r.txHash}:${r.outputIndex}`)); for (let attempt = 0; attempt < maxRetries; attempt++) { @@ -78,7 +78,7 @@ async function pollUntilUtxosConsumed(args: { } } throw new Error( - `Timed out after ${maxRetries} attempts (${(maxRetries * (args.retryDelayMs ?? 8000)) / 1000}s) waiting for cert tx inputs to be confirmed on-chain`, + `Timed out after ${maxRetries} attempts (${(maxRetries * (args.retryDelayMs ?? 5000)) / 1000}s) waiting for cert tx inputs to be confirmed on-chain`, ); } @@ -527,53 +527,60 @@ export function createScenarioDRepCertificates(): Scenario { id: "scenario.drep-certificates", description: "Register and retire DRep for legacy and SDK wallets, restoring pre-test state", - steps: [ - // Legacy: hygiene (deregister if already registered) - createDRepHygieneStep("legacy"), - // Legacy: register - ...createCertPhaseSteps({ - idPrefix: "v1.botDRepCertificate.legacy.register", - walletType: "legacy", - certEndpoint: "botDRepCertificate", - action: "register", - label: "DRep registration (legacy)", - runtime: legacyReg, - requireBroadcastSuccess: true, - buildExtraBody: () => buildDRepRegBody(), - }), - // Legacy: retire - ...createCertPhaseSteps({ - idPrefix: "v1.botDRepCertificate.legacy.retire", - walletType: "legacy", - certEndpoint: "botDRepCertificate", - action: "retire", - label: "DRep retirement (legacy)", - runtime: legacyRetire, - requireBroadcastSuccess: true, - }), - // SDK: hygiene (deregister if already registered) - createDRepHygieneStep("sdk"), - // SDK: register - ...createCertPhaseSteps({ - idPrefix: "v1.botDRepCertificate.sdk.register", - walletType: "sdk", - certEndpoint: "botDRepCertificate", - action: "register", - label: "DRep registration (sdk)", - runtime: sdkReg, - requireBroadcastSuccess: true, - buildExtraBody: () => buildDRepRegBody(), - }), - // SDK: retire - ...createCertPhaseSteps({ - idPrefix: "v1.botDRepCertificate.sdk.retire", - walletType: "sdk", - certEndpoint: "botDRepCertificate", - action: "retire", - label: "DRep retirement (sdk)", - runtime: sdkRetire, - requireBroadcastSuccess: true, - }), + steps: [], + parallelBranches: [ + { + id: "drep-certificates.legacy", + description: "DRep certificate lifecycle (legacy)", + steps: [ + createDRepHygieneStep("legacy"), + ...createCertPhaseSteps({ + idPrefix: "v1.botDRepCertificate.legacy.register", + walletType: "legacy", + certEndpoint: "botDRepCertificate", + action: "register", + label: "DRep registration (legacy)", + runtime: legacyReg, + requireBroadcastSuccess: true, + buildExtraBody: () => buildDRepRegBody(), + }), + ...createCertPhaseSteps({ + idPrefix: "v1.botDRepCertificate.legacy.retire", + walletType: "legacy", + certEndpoint: "botDRepCertificate", + action: "retire", + label: "DRep retirement (legacy)", + runtime: legacyRetire, + requireBroadcastSuccess: true, + }), + ], + }, + { + id: "drep-certificates.sdk", + description: "DRep certificate lifecycle (SDK)", + steps: [ + createDRepHygieneStep("sdk"), + ...createCertPhaseSteps({ + idPrefix: "v1.botDRepCertificate.sdk.register", + walletType: "sdk", + certEndpoint: "botDRepCertificate", + action: "register", + label: "DRep registration (sdk)", + runtime: sdkReg, + requireBroadcastSuccess: true, + buildExtraBody: () => buildDRepRegBody(), + }), + ...createCertPhaseSteps({ + idPrefix: "v1.botDRepCertificate.sdk.retire", + walletType: "sdk", + certEndpoint: "botDRepCertificate", + action: "retire", + label: "DRep retirement (sdk)", + runtime: sdkRetire, + requireBroadcastSuccess: true, + }), + ], + }, ], }; } diff --git a/scripts/ci/scenarios/steps/proxyBot.ts b/scripts/ci/scenarios/steps/proxyBot.ts index ca3334c3..3712a0ad 100644 --- a/scripts/ci/scenarios/steps/proxyBot.ts +++ b/scripts/ci/scenarios/steps/proxyBot.ts @@ -321,8 +321,8 @@ async function pollUntilUtxosConsumed(args: { maxRetries?: number; retryDelayMs?: number; }): Promise<{ attempts: number }> { - const maxRetries = args.maxRetries ?? 30; - const retryDelayMs = args.retryDelayMs ?? 8000; + const maxRetries = args.maxRetries ?? 48; + const retryDelayMs = args.retryDelayMs ?? 5000; const spent = new Set(args.spentUtxoRefs.map(key)); for (let attempt = 0; attempt < maxRetries; attempt++) { if (attempt > 0) { diff --git a/src/__tests__/ciScenarioManifest.test.ts b/src/__tests__/ciScenarioManifest.test.ts index 0fae816f..9d8632be 100644 --- a/src/__tests__/ciScenarioManifest.test.ts +++ b/src/__tests__/ciScenarioManifest.test.ts @@ -60,6 +60,34 @@ describe("route-chain scenario manifest", () => { expect(scenarios.map((scenario) => scenario.id)).toEqual(["scenario.create-wallet"]); }); + it("runs legacy and SDK DRep lifecycles as independent parallel branches", () => { + const [scenario] = getScenarioManifest(mkContext(["legacy", "sdk"]), [ + "scenario.drep-certificates", + ]); + + expect(scenario?.steps).toEqual([]); + expect(scenario?.parallelBranches?.map((branch) => branch.id)).toEqual([ + "drep-certificates.legacy", + "drep-certificates.sdk", + ]); + expect(scenario?.parallelBranches?.[0]?.steps.map((step) => step.id)).toEqual([ + "v1.botDRepCertificate.legacy.hygiene", + "v1.botDRepCertificate.legacy.register.propose", + "v1.botDRepCertificate.legacy.register.pending", + "v1.botDRepCertificate.legacy.register.sign.signer1", + "v1.botDRepCertificate.legacy.register.sign.signer2", + "v1.botDRepCertificate.legacy.register.cleared", + "v1.botDRepCertificate.legacy.register.onchain", + "v1.botDRepCertificate.legacy.retire.propose", + "v1.botDRepCertificate.legacy.retire.pending", + "v1.botDRepCertificate.legacy.retire.sign.signer1", + "v1.botDRepCertificate.legacy.retire.sign.signer2", + "v1.botDRepCertificate.legacy.retire.cleared", + "v1.botDRepCertificate.legacy.retire.onchain", + ]); + expect(scenario?.parallelBranches?.[1]?.steps).toHaveLength(13); + }); + it("still fails clearly when ring transfer is requested without all wallet types", () => { expect(() => getScenarioManifest(mkContext(["legacy"]), ["scenario.real-transfer-and-sign"]),