From 4b08167789b1ba4119401fc1089aa59fc7d3a1eb Mon Sep 17 00:00:00 2001 From: ScriptedAlchemy Date: Sat, 29 Aug 2026 02:24:40 +0000 Subject: [PATCH 1/5] test: derive integration worker count from cores and split shared-artifact writers into a serial pool The parallel pool runs mkdtemp-fixture, ephemeral-port files on min(4, cores/2) workers (still 1 on two-core CI, overridable via AGENT_BUNDLE_INTEGRATION_MAX_WORKERS); the six files that rewrite shared package dists stay on one worker in a chained serial config. --- package.json | 2 +- rstest.integration-serial.config.ts | 16 ++++++++++++ rstest.integration-tests.ts | 38 +++++++++++++++++++++++++++++ rstest.integration.config.ts | 28 ++++++++++++++++++--- 4 files changed, 79 insertions(+), 5 deletions(-) create mode 100644 rstest.integration-serial.config.ts diff --git a/package.json b/package.json index 7b3db36df..6deb835cf 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,7 @@ "test": "pnpm test:unit && pnpm test:integration", "test:unit": "rstest --config rstest.unit.config.ts", "test:integration": "pnpm --filter agent-bundle-workbench build && pnpm test:integration:run", - "test:integration:run": "AGENT_BUNDLE_WORKBENCH_PREBUILT=1 rstest --config rstest.integration.config.ts --pool.maxWorkers 1", + "test:integration:run": "AGENT_BUNDLE_WORKBENCH_PREBUILT=1 rstest --config rstest.integration.config.ts && AGENT_BUNDLE_WORKBENCH_PREBUILT=1 rstest --config rstest.integration-serial.config.ts", "test:watch": "rstest --config rstest.config.ts --watch", "lint": "rslint .", "typecheck": "tsc --noEmit && tsc --project packages/workbench/tsconfig.json", diff --git a/rstest.integration-serial.config.ts b/rstest.integration-serial.config.ts new file mode 100644 index 000000000..448277c11 --- /dev/null +++ b/rstest.integration-serial.config.ts @@ -0,0 +1,16 @@ +import { defineConfig } from '@rstest/core'; + +import { serialIntegrationTestFiles } from './rstest.integration-tests.ts'; +import { withAgentBundleRslibConfig } from './rstest.rslib.ts'; + +/** + * Integration files that rewrite workspace-shared artifacts (see the + * serialIntegrationTestFiles doc in rstest.integration-tests.ts). One worker + * only: they rebuild or repack shared package dist directories that every + * other file in this group also reads. + */ +export default defineConfig({ + extends: withAgentBundleRslibConfig(), + include: [...serialIntegrationTestFiles], + pool: { maxWorkers: 1 }, +}); diff --git a/rstest.integration-tests.ts b/rstest.integration-tests.ts index 78e0cc619..d40ca8b5e 100644 --- a/rstest.integration-tests.ts +++ b/rstest.integration-tests.ts @@ -66,6 +66,44 @@ export const integrationTestFiles: readonly string[] = [ 'packages/workbench/tests/workbench-dev-command.test.ts', ]; +/** + * Integration files that WRITE to workspace-shared locations and therefore + * cannot run alongside other integration files: + * + * - inspector-shell.e2e rewrites `packages/workbench/dist` with an explicit + * development-mode artifact (the build itself is under test). + * - packed-release.e2e runs a root `pnpm build` (rewriting + * `packages/{agent-bundle,rsc-runtime,workbench}/dist`) and `npm pack`. + * - cli.test runs a root `pnpm build` and `npm pack` against the shared + * package dist. + * - overview.e2e, mcp-app-real.e2e, and playground-real.e2e rebuild + * `packages/workbench/dist` with local build helpers that ignore + * AGENT_BUNDLE_WORKBENCH_PREBUILT. + * + * They run on one worker via rstest.integration-serial.config.ts after the + * parallel pool finishes (rstest orders files alphabetically, so + * inspector-shell's development artifact lands after cli.test has used the + * production CLI dist, and packed-release packs the agent-bundle dist copy + * that is unaffected by the workbench dist rewrite). + */ +export const serialIntegrationTestFiles: readonly string[] = [ + 'packages/agent-bundle/tests/cli.test.ts', + 'packages/workbench/tests/inspector-shell.e2e.test.ts', + 'packages/workbench/tests/mcp-app-real.e2e.test.ts', + 'packages/workbench/tests/overview.e2e.test.ts', + 'packages/workbench/tests/packed-release.e2e.test.ts', + 'packages/workbench/tests/playground-real.e2e.test.ts', +]; + +/** + * Integration files safe on parallel workers: they create per-test fixtures + * with `mkdtemp`, bind servers on ephemeral ports (`port: 0` or rsbuild's + * silent free-port fallback), and only READ the prebuilt shared artifacts + * (`packages/workbench/dist`, `packages/agent-bundle/dist`). + */ +export const parallelIntegrationTestFiles: readonly string[] = + integrationTestFiles.filter((file) => !serialIntegrationTestFiles.includes(file)); + /** * Pack-and-install tests: each one runs `npm pack` (and usually a clean * `npm install` of the tarball), which dominates the serialized integration diff --git a/rstest.integration.config.ts b/rstest.integration.config.ts index 47c0a4043..ee1394b83 100644 --- a/rstest.integration.config.ts +++ b/rstest.integration.config.ts @@ -1,13 +1,33 @@ +import { availableParallelism } from 'node:os'; + import { defineConfig } from '@rstest/core'; -import { integrationTestFiles } from './rstest.integration-tests.ts'; +import { parallelIntegrationTestFiles } from './rstest.integration-tests.ts'; import { withAgentBundleRslibConfig } from './rstest.rslib.ts'; -/** Build- and process-running tests: Rslib/Rsbuild caches and output paths are process-shared, so one worker only. */ +/** + * Worker count for the parallel integration pool. Half the cores keeps + * browser + dev-server pairs from starving each other, the cap of 4 bounds + * memory on large machines, and two-core CI still resolves to one worker. + * AGENT_BUNDLE_INTEGRATION_MAX_WORKERS overrides the computed value (e.g. to + * force a serial run when measuring or bisecting). + */ +const overrideWorkers = Number(process.env['AGENT_BUNDLE_INTEGRATION_MAX_WORKERS'] ?? ''); +const maxWorkers = Number.isSafeInteger(overrideWorkers) && overrideWorkers >= 1 + ? overrideWorkers + : Math.max(1, Math.min(4, Math.floor(availableParallelism() / 2))); + +/** + * Build- and process-running tests that only read workspace-shared artifacts; + * files that WRITE shared locations run serialized afterwards through + * rstest.integration-serial.config.ts (rstest has no per-project pool or + * isolate settings, so the split lives in two configs chained by + * `test:integration:run`). + */ export default defineConfig({ extends: withAgentBundleRslibConfig(), - include: [...integrationTestFiles], - pool: { maxWorkers: 1 }, + include: [...parallelIntegrationTestFiles], + pool: { maxWorkers }, // isolate: false would cut Playwright startup cost, but the log pipeline // suites rely on per-file module isolation (verified: logs-real.e2e fails // when sharing a worker with the other log suites). From e42702a42d7ba7f0df95a12ac6c108cb9093a030 Mon Sep 17 00:00:00 2001 From: ScriptedAlchemy Date: Sat, 29 Aug 2026 02:28:28 +0000 Subject: [PATCH 2/5] test: stop rebuilding shared artifacts inside the integration pool overview.e2e, mcp-app-real.e2e, and playground-real.e2e now use the shared memoized buildWorkbench (which honors AGENT_BUNDLE_WORKBENCH_PREBUILT) instead of local copies that rebuilt packages/workbench/dist up to ten times per run; cli.test and the packed-release harness honor a new AGENT_BUNDLE_PACKAGE_PREBUILT flag, with `test:integration` building the whole workspace once up front. With those writes gone, the four files move from the serial pool to the parallel one. --- package.json | 4 ++-- packages/agent-bundle/tests/cli.test.ts | 1 + .../workbench/tests/mcp-app-real.e2e.test.ts | 13 +----------- packages/workbench/tests/overview.e2e.test.ts | 12 +---------- .../tests/playground-real.e2e.test.ts | 15 +------------- .../tests/support/packed-release-harness.ts | 1 + rstest.integration-tests.ts | 20 ++++++------------- 7 files changed, 13 insertions(+), 53 deletions(-) diff --git a/package.json b/package.json index 6deb835cf..04a871f2a 100644 --- a/package.json +++ b/package.json @@ -12,8 +12,8 @@ "lint:package": "publint packages/agent-bundle", "test": "pnpm test:unit && pnpm test:integration", "test:unit": "rstest --config rstest.unit.config.ts", - "test:integration": "pnpm --filter agent-bundle-workbench build && pnpm test:integration:run", - "test:integration:run": "AGENT_BUNDLE_WORKBENCH_PREBUILT=1 rstest --config rstest.integration.config.ts && AGENT_BUNDLE_WORKBENCH_PREBUILT=1 rstest --config rstest.integration-serial.config.ts", + "test:integration": "pnpm build && pnpm test:integration:run", + "test:integration:run": "AGENT_BUNDLE_WORKBENCH_PREBUILT=1 AGENT_BUNDLE_PACKAGE_PREBUILT=1 rstest --config rstest.integration.config.ts && AGENT_BUNDLE_WORKBENCH_PREBUILT=1 AGENT_BUNDLE_PACKAGE_PREBUILT=1 rstest --config rstest.integration-serial.config.ts", "test:watch": "rstest --config rstest.config.ts --watch", "lint": "rslint .", "typecheck": "tsc --noEmit && tsc --project packages/workbench/tsconfig.json", diff --git a/packages/agent-bundle/tests/cli.test.ts b/packages/agent-bundle/tests/cli.test.ts index 9fe2f5436..a858d0b64 100644 --- a/packages/agent-bundle/tests/cli.test.ts +++ b/packages/agent-bundle/tests/cli.test.ts @@ -15,6 +15,7 @@ const cliPath = join(packageRoot, 'dist/cli.js'); let buildPackage: Promise | undefined; const buildCliPackage = async (): Promise => { + if (process.env['AGENT_BUNDLE_PACKAGE_PREBUILT'] === '1') return; buildPackage ??= execFile('pnpm', ['build'], { cwd: workspaceRoot }).then(() => undefined); await buildPackage; }; diff --git a/packages/workbench/tests/mcp-app-real.e2e.test.ts b/packages/workbench/tests/mcp-app-real.e2e.test.ts index f2b5c235e..4ba1a8ec6 100644 --- a/packages/workbench/tests/mcp-app-real.e2e.test.ts +++ b/packages/workbench/tests/mcp-app-real.e2e.test.ts @@ -1,7 +1,5 @@ -import { execFile as executeFile } from 'node:child_process'; import { access, mkdir, readFile, symlink, writeFile } from 'node:fs/promises'; import { join } from 'node:path'; -import { promisify } from 'node:util'; import { expect, test, type PlaywrightOptions } from '@rstest/playwright'; import type { Page, WebSocketRoute } from 'playwright'; @@ -12,12 +10,11 @@ import { startDevServer } from '../../agent-bundle/src/dev/workbench-server.ts'; import { createProjectFixture, removeProjectFixture } from '../../agent-bundle/tests/helpers/project-fixture.ts'; import { startRuntimePlaygroundFixture } from './helpers/runtime-playground-fixture.ts'; import { timeScale } from '../../agent-bundle/tests/support/time-scale.ts'; -import { workbenchUrl } from './support/workbench-e2e.ts'; +import { buildWorkbench, workbenchUrl } from './support/workbench-e2e.ts'; const workspaceRoot = process.cwd(); const workbenchAssets = join(workspaceRoot, 'packages', 'workbench', 'dist'); const browserTimeout = 8_000 * timeScale; -const execFile = promisify(executeFile); const e2e = test.extend({ playwright: { @@ -26,14 +23,6 @@ const e2e = test.extend({ } satisfies PlaywrightOptions, }); -const buildWorkbench = async (): Promise => { - const { RSTEST: _rstest, ...environment } = process.env; - await execFile('pnpm', ['--filter', 'agent-bundle-workbench', 'build'], { - cwd: workspaceRoot, - env: { ...environment, NODE_ENV: 'production' }, - }); -}; - const appFixtureHtml = [ '
waiting
', '