From b019f80f21f51d0b933538c1722d954b11681089 Mon Sep 17 00:00:00 2001 From: ScriptedAlchemy Date: Sat, 29 Aug 2026 00:31:43 +0000 Subject: [PATCH 01/16] fix(capture): scale the capture script's browser budget for CI runners --- packages/workbench/scripts/capture-runtime-playground.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/workbench/scripts/capture-runtime-playground.mjs b/packages/workbench/scripts/capture-runtime-playground.mjs index 569a2670f..9a3ce022b 100644 --- a/packages/workbench/scripts/capture-runtime-playground.mjs +++ b/packages/workbench/scripts/capture-runtime-playground.mjs @@ -6,7 +6,7 @@ import { chromium } from 'playwright'; import { startRuntimePlaygroundFixture } from '../tests/helpers/runtime-playground-fixture.ts'; -const browserTimeout = 30_000; +const browserTimeout = 30_000 * (process.env.CI === undefined ? 1 : 4); const desktopViewport = Object.freeze({ height: 900, width: 1440 }); const mobileViewport = Object.freeze({ height: 844, width: 390 }); const outputFlags = Object.freeze([ From 3dcee1ca30ae53c5573742fe822697ab160b30a7 Mon Sep 17 00:00:00 2001 From: ScriptedAlchemy Date: Sat, 29 Aug 2026 00:34:56 +0000 Subject: [PATCH 02/16] test(workbench): scale the HMR e2e budget through the shared time scale The runtime-playground HMR e2e test still stamped a fixed 30s Playwright budget tuned on many-core machines onto waits that sit behind rsbuild compiles and Chrome sharing a two-core runner - the same shape that tripped the capture script on the Node 22.19 Verify job. Both surfaces now read the shared timeScale helper instead of an inline CI multiplier, so the two-core rationale lives in one documented place. Scaling costs nothing on green runs since every wait returns on success. --- packages/workbench/scripts/capture-runtime-playground.mjs | 3 ++- packages/workbench/tests/runtime-playground-hmr.e2e.test.ts | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/workbench/scripts/capture-runtime-playground.mjs b/packages/workbench/scripts/capture-runtime-playground.mjs index 9a3ce022b..1d2203677 100644 --- a/packages/workbench/scripts/capture-runtime-playground.mjs +++ b/packages/workbench/scripts/capture-runtime-playground.mjs @@ -4,9 +4,10 @@ import { fileURLToPath } from 'node:url'; import { chromium } from 'playwright'; +import { timeScale } from '../../agent-bundle/tests/support/time-scale.ts'; import { startRuntimePlaygroundFixture } from '../tests/helpers/runtime-playground-fixture.ts'; -const browserTimeout = 30_000 * (process.env.CI === undefined ? 1 : 4); +const browserTimeout = 30_000 * timeScale; const desktopViewport = Object.freeze({ height: 900, width: 1440 }); const mobileViewport = Object.freeze({ height: 844, width: 390 }); const outputFlags = Object.freeze([ diff --git a/packages/workbench/tests/runtime-playground-hmr.e2e.test.ts b/packages/workbench/tests/runtime-playground-hmr.e2e.test.ts index b43904fce..15b20d7c6 100644 --- a/packages/workbench/tests/runtime-playground-hmr.e2e.test.ts +++ b/packages/workbench/tests/runtime-playground-hmr.e2e.test.ts @@ -4,8 +4,9 @@ import { expect, test, type PlaywrightOptions } from '@rstest/playwright'; import { startRuntimePlaygroundFixture } from './helpers/runtime-playground-fixture.ts'; import { workbenchUrl } from './support/workbench-e2e.ts'; +import { timeScale } from '../../agent-bundle/tests/support/time-scale.ts'; -const browserTimeout = 30_000; +const browserTimeout = 30_000 * timeScale; const e2e = test.extend({ playwright: { From 77e772323d6c07691e3fa8f1a51094ed2d9a0036 Mon Sep 17 00:00:00 2001 From: ScriptedAlchemy Date: Sat, 29 Aug 2026 01:06:37 +0000 Subject: [PATCH 03/16] fix(tests): stage atomic-write temp files outside the watched project and budget the retention test for CI --- .../tests/dev-provider.integration.test.ts | 21 ++++++++++++------- .../tests/playground-service.test.ts | 5 ++++- 2 files changed, 18 insertions(+), 8 deletions(-) diff --git a/examples/rsc-agent-runtime/tests/dev-provider.integration.test.ts b/examples/rsc-agent-runtime/tests/dev-provider.integration.test.ts index ac971022b..0a65f5c8e 100644 --- a/examples/rsc-agent-runtime/tests/dev-provider.integration.test.ts +++ b/examples/rsc-agent-runtime/tests/dev-provider.integration.test.ts @@ -1,6 +1,6 @@ import { cp, lstat, mkdir, mkdtemp, readFile, readdir, rename, rm, symlink, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; -import { basename, dirname, join } from 'node:path'; +import { basename, join } from 'node:path'; import { expect, test } from '@rstest/core'; import type { createRsbuild, StartDevServerResult } from '@rsbuild/core'; @@ -99,20 +99,25 @@ const copyProviderExample = async (): Promise => copyExample(exampleRoot, { linkPackages: true, prefix: 'rsc-agent-runtime-provider-' }); /** - * Replaces source atomically through a same-directory rename. An in-place - * write is truncate-then-append, which a loaded watcher observes as two - * change events and compiles twice; the duplicate attempt supersedes the - * generation that ordinal-pinned assertions expect to commit. + * Replaces source atomically through a rename staged OUTSIDE the watched + * project. An in-place write is truncate-then-append, which a loaded watcher + * observes as two change events and compiles twice; a temp file created + * inside the watched directory is just as bad, because the watcher also sees + * the temp file's creation as a directory change. Either duplicate compile + * supersedes the generation that ordinal-pinned assertions expect to commit. + * The temp file lives in the project's parent (the copied workspace root, + * same filesystem, never watched) so the rename into place is the only event. */ -const replaceSource = async (path: string, replace: (source: string) => string): Promise => { +const replaceSource = async (projectRoot: string, path: string, replace: (source: string) => string): Promise => { const source = await readFile(path, 'utf8'); - const temporary = join(dirname(path), `.${basename(path)}.${process.pid}.tmp`); + const temporary = join(projectRoot, '..', `.${basename(path)}.${process.pid}.tmp`); await writeFile(temporary, replace(source)); await rename(temporary, path); }; const changeDefinition = async (projectRoot: string, replacement: string): Promise => { await replaceSource( + projectRoot, join(projectRoot, 'src', 'definition.ts'), (source) => source.replace('Read the current shared runtime state.', replacement), ); @@ -120,6 +125,7 @@ const changeDefinition = async (projectRoot: string, replacement: string): Promi const changeWorkerImplementation = async (projectRoot: string, marker: string): Promise => { await replaceSource( + projectRoot, join(projectRoot, 'src', 'rsc', 'worker.tsx'), (source) => source.replace( /RSC worker received an invalid event(?: [^']*)?/u, @@ -130,6 +136,7 @@ const changeWorkerImplementation = async (projectRoot: string, marker: string): const introduceWorkerSyntaxError = async (projectRoot: string): Promise => { await replaceSource( + projectRoot, join(projectRoot, 'src', 'rsc', 'worker.tsx'), (source) => `${source}\nconst = ;\n`, ); diff --git a/packages/agent-bundle/tests/playground-service.test.ts b/packages/agent-bundle/tests/playground-service.test.ts index e6887b635..5dcaf8f97 100644 --- a/packages/agent-bundle/tests/playground-service.test.ts +++ b/packages/agent-bundle/tests/playground-service.test.ts @@ -5,6 +5,7 @@ import { dirname, join } from 'node:path'; import { expect, it } from '@rstest/core'; +import { timeScale } from './support/time-scale.ts'; import { PlaygroundStore as PlaygroundService, PlaygroundServiceCloseError, @@ -2166,7 +2167,9 @@ it('evicts the oldest settled sessions from memory while every by-id operation s } }); -it('retains a settled session while a subscription is attached and evicts it after the subscription closes', async () => { +// Settles ~22 real sessions sequentially; the default 5s budget starves on +// 2-core CI runners. +it('retains a settled session while a subscription is attached and evicts it after the subscription closes', { timeout: 30_000 * timeScale }, async () => { const fixture = await createFixture(); try { await settleSession(fixture.service, 'subscribed-retention'); From 6b8dca70dc9a8378232f7bcd255838c3fcaed29a Mon Sep 17 00:00:00 2001 From: ScriptedAlchemy Date: Sat, 29 Aug 2026 01:21:44 +0000 Subject: [PATCH 04/16] test(workbench): delete vacuous and duplicate tests across workbench and rsc example Removes tests that re-assert coverage pinned elsewhere or assert file contents instead of behavior: the artifacts-real e2e file (its table coverage lives in overview), the 390px Runtime controls e2e, the safe launch configuration overview test (redaction lives in the epoch MCP session test), the handoff close-retry overview test (pinned by runtime-mcp-handoff.test.ts), config/source string-matching tests, and the rsc example's tsconfig/doc/manifest duplicates. Also trims the Inspector-tab detours from the Runtime sibling e2e ahead of the inspector removal. --- .../tests/docs-contract.test.ts | 8 - .../tests/runtime-artifact-manifest.test.ts | 26 --- .../tests/tsconfig-coverage.test.ts | 15 -- .../tests/artifacts-real.e2e.test.ts | 34 ---- packages/workbench/tests/overview.e2e.test.ts | 130 ------------- .../workbench/tests/rsbuild-workbench.test.ts | 12 -- .../tests/runtime-playground.e2e.test.ts | 174 +----------------- .../tests/runtime-playground.test.ts | 10 - .../tests/workbench-dev-command.test.ts | 8 - rstest.integration-tests.ts | 1 - 10 files changed, 1 insertion(+), 417 deletions(-) delete mode 100644 examples/rsc-agent-runtime/tests/tsconfig-coverage.test.ts delete mode 100644 packages/workbench/tests/artifacts-real.e2e.test.ts diff --git a/examples/rsc-agent-runtime/tests/docs-contract.test.ts b/examples/rsc-agent-runtime/tests/docs-contract.test.ts index 9d5b9c2b1..5423961a0 100644 --- a/examples/rsc-agent-runtime/tests/docs-contract.test.ts +++ b/examples/rsc-agent-runtime/tests/docs-contract.test.ts @@ -10,14 +10,6 @@ import { expect, test } from '@rstest/core'; const readme = async (): Promise => readFile(join(process.cwd(), 'README.md'), 'utf8'); const execFile = promisify(executeFile); -test('keeps the Hook JSX author example executable', async () => { - const source = await readme(); - const afterFileEdit = source.match(/export function AfterFileEdit\(\) \{[\s\S]*?\n}\n```/); - - expect(afterFileEdit?.[0]).toContain('\n '); - expect(afterFileEdit?.[0]).toContain('\n '); -}); - test('requires attached native evidence before documenting Claude or Codex observations', async () => { const source = await readme(); diff --git a/examples/rsc-agent-runtime/tests/runtime-artifact-manifest.test.ts b/examples/rsc-agent-runtime/tests/runtime-artifact-manifest.test.ts index 891d27d33..6ce1439f7 100644 --- a/examples/rsc-agent-runtime/tests/runtime-artifact-manifest.test.ts +++ b/examples/rsc-agent-runtime/tests/runtime-artifact-manifest.test.ts @@ -36,32 +36,6 @@ test('declares every executable and contained runtime asset in the runtime manif } }); -test('uses an explicitly captured definition instead of the host module serializer', async () => { - const runtimeRoot = await mkdtemp(join(tmpdir(), 'rsc-agent-runtime-manifest-')); - const runtimeAssets = ['hook/index.js', 'rsc/index.js', 'mcp/stdio.js', 'mcp/http.js']; - const definition = { - nativeHooks: [], - resources: [], - tools: [], - }; - - try { - for (const asset of runtimeAssets) { - const target = join(runtimeRoot, asset); - await mkdir(dirname(target), { recursive: true }); - await writeFile(target, 'artifact', 'utf8'); - } - await writeFile(join(runtimeRoot, 'runtime-assets.json'), JSON.stringify({ allFiles: runtimeAssets }), 'utf8'); - - await emitRuntimeArtifacts(runtimeRoot, definition); - - const manifest = JSON.parse(await readFile(join(runtimeRoot, 'agent-runtime.manifest.json'), 'utf8')) as { tools: unknown[] }; - expect(manifest.tools).toEqual([]); - } finally { - await rm(runtimeRoot, { force: true, recursive: true }); - } -}); - test('rejects a runtime asset that escapes the manifest root', async () => { const runtimeRoot = await mkdtemp(join(tmpdir(), 'rsc-agent-runtime-manifest-')); try { diff --git a/examples/rsc-agent-runtime/tests/tsconfig-coverage.test.ts b/examples/rsc-agent-runtime/tests/tsconfig-coverage.test.ts deleted file mode 100644 index 81224d9f9..000000000 --- a/examples/rsc-agent-runtime/tests/tsconfig-coverage.test.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { readFile } from 'node:fs/promises'; -import { join } from 'node:path'; - -import { expect, test } from '@rstest/core'; - -test('typechecks all TypeScript source and test files, including development materializers', async () => { - const config = JSON.parse(await readFile(join(process.cwd(), 'tsconfig.json'), 'utf8')) as { include: string[] }; - - expect(config.include).toEqual(expect.arrayContaining([ - 'src/**/*.ts', - 'src/**/*.tsx', - 'tests/**/*.ts', - 'tests/**/*.tsx', - ])); -}); diff --git a/packages/workbench/tests/artifacts-real.e2e.test.ts b/packages/workbench/tests/artifacts-real.e2e.test.ts deleted file mode 100644 index cb47e5715..000000000 --- a/packages/workbench/tests/artifacts-real.e2e.test.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { expect } from '@rstest/playwright'; - -import { createWorkbenchAssetSource } from '../../agent-bundle/src/dev/workbench-assets.ts'; -import { startDevServer } from '../../agent-bundle/src/dev/workbench-server.ts'; -import { createProjectFixture, removeProjectFixture } from '../../agent-bundle/tests/helpers/project-fixture.ts'; -import { buildWorkbench, e2e, workbenchAssets, workbenchUrl } from './support/workbench-e2e.ts'; - -const browserTimeout = 12_000; - -e2e('contains the mounted Artifacts page and its table at desktop and 390px widths', { timeout: 90_000 }, async ({ page }) => { - await buildWorkbench(); - const project = await createProjectFixture(); - const server = await startDevServer({ - assets: createWorkbenchAssetSource({ root: workbenchAssets }), - open: false, - port: 0, - root: project.root, - }); - try { - const pageErrors: Error[] = []; - page.on('pageerror', (error) => pageErrors.push(error)); - await page.goto(workbenchUrl(server.url, 'artifacts')); - await expect(page.getByRole('heading', { name: 'Artifacts' })).toBeVisible({ timeout: browserTimeout }); - await expect(page.locator('.artifact-table').first()).toBeVisible({ timeout: browserTimeout }); - expect(await page.evaluate(() => document.documentElement.scrollWidth <= window.innerWidth)).toBe(true); - - await page.setViewportSize({ height: 844, width: 390 }); - expect(await page.evaluate(() => document.documentElement.scrollWidth <= window.innerWidth)).toBe(true); - expect(pageErrors).toEqual([]); - } finally { - await server.close(); - await removeProjectFixture(project.root); - } -}); diff --git a/packages/workbench/tests/overview.e2e.test.ts b/packages/workbench/tests/overview.e2e.test.ts index 2fa6566e5..1aa224522 100644 --- a/packages/workbench/tests/overview.e2e.test.ts +++ b/packages/workbench/tests/overview.e2e.test.ts @@ -141,23 +141,6 @@ const writeMcpPlaygroundProject = async (root: string): Promise => { ]); }; -const expectSafeLaunchConfiguration = async (page: Page, { command, compiledEntry, cwd }: { - readonly command: string; - readonly compiledEntry: string; - readonly cwd: string; -}): Promise => { - const launchConfiguration = page.locator('.mcp-page-launch-configuration'); - await expect(launchConfiguration).toContainText('Launch configuration', { timeout: browserTimeout }); - await expect(launchConfiguration).toContainText('stdio'); - await expect(launchConfiguration).toContainText(command); - await expect(launchConfiguration).toContainText('[REDACTED]'); - await expect(launchConfiguration).toContainText(cwd); - await expect(launchConfiguration).toContainText('NO_COLOR'); - expect(await launchConfiguration.textContent()).not.toContain(compiledEntry); - expect(await launchConfiguration.textContent()).not.toContain('SECRET_TOKEN'); - expect(await launchConfiguration.textContent()).not.toContain('fixture-secret'); -}; - e2e('preserves a direct Runtime deep link until capability discovery succeeds', { timeout: 120_000 }, async ({ page }) => { const fixture = await startRuntimePlaygroundFixture(); let releaseRuntimeStatus = (): void => undefined; @@ -595,69 +578,6 @@ e2e('offers the host-owned MCP playground handoff only after a selected Runtime } }); -e2e('retains the exact Runtime App owner when its handoff close rejects, then retries it once', { timeout: 120_000 }, async ({ page }) => { - const fixture = await startRuntimePlaygroundFixture(); - let clientPage: Page | undefined; - let clientSurface: Awaited> | undefined; - const runtimeAppRequests: string[] = []; - let rejectFirstRuntimeClose = true; - page.on('request', (request) => { - const requestUrl = new URL(request.url()); - if (requestUrl.origin === fixture.url && requestUrl.pathname.startsWith('/api/runtime/apps')) { - runtimeAppRequests.push(`${request.method()} ${requestUrl.pathname}`); - } - }); - await page.route(`${fixture.url}/api/runtime/apps/**`, async (route) => { - if (route.request().method() === 'DELETE' && rejectFirstRuntimeClose) { - rejectFirstRuntimeClose = false; - await route.fulfill({ body: JSON.stringify({ error: 'close rejected for retry proof' }), contentType: 'application/json', status: 500 }); - return; - } - await route.continue(); - }); - try { - await page.goto(`${fixture.url}#runtime`); - await expect(page.getByRole('heading', { name: 'Runtime Playground' })).toBeVisible({ timeout: browserTimeout }); - const runtimeIdentity = page.locator('[data-runtime-provider-session]'); - const runtimeSurface = page.getByLabel('Runtime surface'); - await expect(runtimeIdentity).toHaveAttribute('data-runtime-hmr-ready', 'true', { timeout: 15_000 }); - await runtimeSurface.selectOption('mcp.edit-timeline'); - clientSurface = await fixture.openRuntimeClientSurface('mcp.edit-timeline'); - if (clientSurface === undefined) throw new Error('Runtime client surface was not available.'); - clientPage = await page.context().newPage(); - const bootstrapResponse = await clientPage.goto(clientSurface.bootstrapUrl, { waitUntil: 'domcontentloaded' }); - expect(bootstrapResponse?.status()).toBe(200); - await expect.poll(async () => runtimeIdentity.getAttribute('data-runtime-hmr-client-count'), { timeout: 15_000 }).toBe('1'); - await runtimeSurface.selectOption('mcp.render_edit_timeline'); - await page.getByLabel('Runtime target').selectOption('portable'); - await page.getByRole('radio', { name: 'Raw JSON' }).check(); - await page.locator('#runtime-input-raw').fill('{}'); - await page.getByRole('button', { name: 'Run', exact: true }).click(); - await expect(page.getByRole('button', { name: 'Open in MCP playground' })).toBeEnabled({ timeout: 15_000 }); - await page.locator('.runtime-stage .mcp-app-preview iframe').waitFor({ state: 'attached', timeout: 15_000 }); - await expect.poll(() => runtimeAppRequests.filter((request) => request === 'POST /api/runtime/apps').length, { timeout: 15_000 }).toBe(1); - - const handoff = page.getByRole('button', { name: 'Open in MCP playground' }); - await handoff.click(); - await expect(page.locator('.runtime-content > p[role="alert"]')).toContainText('MCP playground handoff could not close the Runtime App', { timeout: 15_000 }); - await expect.poll(() => new URL(page.url()).hash, { timeout: 15_000 }).toBe('#runtime'); - await page.waitForTimeout(250); - await expect.poll(() => runtimeAppRequests.filter((request) => request === 'POST /api/runtime/apps').length, { timeout: 15_000 }).toBe(1); - await expect(page.locator('.runtime-stage .mcp-app-preview')).toHaveCount(1, { timeout: 15_000 }); - await expect(handoff).toBeEnabled({ timeout: 15_000 }); - - await handoff.click(); - await expect(page.getByRole('heading', { name: 'MCP playground' })).toBeVisible({ timeout: browserTimeout }); - await page.locator('.mcp-page-app-preview iframe').waitFor({ state: 'attached', timeout: 15_000 }); - await expect.poll(() => runtimeAppRequests.filter((request) => request === 'POST /api/runtime/apps').length, { timeout: 15_000 }).toBe(2); - await expect.poll(() => runtimeAppRequests.filter((request) => request.startsWith('DELETE /api/runtime/apps/')).length, { timeout: 15_000 }).toBe(2); - } finally { - await clientPage?.close(); - await clientSurface?.close(); - await fixture.close(); - } -}); - e2e('keeps runtime Inspector routing constrained after direct MCP navigation from a bound source', { timeout: 120_000 }, async ({ page }) => { const fixture = await startRuntimePlaygroundFixture(); let clientPage: Page | undefined; @@ -1352,56 +1272,6 @@ e2e('opens one real epoch MCP session and keeps its playground operations respon if (cleanupFailure !== undefined) throw cleanupFailure; }); -e2e('renders the safe launch configuration for one real artifact MCP session', { timeout: 60_000 }, async ({ page }) => { - await buildWorkbench(); - let project: Awaited> | undefined; - let server: Awaited> | undefined; - try { - project = await createProjectFixture(); - await writeMcpPlaygroundProject(project.root); - server = await startDevServer({ - assets: createWorkbenchAssetSource({ root: workbenchAssets }), - open: false, - port: 0, - root: project.root, - }); - const serverUrl = server.url; - const artifact = server.status().artifact; - if (artifact.state === 'missing') throw new Error('Expected an active fixture artifact epoch.'); - const epochId = artifact.activeEpoch.id; - const manifest = JSON.parse(await readFile(join(project.root, '.agent-bundle', 'epochs', epochId, 'portable', 'mcp.json'), 'utf8')) as { - readonly mcpServers: Readonly<{ - readonly fixture: Readonly<{ readonly args?: readonly string[]; readonly command: string }>; - }>; - }; - const compiledEntry = manifest.mcpServers.fixture.args?.[0]; - if (compiledEntry === undefined) throw new Error('Expected the fixture MCP manifest to include its compiled entry.'); - const pageErrors: Error[] = []; - page.on('pageerror', (error) => pageErrors.push(error)); - await page.goto(`${serverUrl}#mcp`); - await expect(page.getByRole('heading', { name: 'MCP playground' })).toBeVisible({ timeout: browserTimeout }); - await page.locator('#mcp-target').selectOption('portable'); - await page.locator('#mcp-server-name').fill('fixture'); - const opened = page.waitForResponse((response) => - response.url() === `${serverUrl}/api/mcp/sessions` && response.request().method() === 'POST'); - await page.getByRole('button', { name: 'Open MCP session' }).click(); - await opened; - await expect(page.locator('.mcp-page-phase')).toContainText('Session ready', { timeout: browserTimeout }); - - await expectSafeLaunchConfiguration(page, { - command: manifest.mcpServers.fixture.command, - compiledEntry, - cwd: join(project.root, '.agent-bundle', 'epochs', epochId, 'portable'), - }); - await page.setViewportSize({ height: 844, width: 390 }); - expect(await page.evaluate(() => document.documentElement.scrollWidth <= window.innerWidth)).toBe(true); - expect(pageErrors).toEqual([]); - } finally { - await Promise.allSettled(server === undefined ? [] : [server.close()]); - await Promise.allSettled(project === undefined ? [] : [removeProjectFixture(project.root)]); - } -}); - e2e('renders and rebuilds the complete desktop Overview against a real foreground server', { timeout: 60_000 }, async ({ page }) => { await buildWorkbench(); const project = await createProjectFixture(); diff --git a/packages/workbench/tests/rsbuild-workbench.test.ts b/packages/workbench/tests/rsbuild-workbench.test.ts index 23784c378..90ca44ed0 100644 --- a/packages/workbench/tests/rsbuild-workbench.test.ts +++ b/packages/workbench/tests/rsbuild-workbench.test.ts @@ -57,18 +57,6 @@ it('publishes the workbench application at the foreground server index asset', a await expect(access(join(workbenchRoot, 'dist', 'static', 'js', 'index.js'))).resolves.toBeUndefined(); }); -it('keeps the Overview heading single-purpose and compact', async () => { - const [main, styles] = await Promise.all([ - readFile(join(workbenchRoot, 'src/main.tsx'), 'utf8'), - readFile(join(workbenchRoot, 'src/styles.css'), 'utf8'), - ]); - - expect(main).not.toContain('className="eyebrow"'); - expect(styles).not.toContain('.eyebrow'); - expect(styles).toContain('.page-heading { margin-bottom: 30px; }'); - expect(styles).toContain('font-size: clamp(31px, 4vw, 40px);'); -}); - it('emits browser-safe JS from the prepared production build', async () => { const jsRoot = join(workbenchRoot, 'dist', 'static', 'js'); const files = await readdir(jsRoot); diff --git a/packages/workbench/tests/runtime-playground.e2e.test.ts b/packages/workbench/tests/runtime-playground.e2e.test.ts index decd68f6a..151a67c03 100644 --- a/packages/workbench/tests/runtime-playground.e2e.test.ts +++ b/packages/workbench/tests/runtime-playground.e2e.test.ts @@ -46,19 +46,8 @@ e2e('renders the capability-gated Runtime sibling in the real RSC workbench', { await page.goto(workbenchUrl(fixture.url, 'mcp')); await expect(page.getByRole('heading', { name: 'MCP playground' })).toBeVisible({ timeout: browserTimeout }); await expect(page.getByLabel('MCP App preview controls')).toHaveCount(1, { timeout: browserTimeout }); - await page.getByRole('tab', { name: 'Inspector' }).click(); - await expect(page.getByRole('tab', { name: 'Inspector' })).toHaveAttribute('aria-selected', 'true'); - await expect(page.getByRole('heading', { name: 'Inspector' })).toBeVisible({ timeout: browserTimeout }); await page.goto(workbenchUrl(fixture.url, 'runtime')); await expect(page.getByRole('heading', { name: 'Runtime Playground' })).toBeVisible({ timeout: browserTimeout }); - await page.goto(workbenchUrl(fixture.url, 'mcp')); - await page.getByRole('tab', { name: 'Inspector' }).click(); - await expect(page.getByRole('tab', { name: 'Inspector' })).toHaveAttribute('aria-selected', 'true'); - await expect(page.getByRole('heading', { name: 'Inspector' })).toBeVisible({ timeout: browserTimeout }); - await page.getByRole('tab', { name: 'Playground' }).click(); - await expect(page.getByRole('tab', { name: 'Playground' })).toHaveAttribute('aria-selected', 'true'); - await expect(page.getByRole('heading', { name: 'MCP playground' })).toBeVisible({ timeout: browserTimeout }); - await expect(page.getByLabel('MCP App preview controls')).toHaveCount(1, { timeout: browserTimeout }); await page.goto(workbenchUrl(fixture.url, 'runtime')); await expect(page.getByRole('heading', { name: 'Runtime Playground' })).toBeVisible({ timeout: browserTimeout }); @@ -103,9 +92,7 @@ e2e('renders the capability-gated Runtime sibling in the real RSC workbench', { await input.fill('{"broken":'); await expect(page.locator('#runtime-input-raw-error')).toBeVisible(); await page.goto(workbenchUrl(fixture.url, 'mcp')); - await page.getByRole('tab', { name: 'Inspector' }).click(); - await expect(page.getByRole('tab', { name: 'Inspector' })).toHaveAttribute('aria-selected', 'true'); - await expect(page.getByRole('heading', { name: 'Inspector' })).toBeVisible({ timeout: browserTimeout }); + await expect(page.getByRole('heading', { name: 'MCP playground' })).toBeVisible({ timeout: browserTimeout }); await page.goto(workbenchUrl(fixture.url, 'runtime')); await expect(page.getByRole('heading', { name: 'Runtime Playground' })).toBeVisible({ timeout: browserTimeout }); await expect(page.locator('[data-runtime-provider-session]')).toHaveCount(1, { timeout: browserTimeout }); @@ -201,165 +188,6 @@ e2e('renders the capability-gated Runtime sibling in the real RSC workbench', { } }); -e2e('keeps Runtime controls at least 40px tall and inside the 390px viewport without horizontal scrolling', { timeout: 120_000 }, async ({ page }) => { - const fixture = await startRuntimePlaygroundFixture(); - const pageErrors: Error[] = []; - page.on('pageerror', (error) => pageErrors.push(error)); - try { - await page.setViewportSize({ height: 844, width: 390 }); - await page.goto(workbenchUrl(fixture.url, 'runtime')); - await expect(page.getByRole('heading', { name: 'Runtime Playground' })).toBeVisible({ timeout: browserTimeout }); - await expect(page.locator('[data-runtime-provider-session]')).toHaveCount(1, { timeout: browserTimeout }); - await page.getByLabel('Runtime surface').selectOption('mcp.recent_edits'); - await page.getByLabel('Schema form').check(); - await expect(page.locator('#runtime-input-limit')).toBeVisible({ timeout: browserTimeout }); - const schemaForm = await page.evaluate(() => { - const { body, documentElement } = globalThis.document; - documentElement.scrollLeft = 0; - if (body !== null) body.scrollLeft = 0; - globalThis.scrollTo({ left: 0, top: globalThis.scrollY }); - const textInputs = [...globalThis.document.querySelectorAll([ - '.runtime-input input:not([type])', - '.runtime-input input[type="email"]', - '.runtime-input input[type="number"]', - '.runtime-input input[type="password"]', - '.runtime-input input[type="search"]', - '.runtime-input input[type="tel"]', - '.runtime-input input[type="text"]', - '.runtime-input input[type="url"]', - ].join(','))].filter((element) => element.getClientRects().length > 0).map((element) => { - const rect = element.getBoundingClientRect(); - return Object.freeze({ - height: rect.height, - label: element.getAttribute('aria-label') ?? element.labels?.[0]?.textContent?.trim() ?? element.id, - left: rect.left, - right: rect.right, - }); - }); - const choiceLabels = [...globalThis.document.querySelectorAll('.runtime-input input[type="checkbox"], .runtime-input input[type="radio"]')] - .filter((element) => element.getClientRects().length > 0) - .map((element) => { - const label = element.labels?.[0]; - if (!(label instanceof globalThis.HTMLLabelElement)) throw new Error('Runtime Schema form choice omitted its associated label.'); - const labelRect = label.getBoundingClientRect(); - const controlRect = element.getBoundingClientRect(); - return Object.freeze({ - associated: [...element.labels ?? []].includes(label), - controlBottom: controlRect.bottom, - controlLeft: controlRect.left, - controlRight: controlRect.right, - controlTop: controlRect.top, - height: labelRect.height, - label: label.textContent?.trim() ?? element.id, - labelBottom: labelRect.bottom, - left: labelRect.left, - right: labelRect.right, - top: labelRect.top, - }); - }); - return Object.freeze({ - bodyScrollLeft: body?.scrollLeft ?? 0, - choiceLabels: Object.freeze(choiceLabels), - documentScrollLeft: documentElement.scrollLeft, - textInputs: Object.freeze(textInputs), - viewportWidth: globalThis.innerWidth, - windowScrollX: globalThis.scrollX, - }); - }); - const run = page.getByRole('button', { name: 'Run', exact: true }); - const history = page.getByRole('region', { name: 'Runtime run history' }).locator('ol > li'); - await page.getByLabel('Runtime surface').selectOption('mcp.runtime_status'); - await run.click(); - await expect.poll(async () => history.count(), { timeout: browserTimeout }).toBeGreaterThan(0); - await page.getByLabel('Runtime surface').selectOption('hook.claude'); - await page.getByLabel('Runtime fixture').selectOption('claude-post-tool-use-write'); - await run.click(); - await expect(page.getByRole('dialog')).toBeVisible({ timeout: browserTimeout }); - const layout = await page.evaluate(() => { - const { body, documentElement } = globalThis.document; - documentElement.scrollLeft = 0; - if (body !== null) body.scrollLeft = 0; - globalThis.scrollTo({ left: 0, top: globalThis.scrollY }); - const elements = [ - globalThis.document.querySelector('.runtime-playground'), - globalThis.document.querySelector('.runtime-controls'), - globalThis.document.querySelector('.runtime-stage'), - ...[...globalThis.document.querySelectorAll('.runtime-controls label, .runtime-controls select')] - .filter((element) => element.getClientRects().length > 0), - ]; - const controls = [...globalThis.document.querySelectorAll([ - '.runtime-controls select', - '.runtime-input select', - '.runtime-input button', - '.runtime-actions button', - '.runtime-history button', - '.runtime-confirmation button', - ].join(','))].filter((element) => element.getClientRects().length > 0); - return Object.freeze({ - bodyScrollLeft: body?.scrollLeft ?? 0, - boxes: Object.freeze(elements.map((element) => { - if (!(element instanceof globalThis.HTMLElement)) { - throw new Error('Runtime mobile layout omitted a required visible control or stage.'); - } - const rect = element.getBoundingClientRect(); - return Object.freeze({ left: rect.left, right: rect.right }); - })), - controls: Object.freeze(controls.map((element) => { - const rect = element.getBoundingClientRect(); - return Object.freeze({ - height: rect.height, - label: element.getAttribute('aria-label') ?? element.textContent?.trim() ?? element.tagName, - left: rect.left, - right: rect.right, - }); - })), - documentScrollLeft: documentElement.scrollLeft, - viewportWidth: globalThis.innerWidth, - windowScrollX: globalThis.scrollX, - }); - }); - expect(layout.windowScrollX).toBe(0); - expect(layout.documentScrollLeft).toBe(0); - expect(layout.bodyScrollLeft).toBe(0); - expect(layout.viewportWidth).toBe(390); - expect(schemaForm.windowScrollX).toBe(0); - expect(schemaForm.documentScrollLeft).toBe(0); - expect(schemaForm.bodyScrollLeft).toBe(0); - expect(schemaForm.viewportWidth).toBe(390); - expect(schemaForm.textInputs.length).toBeGreaterThan(0); - expect(schemaForm.choiceLabels.length).toBeGreaterThan(0); - for (const input of schemaForm.textInputs) { - expect(input.height, input.label).toBeGreaterThanOrEqual(40); - expect(input.left, input.label).toBeGreaterThanOrEqual(0); - expect(input.right, input.label).toBeLessThanOrEqual(schemaForm.viewportWidth); - } - for (const choice of schemaForm.choiceLabels) { - expect(choice.associated, choice.label).toBe(true); - expect(choice.label.length).toBeGreaterThan(0); - expect(choice.height, choice.label).toBeGreaterThanOrEqual(40); - expect(choice.left, choice.label).toBeGreaterThanOrEqual(0); - expect(choice.right, choice.label).toBeLessThanOrEqual(schemaForm.viewportWidth); - expect(choice.controlLeft, choice.label).toBeGreaterThanOrEqual(choice.left); - expect(choice.controlRight, choice.label).toBeLessThanOrEqual(choice.right); - expect(choice.controlTop, choice.label).toBeGreaterThanOrEqual(choice.top); - expect(choice.controlBottom, choice.label).toBeLessThanOrEqual(choice.labelBottom); - } - expect(layout.controls.length).toBeGreaterThan(0); - for (const box of layout.boxes) { - expect(box.left).toBeGreaterThanOrEqual(0); - expect(box.right).toBeLessThanOrEqual(layout.viewportWidth); - } - for (const control of layout.controls) { - expect(control.height, control.label).toBeGreaterThanOrEqual(40); - expect(control.left, control.label).toBeGreaterThanOrEqual(0); - expect(control.right, control.label).toBeLessThanOrEqual(layout.viewportWidth); - } - expect(pageErrors).toEqual([]); - } finally { - await fixture.close(); - } -}); - e2e('resets the selected Claude fixture to its seed without replacing prior runtime evidence', { timeout: 120_000 }, async ({ page }) => { const fixture = await startRuntimePlaygroundFixture(); const resetRequests: unknown[] = []; diff --git a/packages/workbench/tests/runtime-playground.test.ts b/packages/workbench/tests/runtime-playground.test.ts index b0ff08c1e..e0ec39d01 100644 --- a/packages/workbench/tests/runtime-playground.test.ts +++ b/packages/workbench/tests/runtime-playground.test.ts @@ -1,6 +1,3 @@ -import { readFile } from 'node:fs/promises'; -import { join } from 'node:path'; - import { expect, it } from '@rstest/core'; import { createElement } from 'react'; import { renderToReadableStream, renderToStaticMarkup } from 'react-dom/server'; @@ -142,13 +139,6 @@ const renderWhenReady = async (node: React.ReactNode): Promise => { return new Response(stream).text(); }; -it('includes the Runtime Playground unit contract in its dedicated coverage selection', async () => { - const config = await readFile(join(process.cwd(), 'rstest.runtime-playground.config.ts'), 'utf8'); - - expect(config).toContain("'packages/workbench/tests/runtime-playground.test.ts'"); - expect(config).toContain("'packages/workbench/tests/runtime-playground.browser.test.tsx'"); -}); - it('keeps unavailable runtime absent and composes no live MCP page adapter', () => { const controller = createRuntimePlaygroundController({ bootstrap: Object.freeze({ kind: 'unavailable' }), client: clientFor(), profiles }); diff --git a/packages/workbench/tests/workbench-dev-command.test.ts b/packages/workbench/tests/workbench-dev-command.test.ts index 1e56af4ef..dd607272e 100644 --- a/packages/workbench/tests/workbench-dev-command.test.ts +++ b/packages/workbench/tests/workbench-dev-command.test.ts @@ -1,18 +1,10 @@ import { execFile as executeFile } from 'node:child_process'; -import { readFile } from 'node:fs/promises'; import { promisify } from 'node:util'; import { expect, it } from '@rstest/core'; const execFile = promisify(executeFile); -it('launches contributor HMR through the workspace pnpm toolchain', async () => { - const source = await readFile(`${process.cwd()}/packages/workbench/scripts/dev.mjs`, 'utf8'); - - expect(source).toContain("process.platform === 'win32' ? 'pnpm.cmd' : 'pnpm'"); - expect(source).not.toMatch(/\bnpm(?:\.cmd)?\b/u); -}); - it('fails clearly instead of starting contributor HMR without a live foreground proxy target', async () => { await expect(execFile(process.execPath, ['scripts/dev.mjs'], { cwd: `${process.cwd()}/packages/workbench`, diff --git a/rstest.integration-tests.ts b/rstest.integration-tests.ts index c6427fc05..be1049615 100644 --- a/rstest.integration-tests.ts +++ b/rstest.integration-tests.ts @@ -46,7 +46,6 @@ export const integrationTestFiles: readonly string[] = [ 'packages/agent-bundle/tests/script-playground-service.test.ts', 'packages/agent-bundle/tests/target-hook-contract.test.ts', 'packages/agent-bundle/tests/target-mcp-runtime.test.ts', - 'packages/workbench/tests/artifacts-real.e2e.test.ts', 'packages/workbench/tests/comparisons-page-client-scope-browser.test.ts', 'packages/workbench/tests/evals-real.e2e.test.ts', 'packages/workbench/tests/examples-real.e2e.test.ts', From 0b835da5008c9308afbfd07dca754ccff92ad547 Mon Sep 17 00:00:00 2001 From: ScriptedAlchemy Date: Sat, 29 Aug 2026 01:43:57 +0000 Subject: [PATCH 05/16] test(workbench): drop 390px mobile assertion tails from desktop-only suites The Workbench is a desktop-only product validated at 1440x900, so the 390px viewport resizes and horizontal-overflow checks appended to real host e2e tests assert a layout the product does not ship. Removes only those mobile assertion sites, keeps every host test, runs the MCP App preview browser test at the desktop viewport, and deletes the mobile capture path (PNG, mobileLayout evidence, --mobile flag) from the runtime playground capture script plus the README/topology command strings that pin its exact invocation. --- examples/rsc-agent-runtime/README.md | 1 - .../tests/rsc-runtime-topology-script.test.ts | 1 - .../scripts/capture-runtime-playground.mjs | 138 +----------------- .../workbench/tests/evals-real.e2e.test.ts | 2 - .../workbench/tests/logs-real.e2e.test.ts | 3 - .../tests/mcp-app-preview-browser.test.ts | 5 +- .../workbench/tests/mcp-app-real.e2e.test.ts | 6 - .../tests/mcp-session-timeout.e2e.test.ts | 2 - packages/workbench/tests/overview.e2e.test.ts | 2 - .../tests/playground-real.e2e.test.ts | 4 - .../tests/runtime-playground-capture.test.ts | 64 +------- 11 files changed, 4 insertions(+), 224 deletions(-) diff --git a/examples/rsc-agent-runtime/README.md b/examples/rsc-agent-runtime/README.md index 8d18e9024..25af75ded 100644 --- a/examples/rsc-agent-runtime/README.md +++ b/examples/rsc-agent-runtime/README.md @@ -62,7 +62,6 @@ the published package: ```bash node packages/workbench/scripts/capture-runtime-playground.mjs \ --desktop "$PWD/docs/assets/rsc-runtime-workbench/desktop.png" \ - --mobile "$PWD/docs/assets/rsc-runtime-workbench/mobile.png" \ --hmr-before "$PWD/docs/assets/rsc-runtime-workbench/hmr-before.png" \ --hmr-after "$PWD/docs/assets/rsc-runtime-workbench/hmr-after.png" \ --compile-error "$PWD/docs/assets/rsc-runtime-workbench/compile-error.png" \ diff --git a/packages/agent-bundle/tests/rsc-runtime-topology-script.test.ts b/packages/agent-bundle/tests/rsc-runtime-topology-script.test.ts index 084a57d4c..cbb00876b 100644 --- a/packages/agent-bundle/tests/rsc-runtime-topology-script.test.ts +++ b/packages/agent-bundle/tests/rsc-runtime-topology-script.test.ts @@ -13,7 +13,6 @@ const script = join(workspaceRoot, 'scripts', 'rsc-runtime-topology.mjs'); const output = 'docs/architecture/rsc-runtime-workbench.md'; const captureCommand = `node packages/workbench/scripts/capture-runtime-playground.mjs \\ --desktop "$PWD/docs/assets/rsc-runtime-workbench/desktop.png" \\ - --mobile "$PWD/docs/assets/rsc-runtime-workbench/mobile.png" \\ --hmr-before "$PWD/docs/assets/rsc-runtime-workbench/hmr-before.png" \\ --hmr-after "$PWD/docs/assets/rsc-runtime-workbench/hmr-after.png" \\ --compile-error "$PWD/docs/assets/rsc-runtime-workbench/compile-error.png" \\ diff --git a/packages/workbench/scripts/capture-runtime-playground.mjs b/packages/workbench/scripts/capture-runtime-playground.mjs index 1d2203677..d52e4a5e3 100644 --- a/packages/workbench/scripts/capture-runtime-playground.mjs +++ b/packages/workbench/scripts/capture-runtime-playground.mjs @@ -9,10 +9,8 @@ import { startRuntimePlaygroundFixture } from '../tests/helpers/runtime-playgrou const browserTimeout = 30_000 * timeScale; const desktopViewport = Object.freeze({ height: 900, width: 1440 }); -const mobileViewport = Object.freeze({ height: 844, width: 390 }); const outputFlags = Object.freeze([ '--desktop', - '--mobile', '--hmr-before', '--hmr-after', '--compile-error', @@ -47,7 +45,6 @@ const parseArguments = (argv) => { evidence: values.get('--evidence'), hmrAfter: values.get('--hmr-after'), hmrBefore: values.get('--hmr-before'), - mobile: values.get('--mobile'), recovered: values.get('--recovered'), }); }; @@ -271,16 +268,6 @@ const boundedLayoutNumber = (value, label) => { return Math.round(value * 1000) / 1000; }; -const boundedHorizontalBounds = (value, label) => { - if (typeof value !== 'object' || value === null) throw new Error(`Runtime capture ${label} bounds were absent.`); - const candidate = value; - const left = boundedLayoutNumber(candidate.left, `${label} left`); - const right = boundedLayoutNumber(candidate.right, `${label} right`); - const viewportWidth = boundedLayoutNumber(candidate.viewportWidth, `${label} viewport width`); - if (viewportWidth <= 0) throw new Error(`Runtime capture ${label} viewport width was not positive.`); - return Object.freeze({ left, right, viewportWidth }); -}; - const boundedVerticalBounds = (value, label) => { if (typeof value !== 'object' || value === null) throw new Error(`Runtime capture ${label} bounds were absent.`); const candidate = value; @@ -291,114 +278,6 @@ const boundedVerticalBounds = (value, label) => { return Object.freeze({ bottom, top, viewportHeight }); }; -const captureMobileLayout = async (page, frame) => { - const host = await page.evaluate(() => { - const runtimeContent = globalThis.document.querySelector('.runtime-content'); - const playground = globalThis.document.querySelector('.runtime-playground'); - const controls = globalThis.document.querySelector('.runtime-controls'); - const stage = globalThis.document.querySelector('.runtime-stage'); - const outerFrame = globalThis.document.querySelector('.runtime-stage .mcp-app-preview iframe'); - if (!(runtimeContent instanceof globalThis.HTMLElement) - || !(playground instanceof globalThis.HTMLElement) - || !(controls instanceof globalThis.HTMLElement) - || !(stage instanceof globalThis.HTMLElement) - || !(outerFrame instanceof globalThis.HTMLIFrameElement)) { - throw new Error('Runtime capture mobile host elements were absent.'); - } - const scrollers = new Set(); - for (const start of [runtimeContent, playground, controls, stage, outerFrame]) { - for (let element = start; element instanceof globalThis.HTMLElement && element !== globalThis.document.body; element = element.parentElement) { - scrollers.add(element); - } - } - globalThis.document.documentElement.scrollLeft = 0; - if (globalThis.document.body !== null) globalThis.document.body.scrollLeft = 0; - for (const element of scrollers) element.scrollLeft = 0; - globalThis.scrollTo({ left: 0, top: globalThis.scrollY }); - const bounds = (element) => { - const rect = element.getBoundingClientRect(); - return Object.freeze({ left: rect.left, right: rect.right, viewportWidth: globalThis.innerWidth }); - }; - return Object.freeze({ - bodyScrollLeft: globalThis.document.body?.scrollLeft ?? 0, - controls: bounds(controls), - documentScrollLeft: globalThis.document.documentElement.scrollLeft, - host: bounds(runtimeContent), - hostScrollerScrollLefts: Object.freeze([...scrollers].map((element) => element.scrollLeft)), - outerFrame: bounds(outerFrame), - playground: bounds(playground), - stage: bounds(stage), - windowScrollX: globalThis.scrollX, - }); - }); - const child = await frame.evaluate(() => { - const heading = globalThis.document.querySelector('h1'); - const marker = globalThis.document.querySelector('[data-testid="runtime-capture-marker"]'); - if (!(heading instanceof globalThis.HTMLElement) || !(marker instanceof globalThis.HTMLElement)) { - throw new Error('Runtime capture mobile child landmarks were absent.'); - } - globalThis.document.documentElement.scrollLeft = 0; - if (globalThis.document.body !== null) globalThis.document.body.scrollLeft = 0; - globalThis.scrollTo({ left: 0, top: globalThis.scrollY }); - const bounds = (element) => { - const rect = element.getBoundingClientRect(); - return Object.freeze({ left: rect.left, right: rect.right, viewportWidth: globalThis.innerWidth }); - }; - return Object.freeze({ - heading: bounds(heading), - marker: bounds(marker), - scrollX: globalThis.scrollX, - }); - }); - if (!Array.isArray(host.hostScrollerScrollLefts) || host.hostScrollerScrollLefts.length === 0 || host.hostScrollerScrollLefts.length > 32) { - throw new Error('Runtime capture mobile host scroller evidence was outside its bounded shape.'); - } - const hostScrollerScrollLefts = Object.freeze(host.hostScrollerScrollLefts.map((value, index) => - boundedLayoutNumber(value, `host scroller ${index} scrollLeft`))); - const controls = boundedHorizontalBounds(host.controls, 'controls'); - const runtimeHost = boundedHorizontalBounds(host.host, 'runtime host'); - const playground = boundedHorizontalBounds(host.playground, 'playground'); - const stage = boundedHorizontalBounds(host.stage, 'stage'); - const outerFrame = boundedHorizontalBounds(host.outerFrame, 'outer frame'); - const childHeading = boundedHorizontalBounds(child.heading, 'child heading'); - const childMarker = boundedHorizontalBounds(child.marker, 'child marker'); - const mobileLayout = Object.freeze({ - bodyScrollLeft: boundedLayoutNumber(host.bodyScrollLeft, 'body scrollLeft'), - childHeading, - childHeadingWithinViewport: childHeading.left >= 0 && childHeading.right <= childHeading.viewportWidth, - childMarker, - childMarkerWithinViewport: childMarker.left >= 0 && childMarker.right <= childMarker.viewportWidth, - childScrollX: boundedLayoutNumber(child.scrollX, 'child scrollX'), - controls, - controlsWithinViewport: controls.left >= 0 && controls.right <= controls.viewportWidth, - documentScrollLeft: boundedLayoutNumber(host.documentScrollLeft, 'document scrollLeft'), - host: runtimeHost, - hostWithinViewport: runtimeHost.left >= 0 && runtimeHost.right <= runtimeHost.viewportWidth, - hostScrollerScrollLefts, - outerFrame, - outerFrameWithinViewport: outerFrame.left >= 0 && outerFrame.right <= outerFrame.viewportWidth, - playground, - playgroundWithinViewport: playground.left >= 0 && playground.right <= playground.viewportWidth, - stage, - stageWithinViewport: stage.left >= 0 && stage.right <= stage.viewportWidth, - windowScrollX: boundedLayoutNumber(host.windowScrollX, 'window scrollX'), - }); - const settled = mobileLayout.windowScrollX === 0 - && mobileLayout.documentScrollLeft === 0 - && mobileLayout.bodyScrollLeft === 0 - && mobileLayout.hostScrollerScrollLefts.every((value) => value === 0) - && mobileLayout.hostWithinViewport - && mobileLayout.playgroundWithinViewport - && mobileLayout.controlsWithinViewport - && mobileLayout.stageWithinViewport - && mobileLayout.outerFrameWithinViewport - && mobileLayout.childScrollX === 0 - && mobileLayout.childHeadingWithinViewport - && mobileLayout.childMarkerWithinViewport; - if (!settled) throw new Error(`Runtime App did not settle into the 390px mobile capture viewport: ${JSON.stringify(mobileLayout)}`); - return mobileLayout; -}; - const captureCompileErrorLayout = async (page, generation) => { const layout = await page.evaluate(({ diagnosticsSelector, lastGoodText }) => { const diagnostics = globalThis.document.querySelector(diagnosticsSelector); @@ -644,19 +523,6 @@ const capture = async (outputs) => { }); if (desktopControlColumns !== 4) throw new Error(`Runtime capture expected four desktop control columns, received ${desktopControlColumns}.`); await screenshot(page, outputs.desktop); - await page.setViewportSize(mobileViewport); - await outerFrame.scrollIntoViewIfNeeded(); - await marker.scrollIntoViewIfNeeded(); - const mobileLayout = await captureMobileLayout(page, appFrame); - const mobileWithoutHorizontalOverflow = await page.evaluate(() => { - const { body, documentElement } = globalThis.document; - return documentElement.scrollWidth <= documentElement.clientWidth - && (body === null || body.scrollWidth <= documentElement.clientWidth); - }); - if (!mobileWithoutHorizontalOverflow) { - throw new Error('Runtime Playground overflowed the 390px document viewport.'); - } - await screenshot(page, outputs.mobile); await Promise.all([ restore(fixture.serverComponentSource, originals[0]), @@ -690,14 +556,12 @@ const capture = async (outputs) => { hmrWithoutReload: documentTimeOriginAfter === documentTimeOriginBefore, lastGoodGenerationDuringError, lastGoodPreserved, - mobileLayout, - mobileWithoutHorizontalOverflow, providerSessionId, recovered: generationRecovered !== lastGoodGenerationDuringError, runAfter, runBefore, sandboxOpaqueOrigin, - viewports: Object.freeze({ desktop: desktopViewport, mobile: mobileViewport }), + viewports: Object.freeze({ desktop: desktopViewport }), }); await writeEvidence(outputs.evidence, evidence); } catch (error) { diff --git a/packages/workbench/tests/evals-real.e2e.test.ts b/packages/workbench/tests/evals-real.e2e.test.ts index 1061b461c..1c8dfb7e0 100644 --- a/packages/workbench/tests/evals-real.e2e.test.ts +++ b/packages/workbench/tests/evals-real.e2e.test.ts @@ -273,8 +273,6 @@ e2e('admits a deterministic Eval promptly and renders refreshed durable evidence await expect(page.getByText(`Run ${replacement.run.id} finished:`)).toBeVisible({ timeout: runCompletionTimeout }); await expect(page.getByRole('link', { name: 'Download evidence.json' })).toHaveCount(0); - expect(await page.evaluate(() => document.documentElement.scrollWidth <= window.innerWidth)).toBe(true); - await page.setViewportSize({ height: 844, width: 390 }); expect(await page.evaluate(() => document.documentElement.scrollWidth <= window.innerWidth)).toBe(true); expect(pageErrors).toEqual([]); } finally { diff --git a/packages/workbench/tests/logs-real.e2e.test.ts b/packages/workbench/tests/logs-real.e2e.test.ts index 08f2ebcae..43aa86789 100644 --- a/packages/workbench/tests/logs-real.e2e.test.ts +++ b/packages/workbench/tests/logs-real.e2e.test.ts @@ -67,9 +67,6 @@ e2e('shows real producer logs with replay, filters, redaction, responsive layout const bodyText = await page.locator('body').innerText(); expect(bodyText).not.toContain(project.root); expect(bodyText).not.toContain('fixture-secret'); - - await page.setViewportSize({ height: 844, width: 390 }); - expect(await page.evaluate(() => document.documentElement.scrollWidth <= window.innerWidth)).toBe(true); expect(pageErrors).toEqual([]); } finally { await server.close(); diff --git a/packages/workbench/tests/mcp-app-preview-browser.test.ts b/packages/workbench/tests/mcp-app-preview-browser.test.ts index 5997098a3..1f9157ec4 100644 --- a/packages/workbench/tests/mcp-app-preview-browser.test.ts +++ b/packages/workbench/tests/mcp-app-preview-browser.test.ts @@ -144,10 +144,10 @@ const mountedPreviewFixture = async () => { }; describe('MCP App preview browser', () => { - it('mounts the preview in Chrome for ready, error, fallback, unmount-race, and 390px layouts', async () => { + it('mounts the preview in Chrome for ready, error, fallback, and unmount-race states', async () => { const fixture = await mountedPreviewFixture(); const browser = await chromium.launch({ channel: 'chrome' }); - const page = await browser.newPage({ viewport: { height: 800, width: 390 } }); + const page = await browser.newPage({ viewport: { height: 900, width: 1440 } }); const browserErrors: string[] = []; const responses: string[] = []; page.on('pageerror', (error) => { browserErrors.push(error.message); }); @@ -177,7 +177,6 @@ describe('MCP App preview browser', () => { expect(await frame.getAttribute('sandbox')).toBe('allow-scripts allow-same-origin'); expect(await frame.getAttribute('referrerpolicy')).toBe('no-referrer'); expect(await frame.getAttribute('src')).toBe('http://127.0.0.1:43124/#mcp-app-preview'); - expect(await page.evaluate(() => document.documentElement.scrollWidth <= window.innerWidth)).toBe(true); await load(); await page.evaluate(() => (globalThis as typeof globalThis & { diff --git a/packages/workbench/tests/mcp-app-real.e2e.test.ts b/packages/workbench/tests/mcp-app-real.e2e.test.ts index e4611ecd9..f2b5c235e 100644 --- a/packages/workbench/tests/mcp-app-real.e2e.test.ts +++ b/packages/workbench/tests/mcp-app-real.e2e.test.ts @@ -420,9 +420,6 @@ e2e('runs a generated SDK-v2 App through the real foreground session and separat }), { timeout: browserTimeout }).toBe(true); expect(await appFrame.content()).not.toContain(foregroundToken); - await page.setViewportSize({ height: 844, width: 390 }); - expect(await page.evaluate(() => document.documentElement.scrollWidth <= window.innerWidth)).toBe(true); - const firstClose = page.waitForRequest((request) => request.url().startsWith(`${foregroundOrigin}/api/mcp/apps/`) && request.url().endsWith('/close')); await page.getByRole('button', { name: 'Close App preview' }).click(); const firstCloseBody = requestBody((await firstClose).postData()) as Readonly<{ readonly id: string }>; @@ -1277,9 +1274,6 @@ e2e('opens the real RSC runtime timeline App from provider-owned run evidence', new URL(entry.href).origin === fixture.url && entry.senderOrigin === destinationOrigin && entry.message !== null && typeof entry.message === 'object' && (entry.message as Readonly>).method === 'ui/initialize').length, { timeout: 15_000 * timeScale }).toBe(controllerOrigin === destinationOrigin ? 2 : 1); - await page.setViewportSize({ height: 900, width: 390 }); - await expect.poll(() => page.evaluate(() => document.documentElement.scrollWidth <= document.documentElement.clientWidth), { timeout: 15_000 * timeScale }).toBe(true); - const destinationAppFrame = async () => { for (const frame of page.frames()) { const parent = frame.parentFrame(); diff --git a/packages/workbench/tests/mcp-session-timeout.e2e.test.ts b/packages/workbench/tests/mcp-session-timeout.e2e.test.ts index 0c5e2bc0b..d39d96d51 100644 --- a/packages/workbench/tests/mcp-session-timeout.e2e.test.ts +++ b/packages/workbench/tests/mcp-session-timeout.e2e.test.ts @@ -91,8 +91,6 @@ e2e('opens one browser MCP session with an immutable timeout', { timeout: 90_000 await expect(page.locator('.mcp-page-phase')).toContainText('Session ready', { timeout: browserTimeout }); expect(await page.getByLabel('Session timeout (ms)').isDisabled()).toBe(true); await expect(page.getByLabel('Session timeout (ms)')).toHaveValue('12345'); - await page.setViewportSize({ height: 844, width: 390 }); - expect(await page.evaluate(() => document.documentElement.scrollWidth <= window.innerWidth)).toBe(true); expect(pageErrors).toEqual([]); } catch (error) { testFailure = error; diff --git a/packages/workbench/tests/overview.e2e.test.ts b/packages/workbench/tests/overview.e2e.test.ts index 1aa224522..95ac98113 100644 --- a/packages/workbench/tests/overview.e2e.test.ts +++ b/packages/workbench/tests/overview.e2e.test.ts @@ -1257,8 +1257,6 @@ e2e('opens one real epoch MCP session and keeps its playground operations respon expect(artifactMcpSessionRequests.some((request) => request.startsWith('POST /api/mcp/sessions/'))).toBe(true); expect(runtimeRequests).toEqual([]); expect(projectEventRequests).toEqual(['GET /api/project/events']); - await page.setViewportSize({ height: 844, width: 390 }); - expect(await page.evaluate(() => document.documentElement.scrollWidth <= window.innerWidth)).toBe(true); expect(pageErrors).toEqual([]); } catch (error) { testFailure = error; diff --git a/packages/workbench/tests/playground-real.e2e.test.ts b/packages/workbench/tests/playground-real.e2e.test.ts index ed54b0822..84fa5d905 100644 --- a/packages/workbench/tests/playground-real.e2e.test.ts +++ b/packages/workbench/tests/playground-real.e2e.test.ts @@ -293,8 +293,6 @@ e2e('executes server-owned Playground operations with pinned traces, export, pro expect(body).not.toHaveProperty('task'); expect(body).not.toHaveProperty('script'); } - await page.setViewportSize({ height: 844, width: 390 }); - expect(await page.evaluate(() => document.documentElement.scrollWidth <= window.innerWidth)).toBe(true); expect(consoleErrors).toEqual([]); expect(pageErrors).toEqual([]); } finally { @@ -515,8 +513,6 @@ e2e('executes catalog-admitted native prompts through the real host harness', { expect(request).not.toHaveProperty('outcome'); } expect(await page.evaluate(() => document.documentElement.scrollWidth <= window.innerWidth)).toBe(true); - await page.setViewportSize({ height: 844, width: 390 }); - expect(await page.evaluate(() => document.documentElement.scrollWidth <= window.innerWidth)).toBe(true); expect(consoleErrors).toEqual([]); expect(pageErrors).toEqual([]); } finally { diff --git a/packages/workbench/tests/runtime-playground-capture.test.ts b/packages/workbench/tests/runtime-playground-capture.test.ts index ec606b17f..1c4868846 100644 --- a/packages/workbench/tests/runtime-playground-capture.test.ts +++ b/packages/workbench/tests/runtime-playground-capture.test.ts @@ -14,12 +14,6 @@ const workspaceRoot = process.cwd(); const captureScript = join(workspaceRoot, 'packages', 'workbench', 'scripts', 'capture-runtime-playground.mjs'); const pngSignature = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]); -type HorizontalBounds = Readonly<{ - readonly left: number; - readonly right: number; - readonly viewportWidth: number; -}>; - type VerticalBounds = Readonly<{ readonly bottom: number; readonly top: number; @@ -52,28 +46,6 @@ type CaptureEvidence = Readonly<{ readonly hmrWithoutReload: boolean; readonly lastGoodGenerationDuringError: string; readonly lastGoodPreserved: boolean; - readonly mobileLayout: Readonly<{ - readonly bodyScrollLeft: number; - readonly childHeading: HorizontalBounds; - readonly childHeadingWithinViewport: boolean; - readonly childMarker: HorizontalBounds; - readonly childMarkerWithinViewport: boolean; - readonly childScrollX: number; - readonly documentScrollLeft: number; - readonly controls: HorizontalBounds; - readonly controlsWithinViewport: boolean; - readonly host: HorizontalBounds; - readonly hostWithinViewport: boolean; - readonly hostScrollerScrollLefts: readonly number[]; - readonly outerFrame: HorizontalBounds; - readonly outerFrameWithinViewport: boolean; - readonly playground: HorizontalBounds; - readonly playgroundWithinViewport: boolean; - readonly stage: HorizontalBounds; - readonly stageWithinViewport: boolean; - readonly windowScrollX: number; - }>; - readonly mobileWithoutHorizontalOverflow: boolean; readonly providerSessionId: string; readonly recovered: boolean; readonly runAfter: string; @@ -81,7 +53,6 @@ type CaptureEvidence = Readonly<{ readonly sandboxOpaqueOrigin: boolean; readonly viewports: Readonly<{ readonly desktop: Readonly<{ readonly height: number; readonly width: number }>; - readonly mobile: Readonly<{ readonly height: number; readonly width: number }>; }>; }>; @@ -150,7 +121,7 @@ test('settles every capture cleanup action without masking the primary failure', } }); -test('captures identity-backed HMR, last-good, recovery, and responsive browser evidence', { timeout: 600_000 }, async () => { +test('captures identity-backed HMR, last-good, recovery, and desktop browser evidence', { timeout: 600_000 }, async () => { const outputRoot = await mkdtemp(join(tmpdir(), 'agent-bundle-runtime-capture-')); const outputs = Object.freeze({ compileError: join(outputRoot, 'compile-error.png'), @@ -158,13 +129,11 @@ test('captures identity-backed HMR, last-good, recovery, and responsive browser evidence: join(outputRoot, 'evidence.json'), hmrAfter: join(outputRoot, 'hmr-after.png'), hmrBefore: join(outputRoot, 'hmr-before.png'), - mobile: join(outputRoot, 'mobile.png'), recovered: join(outputRoot, 'recovered.png'), }); try { const { stdout } = await execFile(process.execPath, [captureScript, '--desktop', outputs.desktop, - '--mobile', outputs.mobile, '--hmr-before', outputs.hmrBefore, '--hmr-after', outputs.hmrAfter, '--compile-error', outputs.compileError, @@ -174,7 +143,6 @@ test('captures identity-backed HMR, last-good, recovery, and responsive browser await Promise.all([ expectPng(outputs.desktop, 1440, 900), - expectPng(outputs.mobile, 390, 844), expectPng(outputs.hmrBefore, 1440, 900), expectPng(outputs.hmrAfter, 1440, 900), expectPng(outputs.compileError, 1440, 900), @@ -186,7 +154,6 @@ test('captures identity-backed HMR, last-good, recovery, and responsive browser 'evidence.json', 'hmr-after.png', 'hmr-before.png', - 'mobile.png', 'recovered.png', ]); @@ -211,41 +178,12 @@ test('captures identity-backed HMR, last-good, recovery, and responsive browser desktopControlColumns: 4, hmrWithoutReload: true, lastGoodPreserved: true, - mobileLayout: { - bodyScrollLeft: 0, - childHeadingWithinViewport: true, - childMarkerWithinViewport: true, - childScrollX: 0, - controlsWithinViewport: true, - documentScrollLeft: 0, - hostWithinViewport: true, - outerFrameWithinViewport: true, - playgroundWithinViewport: true, - stageWithinViewport: true, - windowScrollX: 0, - }, - mobileWithoutHorizontalOverflow: true, recovered: true, sandboxOpaqueOrigin: true, viewports: { desktop: { height: 900, width: 1440 }, - mobile: { height: 844, width: 390 }, }, }); - expect(evidence.mobileLayout.hostScrollerScrollLefts.every((value) => value === 0)).toBe(true); - for (const bounds of [ - evidence.mobileLayout.host, - evidence.mobileLayout.playground, - evidence.mobileLayout.controls, - evidence.mobileLayout.stage, - evidence.mobileLayout.outerFrame, - evidence.mobileLayout.childHeading, - evidence.mobileLayout.childMarker, - ]) { - expect(bounds.left).toBeGreaterThanOrEqual(0); - expect(bounds.right).toBeLessThanOrEqual(bounds.viewportWidth); - expect(bounds.viewportWidth).toBeGreaterThan(0); - } expect(evidence.providerSessionId).toEqual(expect.any(String)); expect(evidence.providerSessionId.length).toBeGreaterThan(0); expect(evidence.compactRunId).toEqual(expect.any(String)); From b9de1ffe10919a1d06c180e51fad6c21663d402e Mon Sep 17 00:00:00 2001 From: ScriptedAlchemy Date: Sat, 29 Aug 2026 01:44:05 +0000 Subject: [PATCH 06/16] test(rsc): consolidate the four fifty-run eviction sessions into one Each eviction test prepared its own dev session and drove fifty real invocations before exercising one hook, repeating the slowest setup in the suite four times (and flaking under load). One session now fills the fifty-artifact window once and drives the happy eviction, the held-reader reservation, the failed run-directory removal, and the failed artifact release in eviction order, preserving every distinct assertion including the readRunFlight path-traversal check and the close-retry accounting. Neighbouring retain-until-close, worker-bound, and containment tests are untouched. --- .../tests/dev-invocation.integration.test.ts | 332 ++++++------------ 1 file changed, 114 insertions(+), 218 deletions(-) diff --git a/examples/rsc-agent-runtime/tests/dev-invocation.integration.test.ts b/examples/rsc-agent-runtime/tests/dev-invocation.integration.test.ts index 6eafe5441..be1c11c07 100644 --- a/examples/rsc-agent-runtime/tests/dev-invocation.integration.test.ts +++ b/examples/rsc-agent-runtime/tests/dev-invocation.integration.test.ts @@ -1703,58 +1703,148 @@ process.stdout.end(JSON.stringify({ } }, 30_000); -test('keeps the newest fifty immutable run artifacts and evicts the oldest completed Flight', async () => { - const storageRoot = await mkdtemp(join(tmpdir(), 'rsc-agent-runtime-session-history-')); +test('drives the fifty-run eviction window through its happy, held-reader, failed-removal, and failed-release paths', async () => { + const storageRoot = await mkdtemp(join(tmpdir(), 'rsc-agent-runtime-session-eviction-')); const projectRoot = process.cwd(); const prepared = await new ProjectService({ includeDevRuntime: true, mode: 'development', root: projectRoot }).prepare('dev'); - const session = await createDevRuntimeProvider().start({ + const readerEntered = deferred(); + const releaseReader = deferred(); + const evictionReserved = deferred(); + let heldRunId: string | undefined; + let heldReaderAdmissions = 0; + let reservedRunId: string | undefined; + let failReleaseRunId: string | undefined; + let failedReleaseAttempts = 0; + let removalFailureRunId: string | undefined; + let failRemovalOnce = false; + let removalVictimReleaseAttempts = 0; + let removalVictimRemovalAttempts = 0; + const session = await RsbuildRuntimeSession.start({ artifactStatus: () => Object.freeze({ state: 'missing' as const }), emit: () => undefined, environment: Object.freeze({}), projectRoot, preparedRuntime: prepared.devRuntime!, - providerSessionId: 'session-history-test', + providerSessionId: 'session-eviction-test', signal: new AbortController().signal, storageRoot, + }, { + afterRunArtifactEvictionReserved: ({ runId }: Readonly<{ readonly runId: string }>) => { + if (runId === reservedRunId) evictionReserved.resolve(); + }, + beforeRunArtifactRelease: ({ runId }: Readonly<{ readonly runId: string }>) => { + if (runId === removalFailureRunId) removalVictimReleaseAttempts += 1; + if (runId !== failReleaseRunId) return; + failedReleaseAttempts += 1; + throw new Error('do-not-expose-eviction-release-secret'); + }, + beforeRunDirectoryRemoval: ({ runId }: Readonly<{ readonly runId: string }>) => { + if (runId === removalFailureRunId) removalVictimRemovalAttempts += 1; + if (failRemovalOnce && runId === removalFailureRunId) { + failRemovalOnce = false; + throw new Error('do-not-expose-evicted-run-directory-removal-secret'); + } + }, + beforeRunFlightRead: async ({ runId }: Readonly<{ readonly runId: string }>) => { + if (runId !== heldRunId) return; + heldReaderAdmissions += 1; + if (heldReaderAdmissions !== 1) return; + readerEntered.resolve(); + await releaseReader.promise; + }, }); try { await waitFor(() => session.status().activeVector !== undefined, 'Timed out waiting for an active runtime generation', 15_000); const generationId = session.status().activeVector!.runtimeGenerationId; const target = session.surfaces().find((surface) => surface.id === 'mcp.runtime_status')!.targets[0]!; - const first = await session.invoke({ + const request = { expectedGenerationId: generationId, input: {}, surfaceId: 'mcp.runtime_status', target, - }); - if (first.status !== 'succeeded') throw new Error(JSON.stringify(first.diagnostics)); - const firstFlight = await session.readRunFlight(first.id); - expect(firstFlight?.body.byteLength).toBeGreaterThan(0); + } as const; + const invokeSucceeded = async () => { + const run = await session.invoke(request); + if (run.status !== 'succeeded') throw new Error(JSON.stringify(run.status === 'failed' ? run.diagnostics : run)); + return run; + }; + + // The four oldest runs become, in eviction order, the victims of each exercised path. + const happyVictim = await invokeSucceeded(); + const readerVictim = await invokeSucceeded(); + const removalVictim = await invokeSucceeded(); + const releaseVictim = await invokeSucceeded(); + const happyFlight = await session.readRunFlight(happyVictim.id); + expect(happyFlight?.body.byteLength).toBeGreaterThan(0); await session.resetState({ expectedGenerationId: generationId, stateStoreId: 'playground' }); - expect(session.run(first.id)).toEqual(first); - - for (let index = 0; index < 50; index += 1) { - const run = await session.invoke({ - expectedGenerationId: generationId, - input: {}, - surfaceId: 'mcp.runtime_status', - target, - }); - expect(run.status).toBe('succeeded'); - } + expect(session.run(happyVictim.id)).toEqual(happyVictim); + + for (let index = 0; index < 46; index += 1) await invokeSucceeded(); + expect(session.runs(50)).toHaveLength(50); - expect(session.run(first.id)).toBeUndefined(); - await expect(session.readRunFlight(first.id)).resolves.toBeUndefined(); + // Happy path: the fifty-first run evicts the oldest completed Flight. + await invokeSucceeded(); + expect(session.run(happyVictim.id)).toBeUndefined(); + await expect(session.readRunFlight(happyVictim.id)).resolves.toBeUndefined(); await expect(session.readRunFlight('../flight.bin')).resolves.toBeUndefined(); expect(session.runs(50)).toHaveLength(50); - expect(session.runs(50)[0]!.id).not.toBe(first.id); + expect(session.runs(50)[0]!.id).not.toBe(happyVictim.id); expect((await readdir(join(storageRoot, 'runs'))).filter((entry) => entry !== '.agent-bundle-runtime-owner')).toHaveLength(50); + + // Held reader: eviction reserves the terminal run before draining its admitted Flight reader. + heldRunId = readerVictim.id; + reservedRunId = readerVictim.id; + const admittedReader = session.readRunFlight(readerVictim.id); + await readerEntered.promise; + const evicting = session.invoke(request); + await evictionReserved.promise; + await expect(session.readRunFlight(readerVictim.id)).resolves.toBeUndefined(); + expect(heldReaderAdmissions).toBe(1); + releaseReader.resolve(); + await expect(admittedReader).resolves.toMatchObject({ body: expect.any(Buffer) }); + await expect(evicting).resolves.toMatchObject({ status: 'succeeded' }); + await expect(session.readRunFlight(readerVictim.id)).resolves.toBeUndefined(); + + // Failed run-directory removal: successful history finalizes before the removal failure surfaces. + removalFailureRunId = removalVictim.id; + failRemovalOnce = true; + const removalFailure = await session.invoke(request); + expect(removalFailure).toMatchObject({ + diagnostics: [expect.objectContaining({ message: 'RSC runtime run artifact cleanup failed; cleanup failures: run-artifact.' })], + status: 'failed', + }); + expect(removalFailure.status === 'failed' && removalFailure.diagnostics[0]!.message) + .not.toContain('do-not-expose-evicted-run-directory-removal-secret'); + expect(session.run(removalVictim.id)).toBeUndefined(); + await expect(session.readRunFlight(removalVictim.id)).resolves.toBeUndefined(); + expect(await readdir(join(storageRoot, 'runs'))).toEqual(expect.arrayContaining([removalVictim.id])); + expect(removalVictimReleaseAttempts).toBe(1); + + // Failed artifact release: the oldest artifact and its terminal history stay owned. + failReleaseRunId = releaseVictim.id; + await expect(session.invoke(request)).rejects.toThrow('RSC runtime run artifact cleanup failed; cleanup failures: run-artifact.'); + expect(failedReleaseAttempts).toBeGreaterThan(0); + expect(session.run(releaseVictim.id)).toEqual(releaseVictim); + await expect(session.readRunFlight(releaseVictim.id)).resolves.toMatchObject({ body: expect.any(Buffer) }); + expect(await readdir(join(storageRoot, 'runs'))).toEqual(expect.arrayContaining([releaseVictim.id])); + + // Close retries the failed directory removal exactly once while the still-failing release keeps close owned. + const closing = session.close(); + expect(session.close()).toBe(closing); + await expect(closing).rejects.toMatchObject({ + message: 'RSC runtime session close failed; cleanup failures: run-artifact.', + }); + await expect(closing).rejects.not.toThrow('do-not-expose-eviction-release-secret'); + expect(failedReleaseAttempts).toBeGreaterThan(1); + expect(removalVictimReleaseAttempts).toBe(1); + expect(removalVictimRemovalAttempts).toBe(2); } finally { + releaseReader.resolve(); await session.close().catch(() => undefined); await rm(storageRoot, { force: true, recursive: true }); } -}, 45_000); +}, 240_000); test('retains a failed invocation Flight artifact until its explicit session-close release succeeds', async () => { const storageRoot = await mkdtemp(join(tmpdir(), 'rsc-agent-runtime-session-artifact-release-')); @@ -1811,200 +1901,6 @@ test('retains a failed invocation Flight artifact until its explicit session-clo } }, 45_000); -test('keeps the oldest artifact and terminal history owned when eviction release fails', async () => { - const storageRoot = await mkdtemp(join(tmpdir(), 'rsc-agent-runtime-session-eviction-release-')); - const projectRoot = process.cwd(); - const prepared = await new ProjectService({ includeDevRuntime: true, mode: 'development', root: projectRoot }).prepare('dev'); - let firstRunId: string | undefined; - let failedReleaseAttempts = 0; - const session = await RsbuildRuntimeSession.start({ - artifactStatus: () => Object.freeze({ state: 'missing' as const }), - emit: () => undefined, - environment: Object.freeze({}), - projectRoot, - preparedRuntime: prepared.devRuntime!, - providerSessionId: 'session-eviction-release-test', - signal: new AbortController().signal, - storageRoot, - }, { - beforeRunArtifactRelease: ({ runId }: Readonly<{ readonly runId: string }>) => { - if (runId !== firstRunId) return; - failedReleaseAttempts += 1; - throw new Error('do-not-expose-eviction-release-secret'); - }, - }); - - try { - await waitFor(() => session.status().activeVector !== undefined, 'Timed out waiting for an active runtime generation', 15_000); - const generationId = session.status().activeVector!.runtimeGenerationId; - const target = session.surfaces().find((surface) => surface.id === 'mcp.runtime_status')!.targets[0]!; - const request = { - expectedGenerationId: generationId, - input: {}, - surfaceId: 'mcp.runtime_status', - target, - } as const; - const first = await session.invoke(request); - if (first.status !== 'succeeded') throw new Error(JSON.stringify(first.diagnostics)); - firstRunId = first.id; - - for (let index = 0; index < 49; index += 1) { - await expect(session.invoke(request)).resolves.toMatchObject({ status: 'succeeded' }); - } - await expect(session.invoke(request)).rejects.toThrow('RSC runtime run artifact cleanup failed; cleanup failures: run-artifact.'); - - expect(failedReleaseAttempts).toBeGreaterThan(0); - expect(session.run(first.id)).toEqual(first); - await expect(session.readRunFlight(first.id)).resolves.toMatchObject({ body: expect.any(Buffer) }); - expect(await readdir(join(storageRoot, 'runs'))).toEqual(expect.arrayContaining([first.id])); - - const closing = session.close(); - expect(session.close()).toBe(closing); - await expect(closing).rejects.toMatchObject({ - message: 'RSC runtime session close failed; cleanup failures: run-artifact.', - }); - await expect(closing).rejects.not.toThrow('do-not-expose-eviction-release-secret'); - expect(failedReleaseAttempts).toBeGreaterThan(1); - } finally { - await session.close().catch(() => undefined); - await rm(storageRoot, { force: true, recursive: true }); - } -}, 60_000); - -test('reserves an evicting terminal run before draining its admitted Flight readers', async () => { - const storageRoot = await mkdtemp(join(tmpdir(), 'rsc-agent-runtime-session-eviction-reader-')); - const projectRoot = process.cwd(); - const prepared = await new ProjectService({ includeDevRuntime: true, mode: 'development', root: projectRoot }).prepare('dev'); - const readerEntered = deferred(); - const releaseReader = deferred(); - const evictionReserved = deferred(); - let firstRunId: string | undefined; - let holdFirstReader = false; - let firstReaderAdmissions = 0; - const session = await RsbuildRuntimeSession.start({ - artifactStatus: () => Object.freeze({ state: 'missing' as const }), - emit: () => undefined, - environment: Object.freeze({}), - projectRoot, - preparedRuntime: prepared.devRuntime!, - providerSessionId: 'session-eviction-reader-test', - signal: new AbortController().signal, - storageRoot, - }, { - afterRunArtifactEvictionReserved: ({ runId }: Readonly<{ readonly runId: string }>) => { - if (runId === firstRunId) evictionReserved.resolve(); - }, - beforeRunFlightRead: async ({ runId }: Readonly<{ readonly runId: string }>) => { - if (!holdFirstReader || runId !== firstRunId) return; - firstReaderAdmissions += 1; - if (firstReaderAdmissions !== 1) return; - readerEntered.resolve(); - await releaseReader.promise; - }, - }); - - try { - await waitFor(() => session.status().activeVector !== undefined, 'Timed out waiting for an active runtime generation', 15_000); - const generationId = session.status().activeVector!.runtimeGenerationId; - const target = session.surfaces().find((surface) => surface.id === 'mcp.runtime_status')!.targets[0]!; - const request = { - expectedGenerationId: generationId, - input: {}, - surfaceId: 'mcp.runtime_status', - target, - } as const; - const first = await session.invoke(request); - if (first.status !== 'succeeded') throw new Error(JSON.stringify(first.diagnostics)); - firstRunId = first.id; - - holdFirstReader = true; - const admittedReader = session.readRunFlight(first.id); - await readerEntered.promise; - for (let index = 0; index < 49; index += 1) await expect(session.invoke(request)).resolves.toMatchObject({ status: 'succeeded' }); - - const evicting = session.invoke(request); - await evictionReserved.promise; - await expect(session.readRunFlight(first.id)).resolves.toBeUndefined(); - expect(firstReaderAdmissions).toBe(1); - - releaseReader.resolve(); - await expect(admittedReader).resolves.toMatchObject({ body: expect.any(Buffer) }); - await expect(evicting).resolves.toMatchObject({ status: 'succeeded' }); - await expect(session.readRunFlight(first.id)).resolves.toBeUndefined(); - } finally { - releaseReader.resolve(); - await session.close().catch(() => undefined); - await rm(storageRoot, { force: true, recursive: true }); - } -}, 90_000); - -test('finalizes successful history before a failed evicted run-directory removal', async () => { - const storageRoot = await mkdtemp(join(tmpdir(), 'rsc-agent-runtime-session-eviction-directory-')); - const projectRoot = process.cwd(); - const prepared = await new ProjectService({ includeDevRuntime: true, mode: 'development', root: projectRoot }).prepare('dev'); - let firstRunId: string | undefined; - let failFirstDirectoryRemoval = true; - let firstArtifactReleaseAttempts = 0; - let firstDirectoryRemovalAttempts = 0; - const session = await RsbuildRuntimeSession.start({ - artifactStatus: () => Object.freeze({ state: 'missing' as const }), - emit: () => undefined, - environment: Object.freeze({}), - projectRoot, - preparedRuntime: prepared.devRuntime!, - providerSessionId: 'session-eviction-directory-test', - signal: new AbortController().signal, - storageRoot, - }, { - beforeRunArtifactRelease: ({ runId }: Readonly<{ readonly runId: string }>) => { - if (runId === firstRunId) firstArtifactReleaseAttempts += 1; - }, - beforeRunDirectoryRemoval: ({ runId }: Readonly<{ readonly runId: string }>) => { - if (runId === firstRunId) firstDirectoryRemovalAttempts += 1; - if (failFirstDirectoryRemoval && runId === firstRunId) { - failFirstDirectoryRemoval = false; - throw new Error('do-not-expose-evicted-run-directory-removal-secret'); - } - }, - }); - - try { - await waitFor(() => session.status().activeVector !== undefined, 'Timed out waiting for an active runtime generation', 15_000); - const generationId = session.status().activeVector!.runtimeGenerationId; - const target = session.surfaces().find((surface) => surface.id === 'mcp.runtime_status')!.targets[0]!; - const request = { - expectedGenerationId: generationId, - input: {}, - surfaceId: 'mcp.runtime_status', - target, - } as const; - const first = await session.invoke(request); - if (first.status !== 'succeeded') throw new Error(JSON.stringify(first.diagnostics)); - firstRunId = first.id; - - for (let index = 0; index < 49; index += 1) await expect(session.invoke(request)).resolves.toMatchObject({ status: 'succeeded' }); - const evictionFailure = await session.invoke(request); - - expect(evictionFailure).toMatchObject({ - diagnostics: [expect.objectContaining({ message: 'RSC runtime run artifact cleanup failed; cleanup failures: run-artifact.' })], - status: 'failed', - }); - expect(evictionFailure.status === 'failed' && evictionFailure.diagnostics[0]!.message) - .not.toContain('do-not-expose-evicted-run-directory-removal-secret'); - expect(session.run(first.id)).toBeUndefined(); - await expect(session.readRunFlight(first.id)).resolves.toBeUndefined(); - expect(await readdir(join(storageRoot, 'runs'))).toEqual(expect.arrayContaining([first.id])); - expect(firstArtifactReleaseAttempts).toBe(1); - - await expect(session.close()).resolves.toBeUndefined(); - expect(firstArtifactReleaseAttempts).toBe(1); - expect(firstDirectoryRemovalAttempts).toBe(2); - } finally { - await session.close().catch(() => undefined); - await rm(storageRoot, { force: true, recursive: true }); - } -}, 60_000); - test('rejects a fifth blocked generation worker and settles every leased worker on close', async () => { const storageRoot = await mkdtemp(join(tmpdir(), 'rsc-agent-runtime-session-bound-')); const projectRoot = process.cwd(); From 805fef499ccb8f202a5ea07680d466bc78771e6a Mon Sep 17 00:00:00 2001 From: ScriptedAlchemy Date: Sat, 29 Aug 2026 01:44:12 +0000 Subject: [PATCH 07/16] test(rsc): fold stale-chunk and self-contained HTML checks into the production build test The second multi-environment rebuild test repeated the full example build only to check that a planted stale async chunk disappears, and the self-contained widget HTML test duplicated assertions the host artifacts suite already makes per artifact. host-artifacts now plants the stale chunk before its existing production build and asserts its removal, and its HTML artifact loop keeps the inline script/style presence checks, so mcp-transports drops both duplicates. --- .../tests/host-artifacts.test.ts | 19 ++++++++-- .../tests/mcp-transports.integration.test.ts | 37 +------------------ 2 files changed, 17 insertions(+), 39 deletions(-) diff --git a/examples/rsc-agent-runtime/tests/host-artifacts.test.ts b/examples/rsc-agent-runtime/tests/host-artifacts.test.ts index 57e18af88..4cd4d057c 100644 --- a/examples/rsc-agent-runtime/tests/host-artifacts.test.ts +++ b/examples/rsc-agent-runtime/tests/host-artifacts.test.ts @@ -22,10 +22,19 @@ const runPackageHosts = async (): Promise => { const runProductionBuild = async (): Promise => { await rm(join(exampleRoot, 'dist/app'), { force: true, recursive: true }); - const child = spawn('npm', ['run', 'build'], { cwd: exampleRoot, stdio: 'pipe' }); - const [exitCode, signal] = (await once(child, 'close')) as [number | null, NodeJS.Signals | null]; - expect(signal).toBeNull(); - expect(exitCode).toBe(0); + // Plant a leftover async chunk; the multi-environment build itself must remove stale app assets. + const staleAsset = join(exampleRoot, 'dist/app/static/js/async/stale.js'); + await mkdir(dirname(staleAsset), { recursive: true }); + await writeFile(staleAsset, 'stale artifact', 'utf8'); + try { + const child = spawn('npm', ['run', 'build'], { cwd: exampleRoot, stdio: 'pipe' }); + const [exitCode, signal] = (await once(child, 'close')) as [number | null, NodeJS.Signals | null]; + expect(signal).toBeNull(); + expect(exitCode).toBe(0); + await expect(access(staleAsset)).rejects.toThrow(); + } finally { + await rm(staleAsset, { force: true }); + } }; const readJson = async (path: string): Promise => JSON.parse(await readFile(path, 'utf8')) as T; @@ -180,6 +189,8 @@ test('keeps fresh production App legal payload names stable and package-identica expect(await readFile(join(appRoot, target), 'utf8')).toBe(legalNoticeContent); } if (artifact.path.endsWith('.html')) { + expect(source).toContain(']+src=|]+rel=["']stylesheet["']/iu); } } diff --git a/examples/rsc-agent-runtime/tests/mcp-transports.integration.test.ts b/examples/rsc-agent-runtime/tests/mcp-transports.integration.test.ts index 7d417297d..3035845e3 100644 --- a/examples/rsc-agent-runtime/tests/mcp-transports.integration.test.ts +++ b/examples/rsc-agent-runtime/tests/mcp-transports.integration.test.ts @@ -1,8 +1,8 @@ import { spawn } from 'node:child_process'; -import { access, mkdir, mkdtemp, readFile, readdir, rm, writeFile } from 'node:fs/promises'; +import { access, mkdtemp, readFile, readdir, rm } from 'node:fs/promises'; import { request as httpRequest } from 'node:http'; import { tmpdir } from 'node:os'; -import { dirname, join } from 'node:path'; +import { join } from 'node:path'; import { once } from 'node:events'; import { pathToFileURL } from 'node:url'; @@ -314,18 +314,6 @@ test('adds an explicit public MCP URL domain only to returned resource content', } }); -test('built widget HTML is self-contained without external app bundle assets', async () => { - for (const name of ['edit-timeline-v1', 'standalone']) { - const artifact = join(process.cwd(), 'dist/app', `${name}.html`); - await access(artifact); - const html = await readFile(artifact, 'utf8'); - expect(html).toContain(' { const entries = ['hook/index.js', 'rsc/index.js', 'mcp/stdio.js', 'mcp/http.js']; const runtimeRoot = join(process.cwd(), 'dist/runtime'); @@ -403,24 +391,3 @@ test('production and development runtime graphs exclude state test controls', as await rm(compilerRoot, { force: true, recursive: true }); } }); - -test('a second multi-environment build removes stale app chunks', async () => { - const staleAsset = join(process.cwd(), 'dist/app/static/js/async/stale.js'); - await mkdir(dirname(staleAsset), { recursive: true }); - await writeFile(staleAsset, 'stale artifact', 'utf8'); - - try { - const child = spawn('npm', ['run', 'build'], { cwd: process.cwd(), stdio: 'ignore' }); - const [exitCode, signal] = (await once(child, 'close')) as [number | null, NodeJS.Signals | null]; - expect(exitCode).toBe(0); - expect(signal).toBeNull(); - await expect(access(staleAsset)).rejects.toThrow(); - for (const name of ['edit-timeline-v1', 'standalone']) { - const html = await readFile(join(process.cwd(), 'dist/app', `${name}.html`), 'utf8'); - expect(html).toContain(' Date: Sat, 29 Aug 2026 01:44:59 +0000 Subject: [PATCH 08/16] test(workbench): run the readFinalizedEvalRun fake-client tests on the unit pool The three finalization-polling tests exercise readFinalizedEvalRun with in-memory fake clients and never touch a browser or server, yet they lived in evals-real.e2e.test.ts and paid the serialized integration pool for it. They move verbatim to evals-finalized-run.test.ts, which the unit config picks up by default. --- .../tests/evals-finalized-run.test.ts | 52 +++++++++++++++++++ .../workbench/tests/evals-real.e2e.test.ts | 50 ------------------ 2 files changed, 52 insertions(+), 50 deletions(-) create mode 100644 packages/workbench/tests/evals-finalized-run.test.ts diff --git a/packages/workbench/tests/evals-finalized-run.test.ts b/packages/workbench/tests/evals-finalized-run.test.ts new file mode 100644 index 000000000..7abcc6c67 --- /dev/null +++ b/packages/workbench/tests/evals-finalized-run.test.ts @@ -0,0 +1,52 @@ +import { expect, test } from '@rstest/core'; + +import { readFinalizedEvalRun } from '../src/evals/evals-page.tsx'; + +test('retries a terminal canonical read until the durable run finalization is visible', async () => { + let reads = 0; + const waits: number[] = []; + const result = await readFinalizedEvalRun({ + client: { + read: async () => ++reads === 1 + ? { run: { completedAt: undefined } } as never + : { run: { completedAt: '2026-08-18T00:00:02.000Z' } } as never, + }, + runId: 'run-terminal-race', + signal: new AbortController().signal, + wait: async (milliseconds) => { waits.push(milliseconds); }, + }); + + expect(reads).toBe(2); + expect(waits).toHaveLength(1); + expect(result.run.completedAt).toBe('2026-08-18T00:00:02.000Z'); +}); + +test('surfaces a terminal canonical-read error without retrying it', async () => { + let reads = 0; + let waits = 0; + + await expect(readFinalizedEvalRun({ + client: { read: async () => { reads += 1; throw new Error('invalid durable DTO'); } }, + runId: 'run-terminal-error', + signal: new AbortController().signal, + wait: async () => { waits += 1; }, + })).rejects.toThrow('invalid durable DTO'); + + expect(reads).toBe(1); + expect(waits).toBe(0); +}); + +test('stops bounded terminal finalization polling instead of looping forever', async () => { + let reads = 0; + let waits = 0; + + await expect(readFinalizedEvalRun({ + client: { read: async () => { reads += 1; return { run: { completedAt: undefined } } as never; } }, + runId: 'run-terminal-timeout', + signal: new AbortController().signal, + wait: async () => { waits += 1; }, + })).rejects.toThrow('Recorded eval results were not finalized in time.'); + + expect(reads).toBe(8); + expect(waits).toBe(7); +}); diff --git a/packages/workbench/tests/evals-real.e2e.test.ts b/packages/workbench/tests/evals-real.e2e.test.ts index 1c8dfb7e0..e26107228 100644 --- a/packages/workbench/tests/evals-real.e2e.test.ts +++ b/packages/workbench/tests/evals-real.e2e.test.ts @@ -12,7 +12,6 @@ import { createWorkbenchAssetSource } from '../../agent-bundle/src/dev/workbench import { startDevServer } from '../../agent-bundle/src/dev/workbench-server.ts'; import { createProjectFixture, removeProjectFixture } from '../../agent-bundle/tests/helpers/project-fixture.ts'; import { seedEvalProject, writeEvalSuite } from '../../agent-bundle/tests/support/eval-project.ts'; -import { readFinalizedEvalRun } from '../src/evals/evals-page.tsx'; import { closeServer } from './support/http.ts'; import { workbenchBrowserAliases } from './support/workbench-browser-modules.ts'; import { buildWorkbench, e2e, workbenchAssets, workspaceRoot, workbenchUrl } from './support/workbench-e2e.ts'; @@ -21,55 +20,6 @@ const evalsPage = join(workspaceRoot, 'packages', 'workbench', 'src', 'evals', ' const browserTimeout = 12_000; const runCompletionTimeout = 60_000; -e2e('retries a terminal canonical read until the durable run finalization is visible', async () => { - let reads = 0; - const waits: number[] = []; - const result = await readFinalizedEvalRun({ - client: { - read: async () => ++reads === 1 - ? { run: { completedAt: undefined } } as never - : { run: { completedAt: '2026-08-18T00:00:02.000Z' } } as never, - }, - runId: 'run-terminal-race', - signal: new AbortController().signal, - wait: async (milliseconds) => { waits.push(milliseconds); }, - }); - - expect(reads).toBe(2); - expect(waits).toHaveLength(1); - expect(result.run.completedAt).toBe('2026-08-18T00:00:02.000Z'); -}); - -e2e('surfaces a terminal canonical-read error without retrying it', async () => { - let reads = 0; - let waits = 0; - - await expect(readFinalizedEvalRun({ - client: { read: async () => { reads += 1; throw new Error('invalid durable DTO'); } }, - runId: 'run-terminal-error', - signal: new AbortController().signal, - wait: async () => { waits += 1; }, - })).rejects.toThrow('invalid durable DTO'); - - expect(reads).toBe(1); - expect(waits).toBe(0); -}); - -e2e('stops bounded terminal finalization polling instead of looping forever', async () => { - let reads = 0; - let waits = 0; - - await expect(readFinalizedEvalRun({ - client: { read: async () => { reads += 1; return { run: { completedAt: undefined } } as never; } }, - runId: 'run-terminal-timeout', - signal: new AbortController().signal, - wait: async () => { waits += 1; }, - })).rejects.toThrow('Recorded eval results were not finalized in time.'); - - expect(reads).toBe(8); - expect(waits).toBe(7); -}); - const listen = async (server: Server): Promise => { server.listen(0, '127.0.0.1'); await once(server, 'listening'); From 295b83321fe61f99bbd4168c92fb4579ee8f4502 Mon Sep 17 00:00:00 2001 From: ScriptedAlchemy Date: Sat, 29 Aug 2026 01:49:52 +0000 Subject: [PATCH 09/16] test(agent-bundle): run pack-and-install suites through test:packed instead of the default integration pool Every npm pack + clean-install suite (dev-workbench-packaging, packed-consumer, packed-native-smoke, release-audit, rsc-runtime-optional-packaging) leaves the serialized integration pool and moves to a dedicated packedTestFiles list that the unit pool also excludes. The three pack+install cases in public-api.test.ts split into public-api-packed.test.ts so the cheap export and built-entrypoint checks stay in the default loop. test:packed now lists the split file plus rsc-runtime-optional-packaging and packed-native-smoke, keeping per-PR CI coverage through the release-gates job's check:release run. --- package.json | 2 +- .../tests/public-api-packed.test.ts | 248 ++++++++++++++++++ .../agent-bundle/tests/public-api.test.ts | 192 +------------- rstest.integration-tests.ts | 22 +- rstest.unit.config.ts | 4 +- 5 files changed, 269 insertions(+), 199 deletions(-) create mode 100644 packages/agent-bundle/tests/public-api-packed.test.ts diff --git a/package.json b/package.json index d3db2a7e5..c7f9d24f1 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,7 @@ "check:runtime-topology": "node scripts/rsc-runtime-topology.mjs --root . --output docs/architecture/rsc-runtime-workbench.md --check", "test:spot-check": "rstest --config rstest.config.ts packages/agent-bundle/tests/micro-eval-spot-check.test.ts", "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/workbench/tests/packed-release.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/workbench/tests/packed-release.e2e.test.ts", "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/public-api-packed.test.ts b/packages/agent-bundle/tests/public-api-packed.test.ts new file mode 100644 index 000000000..ed46116b8 --- /dev/null +++ b/packages/agent-bundle/tests/public-api-packed.test.ts @@ -0,0 +1,248 @@ +import { execFile as executeFile } from 'node:child_process'; +import { mkdtemp, mkdir, readFile, rm, stat, symlink, writeFile } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import { promisify } from 'node:util'; + +import { expect, it } from '@rstest/core'; + +import { writeFixtureManifest } from './support/manifest.ts'; + +interface PackageManifest { + bin: { + 'agent-bundle': string; + }; + version: string; +} + +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( + await readFile(join(packageRoot, 'package.json'), 'utf8'), + ) as PackageManifest; + +const createBuildProject = async (root: string): Promise<{ readonly output: string; readonly project: string }> => { + const project = join(root, 'manifest-version-project'); + const output = join(project, 'manifest-version-artifact'); + await mkdir(join(project, 'skills', 'review'), { recursive: true }); + await Promise.all([ + writeFile(join(project, 'package.json'), '{"type":"module"}\n'), + writeFile( + join(project, 'agent-bundle.config.ts'), + "export default { plugin: { name: 'manifest-version-fixture', version: '1.0.0' }, targets: ['portable'] };\n", + ), + writeFile( + join(project, 'skills', 'review', 'SKILL.md'), + '---\nname: review\ndescription: Reviews changes\n---\n# Review\n', + ), + ]); + return { output, project }; +}; + +const producerFrom = async (output: string): Promise<{ readonly name: string; readonly version: string }> => { + const manifest = JSON.parse( + await readFile(join(output, 'agent-bundle.manifest.json'), 'utf8'), + ) as { readonly producer: { readonly name: string; readonly version: string } }; + return manifest.producer; +}; + +it('writes the package version as the producer of a packed CLI manifest', async () => { + await buildPackage(); + + 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)], + { cwd: consumerRoot }, + ); + + const project = await createBuildProject(consumerRoot); + const packedCli = join( + consumerRoot, + 'node_modules', + 'agent-bundle', + manifest.bin['agent-bundle'], + ); + await execFile(process.execPath, [packedCli, 'build', '--root', project.project, '--output', project.output], { + cwd: consumerRoot, + }); + + await expect(producerFrom(project.output)).resolves.toEqual({ + name: 'agent-bundle', + version: manifest.version, + }); + } finally { + await rm(consumerRoot, { force: true, recursive: true }); + } +}, 30_000); + +it('imports the externalized config entry from a packed npm consumer', async () => { + await buildPackage(); + + 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', + ['install', '--ignore-scripts', '--no-audit', '--no-fund', tarball], + { cwd: consumerRoot }, + ); + + expect((await stat(join(packageRoot, 'dist/config.js'))).size).toBeLessThan( + 100_000, + ); + await expect( + execFile(process.execPath, [ + '--input-type=module', + '--eval', + [ + "import { defineConfig as rootDefineConfig } from 'agent-bundle';", + "import { defineConfig } from 'agent-bundle/config';", + 'if (defineConfig !== rootDefineConfig) throw new Error(\'config factory identity mismatch\');', + ].join('\n'), + ], { cwd: consumerRoot }), + ).resolves.toMatchObject({ stderr: '', stdout: '' }); + await symlink( + join(workspaceRoot, 'node_modules', '@types'), + join(consumerRoot, 'node_modules', '@types'), + 'dir', + ); + await writeFile(join(consumerRoot, 'config.mts'), [ + "import { defineConfig, type AgentBundleConfig } from 'agent-bundle/config';", + '', + 'const config: AgentBundleConfig = {', + " claude: { nativeHooks: './claude-hooks.json' },", + " codex: { nativeHooks: './codex-hooks.json' },", + " plugin: { name: 'packed-config-types', version: '1.0.0' },", + " portable: { compatibility: 'v1' },", + '};', + '', + 'const claudeHook: string | undefined = config.claude?.nativeHooks;', + 'const codexHook: string | undefined = config.codex?.nativeHooks;', + 'const portableConfig: { readonly [key: string]: unknown } | undefined = config.portable;', + 'void defineConfig(config);', + 'void [claudeHook, codexHook, portableConfig];', + '', + ].join('\n')); + await expect(execFile(join(workspaceRoot, 'node_modules', '.bin', 'tsc'), [ + '--module', 'nodenext', + '--moduleResolution', 'nodenext', + '--noEmit', + '--strict', + '--target', 'es2022', + '--types', 'node', + 'config.mts', + ], { cwd: consumerRoot })).resolves.toMatchObject({ stderr: '', stdout: '' }); + } finally { + await rm(consumerRoot, { force: true, recursive: true }); + } +}, 15_000); + +it('invokes a prebuilt MCP server from a clean packed consumer', async () => { + await buildPackage(); + + const consumerRoot = await mkdtemp(join(tmpdir(), 'agent-bundle-mcp-consumer-')); + try { + const artifact = join(consumerRoot, 'artifact'); + await mkdir(join(artifact, 'portable', 'mcp'), { recursive: true }); + await writeFile( + join(artifact, 'portable', 'mcp', 'server.mjs'), + [ + "let buffer = '';", + 'const send = (id, result) => process.stdout.write(`${JSON.stringify({ jsonrpc: \'2.0\', id, result })}\\n`);', + "process.stdin.setEncoding('utf8');", + "process.stdin.on('data', (chunk) => {", + ' buffer += chunk;', + " for (let newline; (newline = buffer.indexOf('\\n')) >= 0;) {", + ' const line = buffer.slice(0, newline).trim();', + ' buffer = buffer.slice(newline + 1);', + ' if (!line) continue;', + ' const request = JSON.parse(line);', + " if (request.method === 'initialize') send(request.id, { capabilities: { tools: {} }, protocolVersion: request.params.protocolVersion, serverInfo: { name: 'packed-fixture', version: '1.0.0' } });", + " if (request.method === 'tools/list') send(request.id, { tools: [{ description: 'Packed fixture', inputSchema: { properties: {}, type: 'object' }, name: 'inspect' }] });", + " if (request.method === 'tools/call') send(request.id, { content: [{ text: 'packed result', type: 'text' }], structuredContent: { packed: true } });", + ' }', + '});', + '', + ].join('\n'), + ); + await writeFile( + join(artifact, 'portable', 'plugin.json'), + '{"$schema":"https://agent-plugins.org/schemas/1.0.0/plugin.schema.json","name":"packed-fixture","version":"1.0.0"}\n', + ); + await writeFile( + join(artifact, 'portable', 'mcp.json'), + `${JSON.stringify({ + $schema: 'https://agent-plugins.org/schemas/1.0.0/mcp.schema.json', + mcpServers: { + fixture: { + args: ['mcp/server.mjs'], + command: process.execPath, + cwd: '${PLUGIN_ROOT}', + type: 'stdio', + }, + }, + })}\n`, + ); + await writeFixtureManifest({ artifactRoot: artifact, targets: ['portable'] }); + await expect(readFile(join(artifact, 'agent-bundle.hooks.json'), 'utf8')).resolves.toBe( + '{"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)], + { cwd: consumerRoot }, + ); + const { stdout } = await execFile(process.execPath, [ + '--input-type=module', + '--eval', + [ + "import { McpService } from 'agent-bundle/api';", + "const result = await new McpService().invoke({ artifact: './artifact', input: {}, server: 'fixture', target: 'portable', tool: 'inspect' });", + 'console.log(JSON.stringify(result));', + ].join('\n'), + ], { cwd: consumerRoot }); + expect(JSON.parse(stdout)).toMatchObject({ + result: { + content: [{ text: 'packed result', type: 'text' }], + structuredContent: { packed: true }, + }, + server: { name: 'packed-fixture', version: '1.0.0' }, + }); + } finally { + await rm(consumerRoot, { force: true, recursive: true }); + } +}, 30_000); diff --git a/packages/agent-bundle/tests/public-api.test.ts b/packages/agent-bundle/tests/public-api.test.ts index f41a49f36..8867b3026 100644 --- a/packages/agent-bundle/tests/public-api.test.ts +++ b/packages/agent-bundle/tests/public-api.test.ts @@ -1,5 +1,5 @@ import { execFile as executeFile } from 'node:child_process'; -import { access, mkdtemp, mkdir, readFile, readdir, rm, stat, symlink, writeFile } from 'node:fs/promises'; +import { access, mkdtemp, mkdir, readFile, readdir, rm, symlink, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { promisify } from 'node:util'; @@ -38,7 +38,6 @@ import type { DevRuntimeProvider, } from '../src/api.ts'; import { agentBundleNodeModules, workspaceNodeModules } from './helpers/workspace-paths.ts'; -import { writeFixtureManifest } from './support/manifest.ts'; interface PackageManifest { bin: { @@ -278,112 +277,6 @@ it('writes the package version as the producer of a built CLI manifest', async ( } }); -it('writes the package version as the producer of a packed CLI manifest', async () => { - await buildPackage(); - - 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)], - { cwd: consumerRoot }, - ); - - const project = await createBuildProject(consumerRoot); - const packedCli = join( - consumerRoot, - 'node_modules', - 'agent-bundle', - manifest.bin['agent-bundle'], - ); - await execFile(process.execPath, [packedCli, 'build', '--root', project.project, '--output', project.output], { - cwd: consumerRoot, - }); - - await expect(producerFrom(project.output)).resolves.toEqual({ - name: 'agent-bundle', - version: manifest.version, - }); - } finally { - await rm(consumerRoot, { force: true, recursive: true }); - } -}, 30_000); - -it('imports the externalized config entry from a packed npm consumer', async () => { - await buildPackage(); - - 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', - ['install', '--ignore-scripts', '--no-audit', '--no-fund', tarball], - { cwd: consumerRoot }, - ); - - expect((await stat(join(packageRoot, 'dist/config.js'))).size).toBeLessThan( - 100_000, - ); - await expect( - execFile(process.execPath, [ - '--input-type=module', - '--eval', - [ - "import { defineConfig as rootDefineConfig } from 'agent-bundle';", - "import { defineConfig } from 'agent-bundle/config';", - 'if (defineConfig !== rootDefineConfig) throw new Error(\'config factory identity mismatch\');', - ].join('\n'), - ], { cwd: consumerRoot }), - ).resolves.toMatchObject({ stderr: '', stdout: '' }); - await symlink( - join(workspaceRoot, 'node_modules', '@types'), - join(consumerRoot, 'node_modules', '@types'), - 'dir', - ); - await writeFile(join(consumerRoot, 'config.mts'), [ - "import { defineConfig, type AgentBundleConfig } from 'agent-bundle/config';", - '', - 'const config: AgentBundleConfig = {', - " claude: { nativeHooks: './claude-hooks.json' },", - " codex: { nativeHooks: './codex-hooks.json' },", - " plugin: { name: 'packed-config-types', version: '1.0.0' },", - " portable: { compatibility: 'v1' },", - '};', - '', - 'const claudeHook: string | undefined = config.claude?.nativeHooks;', - 'const codexHook: string | undefined = config.codex?.nativeHooks;', - 'const portableConfig: { readonly [key: string]: unknown } | undefined = config.portable;', - 'void defineConfig(config);', - 'void [claudeHook, codexHook, portableConfig];', - '', - ].join('\n')); - await expect(execFile(join(workspaceRoot, 'node_modules', '.bin', 'tsc'), [ - '--module', 'nodenext', - '--moduleResolution', 'nodenext', - '--noEmit', - '--strict', - '--target', 'es2022', - '--types', 'node', - 'config.mts', - ], { cwd: consumerRoot })).resolves.toMatchObject({ stderr: '', stdout: '' }); - } finally { - await rm(consumerRoot, { force: true, recursive: true }); - } -}, 15_000); - it('keeps bundled config extension types in emitted root declarations', async () => { const consumerRoot = await mkdtemp(join(tmpdir(), 'agent-bundle-root-types-')); try { @@ -455,86 +348,3 @@ it('keeps bundled config extension types in emitted root declarations', async () } }, 30_000); -it('invokes a prebuilt MCP server from a clean packed consumer', async () => { - await buildPackage(); - - const consumerRoot = await mkdtemp(join(tmpdir(), 'agent-bundle-mcp-consumer-')); - try { - const artifact = join(consumerRoot, 'artifact'); - await mkdir(join(artifact, 'portable', 'mcp'), { recursive: true }); - await writeFile( - join(artifact, 'portable', 'mcp', 'server.mjs'), - [ - "let buffer = '';", - 'const send = (id, result) => process.stdout.write(`${JSON.stringify({ jsonrpc: \'2.0\', id, result })}\\n`);', - "process.stdin.setEncoding('utf8');", - "process.stdin.on('data', (chunk) => {", - ' buffer += chunk;', - " for (let newline; (newline = buffer.indexOf('\\n')) >= 0;) {", - ' const line = buffer.slice(0, newline).trim();', - ' buffer = buffer.slice(newline + 1);', - ' if (!line) continue;', - ' const request = JSON.parse(line);', - " if (request.method === 'initialize') send(request.id, { capabilities: { tools: {} }, protocolVersion: request.params.protocolVersion, serverInfo: { name: 'packed-fixture', version: '1.0.0' } });", - " if (request.method === 'tools/list') send(request.id, { tools: [{ description: 'Packed fixture', inputSchema: { properties: {}, type: 'object' }, name: 'inspect' }] });", - " if (request.method === 'tools/call') send(request.id, { content: [{ text: 'packed result', type: 'text' }], structuredContent: { packed: true } });", - ' }', - '});', - '', - ].join('\n'), - ); - await writeFile( - join(artifact, 'portable', 'plugin.json'), - '{"$schema":"https://agent-plugins.org/schemas/1.0.0/plugin.schema.json","name":"packed-fixture","version":"1.0.0"}\n', - ); - await writeFile( - join(artifact, 'portable', 'mcp.json'), - `${JSON.stringify({ - $schema: 'https://agent-plugins.org/schemas/1.0.0/mcp.schema.json', - mcpServers: { - fixture: { - args: ['mcp/server.mjs'], - command: process.execPath, - cwd: '${PLUGIN_ROOT}', - type: 'stdio', - }, - }, - })}\n`, - ); - await writeFixtureManifest({ artifactRoot: artifact, targets: ['portable'] }); - await expect(readFile(join(artifact, 'agent-bundle.hooks.json'), 'utf8')).resolves.toBe( - '{"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)], - { cwd: consumerRoot }, - ); - const { stdout } = await execFile(process.execPath, [ - '--input-type=module', - '--eval', - [ - "import { McpService } from 'agent-bundle/api';", - "const result = await new McpService().invoke({ artifact: './artifact', input: {}, server: 'fixture', target: 'portable', tool: 'inspect' });", - 'console.log(JSON.stringify(result));', - ].join('\n'), - ], { cwd: consumerRoot }); - expect(JSON.parse(stdout)).toMatchObject({ - result: { - content: [{ text: 'packed result', type: 'text' }], - structuredContent: { packed: true }, - }, - server: { name: 'packed-fixture', version: '1.0.0' }, - }); - } finally { - await rm(consumerRoot, { force: true, recursive: true }); - } -}, 30_000); diff --git a/rstest.integration-tests.ts b/rstest.integration-tests.ts index be1049615..3c6d46d23 100644 --- a/rstest.integration-tests.ts +++ b/rstest.integration-tests.ts @@ -17,7 +17,6 @@ export const integrationTestFiles: readonly string[] = [ 'packages/agent-bundle/tests/build.test.ts', 'packages/agent-bundle/tests/cli.test.ts', 'packages/agent-bundle/tests/dev-artifact-service.test.ts', - 'packages/agent-bundle/tests/dev-workbench-packaging.test.ts', 'packages/agent-bundle/tests/dev-workbench.test.ts', 'packages/agent-bundle/tests/epoch-atomicity-spike.test.ts', 'packages/agent-bundle/tests/eval-claude-harness.test.ts', @@ -35,13 +34,9 @@ export const integrationTestFiles: readonly string[] = [ 'packages/agent-bundle/tests/mcp-session-service.test.ts', 'packages/agent-bundle/tests/mcp.test.ts', 'packages/agent-bundle/tests/micro-eval-spot-check.test.ts', - 'packages/agent-bundle/tests/packed-consumer.test.ts', - 'packages/agent-bundle/tests/packed-native-smoke.test.ts', 'packages/agent-bundle/tests/path-token-resolver.test.ts', 'packages/agent-bundle/tests/plugin-bundle.test.ts', 'packages/agent-bundle/tests/public-api.test.ts', - 'packages/agent-bundle/tests/release-audit.test.ts', - 'packages/agent-bundle/tests/rsc-runtime-optional-packaging.test.ts', 'packages/agent-bundle/tests/rsc-runtime-topology-script.test.ts', 'packages/agent-bundle/tests/script-playground-service.test.ts', 'packages/agent-bundle/tests/target-hook-contract.test.ts', @@ -72,3 +67,20 @@ export const integrationTestFiles: readonly string[] = [ 'packages/workbench/tests/sync-inspector.test.ts', 'packages/workbench/tests/workbench-dev-command.test.ts', ]; + +/** + * 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 + * native-host-smoke workflow keep them covered — and stay excluded from the + * parallel unit pool. + */ +export const packedTestFiles: readonly string[] = [ + 'packages/agent-bundle/tests/dev-workbench-packaging.test.ts', + 'packages/agent-bundle/tests/packed-consumer.test.ts', + 'packages/agent-bundle/tests/packed-native-smoke.test.ts', + 'packages/agent-bundle/tests/public-api-packed.test.ts', + 'packages/agent-bundle/tests/release-audit.test.ts', + 'packages/agent-bundle/tests/rsc-runtime-optional-packaging.test.ts', +]; diff --git a/rstest.unit.config.ts b/rstest.unit.config.ts index 6c2a0a446..7ffc0b3ae 100644 --- a/rstest.unit.config.ts +++ b/rstest.unit.config.ts @@ -1,6 +1,6 @@ import { defineConfig } from '@rstest/core'; -import { integrationTestFiles } from './rstest.integration-tests.ts'; +import { integrationTestFiles, packedTestFiles } from './rstest.integration-tests.ts'; import { withAgentBundleRslibConfig } from './rstest.rslib.ts'; /** Build-free, process-free tests only; safe on parallel workers. `pnpm test` runs this before the integration config. */ @@ -10,5 +10,5 @@ export default defineConfig({ 'packages/**/tests/**/*.test.ts', 'packages/workbench/src/inspector/vendor/clients/web/src/utils/inspectorTabs.test.ts', ], - exclude: [...integrationTestFiles], + exclude: [...integrationTestFiles, ...packedTestFiles], }); From 7efd4bda9eebd4c4e70dc96f6c1b2fd631050b99 Mon Sep 17 00:00:00 2001 From: ScriptedAlchemy Date: Sat, 29 Aug 2026 01:57:16 +0000 Subject: [PATCH 10/16] test(agent-bundle): delete vacuous export, doc-string, and duplicate-service tests Removes tests that only re-assert their own fixtures, string-match documentation, or duplicate a sibling suite: the public-api config/type re-export tests (the built-entrypoint test still pins subpath imports and defineConfig identity), the packed-consumer regex self-test, the manifest re-export identity test, the examples README string test, the topology capture-command README pin, the audiobook tool-catalog and CLI-receipt duplicates, the eval-cli duplicate of the eval-service run path, the release-audit pack dry run (check:release runs pack:dry-run directly), and one redundant JSON.stringify assertion in the native smoke. Type-only contracts previously wrapped in vacuous runtime tests (modern MCP transports, runtime provider binding/surface shapes) stay as module-level @ts-expect-error checks. Deletes the orphaned epoch-atomicity spike (production coverage lives in epoch-store and dev-lock tests) and the micro-eval spot-check suite together with its test:spot-check script and CI job; the examples:check step from that job survives as its own examples-check job. --- .github/workflows/ci.yml | 8 +- README.md | 5 - examples/audiobook-curator/tests/cli.test.ts | 7 - .../tests/mcp-tools.test.tsx | 22 --- package.json | 1 - packages/agent-bundle/README.md | 5 +- packages/agent-bundle/tests/core.test.ts | 13 +- .../tests/epoch-atomicity-spike.test.ts | 177 ------------------ packages/agent-bundle/tests/eval-cli.test.ts | 17 -- .../tests/examples-contract.test.ts | 17 -- packages/agent-bundle/tests/manifest.test.ts | 28 +-- .../tests/micro-eval-spot-check.test.ts | 95 ---------- .../tests/packed-consumer.test.ts | 5 - .../tests/packed-native-smoke.test.ts | 1 - .../agent-bundle/tests/public-api.test.ts | 93 --------- .../agent-bundle/tests/release-audit.test.ts | 10 - .../tests/rsc-runtime-topology-script.test.ts | 13 -- .../tests/runtime-provider.test.ts | 14 +- rstest.integration-tests.ts | 2 - 19 files changed, 14 insertions(+), 519 deletions(-) delete mode 100644 packages/agent-bundle/tests/epoch-atomicity-spike.test.ts delete mode 100644 packages/agent-bundle/tests/micro-eval-spot-check.test.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index cf9e66a6e..474b0cafe 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -14,10 +14,9 @@ concurrency: cancel-in-progress: true jobs: - # Fast end-to-end confidence gate: builds, validates, and deterministically evals the - # checked-in micro fixture through the real CLI. Never skip-gated and needs no native host. - spot-check: - name: Micro-eval spot-check (Node 22.19) + # Builds and checks every public example through its own toolchain. + examples-check: + name: Examples check (Node 22.19) runs-on: ubuntu-latest timeout-minutes: 25 steps: @@ -29,7 +28,6 @@ jobs: runtime: node@22.19.0 - run: pnpm install --frozen-lockfile - run: pnpm examples:check - - run: pnpm test:spot-check verify: name: Verify (Node ${{ matrix.node-version }}) diff --git a/README.md b/README.md index 06bfd9868..386b23be3 100644 --- a/README.md +++ b/README.md @@ -308,11 +308,6 @@ pnpm check && pnpm check:release `pnpm check:release` is the release-only gate: it runs `pnpm pack:dry-run`, `pnpm audit:release`, and `pnpm test:packed`; it does not replace `pnpm check`. -The micro-eval spot-check is the end-to-end CI confidence gate: `pnpm test:spot-check` builds, -validates, and runs one deterministic eval against the checked-in `fixtures/integration/micro-eval` -project through the real CLI. It also runs inside every default `pnpm test`, is never skip-gated, -and needs no Claude or Codex installation. - Native Claude/Codex host smokes are intentionally opt-in and stay skipped in CI. They run only on a machine with that CLI installed and signed in — via `AGENT_BUNDLE_NATIVE_CLAUDE_SMOKE=1` / `AGENT_BUNDLE_NATIVE_CODEX_SMOKE=1`, the packed variants `pnpm test:packed:native:claude` / diff --git a/examples/audiobook-curator/tests/cli.test.ts b/examples/audiobook-curator/tests/cli.test.ts index 030283e07..2db09e8c5 100644 --- a/examples/audiobook-curator/tests/cli.test.ts +++ b/examples/audiobook-curator/tests/cli.test.ts @@ -20,13 +20,6 @@ const operations = (): CuratorOperations => ({ }); describe('audiobook-curator CLI', () => { - it('emits one JSON receipt for each exact subcommand', async () => { - const output: string[] = []; - await expect(runCli(['inspect', '/library'], { operations: operations(), write: (value) => output.push(value) })) - .resolves.toBe(0); - expect(JSON.parse(output[0]!)).toEqual({ files: [], operation: 'inspect', root: '/library', totalBytes: 0 }); - }); - it('enables application only through the typed flag', async () => { let applied = false; const fixture = operations(); diff --git a/examples/audiobook-curator/tests/mcp-tools.test.tsx b/examples/audiobook-curator/tests/mcp-tools.test.tsx index e9df5d3fd..f8bd9b3ff 100644 --- a/examples/audiobook-curator/tests/mcp-tools.test.tsx +++ b/examples/audiobook-curator/tests/mcp-tools.test.tsx @@ -2,7 +2,6 @@ import { describe, expect, it } from '@rstest/core'; import { createCuratorTools, - curatorToolNames, type CuratorToolOperations, } from '../src/mcp-tools.js'; @@ -34,27 +33,6 @@ const operations = (): CuratorToolOperations => ({ }); describe('audiobook curator MCP tools', () => { - it('derives the current tool catalog from the shared application', () => { - expect(curatorToolNames).toEqual([ - 'verify_audible_sample', - 'identify_audible_sample', - 'verify_with_whisper', - 'apply_audiobook_metadata', - 'apply_audiobook_chapters', - 'search_audible', - 'select_audible_edition', - 'cache_audible_edition', - 'inspect_sources', - 'inventory_sources', - 'audit_library', - 'select_sources', - 'convert_audiobook', - 'prepare_audiobook', - 'audit_audiobook', - ]); - expect(createCuratorTools({ operations: operations() }).map(({ name }) => name)).toEqual(curatorToolNames); - }); - it('renders text and detached structured receipts through the public RSC lowerer', async () => { const tools = createCuratorTools({ operations: operations() }); const inspect = tools.find(({ name }) => name === 'inspect_sources')!; diff --git a/package.json b/package.json index c7f9d24f1..7b3db36df 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,6 @@ "docs:runtime-topology": "node scripts/rsc-runtime-topology.mjs --root . --output docs/architecture/rsc-runtime-workbench.md", "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:spot-check": "rstest --config rstest.config.ts packages/agent-bundle/tests/micro-eval-spot-check.test.ts", "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/workbench/tests/packed-release.e2e.test.ts", "test:packed:native": "rstest --config rstest.config.ts packages/agent-bundle/tests/packed-native-smoke.test.ts", diff --git a/packages/agent-bundle/README.md b/packages/agent-bundle/README.md index d18510649..82db4aecc 100644 --- a/packages/agent-bundle/README.md +++ b/packages/agent-bundle/README.md @@ -235,9 +235,6 @@ Run the complete local delivery gate with `pnpm check && pnpm check:release`. `pnpm check:release` is release-only: its exact package-script components are `pnpm pack:dry-run`, `pnpm audit:release`, and `pnpm test:packed`, and it does not replace `pnpm check`. -`pnpm test:spot-check` is the fast end-to-end confidence gate: it builds, validates, and runs -one deterministic eval against the checked-in micro fixture through the real CLI, with no native -host and no opt-in environment gate. Native Claude/Codex smokes stay intentionally opt-in and -skipped in ordinary CI. +Native Claude/Codex smokes stay intentionally opt-in and skipped in ordinary CI. Publication is deliberately not scripted here: the release owner must decide the npm package name/scope, license, and `publishConfig` before publishing. diff --git a/packages/agent-bundle/tests/core.test.ts b/packages/agent-bundle/tests/core.test.ts index b8c57c265..808db9376 100644 --- a/packages/agent-bundle/tests/core.test.ts +++ b/packages/agent-bundle/tests/core.test.ts @@ -9,14 +9,11 @@ import { digest, stableJson } from '../src/core/digest.ts'; import { assertInside } from '../src/core/paths.ts'; import type { McpTransport } from '../src/index.ts'; -it('exposes only modern MCP transports', () => { - const transport: McpTransport = 'streamable-http'; - // @ts-expect-error Legacy HTTP+SSE is not part of the public MCP transport contract. - const legacyTransport: McpTransport = 'sse'; - - expect(transport).toBe('streamable-http'); - expect(legacyTransport).toBe('sse'); -}); +// Type-level contract: only modern MCP transports are public. +const modernTransport: McpTransport = 'streamable-http'; +// @ts-expect-error Legacy HTTP+SSE is not part of the public MCP transport contract. +const legacyTransport: McpTransport = 'sse'; +void [modernTransport, legacyTransport]; it('serializes plain-object keys deterministically without changing JSON values', () => { const value = { diff --git a/packages/agent-bundle/tests/epoch-atomicity-spike.test.ts b/packages/agent-bundle/tests/epoch-atomicity-spike.test.ts deleted file mode 100644 index 0a9cacf0a..000000000 --- a/packages/agent-bundle/tests/epoch-atomicity-spike.test.ts +++ /dev/null @@ -1,177 +0,0 @@ -import { mkdtemp, open, readFile, rm, writeFile } from 'node:fs/promises'; -import { spawn } from 'node:child_process'; -import { tmpdir } from 'node:os'; -import { join } from 'node:path'; - -import { expect, it } from '@rstest/core'; - -interface EpochEvidence { - readonly environment: Readonly>; - readonly operations: readonly Readonly>[]; - readonly spike: Readonly>; -} - -interface LockRecord { - readonly owner: string; - readonly pid: number; -} - -const fixture = JSON.parse( - await readFile(new URL('../fixtures/contracts/epoch-atomicity/local-linux.json', import.meta.url), 'utf8'), -) as EpochEvidence; - -const errorCode = (error: unknown): string | undefined => - error !== null && typeof error === 'object' && 'code' in error && typeof error.code === 'string' - ? error.code - : undefined; - -const readLock = async (path: string): Promise => - JSON.parse(await readFile(path, 'utf8')) as LockRecord; - -const acquireLock = async ( - path: string, - owner: string, - pid: number, -): Promise>> => { - try { - const handle = await open(path, 'wx'); - await handle.writeFile(JSON.stringify({ owner, pid })); - await handle.close(); - return { status: 'acquired' }; - } catch (error) { - if (errorCode(error) !== 'EEXIST') throw error; - const current = await readLock(path); - return { observedOwner: current.owner, reason: 'lock-exists', status: 'rejected' }; - } -}; - -const probePid = (pid: number): Readonly> => { - try { - process.kill(pid, 0); - return { status: 'running' }; - } catch (error) { - return { status: 'not-running', systemCode: errorCode(error) ?? 'unknown' }; - } -}; - -const waitForSpawn = async (child: ReturnType): Promise => - new Promise((resolvePromise, reject) => { - child.once('error', reject); - child.once('spawn', resolvePromise); - }); - -const stopChild = async (child: ReturnType): Promise => { - if (child.exitCode !== null) return; - const exited = new Promise((resolvePromise) => child.once('exit', () => resolvePromise())); - child.kill(); - await exited; -}; - -const runDisposableEpochSpike = async (root: string): Promise => { - const active = join(root, 'active.json'); - const lock = join(root, 'publish.lock'); - const staged = join(root, 'epoch-2.staged.json'); - const stalePid = 2_147_483_647; - const liveWriter = spawn(process.execPath, ['--eval', 'setInterval(() => undefined, 1_000);'], { stdio: 'ignore' }); - await waitForSpawn(liveWriter); - if (liveWriter.pid === undefined) throw new Error('Live lock writer did not expose a PID.'); - - try { - await writeFile(active, JSON.stringify({ epochId: 'epoch-1' })); - await writeFile(staged, JSON.stringify({ epochId: 'epoch-2' })); - let failure: string | undefined; - try { - throw new Error('simulated publication failure before atomic rename'); - } catch (error) { - failure = error instanceof Error ? error.message : String(error); - } - const retained = JSON.parse(await readFile(active, 'utf8')) as { readonly epochId: string }; - - const firstWriter = await acquireLock(lock, 'writer-a', liveWriter.pid); - const liveOwner = probePid(liveWriter.pid); - const secondWriter = await acquireLock(lock, 'writer-b', process.pid + 1); - - await rm(lock); - await writeFile(lock, JSON.stringify({ owner: 'dead-writer', pid: stalePid })); - const staleOwner = probePid(stalePid); - if (staleOwner.status === 'not-running') await rm(lock); - const recoveredWriter = await acquireLock(lock, 'writer-c', process.pid); - - return { - environment: { - architecture: process.arch, - nodeVersion: process.version, - platform: process.platform, - runtime: 'node', - }, - operations: [ - { - id: 'failed-publication-retention', - mechanism: 'stage-write then atomic rename', - observed: { - activeEpochIdAfterFailure: retained.epochId, - candidateEpochId: 'epoch-2', - retainedPriorActive: retained.epochId === 'epoch-1', - }, - steps: [ - { action: 'seed-active', result: { epochId: 'epoch-1' } }, - { action: 'stage-candidate', result: { epochId: 'epoch-2' } }, - { action: 'inject-failure-before-rename', result: { error: failure } }, - { action: 'read-active-after-failure', result: { epochId: retained.epochId } }, - ], - }, - { - id: 'live-lock-second-writer-rejection', - observed: { - firstWriter, - liveOwner, - secondWriter, - }, - steps: [ - { action: 'acquire-exclusive-lock', actor: 'writer-a', result: firstWriter }, - { action: 'probe-live-owner', actor: 'writer-a', result: liveOwner }, - { action: 'acquire-exclusive-lock', actor: 'writer-b', result: secondWriter }, - ], - }, - { - id: 'dead-pid-lock-recovery', - observed: { - recoveredWriter, - staleOwner, - }, - steps: [ - { action: 'seed-stale-lock', actor: 'dead-writer', result: { stalePid } }, - { action: 'probe-owner-pid', actor: 'dead-writer', result: staleOwner }, - { action: 'remove-stale-lock-after-esrch', result: { removed: staleOwner.status === 'not-running' } }, - { action: 'acquire-exclusive-lock', actor: 'writer-c', result: recoveredWriter }, - ], - }, - ], - spike: { - name: 'atomic-epoch-publication-and-lock-ownership', - scope: 'disposable local filesystem probe; evidence only', - }, - }; - } finally { - await stopChild(liveWriter); - } -}; - -it('generates and validates local epoch publication and lock-ownership evidence', async () => { - const root = await mkdtemp(join(tmpdir(), 'agent-bundle-epoch-spike-')); - try { - // The recorded environment stamp names the machine that generated the fixture; the - // durable contract is the operations evidence, so compare against the live runtime. - expect(await runDisposableEpochSpike(root)).toEqual({ - ...fixture, - environment: { - architecture: process.arch, - nodeVersion: process.version, - platform: process.platform, - runtime: 'node', - }, - }); - } finally { - await rm(root, { force: true, recursive: true }); - } -}); diff --git a/packages/agent-bundle/tests/eval-cli.test.ts b/packages/agent-bundle/tests/eval-cli.test.ts index eba38469a..e53079d64 100644 --- a/packages/agent-bundle/tests/eval-cli.test.ts +++ b/packages/agent-bundle/tests/eval-cli.test.ts @@ -93,23 +93,6 @@ const persistComparisonRun = async ( } }; -it('runs a selected case through the same service the workbench uses', async () => { - const project = await createProjectFixture(); - try { - await seedEvalProject(project.root); - - const result = await runEvals({ caseIds: ['reads-result'], root: project.root, trials: 2 }); - - expect(result.trials).toHaveLength(2); - expect(result.run.harness).toBe('deterministic'); - expect(result.run.summary).toMatchObject({ pass: 2, trials: 2 }); - expect(result.diagnostics).toEqual([]); - await expect(access(join(project.root, '.agent-bundle', 'runs', result.run.id, 'run.json'))).resolves.toBeUndefined(); - } finally { - await removeProjectFixture(project.root); - } -}, 120_000); - it('evaluates exactly the artifact the caller named instead of building a new one', async () => { const project = await createProjectFixture(); try { diff --git a/packages/agent-bundle/tests/examples-contract.test.ts b/packages/agent-bundle/tests/examples-contract.test.ts index 5cbb3d4cc..0e0b9a2c7 100644 --- a/packages/agent-bundle/tests/examples-contract.test.ts +++ b/packages/agent-bundle/tests/examples-contract.test.ts @@ -11,23 +11,6 @@ import { build, inspect, invokeMcp, listHooks, listMcp, runEvals, simulateHook, const execFile = promisify(executeFile); const examplesRoot = join(process.cwd(), 'examples'); -it('documents the local command flow and each example-specific interaction', async () => { - const [skills, hooks, mcpApp] = await Promise.all([ - readFile(join(examplesRoot, 'skills-starter', 'README.md'), 'utf8'), - readFile(join(examplesRoot, 'hooks-and-scripts', 'README.md'), 'utf8'), - readFile(join(examplesRoot, 'mcp-app', 'README.md'), 'utf8'), - ]); - - for (const readme of [skills, hooks, mcpApp]) { - expect(readme).toContain('pnpm validate'); - expect(readme).toContain('pnpm build'); - expect(readme).toContain('pnpm dev'); - } - expect(skills).toContain('dist/agent-bundle.manifest.json'); - expect(hooks).toContain('Replay saved simulation'); - expect(mcpApp).toContain('Restart MCP session'); -}); - it('builds the Skills Starter through public Agent Bundle APIs', async () => { const root = join(examplesRoot, 'skills-starter'); const output = join(root, '.agent-bundle', 'example-contract'); diff --git a/packages/agent-bundle/tests/manifest.test.ts b/packages/agent-bundle/tests/manifest.test.ts index a8e4e3a1a..0dd4f6e28 100644 --- a/packages/agent-bundle/tests/manifest.test.ts +++ b/packages/agent-bundle/tests/manifest.test.ts @@ -1,11 +1,6 @@ import { expect, it } from '@rstest/core'; -import { - assembleArtifactManifest as assembleArtifactManifestFromApi, - parseArtifactManifest as parseArtifactManifestFromApi, - serializeArtifactManifest as serializeArtifactManifestFromApi, - type ArtifactManifest as ApiArtifactManifest, -} from '../src/api.ts'; +import type { ArtifactManifest as ApiArtifactManifest } from '../src/api.ts'; import { assembleArtifactManifest, parseArtifactManifest, @@ -14,12 +9,7 @@ import { } from '../src/build/manifest.ts'; import { digest, stableJson } from '../src/core/digest.ts'; import { evalTargetDigests } from '../src/eval/artifact.ts'; -import { - assembleArtifactManifest as assembleArtifactManifestFromIndex, - parseArtifactManifest as parseArtifactManifestFromIndex, - serializeArtifactManifest as serializeArtifactManifestFromIndex, - type ArtifactManifest as PublicArtifactManifest, -} from '../src/index.ts'; +import type { ArtifactManifest as PublicArtifactManifest } from '../src/index.ts'; const hash = (character: string): string => character.repeat(64); @@ -132,20 +122,6 @@ it('returns a deeply frozen manifest and exports the public manifest type', () = }).toThrow(TypeError); }); -it('uses the ProjectService source-input revision contract and exports runtime APIs', () => { - const manifest = validManifest(); - const bytes = serializeArtifactManifest(manifest); - - expect(manifest.project.revision).toBe(digest({ inputs: manifest.project.sourceInputs })); - expect(parseArtifactManifestFromApi).toBe(parseArtifactManifest); - expect(serializeArtifactManifestFromApi).toBe(serializeArtifactManifest); - expect(assembleArtifactManifestFromApi).toBe(assembleArtifactManifest); - expect(parseArtifactManifestFromIndex).toBe(parseArtifactManifest); - expect(serializeArtifactManifestFromIndex).toBe(serializeArtifactManifest); - expect(assembleArtifactManifestFromIndex).toBe(assembleArtifactManifest); - expect(parseArtifactManifestFromApi(bytes)).toEqual(parseArtifactManifest(bytes)); -}); - it('produces root-independent canonical bytes without silently sorting caller arrays', () => { const first = assembleArtifactManifest(validManifest()); const second = assembleArtifactManifest(structuredClone(validManifest())); diff --git a/packages/agent-bundle/tests/micro-eval-spot-check.test.ts b/packages/agent-bundle/tests/micro-eval-spot-check.test.ts deleted file mode 100644 index 162b27c27..000000000 --- a/packages/agent-bundle/tests/micro-eval-spot-check.test.ts +++ /dev/null @@ -1,95 +0,0 @@ -import { cp, mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'; -import { tmpdir } from 'node:os'; -import { join, resolve } from 'node:path'; - -import { expect, it } from '@rstest/core'; - -import { runCli } from '../src/cli.ts'; -import type { EvalRunResult } from '../src/dev/eval/eval-service.ts'; - -const fixtureRoot = join(process.cwd(), 'fixtures', 'integration', 'micro-eval'); -const evalEntryPoint = resolve(process.cwd(), 'packages/agent-bundle/src/eval/index.ts'); - -const runCliWithOutput = async (args: readonly string[]): Promise<{ - readonly code: number; - readonly stderr: string; - readonly stdout: string; -}> => { - const stderr: string[] = []; - const stdout: string[] = []; - Object.defineProperty(globalThis, '__AGENT_BUNDLE_VERSION__', { configurable: true, value: 'test' }); - const code = await runCli([...args], { - stderr: { write: (chunk: string) => stderr.push(chunk) }, - stdout: { write: (chunk: string) => stdout.push(chunk) }, - }); - return { code, stderr: stderr.join(''), stdout: stdout.join('') }; -}; - -// A suite module must default-export defineEvalSuite output, so it imports agent-bundle/eval. -// That import only typechecks against a built package, so the suite file and the package shim -// are written into the temporary fixture copy instead of being checked in with the fixture. -const suiteModule = `import { defineEvalSuite, expectOutcome } from 'agent-bundle/eval'; - -export default defineEvalSuite({ - cases: [ - { - assertions: [expectOutcome({ script: './graders/reads-result.ts' })], - fixture: './fixtures/repo', - hosts: { portable: { model: 'deterministic' } }, - id: 'reads-result', - invocation: { mode: 'automatic' }, - prompt: 'Report the highest-risk regression recorded in this repository.', - }, - ], - name: 'micro', -}); -`; - -/** - * The CI end-to-end spot-check: the checked-in micro fixture must build, its artifact must - * validate, and one deterministic eval trial must pass through the real CLI — with no native - * Claude/Codex host and no opt-in environment gate. - */ -it('spot-checks build, validate, and one deterministic eval on the micro fixture', async () => { - const parent = await mkdtemp(join(tmpdir(), 'agent-bundle-micro-eval-')); - const root = join(parent, 'micro-eval'); - const artifact = join(root, 'artifact'); - await cp(fixtureRoot, root, { recursive: true }); - await mkdir(join(root, 'node_modules', 'agent-bundle'), { recursive: true }); - await Promise.all([ - writeFile( - join(root, 'node_modules', 'agent-bundle', 'package.json'), - JSON.stringify({ exports: { './eval': './eval.ts' }, name: 'agent-bundle', type: 'module' }), - ), - writeFile(join(root, 'node_modules', 'agent-bundle', 'eval.ts'), `export * from ${JSON.stringify(evalEntryPoint)};\n`), - writeFile(join(root, 'evals', 'micro.eval.ts'), suiteModule), - ]); - - try { - const build = await runCliWithOutput(['build', '--root', root, '--output', 'artifact']); - expect(build.stderr).toBe(''); - expect(build.code).toBe(0); - await expect(readFile(join(artifact, 'portable', 'skills', 'triage', 'SKILL.md'), 'utf8')).resolves.toContain( - 'name: triage', - ); - - const validated = await runCliWithOutput(['validate', '--root', root, '--artifact', artifact, '--json']); - expect(validated.stderr).toBe(''); - expect(validated.code).toBe(0); - expect(JSON.parse(validated.stdout)).toEqual({ diagnostics: [] }); - - const evaluated = await runCliWithOutput([ - 'eval', '--root', root, '--artifact', artifact, '--case', 'reads-result', '--trials', '1', '--json', - ]); - expect(evaluated.stderr).toBe(''); - expect(evaluated.code).toBe(0); - const parsed = JSON.parse(evaluated.stdout) as EvalRunResult; - expect(parsed.run.harness).toBe('deterministic'); - expect(parsed.run.artifact.source).toBe('explicit'); - expect(parsed.run.summary).toMatchObject({ cases: 1, fail: 0, inconclusive: 0, pass: 1, trials: 1 }); - expect(parsed.trials).toHaveLength(1); - expect(parsed.trials[0]).toMatchObject({ caseId: 'reads-result', host: 'portable', outcome: 'pass' }); - } finally { - await rm(parent, { force: true, recursive: true }); - } -}, 120_000); diff --git a/packages/agent-bundle/tests/packed-consumer.test.ts b/packages/agent-bundle/tests/packed-consumer.test.ts index 1310e398f..afac51e29 100644 --- a/packages/agent-bundle/tests/packed-consumer.test.ts +++ b/packages/agent-bundle/tests/packed-consumer.test.ts @@ -69,11 +69,6 @@ const runInstalled = async ( env: installedEnvironment(), }); -it('recognizes agent-bundle re-exports and CommonJS requires in generated code', () => { - expect("export { build } from 'agent-bundle';").toMatch(agentBundleImport); - expect("const bundle = require('agent-bundle/api');").toMatch(agentBundleImport); -}); - it('uses only an installed tarball after source deletion', async () => { const consumerRoot = await mkdtemp(join(tmpdir(), 'agent-bundle-packed-consumer-')); const packedPackageRoot = join(consumerRoot, 'packed-agent-bundle'); diff --git a/packages/agent-bundle/tests/packed-native-smoke.test.ts b/packages/agent-bundle/tests/packed-native-smoke.test.ts index 50025a25a..ce356c06a 100644 --- a/packages/agent-bundle/tests/packed-native-smoke.test.ts +++ b/packages/agent-bundle/tests/packed-native-smoke.test.ts @@ -139,7 +139,6 @@ it('opaquely detects default ~/.claude.json mutation without extending custom co await writeFile(join(userHome, '.claude.json'), `${privateValue}-changed\n`); }, { homeDirectory: userHome }); expect(changed).toBe(false); - expect(JSON.stringify(changed)).toBe('false'); expect(JSON.stringify(changed)).not.toContain(privateValue); expect(JSON.stringify(changed)).not.toContain(userHome); diff --git a/packages/agent-bundle/tests/public-api.test.ts b/packages/agent-bundle/tests/public-api.test.ts index 8867b3026..a46173e11 100644 --- a/packages/agent-bundle/tests/public-api.test.ts +++ b/packages/agent-bundle/tests/public-api.test.ts @@ -9,16 +9,10 @@ import { expect, it } from '@rstest/core'; import { createCodexEvalHarness, createEvalHarness, - defineConfig, - pathTokens, runClaudeTrial, runCodexEvalTrial, - type AgentBundleConfig, - type ArtifactOutputProvenance, type EvalHarness, type EvalServiceNativeOptions, - type NormalizedConfigExtension, - type NormalizedPlugin, } from '../src/index.ts'; import { TargetRegistry, createDefaultRegistry } from '../src/api.ts'; import type { @@ -27,16 +21,6 @@ import type { TargetMcpRuntimeContract, } from '../src/api.ts'; import { runCli } from '../src/cli.ts'; -import type { - AgentBundleConfig as ConfigEntryAgentBundleConfig, - AgentBundleDevConfig, - AgentBundleDevRuntimeConfig, -} from '../src/config/index.ts'; -import { defineConfig as defineConfigFromConfigEntry } from '../src/config/index.ts'; -import type { - CreateDevRuntimeProvider, - DevRuntimeProvider, -} from '../src/api.ts'; import { agentBundleNodeModules, workspaceNodeModules } from './helpers/workspace-paths.ts'; interface PackageManifest { @@ -98,75 +82,6 @@ it('keeps package output filenames stable', async () => { expect(config.output).not.toHaveProperty('externals'); }); -it('preserves a synchronous config and exposes opaque path tokens', () => { - const config = { plugin: { name: 'demo', version: '1.0.0' } }; - expect(defineConfig(config)).toBe(config); - expect(pathTokens).toEqual({ - pluginRoot: 'agent-bundle:path:plugin-root', - pluginData: 'agent-bundle:path:plugin-data', - workspaceRoot: 'agent-bundle:path:workspace-root', - }); -}); - -it('exposes the same typed config factory from the config entrypoint', () => { - const config = { - plugin: { name: 'config-entrypoint', version: '1.0.0' }, - } satisfies ConfigEntryAgentBundleConfig; - - expect(defineConfigFromConfigEntry).toBe(defineConfig); - expect(defineConfigFromConfigEntry(config)).toBe(config); -}); - -it('exposes an optional author-facing development runtime declaration', () => { - const runtime = { - provider: './src/dev/provider.ts', - } satisfies AgentBundleDevRuntimeConfig; - const dev = { runtime } satisfies AgentBundleDevConfig; - const config = { - dev, - plugin: { name: 'runtime-contract', version: '1.0.0' }, - } satisfies AgentBundleConfig; - - const providerFactory: CreateDevRuntimeProvider | undefined = undefined; - const provider: DevRuntimeProvider | undefined = undefined; - - expect(defineConfig(config)).toBe(config); - expect(config.dev?.runtime?.provider).toBe('./src/dev/provider.ts'); - expect(providerFactory).toBeUndefined(); - expect(provider).toBeUndefined(); -}); - -it('exposes bundled adapter extension and normalized-extension types from the root import', () => { - const config = { - claude: { nativeHooks: './claude-hooks.json' }, - codex: { nativeHooks: './codex-hooks.json' }, - plugin: { name: 'typed-extension-fixture', version: '1.0.0' }, - portable: { compatibility: 'portable-v1' }, - } satisfies AgentBundleConfig; - const extension: NormalizedConfigExtension = { - id: 'extension:portable', - key: 'portable', - provenance: { kind: 'config', sourcePath: '/workspace/agent-bundle.config.ts' }, - target: 'portable', - value: config.portable, - }; - const model = { - extensions: { portable: extension }, - } satisfies Pick; - - expect(model.extensions.portable.key).toBe('portable'); -}); - -it('exposes immutable output provenance types from the root import', () => { - const output: ArtifactOutputProvenance = { - kind: 'bundle', - path: 'portable/scripts/greeting.mjs', - sourceInputs: ['skills/review/scripts/greeting.ts'], - }; - - expect(output.kind).toBe('bundle'); -}); - it('exposes native eval descriptors, runners, and injection types from the root import', () => { const descriptor: EvalHarness = createEvalHarness('claude'); const native: EvalServiceNativeOptions = { environment: { PATH: '/usr/bin' } }; @@ -222,14 +137,6 @@ it('loads every public subpath and reports the package version', async () => { await expect(runCli(['--version'])).resolves.toBe(0); }); -it('exposes defineConfig from the config subpath exactly as the README documents', async () => { - const configEntry = await import('../src/config/index.ts'); - const config = { plugin: { name: 'demo', version: '1.0.0' } }; - - expect(configEntry.defineConfig).toBe(defineConfig); - expect(configEntry.defineConfig(config)).toBe(config); -}); - it('publishes directly executable built entrypoints with declarations', async () => { await buildPackage(); diff --git a/packages/agent-bundle/tests/release-audit.test.ts b/packages/agent-bundle/tests/release-audit.test.ts index 30e0dab66..178505882 100644 --- a/packages/agent-bundle/tests/release-audit.test.ts +++ b/packages/agent-bundle/tests/release-audit.test.ts @@ -108,16 +108,6 @@ it('ships repository and support metadata that matches the verified origin', asy } }); -it('runs a release pack dry run with the CLI in its tarball', async () => { - const { stdout } = await execFile('pnpm', ['pack:dry-run'], { - cwd: workspaceRoot, - env: releaseEnvironment(), - }); - - expect(stdout).toContain('agent-bundle-0.1.0.tgz'); - expect(stdout).toContain('dist/cli.js'); -}, 120_000); - it('packs generated Workbench legal companion files', async () => { const tarballRoot = await mkdtemp(join(tmpdir(), 'agent-bundle-release-audit-')); diff --git a/packages/agent-bundle/tests/rsc-runtime-topology-script.test.ts b/packages/agent-bundle/tests/rsc-runtime-topology-script.test.ts index cbb00876b..6855a95b6 100644 --- a/packages/agent-bundle/tests/rsc-runtime-topology-script.test.ts +++ b/packages/agent-bundle/tests/rsc-runtime-topology-script.test.ts @@ -11,13 +11,6 @@ const workspaceRoot = process.cwd(); const script = join(workspaceRoot, 'scripts', 'rsc-runtime-topology.mjs'); const output = 'docs/architecture/rsc-runtime-workbench.md'; -const captureCommand = `node packages/workbench/scripts/capture-runtime-playground.mjs \\ - --desktop "$PWD/docs/assets/rsc-runtime-workbench/desktop.png" \\ - --hmr-before "$PWD/docs/assets/rsc-runtime-workbench/hmr-before.png" \\ - --hmr-after "$PWD/docs/assets/rsc-runtime-workbench/hmr-after.png" \\ - --compile-error "$PWD/docs/assets/rsc-runtime-workbench/compile-error.png" \\ - --recovered "$PWD/docs/assets/rsc-runtime-workbench/recovered.png" \\ - --evidence /tmp/rsc-runtime-delivery/evidence.json`; const expectedTree = `packages/ agent-bundle/ src/adapters/registry.ts @@ -62,12 +55,6 @@ const run = (root: string, check = false): Promise<{ readonly stdout: string; re ); describe('rsc runtime topology script', () => { - it('documents every required absolute runtime capture output', async () => { - const readme = await readFile(join(workspaceRoot, 'examples', 'rsc-agent-runtime', 'README.md'), 'utf8'); - - expect(readme).toContain(captureCommand); - }); - it('renders the tracked feature tree and detects a stale marker block', async () => { const root = await mkdtemp(join(tmpdir(), 'rsc-runtime-topology-')); try { diff --git a/packages/agent-bundle/tests/runtime-provider.test.ts b/packages/agent-bundle/tests/runtime-provider.test.ts index 531740a60..8b7dd0df0 100644 --- a/packages/agent-bundle/tests/runtime-provider.test.ts +++ b/packages/agent-bundle/tests/runtime-provider.test.ts @@ -142,17 +142,9 @@ const incompleteBinding = { // @ts-expect-error Stable MCP bindings include registry/session revisions and all three digests. const completeBinding: DevRuntimeMcpSessionBinding = incompleteBinding; -it('publishes JSON-safe runtime run, surface, and stable MCP binding contracts', () => { - expect(surface.targets).toEqual(['claude', 'codex']); - expect(binding.sessionRevision).toBe(2); - expect(run.status).toBe('succeeded'); - expect(invalidReactRun).toBeDefined(); - expect(jsonOnlyRun).toBeDefined(); - expect(targetlessSurface).toBeDefined(); - expect(targetfulSurface).toBeDefined(); - expect(incompleteBinding).toBeDefined(); - expect(completeBinding).toBeDefined(); -}); +// The satisfies/@ts-expect-error declarations above are the contract checks; +// they need no runtime test to compile. +void [binding, jsonOnlyRun, targetfulSurface, completeBinding]; it('uses stable errors for unavailable and stale runtime generations', () => { const unavailable = new DevRuntimeUnavailableError(); diff --git a/rstest.integration-tests.ts b/rstest.integration-tests.ts index 3c6d46d23..78e0cc619 100644 --- a/rstest.integration-tests.ts +++ b/rstest.integration-tests.ts @@ -18,7 +18,6 @@ export const integrationTestFiles: readonly string[] = [ 'packages/agent-bundle/tests/cli.test.ts', 'packages/agent-bundle/tests/dev-artifact-service.test.ts', 'packages/agent-bundle/tests/dev-workbench.test.ts', - 'packages/agent-bundle/tests/epoch-atomicity-spike.test.ts', 'packages/agent-bundle/tests/eval-claude-harness.test.ts', 'packages/agent-bundle/tests/eval-cli.test.ts', 'packages/agent-bundle/tests/eval-fixtures.test.ts', @@ -33,7 +32,6 @@ export const integrationTestFiles: readonly string[] = [ 'packages/agent-bundle/tests/integration-matrix.test.ts', 'packages/agent-bundle/tests/mcp-session-service.test.ts', 'packages/agent-bundle/tests/mcp.test.ts', - 'packages/agent-bundle/tests/micro-eval-spot-check.test.ts', 'packages/agent-bundle/tests/path-token-resolver.test.ts', 'packages/agent-bundle/tests/plugin-bundle.test.ts', 'packages/agent-bundle/tests/public-api.test.ts', From badb229009d1fc2f542d6cfe2f01dcd5151c98e0 Mon Sep 17 00:00:00 2001 From: ScriptedAlchemy Date: Sat, 29 Aug 2026 01:57:25 +0000 Subject: [PATCH 11/16] test(agent-bundle): drop schema hash tables duplicated by the adapter-metadata rehash test adapter-metadata.test.ts already rehashes every capability and schema snapshot against its pinned provenance for all built-in targets, so the host-adapters CLI-version test keeps only its unique assertions: the observed CLI version pins, the redacted help text, and the codex marketplace validator fixture. --- .../agent-bundle/tests/host-adapters.test.ts | 29 +++---------------- 1 file changed, 4 insertions(+), 25 deletions(-) diff --git a/packages/agent-bundle/tests/host-adapters.test.ts b/packages/agent-bundle/tests/host-adapters.test.ts index c53c363cd..142d51618 100644 --- a/packages/agent-bundle/tests/host-adapters.test.ts +++ b/packages/agent-bundle/tests/host-adapters.test.ts @@ -8,7 +8,6 @@ import { expect, it } from '@rstest/core'; import { createDefaultRegistry } from '../src/adapters/registry.ts'; import { build } from './support/build.ts'; -import { sha256Hex } from '../src/core/digest.ts'; import { pathTokens, type NormalizedPlugin } from '../src/core/types.ts'; const installFormats = addFormats as unknown as (target: Ajv2020) => void; @@ -116,25 +115,11 @@ const validateDocuments = async ( }; it('pins host help, capabilities, and every schema snapshot to the supported CLI versions', async () => { + // Schema snapshot hashes are pinned by adapter-metadata.test.ts's rehash + // test; this test pins the observed CLI versions and the redacted help text. const hosts = { - claude: { - hashes: { - 'hooks.schema.json': '3c6f3e4391f3dca939d75bd0b200ea88e68db939a2cb885d46f0b143293efb84', - 'marketplace.schema.json': '5a08f241f9e856bb59489a265d9bf4db9c905e874d720f46def59fdb6f3ca257', - 'mcp.schema.json': '76ccf02c7bfe2d57945ba18e84da8d655529bd68b4d692f72bce28238c99067e', - 'plugin.schema.json': 'd145d370f5ad16fb9f29a6f1b5c9cb3ae8a6b9c33b3a11513eea324e8feb17c5', - }, - version: '2.1.250', - }, - codex: { - hashes: { - 'hooks.schema.json': 'e42eef736997b9abb8f28b2ee9262f5c7b1f7f11d8289e9c25da8cc94a504eff', - 'marketplace.schema.json': '1d43c5ed19de401fb7455c5912e4c21113f6e387aef4c28d2eca121f7554c4e8', - 'mcp.schema.json': '75bd50f9fcb85c2e8d43bc132d61c172a02f28ea8bb77389816ae77b14a4257e', - 'plugin.schema.json': 'f6e8e7d2ecb48c50ffa850d1a8190ad85ceffec705b8f0f39bb44a1d10aca0d9', - }, - version: '0.147.0', - }, + claude: { version: '2.1.250' }, + codex: { version: '0.147.0' }, } as const; for (const [host, expected] of Object.entries(hosts)) { @@ -142,7 +127,6 @@ it('pins host help, capabilities, and every schema snapshot to the supported CLI const contractRoot = new URL(`../fixtures/contracts/${host}/`, import.meta.url); const provenance = JSON.parse(await readFile(new URL('PROVENANCE.json', schemaRoot), 'utf8')) as { readonly observedCliVersion: string; - readonly schemas: Record; }; const contract = JSON.parse(await readFile(new URL('capabilities.json', contractRoot), 'utf8')) as { readonly observedCliVersion: string; @@ -153,11 +137,6 @@ it('pins host help, capabilities, and every schema snapshot to the supported CLI expect(contract.observedCliVersion).toBe(expected.version); expect(help).toContain(`version: ${expected.version}`); expect(help).not.toMatch(/(?:\/home\/|logged in|credential state|session id)/i); - for (const [name, hash] of Object.entries(expected.hashes)) { - const schema = await readFile(new URL(name, schemaRoot)); - expect(sha256Hex(schema)).toBe(hash); - expect(provenance.schemas[name]?.sha256).toBe(hash); - } } const codexValidatorFixture = JSON.parse( From 4b08167789b1ba4119401fc1089aa59fc7d3a1eb Mon Sep 17 00:00:00 2001 From: ScriptedAlchemy Date: Sat, 29 Aug 2026 02:24:40 +0000 Subject: [PATCH 12/16] test: derive integration worker count from cores and split shared-artifact writers into a serial pool The parallel pool runs mkdtemp-fixture, ephemeral-port files on min(4, cores/2) workers (still 1 on two-core CI, overridable via AGENT_BUNDLE_INTEGRATION_MAX_WORKERS); the six files that rewrite shared package dists stay on one worker in a chained serial config. --- package.json | 2 +- rstest.integration-serial.config.ts | 16 ++++++++++++ rstest.integration-tests.ts | 38 +++++++++++++++++++++++++++++ rstest.integration.config.ts | 28 ++++++++++++++++++--- 4 files changed, 79 insertions(+), 5 deletions(-) create mode 100644 rstest.integration-serial.config.ts diff --git a/package.json b/package.json index 7b3db36df..6deb835cf 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,7 @@ "test": "pnpm test:unit && pnpm test:integration", "test:unit": "rstest --config rstest.unit.config.ts", "test:integration": "pnpm --filter agent-bundle-workbench build && pnpm test:integration:run", - "test:integration:run": "AGENT_BUNDLE_WORKBENCH_PREBUILT=1 rstest --config rstest.integration.config.ts --pool.maxWorkers 1", + "test:integration:run": "AGENT_BUNDLE_WORKBENCH_PREBUILT=1 rstest --config rstest.integration.config.ts && AGENT_BUNDLE_WORKBENCH_PREBUILT=1 rstest --config rstest.integration-serial.config.ts", "test:watch": "rstest --config rstest.config.ts --watch", "lint": "rslint .", "typecheck": "tsc --noEmit && tsc --project packages/workbench/tsconfig.json", diff --git a/rstest.integration-serial.config.ts b/rstest.integration-serial.config.ts new file mode 100644 index 000000000..448277c11 --- /dev/null +++ b/rstest.integration-serial.config.ts @@ -0,0 +1,16 @@ +import { defineConfig } from '@rstest/core'; + +import { serialIntegrationTestFiles } from './rstest.integration-tests.ts'; +import { withAgentBundleRslibConfig } from './rstest.rslib.ts'; + +/** + * Integration files that rewrite workspace-shared artifacts (see the + * serialIntegrationTestFiles doc in rstest.integration-tests.ts). One worker + * only: they rebuild or repack shared package dist directories that every + * other file in this group also reads. + */ +export default defineConfig({ + extends: withAgentBundleRslibConfig(), + include: [...serialIntegrationTestFiles], + pool: { maxWorkers: 1 }, +}); diff --git a/rstest.integration-tests.ts b/rstest.integration-tests.ts index 78e0cc619..d40ca8b5e 100644 --- a/rstest.integration-tests.ts +++ b/rstest.integration-tests.ts @@ -66,6 +66,44 @@ export const integrationTestFiles: readonly string[] = [ 'packages/workbench/tests/workbench-dev-command.test.ts', ]; +/** + * Integration files that WRITE to workspace-shared locations and therefore + * cannot run alongside other integration files: + * + * - inspector-shell.e2e rewrites `packages/workbench/dist` with an explicit + * development-mode artifact (the build itself is under test). + * - packed-release.e2e runs a root `pnpm build` (rewriting + * `packages/{agent-bundle,rsc-runtime,workbench}/dist`) and `npm pack`. + * - cli.test runs a root `pnpm build` and `npm pack` against the shared + * package dist. + * - overview.e2e, mcp-app-real.e2e, and playground-real.e2e rebuild + * `packages/workbench/dist` with local build helpers that ignore + * AGENT_BUNDLE_WORKBENCH_PREBUILT. + * + * They run on one worker via rstest.integration-serial.config.ts after the + * parallel pool finishes (rstest orders files alphabetically, so + * inspector-shell's development artifact lands after cli.test has used the + * production CLI dist, and packed-release packs the agent-bundle dist copy + * that is unaffected by the workbench dist rewrite). + */ +export const serialIntegrationTestFiles: readonly string[] = [ + 'packages/agent-bundle/tests/cli.test.ts', + 'packages/workbench/tests/inspector-shell.e2e.test.ts', + 'packages/workbench/tests/mcp-app-real.e2e.test.ts', + 'packages/workbench/tests/overview.e2e.test.ts', + 'packages/workbench/tests/packed-release.e2e.test.ts', + 'packages/workbench/tests/playground-real.e2e.test.ts', +]; + +/** + * Integration files safe on parallel workers: they create per-test fixtures + * with `mkdtemp`, bind servers on ephemeral ports (`port: 0` or rsbuild's + * silent free-port fallback), and only READ the prebuilt shared artifacts + * (`packages/workbench/dist`, `packages/agent-bundle/dist`). + */ +export const parallelIntegrationTestFiles: readonly string[] = + integrationTestFiles.filter((file) => !serialIntegrationTestFiles.includes(file)); + /** * Pack-and-install tests: each one runs `npm pack` (and usually a clean * `npm install` of the tarball), which dominates the serialized integration diff --git a/rstest.integration.config.ts b/rstest.integration.config.ts index 47c0a4043..ee1394b83 100644 --- a/rstest.integration.config.ts +++ b/rstest.integration.config.ts @@ -1,13 +1,33 @@ +import { availableParallelism } from 'node:os'; + import { defineConfig } from '@rstest/core'; -import { integrationTestFiles } from './rstest.integration-tests.ts'; +import { parallelIntegrationTestFiles } from './rstest.integration-tests.ts'; import { withAgentBundleRslibConfig } from './rstest.rslib.ts'; -/** Build- and process-running tests: Rslib/Rsbuild caches and output paths are process-shared, so one worker only. */ +/** + * Worker count for the parallel integration pool. Half the cores keeps + * browser + dev-server pairs from starving each other, the cap of 4 bounds + * memory on large machines, and two-core CI still resolves to one worker. + * AGENT_BUNDLE_INTEGRATION_MAX_WORKERS overrides the computed value (e.g. to + * force a serial run when measuring or bisecting). + */ +const overrideWorkers = Number(process.env['AGENT_BUNDLE_INTEGRATION_MAX_WORKERS'] ?? ''); +const maxWorkers = Number.isSafeInteger(overrideWorkers) && overrideWorkers >= 1 + ? overrideWorkers + : Math.max(1, Math.min(4, Math.floor(availableParallelism() / 2))); + +/** + * Build- and process-running tests that only read workspace-shared artifacts; + * files that WRITE shared locations run serialized afterwards through + * rstest.integration-serial.config.ts (rstest has no per-project pool or + * isolate settings, so the split lives in two configs chained by + * `test:integration:run`). + */ export default defineConfig({ extends: withAgentBundleRslibConfig(), - include: [...integrationTestFiles], - pool: { maxWorkers: 1 }, + include: [...parallelIntegrationTestFiles], + pool: { maxWorkers }, // isolate: false would cut Playwright startup cost, but the log pipeline // suites rely on per-file module isolation (verified: logs-real.e2e fails // when sharing a worker with the other log suites). From e42702a42d7ba7f0df95a12ac6c108cb9093a030 Mon Sep 17 00:00:00 2001 From: ScriptedAlchemy Date: Sat, 29 Aug 2026 02:28:28 +0000 Subject: [PATCH 13/16] test: stop rebuilding shared artifacts inside the integration pool overview.e2e, mcp-app-real.e2e, and playground-real.e2e now use the shared memoized buildWorkbench (which honors AGENT_BUNDLE_WORKBENCH_PREBUILT) instead of local copies that rebuilt packages/workbench/dist up to ten times per run; cli.test and the packed-release harness honor a new AGENT_BUNDLE_PACKAGE_PREBUILT flag, with `test:integration` building the whole workspace once up front. With those writes gone, the four files move from the serial pool to the parallel one. --- package.json | 4 ++-- packages/agent-bundle/tests/cli.test.ts | 1 + .../workbench/tests/mcp-app-real.e2e.test.ts | 13 +----------- packages/workbench/tests/overview.e2e.test.ts | 12 +---------- .../tests/playground-real.e2e.test.ts | 15 +------------- .../tests/support/packed-release-harness.ts | 1 + rstest.integration-tests.ts | 20 ++++++------------- 7 files changed, 13 insertions(+), 53 deletions(-) diff --git a/package.json b/package.json index 6deb835cf..04a871f2a 100644 --- a/package.json +++ b/package.json @@ -12,8 +12,8 @@ "lint:package": "publint packages/agent-bundle", "test": "pnpm test:unit && pnpm test:integration", "test:unit": "rstest --config rstest.unit.config.ts", - "test:integration": "pnpm --filter agent-bundle-workbench build && pnpm test:integration:run", - "test:integration:run": "AGENT_BUNDLE_WORKBENCH_PREBUILT=1 rstest --config rstest.integration.config.ts && AGENT_BUNDLE_WORKBENCH_PREBUILT=1 rstest --config rstest.integration-serial.config.ts", + "test:integration": "pnpm build && pnpm test:integration:run", + "test:integration:run": "AGENT_BUNDLE_WORKBENCH_PREBUILT=1 AGENT_BUNDLE_PACKAGE_PREBUILT=1 rstest --config rstest.integration.config.ts && AGENT_BUNDLE_WORKBENCH_PREBUILT=1 AGENT_BUNDLE_PACKAGE_PREBUILT=1 rstest --config rstest.integration-serial.config.ts", "test:watch": "rstest --config rstest.config.ts --watch", "lint": "rslint .", "typecheck": "tsc --noEmit && tsc --project packages/workbench/tsconfig.json", diff --git a/packages/agent-bundle/tests/cli.test.ts b/packages/agent-bundle/tests/cli.test.ts index 9fe2f5436..a858d0b64 100644 --- a/packages/agent-bundle/tests/cli.test.ts +++ b/packages/agent-bundle/tests/cli.test.ts @@ -15,6 +15,7 @@ const cliPath = join(packageRoot, 'dist/cli.js'); let buildPackage: Promise | undefined; const buildCliPackage = async (): Promise => { + if (process.env['AGENT_BUNDLE_PACKAGE_PREBUILT'] === '1') return; buildPackage ??= execFile('pnpm', ['build'], { cwd: workspaceRoot }).then(() => undefined); await buildPackage; }; diff --git a/packages/workbench/tests/mcp-app-real.e2e.test.ts b/packages/workbench/tests/mcp-app-real.e2e.test.ts index f2b5c235e..4ba1a8ec6 100644 --- a/packages/workbench/tests/mcp-app-real.e2e.test.ts +++ b/packages/workbench/tests/mcp-app-real.e2e.test.ts @@ -1,7 +1,5 @@ -import { execFile as executeFile } from 'node:child_process'; import { access, mkdir, readFile, symlink, writeFile } from 'node:fs/promises'; import { join } from 'node:path'; -import { promisify } from 'node:util'; import { expect, test, type PlaywrightOptions } from '@rstest/playwright'; import type { Page, WebSocketRoute } from 'playwright'; @@ -12,12 +10,11 @@ import { startDevServer } from '../../agent-bundle/src/dev/workbench-server.ts'; import { createProjectFixture, removeProjectFixture } from '../../agent-bundle/tests/helpers/project-fixture.ts'; import { startRuntimePlaygroundFixture } from './helpers/runtime-playground-fixture.ts'; import { timeScale } from '../../agent-bundle/tests/support/time-scale.ts'; -import { workbenchUrl } from './support/workbench-e2e.ts'; +import { buildWorkbench, workbenchUrl } from './support/workbench-e2e.ts'; const workspaceRoot = process.cwd(); const workbenchAssets = join(workspaceRoot, 'packages', 'workbench', 'dist'); const browserTimeout = 8_000 * timeScale; -const execFile = promisify(executeFile); const e2e = test.extend({ playwright: { @@ -26,14 +23,6 @@ const e2e = test.extend({ } satisfies PlaywrightOptions, }); -const buildWorkbench = async (): Promise => { - const { RSTEST: _rstest, ...environment } = process.env; - await execFile('pnpm', ['--filter', 'agent-bundle-workbench', 'build'], { - cwd: workspaceRoot, - env: { ...environment, NODE_ENV: 'production' }, - }); -}; - const appFixtureHtml = [ '
waiting
', '