From 922299ae8a9aed2dedca07097b122d8165d3a362 Mon Sep 17 00:00:00 2001 From: rookiestar28 Date: Sat, 18 Apr 2026 21:31:10 +0800 Subject: [PATCH] fix(e2e): recover harness after exhausted import retries --- tests/e2e/specs/sidebar.spec.js | 27 +++++++++++++++ tests/e2e/utils/helpers.js | 60 +++++++++++++++++++++++++++------ 2 files changed, 76 insertions(+), 11 deletions(-) diff --git a/tests/e2e/specs/sidebar.spec.js b/tests/e2e/specs/sidebar.spec.js index a21a9bd..d4a398f 100644 --- a/tests/e2e/specs/sidebar.spec.js +++ b/tests/e2e/specs/sidebar.spec.js @@ -118,4 +118,31 @@ test.describe('OpenClaw Sidebar', () => { .poll(() => page.evaluate(() => window.__openclawTestLoadAttempts)) .toBe(4); }); + + test('recovers when the first harness boot exhausts transient entry fetch retries', async ({ page }) => { + let remainingFailures = 4; + let totalEntryRequests = 0; + + await page.route('**/web/openclaw.js?openclaw_harness_attempt=*', async (route) => { + const url = new URL(route.request().url()); + if (url.pathname !== '/web/openclaw.js') { + await route.fallback(); + return; + } + + totalEntryRequests += 1; + if (remainingFailures > 0) { + remainingFailures -= 1; + await route.abort('failed'); + return; + } + + await route.fallback(); + }); + + await page.reload(); + await waitForOpenClawReady(page); + await expect(page.locator('.openclaw-title')).toHaveText('OpenClaw'); + await expect.poll(() => totalEntryRequests).toBe(5); + }); }); diff --git a/tests/e2e/utils/helpers.js b/tests/e2e/utils/helpers.js index 46932a6..3e8447e 100644 --- a/tests/e2e/utils/helpers.js +++ b/tests/e2e/utils/helpers.js @@ -24,6 +24,23 @@ function resolveUiTimeoutMs() { return 30_000; } +function resolveHarnessReloadBudget() { + const raw = process.env.OPENCLAW_E2E_HARNESS_RELOADS; + if (raw) { + const parsed = Number.parseInt(raw, 10); + if (Number.isInteger(parsed) && parsed >= 0) { + return parsed; + } + } + + return 1; +} + +function isTransientModuleFetchFailure(error) { + const message = String(error?.message || error || ''); + return message.includes('Failed to fetch dynamically imported module'); +} + function normalizeApiPath(pathname) { if (typeof pathname !== 'string') return ''; const stripped = pathname.startsWith('/api/') ? pathname.slice(4) : pathname; @@ -573,20 +590,41 @@ export async function mockRemoteAdminBaseline( export async function waitForOpenClawReady(page) { const timeoutMs = resolveUiTimeoutMs(); - await page.waitForFunction( - () => window.__openclawTestReady === true || window.__openclawTestError, - null, - { timeout: timeoutMs } - ); + const maxHarnessReloads = resolveHarnessReloadBudget(); + + for (let reloadAttempt = 0; reloadAttempt <= maxHarnessReloads; reloadAttempt += 1) { + await page.waitForFunction( + () => window.__openclawTestReady === true || window.__openclawTestError, + null, + { timeout: timeoutMs } + ); + + const [error, loadAttempts] = await Promise.all([ + page.evaluate(() => window.__openclawTestError), + page.evaluate(() => window.__openclawTestLoadAttempts || 0), + ]); + + if (!error) { + // Basic sanity: header + tab bar exists + await expect(page.locator('.openclaw-header')).toBeVisible(); + await expect(page.locator('.openclaw-tabs')).toBeVisible(); + return; + } + + // IMPORTANT: only recover via full-page reload after the in-page harness + // has already exhausted its own transient-fetch retry budget. This keeps + // CI-only fetch flakes recoverable without masking real module/runtime bugs. + if ( + isTransientModuleFetchFailure(error) && + loadAttempts >= 4 && + reloadAttempt < maxHarnessReloads + ) { + await page.reload({ waitUntil: 'load' }); + continue; + } - const error = await page.evaluate(() => window.__openclawTestError); - if (error) { throw new Error(`OpenClaw test harness failed to load: ${error?.message || error}`); } - - // Basic sanity: header + tab bar exists - await expect(page.locator('.openclaw-header')).toBeVisible(); - await expect(page.locator('.openclaw-tabs')).toBeVisible(); } export async function clickTab(page, title) {