From f2a3371656341f3de9300a83ab92101483df7d07 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Wed, 22 Jul 2026 00:14:33 -0700 Subject: [PATCH] test(ui): isolate full chat-pane lifecycle tests to fix core-runtime-media-ui flake (#112566) The core-runtime-media-ui shard runs its ui config non-isolated for speed, but the full chat-pane lifecycle tests instantiate the pane component, which relies on chat-thread/chat-message module-level singletons (thread-state map, confirmation dismisser WeakMap, module-scoped document context-menu listeners) and spies on those modules. Under the shared non-isolated graph a stateful predecessor file can leave those modules duplicated, so the pane binds to a different instance than the test's spy/registry -- producing order-dependent flakes: `removeEventListener`-not-called teardown assertions or 120s session-lifecycle hangs. Reproduced deterministically on a Linux Node 24 Testbox (MAX_WORKERS=1); isolating a single file only shifted the failure to a sibling pane test, so the whole full-pane family shares the fragility. Route the 7 full-pane lifecycle test files through a new isolated jsdom lane (vitest.ui-isolated.config.ts, isolate: true) for a fresh module graph; the other ~370 ui tests stay fast and non-isolated. Registered in both shard registries (ci-node-test-plan.mjs, vitest.test-shards.mjs) and excluded from vitest.ui.config.ts. Verified on Testbox: the deterministic single-worker media-ui shard goes from failing to 5182 passed / 0 failed. Test-infrastructure only; no product code changes. --- scripts/lib/ci-node-test-plan.mjs | 1 + test/scripts/ci-node-test-plan.test.ts | 1 + test/scripts/test-projects.test.ts | 1 + test/vitest/vitest.test-shards.mjs | 1 + test/vitest/vitest.ui-isolated.config.ts | 23 +++++++++++++++++++++++ test/vitest/vitest.ui.config.ts | 22 +++++++++++++++++++++- 6 files changed, 48 insertions(+), 1 deletion(-) create mode 100644 test/vitest/vitest.ui-isolated.config.ts diff --git a/scripts/lib/ci-node-test-plan.mjs b/scripts/lib/ci-node-test-plan.mjs index 9fa7b2676b14..f5d063ebc6f3 100644 --- a/scripts/lib/ci-node-test-plan.mjs +++ b/scripts/lib/ci-node-test-plan.mjs @@ -1129,6 +1129,7 @@ const SPLIT_NODE_SHARDS = new Map([ "test/vitest/vitest.media-understanding.config.ts", "test/vitest/vitest.tui.config.ts", "test/vitest/vitest.ui.config.ts", + "test/vitest/vitest.ui-isolated.config.ts", "test/vitest/vitest.wizard.config.ts", ], requiresDist: false, diff --git a/test/scripts/ci-node-test-plan.test.ts b/test/scripts/ci-node-test-plan.test.ts index 4c1358936952..51500b1505cb 100644 --- a/test/scripts/ci-node-test-plan.test.ts +++ b/test/scripts/ci-node-test-plan.test.ts @@ -664,6 +664,7 @@ describe("scripts/lib/ci-node-test-plan.mjs", () => { "test/vitest/vitest.media-understanding.config.ts", "test/vitest/vitest.tui.config.ts", "test/vitest/vitest.ui.config.ts", + "test/vitest/vitest.ui-isolated.config.ts", "test/vitest/vitest.wizard.config.ts", ], requiresDist: false, diff --git a/test/scripts/test-projects.test.ts b/test/scripts/test-projects.test.ts index dc57b23baa70..76c8acda9dfb 100644 --- a/test/scripts/test-projects.test.ts +++ b/test/scripts/test-projects.test.ts @@ -4731,6 +4731,7 @@ describe("scripts/test-projects full-suite sharding", () => { "test/vitest/vitest.tui.config.ts", "test/vitest/vitest.tui-pty.config.ts", "test/vitest/vitest.ui.config.ts", + "test/vitest/vitest.ui-isolated.config.ts", "test/vitest/vitest.utils.config.ts", "test/vitest/vitest.wizard.config.ts", "test/vitest/vitest.gateway-core.config.ts", diff --git a/test/vitest/vitest.test-shards.mjs b/test/vitest/vitest.test-shards.mjs index 52dbda6b6233..d3982475ed24 100644 --- a/test/vitest/vitest.test-shards.mjs +++ b/test/vitest/vitest.test-shards.mjs @@ -84,6 +84,7 @@ export const fullSuiteVitestShards = [ "test/vitest/vitest.tui.config.ts", "test/vitest/vitest.tui-pty.config.ts", "test/vitest/vitest.ui.config.ts", + "test/vitest/vitest.ui-isolated.config.ts", "test/vitest/vitest.utils.config.ts", "test/vitest/vitest.wizard.config.ts", ], diff --git a/test/vitest/vitest.ui-isolated.config.ts b/test/vitest/vitest.ui-isolated.config.ts new file mode 100644 index 000000000000..f6e3f23a1785 --- /dev/null +++ b/test/vitest/vitest.ui-isolated.config.ts @@ -0,0 +1,23 @@ +// Vitest ui-isolated config runs jsdom ui tests that need a fresh module graph. +// The shared ui shard runs non-isolated for speed, but tests that spy on module +// internals and assert the component uses that spy must not share a module cache +// with stateful predecessor files (see UI_ISOLATED_TEST_FILES). +import { createScopedVitestConfig } from "./vitest.scoped-config.ts"; +import { jsdomOptimizedDeps } from "./vitest.shared.config.ts"; +import { UI_ISOLATED_TEST_FILES } from "./vitest.ui.config.ts"; + +export function createUiIsolatedVitestConfig(env?: Record) { + return createScopedVitestConfig(UI_ISOLATED_TEST_FILES, { + deps: jsdomOptimizedDeps, + environment: "jsdom", + env, + excludeUnitFastTests: false, + includeOpenClawRuntimeSetup: false, + isolate: true, + name: "ui-isolated", + setupFiles: ["ui/src/test-helpers/lit-warnings.setup.ts"], + useNonIsolatedRunner: false, + }); +} + +export default createUiIsolatedVitestConfig(); diff --git a/test/vitest/vitest.ui.config.ts b/test/vitest/vitest.ui.config.ts index 551fecdf5516..950c71f23ce0 100644 --- a/test/vitest/vitest.ui.config.ts +++ b/test/vitest/vitest.ui.config.ts @@ -2,12 +2,32 @@ import { createScopedVitestConfig } from "./vitest.scoped-config.ts"; import { jsdomOptimizedDeps } from "./vitest.shared.config.ts"; +// Full chat-pane lifecycle tests instantiate the pane component, which relies on +// chat-thread/chat-message module-level singletons (thread state maps, module-scoped +// document context-menu listeners) and spies on those modules. Under the non-isolated +// ui runner a stateful predecessor file can leave those modules duplicated across the +// shared graph, so the pane binds to a different instance than the test's spy/registry +// — surfacing as flaky teardown assertions or 120s session-lifecycle hangs, depending +// on file order. These tests run in the isolated ui lane for a fresh module graph; +// keep this list in sync with vitest.ui-isolated.config.ts's include. +export const UI_ISOLATED_TEST_FILES = [ + "ui/src/pages/chat/chat-pane-history.test.ts", + "ui/src/pages/chat/chat-pane-lifecycle.test.ts", + "ui/src/pages/chat/chat-pane-pull-requests.test.ts", + "ui/src/pages/chat/chat-pane.message-cut.test.ts", + "ui/src/pages/chat/chat-pane.read-marker.test.ts", + "ui/src/pages/chat/chat-pane.session-discussion.test.ts", + "ui/src/pages/chat/chat-pane.test.ts", +]; + export function createUiVitestConfig( env?: Record, options?: { includePatterns?: string[]; name?: string }, ) { const includePatterns = options?.includePatterns ?? ["ui/src/**/*.test.ts"]; - const exclude = options?.includePatterns ? [] : ["ui/src/**/*.e2e.test.ts"]; + const exclude = options?.includePatterns + ? [] + : ["ui/src/**/*.e2e.test.ts", ...UI_ISOLATED_TEST_FILES]; return createScopedVitestConfig(includePatterns, { deps: jsdomOptimizedDeps, environment: "jsdom",