From 8d873fbb23553699cfaf3ca61cf7d78c9816dd94 Mon Sep 17 00:00:00 2001 From: Gio Lodi Date: Mon, 6 Jul 2026 16:49:22 +1000 Subject: [PATCH 1/2] fix(e2e): stop wp-env warm-up JSON parse flakes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit E2E runs failed intermittently when the wp-env REST endpoints returned a non-JSON body during warm-up — a PHP notice printed into the response (`
Warning...`) or a transient `500` — and `response.json()` threw. Because editor settings are cached in module scope only on success, a single early hiccup poisoned the first fetch and failed every early spec while later ones passed: the flaky signature. Two complementary fixes: - Set `WP_DEBUG_DISPLAY: false` so PHP notices never print into REST bodies. `WP_DEBUG_LOG` stays on, so notices still land in `debug.log`. - Route the settings and assets fetches through a `fetchJson` helper that reads the body as text, retries transient `5xx`/parse failures, and surfaces a body snippet in the final error instead of an opaque `Unexpected token '<'`. --- Generated with the help of Claude Code, https://claude.com/claude-code Co-Authored-By: Claude Opus 4.8 (1M context) --- .wp-env.json | 1 + e2e/wp-env-fixtures.js | 89 +++++++++++++++++++++++++++++++----------- 2 files changed, 67 insertions(+), 23 deletions(-) diff --git a/.wp-env.json b/.wp-env.json index 60fac31da..afed5482f 100644 --- a/.wp-env.json +++ b/.wp-env.json @@ -9,6 +9,7 @@ "config": { "WP_DEBUG": true, "WP_DEBUG_LOG": true, + "WP_DEBUG_DISPLAY": false, "WP_ENVIRONMENT_TYPE": "local" } } diff --git a/e2e/wp-env-fixtures.js b/e2e/wp-env-fixtures.js index fd1a5483e..eb2070721 100644 --- a/e2e/wp-env-fixtures.js +++ b/e2e/wp-env-fixtures.js @@ -30,6 +30,64 @@ let cachedEditorSettings = null; /** Cached editor assets — fetched once and reused across all tests. */ let cachedEditorAssets = null; +const MAX_FETCH_ATTEMPTS = 5; +const FETCH_RETRY_DELAY_MS = 1000; + +const delay = ( ms ) => new Promise( ( resolve ) => setTimeout( resolve, ms ) ); + +/** + * Fetch and parse JSON from a wp-env REST endpoint, retrying on transient + * failures. + * + * During warm-up the Playground/wp-env instance can briefly return a 5xx or + * leak a PHP notice into the body, which corrupts the JSON. Both surface here + * as errors we retry, and the last failure carries a snippet of the body so a + * genuine leak is diagnosable instead of an opaque "Unexpected token '<'". + * + * @param {string} url Fully-qualified REST URL. + * @param {Object} creds Credentials object from readCredentials(). + * @param {string} label Human-readable resource name for error messages. + * @return {Promise} The parsed JSON response. + */ +async function fetchJson( url, creds, label ) { + let lastError; + + for ( let attempt = 1; attempt <= MAX_FETCH_ATTEMPTS; attempt++ ) { + try { + const response = await fetch( url, { + headers: { Authorization: creds.authHeader }, + } ); + + const body = await response.text(); + + if ( ! response.ok ) { + throw new Error( + `HTTP ${ response.status } ${ + response.statusText + } — ${ body.slice( 0, 200 ) }` + ); + } + + try { + return JSON.parse( body ); + } catch { + throw new Error( + `response was not valid JSON — ${ body.slice( 0, 200 ) }` + ); + } + } catch ( error ) { + lastError = error; + if ( attempt < MAX_FETCH_ATTEMPTS ) { + await delay( FETCH_RETRY_DELAY_MS ); + } + } + } + + throw new Error( + `Failed to fetch ${ label } after ${ MAX_FETCH_ATTEMPTS } attempts: ${ lastError.message }` + ); +} + /** * Fetch editor settings from the wp-env WordPress instance. * @@ -44,20 +102,11 @@ async function fetchEditorSettings( creds ) { return cachedEditorSettings; } - const response = await fetch( + cachedEditorSettings = await fetchJson( `${ creds.siteApiRoot }wp-block-editor/v1/settings`, - { - headers: { Authorization: creds.authHeader }, - } + creds, + 'editor settings' ); - - if ( ! response.ok ) { - throw new Error( - `Failed to fetch editor settings: ${ response.status } ${ response.statusText }` - ); - } - - cachedEditorSettings = await response.json(); return cachedEditorSettings; } @@ -75,17 +124,11 @@ async function fetchEditorAssets( creds ) { const url = new URL( `${ creds.siteApiRoot }wpcom/v2/editor-assets` ); url.searchParams.set( 'exclude', 'core,gutenberg' ); - const response = await fetch( url.toString(), { - headers: { Authorization: creds.authHeader }, - } ); - - if ( ! response.ok ) { - throw new Error( - `Failed to fetch editor assets: ${ response.status } ${ response.statusText }` - ); - } - - cachedEditorAssets = await response.json(); + cachedEditorAssets = await fetchJson( + url.toString(), + creds, + 'editor assets' + ); return cachedEditorAssets; } From 4fed003930d393c3091ee0735f7ccd5b02442637 Mon Sep 17 00:00:00 2001 From: Gio Lodi Date: Mon, 6 Jul 2026 17:02:12 +1000 Subject: [PATCH 2/2] fix(e2e): gate suite on WordPress leaving maintenance MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CI surfaced the real warm-up failure the earlier retry only papered over: the Playground runtime keeps installing plugins (Blueprint steps) after it starts serving requests. During those installs WordPress drops a `.maintenance` file and returns a `503`, and there is a race where the file is removed mid-request and PHP fatals — both corrupt the first editor REST call. Gate provisioning in `wp-env-setup.sh` on the settings endpoint returning valid JSON, so the suite never starts against a half-provisioned instance. Raise the in-test fetch retry budget (5s -> 30s) as a backstop for any mid-run blip. --- Generated with the help of Claude Code, https://claude.com/claude-code Co-Authored-By: Claude Opus 4.8 (1M context) --- bin/wp-env-setup.sh | 37 +++++++++++++++++++++++++++++++++++++ e2e/wp-env-fixtures.js | 4 ++-- 2 files changed, 39 insertions(+), 2 deletions(-) diff --git a/bin/wp-env-setup.sh b/bin/wp-env-setup.sh index 77e431813..e999a4d81 100755 --- a/bin/wp-env-setup.sh +++ b/bin/wp-env-setup.sh @@ -169,3 +169,40 @@ cat > "$CREDENTIALS_FILE" < data += chunk); + process.stdin.on('end', () => { + try { JSON.parse(data); } + catch { process.exit(1); } + }); + "; then + echo "Editor settings endpoint is ready." + break + fi + + if [ "$attempt" -eq "$READY_MAX_RETRIES" ]; then + echo "Error: editor settings endpoint did not return valid JSON after $((READY_MAX_RETRIES * READY_RETRY_INTERVAL)) seconds." + exit 1 + fi + + sleep $READY_RETRY_INTERVAL +done diff --git a/e2e/wp-env-fixtures.js b/e2e/wp-env-fixtures.js index eb2070721..b29c21982 100644 --- a/e2e/wp-env-fixtures.js +++ b/e2e/wp-env-fixtures.js @@ -30,8 +30,8 @@ let cachedEditorSettings = null; /** Cached editor assets — fetched once and reused across all tests. */ let cachedEditorAssets = null; -const MAX_FETCH_ATTEMPTS = 5; -const FETCH_RETRY_DELAY_MS = 1000; +const MAX_FETCH_ATTEMPTS = 15; +const FETCH_RETRY_DELAY_MS = 2000; const delay = ( ms ) => new Promise( ( resolve ) => setTimeout( resolve, ms ) );