Commit 4b9d141b47d for woocommerce
commit 4b9d141b47de5ce1ac6f9d19c7711dca68ad1451
Author: Vlad Olaru <vlad.olaru@automattic.com>
Date: Mon Sep 14 13:07:06 2026 +0300
[tests] Stop the Settings UI E2E spec from corrupting wp-env's cache (#68684)
* fix(e2e): Queue wpCLI calls so they cannot corrupt wp-env's cache
Two wp-env commands running at once can leave the environment unusable
for the rest of a CI job. Every wp-env command rewrites
wp-env-cache.json in the environment's work directory, and cache.js does
it by reading the file, adding a key and writing the whole object back,
with no locking and no atomic replace. When one command truncates the
file while the other is reading it, the reader's JSON.parse throws,
getCacheFile swallows the error and returns an empty object, and the
reader then writes that empty object back with only its own key. The
runtime key is gone.
After that, wp-env refuses every command -- run and destroy alike --
with "Environment not initialized. Run `wp-env start` first.", because
detectRuntime finds no runtime in the cache. Nothing restores the key
but wp-env start, so the rest of the job's CLI calls fail while the site
itself stays up and browser assertions keep passing.
settings-ui-feature-flag.spec.ts is the only spec that runs two wpCLI
calls concurrently, through a Promise.all, and it is where this has been
showing up: two tests fail there, then later specs fail on unrelated
option reads and plugin activations. Nothing about the helper warns that
parallel calls are unsafe, so fix it in the helper rather than at the
one call site: queue the calls, and let a rejected call pass the queue
on rather than poisoning it.
The underlying bug belongs to @wordpress/env and is worth reporting
upstream. Queueing costs a couple of seconds per overlapping pair and
removes the whole failure mode here today.
Verified against a local e2e environment with a harness that fires two
wpCLI calls through Promise.all and checks the cache after each round.
On trunk's helper the runtime key was destroyed on round 4 of 10; with
this change, 25 rounds left it intact. settings-ui-feature-flag.spec.ts
and product-permalinks.spec.ts pass twice each.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(e2e): Replace the wpCLI queue with sequential option reads
The previous commit queued every wpCLI call in the helper so two wp-env
processes could not race on wp-env-cache.json. That queue only
serializes calls inside one Node process. It never covered other
Playwright workers, utils/wordpress.ts, the blocks wpCLI helper or the
shell setup scripts, and it added a failure mode of its own: a hung
wp-env call would block every later wpCLI call in the worker, afterAll
cleanup included.
The only place in the suite that runs two wpCLI calls at once is the
Promise.all in settings-ui-feature-flag.spec.ts, and core-serial runs a
single worker, so nothing else can overlap there. Restore the helper to
its trunk shape, read the two options with sequential awaits, and warn
in the wpCLI docblock that calls must never overlap.
A standalone harness calling wp-env 11.9.0's setCache from two
processes at the same instant lost the runtime key in 7 of 100 rounds,
leaving the same cache contents seen in CI.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* chore(changelog): Describe the sequential Settings UI option reads
The changelog entry still described queueing wpCLI calls in the
helper. Rewrite it for the spec-level fix, and move the text into a
Comment since a test-only change ships no user-facing line.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* docs(e2e): Warn against overlapping wp-env commands in E2E specs
wp-env rewrites wp-env-cache.json without locking, so two wp-env
commands running at once can drop its runtime key. Every later wp-env
command, run and destroy included, then fails with "Environment not
initialized" until the environment starts again, which breaks every
spec after the overlap in a CI job. settings-ui-feature-flag.spec.ts
fell into this through a Promise.all of two wpCLI calls.
Add a Gotchas subsection to the README's "Writing e2e tests" guide so
new specs await wp-env helpers in sequence, and note that overlapping a
single call with browser work is still fine.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
diff --git a/plugins/woocommerce/changelog/fix-e2e-wp-env-cli-cache-race b/plugins/woocommerce/changelog/fix-e2e-wp-env-cli-cache-race
new file mode 100644
index 00000000000..e942324335c
--- /dev/null
+++ b/plugins/woocommerce/changelog/fix-e2e-wp-env-cli-cache-race
@@ -0,0 +1,3 @@
+Significance: patch
+Type: dev
+Comment: Read the Settings UI E2E options one at a time so two wp-env processes cannot corrupt its cache; no production change.
diff --git a/plugins/woocommerce/tests/e2e/README.md b/plugins/woocommerce/tests/e2e/README.md
index a126290cdb6..2081c553575 100644
--- a/plugins/woocommerce/tests/e2e/README.md
+++ b/plugins/woocommerce/tests/e2e/README.md
@@ -169,6 +169,10 @@ Still, here's a few tips to get you started:
Playwright's Best Practices guide is a good
read: [Playwright Best Practices](https://playwright.dev/docs/best-practices).
+### Gotchas
+
+- **Never run two wp-env commands at once.** Await each `wpCLI` call (or any other helper that shells out to `wp-env`, such as `getInstalledWordPressVersion`) before starting the next, and keep them out of `Promise.all`. Every wp-env command rewrites `wp-env-cache.json` in the environment's work directory without locking, so two overlapping commands can drop its `runtime` key. From then on every wp-env command, `run` and `destroy` included, fails with "Environment not initialized. Run `wp-env start` first." until the environment starts again, so one overlap breaks every later spec in the CI job. Overlapping a single `wpCLI` call with browser work such as `page.goto` is fine, since that doesn't start wp-env.
+
## Test helper plugins
Some E2E suites need fixture mechanisms that can't be expressed cleanly with REST or WP-CLI alone — for example, filter-driven content overrides, server-side event mirroring, or synchronous triggers for normally-scheduled jobs. These ship as small PHP plugins under `tests/e2e/test-plugins/`.
diff --git a/plugins/woocommerce/tests/e2e/tests/settings/settings-ui-feature-flag.spec.ts b/plugins/woocommerce/tests/e2e/tests/settings/settings-ui-feature-flag.spec.ts
index 88c33951943..2bf37d365a8 100644
--- a/plugins/woocommerce/tests/e2e/tests/settings/settings-ui-feature-flag.spec.ts
+++ b/plugins/woocommerce/tests/e2e/tests/settings/settings-ui-feature-flag.spec.ts
@@ -301,14 +301,13 @@ test.describe( 'Settings UI feature flag', { tag: [ tags.NOT_E2E ] }, () => {
await expect( preservedLowStock ).toHaveValue( '2' );
await expect( editedHoldStock ).toHaveValue( '61' );
- const [ holdStockOption, lowStockOption ] = await Promise.all( [
- wpCLI(
- 'wp option get woocommerce_hold_stock_minutes --skip-plugins'
- ),
- wpCLI(
- 'wp option get woocommerce_notify_low_stock_amount --skip-plugins'
- ),
- ] );
+ // One at a time: overlapping wp-env processes can corrupt its cache (see `wpCLI`).
+ const holdStockOption = await wpCLI(
+ 'wp option get woocommerce_hold_stock_minutes --skip-plugins'
+ );
+ const lowStockOption = await wpCLI(
+ 'wp option get woocommerce_notify_low_stock_amount --skip-plugins'
+ );
expect( holdStockOption.stdout.trim() ).toBe( '61' );
expect( lowStockOption.stdout.trim() ).toBe( '2' );
} );
diff --git a/plugins/woocommerce/tests/e2e/utils/cli.ts b/plugins/woocommerce/tests/e2e/utils/cli.ts
index 9e3ce6251a9..d2d653418fa 100644
--- a/plugins/woocommerce/tests/e2e/utils/cli.ts
+++ b/plugins/woocommerce/tests/e2e/utils/cli.ts
@@ -9,6 +9,8 @@ const execFileAsync = promisify( execFile );
/**
* Runs a command in the E2E CLI container. Use an argument array when the command contains dynamic values.
+ *
+ * Await each call before starting the next, never through `Promise.all`: wp-env rewrites its cache file without locking, and two overlapping calls can drop its `runtime` key so every later wp-env command fails with "Environment not initialized".
*/
const wpCLI = async ( command: string | string[] ) => {
const { stdout, stderr } = Array.isArray( command )