Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 25 additions & 22 deletions packages/agent-bundle/tests/dev-workbench-packaging.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { execFile as executeFile } from 'node:child_process';
import { access, mkdtemp, mkdir, readFile, readdir, rm, writeFile } from 'node:fs/promises';
import { access, cp, mkdtemp, mkdir, readFile, readdir, rm, writeFile } from 'node:fs/promises';
import { createServer } from 'node:net';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
Expand All @@ -16,13 +16,7 @@ const workbenchRoot = join(workspaceRoot, 'packages', 'workbench');
const appRendererLicense = join('src', 'mcp', 'APP-RENDERER-LICENSE');
let built: Promise<void> | undefined;

const buildPackage = async (force = false): Promise<void> => {
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;
}
const buildPackage = async (): Promise<void> => {
if (process.env['AGENT_BUNDLE_PACKAGE_PREBUILT'] === '1') return;
built ??= execFile('pnpm', ['build'], { cwd: workspaceRoot }).then(() => undefined);
await built;
Expand Down Expand Up @@ -57,17 +51,26 @@ it('copies stable prebuilt workbench assets and the exact app-renderer license i

it('prunes stale copied workbench assets without removing the package library output', async () => {
await buildPackage();
const workbench = join(packageRoot, 'dist', 'workbench');
const stale = join(workbench, 'static', 'js', 'async', 'stale-nested.js');
await mkdir(join(workbench, 'static', 'js', 'async'), { recursive: true });
await writeFile(stale, 'obsolete workbench output\n');
await expect(access(stale)).resolves.toBeUndefined();

await buildPackage(true);

await expect(access(stale)).rejects.toThrow();
await expect(access(join(packageRoot, 'dist', 'cli.js'))).resolves.toBeUndefined();
expect(await readdir(workbench, { recursive: true })).not.toContain('index.js.map');
const isolatedRoot = await mkdtemp(join(tmpdir(), 'agent-bundle-workbench-prune-'));
const isolatedDist = join(isolatedRoot, 'dist');
try {
await cp(join(packageRoot, 'dist'), isolatedDist, { recursive: true });
const workbench = join(isolatedDist, 'workbench');
const stale = join(workbench, 'static', 'js', 'async', 'stale-nested.js');
await mkdir(join(workbench, 'static', 'js', 'async'), { recursive: true });
await writeFile(stale, 'obsolete workbench output\n');
await expect(access(stale)).resolves.toBeUndefined();
await execFile(join(workspaceRoot, 'node_modules', '.bin', 'rslib'), [
'build',
'--config', join(packageRoot, 'rslib.config.ts'),
'--dist-path', isolatedDist,
], { cwd: workspaceRoot });
await expect(access(stale)).rejects.toThrow();
await expect(access(join(isolatedDist, 'cli.js'))).resolves.toBeUndefined();
expect(await readdir(workbench, { recursive: true })).not.toContain('index.js.map');
} finally {
await rm(isolatedRoot, { force: true, recursive: true });
}
}, 60_000);

it('serves prebuilt workbench assets from an installed tarball without the repository source tree', async () => {
Expand All @@ -83,7 +86,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', ...npmInstallArguments, tarball], { cwd: consumer });
await execFile('npm', ['install', ...npmInstallArguments, tarball], { cwd: consumer, env: installedEnvironment() });
await mkdir(join(project, 'skills', 'review'), { recursive: true });
await Promise.all([
writeFile(join(project, 'package.json'), '{"type":"module"}\n'),
Expand All @@ -99,7 +102,7 @@ it('serves prebuilt workbench assets from an installed tarball without the repos
' console.log(JSON.stringify({ body: await response.text(), status: response.status }));',
'} finally { await session.close(); }',
].join('\n');
const served = await execFile(process.execPath, ['--input-type=module', '--eval', script], { cwd: consumer });
const served = await execFile(process.execPath, ['--input-type=module', '--eval', script], { cwd: consumer, env: installedEnvironment() });
expect(JSON.parse(served.stdout)).toMatchObject({
body: expect.stringContaining('Agent Bundle workbench'),
status: 200,
Expand All @@ -115,7 +118,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', ...npmInstallArguments, tarball], { cwd: consumer });
await execFile('npm', ['install', '--omit=dev', ...npmInstallArguments, tarball], { cwd: consumer, env: installedEnvironment() });
await mkdir(join(project, 'skills', 'review'), { recursive: true });
await Promise.all([
writeFile(join(project, 'package.json'), '{"type":"module"}\n'),
Expand Down
5 changes: 3 additions & 2 deletions packages/agent-bundle/tests/helpers/project-fixture.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { dirname, join, resolve } from 'node:path';

import { rstestWorkerRoot } from '../../../../rstest.worker-isolation.ts';

export interface ProjectFixture {
configPath: string;
imagePath: string;
Expand Down Expand Up @@ -33,7 +34,7 @@ const sourceEntryPoint = resolve(
export const createProjectFixture = async (
options: ProjectFixtureOptions = {},
): Promise<ProjectFixture> => {
const root = await mkdtemp(join(tmpdir(), options.prefix ?? 'agent-bundle-config-'));
const root = await mkdtemp(join(rstestWorkerRoot(), options.prefix ?? 'agent-bundle-config-'));
const skillDir = join(root, 'skills/review');
const skillSource = join(skillDir, 'SKILL.md');
const imagePath = join(skillDir, 'assets/diagram.png');
Expand Down
13 changes: 7 additions & 6 deletions packages/agent-bundle/tests/public-api-packed.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { promisify } from 'node:util';

import { expect, it } from '@rstest/core';

import { isolatedCommandEnvironment } from '../../../rstest.worker-isolation.ts';
import { writeFixtureManifest } from './support/manifest.ts';
import { npmInstallArguments, sharedPackedTarball } from './support/shared-pack.ts';

Expand Down Expand Up @@ -59,7 +60,7 @@ it('writes the package version as the producer of a packed CLI manifest', async
await writeFile(join(consumerRoot, 'package.json'), '{"type":"module"}\n');
await execFile(
'npm', ['install', ...npmInstallArguments, tarball],
{ cwd: consumerRoot },
{ cwd: consumerRoot, env: isolatedCommandEnvironment() },
);

const project = await createBuildProject(consumerRoot);
Expand Down Expand Up @@ -91,7 +92,7 @@ it('imports the externalized config entry from a packed npm consumer', async ()
await execFile(
'npm',
['install', ...npmInstallArguments, tarball],
{ cwd: consumerRoot },
{ cwd: consumerRoot, env: isolatedCommandEnvironment() },
);

expect((await stat(join(packageRoot, 'dist/config.js'))).size).toBeLessThan(
Expand All @@ -106,7 +107,7 @@ it('imports the externalized config entry from a packed npm consumer', async ()
"import { defineConfig } from 'agent-bundle/config';",
'if (defineConfig !== rootDefineConfig) throw new Error(\'config factory identity mismatch\');',
].join('\n'),
], { cwd: consumerRoot }),
], { cwd: consumerRoot, env: isolatedCommandEnvironment() }),
).resolves.toMatchObject({ stderr: '', stdout: '' });
await symlink(
join(workspaceRoot, 'node_modules', '@types'),
Expand Down Expand Up @@ -138,7 +139,7 @@ it('imports the externalized config entry from a packed npm consumer', async ()
'--target', 'es2022',
'--types', 'node',
'config.mts',
], { cwd: consumerRoot })).resolves.toMatchObject({ stderr: '', stdout: '' });
], { cwd: consumerRoot, env: isolatedCommandEnvironment() })).resolves.toMatchObject({ stderr: '', stdout: '' });
} finally {
await rm(consumerRoot, { force: true, recursive: true });
}
Expand Down Expand Up @@ -199,7 +200,7 @@ it('invokes a prebuilt MCP server from a clean packed consumer', async () => {
await execFile(
'npm',
['install', ...npmInstallArguments, tarball],
{ cwd: consumerRoot },
{ cwd: consumerRoot, env: isolatedCommandEnvironment() },
);
const { stdout } = await execFile(process.execPath, [
'--input-type=module',
Expand All @@ -209,7 +210,7 @@ it('invokes a prebuilt MCP server from a clean packed consumer', async () => {
"const result = await new McpService().invoke({ artifact: './artifact', input: {}, server: 'fixture', target: 'portable', tool: 'inspect' });",
'console.log(JSON.stringify(result));',
].join('\n'),
], { cwd: consumerRoot });
], { cwd: consumerRoot, env: isolatedCommandEnvironment() });
expect(JSON.parse(stdout)).toMatchObject({
result: {
content: [{ text: 'packed result', type: 'text' }],
Expand Down
3 changes: 2 additions & 1 deletion packages/agent-bundle/tests/release-audit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,14 @@ import { promisify } from 'node:util';

import { expect, it } from '@rstest/core';

import { isolatedCommandEnvironment } from '../../../rstest.worker-isolation.ts';
import { npmInstallArguments, sharedPackedTarball } from './support/shared-pack.ts';

const execFile = promisify(executeFile);
const workspaceRoot = process.cwd();
const packageRoot = join(workspaceRoot, 'packages', 'agent-bundle');

const releaseEnvironment = (): NodeJS.ProcessEnv => ({ ...process.env, NODE_ENV: 'production' });
const releaseEnvironment = (): NodeJS.ProcessEnv => isolatedCommandEnvironment({ ...process.env, NODE_ENV: 'production' });

it('audits an externally installed production tarball and generates its CycloneDX SBOM', async () => {
const { stdout } = await execFile(process.execPath, ['scripts/audit-packed-release.mjs'], {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { execFile as executeFile } from 'node:child_process';
import { cp, mkdtemp, readdir, rm } from 'node:fs/promises';
import { cp, mkdtemp, readdir, rm, writeFile } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { promisify } from 'node:util';

import { describe, expect, it } from '@rstest/core';

import { npmInstallArguments, sharedPackedTarball } from './support/shared-pack.ts';
import { installedEnvironment, npmInstallArguments, sharedPackedTarball } from './support/shared-pack.ts';

const execFile = promisify(executeFile);
const workspaceRoot = process.cwd();
Expand Down Expand Up @@ -42,11 +42,12 @@ describe.sequential('optional RSC runtime package boundary', () => {
const project = join(consumer, 'project');
const artifact = join(project, '.agent-bundle', 'artifact');
try {
await writeFile(join(consumer, 'package.json'), '{"name":"rsc-optional-consumer","type":"module"}\n');
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', ...npmInstallArguments, tarball], { cwd: consumer });
const dependencyTree = JSON.parse((await execFile('npm', ['ls', '--all', '--json'], { cwd: consumer })).stdout) as InstalledDependencyTree;
await execFile('npm', ['install', ...npmInstallArguments, tarball], { cwd: consumer, env: installedEnvironment() });
const dependencyTree = JSON.parse((await execFile('npm', ['ls', '--all', '--json'], { cwd: consumer, env: installedEnvironment() })).stdout) as InstalledDependencyTree;
const installedNames = installedDependencyNames(dependencyTree);
for (const name of ['react', 'react-dom', 'react-server-dom-rspack', 'rsbuild-plugin-rsc']) {
expect(installedNames).not.toContain(name);
Expand All @@ -69,7 +70,7 @@ describe.sequential('optional RSC runtime package boundary', () => {
" process.stdout.write(JSON.stringify({ diagnostics: validated.diagnostics, runtimeBody: await runtimeResponse.json(), runtimeStatus: runtimeResponse.status, status: session.status(), surfacesBody: await surfacesResponse.json(), surfacesStatus: surfacesResponse.status, targets: inspected.model.targets.map(({ name }) => name) }));",
'} finally { await session.close(); }',
].join('\n');
const result = JSON.parse((await execFile(process.execPath, ['--input-type=module', '--eval', script], { cwd: consumer })).stdout) as Readonly<{
const result = JSON.parse((await execFile(process.execPath, ['--input-type=module', '--eval', script], { cwd: consumer, env: installedEnvironment() })).stdout) as Readonly<{
readonly diagnostics: unknown;
readonly runtimeBody: unknown;
readonly runtimeStatus: number;
Expand Down
12 changes: 8 additions & 4 deletions packages/agent-bundle/tests/support/shared-pack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { promisify } from 'node:util';

import { isolatedCommandEnvironment } from '../../../../rstest.worker-isolation.ts';

const execFile = promisify(executeFile);
const workspaceRoot = process.cwd();

Expand All @@ -21,10 +23,12 @@ export interface SharedPack {

export type SharedPackPackage = 'agent-bundle' | 'create-agent-bundle';

export const installedEnvironment = (): NodeJS.ProcessEnv => {
const { NODE_PATH: _nodePath, ...environment } = process.env;
return environment;
};
/**
* NODE_PATH-free environment with per-command npm cache and tmp roots under
* the worker's RSTEST_WORKER_ID directory (see rstest.worker-isolation.ts),
* so concurrent workers never contend on shared npm or tmp state.
*/
export const installedEnvironment = (): NodeJS.ProcessEnv => isolatedCommandEnvironment();

/** Canonical flags for installing a packed tarball into a consumer fixture. */
export const npmInstallArguments = ['--ignore-scripts', '--no-audit', '--no-fund'] as const;
Expand Down
5 changes: 3 additions & 2 deletions packages/agent-bundle/tests/support/time-scale.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,10 @@
* costs nothing on green runs - polling assertions return on success - and
* the workflow-level timeout-minutes still bounds real hangs.
*
* AGENT_BUNDLE_TEST_TIME_SCALE (set by rstest.integration.config.ts when the
* pool runs multiple workers) covers the same contention on development
* AGENT_BUNDLE_TEST_TIME_SCALE covers the same contention on development
* machines, where concurrent Chrome + dev-server + rsbuild pairs share cores.
* rstest.integration.config.ts sets it locally from core count without pinning
* workers. CI always uses 4, independent of pool size.
*/
const localScale = Number(process.env['AGENT_BUNDLE_TEST_TIME_SCALE'] ?? '');
export const timeScale = process.env['CI'] !== undefined
Expand Down
4 changes: 1 addition & 3 deletions rstest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,7 @@ export default defineConfig({
'packages/**/tests/**/*.test.ts',
],
exclude: [...templateTestFiles],
// Several integration tests run Rslib, whose build cache and configured
// output paths are process-shared. Keep those builds from racing each other.
pool: { maxWorkers: 1 },
setupFiles: ['./rstest.setup.ts'],
Comment thread
ScriptedAlchemy marked this conversation as resolved.
// isolate: false would cut Playwright startup cost, but the log pipeline
// suites rely on per-file module isolation (verified: logs-real.e2e fails
// when sharing a worker with the other log suites).
Expand Down
4 changes: 2 additions & 2 deletions rstest.integration-tests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,8 +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 the release gates don't each run the same long
* packed-browser suite; `rstest.packed.config.ts` keeps `test:packed` on one
* worker.
* packed-browser suite. `rstest.packed.config.ts` does not cap `test:packed`
* workers; pack destinations and tmp roots are per RSTEST_WORKER_ID.
*/
export const packedTestFiles: readonly string[] = [
'packages/agent-bundle/tests/dev-workbench-packaging.test.ts',
Expand Down
44 changes: 17 additions & 27 deletions rstest.integration.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,53 +6,43 @@ import { integrationTestFiles } from './rstest.integration-tests.ts';
import { withAgentBundleRslibConfig } from './rstest.rslib.ts';

/**
* Worker count for the parallel integration pool. Half the cores keeps
* browser + dev-server pairs from starving each other and the cap of 4 bounds
* memory on large machines. CI pins one worker explicitly: hosted runners
* report 4 cores (which would compute 2 workers), but each Chrome +
* dev-server + rsbuild pair already saturates them, and 2-worker matrix runs
* flaked on a rotating test per leg even at timeScale 4. Parallelism is a
* development-machine speedup; CI keeps the serialized shape it was tuned
* for. AGENT_BUNDLE_INTEGRATION_MAX_WORKERS overrides the computed value
* (e.g. to measure a parallel CI run or bisect locally in serial).
* Rstest computes worker count from CPU and command mode when pool.maxWorkers
* is omitted. Shared cache, tmp, and pack roots are isolated per worker via
* RSTEST_WORKER_ID (see rstest.setup.ts).
*/
const overrideWorkers = Number(process.env['AGENT_BUNDLE_INTEGRATION_MAX_WORKERS'] ?? '');
const maxWorkers = Number.isSafeInteger(overrideWorkers) && overrideWorkers >= 1
? overrideWorkers
: process.env['CI'] !== undefined
? 1
: Math.max(1, Math.min(4, Math.floor(availableParallelism() / 2)));

/**
* Polling budgets scale with contention. A multi-worker pool needs at least
* 2 (see the env comment below); an externally set
* AGENT_BUNDLE_TEST_TIME_SCALE raises it further when the machine is shared —
* scripts/local-ci.mjs passes 4 (hosted CI's own scale) because it runs
* three Node legs plus the release gates concurrently. The external value
* never lowers the scale below what the pool shape requires.
* Polling budgets scale with contention. The auto-sized pool runs multiple
* workers on any multi-core machine, which needs at least 2 (see the env
* comment below); an externally set AGENT_BUNDLE_TEST_TIME_SCALE raises it
* further when the machine is shared — scripts/local-ci.mjs passes 4 (hosted
* CI's own scale) because it runs three Node legs plus the release gates
* concurrently. The external value never lowers the scale below what the
* pool shape requires.
*/
const externalTimeScale = Number(process.env['AGENT_BUNDLE_TEST_TIME_SCALE'] ?? '');
const poolTimeScale = maxWorkers > 1 ? 2 : 1;
const poolTimeScale = availableParallelism() > 1 ? 2 : 1;
const timeScale = Number.isSafeInteger(externalTimeScale) && externalTimeScale >= 1
? Math.max(externalTimeScale, poolTimeScale)
: poolTimeScale;

/**
* Build- and process-running tests that only read workspace-shared artifacts;
* files that WRITE shared locations (root builds, `npm pack`) run through the
* single-worker `test:packed` script instead (see rstest.integration-tests.ts).
* `test:packed` script instead (see rstest.integration-tests.ts).
*/
export default defineConfig({
extends: withAgentBundleRslibConfig(),
include: [...integrationTestFiles],
pool: { maxWorkers },
setupFiles: ['./rstest.setup.ts'],
// isolate: false would cut Playwright startup cost, but the log pipeline
// suites rely on per-file module isolation (verified: logs-real.e2e fails
// when sharing a worker with the other log suites).
isolate: true,
// Concurrent Chrome + dev-server + rsbuild pairs contend for cores, so
// parallel runs double the polling budgets (see tests/support/time-scale.ts)
// and raise the 5s default test timeout, which real in-process builds can
// exceed when workers share the machine. Explicit per-test timeouts win.
env: { AGENT_BUNDLE_TEST_TIME_SCALE: String(timeScale) },
testTimeout: 30_000,
// isolate: false would cut Playwright startup cost, but the log pipeline
// suites rely on per-file module isolation (verified: logs-real.e2e fails
// when sharing a worker with the other log suites).
});
Loading
Loading