From 13b0c470ebff5e45aa7db661d00ee2cdc75ddeff Mon Sep 17 00:00:00 2001 From: ScriptedAlchemy Date: Mon, 31 Aug 2026 14:35:28 +0000 Subject: [PATCH 1/8] perf(test): pack each public package once per test:packed run MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The packed pool re-derived the same release artifacts per test file: seven full workspace `pnpm build`s (public-api-packed, release-audit x2, dev-workbench-packaging, rsc-runtime-optional-packaging, packed-release.e2e via the harness — the pool never set AGENT_BUNDLE_PACKAGE_PREBUILT) plus ten `npm pack`s and two copy+rslib rebuilds in the scaffolder e2e. `test:packed` now builds once, packs agent-bundle and create-agent-bundle once (scripts/run-packed-tests.mjs), and hands the shared tarballs plus the prebuilt seams to the pool through AGENT_BUNDLE_SHARED_PACK_DIR (tests/support/shared-pack.ts, with a lazy build-and-pack fallback for ad-hoc single-file runs). The stale-asset pruning test keeps its forced rebuild — the rebuild is the behavior under test. Census is unchanged (8 files, 22 passed / 1 skipped); the pool drops from 5m41s to 3m30s on the same machine and no longer rebuilds the workspace at all. --- package.json | 2 +- .../tests/dev-workbench-packaging.test.ts | 15 ++--- .../tests/public-api-packed.test.ts | 37 ++-------- .../agent-bundle/tests/release-audit.test.ts | 55 ++++----------- .../rsc-runtime-optional-packaging.test.ts | 15 +---- .../agent-bundle/tests/support/shared-pack.ts | 67 +++++++++++++++++++ .../tests/scaffold-packed.e2e.test.ts | 36 ++++------ .../tests/packed-release.e2e.test.ts | 11 +-- .../tests/support/packed-release-harness.ts | 10 --- rstest.integration-tests.ts | 3 +- rstest.packed.config.ts | 17 +++++ scripts/run-packed-tests.mjs | 49 ++++++++++++++ 12 files changed, 180 insertions(+), 137 deletions(-) create mode 100644 packages/agent-bundle/tests/support/shared-pack.ts create mode 100644 rstest.packed.config.ts create mode 100644 scripts/run-packed-tests.mjs diff --git a/package.json b/package.json index f3ef963bd..ee968eb82 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,7 @@ "eval:spot": "pnpm build && pnpm --filter @agent-bundle/rsc-agent-runtime-demo build && pnpm --filter @agent-bundle/rsc-agent-runtime-demo exec rstest run tests/micro-eval.spot.test.ts --config rstest.config.ts", "check:runtime-topology": "node scripts/rsc-runtime-topology.mjs --root . --output docs/architecture/rsc-runtime-workbench.md --check", "test:examples:browser": "rstest --config rstest.config.ts packages/workbench/tests/examples-real.e2e.test.ts", - "test:packed": "rstest --config rstest.config.ts packages/agent-bundle/tests/release-audit.test.ts packages/agent-bundle/tests/packed-consumer.test.ts packages/agent-bundle/tests/dev-workbench-packaging.test.ts packages/agent-bundle/tests/public-api-packed.test.ts packages/agent-bundle/tests/rsc-runtime-optional-packaging.test.ts packages/agent-bundle/tests/packed-native-smoke.test.ts packages/create-agent-bundle/tests/scaffold-packed.e2e.test.ts packages/workbench/tests/packed-release.e2e.test.ts", + "test:packed": "pnpm build && node scripts/run-packed-tests.mjs", "test:packed:native": "rstest --config rstest.config.ts packages/agent-bundle/tests/packed-native-smoke.test.ts", "test:packed:native:claude": "pnpm build && AGENT_BUNDLE_PACKED_NATIVE_CLAUDE_SMOKE=1 pnpm test:packed:native", "test:packed:native:codex": "pnpm build && AGENT_BUNDLE_PACKED_NATIVE_CODEX_SMOKE=1 pnpm test:packed:native", diff --git a/packages/agent-bundle/tests/dev-workbench-packaging.test.ts b/packages/agent-bundle/tests/dev-workbench-packaging.test.ts index 52ae47538..c195c2785 100644 --- a/packages/agent-bundle/tests/dev-workbench-packaging.test.ts +++ b/packages/agent-bundle/tests/dev-workbench-packaging.test.ts @@ -7,6 +7,8 @@ import { promisify } from 'node:util'; import { describe, expect, it } from '@rstest/core'; +import { sharedPackedTarball } from './support/shared-pack.ts'; + const execFile = promisify(executeFile); const workspaceRoot = process.cwd(); const packageRoot = join(workspaceRoot, 'packages', 'agent-bundle'); @@ -21,9 +23,12 @@ const packedEnvironment = (): NodeJS.ProcessEnv => { const buildPackage = async (force = false): Promise => { if (force) { + // The stale-asset pruning test rebuilds on purpose; the prebuilt seam + // never skips it because the rebuild itself is the behavior under test. await execFile('pnpm', ['build'], { cwd: workspaceRoot }); return; } + if (process.env['AGENT_BUNDLE_PACKAGE_PREBUILT'] === '1') return; built ??= execFile('pnpm', ['build'], { cwd: workspaceRoot }).then(() => undefined); await built; }; @@ -71,13 +76,10 @@ it('prunes stale copied workbench assets without removing the package library ou }, 60_000); it('serves prebuilt workbench assets from an installed tarball without the repository source tree', async () => { - await buildPackage(); + const { tarball } = await sharedPackedTarball('agent-bundle'); const consumer = await mkdtemp(join(tmpdir(), 'agent-bundle-workbench-consumer-')); const project = join(consumer, 'project'); try { - const { stdout } = await execFile('npm', ['pack', '--json', '--pack-destination', consumer], { cwd: packageRoot }); - const [packed] = JSON.parse(stdout) as Array<{ readonly filename: string }>; - const tarball = join(consumer, packed.filename); const listing = await execFile('tar', ['-tf', tarball]); expect(listing.stdout).toContain('package/dist/workbench/index.html'); expect(listing.stdout).toContain('package/dist/workbench/THIRD_PARTY_NOTICES'); @@ -113,13 +115,10 @@ it('serves prebuilt workbench assets from an installed tarball without the repos }, 60_000); it('runs the Agent API from an omit-dev installed tarball with its runtime MCP dependencies', async () => { - await buildPackage(); + const { tarball } = await sharedPackedTarball('agent-bundle'); const consumer = await mkdtemp(join(tmpdir(), 'agent-bundle-agent-api-consumer-')); const project = join(consumer, 'project'); try { - const { stdout } = await execFile('npm', ['pack', '--json', '--pack-destination', consumer], { cwd: packageRoot }); - const [packed] = JSON.parse(stdout) as Array<{ readonly filename: string }>; - const tarball = join(consumer, packed.filename); await writeFile(join(consumer, 'package.json'), '{"type":"module"}\n'); await execFile('npm', [ 'install', '--omit=dev', '--ignore-scripts', '--no-audit', '--no-fund', tarball, diff --git a/packages/agent-bundle/tests/public-api-packed.test.ts b/packages/agent-bundle/tests/public-api-packed.test.ts index ed46116b8..651e4c01b 100644 --- a/packages/agent-bundle/tests/public-api-packed.test.ts +++ b/packages/agent-bundle/tests/public-api-packed.test.ts @@ -7,6 +7,7 @@ import { promisify } from 'node:util'; import { expect, it } from '@rstest/core'; import { writeFixtureManifest } from './support/manifest.ts'; +import { sharedPackedTarball } from './support/shared-pack.ts'; interface PackageManifest { bin: { @@ -18,14 +19,6 @@ interface PackageManifest { const execFile = promisify(executeFile); const workspaceRoot = process.cwd(); const packageRoot = join(workspaceRoot, 'packages/agent-bundle'); -let buildPromise: Promise | undefined; - -const buildPackage = async (): Promise => { - buildPromise ??= execFile('pnpm', ['build'], { - cwd: workspaceRoot, - }).then(() => undefined); - await buildPromise; -}; const readPackageManifest = async (): Promise => JSON.parse( @@ -58,18 +51,14 @@ const producerFrom = async (output: string): Promise<{ readonly name: string; re }; it('writes the package version as the producer of a packed CLI manifest', async () => { - await buildPackage(); + const { tarball } = await sharedPackedTarball('agent-bundle'); const consumerRoot = await mkdtemp(join(tmpdir(), 'agent-bundle-packed-manifest-')); const manifest = await readPackageManifest(); try { - const { stdout: packedOutput } = await execFile( - 'npm', ['pack', '--json', '--pack-destination', consumerRoot], { cwd: packageRoot }, - ); - const [packed] = JSON.parse(packedOutput) as Array<{ filename: string }>; await writeFile(join(consumerRoot, 'package.json'), '{"type":"module"}\n'); await execFile( - 'npm', ['install', '--ignore-scripts', '--no-audit', '--no-fund', join(consumerRoot, packed.filename)], + 'npm', ['install', '--ignore-scripts', '--no-audit', '--no-fund', tarball], { cwd: consumerRoot }, ); @@ -94,18 +83,10 @@ it('writes the package version as the producer of a packed CLI manifest', async }, 30_000); it('imports the externalized config entry from a packed npm consumer', async () => { - await buildPackage(); + const { tarball } = await sharedPackedTarball('agent-bundle'); const consumerRoot = await mkdtemp(join(tmpdir(), 'agent-bundle-consumer-')); try { - const { stdout: packedOutput } = await execFile( - 'npm', - ['pack', '--json', '--pack-destination', consumerRoot], - { cwd: packageRoot }, - ); - const [packed] = JSON.parse(packedOutput) as Array<{ filename: string }>; - const tarball = join(consumerRoot, packed.filename); - await writeFile(join(consumerRoot, 'package.json'), '{"type":"module"}\n'); await execFile( 'npm', @@ -164,7 +145,7 @@ it('imports the externalized config entry from a packed npm consumer', async () }, 15_000); it('invokes a prebuilt MCP server from a clean packed consumer', async () => { - await buildPackage(); + const { tarball } = await sharedPackedTarball('agent-bundle'); const consumerRoot = await mkdtemp(join(tmpdir(), 'agent-bundle-mcp-consumer-')); try { @@ -214,16 +195,10 @@ it('invokes a prebuilt MCP server from a clean packed consumer', async () => { '{"hooks":[]}\n', ); - const { stdout: packedOutput } = await execFile( - 'npm', - ['pack', '--json', '--pack-destination', consumerRoot], - { cwd: packageRoot }, - ); - const [packed] = JSON.parse(packedOutput) as Array<{ filename: string }>; await writeFile(join(consumerRoot, 'package.json'), '{"type":"module"}\n'); await execFile( 'npm', - ['install', '--ignore-scripts', '--no-audit', '--no-fund', join(consumerRoot, packed.filename)], + ['install', '--ignore-scripts', '--no-audit', '--no-fund', tarball], { cwd: consumerRoot }, ); const { stdout } = await execFile(process.execPath, [ diff --git a/packages/agent-bundle/tests/release-audit.test.ts b/packages/agent-bundle/tests/release-audit.test.ts index 178505882..c870928b2 100644 --- a/packages/agent-bundle/tests/release-audit.test.ts +++ b/packages/agent-bundle/tests/release-audit.test.ts @@ -6,6 +6,8 @@ import { promisify } from 'node:util'; import { expect, it } from '@rstest/core'; +import { sharedPackedTarball } from './support/shared-pack.ts'; + const execFile = promisify(executeFile); const workspaceRoot = process.cwd(); const packageRoot = join(workspaceRoot, 'packages', 'agent-bundle'); @@ -81,14 +83,8 @@ it('ships repository and support metadata that matches the verified origin', asy const tarballRoot = await mkdtemp(join(tmpdir(), 'agent-bundle-package-metadata-')); try { - const { stdout } = await execFile('npm', [ - 'pack', - '--json', - '--pack-destination', - tarballRoot, - ], { cwd: packageRoot, env: releaseEnvironment() }); - const [{ filename }] = JSON.parse(stdout) as Array<{ readonly filename: string }>; - await execFile('tar', ['--extract', '--file', join(tarballRoot, filename), '--directory', tarballRoot]); + const { tarball } = await sharedPackedTarball('agent-bundle'); + await execFile('tar', ['--extract', '--file', tarball, '--directory', tarballRoot]); const manifest = JSON.parse(await readFile(join(tarballRoot, 'package', 'package.json'), 'utf8')) as { readonly bugs?: { readonly url?: string }; readonly description?: string; @@ -109,43 +105,21 @@ it('ships repository and support metadata that matches the verified origin', asy }); it('packs generated Workbench legal companion files', async () => { - const tarballRoot = await mkdtemp(join(tmpdir(), 'agent-bundle-release-audit-')); - - try { - await execFile('pnpm', ['build'], { cwd: workspaceRoot, env: releaseEnvironment() }); - const { stdout } = await execFile('npm', [ - 'pack', - '--json', - '--pack-destination', - tarballRoot, - ], { cwd: packageRoot, env: releaseEnvironment() }); - const [{ files }] = JSON.parse(stdout) as Array<{ readonly files: readonly { readonly path: string }[] }>; - const productManifest = await readFile(join(packageRoot, 'package.json'), 'utf8'); + const { packOutput } = await sharedPackedTarball('agent-bundle'); + const productManifest = await readFile(join(packageRoot, 'package.json'), 'utf8'); - expect(files.map((file) => file.path)).toContainEqual( - expect.stringMatching(/^dist\/workbench\/.*\.LICENSE\.txt$/u), - ); - expect(files.some(({ path }) => path.startsWith('examples/'))).toBe(false); - expect(productManifest).not.toContain('workspace:'); - } finally { - await rm(tarballRoot, { force: true, recursive: true }); - } + expect(packOutput.files.map((file) => file.path)).toContainEqual( + expect.stringMatching(/^dist\/workbench\/.*\.LICENSE\.txt$/u), + ); + expect(packOutput.files.some(({ path }) => path.startsWith('examples/'))).toBe(false); + expect(productManifest).not.toContain('workspace:'); }, 120_000); it('installs public entrypoints and an externally resolved CLI for production consumers', async () => { const consumerRoot = await mkdtemp(join(tmpdir(), 'agent-bundle-release-consumer-')); - const tarballRoot = await mkdtemp(join(tmpdir(), 'agent-bundle-release-tarball-')); try { - await execFile('pnpm', ['build'], { cwd: workspaceRoot, env: releaseEnvironment() }); - const { stdout: packed } = await execFile('npm', [ - 'pack', - '--json', - '--pack-destination', - tarballRoot, - ], { cwd: packageRoot, env: releaseEnvironment() }); - const [{ filename }] = JSON.parse(packed) as Array<{ readonly filename: string }>; - const tarball = join(tarballRoot, filename); + const { tarball } = await sharedPackedTarball('agent-bundle'); await writeFile(join(consumerRoot, 'package.json'), '{"private":true,"type":"module"}\n'); await execFile('npm', [ 'install', @@ -221,9 +195,6 @@ it('installs public entrypoints and an externally resolved CLI for production co await rm(join(consumerRoot, 'node_modules', 'commander'), { force: true, recursive: true }); await expect(execFile(cli, ['--help'], { cwd: consumerRoot, env: releaseEnvironment() })).rejects.toThrow(); } finally { - await Promise.all([ - rm(consumerRoot, { force: true, recursive: true }), - rm(tarballRoot, { force: true, recursive: true }), - ]); + await rm(consumerRoot, { force: true, recursive: true }); } }, 120_000); diff --git a/packages/agent-bundle/tests/rsc-runtime-optional-packaging.test.ts b/packages/agent-bundle/tests/rsc-runtime-optional-packaging.test.ts index 9c2a38aa9..ea04e3a36 100644 --- a/packages/agent-bundle/tests/rsc-runtime-optional-packaging.test.ts +++ b/packages/agent-bundle/tests/rsc-runtime-optional-packaging.test.ts @@ -6,16 +6,11 @@ import { promisify } from 'node:util'; import { describe, expect, it } from '@rstest/core'; +import { sharedPackedTarball } from './support/shared-pack.ts'; + const execFile = promisify(executeFile); const workspaceRoot = process.cwd(); -const packageRoot = join(workspaceRoot, 'packages', 'agent-bundle'); const skillsOnlyFixture = join(workspaceRoot, 'fixtures', 'integration', 'skills-only'); -let built: Promise | undefined; - -const buildPackage = async (): Promise => { - built ??= execFile('npm', ['run', 'build'], { cwd: workspaceRoot }).then(() => undefined); - await built; -}; type InstalledDependencyTree = Readonly<{ readonly dependencies?: Readonly>; @@ -42,15 +37,11 @@ const namedFiles = async (root: string, name: string): Promise { it('runs an ordinary skills-only project from a fresh installed tarball without the RSC runtime', async () => { - await buildPackage(); + const { tarball } = await sharedPackedTarball('agent-bundle'); const consumer = await mkdtemp(join(tmpdir(), 'agent-bundle-rsc-optional-consumer-')); const project = join(consumer, 'project'); const artifact = join(project, '.agent-bundle', 'artifact'); try { - const { stdout } = await execFile('npm', ['pack', '--json', '--pack-destination', consumer], { cwd: packageRoot }); - const [packed] = JSON.parse(stdout) as readonly Readonly<{ readonly filename: string }>[]; - if (packed === undefined) throw new Error('npm pack did not produce an Agent Bundle tarball.'); - const tarball = join(consumer, packed.filename); const tarListing = (await execFile('tar', ['-tf', tarball])).stdout; expect(tarListing).not.toMatch(/examples\/rsc-agent-runtime|react-server-dom-rspack|rsbuild-plugin-rsc/u); diff --git a/packages/agent-bundle/tests/support/shared-pack.ts b/packages/agent-bundle/tests/support/shared-pack.ts new file mode 100644 index 000000000..451be3327 --- /dev/null +++ b/packages/agent-bundle/tests/support/shared-pack.ts @@ -0,0 +1,67 @@ +import { execFile as executeFile } from 'node:child_process'; +import { rmSync } from 'node:fs'; +import { mkdtemp, readFile } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import { promisify } from 'node:util'; + +const execFile = promisify(executeFile); +const workspaceRoot = process.cwd(); + +export interface SharedPackOutput { + readonly filename: string; + readonly files: readonly { readonly path: string }[]; +} + +export interface SharedPack { + /** First `npm pack --json` entry recorded when the tarball was produced. */ + readonly packOutput: SharedPackOutput; + readonly tarball: string; +} + +export type SharedPackPackage = 'agent-bundle' | 'create-agent-bundle'; + +const installedEnvironment = (): NodeJS.ProcessEnv => { + const { NODE_PATH: _nodePath, ...environment } = process.env; + return environment; +}; + +const packs = new Map>(); + +const packOnce = async (packageName: SharedPackPackage): Promise => { + const sharedDirectory = process.env['AGENT_BUNDLE_SHARED_PACK_DIR']; + if (sharedDirectory !== undefined && sharedDirectory.length > 0) { + return JSON.parse(await readFile(join(sharedDirectory, `${packageName}.json`), 'utf8')) as SharedPack; + } + // Ad-hoc single-file runs have no run-level tarball, so build once (unless + // the caller marked the workspace dist prebuilt) and pack into a + // per-process temporary directory that is dropped on exit. + if (process.env['AGENT_BUNDLE_PACKAGE_PREBUILT'] !== '1') { + await execFile('pnpm', ['build'], { cwd: workspaceRoot, env: installedEnvironment() }); + } + const destination = await mkdtemp(join(tmpdir(), 'agent-bundle-shared-pack-')); + process.once('exit', () => { + rmSync(destination, { force: true, recursive: true }); + }); + const { stdout } = await execFile('npm', ['pack', '--json', '--pack-destination', destination], { + cwd: join(workspaceRoot, 'packages', packageName), + env: installedEnvironment(), + }); + const [packOutput] = JSON.parse(stdout) as [SharedPackOutput]; + return { packOutput, tarball: join(destination, packOutput.filename) }; +}; + +/** + * Run-level release tarball for a public package. `test:packed` builds and + * `npm pack`s each package exactly once per run (scripts/run-packed-tests.mjs) + * and shares the result through AGENT_BUNDLE_SHARED_PACK_DIR, so every + * pack-and-install suite consumes the same tarball a release would publish + * instead of re-packing (and previously rebuilding) per test file. + */ +export const sharedPackedTarball = (packageName: SharedPackPackage): Promise => { + const existing = packs.get(packageName); + if (existing !== undefined) return existing; + const created = packOnce(packageName); + packs.set(packageName, created); + return created; +}; diff --git a/packages/create-agent-bundle/tests/scaffold-packed.e2e.test.ts b/packages/create-agent-bundle/tests/scaffold-packed.e2e.test.ts index 1bca9a9e3..75523ff6c 100644 --- a/packages/create-agent-bundle/tests/scaffold-packed.e2e.test.ts +++ b/packages/create-agent-bundle/tests/scaffold-packed.e2e.test.ts @@ -1,5 +1,5 @@ import { execFile as executeFile } from 'node:child_process'; -import { cp, mkdir, mkdtemp, readFile, rm, stat, writeFile } from 'node:fs/promises'; +import { mkdir, mkdtemp, readFile, rm, stat, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { pathToFileURL } from 'node:url'; @@ -7,8 +7,9 @@ import { promisify } from 'node:util'; import { afterAll, expect, it } from '@rstest/core'; +import { sharedPackedTarball } from '../../agent-bundle/tests/support/shared-pack.ts'; + const execFile = promisify(executeFile); -const workspaceRoot = process.cwd(); const installedEnvironment = (): NodeJS.ProcessEnv => { const { NODE_PATH: _nodePath, ...environment } = process.env; @@ -23,30 +24,19 @@ interface PackedFixture { } /** - * Build and `npm pack` agent-bundle and create-agent-bundle once (the - * packed-consumer mechanism: copy the package, `rslib build --dist-path` - * into the copy, pack the copy), then install the scaffolder tarball into a - * clean runner project. Every template test drives the installed bin and - * pins the framework with `--framework-version file:`, so the run - * never depends on pkg.pr.new. + * Take the run-level agent-bundle and create-agent-bundle release tarballs + * (packed once per `test:packed` run — see tests/support/shared-pack.ts), + * then install the scaffolder tarball into a clean runner project. Every + * template test drives the installed bin and pins the framework with + * `--framework-version file:`, so the run never depends on + * pkg.pr.new. */ const packFixture = async (): Promise => { const root = await mkdtemp(join(tmpdir(), 'create-agent-bundle-e2e-')); - const pack = async (packageName: string): Promise => { - const packageRoot = join(workspaceRoot, 'packages', packageName); - const packedRoot = join(root, `packed-${packageName}`); - await cp(packageRoot, packedRoot, { recursive: true }); - await execFile(join(workspaceRoot, 'node_modules', '.bin', 'rslib'), [ - 'build', '--config', join(packageRoot, 'rslib.config.ts'), '--dist-path', join(packedRoot, 'dist'), - ], { cwd: workspaceRoot, env: installedEnvironment() }); - const { stdout } = await execFile('npm', ['pack', '--json', '--pack-destination', root], { - cwd: packedRoot, - env: installedEnvironment(), - }); - return join(root, (JSON.parse(stdout) as [{ readonly filename: string }])[0].filename); - }; - const frameworkTarball = await pack('agent-bundle'); - const scaffolderTarball = await pack('create-agent-bundle'); + const [{ tarball: frameworkTarball }, { tarball: scaffolderTarball }] = await Promise.all([ + sharedPackedTarball('agent-bundle'), + sharedPackedTarball('create-agent-bundle'), + ]); const runnerRoot = join(root, 'runner'); await mkdir(runnerRoot, { recursive: true }); diff --git a/packages/workbench/tests/packed-release.e2e.test.ts b/packages/workbench/tests/packed-release.e2e.test.ts index ccc8e8550..47554d3e4 100644 --- a/packages/workbench/tests/packed-release.e2e.test.ts +++ b/packages/workbench/tests/packed-release.e2e.test.ts @@ -14,18 +14,17 @@ import { validateOutageLedger, type ConsoleErrorRecord, } from './support/packed-outage-ledger.ts'; +import { sharedPackedTarball } from '../../agent-bundle/tests/support/shared-pack.ts'; import { timeScale } from '../../agent-bundle/tests/support/time-scale.ts'; import { availablePort, awaitReady, - buildPackage, closeChild, descendantProcessIds, execFile, firstRecord, installedEnvironment, isWithin, - packageRoot, record, string, workspaceRoot, @@ -71,7 +70,7 @@ const isAppRoute = (url: URL): boolean => e2e('runs every Agent API tool from the installed tarball', { timeout: 360_000 * timeScale }, async ({ page }) => { - await buildPackage(); + const { tarball } = await sharedPackedTarball('agent-bundle'); const consumer = await mkdtemp(join(tmpdir(), 'agent-bundle-packed-release-')); const forbiddenStagedPackage = join(consumer, 'staged-package'); const project = join(consumer, 'project'); @@ -84,12 +83,6 @@ e2e('runs every Agent API tool from the installed tarball', { timeout: 360_000 * let cleanupFailure: AggregateError | undefined; let primaryFailure: Error | undefined; try { - const { stdout } = await execFile('npm', ['pack', '--json', '--pack-destination', consumer], { - cwd: packageRoot, - env: installedEnvironment(), - }); - const [packed] = JSON.parse(stdout) as Array<{ readonly filename: string }>; - const tarball = join(consumer, packed.filename); await writeFile(join(consumer, 'package.json'), '{"type":"module"}\n'); await execFile('npm', ['install', '--omit=dev', '--ignore-scripts', '--no-audit', '--no-fund', tarball], { cwd: consumer, diff --git a/packages/workbench/tests/support/packed-release-harness.ts b/packages/workbench/tests/support/packed-release-harness.ts index c1f8cc806..4249744f5 100644 --- a/packages/workbench/tests/support/packed-release-harness.ts +++ b/packages/workbench/tests/support/packed-release-harness.ts @@ -8,7 +8,6 @@ export const execFile = promisify((await import('node:child_process')).execFile) export const workspaceRoot = process.cwd(); export const packageRoot = join(workspaceRoot, 'packages', 'agent-bundle'); const packedServerStartupBudget = 45_000; -let builtPackage: Promise | undefined; export const installedEnvironment = (): NodeJS.ProcessEnv => { const { NODE_PATH: _nodePath, ...environment } = process.env; @@ -30,15 +29,6 @@ export const availablePort = async (): Promise => { return address.port; }; -export const buildPackage = (): Promise => builtPackage ??= (async (): Promise => { - if (process.env['AGENT_BUNDLE_PACKAGE_PREBUILT'] === '1') return; - const { RSTEST: _rstest, ...environment } = process.env; - await execFile('pnpm', ['build'], { - cwd: workspaceRoot, - env: { ...environment, NODE_ENV: 'production' }, - }); -})(); - export const awaitReady = async (origin: string, child: ChildProcess, output: () => string): Promise => { const startedAt = Date.now(); const diagnostics = (): string => diff --git a/rstest.integration-tests.ts b/rstest.integration-tests.ts index 3adb612fa..b815c4321 100644 --- a/rstest.integration-tests.ts +++ b/rstest.integration-tests.ts @@ -73,7 +73,8 @@ export const integrationTestFiles: readonly string[] = [ * native-host-smoke workflow keep them covered — and stay excluded from the * parallel unit pool. packed-release.e2e lives here (not in the integration * pool) so `pnpm test` and `check:release` don't each run the same long - * packed-browser suite; `rstest.config.ts` keeps `test:packed` on one worker. + * packed-browser suite; `rstest.packed.config.ts` keeps `test:packed` on one + * worker. */ export const packedTestFiles: readonly string[] = [ 'packages/agent-bundle/tests/dev-workbench-packaging.test.ts', diff --git a/rstest.packed.config.ts b/rstest.packed.config.ts new file mode 100644 index 000000000..ae33611d6 --- /dev/null +++ b/rstest.packed.config.ts @@ -0,0 +1,17 @@ +import { defineConfig } from '@rstest/core'; + +import { packedTestFiles } from './rstest.integration-tests.ts'; +import { withAgentBundleRslibConfig } from './rstest.rslib.ts'; + +/** + * Pack-and-install suites, normally launched through + * `scripts/run-packed-tests.mjs` so every file consumes one shared tarball + * per public package. The pool stays on one worker: dev-workbench-packaging + * rebuilds the workspace `dist` in place while release-audit's audit script + * packs it, so the files still contend on workspace-shared writes. + */ +export default defineConfig({ + extends: withAgentBundleRslibConfig(), + include: [...packedTestFiles], + pool: { maxWorkers: 1 }, +}); diff --git a/scripts/run-packed-tests.mjs b/scripts/run-packed-tests.mjs new file mode 100644 index 000000000..a0424da3d --- /dev/null +++ b/scripts/run-packed-tests.mjs @@ -0,0 +1,49 @@ +import { execFile as executeFile, spawn } from 'node:child_process'; +import { mkdtemp, rm, writeFile } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import { dirname, join, resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { promisify } from 'node:util'; + +const execFile = promisify(executeFile); +const repositoryRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..'); +const { NODE_PATH: _nodePath, ...environment } = process.env; + +/** + * Packs each public package once and runs the packed pool against the shared + * tarballs (tests/support/shared-pack.ts). The caller (`test:packed`) builds + * first, so the pool also runs with the prebuilt seams set instead of every + * test file rebuilding the workspace for itself. + */ +const packDirectory = await mkdtemp(join(tmpdir(), 'agent-bundle-shared-pack-')); +try { + for (const packageName of ['agent-bundle', 'create-agent-bundle']) { + const { stdout } = await execFile('npm', ['pack', '--json', '--pack-destination', packDirectory], { + cwd: join(repositoryRoot, 'packages', packageName), + env: environment, + }); + const [packOutput] = JSON.parse(stdout); + await writeFile( + join(packDirectory, `${packageName}.json`), + `${JSON.stringify({ packOutput, tarball: join(packDirectory, packOutput.filename) })}\n`, + ); + } + process.exitCode = await new Promise((resolvePromise, rejectPromise) => { + const child = spawn('pnpm', ['exec', 'rstest', '--config', 'rstest.packed.config.ts', ...process.argv.slice(2)], { + cwd: repositoryRoot, + env: { + ...environment, + AGENT_BUNDLE_PACKAGE_PREBUILT: '1', + AGENT_BUNDLE_SHARED_PACK_DIR: packDirectory, + AGENT_BUNDLE_WORKBENCH_PREBUILT: '1', + }, + stdio: 'inherit', + }); + child.once('error', rejectPromise); + child.once('exit', (code, signal) => { + resolvePromise(signal === null ? (code ?? 1) : 1); + }); + }); +} finally { + await rm(packDirectory, { force: true, recursive: true }); +} From 41858abc5990d81919fd70bb76981cf4423ac07f Mon Sep 17 00:00:00 2001 From: ScriptedAlchemy Date: Mon, 31 Aug 2026 14:39:30 +0000 Subject: [PATCH 2/8] perf(test): scaffold the three packed templates concurrently Each template test scaffolds, installs, and checks its own project under the shared runner, so nothing but the memoized pack fixture is shared. Running them concurrently cuts the scaffolder e2e from ~57s to ~19s wall on a development machine; the census is unchanged (same three tests). --- .../tests/scaffold-packed.e2e.test.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/packages/create-agent-bundle/tests/scaffold-packed.e2e.test.ts b/packages/create-agent-bundle/tests/scaffold-packed.e2e.test.ts index 75523ff6c..51b460814 100644 --- a/packages/create-agent-bundle/tests/scaffold-packed.e2e.test.ts +++ b/packages/create-agent-bundle/tests/scaffold-packed.e2e.test.ts @@ -96,7 +96,10 @@ const expectCleanValidate = async (projectRoot: string): Promise => { expect(validated.diagnostics).toEqual([]); }; -it('scaffolds the minimal template, auto-installs, and passes its own check', async () => { +// The template tests run concurrently: each scaffolds its own project +// directory under the shared runner and npm's cache tolerates concurrent +// installs, so the only shared state is the memoized fixture promise. +it.concurrent('scaffolds the minimal template, auto-installs, and passes its own check', async () => { // No --no-install: this run covers the scaffolder-driven `npm install` path. const projectRoot = await scaffoldProject('minimal', 'minimal-project', []); @@ -114,7 +117,7 @@ it('scaffolds the minimal template, auto-installs, and passes its own check', as .resolves.toContain('# Getting started'); }, 600_000); -it('scaffolds the mcp-server template and serves the conventional entry from the artifact', async () => { +it.concurrent('scaffolds the mcp-server template and serves the conventional entry from the artifact', async () => { const projectRoot = await scaffoldProject('mcp-server', 'status-plugin', ['--no-install']); await execFile('npm', ['install', '--ignore-scripts', '--no-audit', '--no-fund'], { cwd: projectRoot, @@ -149,7 +152,7 @@ it('scaffolds the mcp-server template and serves the conventional entry from the }); }, 600_000); -it('scaffolds the cli-tool template with a framework-built bin, lib, and artifact script', async () => { +it.concurrent('scaffolds the cli-tool template with a framework-built bin, lib, and artifact script', async () => { const projectRoot = await scaffoldProject('cli-tool', 'greeter', ['--no-install']); await execFile('npm', ['install', '--ignore-scripts', '--no-audit', '--no-fund'], { cwd: projectRoot, From 744a65f86f639f7dd368487f747ef485fba3ebbd Mon Sep 17 00:00:00 2001 From: ScriptedAlchemy Date: Mon, 31 Aug 2026 14:55:58 +0000 Subject: [PATCH 3/8] perf(test): stub seeded projects' defineConfig from its defining module MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit seedEvalProject's node_modules/agent-bundle shim re-exported defineConfig from src/index.ts — the whole package entry. Seeded configs load through Jiti with the module cache off, so every project's config load re-transpiled the entire package graph: a flat ~5s floor under each of the 35 eval-service tests (3m9s for the file; the slowest file in the CI-serial integration leg at ~150s) and under every other eval-project consumer. Re-export from src/core/types.ts — defineConfig's defining module and the exact symbol src/index.ts re-exports — the same way project-fixture.ts already stubs it. eval-service drops from 3m9s to 15s locally with an identical 35-test census; eval-cli, eval-workbench, agent-api, dev-artifact-service, eval-native-mount, and evals-real inherit the same floor removal. --- packages/agent-bundle/tests/support/eval-project.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/agent-bundle/tests/support/eval-project.ts b/packages/agent-bundle/tests/support/eval-project.ts index 09e5ba264..fc5c5e873 100644 --- a/packages/agent-bundle/tests/support/eval-project.ts +++ b/packages/agent-bundle/tests/support/eval-project.ts @@ -21,7 +21,11 @@ export interface SeedEvalProjectOptions { } const evalEntryPoint = resolve(process.cwd(), 'packages/agent-bundle/src/eval/index.ts'); -const sourceEntryPoint = resolve(process.cwd(), 'packages/agent-bundle/src/index.ts'); +// defineConfig's defining module, not the package entry: seeded configs load +// through Jiti with the module cache off, so re-exporting src/index.ts made +// every config load re-transpile the whole package graph (~5s per project). +// project-fixture.ts stubs the same way. +const sourceEntryPoint = resolve(process.cwd(), 'packages/agent-bundle/src/core/types.ts'); const graderModule = (expected: string): string => [ "import { readFile } from 'node:fs/promises';", From 9b49dd7d134a818a081d138bcdf49765e021df85 Mon Sep 17 00:00:00 2001 From: ScriptedAlchemy Date: Mon, 31 Aug 2026 16:10:46 +0000 Subject: [PATCH 4/8] perf(test): build and pack the shared release tarball under NODE_ENV=production Review follow-ups from the shared-pack rework: the run-level build and both npm pack invocations now run with NODE_ENV=production (matching the production-build guarantee release-audit's in-test builds used to provide), the build moved into scripts/run-packed-tests.mjs so `test:packed` has one owner for the sequence, and the ad-hoc fallback in shared-pack.ts memoizes a single process-wide build so concurrent callers (the scaffolder e2e requests both packages at once) cannot race two workspace builds. --- package.json | 2 +- .../agent-bundle/tests/support/shared-pack.ts | 14 ++++-- scripts/run-packed-tests.mjs | 49 +++++++++++-------- 3 files changed, 41 insertions(+), 24 deletions(-) diff --git a/package.json b/package.json index ee968eb82..d33b98b2d 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,7 @@ "eval:spot": "pnpm build && pnpm --filter @agent-bundle/rsc-agent-runtime-demo build && pnpm --filter @agent-bundle/rsc-agent-runtime-demo exec rstest run tests/micro-eval.spot.test.ts --config rstest.config.ts", "check:runtime-topology": "node scripts/rsc-runtime-topology.mjs --root . --output docs/architecture/rsc-runtime-workbench.md --check", "test:examples:browser": "rstest --config rstest.config.ts packages/workbench/tests/examples-real.e2e.test.ts", - "test:packed": "pnpm build && node scripts/run-packed-tests.mjs", + "test:packed": "node scripts/run-packed-tests.mjs", "test:packed:native": "rstest --config rstest.config.ts packages/agent-bundle/tests/packed-native-smoke.test.ts", "test:packed:native:claude": "pnpm build && AGENT_BUNDLE_PACKED_NATIVE_CLAUDE_SMOKE=1 pnpm test:packed:native", "test:packed:native:codex": "pnpm build && AGENT_BUNDLE_PACKED_NATIVE_CODEX_SMOKE=1 pnpm test:packed:native", diff --git a/packages/agent-bundle/tests/support/shared-pack.ts b/packages/agent-bundle/tests/support/shared-pack.ts index 451be3327..c75a00139 100644 --- a/packages/agent-bundle/tests/support/shared-pack.ts +++ b/packages/agent-bundle/tests/support/shared-pack.ts @@ -27,6 +27,7 @@ const installedEnvironment = (): NodeJS.ProcessEnv => { }; const packs = new Map>(); +let fallbackBuild: Promise | undefined; const packOnce = async (packageName: SharedPackPackage): Promise => { const sharedDirectory = process.env['AGENT_BUNDLE_SHARED_PACK_DIR']; @@ -35,9 +36,16 @@ const packOnce = async (packageName: SharedPackPackage): Promise => } // Ad-hoc single-file runs have no run-level tarball, so build once (unless // the caller marked the workspace dist prebuilt) and pack into a - // per-process temporary directory that is dropped on exit. + // per-process temporary directory that is dropped on exit. The build + // promise is process-wide so concurrent callers share one build, and it + // runs with NODE_ENV=production like the release pipeline the tarball + // stands in for. if (process.env['AGENT_BUNDLE_PACKAGE_PREBUILT'] !== '1') { - await execFile('pnpm', ['build'], { cwd: workspaceRoot, env: installedEnvironment() }); + fallbackBuild ??= execFile('pnpm', ['build'], { + cwd: workspaceRoot, + env: { ...installedEnvironment(), NODE_ENV: 'production' }, + }).then(() => undefined); + await fallbackBuild; } const destination = await mkdtemp(join(tmpdir(), 'agent-bundle-shared-pack-')); process.once('exit', () => { @@ -45,7 +53,7 @@ const packOnce = async (packageName: SharedPackPackage): Promise => }); const { stdout } = await execFile('npm', ['pack', '--json', '--pack-destination', destination], { cwd: join(workspaceRoot, 'packages', packageName), - env: installedEnvironment(), + env: { ...installedEnvironment(), NODE_ENV: 'production' }, }); const [packOutput] = JSON.parse(stdout) as [SharedPackOutput]; return { packOutput, tarball: join(destination, packOutput.filename) }; diff --git a/scripts/run-packed-tests.mjs b/scripts/run-packed-tests.mjs index a0424da3d..9826606c1 100644 --- a/scripts/run-packed-tests.mjs +++ b/scripts/run-packed-tests.mjs @@ -10,17 +10,37 @@ const repositoryRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..'); const { NODE_PATH: _nodePath, ...environment } = process.env; /** - * Packs each public package once and runs the packed pool against the shared - * tarballs (tests/support/shared-pack.ts). The caller (`test:packed`) builds - * first, so the pool also runs with the prebuilt seams set instead of every - * test file rebuilding the workspace for itself. + * Builds once, packs each public package once, and runs the packed pool + * against the shared tarballs (tests/support/shared-pack.ts) with the + * prebuilt seams set instead of every test file rebuilding the workspace for + * itself. Build and pack run with NODE_ENV=production like the release + * pipeline they stand in for; the test run itself keeps the ambient + * environment. */ +const run = (command, args, extraEnvironment = {}) => new Promise((resolvePromise, rejectPromise) => { + const child = spawn(command, args, { + cwd: repositoryRoot, + env: { ...environment, ...extraEnvironment }, + stdio: 'inherit', + }); + child.once('error', rejectPromise); + child.once('exit', (code, signal) => { + resolvePromise(signal === null ? (code ?? 1) : 1); + }); +}); + +const buildExitCode = await run('pnpm', ['build'], { NODE_ENV: 'production' }); +if (buildExitCode !== 0) { + process.exitCode = buildExitCode; + process.exit(); +} + const packDirectory = await mkdtemp(join(tmpdir(), 'agent-bundle-shared-pack-')); try { for (const packageName of ['agent-bundle', 'create-agent-bundle']) { const { stdout } = await execFile('npm', ['pack', '--json', '--pack-destination', packDirectory], { cwd: join(repositoryRoot, 'packages', packageName), - env: environment, + env: { ...environment, NODE_ENV: 'production' }, }); const [packOutput] = JSON.parse(stdout); await writeFile( @@ -28,21 +48,10 @@ try { `${JSON.stringify({ packOutput, tarball: join(packDirectory, packOutput.filename) })}\n`, ); } - process.exitCode = await new Promise((resolvePromise, rejectPromise) => { - const child = spawn('pnpm', ['exec', 'rstest', '--config', 'rstest.packed.config.ts', ...process.argv.slice(2)], { - cwd: repositoryRoot, - env: { - ...environment, - AGENT_BUNDLE_PACKAGE_PREBUILT: '1', - AGENT_BUNDLE_SHARED_PACK_DIR: packDirectory, - AGENT_BUNDLE_WORKBENCH_PREBUILT: '1', - }, - stdio: 'inherit', - }); - child.once('error', rejectPromise); - child.once('exit', (code, signal) => { - resolvePromise(signal === null ? (code ?? 1) : 1); - }); + process.exitCode = await run('pnpm', ['exec', 'rstest', '--config', 'rstest.packed.config.ts', ...process.argv.slice(2)], { + AGENT_BUNDLE_PACKAGE_PREBUILT: '1', + AGENT_BUNDLE_SHARED_PACK_DIR: packDirectory, + AGENT_BUNDLE_WORKBENCH_PREBUILT: '1', }); } finally { await rm(packDirectory, { force: true, recursive: true }); From f45dade4b02e3f3ff48db0fd510020ae32bee7f4 Mon Sep 17 00:00:00 2001 From: ScriptedAlchemy Date: Mon, 31 Aug 2026 18:57:40 +0000 Subject: [PATCH 5/8] refactor(test): consolidate packed-pool fixtures and prune refactor leftovers Review follow-ups across the packed pool: one canonical installedEnvironment and npm-install flag list exported from tests/support/shared-pack.ts (five copies deleted; the workbench harness re-exports so its consumers are untouched), the two run-level npm packs in scripts/run-packed-tests.mjs now run concurrently, packed-consumer's two disjoint consumer installs run concurrently and the test documents why it deliberately bypasses the shared tarball (its deletable pack source proves the tarball holds no path references back to the pack root), dead packageRoot/packedServerStartupBudget leftovers and the harness's inline dynamic import are gone, the stale mobile-era locals in the desktop navigation walk are renamed with the redundant viewport call dropped, and public-api-packed's 15s budget joins its siblings at 30s (it wraps a real npm install plus a tsc run). --- .../tests/dev-workbench-packaging.test.ts | 15 +++----- .../tests/packed-consumer.test.ts | 28 +++++++-------- .../tests/public-api-packed.test.ts | 10 +++--- .../agent-bundle/tests/release-audit.test.ts | 20 +++++------ .../rsc-runtime-optional-packaging.test.ts | 4 +-- .../agent-bundle/tests/support/shared-pack.ts | 5 ++- .../tests/scaffold-packed.e2e.test.ts | 20 +++++------ .../tests/packed-release.e2e.test.ts | 35 +++++++++---------- .../tests/support/packed-release-harness.ts | 14 +++----- scripts/run-packed-tests.mjs | 25 ++++++------- 10 files changed, 78 insertions(+), 98 deletions(-) diff --git a/packages/agent-bundle/tests/dev-workbench-packaging.test.ts b/packages/agent-bundle/tests/dev-workbench-packaging.test.ts index c195c2785..326ffde08 100644 --- a/packages/agent-bundle/tests/dev-workbench-packaging.test.ts +++ b/packages/agent-bundle/tests/dev-workbench-packaging.test.ts @@ -7,7 +7,7 @@ import { promisify } from 'node:util'; import { describe, expect, it } from '@rstest/core'; -import { sharedPackedTarball } from './support/shared-pack.ts'; +import { installedEnvironment, npmInstallArguments, sharedPackedTarball } from './support/shared-pack.ts'; const execFile = promisify(executeFile); const workspaceRoot = process.cwd(); @@ -16,11 +16,6 @@ const workbenchRoot = join(workspaceRoot, 'packages', 'workbench'); const appRendererLicense = join('src', 'mcp', 'APP-RENDERER-LICENSE'); let built: Promise | undefined; -const packedEnvironment = (): NodeJS.ProcessEnv => { - const { NODE_PATH: _nodePath, ...environment } = process.env; - return environment; -}; - const buildPackage = async (force = false): Promise => { if (force) { // The stale-asset pruning test rebuilds on purpose; the prebuilt seam @@ -88,7 +83,7 @@ it('serves prebuilt workbench assets from an installed tarball without the repos expect(listing.stdout).not.toMatch(/package\/dist\/workbench\/.*-[a-f0-9]{8,}/iu); await writeFile(join(consumer, 'package.json'), '{"type":"module"}\n'); - await execFile('npm', ['install', '--ignore-scripts', '--no-audit', '--no-fund', tarball], { cwd: consumer }); + await execFile('npm', ['install', ...npmInstallArguments, tarball], { cwd: consumer }); await mkdir(join(project, 'skills', 'review'), { recursive: true }); await Promise.all([ writeFile(join(project, 'package.json'), '{"type":"module"}\n'), @@ -120,9 +115,7 @@ it('runs the Agent API from an omit-dev installed tarball with its runtime MCP d const project = join(consumer, 'project'); try { await writeFile(join(consumer, 'package.json'), '{"type":"module"}\n'); - await execFile('npm', [ - 'install', '--omit=dev', '--ignore-scripts', '--no-audit', '--no-fund', tarball, - ], { cwd: consumer }); + await execFile('npm', ['install', '--omit=dev', ...npmInstallArguments, tarball], { cwd: consumer }); await mkdir(join(project, 'skills', 'review'), { recursive: true }); await Promise.all([ writeFile(join(project, 'package.json'), '{"type":"module"}\n'), @@ -152,7 +145,7 @@ it('runs the Agent API from an omit-dev installed tarball with its runtime MCP d ].join('\n'); const result = await execFile(process.execPath, ['--input-type=module', '--eval', script], { cwd: consumer, - env: { ...packedEnvironment(), AGENT_BUNDLE_AGENT_API_TOKEN: 'packed-agent-api-token' }, + env: { ...installedEnvironment(), AGENT_BUNDLE_AGENT_API_TOKEN: 'packed-agent-api-token' }, }); expect(JSON.parse(result.stdout)).toEqual({ runtime: ['function', 'function', 'function'], diff --git a/packages/agent-bundle/tests/packed-consumer.test.ts b/packages/agent-bundle/tests/packed-consumer.test.ts index 476c2798a..10b639724 100644 --- a/packages/agent-bundle/tests/packed-consumer.test.ts +++ b/packages/agent-bundle/tests/packed-consumer.test.ts @@ -20,6 +20,7 @@ import { promisify } from 'node:util'; import { expect, it } from '@rstest/core'; import { sha256Hex } from '../src/core/digest.ts'; +import { installedEnvironment, npmInstallArguments } from './support/shared-pack.ts'; const execFile = promisify(executeFile); const workspaceRoot = process.cwd(); @@ -37,11 +38,6 @@ interface ManifestDigest extends FileDigest { readonly mode?: number; } -const installedEnvironment = (): NodeJS.ProcessEnv => { - const { NODE_PATH: _nodePath, ...environment } = process.env; - return environment; -}; - const artifactDigest = async (root: string): Promise => { const collect = async (directory: string): Promise => { const entries = await readdir(directory, { withFileTypes: true }); @@ -78,6 +74,11 @@ it('uses only an installed tarball after source deletion', async () => { const artifact = join(projectRoot, 'artifact with spaces'); try { + // Deliberately not sharedPackedTarball: this test packs from a deletable + // copy of the package and removes that copy after install, proving the + // tarball's contents hold no path references back to the pack source. The + // shared tarball packs from the live workspace, which survives the run, + // so a leaked path would resolve and pass silently. await cp(packageRoot, packedPackageRoot, { recursive: true }); await execFile(join(workspaceRoot, 'node_modules', '.bin', 'rslib'), [ 'build', '--config', join(packageRoot, 'rslib.config.ts'), '--dist-path', join(packedPackageRoot, 'dist'), @@ -97,10 +98,6 @@ it('uses only an installed tarball after source deletion', async () => { stat(join(projectRoot, 'src', 'shell.sh')).then((metadata) => metadata.mode & 0o777), stat(join(projectRoot, 'src', 'python.py')).then((metadata) => metadata.mode & 0o777), ]); - await execFile('npm', ['install', '--ignore-scripts', '--no-audit', '--no-fund', tarball], { - cwd: projectRoot, - env: installedEnvironment(), - }); await mkdir(scriptProjectRoot, { recursive: true }); await Promise.all([ writeFile(join(scriptProjectRoot, 'package.json'), '{"type":"module"}\n'), @@ -110,10 +107,13 @@ it('uses only an installed tarball after source deletion', async () => { ), writeFile(join(scriptProjectRoot, 'shell.sh'), "printf 'packed script stdout\\n'\nprintf 'packed script stderr\\n' >&2\n"), ]); - await execFile('npm', ['install', '--ignore-scripts', '--no-audit', '--no-fund', tarball], { - cwd: scriptProjectRoot, - env: installedEnvironment(), - }); + // The two consumers are disjoint directories; npm's cache handles the + // concurrent installs (the scaffolder e2e relies on the same property). + await Promise.all([projectRoot, scriptProjectRoot].map(async (consumer) => + execFile('npm', ['install', ...npmInstallArguments, tarball], { + cwd: consumer, + env: installedEnvironment(), + }))); const cli = join(projectRoot, 'node_modules', '.bin', 'agent-bundle'); const installedPackage = await realpath(join(projectRoot, 'node_modules', 'agent-bundle')); @@ -331,7 +331,7 @@ it('uses only an installed tarball after source deletion', async () => { '', ].join('\n')), ]); - await execFile('npm', ['install', '--ignore-scripts', '--no-audit', '--no-fund', tarball], { + await execFile('npm', ['install', ...npmInstallArguments, tarball], { cwd: frameworkRoot, env: installedEnvironment(), }); diff --git a/packages/agent-bundle/tests/public-api-packed.test.ts b/packages/agent-bundle/tests/public-api-packed.test.ts index 651e4c01b..5db4ae6bf 100644 --- a/packages/agent-bundle/tests/public-api-packed.test.ts +++ b/packages/agent-bundle/tests/public-api-packed.test.ts @@ -7,7 +7,7 @@ import { promisify } from 'node:util'; import { expect, it } from '@rstest/core'; import { writeFixtureManifest } from './support/manifest.ts'; -import { sharedPackedTarball } from './support/shared-pack.ts'; +import { npmInstallArguments, sharedPackedTarball } from './support/shared-pack.ts'; interface PackageManifest { bin: { @@ -58,7 +58,7 @@ it('writes the package version as the producer of a packed CLI manifest', async try { await writeFile(join(consumerRoot, 'package.json'), '{"type":"module"}\n'); await execFile( - 'npm', ['install', '--ignore-scripts', '--no-audit', '--no-fund', tarball], + 'npm', ['install', ...npmInstallArguments, tarball], { cwd: consumerRoot }, ); @@ -90,7 +90,7 @@ it('imports the externalized config entry from a packed npm consumer', async () await writeFile(join(consumerRoot, 'package.json'), '{"type":"module"}\n'); await execFile( 'npm', - ['install', '--ignore-scripts', '--no-audit', '--no-fund', tarball], + ['install', ...npmInstallArguments, tarball], { cwd: consumerRoot }, ); @@ -142,7 +142,7 @@ it('imports the externalized config entry from a packed npm consumer', async () } finally { await rm(consumerRoot, { force: true, recursive: true }); } -}, 15_000); +}, 30_000); it('invokes a prebuilt MCP server from a clean packed consumer', async () => { const { tarball } = await sharedPackedTarball('agent-bundle'); @@ -198,7 +198,7 @@ it('invokes a prebuilt MCP server from a clean packed consumer', async () => { await writeFile(join(consumerRoot, 'package.json'), '{"type":"module"}\n'); await execFile( 'npm', - ['install', '--ignore-scripts', '--no-audit', '--no-fund', tarball], + ['install', ...npmInstallArguments, tarball], { cwd: consumerRoot }, ); const { stdout } = await execFile(process.execPath, [ diff --git a/packages/agent-bundle/tests/release-audit.test.ts b/packages/agent-bundle/tests/release-audit.test.ts index c870928b2..d8d8a70ba 100644 --- a/packages/agent-bundle/tests/release-audit.test.ts +++ b/packages/agent-bundle/tests/release-audit.test.ts @@ -6,7 +6,7 @@ import { promisify } from 'node:util'; import { expect, it } from '@rstest/core'; -import { sharedPackedTarball } from './support/shared-pack.ts'; +import { npmInstallArguments, sharedPackedTarball } from './support/shared-pack.ts'; const execFile = promisify(executeFile); const workspaceRoot = process.cwd(); @@ -46,7 +46,12 @@ it('audits an externally installed production tarball and generates its CycloneD const componentReferences = new Set(components.flatMap((component) => ( component['bom-ref'] === undefined ? [] : [component['bom-ref']] ))); - const dependencyReferences = new Set([root?.['bom-ref'], ...componentReferences]); + const rootReference = root?.['bom-ref']; + expect(rootReference).toBeDefined(); + const dependencyReferences = new Set([ + ...(rootReference === undefined ? [] : [rootReference]), + ...componentReferences, + ]); const declaredDependencyReferences = new Set((sbom.dependencies ?? []).flatMap((dependency) => ( dependency.ref === undefined ? [] : [dependency.ref] ))); @@ -60,9 +65,7 @@ it('audits an externally installed production tarball and generates its CycloneD expect(product).toBeDefined(); expect(rootDependencies?.dependsOn).toContain(product?.['bom-ref']); expect(productDependencies?.dependsOn?.length).toBeGreaterThan(0); - expect([...dependencyReferences]).not.toContain(undefined); - expect([...dependencyReferences].filter((reference): reference is string => reference !== undefined) - .every((reference) => declaredDependencyReferences.has(reference))).toBe(true); + expect([...dependencyReferences].every((reference) => declaredDependencyReferences.has(reference))).toBe(true); expect((sbom.dependencies ?? []).every((dependency) => ( dependency.ref !== undefined && dependencyReferences.has(dependency.ref) @@ -122,12 +125,7 @@ it('installs public entrypoints and an externally resolved CLI for production co const { tarball } = await sharedPackedTarball('agent-bundle'); await writeFile(join(consumerRoot, 'package.json'), '{"private":true,"type":"module"}\n'); await execFile('npm', [ - 'install', - '--omit=dev', - '--ignore-scripts', - '--no-audit', - '--no-fund', - tarball, + 'install', '--omit=dev', ...npmInstallArguments, tarball, ], { cwd: consumerRoot, env: releaseEnvironment() }); const installedPackageRoot = await realpath(join(consumerRoot, 'node_modules', 'agent-bundle')); diff --git a/packages/agent-bundle/tests/rsc-runtime-optional-packaging.test.ts b/packages/agent-bundle/tests/rsc-runtime-optional-packaging.test.ts index ea04e3a36..ee8477592 100644 --- a/packages/agent-bundle/tests/rsc-runtime-optional-packaging.test.ts +++ b/packages/agent-bundle/tests/rsc-runtime-optional-packaging.test.ts @@ -6,7 +6,7 @@ import { promisify } from 'node:util'; import { describe, expect, it } from '@rstest/core'; -import { sharedPackedTarball } from './support/shared-pack.ts'; +import { npmInstallArguments, sharedPackedTarball } from './support/shared-pack.ts'; const execFile = promisify(executeFile); const workspaceRoot = process.cwd(); @@ -45,7 +45,7 @@ describe.sequential('optional RSC runtime package boundary', () => { const tarListing = (await execFile('tar', ['-tf', tarball])).stdout; expect(tarListing).not.toMatch(/examples\/rsc-agent-runtime|react-server-dom-rspack|rsbuild-plugin-rsc/u); - await execFile('npm', ['install', '--ignore-scripts', '--no-audit', '--no-fund', tarball], { cwd: consumer }); + await execFile('npm', ['install', ...npmInstallArguments, tarball], { cwd: consumer }); const dependencyTree = JSON.parse((await execFile('npm', ['ls', '--all', '--json'], { cwd: consumer })).stdout) as InstalledDependencyTree; const installedNames = installedDependencyNames(dependencyTree); for (const name of ['react', 'react-dom', 'react-server-dom-rspack', 'rsbuild-plugin-rsc']) { diff --git a/packages/agent-bundle/tests/support/shared-pack.ts b/packages/agent-bundle/tests/support/shared-pack.ts index c75a00139..124584daf 100644 --- a/packages/agent-bundle/tests/support/shared-pack.ts +++ b/packages/agent-bundle/tests/support/shared-pack.ts @@ -21,11 +21,14 @@ export interface SharedPack { export type SharedPackPackage = 'agent-bundle' | 'create-agent-bundle'; -const installedEnvironment = (): NodeJS.ProcessEnv => { +export const installedEnvironment = (): NodeJS.ProcessEnv => { const { NODE_PATH: _nodePath, ...environment } = process.env; return environment; }; +/** Canonical flags for installing a packed tarball into a consumer fixture. */ +export const npmInstallArguments = ['--ignore-scripts', '--no-audit', '--no-fund'] as const; + const packs = new Map>(); let fallbackBuild: Promise | undefined; diff --git a/packages/create-agent-bundle/tests/scaffold-packed.e2e.test.ts b/packages/create-agent-bundle/tests/scaffold-packed.e2e.test.ts index 51b460814..10d712ac5 100644 --- a/packages/create-agent-bundle/tests/scaffold-packed.e2e.test.ts +++ b/packages/create-agent-bundle/tests/scaffold-packed.e2e.test.ts @@ -7,15 +7,10 @@ import { promisify } from 'node:util'; import { afterAll, expect, it } from '@rstest/core'; -import { sharedPackedTarball } from '../../agent-bundle/tests/support/shared-pack.ts'; +import { installedEnvironment, npmInstallArguments, sharedPackedTarball } from '../../agent-bundle/tests/support/shared-pack.ts'; const execFile = promisify(executeFile); -const installedEnvironment = (): NodeJS.ProcessEnv => { - const { NODE_PATH: _nodePath, ...environment } = process.env; - return environment; -}; - interface PackedFixture { readonly frameworkTarball: string; readonly root: string; @@ -41,7 +36,7 @@ const packFixture = async (): Promise => { const runnerRoot = join(root, 'runner'); await mkdir(runnerRoot, { recursive: true }); await writeFile(join(runnerRoot, 'package.json'), '{"name":"scaffold-runner","type":"module","private":true}\n'); - await execFile('npm', ['install', '--ignore-scripts', '--no-audit', '--no-fund', scaffolderTarball], { + await execFile('npm', ['install', ...npmInstallArguments, scaffolderTarball], { cwd: runnerRoot, env: installedEnvironment(), }); @@ -61,7 +56,7 @@ const fixture = (): Promise => { afterAll(async () => { if (fixturePromise === undefined) return; - const { root } = await fixture(); + const { root } = await fixturePromise; await rm(root, { force: true, recursive: true }); }); @@ -82,8 +77,9 @@ const scaffoldProject = async ( return join(runnerRoot, projectName); }; -const npmRun = async (projectRoot: string, script: string): Promise<{ readonly stdout: string }> => - execFile('npm', ['run', script], { cwd: projectRoot, env: installedEnvironment() }); +const npmRun = async (projectRoot: string, script: string): Promise => { + await execFile('npm', ['run', script], { cwd: projectRoot, env: installedEnvironment() }); +}; /** Zero diagnostics — including the informational AB473x migration nudges. */ const expectCleanValidate = async (projectRoot: string): Promise => { @@ -119,7 +115,7 @@ it.concurrent('scaffolds the minimal template, auto-installs, and passes its own it.concurrent('scaffolds the mcp-server template and serves the conventional entry from the artifact', async () => { const projectRoot = await scaffoldProject('mcp-server', 'status-plugin', ['--no-install']); - await execFile('npm', ['install', '--ignore-scripts', '--no-audit', '--no-fund'], { + await execFile('npm', ['install', ...npmInstallArguments], { cwd: projectRoot, env: installedEnvironment(), }); @@ -154,7 +150,7 @@ it.concurrent('scaffolds the mcp-server template and serves the conventional ent it.concurrent('scaffolds the cli-tool template with a framework-built bin, lib, and artifact script', async () => { const projectRoot = await scaffoldProject('cli-tool', 'greeter', ['--no-install']); - await execFile('npm', ['install', '--ignore-scripts', '--no-audit', '--no-fund'], { + await execFile('npm', ['install', ...npmInstallArguments], { cwd: projectRoot, env: installedEnvironment(), }); diff --git a/packages/workbench/tests/packed-release.e2e.test.ts b/packages/workbench/tests/packed-release.e2e.test.ts index 47554d3e4..dfe352f39 100644 --- a/packages/workbench/tests/packed-release.e2e.test.ts +++ b/packages/workbench/tests/packed-release.e2e.test.ts @@ -14,7 +14,7 @@ import { validateOutageLedger, type ConsoleErrorRecord, } from './support/packed-outage-ledger.ts'; -import { sharedPackedTarball } from '../../agent-bundle/tests/support/shared-pack.ts'; +import { npmInstallArguments, sharedPackedTarball } from '../../agent-bundle/tests/support/shared-pack.ts'; import { timeScale } from '../../agent-bundle/tests/support/time-scale.ts'; import { availablePort, @@ -35,7 +35,6 @@ import { workbenchUrl } from './support/workbench-e2e.ts'; const fixtureRoot = join(workspaceRoot, 'fixtures', 'integration', 'packed-release'); const browserTimeout = 12_000 * timeScale; -const packedServerStartupBudget = 45_000 * timeScale; const productTemporaryRootPrefixes = [ 'agent-bundle-hook-playground-', 'agent-bundle-mcp-', @@ -84,7 +83,7 @@ e2e('runs every Agent API tool from the installed tarball', { timeout: 360_000 * let primaryFailure: Error | undefined; try { await writeFile(join(consumer, 'package.json'), '{"type":"module"}\n'); - await execFile('npm', ['install', '--omit=dev', '--ignore-scripts', '--no-audit', '--no-fund', tarball], { + await execFile('npm', ['install', '--omit=dev', ...npmInstallArguments, tarball], { cwd: consumer, env: installedEnvironment(), }); @@ -468,8 +467,7 @@ e2e('runs every Agent API tool from the installed tarball', { timeout: 360_000 * const exportedScriptSection = page.getByRole('heading', { name: 'Exported trace' }).locator('..'); await expect(exportedScriptSection).toContainText(scriptSessionId); await expect(exportedScriptSection).toContainText(scriptCompletedReference); - const scriptSelectedCheckbox = scriptCompletedCheckbox; - await scriptSelectedCheckbox.check(); + await scriptCompletedCheckbox.check(); await expect(page.getByRole('button', { name: 'Promote to draft eval case' })).toBeEnabled({ timeout: browserTimeout }); const promotedScriptResponse = page.waitForResponse((response) => response.url() === `${origin}/api/playground/sessions/${encodeURIComponent(scriptSessionId)}/draft-eval` && @@ -942,9 +940,8 @@ e2e('runs every Agent API tool from the installed tarball', { timeout: 360_000 * const browserMcpSessionBCloseCompletedAt = Date.now(); phase = 'desktop navigation floor'; - await page.setViewportSize({ height: 900, width: 1440 }); - const mobileNavigationRequestIndex = browserRequests.length; - const mobileRoutes: readonly Readonly<{ heading: string; label: string }>[] = [ + const navigationFloorRequestIndex = browserRequests.length; + const navigationRoutes: readonly Readonly<{ heading: string; label: string }>[] = [ { heading: 'Bundle dashboard', label: 'Overview' }, { heading: 'Skills', label: 'Skills' }, { heading: 'Hooks', label: 'Hooks' }, { heading: 'MCP playground', label: 'MCP playground' }, { heading: 'Artifacts', label: 'Artifacts' }, { heading: 'Playground', label: 'Playground' }, { heading: 'Logs', label: 'Logs' }, { heading: 'Evals', label: 'Evals' }, { heading: 'Comparisons', label: 'Comparisons' }, @@ -964,20 +961,20 @@ e2e('runs every Agent API tool from the installed tarball', { timeout: 360_000 * respondedStream?: true; url: string; }>> = []; - let activeMobileRoute: Readonly<{ openedAt: number; urls?: readonly string[] }> | undefined; - const leaveActiveMobileRoute = (leftAt: number): void => { - if (activeMobileRoute === undefined) return; - for (const url of activeMobileRoute.urls ?? []) postRecoveryNavigation.push(Object.freeze({ + let activeNavigationRoute: Readonly<{ openedAt: number; urls?: readonly string[] }> | undefined; + const leaveActiveNavigationRoute = (leftAt: number): void => { + if (activeNavigationRoute === undefined) return; + for (const url of activeNavigationRoute.urls ?? []) postRecoveryNavigation.push(Object.freeze({ leftAt, - openedAt: activeMobileRoute.openedAt, + openedAt: activeNavigationRoute.openedAt, ...(respondedNavigationStreams.has(url) ? { respondedStream: true as const } : {}), url, })); - activeMobileRoute = undefined; + activeNavigationRoute = undefined; }; - for (const route of mobileRoutes) { + for (const route of navigationRoutes) { const openedAt = Date.now(); - leaveActiveMobileRoute(openedAt); + leaveActiveNavigationRoute(openedAt); const logsStreamResponse = route.label === 'Logs' ? page.waitForResponse((response) => { const url = new URL(response.url()); @@ -1003,13 +1000,13 @@ e2e('runs every Agent API tool from the installed tarball', { timeout: 360_000 * respondedNavigationStreams.add(streamUrl); routeUrls.push(streamUrl); } - activeMobileRoute = Object.freeze({ openedAt, urls: Object.freeze(routeUrls) }); + activeNavigationRoute = Object.freeze({ openedAt, urls: Object.freeze(routeUrls) }); } - leaveActiveMobileRoute(Date.now()); + leaveActiveNavigationRoute(Date.now()); await page.getByRole('link', { name: 'Overview', exact: true }).focus(); await page.keyboard.press('Enter'); await expect(page.getByRole('heading', { name: 'Bundle dashboard' })).toBeVisible({ timeout: browserTimeout }); - await waitForBrowserRequestsAfter(mobileNavigationRequestIndex); + await waitForBrowserRequestsAfter(navigationFloorRequestIndex); phase = 'foreground outage ledger quiet fence'; const requestFailuresBeforeQuietFence = browserRequests.filter((request) => request.error !== undefined); diff --git a/packages/workbench/tests/support/packed-release-harness.ts b/packages/workbench/tests/support/packed-release-harness.ts index 4249744f5..68478f839 100644 --- a/packages/workbench/tests/support/packed-release-harness.ts +++ b/packages/workbench/tests/support/packed-release-harness.ts @@ -1,19 +1,15 @@ -import type { ChildProcess } from 'node:child_process'; +import { execFile as executeFile, type ChildProcess } from 'node:child_process'; import { chmod, mkdir, writeFile } from 'node:fs/promises'; import { createServer } from 'node:net'; -import { join, relative, isAbsolute } from 'node:path'; +import { relative, isAbsolute, join } from 'node:path'; import { promisify } from 'node:util'; -export const execFile = promisify((await import('node:child_process')).execFile); +export { installedEnvironment } from '../../../agent-bundle/tests/support/shared-pack.ts'; + +export const execFile = promisify(executeFile); export const workspaceRoot = process.cwd(); -export const packageRoot = join(workspaceRoot, 'packages', 'agent-bundle'); const packedServerStartupBudget = 45_000; -export const installedEnvironment = (): NodeJS.ProcessEnv => { - const { NODE_PATH: _nodePath, ...environment } = process.env; - return environment; -}; - export const availablePort = async (): Promise => { const server = createServer(); await new Promise((resolvePromise, rejectPromise) => { diff --git a/scripts/run-packed-tests.mjs b/scripts/run-packed-tests.mjs index 9826606c1..15d87d9f1 100644 --- a/scripts/run-packed-tests.mjs +++ b/scripts/run-packed-tests.mjs @@ -1,3 +1,11 @@ +/** + * Builds once, packs each public package once, and runs the packed pool + * against the shared tarballs (tests/support/shared-pack.ts) with the + * prebuilt seams set instead of every test file rebuilding the workspace for + * itself. Build and pack run with NODE_ENV=production like the release + * pipeline they stand in for; every child inherits the ambient environment + * minus NODE_PATH. + */ import { execFile as executeFile, spawn } from 'node:child_process'; import { mkdtemp, rm, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; @@ -9,14 +17,6 @@ const execFile = promisify(executeFile); const repositoryRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..'); const { NODE_PATH: _nodePath, ...environment } = process.env; -/** - * Builds once, packs each public package once, and runs the packed pool - * against the shared tarballs (tests/support/shared-pack.ts) with the - * prebuilt seams set instead of every test file rebuilding the workspace for - * itself. Build and pack run with NODE_ENV=production like the release - * pipeline they stand in for; the test run itself keeps the ambient - * environment. - */ const run = (command, args, extraEnvironment = {}) => new Promise((resolvePromise, rejectPromise) => { const child = spawn(command, args, { cwd: repositoryRoot, @@ -30,14 +30,11 @@ const run = (command, args, extraEnvironment = {}) => new Promise((resolvePromis }); const buildExitCode = await run('pnpm', ['build'], { NODE_ENV: 'production' }); -if (buildExitCode !== 0) { - process.exitCode = buildExitCode; - process.exit(); -} +if (buildExitCode !== 0) process.exit(buildExitCode); const packDirectory = await mkdtemp(join(tmpdir(), 'agent-bundle-shared-pack-')); try { - for (const packageName of ['agent-bundle', 'create-agent-bundle']) { + await Promise.all(['agent-bundle', 'create-agent-bundle'].map(async (packageName) => { const { stdout } = await execFile('npm', ['pack', '--json', '--pack-destination', packDirectory], { cwd: join(repositoryRoot, 'packages', packageName), env: { ...environment, NODE_ENV: 'production' }, @@ -47,7 +44,7 @@ try { join(packDirectory, `${packageName}.json`), `${JSON.stringify({ packOutput, tarball: join(packDirectory, packOutput.filename) })}\n`, ); - } + })); process.exitCode = await run('pnpm', ['exec', 'rstest', '--config', 'rstest.packed.config.ts', ...process.argv.slice(2)], { AGENT_BUNDLE_PACKAGE_PREBUILT: '1', AGENT_BUNDLE_SHARED_PACK_DIR: packDirectory, From 0bf2d54e395a36169204277f3851bb57d24cf5a5 Mon Sep 17 00:00:00 2001 From: ScriptedAlchemy Date: Mon, 31 Aug 2026 19:54:08 +0000 Subject: [PATCH 6/8] perf(ci): move the scaffolder template matrix to the release boundary MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Frequency policy for the packed pool: per-PR release gates keep the single-cycle consumer proofs (pack once, install once per contract) plus one full scaffolder journey — the minimal template, which covers the installed scaffolder bin, template scaffold, scaffolder-driven npm install, project check, and clean validate. The mcp-server and cli-tool template runs move to scaffold-packed-matrix.e2e.test.ts, which runs in `test:packed:release` (pre-publish check:release and a new nightly CI schedule), not per PR. The shared scaffold fixture moves to tests/support/scaffold-fixture.ts; test names are unchanged, so the release-boundary pool census is identical to the old per-PR pool census, and the per-PR pool drops exactly the two matrix templates. CI's release-gates job now runs check:release:ci (per-PR pool); the nightly packed-matrix job runs the full check:release. --- .github/workflows/ci.yml | 29 ++++ package.json | 4 +- .../tests/scaffold-packed-matrix.e2e.test.ts | 89 ++++++++++ .../tests/scaffold-packed.e2e.test.ts | 164 +----------------- .../tests/support/scaffold-fixture.ts | 93 ++++++++++ rstest.integration-tests.ts | 21 ++- rstest.packed.config.ts | 11 +- scripts/run-packed-tests.mjs | 9 +- 8 files changed, 254 insertions(+), 166 deletions(-) create mode 100644 packages/create-agent-bundle/tests/scaffold-packed-matrix.e2e.test.ts create mode 100644 packages/create-agent-bundle/tests/support/scaffold-fixture.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 474b0cafe..cba49f12c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -4,6 +4,10 @@ on: pull_request: push: branches: [main] + # Nightly release-boundary matrix (packed-matrix job): the scaffolder + # template tests beyond the per-PR minimal-template smoke. + schedule: + - cron: '17 6 * * *' workflow_dispatch: permissions: @@ -16,6 +20,7 @@ concurrency: jobs: # Builds and checks every public example through its own toolchain. examples-check: + if: github.event_name != 'schedule' name: Examples check (Node 22.19) runs-on: ubuntu-latest timeout-minutes: 25 @@ -30,6 +35,7 @@ jobs: - run: pnpm examples:check verify: + if: github.event_name != 'schedule' name: Verify (Node ${{ matrix.node-version }}) runs-on: ubuntu-latest timeout-minutes: 45 @@ -56,9 +62,31 @@ jobs: - run: pnpm test release-gates: + if: github.event_name != 'schedule' name: Release gates (Node 22.19) runs-on: ubuntu-latest timeout-minutes: 30 + steps: + - uses: actions/checkout@v7 + - uses: pnpm/setup@v2 + with: + cache: true + install: false + runtime: node@22.19.0 + - run: pnpm install --frozen-lockfile + - run: pnpm exec playwright install --with-deps chrome + # Per-PR packed pool: single pack+install proofs plus the + # minimal-template scaffolder smoke. The full template matrix runs in + # the nightly packed-matrix job and in pre-publish `pnpm check:release`. + - run: pnpm check:release:ci + + # Release-boundary scaffolder template matrix (mcp-server, cli-tool) plus + # the full packed pool — the nightly form of pre-publish `check:release`. + packed-matrix: + if: github.event_name == 'schedule' + name: Packed release matrix (Node 22.19) + runs-on: ubuntu-latest + timeout-minutes: 40 steps: - uses: actions/checkout@v7 - uses: pnpm/setup@v2 @@ -75,6 +103,7 @@ jobs: # (hook -> RSC worker -> shared kernel state -> MCP tool lowering) without # any real Claude/Codex host. Real native-host smokes stay skip-gated in # the manually dispatched native-host-smoke workflow on purpose. + if: github.event_name != 'schedule' name: RSC runtime micro-eval (Node 22.19) runs-on: ubuntu-latest timeout-minutes: 15 diff --git a/package.json b/package.json index d33b98b2d..d28b8786f 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,7 @@ "check:runtime-topology": "node scripts/rsc-runtime-topology.mjs --root . --output docs/architecture/rsc-runtime-workbench.md --check", "test:examples:browser": "rstest --config rstest.config.ts packages/workbench/tests/examples-real.e2e.test.ts", "test:packed": "node scripts/run-packed-tests.mjs", + "test:packed:release": "node scripts/run-packed-tests.mjs --release", "test:packed:native": "rstest --config rstest.config.ts packages/agent-bundle/tests/packed-native-smoke.test.ts", "test:packed:native:claude": "pnpm build && AGENT_BUNDLE_PACKED_NATIVE_CLAUDE_SMOKE=1 pnpm test:packed:native", "test:packed:native:codex": "pnpm build && AGENT_BUNDLE_PACKED_NATIVE_CODEX_SMOKE=1 pnpm test:packed:native", @@ -32,7 +33,8 @@ "preview:publish": "pkg-pr-new publish --previewVersion --peerDeps --no-compact --no-template './packages/agent-bundle' './packages/rsc-runtime' './packages/create-agent-bundle'", "pack:dry-run": "pnpm build && npm pack ./packages/agent-bundle --dry-run --json", "audit:release": "pnpm lint:package && attw --pack --profile esm-only packages/agent-bundle && node scripts/audit-packed-release.mjs", - "check:release": "pnpm pack:dry-run && pnpm audit:release && pnpm test:packed", + "check:release": "pnpm pack:dry-run && pnpm audit:release && pnpm test:packed:release", + "check:release:ci": "pnpm pack:dry-run && pnpm audit:release && pnpm test:packed", "example:hooks": "pnpm build && pnpm --filter @agent-bundle-example/hooks-and-scripts dev", "example:audiobook": "pnpm build && pnpm --filter @agent-bundle-example/audiobook-curator dev", "example:mcp-app": "pnpm build && pnpm --filter @agent-bundle-example/mcp-app dev", diff --git a/packages/create-agent-bundle/tests/scaffold-packed-matrix.e2e.test.ts b/packages/create-agent-bundle/tests/scaffold-packed-matrix.e2e.test.ts new file mode 100644 index 000000000..7cd602339 --- /dev/null +++ b/packages/create-agent-bundle/tests/scaffold-packed-matrix.e2e.test.ts @@ -0,0 +1,89 @@ +import { execFile as executeFile } from 'node:child_process'; +import { readFile, stat } from 'node:fs/promises'; +import { join } from 'node:path'; +import { pathToFileURL } from 'node:url'; +import { promisify } from 'node:util'; + +import { afterAll, expect, it } from '@rstest/core'; + +import { installedEnvironment, npmInstallArguments } from '../../agent-bundle/tests/support/shared-pack.ts'; +import { cleanupScaffoldFixture, expectCleanValidate, npmRun, scaffoldProject } from './support/scaffold-fixture.ts'; + +const execFile = promisify(executeFile); + +afterAll(cleanupScaffoldFixture); + +/** + * Release-boundary template matrix: the remaining scaffolder templates, each + * through scaffold, install, check, and validate. Runs in + * `test:packed:release` (check:release and the nightly CI schedule), not in + * the per-PR packed pool — the per-PR scaffolder proof is the minimal-template + * smoke in scaffold-packed.e2e.test.ts. The template tests run concurrently: + * each scaffolds its own project directory under the shared runner and npm's + * cache tolerates concurrent installs, so the only shared state is the + * memoized fixture promise. + */ +it.concurrent('scaffolds the mcp-server template and serves the conventional entry from the artifact', async () => { + const projectRoot = await scaffoldProject('mcp-server', 'status-plugin', ['--no-install']); + await execFile('npm', ['install', ...npmInstallArguments], { + cwd: projectRoot, + env: installedEnvironment(), + }); + + await npmRun(projectRoot, 'check'); + await expectCleanValidate(projectRoot); + + const artifact = join(projectRoot, 'artifact'); + const manifest = JSON.parse(await readFile(join(artifact, 'portable', 'mcp.json'), 'utf8')) as { + readonly mcpServers: { readonly status: { readonly args: readonly [string, ...string[]] } }; + }; + const entry = join(artifact, 'portable', manifest.mcpServers.status.args[0]); + // The factory export was wrapped in the framework stdio lifecycle shell. + await expect(readFile(entry, 'utf8')).resolves.toContain('stdio heartbeat'); + + const cli = join(projectRoot, 'node_modules', '.bin', 'agent-bundle'); + const { stdout: listed } = await execFile(cli, [ + 'mcp', 'list', '--json', '--root', projectRoot, '--artifact', artifact, '--target', 'portable', '--server', 'status', + ], { cwd: projectRoot, env: installedEnvironment() }); + expect(JSON.parse(listed)).toMatchObject({ tools: [{ name: 'report-status' }] }); + const { stdout: invoked } = await execFile(cli, [ + 'mcp', 'invoke', '--json', '--root', projectRoot, '--artifact', artifact, '--target', 'portable', + '--server', 'status', '--tool', 'report-status', '--input', '{"service":"docs"}', + ], { cwd: projectRoot, env: installedEnvironment() }); + expect(JSON.parse(invoked)).toMatchObject({ + result: { + content: [{ text: 'docs is ready.', type: 'text' }], + structuredContent: { service: 'docs', status: 'healthy' }, + }, + }); +}, 600_000); + +it.concurrent('scaffolds the cli-tool template with a framework-built bin, lib, and artifact script', async () => { + const projectRoot = await scaffoldProject('cli-tool', 'greeter', ['--no-install']); + await execFile('npm', ['install', ...npmInstallArguments], { + cwd: projectRoot, + env: installedEnvironment(), + }); + + await npmRun(projectRoot, 'check'); + await expectCleanValidate(projectRoot); + + // The src/cli.ts convention produced the executable package bin. + const bin = join(projectRoot, 'dist', 'bin', 'greeter.js'); + expect((await stat(bin)).mode & 0o111).not.toBe(0); + expect((await readFile(bin, 'utf8')).startsWith('#!/usr/bin/env node\n')).toBe(true); + await expect(execFile(bin, ['World'], { cwd: projectRoot, env: installedEnvironment() })) + .resolves.toMatchObject({ stdout: 'Hello, World!\n' }); + + // The src/index.ts convention produced the library export with declarations. + const library = await import(pathToFileURL(join(projectRoot, 'dist', 'index.js')).href) as { + readonly greet: (name: string) => { readonly message: string }; + }; + expect(library.greet('World').message).toBe('Hello, World!'); + await expect(readFile(join(projectRoot, 'dist', 'index.d.ts'), 'utf8')).resolves.toContain('Greeting'); + + // The same CLI also shipped inside the host artifact as a script. + await expect(execFile(process.execPath, [ + join(projectRoot, 'artifact', 'portable', 'scripts', 'greeter.mjs'), 'World', + ], { cwd: projectRoot, env: installedEnvironment() })).resolves.toMatchObject({ stdout: 'Hello, World!\n' }); +}, 600_000); diff --git a/packages/create-agent-bundle/tests/scaffold-packed.e2e.test.ts b/packages/create-agent-bundle/tests/scaffold-packed.e2e.test.ts index 10d712ac5..936e1f631 100644 --- a/packages/create-agent-bundle/tests/scaffold-packed.e2e.test.ts +++ b/packages/create-agent-bundle/tests/scaffold-packed.e2e.test.ts @@ -1,101 +1,20 @@ -import { execFile as executeFile } from 'node:child_process'; -import { mkdir, mkdtemp, readFile, rm, stat, writeFile } from 'node:fs/promises'; -import { tmpdir } from 'node:os'; +import { readFile } from 'node:fs/promises'; import { join } from 'node:path'; -import { pathToFileURL } from 'node:url'; -import { promisify } from 'node:util'; import { afterAll, expect, it } from '@rstest/core'; -import { installedEnvironment, npmInstallArguments, sharedPackedTarball } from '../../agent-bundle/tests/support/shared-pack.ts'; +import { cleanupScaffoldFixture, expectCleanValidate, npmRun, scaffoldProject } from './support/scaffold-fixture.ts'; -const execFile = promisify(executeFile); - -interface PackedFixture { - readonly frameworkTarball: string; - readonly root: string; - readonly runnerRoot: string; - readonly scaffolderBin: string; -} +afterAll(cleanupScaffoldFixture); /** - * Take the run-level agent-bundle and create-agent-bundle release tarballs - * (packed once per `test:packed` run — see tests/support/shared-pack.ts), - * then install the scaffolder tarball into a clean runner project. Every - * template test drives the installed bin and pins the framework with - * `--framework-version file:`, so the run never depends on - * pkg.pr.new. + * Per-PR scaffolder smoke: one template through the full consumer journey — + * installed scaffolder bin, template scaffold, scaffolder-driven npm install, + * project check, clean validate. The mcp-server and cli-tool templates run in + * the release-boundary matrix (scaffold-packed-matrix.e2e.test.ts) via + * `test:packed:release` and the nightly schedule. */ -const packFixture = async (): Promise => { - const root = await mkdtemp(join(tmpdir(), 'create-agent-bundle-e2e-')); - const [{ tarball: frameworkTarball }, { tarball: scaffolderTarball }] = await Promise.all([ - sharedPackedTarball('agent-bundle'), - sharedPackedTarball('create-agent-bundle'), - ]); - - const runnerRoot = join(root, 'runner'); - await mkdir(runnerRoot, { recursive: true }); - await writeFile(join(runnerRoot, 'package.json'), '{"name":"scaffold-runner","type":"module","private":true}\n'); - await execFile('npm', ['install', ...npmInstallArguments, scaffolderTarball], { - cwd: runnerRoot, - env: installedEnvironment(), - }); - return { - frameworkTarball, - root, - runnerRoot, - scaffolderBin: join(runnerRoot, 'node_modules', '.bin', 'create-agent-bundle'), - }; -}; - -let fixturePromise: Promise | undefined; -const fixture = (): Promise => { - fixturePromise ??= packFixture(); - return fixturePromise; -}; - -afterAll(async () => { - if (fixturePromise === undefined) return; - const { root } = await fixturePromise; - await rm(root, { force: true, recursive: true }); -}); - -const scaffoldProject = async ( - template: string, - projectName: string, - extraArguments: readonly string[], -): Promise => { - const { frameworkTarball, runnerRoot, scaffolderBin } = await fixture(); - await execFile(scaffolderBin, [ - projectName, - '--template', template, - '--targets', 'portable,codex,claude', - '--package-manager', 'npm', - '--framework-version', `file:${frameworkTarball}`, - ...extraArguments, - ], { cwd: runnerRoot, env: installedEnvironment() }); - return join(runnerRoot, projectName); -}; - -const npmRun = async (projectRoot: string, script: string): Promise => { - await execFile('npm', ['run', script], { cwd: projectRoot, env: installedEnvironment() }); -}; - -/** Zero diagnostics — including the informational AB473x migration nudges. */ -const expectCleanValidate = async (projectRoot: string): Promise => { - const cli = join(projectRoot, 'node_modules', '.bin', 'agent-bundle'); - const { stdout } = await execFile(cli, ['validate', '--json', '--root', projectRoot], { - cwd: projectRoot, - env: installedEnvironment(), - }); - const validated = JSON.parse(stdout) as { readonly diagnostics: readonly unknown[] }; - expect(validated.diagnostics).toEqual([]); -}; - -// The template tests run concurrently: each scaffolds its own project -// directory under the shared runner and npm's cache tolerates concurrent -// installs, so the only shared state is the memoized fixture promise. -it.concurrent('scaffolds the minimal template, auto-installs, and passes its own check', async () => { +it('scaffolds the minimal template, auto-installs, and passes its own check', async () => { // No --no-install: this run covers the scaffolder-driven `npm install` path. const projectRoot = await scaffoldProject('minimal', 'minimal-project', []); @@ -112,68 +31,3 @@ it.concurrent('scaffolds the minimal template, auto-installs, and passes its own await expect(readFile(join(projectRoot, 'artifact', 'portable', 'skills', 'getting-started', 'SKILL.md'), 'utf8')) .resolves.toContain('# Getting started'); }, 600_000); - -it.concurrent('scaffolds the mcp-server template and serves the conventional entry from the artifact', async () => { - const projectRoot = await scaffoldProject('mcp-server', 'status-plugin', ['--no-install']); - await execFile('npm', ['install', ...npmInstallArguments], { - cwd: projectRoot, - env: installedEnvironment(), - }); - - await npmRun(projectRoot, 'check'); - await expectCleanValidate(projectRoot); - - const artifact = join(projectRoot, 'artifact'); - const manifest = JSON.parse(await readFile(join(artifact, 'portable', 'mcp.json'), 'utf8')) as { - readonly mcpServers: { readonly status: { readonly args: readonly [string, ...string[]] } }; - }; - const entry = join(artifact, 'portable', manifest.mcpServers.status.args[0]); - // The factory export was wrapped in the framework stdio lifecycle shell. - await expect(readFile(entry, 'utf8')).resolves.toContain('stdio heartbeat'); - - const cli = join(projectRoot, 'node_modules', '.bin', 'agent-bundle'); - const { stdout: listed } = await execFile(cli, [ - 'mcp', 'list', '--json', '--root', projectRoot, '--artifact', artifact, '--target', 'portable', '--server', 'status', - ], { cwd: projectRoot, env: installedEnvironment() }); - expect(JSON.parse(listed)).toMatchObject({ tools: [{ name: 'report-status' }] }); - const { stdout: invoked } = await execFile(cli, [ - 'mcp', 'invoke', '--json', '--root', projectRoot, '--artifact', artifact, '--target', 'portable', - '--server', 'status', '--tool', 'report-status', '--input', '{"service":"docs"}', - ], { cwd: projectRoot, env: installedEnvironment() }); - expect(JSON.parse(invoked)).toMatchObject({ - result: { - content: [{ text: 'docs is ready.', type: 'text' }], - structuredContent: { service: 'docs', status: 'healthy' }, - }, - }); -}, 600_000); - -it.concurrent('scaffolds the cli-tool template with a framework-built bin, lib, and artifact script', async () => { - const projectRoot = await scaffoldProject('cli-tool', 'greeter', ['--no-install']); - await execFile('npm', ['install', ...npmInstallArguments], { - cwd: projectRoot, - env: installedEnvironment(), - }); - - await npmRun(projectRoot, 'check'); - await expectCleanValidate(projectRoot); - - // The src/cli.ts convention produced the executable package bin. - const bin = join(projectRoot, 'dist', 'bin', 'greeter.js'); - expect((await stat(bin)).mode & 0o111).not.toBe(0); - expect((await readFile(bin, 'utf8')).startsWith('#!/usr/bin/env node\n')).toBe(true); - await expect(execFile(bin, ['World'], { cwd: projectRoot, env: installedEnvironment() })) - .resolves.toMatchObject({ stdout: 'Hello, World!\n' }); - - // The src/index.ts convention produced the library export with declarations. - const library = await import(pathToFileURL(join(projectRoot, 'dist', 'index.js')).href) as { - readonly greet: (name: string) => { readonly message: string }; - }; - expect(library.greet('World').message).toBe('Hello, World!'); - await expect(readFile(join(projectRoot, 'dist', 'index.d.ts'), 'utf8')).resolves.toContain('Greeting'); - - // The same CLI also shipped inside the host artifact as a script. - await expect(execFile(process.execPath, [ - join(projectRoot, 'artifact', 'portable', 'scripts', 'greeter.mjs'), 'World', - ], { cwd: projectRoot, env: installedEnvironment() })).resolves.toMatchObject({ stdout: 'Hello, World!\n' }); -}, 600_000); diff --git a/packages/create-agent-bundle/tests/support/scaffold-fixture.ts b/packages/create-agent-bundle/tests/support/scaffold-fixture.ts new file mode 100644 index 000000000..e321895a1 --- /dev/null +++ b/packages/create-agent-bundle/tests/support/scaffold-fixture.ts @@ -0,0 +1,93 @@ +import { execFile as executeFile } from 'node:child_process'; +import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import { promisify } from 'node:util'; + +import { expect } from '@rstest/core'; + +import { installedEnvironment, npmInstallArguments, sharedPackedTarball } from '../../../agent-bundle/tests/support/shared-pack.ts'; + +const execFile = promisify(executeFile); + +interface PackedFixture { + readonly frameworkTarball: string; + readonly root: string; + readonly runnerRoot: string; + readonly scaffolderBin: string; +} + +/** + * Take the run-level agent-bundle and create-agent-bundle release tarballs + * (packed once per `test:packed` run — see tests/support/shared-pack.ts), + * then install the scaffolder tarball into a clean runner project. Every + * template test drives the installed bin and pins the framework with + * `--framework-version file:`, so the run never depends on + * pkg.pr.new. Each test file gets its own fixture instance (rstest isolates + * files), so register `cleanupScaffoldFixture` in an `afterAll` per file. + */ +const packFixture = async (): Promise => { + const root = await mkdtemp(join(tmpdir(), 'create-agent-bundle-e2e-')); + const [{ tarball: frameworkTarball }, { tarball: scaffolderTarball }] = await Promise.all([ + sharedPackedTarball('agent-bundle'), + sharedPackedTarball('create-agent-bundle'), + ]); + + const runnerRoot = join(root, 'runner'); + await mkdir(runnerRoot, { recursive: true }); + await writeFile(join(runnerRoot, 'package.json'), '{"name":"scaffold-runner","type":"module","private":true}\n'); + await execFile('npm', ['install', ...npmInstallArguments, scaffolderTarball], { + cwd: runnerRoot, + env: installedEnvironment(), + }); + return { + frameworkTarball, + root, + runnerRoot, + scaffolderBin: join(runnerRoot, 'node_modules', '.bin', 'create-agent-bundle'), + }; +}; + +let fixturePromise: Promise | undefined; +const fixture = (): Promise => { + fixturePromise ??= packFixture(); + return fixturePromise; +}; + +export const cleanupScaffoldFixture = async (): Promise => { + if (fixturePromise === undefined) return; + const { root } = await fixturePromise; + await rm(root, { force: true, recursive: true }); +}; + +export const scaffoldProject = async ( + template: string, + projectName: string, + extraArguments: readonly string[], +): Promise => { + const { frameworkTarball, runnerRoot, scaffolderBin } = await fixture(); + await execFile(scaffolderBin, [ + projectName, + '--template', template, + '--targets', 'portable,codex,claude', + '--package-manager', 'npm', + '--framework-version', `file:${frameworkTarball}`, + ...extraArguments, + ], { cwd: runnerRoot, env: installedEnvironment() }); + return join(runnerRoot, projectName); +}; + +export const npmRun = async (projectRoot: string, script: string): Promise => { + await execFile('npm', ['run', script], { cwd: projectRoot, env: installedEnvironment() }); +}; + +/** Zero diagnostics — including the informational AB473x migration nudges. */ +export const expectCleanValidate = async (projectRoot: string): Promise => { + const cli = join(projectRoot, 'node_modules', '.bin', 'agent-bundle'); + const { stdout } = await execFile(cli, ['validate', '--json', '--root', projectRoot], { + cwd: projectRoot, + env: installedEnvironment(), + }); + const validated = JSON.parse(stdout) as { readonly diagnostics: readonly unknown[] }; + expect(validated.diagnostics).toEqual([]); +}; diff --git a/rstest.integration-tests.ts b/rstest.integration-tests.ts index b815c4321..ab2a6dacb 100644 --- a/rstest.integration-tests.ts +++ b/rstest.integration-tests.ts @@ -66,13 +66,13 @@ export const integrationTestFiles: readonly string[] = [ ]; /** - * Pack-and-install tests: each one runs `npm pack` (and usually a clean - * `npm install` of the tarball), which dominates the serialized integration - * pool. They run through the root `test:packed` / `test:packed:native` - * scripts instead — CI's release-gates job (`check:release`) and the + * Pack-and-install tests: each one consumes the run-level release tarball + * (and usually a clean `npm install` of it), which dominates the serialized + * integration pool. They run through the root `test:packed` / + * `test:packed:native` scripts instead — CI's release-gates job and the * native-host-smoke workflow keep them covered — and stay excluded from the * parallel unit pool. packed-release.e2e lives here (not in the integration - * pool) so `pnpm test` and `check:release` don't each run the same long + * pool) so `pnpm test` and the release gates don't each run the same long * packed-browser suite; `rstest.packed.config.ts` keeps `test:packed` on one * worker. */ @@ -87,6 +87,17 @@ export const packedTestFiles: readonly string[] = [ 'packages/workbench/tests/packed-release.e2e.test.ts', ]; +/** + * Release-boundary-only pack-and-install tests: the scaffolder template + * matrix beyond the per-PR minimal-template smoke. Runs through + * `test:packed:release` (check:release and CI's nightly schedule), not on + * every PR — the per-PR release gates keep one full scaffold journey via + * scaffold-packed.e2e.test.ts. + */ +export const packedReleaseOnlyTestFiles: readonly string[] = [ + 'packages/create-agent-bundle/tests/scaffold-packed-matrix.e2e.test.ts', +]; + /** * Checked-in scaffolding templates ship their own test files; they run inside * scaffolded projects (the packed e2e drives them through each project's diff --git a/rstest.packed.config.ts b/rstest.packed.config.ts index ae33611d6..ad15c6c0b 100644 --- a/rstest.packed.config.ts +++ b/rstest.packed.config.ts @@ -1,17 +1,22 @@ import { defineConfig } from '@rstest/core'; -import { packedTestFiles } from './rstest.integration-tests.ts'; +import { packedReleaseOnlyTestFiles, packedTestFiles } from './rstest.integration-tests.ts'; import { withAgentBundleRslibConfig } from './rstest.rslib.ts'; /** * Pack-and-install suites, normally launched through * `scripts/run-packed-tests.mjs` so every file consumes one shared tarball - * per public package. The pool stays on one worker: dev-workbench-packaging + * per public package. `--release` (AGENT_BUNDLE_PACKED_RELEASE=1) adds the + * release-boundary-only files — the scaffolder template matrix — on top of + * the per-PR set. The pool stays on one worker: dev-workbench-packaging * rebuilds the workspace `dist` in place while release-audit's audit script * packs it, so the files still contend on workspace-shared writes. */ export default defineConfig({ extends: withAgentBundleRslibConfig(), - include: [...packedTestFiles], + include: [ + ...packedTestFiles, + ...(process.env['AGENT_BUNDLE_PACKED_RELEASE'] === '1' ? packedReleaseOnlyTestFiles : []), + ], pool: { maxWorkers: 1 }, }); diff --git a/scripts/run-packed-tests.mjs b/scripts/run-packed-tests.mjs index 15d87d9f1..78a0e05b3 100644 --- a/scripts/run-packed-tests.mjs +++ b/scripts/run-packed-tests.mjs @@ -4,7 +4,9 @@ * prebuilt seams set instead of every test file rebuilding the workspace for * itself. Build and pack run with NODE_ENV=production like the release * pipeline they stand in for; every child inherits the ambient environment - * minus NODE_PATH. + * minus NODE_PATH. `--release` adds the release-boundary-only files (the + * scaffolder template matrix) to the pool; remaining arguments pass through + * to rstest. */ import { execFile as executeFile, spawn } from 'node:child_process'; import { mkdtemp, rm, writeFile } from 'node:fs/promises'; @@ -16,6 +18,8 @@ import { promisify } from 'node:util'; const execFile = promisify(executeFile); const repositoryRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..'); const { NODE_PATH: _nodePath, ...environment } = process.env; +const releasePool = process.argv.includes('--release'); +const rstestArguments = process.argv.slice(2).filter((argument) => argument !== '--release'); const run = (command, args, extraEnvironment = {}) => new Promise((resolvePromise, rejectPromise) => { const child = spawn(command, args, { @@ -45,8 +49,9 @@ try { `${JSON.stringify({ packOutput, tarball: join(packDirectory, packOutput.filename) })}\n`, ); })); - process.exitCode = await run('pnpm', ['exec', 'rstest', '--config', 'rstest.packed.config.ts', ...process.argv.slice(2)], { + process.exitCode = await run('pnpm', ['exec', 'rstest', '--config', 'rstest.packed.config.ts', ...rstestArguments], { AGENT_BUNDLE_PACKAGE_PREBUILT: '1', + ...(releasePool ? { AGENT_BUNDLE_PACKED_RELEASE: '1' } : {}), AGENT_BUNDLE_SHARED_PACK_DIR: packDirectory, AGENT_BUNDLE_WORKBENCH_PREBUILT: '1', }); From 0034a0ae91d096f8ae744d12687368199892ef91 Mon Sep 17 00:00:00 2001 From: ScriptedAlchemy Date: Mon, 31 Aug 2026 21:44:22 +0000 Subject: [PATCH 7/8] docs(local-ci): note gates-node22 runs the full release pool, a superset of the per-PR hosted gate --- docs/local-ci.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/local-ci.md b/docs/local-ci.md index 0177c4761..7bf1fea6b 100644 --- a/docs/local-ci.md +++ b/docs/local-ci.md @@ -35,6 +35,12 @@ The three hosted Node-22.19 jobs fold into one `gates-node22` worktree because each of their entry scripts starts from `pnpm build` in a fresh install, which one worktree provides just as well as three. +`gates-node22` runs the full `check:release`, a strict superset of the hosted +per-PR release-gates job (`check:release:ci`): it additionally runs the +scaffolder template matrix that the hosted side defers to the nightly +`packed-matrix` job, so local green covers both the per-PR and nightly packed +pools. + All four legs run concurrently. The summary table (leg × step × status × duration × test census) is printed and written to `.worktrees/local-ci/summary.md` (plus `summary.json`); per-step logs land in From 8eb89b3be691defce79c9220de79d23dbbc56141 Mon Sep 17 00:00:00 2001 From: ScriptedAlchemy Date: Mon, 31 Aug 2026 22:05:22 +0000 Subject: [PATCH 8/8] fix(tests): give the snapshot-failure fixture a unique external dir so concurrent local-ci legs don't collide on /tmp --- packages/agent-bundle/tests/dev-services.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/agent-bundle/tests/dev-services.test.ts b/packages/agent-bundle/tests/dev-services.test.ts index e16bf7769..5b0073f5d 100644 --- a/packages/agent-bundle/tests/dev-services.test.ts +++ b/packages/agent-bundle/tests/dev-services.test.ts @@ -731,9 +731,9 @@ it('reports snapshot failures as frozen preparation diagnostics', async () => { '', ].join('\n')); const output = join(root, 'snapshot-output'); - const externalOutput = join(root, '..', 'snapshot-output-external'); + // Unique path: a fixed name under tmpdir collides across concurrent runs. + const externalOutput = await mkdtemp(join(tmpdir(), 'agent-bundle-snapshot-output-external-')); try { - await mkdir(externalOutput); await writeFile(join(root, 'agent-bundle.config.ts'), [ "import { symlinkSync } from 'node:fs';", 'export default ({ projectRoot }) => {',