From 982afd90b6aaa496662b6520c2ed224c7636fe3e Mon Sep 17 00:00:00 2001 From: Leo Li Date: Mon, 12 Oct 2026 03:44:19 -0700 Subject: [PATCH 1/5] test: webviews dev server files run at once, each on its own warm Vite cache #19812 ran every file that starts a dev server one at a time after the rest, so they never optimize into one node_modules/.vite at once; the web test phase went from 7m18s to 9m44s. Pin the faster contract: several at once, never on one cache, each but one per config starting from a warmed copy. Co-Authored-By: Claude Opus 5.5 --- tests/test_webviews_tests_per_file.py | 54 ++++++++++++++++++++++----- 1 file changed, 44 insertions(+), 10 deletions(-) diff --git a/tests/test_webviews_tests_per_file.py b/tests/test_webviews_tests_per_file.py index d41a94a071e9..c1f7e55fdd48 100644 --- a/tests/test_webviews_tests_per_file.py +++ b/tests/test_webviews_tests_per_file.py @@ -24,14 +24,28 @@ FAKE_BUN = """#!/usr/bin/env bash [ "$1" = test ] || exit 9 printf '%s\\n' "$2" >> "$BUN_CALLS" -# A file that starts a Vite dev server holds the shared optimizer cache while it runs. -if grep -q -e '"dev",' -e 'vite.config.gallery-dev.ts' "$2"; then - if mkdir "$DEV_SERVER_LOCK" 2> /dev/null; then +# A file that starts a Vite dev server optimizes dependencies into its Vite cache while it runs: two +# at once on one cache lose each other's files, and a cold cache is the slow first bundle. +if grep -q -e '"dev",' -e 'vite.config.gallery-dev.ts' -e 'vite.config.acpmux-pane.mjs' "$2"; then + config=default + grep -q 'vite.config.settings-page.mjs' "$2" && config=settings + grep -q 'vite.config.gallery-dev.ts' "$2" && config=gallery + grep -q 'vite.config.acpmux-pane.mjs' "$2" && config=pane + cache="${CMUX_VITE_CACHE_DIR:-$SHARED_VITE_CACHE}" + warm=cold + [ -e "$cache/warm-$config" ] && warm=warm + printf '%s %s %s\\n' "$2" "$cache" "$warm" >> "$BUN_DEV" + mkdir "$DEV_RUNNING/$$" + ls "$DEV_RUNNING" | wc -l >> "$BUN_PEAK" + if mkdir "$cache.lock" 2> /dev/null; then sleep 0.5 - rmdir "$DEV_SERVER_LOCK" + mkdir -p "$cache" + touch "$cache/warm-$config" + rmdir "$cache.lock" else printf 'overlap %s\\n' "$2" >> "$BUN_OVERLAPS" fi + rmdir "$DEV_RUNNING/$$" fi case "$2" in *hang*) sleep 60 ;; @@ -77,9 +91,16 @@ def run_runner(self, files, without_timeout=False, contents=None): calls.write_text("", encoding="utf-8") self.overlaps = temp / "overlaps" self.overlaps.write_text("", encoding="utf-8") + self.dev, self.peak, running = temp / "dev", temp / "peak", temp / "running" + self.dev.write_text("", encoding="utf-8") + self.peak.write_text("", encoding="utf-8") + running.mkdir() + (web / "node_modules").mkdir() + self.shared_cache = web / "node_modules" / ".vite" system_path = path_without_timeout(temp) if without_timeout else os.environ["PATH"] env = {**os.environ, "PATH": f"{bin_dir}:{system_path}", "BUN_CALLS": str(calls), - "BUN_OVERLAPS": str(self.overlaps), "DEV_SERVER_LOCK": str(temp / "dev-server.lock"), + "BUN_OVERLAPS": str(self.overlaps), "SHARED_VITE_CACHE": str(web / "node_modules" / ".vite"), + "BUN_DEV": str(self.dev), "BUN_PEAK": str(self.peak), "DEV_RUNNING": str(running), "CMUX_WEB_TEST_FILE_TIMEOUT": "2", "CMUX_WEB_TEST_JOBS": "4"} started = time.monotonic() result = subprocess.run(["bash", str(RUNNER)], cwd=web, env=env, capture_output=True, text=True, timeout=40) @@ -113,19 +134,25 @@ def test_passing_files_pass(self): self.assertEqual(result.returncode, 0, result.stdout + result.stderr) self.assertEqual(calls, ["./src/b.test.tsx", "./test/a.test.ts"]) - def test_files_that_start_a_dev_server_never_run_at_once(self): - # editor-page, ui-a11y and text-field-focus start `vp dev` with the default config. Run at - # once on a fresh checkout, they optimize dependencies into one node_modules/.vite and the - # loser's page never loads: editor-page timed out on react-apps-check for #19746. + def test_dev_server_files_run_at_once_each_on_its_own_warm_vite_cache(self): + # Every file that starts a dev server once ran one at a time after the rest (#19812): on + # one node_modules/.vite two at once lost each other's optimized files, and editor-page + # timed out beside ui-a11y and text-field-focus (#19746). That lane added 2.5 minutes. Each + # file now has its own cache, copied from one warmed per config, so several run at once. default_server = 'spawn(vp, ["dev", "--port", String(port), "--strictPort"]);\n' settings_server = 'spawn(vp, [\n "dev",\n "--config",\n "vite.config.settings-page.mjs",\n]);\n' gallery_server = 'import config from "../vite.config.gallery-dev.ts";\n' + pane_server = 'spawn(vp, ["dev", "--config", "vite.config.acpmux-pane.mjs", "--port", String(port)]);\n' contents = { "test/editor-page.test.ts": default_server, "test/ui-a11y.test.ts": default_server, "test/text-field-focus.test.ts": default_server, "test/settings-scroll.test.ts": settings_server, "test/composer-mention.test.ts": gallery_server, + "test/composer-send-stop.test.ts": gallery_server, + "test/chat-find.test.ts": pane_server, + "test/agent-history.test.ts": pane_server, + "test/agent-pane-peer-chat.test.ts": pane_server, } files = [*contents, "test/a.test.ts", "src/b.test.tsx"] result, _, calls = self.run_runner(files, contents=contents) @@ -133,7 +160,14 @@ def test_files_that_start_a_dev_server_never_run_at_once(self): self.assertEqual(result.returncode, 0, output) self.assertEqual(calls, sorted(f"./{name}" for name in files)) self.assertEqual(self.overlaps.read_text(encoding="utf-8"), "", output) - self.assertIn("(5 dev server files one at a time after)", output) + runs = [line.split() for line in self.dev.read_text(encoding="utf-8").splitlines()] + self.assertEqual(sorted(name for name, _, _ in runs), sorted(f"./{name}" for name in contents)) + caches = [cache for _, cache, _ in runs] + self.assertEqual(len(set(caches)), len(caches), "each dev server file has its own Vite cache") + self.assertNotIn(str(self.shared_cache), caches) + # One file per config warms its cache first; every other file starts from a copy of it. + self.assertEqual(sum(warm == "cold" for _, _, warm in runs), 4, runs) + self.assertGreater(max(int(n) for n in self.peak.read_text(encoding="utf-8").split()), 1, output) def test_no_test_files_is_an_error(self): result, _, _ = self.run_runner([]) From 6f0a9d23b6f91022dc702645794a2f6528620538 Mon Sep 17 00:00:00 2001 From: Leo Li Date: Mon, 12 Oct 2026 03:46:25 -0700 Subject: [PATCH 2/5] ci: run webviews dev server files at once, each on its own warm Vite cache The dev configs read CMUX_VITE_CACHE_DIR (dev-server/viteCacheDir.mjs). The runner runs one file per config first, alone, to warm that config's cache, then the rest CMUX_WEB_TEST_DEV_JOBS at once (default half the jobs), each on its own copy: no two share node_modules/.vite and none starts the cold first bundle beside another. A config whose warming file fails keeps its files one at a time on cold caches. Co-Authored-By: Claude Opus 5.5 --- scripts/ci/run-webviews-tests.sh | 86 ++++++++++++++++++++++---- webviews/dev-server/viteCacheDir.mjs | 4 ++ webviews/vite.config.acpmux-pane.mjs | 2 + webviews/vite.config.settings-page.mjs | 2 + webviews/vite.config.ts | 2 + 5 files changed, 85 insertions(+), 11 deletions(-) create mode 100644 webviews/dev-server/viteCacheDir.mjs diff --git a/scripts/ci/run-webviews-tests.sh b/scripts/ci/run-webviews-tests.sh index da877d9f9bc2..b10f2aca464c 100755 --- a/scripts/ci/run-webviews-tests.sh +++ b/scripts/ci/run-webviews-tests.sh @@ -11,12 +11,16 @@ # CMUX_WEB_TEST_JOBS: files run at once (default: CMUX_CI_CPU_BUDGET, the cores a fleet # worker granted the step, else the CPU count). # -# Files that start a Vite dev server run one at a time after the rest: the agent pane and gallery -# configs (vite.config.acpmux-pane.mjs, vite.config.gallery-dev.ts), and files that spawn `vp dev` -# themselves (editor-page, ui-a11y, settings-scroll). Three of them starting at once (agent-history, -# agent-pane-peer-chat, chat-find) stall in the dependency optimizer's first bundle and never load -# the page, even with a dependency cache each, while each passes alone and any two pass together; -# editor-page timed out the same way beside ui-a11y and text-field-focus. +# Files that start a Vite dev server run after the rest: the agent pane and gallery configs +# (vite.config.acpmux-pane.mjs, vite.config.gallery-dev.ts), and files that spawn `vp dev` +# themselves (editor-page, ui-a11y, settings-scroll). On one node_modules/.vite two at once lose +# each other's optimized files, and three starting cold at once (agent-history, +# agent-pane-peer-chat, chat-find) stall in the optimizer's first bundle, even with a cache each; +# editor-page timed out the same way beside ui-a11y and text-field-focus. So one file per Vite +# config runs first, alone, and warms that config's cache (CMUX_VITE_CACHE_DIR, which the dev +# configs read); the rest then run CMUX_WEB_TEST_DEV_JOBS at once (default half of the jobs), each +# on its own copy of its config's warm cache. A config whose warming file fails keeps its other +# files one at a time on a cold cache each. # # Run from webviews/ (CI, local dev) it tests what is there. Run from anywhere else in the # checkout (a cmux-ci step starts at the checkout root) it first makes the web bundles @@ -55,12 +59,27 @@ run_with_timeout() { fi } +if [ "${1:-}" = --one-cached ]; then + # xargs -n 3 appends FILE CACHE SEED after the fixed arguments. + exec bash "$0" --one "$4" "$2" "$3" "$5" "${6:-}" +fi if [ "${1:-}" = --one ]; then - # Worker: run one file, write its log and status into the results directory. - file="$2" results="$3" timeout_seconds="$4" + # Worker: run one file, write its log and status into the results directory. A dev server + # file gets a Vite cache of its own (CMUX_VITE_CACHE_DIR), a copy of a warm one when given. + file="$2" results="$3" timeout_seconds="$4" cache="${5:-}" seed="${6:-}" log="$results/$(printf '%s' "$file" | tr '/' '_').log" status=0 - run_with_timeout "$timeout_seconds" bun test "./$file" > "$log" 2>&1 < /dev/null || status=$? + if [ -n "$cache" ]; then + rm -rf "$cache" + mkdir -p "$(dirname "$cache")" + if [ -n "$seed" ] && [ -d "$seed" ]; then cp -R "$seed" "$cache"; else mkdir -p "$cache"; fi + CMUX_VITE_CACHE_DIR="$cache" run_with_timeout "$timeout_seconds" bun test "./$file" > "$log" 2>&1 < /dev/null || + status=$? + # A warming file's cache (no seed) is the one the rest copy; any other one only costs disk. + if [ -n "$seed" ]; then rm -rf "$cache"; fi + else + run_with_timeout "$timeout_seconds" bun test "./$file" > "$log" 2>&1 < /dev/null || status=$? + fi case "$status" in 0) result=PASS ;; 124 | 137) result=TIMEOUT ;; @@ -112,10 +131,55 @@ dev_servers='vite\.config\.(acpmux-pane\.mjs|gallery-dev\.ts)|"dev",' xargs -0 grep -lE --null -- "$dev_servers" < "$results/files" > "$results/serial" || true serial="$(tr -cd '\0' < "$results/serial" | wc -c | tr -d ' ')" xargs -0 grep -LE --null -- "$dev_servers" < "$results/files" > "$results/parallel" || true -echo "running $count webviews test files, $jobs at a time ($serial dev server files one at a time after), ${timeout_seconds}s each" +dev_jobs="${CMUX_WEB_TEST_DEV_JOBS:-$((jobs > 1 ? jobs / 2 : 1))}" +echo "running $count webviews test files, $jobs at a time ($serial dev server files after, $dev_jobs at a time), ${timeout_seconds}s each" : > "$results/summary" xargs -0 -P "$jobs" -I{} bash "$self" --one {} "$results" "$timeout_seconds" < "$results/parallel" -xargs -0 -P 1 -I{} bash "$self" --one {} "$results" "$timeout_seconds" < "$results/serial" + +# vite_config FILE: the Vite config whose dependency cache FILE's dev server fills. +vite_config() { + if grep -q 'vite\.config\.acpmux-pane\.mjs' "$1"; then echo pane + elif grep -q 'vite\.config\.gallery-dev\.ts' "$1"; then echo gallery + elif grep -q 'vite\.config\.settings-page\.mjs' "$1"; then echo settings + else echo default + fi +} +# One file per config warms its cache, alone; the others wait for it. +vite="$results/vite" +warmed=" " +: > "$results/dev-rest" +while IFS= read -r -d '' file; do + config="$(vite_config "$file")" + case "$warmed" in + *" $config "*) printf '%s\0%s\0' "$config" "$file" >> "$results/dev-rest" ;; + *) + warmed="$warmed$config " + printf '%s %s\n' "$config" "$file" >> "$results/warmers" + bash "$self" --one "$file" "$results" "$timeout_seconds" "$vite/seed-$config" + ;; + esac +done < "$results/serial" +# The rest copy their config's warm cache and run at once; a config whose warming file did not +# pass has no trustworthy cache, so its files run one at a time, each on a cold cache of its own. +: > "$results/dev-warm" +: > "$results/dev-cold" +index=0 +while IFS= read -r -d '' config && IFS= read -r -d '' file; do + index=$((index + 1)) + warmer="$(sed -n "s/^$config //p" "$results/warmers")" + if grep -qxF "PASS $warmer $results/$(printf '%s' "$warmer" | tr '/' '_').log" "$results/summary"; then + printf '%s\0%s\0%s\0' "$file" "$vite/$index" "$vite/seed-$config" >> "$results/dev-warm" + else + printf '%s\0%s\0%s\0' "$file" "$vite/$index" "$vite/none" >> "$results/dev-cold" + fi +done < "$results/dev-rest" +# Without input, xargs would run the command once with no file (BSD xargs has no -r). +if [ -s "$results/dev-warm" ]; then + xargs -0 -n 3 -P "$dev_jobs" bash "$self" --one-cached "$results" "$timeout_seconds" < "$results/dev-warm" +fi +if [ -s "$results/dev-cold" ]; then + xargs -0 -n 3 -P 1 bash "$self" --one-cached "$results" "$timeout_seconds" < "$results/dev-cold" +fi failed=0 while read -r result file log; do diff --git a/webviews/dev-server/viteCacheDir.mjs b/webviews/dev-server/viteCacheDir.mjs new file mode 100644 index 000000000000..bbe34d2efbe0 --- /dev/null +++ b/webviews/dev-server/viteCacheDir.mjs @@ -0,0 +1,4 @@ +// Where Vite keeps its optimized dependencies. scripts/ci/run-webviews-tests.sh gives each test +// file that starts a dev server its own directory, so files that run at once never optimize into +// one node_modules/.vite and lose each other's files. Unset, Vite's default. +export const viteCacheDir = process.env.CMUX_VITE_CACHE_DIR || undefined; diff --git a/webviews/vite.config.acpmux-pane.mjs b/webviews/vite.config.acpmux-pane.mjs index 49129f9cc776..bfb62690e443 100644 --- a/webviews/vite.config.acpmux-pane.mjs +++ b/webviews/vite.config.acpmux-pane.mjs @@ -2,6 +2,7 @@ import { reactWithCompiler } from "./reactCompiler.mjs"; import { defineConfig } from "vite"; import { fileURLToPath } from "node:url"; import path from "node:path"; +import { viteCacheDir } from "./dev-server/viteCacheDir.mjs"; // Dev server for the real agent pane (src/agent-session/acpmux) with hot // reload. A Debug or tagged cmux-next launched with @@ -17,6 +18,7 @@ const port = Number(process.env.CMUX_AGENT_PANE_DEV_PORT) || 4176; export default defineConfig({ root: paneRoot, + cacheDir: viteCacheDir, server: { host: "127.0.0.1", port, diff --git a/webviews/vite.config.settings-page.mjs b/webviews/vite.config.settings-page.mjs index 8128b6deb347..222adbf999c0 100644 --- a/webviews/vite.config.settings-page.mjs +++ b/webviews/vite.config.settings-page.mjs @@ -2,6 +2,7 @@ import { reactWithCompiler } from "./reactCompiler.mjs"; import { defineConfig } from "vite"; import { fileURLToPath } from "node:url"; import path from "node:path"; +import { viteCacheDir } from "./dev-server/viteCacheDir.mjs"; // Dev server for the Settings page (src/pages/settings) with hot reload. Outside the app the page // runs on the in-memory fake transport (src/pages/settings/fakeTransport.ts). Serve only: the @@ -11,6 +12,7 @@ const webviewsRoot = path.resolve(fileURLToPath(new URL(".", import.meta.url))); const repoRoot = path.resolve(webviewsRoot, ".."); export default defineConfig({ + cacheDir: viteCacheDir, root: path.join(webviewsRoot, "src/pages/settings"), server: { host: "127.0.0.1", diff --git a/webviews/vite.config.ts b/webviews/vite.config.ts index cb50c1b66098..eca4a3c611fd 100644 --- a/webviews/vite.config.ts +++ b/webviews/vite.config.ts @@ -4,6 +4,7 @@ import { defineConfig } from "vite-plus"; import { cmuxCheckConfig } from "../config/vite-plus/check"; import { fileURLToPath } from "node:url"; import { cmuxDevServer, DEV_SERVER_PORT } from "./dev-server/plugins"; +import { viteCacheDir } from "./dev-server/viteCacheDir.mjs"; const outDir = process.env.CMUX_WEBVIEWS_OUT_DIR ?? "../Resources/markdown-viewer/webviews-app"; // The code editor's copy of Pierre's extension map (pages/editor/pierre-filetypes.d.ts). The query @@ -14,6 +15,7 @@ const pierreFiletypes = PIERRE_FILETYPES_QUERY; export default defineConfig({ + cacheDir: viteCacheDir, // `vp check` reads `lint` and `fmt` from here; `vite build` ignores them. // ts-rs emits src/diff/generated; scripts/generate-diff-sidecar-types.sh --check // owns it. Stylesheets are inlined verbatim into the shipped agent pane, so From 68296e0649cf908d51c08f7c997f27e42e934faf Mon Sep 17 00:00:00 2001 From: Leo Li Date: Mon, 12 Oct 2026 04:26:27 -0700 Subject: [PATCH 3/5] ci: run dev server webviews files two at a time At four at a time on the 8-core runner, per-file time barely moved and instructions-notice's frame budget and agent-pane-seen's attach timing failed under the load. Co-Authored-By: Claude Opus 5.5 --- scripts/ci/run-webviews-tests.sh | 5 ++++- tests/test_webviews_tests_per_file.py | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/scripts/ci/run-webviews-tests.sh b/scripts/ci/run-webviews-tests.sh index b10f2aca464c..87cc60cc78b8 100755 --- a/scripts/ci/run-webviews-tests.sh +++ b/scripts/ci/run-webviews-tests.sh @@ -131,7 +131,10 @@ dev_servers='vite\.config\.(acpmux-pane\.mjs|gallery-dev\.ts)|"dev",' xargs -0 grep -lE --null -- "$dev_servers" < "$results/files" > "$results/serial" || true serial="$(tr -cd '\0' < "$results/serial" | wc -c | tr -d ' ')" xargs -0 grep -LE --null -- "$dev_servers" < "$results/files" > "$results/parallel" || true -dev_jobs="${CMUX_WEB_TEST_DEV_JOBS:-$((jobs > 1 ? jobs / 2 : 1))}" +# A quarter of the jobs: each dev server file runs a Vite server and a browser, and at jobs / 2 the +# 8-core runner was CPU bound (no faster per file) and frame-budget plays (instructions-notice) +# and attach timing (agent-pane-seen) failed. +dev_jobs="${CMUX_WEB_TEST_DEV_JOBS:-$((jobs > 3 ? jobs / 4 : 1))}" echo "running $count webviews test files, $jobs at a time ($serial dev server files after, $dev_jobs at a time), ${timeout_seconds}s each" : > "$results/summary" xargs -0 -P "$jobs" -I{} bash "$self" --one {} "$results" "$timeout_seconds" < "$results/parallel" diff --git a/tests/test_webviews_tests_per_file.py b/tests/test_webviews_tests_per_file.py index c1f7e55fdd48..7f2f8849fe8f 100644 --- a/tests/test_webviews_tests_per_file.py +++ b/tests/test_webviews_tests_per_file.py @@ -101,7 +101,7 @@ def run_runner(self, files, without_timeout=False, contents=None): env = {**os.environ, "PATH": f"{bin_dir}:{system_path}", "BUN_CALLS": str(calls), "BUN_OVERLAPS": str(self.overlaps), "SHARED_VITE_CACHE": str(web / "node_modules" / ".vite"), "BUN_DEV": str(self.dev), "BUN_PEAK": str(self.peak), "DEV_RUNNING": str(running), - "CMUX_WEB_TEST_FILE_TIMEOUT": "2", "CMUX_WEB_TEST_JOBS": "4"} + "CMUX_WEB_TEST_FILE_TIMEOUT": "2", "CMUX_WEB_TEST_JOBS": "8"} started = time.monotonic() result = subprocess.run(["bash", str(RUNNER)], cwd=web, env=env, capture_output=True, text=True, timeout=40) return result, time.monotonic() - started, sorted(calls.read_text(encoding="utf-8").split()) From f233adb5e0eb61c9d075a4793180986d9f724871 Mon Sep 17 00:00:00 2001 From: Leo Li Date: Mon, 12 Oct 2026 04:31:23 -0700 Subject: [PATCH 4/5] test: every dev config and createServer file uses its own Vite cache Red: vite.config.ts resolves to node_modules/.vite-dev-deps-0 (the dev server plugin's per-port cache) with CMUX_VITE_CACHE_DIR set, and new-tab-typing (createServer with vite.config.ts) gets no cache of its own. Co-Authored-By: Claude Opus 5.5 --- tests/test_webviews_tests_per_file.py | 4 ++- webviews/test/vite-cache-dir.test.ts | 41 +++++++++++++++++++++++++++ 2 files changed, 44 insertions(+), 1 deletion(-) create mode 100644 webviews/test/vite-cache-dir.test.ts diff --git a/tests/test_webviews_tests_per_file.py b/tests/test_webviews_tests_per_file.py index 7f2f8849fe8f..90a5f3ea1a29 100644 --- a/tests/test_webviews_tests_per_file.py +++ b/tests/test_webviews_tests_per_file.py @@ -26,7 +26,7 @@ printf '%s\\n' "$2" >> "$BUN_CALLS" # A file that starts a Vite dev server optimizes dependencies into its Vite cache while it runs: two # at once on one cache lose each other's files, and a cold cache is the slow first bundle. -if grep -q -e '"dev",' -e 'vite.config.gallery-dev.ts' -e 'vite.config.acpmux-pane.mjs' "$2"; then +if grep -q -e '"dev",' -e 'createServer(' -e 'vite.config.gallery-dev.ts' -e 'vite.config.acpmux-pane.mjs' "$2"; then config=default grep -q 'vite.config.settings-page.mjs' "$2" && config=settings grep -q 'vite.config.gallery-dev.ts' "$2" && config=gallery @@ -143,10 +143,12 @@ def test_dev_server_files_run_at_once_each_on_its_own_warm_vite_cache(self): settings_server = 'spawn(vp, [\n "dev",\n "--config",\n "vite.config.settings-page.mjs",\n]);\n' gallery_server = 'import config from "../vite.config.gallery-dev.ts";\n' pane_server = 'spawn(vp, ["dev", "--config", "vite.config.acpmux-pane.mjs", "--port", String(port)]);\n' + created_server = 'vite = await createServer({\n configFile: path.join(webviews, "vite.config.ts"),\n' contents = { "test/editor-page.test.ts": default_server, "test/ui-a11y.test.ts": default_server, "test/text-field-focus.test.ts": default_server, + "test/new-tab-typing.test.ts": created_server, "test/settings-scroll.test.ts": settings_server, "test/composer-mention.test.ts": gallery_server, "test/composer-send-stop.test.ts": gallery_server, diff --git a/webviews/test/vite-cache-dir.test.ts b/webviews/test/vite-cache-dir.test.ts new file mode 100644 index 000000000000..4165750900ca --- /dev/null +++ b/webviews/test/vite-cache-dir.test.ts @@ -0,0 +1,41 @@ +// The webviews test runner (scripts/ci/run-webviews-tests.sh) runs dev server files at once, each +// on its own copy of a warm Vite cache, named by CMUX_VITE_CACHE_DIR. Every config a test serves +// must then optimize into that directory: one that keeps another (vite.config.ts's dev server +// plugin set a cache per port) runs cold beside the others, and cold servers started together +// stall in the optimizer (#19746). +import { describe, expect, test } from "bun:test"; +import { spawnSync } from "node:child_process"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; + +const webviews = path.join(import.meta.dir, ".."); +const resolve = ` +const { resolveConfig } = await import("vite"); +const config = await resolveConfig({ configFile: process.env.CONFIG, server: { port: 0 } }, "serve", "development"); +console.log("cacheDir=" + config.cacheDir); +`; + +describe("CMUX_VITE_CACHE_DIR", () => { + for (const name of [ + "vite.config.ts", + "vite.config.acpmux-pane.mjs", + "vite.config.gallery-dev.ts", + "vite.config.settings-page.mjs", + ]) { + test(`${name} optimizes into it`, () => { + const cache = fs.mkdtempSync(path.join(os.tmpdir(), "cmux-vite-cache-")); + try { + const result = spawnSync(process.execPath, ["-e", resolve], { + cwd: webviews, + env: { ...process.env, CONFIG: path.join(webviews, name), CMUX_VITE_CACHE_DIR: cache }, + encoding: "utf8", + timeout: 60_000, + }); + expect(result.stdout.match(/^cacheDir=(.*)$/m)?.[1], result.stderr).toBe(cache); + } finally { + fs.rmSync(cache, { recursive: true, force: true }); + } + }, 70_000); + } +}); From 070cc361d275261acbf42d34947518fc0cb4f32a Mon Sep 17 00:00:00 2001 From: Leo Li Date: Mon, 12 Oct 2026 04:33:49 -0700 Subject: [PATCH 5/5] fix: give vite.config.ts and createServer test files their own Vite cache cmuxDevServer's config hook set a per-port cacheDir over vite.config.ts's, so the seven default-config files ignored CMUX_VITE_CACHE_DIR, copied an empty seed and started cold together (the #19746 stall). The plugin now prefers CMUX_VITE_CACHE_DIR. new-tab-typing and diff-toolbar-narrow call createServer and were not matched as dev server files at all. Cache upkeep in the worker no longer aborts the lane under set -e. Co-Authored-By: Claude Opus 5.5 --- scripts/ci/run-webviews-tests.sh | 15 +++++++++------ webviews/dev-server/plugins.ts | 7 +++++-- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/scripts/ci/run-webviews-tests.sh b/scripts/ci/run-webviews-tests.sh index 87cc60cc78b8..8b50b307a761 100755 --- a/scripts/ci/run-webviews-tests.sh +++ b/scripts/ci/run-webviews-tests.sh @@ -18,7 +18,7 @@ # agent-pane-peer-chat, chat-find) stall in the optimizer's first bundle, even with a cache each; # editor-page timed out the same way beside ui-a11y and text-field-focus. So one file per Vite # config runs first, alone, and warms that config's cache (CMUX_VITE_CACHE_DIR, which the dev -# configs read); the rest then run CMUX_WEB_TEST_DEV_JOBS at once (default half of the jobs), each +# configs read); the rest then run CMUX_WEB_TEST_DEV_JOBS at once (default a quarter of the jobs), each # on its own copy of its config's warm cache. A config whose warming file fails keeps its other # files one at a time on a cold cache each. # @@ -70,13 +70,16 @@ if [ "${1:-}" = --one ]; then log="$results/$(printf '%s' "$file" | tr '/' '_').log" status=0 if [ -n "$cache" ]; then - rm -rf "$cache" + # `|| true` on cache upkeep: a failed copy or cleanup (a server outliving its timed-out file) + # must still leave this file's result in the summary, not stop the lane under set -e. + rm -rf "$cache" || true mkdir -p "$(dirname "$cache")" - if [ -n "$seed" ] && [ -d "$seed" ]; then cp -R "$seed" "$cache"; else mkdir -p "$cache"; fi + if [ -n "$seed" ] && [ -d "$seed" ]; then cp -R "$seed" "$cache" || true; fi + mkdir -p "$cache" CMUX_VITE_CACHE_DIR="$cache" run_with_timeout "$timeout_seconds" bun test "./$file" > "$log" 2>&1 < /dev/null || status=$? # A warming file's cache (no seed) is the one the rest copy; any other one only costs disk. - if [ -n "$seed" ]; then rm -rf "$cache"; fi + if [ -n "$seed" ]; then rm -rf "$cache" || true; fi else run_with_timeout "$timeout_seconds" bun test "./$file" > "$log" 2>&1 < /dev/null || status=$? fi @@ -126,8 +129,8 @@ if [ "$count" -eq 0 ]; then fi # Two dev servers at once (composer-mention, composer-send-stop) optimize dependencies into the # same node_modules/.vite, and each loses files the other wrote. `"dev",` is the argument a file -# passes when it spawns `vp dev` itself. `--null`, not `-Z`: BSD grep reads -Z as decompress. -dev_servers='vite\.config\.(acpmux-pane\.mjs|gallery-dev\.ts)|"dev",' +# passes when it spawns `vp dev` itself, `configFile: …` one that calls createServer. `--null`, not `-Z`: BSD grep reads -Z as decompress. +dev_servers='vite\.config\.(acpmux-pane\.mjs|gallery-dev\.ts)|"dev",|configFile: path\.join\(webviews, "vite\.config' xargs -0 grep -lE --null -- "$dev_servers" < "$results/files" > "$results/serial" || true serial="$(tr -cd '\0' < "$results/serial" | wc -c | tr -d ' ')" xargs -0 grep -LE --null -- "$dev_servers" < "$results/files" > "$results/parallel" || true diff --git a/webviews/dev-server/plugins.ts b/webviews/dev-server/plugins.ts index b32912738d79..63dd545e14b5 100644 --- a/webviews/dev-server/plugins.ts +++ b/webviews/dev-server/plugins.ts @@ -75,6 +75,7 @@ import { splitStyles, } from "./markdownHost"; import { galleryHost, galleryModules } from "./galleryHost"; +import { viteCacheDir } from "./viteCacheDir.mjs"; const webviewsRoot = path.resolve(fileURLToPath(new URL("..", import.meta.url))); const repoRoot = path.join(webviewsRoot, ".."); @@ -173,8 +174,10 @@ function devServerShell(): Plugin { return { // One dependency cache per server. A cache shared with another server on the same // node_modules (vite.config.acpmux-pane.mjs, another slot) is re-optimized under it, and - // open pages then fail to load modules and requests mid-load. - cacheDir: `node_modules/${dependencyCacheName(config.server?.port ?? DEV_SERVER_PORT)}`, + // open pages then fail to load modules and requests mid-load. The webviews test runner + // names one of its own (CMUX_VITE_CACHE_DIR), a copy of a warm one, for fresh browsers + // only, so its modules skip the no-cache revalidation below. + cacheDir: viteCacheDir ?? `node_modules/${dependencyCacheName(config.server?.port ?? DEV_SERVER_PORT)}`, // Every surface's entry, so the first scan finds all dependencies instead of a page // discovering one late and triggering an optimizer reload. // Monaco stays unbundled: its language configurations (pages/editor/view.ts) import its API