From 086319da69f93af5a2bf3b5af89aeff445ac00ef Mon Sep 17 00:00:00 2001 From: saltbo Date: Wed, 3 Jun 2026 14:19:17 -0400 Subject: [PATCH] test: harden cloud e2e tunnel handling --- e2e/cloud-store.spec.ts | 51 +++++++++++++++++++++++++++++++++------ scripts/run-cloud-e2e.mjs | 18 +++++++++++++- 2 files changed, 60 insertions(+), 9 deletions(-) diff --git a/e2e/cloud-store.spec.ts b/e2e/cloud-store.spec.ts index ead65e2d..d560d103 100644 --- a/e2e/cloud-store.spec.ts +++ b/e2e/cloud-store.spec.ts @@ -299,8 +299,7 @@ async function createGiftCard(page: Page) { } async function createStoragePlanThroughUi(page: Page, packageName: string) { - await page.goto('/admin/cloud-store') - await expect(page.getByRole('heading', { name: 'Storage Plans' })).toBeVisible({ timeout: 20_000 }) + await gotoAdminCloudStore(page) await page.getByRole('button', { name: 'New plan' }).click() const dialog = page.getByRole('dialog', { name: 'New plan' }) await dialog.getByLabel('Plan name').fill(packageName) @@ -318,8 +317,7 @@ async function createStoragePlanThroughUi(page: Page, packageName: string) { } async function createCreditPackageThroughUi(page: Page, packageName: string) { - await page.goto('/admin/cloud-store') - await expect(page.getByRole('heading', { name: 'Storage Plans' })).toBeVisible({ timeout: 20_000 }) + await gotoAdminCloudStore(page) await page.getByRole('button', { name: 'New Credits package' }).click() const dialog = page.getByRole('dialog', { name: 'New Credits package' }) await dialog.getByLabel('Name').fill(packageName) @@ -336,8 +334,7 @@ async function createCreditPackageThroughUi(page: Page, packageName: string) { } async function createGiftCardThroughUi(page: Page) { - await page.goto('/admin/cloud-store') - await expect(page.getByRole('heading', { name: 'Storage Plans' })).toBeVisible({ timeout: 20_000 }) + await gotoAdminCloudStore(page) await page.getByRole('tab', { name: 'Gift Cards' }).click() await page.getByRole('button', { name: 'Generate gift cards' }).click() const dialog = page.getByRole('dialog', { name: 'Generate gift cards' }) @@ -356,6 +353,38 @@ async function createGiftCardThroughUi(page: Page) { return card.code } +async function gotoAdminCloudStore(page: Page) { + await expect + .poll( + async () => { + try { + await Promise.all([ + getJson(page, '/api/admin/store/settings'), + getJson(page, '/api/admin/store/packages'), + getJson(page, '/api/admin/store/credits/products'), + ]) + return true + } catch { + return false + } + }, + { timeout: 60_000 }, + ) + .toBe(true) + + for (let attempt = 0; attempt < 3; attempt += 1) { + await page.goto('/admin/cloud-store', { waitUntil: 'domcontentloaded' }) + const heading = page.getByRole('heading', { name: 'Storage Plans' }) + try { + await expect(heading).toBeVisible({ timeout: 20_000 }) + return + } catch (error) { + if (attempt === 2) throw error + await page.waitForTimeout(1500) + } + } +} + async function expectAdminProductVisibleInApi(page: Page, packageName: string) { await expect .poll( @@ -456,7 +485,7 @@ async function putJson(page: Page, url: string, data?: unknown): Promise { } async function browserJson(page: Page, method: 'GET' | 'POST' | 'PUT', url: string, data?: unknown): Promise { - const retryDelays = [500, 1000] + const retryDelays = [500, 1000, 3000, 7000, 15000] const stripeRateLimitRetryDelays = [1000, 3000, 7000, 15000, 30000] for (let attempt = 0; attempt <= stripeRateLimitRetryDelays.length; attempt += 1) { try { @@ -497,5 +526,11 @@ function isStripeRateLimitBrowserJsonError(error: unknown) { function isTransientBrowserJsonError(error: unknown) { if (!(error instanceof Error)) return false - return error.message.includes('Failed to fetch') || error.message.includes('Execution context was destroyed') + return ( + error.message.includes('Failed to fetch') || + error.message.includes('Execution context was destroyed') || + error.message.includes('Incoming request ended abruptly') || + error.message.includes('context canceled') || + error.message.includes('Load failed') + ) } diff --git a/scripts/run-cloud-e2e.mjs b/scripts/run-cloud-e2e.mjs index a39fe22c..9ebfe354 100644 --- a/scripts/run-cloud-e2e.mjs +++ b/scripts/run-cloud-e2e.mjs @@ -107,7 +107,23 @@ function s3MockEnv() { } } -function startTunnel(target) { +async function startTunnel(target) { + const maxAttempts = Number(process.env.E2E_TUNNEL_START_ATTEMPTS ?? 3) + let lastError = null + for (let attempt = 1; attempt <= maxAttempts; attempt += 1) { + try { + if (attempt > 1) console.log(`Retrying cloudflared quick tunnel startup (${attempt}/${maxAttempts})...`) + return await startTunnelOnce(target) + } catch (error) { + lastError = error + if (attempt === maxAttempts) break + await new Promise((resolve) => setTimeout(resolve, attempt * 3000)) + } + } + throw lastError +} + +function startTunnelOnce(target) { const child = spawn(cloudflared, ['tunnel', '--url', target, '--no-autoupdate'], { stdio: ['ignore', 'pipe', 'pipe'], })