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 )