From 6ce4b8e1a828823f6c6e0820733415d8fb062eb6 Mon Sep 17 00:00:00 2001 From: ScriptedAlchemy Date: Sat, 5 Sep 2026 00:20:42 +0000 Subject: [PATCH 1/5] =?UTF-8?q?test:=20guard=20the=20Rstest=20pool=20lists?= =?UTF-8?q?,=20strip=20publish-only=20plugins=20from=20pools,=20restore=20?= =?UTF-8?q?state=20between=20tests=20(#566=20=C2=A73)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Pools are defined by subtraction and nothing checked the lists, the package build's publish-time plugins ran in every pool, and no pool restored mocks or stubs between tests. This lands the interim guards from issue #566 section 3 without the test-file reorganization. - rstest-pool-lists.test.ts: every path in rstest.integration-tests.ts exists, every glob matches, no entry repeats, no file is collected by two lists, and no `packages/**/tests` test file is left without a pool. The lists were clean; the orphan check found runtime-playground.browser.test.tsx, runnable only through the dead configs below, and it is deleted with them. - rstest.rslib.ts: `modifyLibConfig` drops `plugin-publint` and `agent-bundle:esm-node-globals` by plugin name and the publish-only `tools.rspack` hook; `source.define` and the tsconfig stay. Resolved config verified with DEBUG=rstest before and after. - `restoreMocks`, `clearMocks`, `unstubEnvs`, `unstubGlobals` on every pool through the shared config (`rstestHygiene`), spread into the route-unit and projection pools that build from the shipped helper. The ten `Object.defineProperty(globalThis, '__AGENT_BUNDLE_VERSION__')` stubs were dead — the compiled test bundle carries `.version("0.1.0")` from `source.define` — and are removed. - Deleted rstest.runtime-playground.config.ts, rstest.runtime-playground.browser.config.ts and rstest.setup.browser.ts: never referenced by a script or workflow, and they fail to load (`node_modules/react` is not resolvable at the workspace root). - ci.yml: note that Rstest already enables the `github-actions` reporter under GITHUB_ACTIONS, so no --reporter flag is needed. --- .github/workflows/ci.yml | 3 + packages/agent-bundle/tests/cli.test.ts | 4 - packages/agent-bundle/tests/eval-cli.test.ts | 1 - .../agent-bundle/tests/inspect-state.test.ts | 1 - packages/agent-bundle/tests/install.test.ts | 3 - packages/agent-bundle/tests/prepack.test.ts | 1 - .../agent-bundle/tests/route-graph.test.ts | 1 - .../tests/rstest-pool-lists.test.ts | 94 +++++++ packages/agent-bundle/tests/uninstall.test.ts | 1 - .../tests/runtime-playground.browser.test.tsx | 256 ------------------ rstest.config.ts | 5 +- rstest.integration-tests.ts | 9 + rstest.projection.config.ts | 14 +- rstest.route-unit.config.ts | 15 +- rstest.rslib.ts | 90 +++++- rstest.runtime-playground.browser.config.ts | 36 --- rstest.runtime-playground.config.ts | 60 ---- rstest.setup.browser.ts | 5 - rstest.unit.config.ts | 5 +- 19 files changed, 210 insertions(+), 394 deletions(-) create mode 100644 packages/agent-bundle/tests/rstest-pool-lists.test.ts delete mode 100644 packages/workbench/tests/runtime-playground.browser.test.tsx delete mode 100644 rstest.runtime-playground.browser.config.ts delete mode 100644 rstest.runtime-playground.config.ts delete mode 100644 rstest.setup.browser.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c19d5d394..f14525ba2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -158,6 +158,9 @@ jobs: - run: pnpm build - run: pnpm typecheck - run: pnpm lint + # Rstest's default reporters under GITHUB_ACTIONS=true are `default` plus + # `github-actions`, so failures already annotate the PR; no --reporter + # flag is needed on these legs (verified on @rstest/core 0.11.10). - run: pnpm test # Required-check anchor for the Verify matrix. When a matrix job is skipped diff --git a/packages/agent-bundle/tests/cli.test.ts b/packages/agent-bundle/tests/cli.test.ts index a21647303..1a2b8d902 100644 --- a/packages/agent-bundle/tests/cli.test.ts +++ b/packages/agent-bundle/tests/cli.test.ts @@ -46,7 +46,6 @@ const runSourceCliWithOutput = async ( dependencies: CliDependencies = {}, ): Promise<{ readonly code: number; readonly stderr: string; readonly stdout: string }> => { const terminal = captureCliTerminal(); - Object.defineProperty(globalThis, '__AGENT_BUNDLE_VERSION__', { configurable: true, value: 'test' }); const code = await runSourceCli(args, terminal.output, dependencies); return { code, stderr: terminal.stderr(), stdout: terminal.stdout() }; }; @@ -799,8 +798,6 @@ it('reports a generated Flight worker collision before compiling scripts', async it('dispatches the install command through the native installer surface', async () => { const terminal = captureCliTerminal(); const calls: unknown[] = []; - Object.defineProperty(globalThis, '__AGENT_BUNDLE_VERSION__', { configurable: true, value: 'test' }); - const code = await runSourceCli( ['install', 'claude', '--from', '/tmp/example bundle', '--scope', 'project', '--json'], terminal.output, @@ -900,7 +897,6 @@ it('reports the bound server exiting on its own as one diagnostic and releases t let closeCalls = 0; const serverExit = Promise.withResolvers(); const terminal = captureCliTerminal(); - Object.defineProperty(globalThis, '__AGENT_BUNDLE_VERSION__', { configurable: true, value: 'test' }); const code = await runSourceCli(['serve-app', 'status/status', '--root', '/project', '--no-open'], terminal.output, { serveApp: async () => ({ close: async () => { closeCalls += 1; }, diff --git a/packages/agent-bundle/tests/eval-cli.test.ts b/packages/agent-bundle/tests/eval-cli.test.ts index 6ef9eb02a..9d6f871c9 100644 --- a/packages/agent-bundle/tests/eval-cli.test.ts +++ b/packages/agent-bundle/tests/eval-cli.test.ts @@ -18,7 +18,6 @@ const runCliWithOutput = async (args: readonly string[]): Promise<{ readonly stdout: string; }> => { const terminal = captureCliTerminal(); - Object.defineProperty(globalThis, '__AGENT_BUNDLE_VERSION__', { configurable: true, value: 'test' }); const code = await runCli([...args], terminal.output); return { code, stderr: terminal.stderr(), stdout: terminal.stdout() }; }; diff --git a/packages/agent-bundle/tests/inspect-state.test.ts b/packages/agent-bundle/tests/inspect-state.test.ts index e5ef21c5f..088163508 100644 --- a/packages/agent-bundle/tests/inspect-state.test.ts +++ b/packages/agent-bundle/tests/inspect-state.test.ts @@ -41,7 +41,6 @@ const inspectCli = async ( args: readonly string[], ): Promise<{ readonly code: number; readonly stderr: string; readonly stdout: string }> => { const terminal = captureCliTerminal(); - Object.defineProperty(globalThis, '__AGENT_BUNDLE_VERSION__', { configurable: true, value: 'test' }); const code = await runCli(['inspect', '--root', root, ...args], terminal.output); return { code, stderr: terminal.stderr(), stdout: terminal.stdout() }; }; diff --git a/packages/agent-bundle/tests/install.test.ts b/packages/agent-bundle/tests/install.test.ts index 4168ff41a..fbb29b065 100644 --- a/packages/agent-bundle/tests/install.test.ts +++ b/packages/agent-bundle/tests/install.test.ts @@ -1453,8 +1453,6 @@ it('rejects a Cursor plugin name that could escape the local install root', asyn it('dispatches the public CLI install command to the native installer', async () => { const terminal = captureCliTerminal(); const calls: unknown[] = []; - Object.defineProperty(globalThis, '__AGENT_BUNDLE_VERSION__', { configurable: true, value: 'test' }); - const code = await runCli( ['install', 'claude', '--from', '/tmp/example bundle', '--scope', 'project', '--force', '--json'], terminal.output, @@ -1774,7 +1772,6 @@ it('rejects an install mode for hosts other than Cursor', async () => { it('passes --mode through the public CLI and prints the staged next steps', async () => { const calls: unknown[] = []; - Object.defineProperty(globalThis, '__AGENT_BUNDLE_VERSION__', { configurable: true, value: 'test' }); const dependencies = { installBundle: async (options: unknown) => { calls.push(options); diff --git a/packages/agent-bundle/tests/prepack.test.ts b/packages/agent-bundle/tests/prepack.test.ts index 4346bf20e..6a961eb19 100644 --- a/packages/agent-bundle/tests/prepack.test.ts +++ b/packages/agent-bundle/tests/prepack.test.ts @@ -131,7 +131,6 @@ it('prepack validates the complete dry-run inventory', async () => { it('exposes --root, --output, and --json through the prepack command', async () => { const calls: unknown[] = []; const terminal = captureCliTerminal(); - Object.defineProperty(globalThis, '__AGENT_BUNDLE_VERSION__', { configurable: true, value: 'test' }); const code = await runCli( ['prepack', '--root', projectRoot, '--output', 'host-packs', '--json'], terminal.output, diff --git a/packages/agent-bundle/tests/route-graph.test.ts b/packages/agent-bundle/tests/route-graph.test.ts index 9ee91a7cd..0707f82da 100644 --- a/packages/agent-bundle/tests/route-graph.test.ts +++ b/packages/agent-bundle/tests/route-graph.test.ts @@ -788,7 +788,6 @@ it('shows projected MCP command provenance and safety in the routes inspect focu }); it('dumps the graph through the CLI --routes focus and rejects ambiguous focuses', async () => { - Object.defineProperty(globalThis, '__AGENT_BUNDLE_VERSION__', { configurable: true, value: 'test' }); const root = await createInspectProject({ 'src/mcp/curator/tools/inspect.ts': moduleSource, }); diff --git a/packages/agent-bundle/tests/rstest-pool-lists.test.ts b/packages/agent-bundle/tests/rstest-pool-lists.test.ts new file mode 100644 index 000000000..336f94a7c --- /dev/null +++ b/packages/agent-bundle/tests/rstest-pool-lists.test.ts @@ -0,0 +1,94 @@ +import { globSync, statSync } from 'node:fs'; +import { dirname, resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +import { describe, expect, it } from '@rstest/core'; + +import * as poolLists from '../../../rstest.integration-tests.ts'; + +/** + * The pools are defined by subtraction: `rstest.unit.config.ts` collects + * `workspaceTestFileGlob` minus every list `rstest.integration-tests.ts` + * exports, and each other pool includes exactly one of those lists. Nothing in + * Rstest checks that a listed path still exists, so a moved or renamed file + * would silently fall into the non-isolated, parallel unit pool while its + * stale entry kept excluding nothing — and a file the glob never matched (a + * `.tsx`, a `.spec.ts`) has no pool at all unless a list names it. This is the + * interim guard until the suites move into per-pool directories (#566). + */ +const workspaceRoot = resolve(dirname(fileURLToPath(import.meta.url)), '../../..'); + +const isGlob = (entry: string): boolean => /[*?[\]{}]/u.test(entry); + +const isFile = (entry: string): boolean => { + try { + return statSync(resolve(workspaceRoot, entry)).isFile(); + } catch { + return false; + } +}; + +/** Rstest's default exclusions, so a dependency's or build output's tests never count. */ +const isBuildOrDependencyPath = (path: string): boolean => /(?:^|\/)(?:node_modules|dist)(?:\/|$)/u.test(path); + +const matchingFiles = (pattern: string): readonly string[] => globSync(pattern, { + cwd: workspaceRoot, + exclude: (path: string) => isBuildOrDependencyPath(path), +}).filter(isFile).sort(); + +/** Files a pool entry collects: the entry itself when literal, its matches when a glob. */ +const collectedFiles = (entry: string): readonly string[] => (isGlob(entry) ? matchingFiles(entry) : [entry]); + +const { workspaceTestFileGlob, ...exportedLists } = poolLists; +const lists = Object.entries(exportedLists) as ReadonlyArray; + +it('exports the include glob plus non-empty string lists, so every pool below is actually checked', () => { + expect(isGlob(workspaceTestFileGlob)).toBe(true); + expect(lists.length).toBeGreaterThan(0); + for (const [name, entries] of lists) { + expect(Array.isArray(entries), `${name} is not an array`).toBe(true); + expect(entries.length, `${name} is empty`).toBeGreaterThan(0); + expect(entries.every((entry) => typeof entry === 'string'), `${name} holds a non-string entry`).toBe(true); + } +}); + +describe.each(lists)('%s', (name, entries) => { + it('lists only files that exist (a moved or deleted test leaves a stale entry)', () => { + const missing = entries.filter((entry) => !isGlob(entry) && !isFile(entry)); + expect(missing, `${name}: paths that no longer exist`).toEqual([]); + }); + + it('lists only globs that still match at least one file', () => { + const empty = entries.filter((entry) => isGlob(entry) && collectedFiles(entry).length === 0); + expect(empty, `${name}: globs that match nothing`).toEqual([]); + }); + + it('lists every entry once', () => { + const duplicates = entries.filter((entry, index) => entries.indexOf(entry) !== index); + expect(duplicates, `${name}: repeated entries`).toEqual([]); + }); +}); + +it('assigns every test file to at most one pool', () => { + const owners = new Map(); + for (const [name, entries] of lists) { + for (const file of entries.flatMap(collectedFiles)) { + owners.set(file, [...(owners.get(file) ?? []), name]); + } + } + const shared = [...owners].filter(([, names]) => names.length > 1).map(([file, names]) => `${file} <- ${names.join(', ')}`); + expect(shared, 'files collected by more than one pool list').toEqual([]); +}); + +it('leaves no test file without a pool', () => { + // Every `.test.ts` the include glob matches has a pool by construction (the + // unit pool, unless a list claims it); anything else — another extension, + // another suffix — runs only if a list names it. + const everyTestFile = matchingFiles('packages/**/tests/**/*.{test,spec}.{ts,tsx,mts,cts,js,mjs,cjs,jsx}'); + const collected = new Set([ + ...matchingFiles(workspaceTestFileGlob), + ...lists.flatMap(([, entries]) => entries.flatMap(collectedFiles)), + ]); + const orphans = everyTestFile.filter((file) => !collected.has(file)); + expect(orphans, 'test files no pool collects: add them to a list in rstest.integration-tests.ts or delete them').toEqual([]); +}); diff --git a/packages/agent-bundle/tests/uninstall.test.ts b/packages/agent-bundle/tests/uninstall.test.ts index 6166b339b..4b931f246 100644 --- a/packages/agent-bundle/tests/uninstall.test.ts +++ b/packages/agent-bundle/tests/uninstall.test.ts @@ -1249,7 +1249,6 @@ it('rejects an uninstall mode for hosts other than Cursor before touching anythi it('exposes uninstall through the public CLI with every lifecycle flag', async () => { const terminal = captureCliTerminal(); const calls: unknown[] = []; - Object.defineProperty(globalThis, '__AGENT_BUNDLE_VERSION__', { configurable: true, value: 'test' }); const result: UninstallResult = { bundleRoot: '/tmp/example bundle', data: { detail: 'kept', outcome: 'kept', paths: ['/tmp/example bundle/state'], policy: 'keep' }, diff --git a/packages/workbench/tests/runtime-playground.browser.test.tsx b/packages/workbench/tests/runtime-playground.browser.test.tsx deleted file mode 100644 index da0f7650e..000000000 --- a/packages/workbench/tests/runtime-playground.browser.test.tsx +++ /dev/null @@ -1,256 +0,0 @@ -import { page } from '@rstest/browser'; -import { render } from '@rstest/browser-react'; -import { expect, test } from '@rstest/core'; -import React from 'react'; - -import type { - DevRuntimeInvocationRequest, - DevRuntimeReplayRequest, - DevRuntimeRun, - DevRuntimeStateIdentity, - DevRuntimeStateResetRequest, - DevRuntimeStatus, - DevRuntimeSurface, -} from '../../agent-bundle/src/dev/runtime-protocol.ts'; -import type { ProjectEventMessage } from '../../agent-bundle/src/dev/types.ts'; -import type { AgentRenderEvent } from '../src/runtime/agent-document-client.ts'; -import { createRuntimePlaygroundController, RuntimePlayground, type RuntimePlaygroundClient } from '../src/runtime-playground.tsx'; -import type { RuntimeProfileOption } from '../src/runtime-model.ts'; -import type { RuntimeBootstrap } from '../src/runtime-client.ts'; - -const vector = Object.freeze({ - providerSessionId: 'browser-provider', - runtimeGenerationId: 'browser-generation', - sourceRevision: 'browser-source', - stateStoreId: 'browser-state', - stateVersion: 1, -}); - -const status = Object.freeze({ - activeVector: vector, - descriptor: Object.freeze({ environmentVariables: [], id: 'rsc', label: 'RSC Runtime', schemaVersion: 1 as const }), - diagnostics: Object.freeze([]), - hmrReady: true, - lastGoodVector: vector, - state: 'active' as const, -}) satisfies DevRuntimeStatus; - -const surface = Object.freeze({ - defaultTarget: 'portable', - fixtures: Object.freeze([{ id: 'browser-fixture', label: 'Browser fixture' }]), - id: 'mcp.browser', - inputSchema: Object.freeze({ - properties: Object.freeze({ city: Object.freeze({ title: 'City', type: 'string' as const }) }), - required: Object.freeze(['city']), - type: 'object' as const, - }), - kind: 'mcp-tool' as const, - label: 'Browser tool', - readOnly: false, - targets: Object.freeze(['portable']), -}) satisfies DevRuntimeSurface; - -const run = (id: string): DevRuntimeRun => Object.freeze({ - completedAt: '2026-08-15T12:00:01.000Z', - fixtureId: 'browser-fixture', - id, - input: Object.freeze({ city: 'London' }), - result: Object.freeze({ - agentVisible: Object.freeze({ city: 'London' }), - state: Object.freeze({ identity: Object.freeze({ stateStoreId: 'browser-state', stateVersion: 1 }) }), - trace: Object.freeze([]), - tree: Object.freeze([]), - }), - startedAt: '2026-08-15T12:00:00.000Z', - status: 'succeeded', - surfaceId: 'mcp.browser', - target: 'portable', - vector, -}); - -const evidenceRun = (id: string): DevRuntimeRun => Object.freeze({ - ...run(id), - result: Object.freeze({ - agentVisible: Object.freeze({ city: 'London' }), - flight: Object.freeze({ bytes: 2, downloadPath: `/api/runtime/runs/${id}/flight`, preview: 'FL', truncated: false }), - state: Object.freeze({ identity: Object.freeze({ stateStoreId: 'browser-state', stateVersion: 1 }) }), - trace: Object.freeze([Object.freeze({ - details: Object.freeze({ step: 'render' }), - id: 'span-a', - phase: 'render', - startedAt: '2026-08-15T12:00:00.500Z', - status: 'succeeded' as const, - })]), - tree: Object.freeze([]), - }), -}) as DevRuntimeRun; - -const profiles = Object.freeze([{ - claimsRealHostParity: false, - evidence: 'simulated', - id: 'portable', - label: 'Portable MCP Apps', - version: 'agent-bundle:mcp-apps:2026-01-26', -}] satisfies readonly RuntimeProfileOption[]); - -const bootstrap = (): RuntimeBootstrap => Object.freeze({ - history: Object.freeze([run('initial')]), - kind: 'available' as const, - providerSessionId: vector.providerSessionId, - status, - surfaces: Object.freeze([surface]), -}); - -const generationFailedEvent = Object.freeze({ - occurredAt: '2026-08-15T12:00:00.000Z', - payload: Object.freeze({ - providerSessionId: vector.providerSessionId, - runtimeGenerationId: vector.runtimeGenerationId, - type: 'runtime.generation.failed' as const, - }), - sequence: 1, - type: 'runtime.event' as const, -}) satisfies ProjectEventMessage; - -const client = (resetState: () => Promise): RuntimePlaygroundClient => ({ - bootstrap: async () => bootstrap(), - createRun: async (_request: DevRuntimeInvocationRequest) => run('created'), - readRun: async (id) => run(id), - readRunDocument: async () => [], - readRunFlight: async () => new Blob(['flight'], { type: 'application/octet-stream' }), - replayRun: async (_request: DevRuntimeReplayRequest) => run('replayed'), - resetState: async (_request: DevRuntimeStateResetRequest) => resetState(), -}); - -test('mounts Runtime controls in a supported browser and fences reset interactions through its correlated success', { timeout: 15_000 }, async () => { - let resetAttempts = 0; - const controller = createRuntimePlaygroundController({ - bootstrap: bootstrap(), - client: client(async () => { - resetAttempts += 1; - if (resetAttempts === 1) throw new Error('The provider rejected this reset.'); - return Object.freeze({ stateStoreId: 'browser-state', stateVersion: 2 }); - }), - profiles, - }); - try { - await render(); - await expect.element(page.getByRole('heading', { name: 'Runtime Playground' })).toBeVisible(); - await expect.element(page.locator('[data-runtime-run-id="initial"]')).toBeVisible(); - controller.dispatch({ event: generationFailedEvent, type: 'event.received' }); - await expect.element(page.locator('.runtime-announcement[role="alert"]')).toHaveText('Runtime generation failed. The last good result remains available.'); - await expect.element(page.getByRole('tab', { name: 'Result', exact: true })).toHaveAttribute('aria-selected', 'true'); - await expect.element(page.getByLabel('Runtime output stage')).toContainText('Agent-visible output'); - - await page.getByRole('radio', { name: 'Raw JSON' }).click(); - const raw = page.locator('#runtime-input-raw'); - await raw.fill('{"city":'); - await expect.element(page.locator('#runtime-input-raw-error')).toHaveText('Draft JSON is invalid. Repair the raw input before running.'); - await raw.fill('{"city":"Paris"}'); - await page.getByRole('button', { name: 'Run', exact: true }).click(); - await expect.element(page.getByRole('dialog')).toBeVisible(); - await expect.element(page.getByRole('button', { name: 'Run', exact: true })).toBeDisabled(); - await expect.element(raw).toBeDisabled(); - await expect.element(page.getByRole('button', { name: 'Replay exact' })).toBeDisabled(); - await page.getByRole('button', { name: 'Cancel' }).click(); - await expect.element(page.getByRole('dialog')).not.toBeVisible(); - - await page.getByRole('button', { name: 'Reset fixture state' }).click(); - await expect.element(page.getByRole('dialog')).toHaveText(/State store.*browser-state/su); - await expect.element(page.getByRole('button', { name: 'Reset fixture state' })).toBeDisabled(); - await expect.element(page.getByLabel('Runtime surface')).toBeDisabled(); - await page.getByRole('button', { name: 'Confirm' }).click(); - const failure = page.locator('.runtime-request-error'); - await expect.element(failure).toHaveText('The provider rejected this reset.'); - await expect.element(failure).toBeFocused(); - await expect.element(page.locator('.runtime-status')).not.toBeFocused(); - await expect.element(page.getByRole('button', { name: 'Run', exact: true })).toBeEnabled(); - controller.dispatch({ tab: 'tree', type: 'selection.tab' }); - await expect.element(failure).toBeFocused(); - - await page.getByRole('button', { name: 'Reset fixture state' }).click(); - await page.getByRole('button', { name: 'Confirm' }).click(); - await expect.element(failure).not.toBeVisible(); - await expect.element(page.getByText('2', { exact: true })).toBeVisible(); - expect(controller.model.resetCompletion?.state).toEqual({ stateStoreId: 'browser-state', stateVersion: 2 }); - expect(controller.model.activeEffect).toBeUndefined(); - expect((document.activeElement as HTMLElement | null)?.className).toBe('runtime-status'); - await expect.element(page.locator('.runtime-status')).toBeFocused(); - controller.dispatch({ tab: 'diagnostics', type: 'selection.tab' }); - await expect.element(page.locator('.runtime-status')).toBeFocused(); - } finally { - controller.close(); - } -}); - -test('downloads the selected Flight payload through the authenticated client and toggles trace span details', { timeout: 15_000 }, async () => { - const documentRequests: string[] = []; - const flightRequests: string[] = []; - let rejectDownload = true; - const controller = createRuntimePlaygroundController({ - bootstrap: Object.freeze({ - history: Object.freeze([evidenceRun('evidence')]), - kind: 'available' as const, - providerSessionId: vector.providerSessionId, - status, - surfaces: Object.freeze([surface]), - }), - client: { - ...client(async () => Object.freeze({ stateStoreId: 'browser-state', stateVersion: 2 })), - readRunDocument: async (id) => { - documentRequests.push(id); - const document = { - root: { - children: [{ kind: 'markdown' as const, text: '# Browser document' }], - kind: 'result' as const, - }, - status: 'success' as const, - version: 1 as const, - }; - return [ - { document, sequence: 0, type: 'shell' as const }, - { completed: 1, message: 'Rendered', sequence: 1, total: 1, type: 'progress' as const }, - { document, sequence: 2, type: 'complete' as const }, - ] satisfies readonly AgentRenderEvent[]; - }, - readRunFlight: async (id) => { - flightRequests.push(id); - if (rejectDownload) throw new Error('The Flight payload is unavailable.'); - return new Blob(['FL'], { type: 'application/octet-stream' }); - }, - }, - profiles, - }); - try { - await render(); - await expect.element(page.getByRole('heading', { name: 'Runtime Playground' })).toBeVisible(); - - await page.getByRole('tab', { name: 'Flight', exact: true }).click(); - const download = page.getByRole('button', { name: 'Download Flight payload' }); - await expect.element(download).toBeVisible(); - await download.click(); - await expect.element(page.locator('.runtime-request-error[role="alert"]')).toHaveText('The Flight payload is unavailable.'); - rejectDownload = false; - await download.click(); - await expect.element(page.locator('.runtime-request-error[role="alert"]')).not.toBeVisible(); - expect(flightRequests).toEqual(['evidence', 'evidence']); - - await page.getByRole('tab', { name: 'Document', exact: true }).click(); - await expect.element(page.getByRole('heading', { name: 'Browser document' })).toBeVisible(); - await expect.element(page.getByLabel('Agent Document')).toContainText('Rendered · 1 / 1'); - await expect.element(page.getByLabel('Agent Document')).toContainText('Version 1 · success'); - expect(documentRequests).toEqual(['evidence']); - - await page.getByRole('tab', { name: 'Diagnostics', exact: true }).click(); - const toggle = page.getByRole('button', { name: 'Show span details' }); - await expect.element(toggle).toBeVisible(); - await expect.element(page.getByLabel('Runtime render trace')).not.toContainText('"step": "render"'); - await toggle.click(); - await expect.element(page.getByRole('button', { name: 'Hide span details' })).toHaveAttribute('aria-expanded', 'true'); - await expect.element(page.getByLabel('Runtime render trace')).toContainText('"step": "render"'); - await page.getByRole('button', { name: 'Hide span details' }).click(); - await expect.element(page.getByLabel('Runtime render trace')).not.toContainText('"step": "render"'); - } finally { - controller.close(); - } -}); diff --git a/rstest.config.ts b/rstest.config.ts index 90a4f494b..789d74f94 100644 --- a/rstest.config.ts +++ b/rstest.config.ts @@ -6,14 +6,13 @@ import { projectionTestFiles, routeUnitTestFiles, templateTestFiles, + workspaceTestFileGlob, } from './rstest.integration-tests.ts'; import { withAgentBundleRslibConfig } from './rstest.rslib.ts'; export default defineConfig({ extends: withAgentBundleRslibConfig(), - include: [ - 'packages/**/tests/**/*.test.ts', - ], + include: [workspaceTestFileGlob], exclude: [ ...fixtureProjectTestFiles, ...mcpConformanceTestFiles, diff --git a/rstest.integration-tests.ts b/rstest.integration-tests.ts index c6b3a3db8..d1baeda6f 100644 --- a/rstest.integration-tests.ts +++ b/rstest.integration-tests.ts @@ -1,3 +1,12 @@ +/** + * What the workspace pools collect before the lists below subtract from it: + * `rstest.unit.config.ts` runs this minus every list; `rstest.config.ts` (the + * whole-workspace run) minus the lists that need their own process shape. A + * test file this glob does not match (another extension, another directory) + * runs only if a list names it — rstest-pool-lists.test.ts enforces that. + */ +export const workspaceTestFileGlob = 'packages/**/tests/**/*.test.ts'; + /** * Test files that run real builds (Rslib/Rsbuild), spawn child processes, or * drive a browser. They run through rstest.integration.config.ts: per-test diff --git a/rstest.projection.config.ts b/rstest.projection.config.ts index 76d47d9c6..aa0a2efbb 100644 --- a/rstest.projection.config.ts +++ b/rstest.projection.config.ts @@ -3,6 +3,7 @@ import { resolve } from 'node:path'; import { defineConfig } from '@rstest/core'; import { agentBundleRstest } from './packages/agent-bundle/src/rstest/index.ts'; +import { rstestHygiene } from './rstest.rslib.ts'; /** * The repository's in-process projection pool (#103 stage 2): the @@ -13,8 +14,13 @@ import { agentBundleRstest } from './packages/agent-bundle/src/rstest/index.ts'; * * Neither level opens a process. The `packed-stdio` level lives in the packed * pool (`pnpm test:packed`), which owns the run's single build and pack. + * Per-test restoration comes from the workspace's shared policy, as in the + * route-unit pool. */ -export default defineConfig(await agentBundleRstest({ - include: ['packages/agent-bundle/tests/projection/**/*.test.ts'], - root: resolve(import.meta.dirname, 'packages/agent-bundle/fixtures/route-harness'), -})); +export default defineConfig({ + ...(await agentBundleRstest({ + include: ['packages/agent-bundle/tests/projection/**/*.test.ts'], + root: resolve(import.meta.dirname, 'packages/agent-bundle/fixtures/route-harness'), + })), + ...rstestHygiene, +}); diff --git a/rstest.route-unit.config.ts b/rstest.route-unit.config.ts index 9bcf886ca..734f011fe 100644 --- a/rstest.route-unit.config.ts +++ b/rstest.route-unit.config.ts @@ -3,15 +3,20 @@ import { resolve } from 'node:path'; import { defineConfig } from '@rstest/core'; import { agentBundleRstest } from './packages/agent-bundle/src/rstest/index.ts'; +import { rstestHygiene } from './rstest.rslib.ts'; /** * The repository's own route-unit pool, built from the consumer configuration * helper so the shipped surface is what CI exercises. The route-unit level * needs the `react-server` Node condition, which is a pool-level process * flag — that is why it is a separate run from `rstest.unit.config.ts` and not - * a project inside it. + * a project inside it. The shipped helper carries no per-test restoration + * policy (that is the consumer's call), so the workspace pool adds its own. */ -export default defineConfig(await agentBundleRstest({ - include: ['packages/agent-bundle/tests/route-unit/**/*.test.ts'], - root: resolve(import.meta.dirname, 'packages/agent-bundle/fixtures/route-harness'), -})); +export default defineConfig({ + ...(await agentBundleRstest({ + include: ['packages/agent-bundle/tests/route-unit/**/*.test.ts'], + root: resolve(import.meta.dirname, 'packages/agent-bundle/fixtures/route-harness'), + })), + ...rstestHygiene, +}); diff --git a/rstest.rslib.ts b/rstest.rslib.ts index 70f095ec4..e8da54a3b 100644 --- a/rstest.rslib.ts +++ b/rstest.rslib.ts @@ -1,18 +1,88 @@ import { resolve } from 'node:path'; import { withRslibConfig } from '@rstest/adapter-rslib'; +import type { ExtendConfig, ExtendConfigFn } from '@rstest/core'; const workspaceRoot = import.meta.dirname; const packageRoot = resolve(workspaceRoot, 'packages/agent-bundle'); -export const withAgentBundleRslibConfig = () => withRslibConfig({ - cwd: packageRoot, - modifyLibConfig: (config) => ({ - ...config, - root: workspaceRoot, - source: { - ...config.source, - tsconfigPath: resolve(workspaceRoot, 'tsconfig.json'), +/** + * Plugins `packages/agent-bundle/rslib.config.ts` registers for publishing + * the package, not for compiling its modules. The adapter copies the lib + * config's `plugins` into every pool verbatim, so without this filter both + * would run inside each test bundle: + * + * - `plugin-publint` (rsbuild-plugin-publint, `throwOn: 'warning'`) audits + * the manifest at `api.context.rootPath` in `onAfterBuild` — under the + * pools that would be the workspace root's private `package.json`, and a + * test build has no manifest to gate. Rstest 0.11 never drives + * `onAfterBuild` (DEBUG=rstest shows no publint output), so today the + * registration is inert; it stays out so a runner change cannot arm it. + * - `agent-bundle:esm-node-globals` prepends the `__filename`/`__dirname` + * shim to emitted chunks that inline the TypeScript 5 parser. Its + * `processAssets` scan did run over every test chunk. Pools leave + * dependencies external, and Rstest supplies the real `__dirname` and + * `__filename` of each test module itself. + * + * Verify the names against the plugin objects, not the package names: + * `pluginPublint().name` is `plugin-publint`. + */ +const publishOnlyPlugins: ReadonlySet = new Set(['plugin-publint', 'agent-bundle:esm-node-globals']); + +/** The `name` of an Rsbuild plugin entry; nested arrays, promises, and falsy entries have none. */ +const pluginName = (plugin: unknown): string | undefined => ( + typeof plugin === 'object' && plugin !== null && 'name' in plugin && typeof plugin.name === 'string' + ? plugin.name + : undefined +); + +/** + * Per-test restoration shared by every pool. The unit pool runs with + * `isolate: false`, so a mock, `rs.stubEnv`, or `rs.stubGlobal` a test leaves + * behind is the next file's problem; restoring before each test makes file + * order irrelevant. Rstest dispatches `restoreMocks` ahead of `clearMocks` + * (its `mockRestore` is `mockReset` plus the original implementation, so + * calls are cleared too); `clearMocks` stays listed as the floor should + * `restoreMocks` ever be relaxed. + */ +export const rstestHygiene = { + clearMocks: true, + restoreMocks: true, + unstubEnvs: true, + unstubGlobals: true, +} as const satisfies ExtendConfig; + +/** + * The shared pool configuration: the package's Rslib build config, reduced to + * what compiling tests needs, plus `rstestHygiene`. + * + * Kept from the package build: `source.define` — `__AGENT_BUNDLE_VERSION__` + * in src/cli.ts is a compile-time identifier and resolves here exactly as in + * the published build — and `source.tsconfigPath`, repointed at the workspace + * tsconfig so test files resolve beside the sources. Dropped: the publish-only + * plugins above and `tools.rspack`, whose `ignoreWarnings` entry and + * `node.__dirname = false` exist for the inlined TypeScript parser (external + * in pools; Rstest sets its own `node` options). + */ +export const withAgentBundleRslibConfig = (): ExtendConfigFn => { + const rslib = withRslibConfig({ + cwd: packageRoot, + modifyLibConfig: ({ plugins, tools, ...config }) => { + const { rspack: _publishOnlyRspack, ...testTools } = tools ?? {}; + return { + ...config, + root: workspaceRoot, + plugins: plugins?.filter((plugin) => { + const name = pluginName(plugin); + return name === undefined || !publishOnlyPlugins.has(name); + }), + source: { + ...config.source, + tsconfigPath: resolve(workspaceRoot, 'tsconfig.json'), + }, + tools: testTools, + }; }, - }), -}); + }); + return async (userConfig) => ({ ...(await rslib(userConfig)), ...rstestHygiene }); +}; diff --git a/rstest.runtime-playground.browser.config.ts b/rstest.runtime-playground.browser.config.ts deleted file mode 100644 index c066ce0c4..000000000 --- a/rstest.runtime-playground.browser.config.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { realpathSync } from 'node:fs'; -import { resolve } from 'node:path'; - -import { pluginReact } from '@rsbuild/plugin-react'; -import { withRslibConfig } from '@rstest/adapter-rslib'; -import { defineConfig } from '@rstest/core'; - -const browserReactRoot = realpathSync(resolve('node_modules/react')); -const browserReactDomRoot = realpathSync(resolve('node_modules/react-dom')); - -export default defineConfig({ - browser: { - enabled: true, - headless: true, - provider: 'playwright', - providerOptions: { launch: { channel: 'chrome' } }, - viewport: { height: 900, width: 1440 }, - }, - extends: withRslibConfig(), - include: ['packages/workbench/tests/runtime-playground.browser.test.tsx'], - plugins: [pluginReact()], - setupFiles: ['./rstest.setup.browser.ts'], - resolve: { - alias: { - react: browserReactRoot, - 'react-dom': browserReactDomRoot, - }, - }, - tools: { - rspack: { - resolve: { - extensionAlias: { '.js': ['.js', '.ts', '.tsx'], '.jsx': ['.jsx', '.tsx'] }, - }, - }, - }, -}); diff --git a/rstest.runtime-playground.config.ts b/rstest.runtime-playground.config.ts deleted file mode 100644 index bccb49df8..000000000 --- a/rstest.runtime-playground.config.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { realpathSync } from 'node:fs'; -import { resolve } from 'node:path'; - -import { pluginReact } from '@rsbuild/plugin-react'; -import { withRslibConfig } from '@rstest/adapter-rslib'; -import { defineConfig, defineInlineProject } from '@rstest/core'; - -const browserReactRoot = realpathSync(resolve('node_modules/react')); -const browserReactDomRoot = realpathSync(resolve('node_modules/react-dom')); - -export default defineConfig({ - coverage: { - enabled: true, - include: ['packages/workbench/src/runtime-{client,model,playground}.{ts,tsx}'], - provider: 'v8', - reporters: ['text', 'json'], - thresholds: { branches: 85, functions: 90, lines: 90, statements: 90 }, - }, - projects: [ - defineInlineProject({ - extends: withRslibConfig(), - include: [ - 'packages/workbench/tests/runtime-client.test.ts', - 'packages/workbench/tests/runtime-contract-compile.test.ts', - 'packages/workbench/tests/runtime-model.test.ts', - 'packages/workbench/tests/runtime-playground.test.ts', - ], - name: 'runtime-node', - setupFiles: ['./rstest.setup.ts'], - testEnvironment: 'node', - }), - defineInlineProject({ - browser: { - enabled: true, - headless: true, - provider: 'playwright', - providerOptions: { launch: { channel: 'chrome' } }, - viewport: { height: 900, width: 1440 }, - }, - extends: withRslibConfig(), - include: ['packages/workbench/tests/runtime-playground.browser.test.tsx'], - name: 'runtime-browser', - setupFiles: ['./rstest.setup.browser.ts'], - plugins: [pluginReact()], - resolve: { - alias: { - react: browserReactRoot, - 'react-dom': browserReactDomRoot, - }, - }, - tools: { - rspack: { - resolve: { - extensionAlias: { '.js': ['.js', '.ts', '.tsx'], '.jsx': ['.jsx', '.tsx'] }, - }, - }, - }, - }), - ], -}); diff --git a/rstest.setup.browser.ts b/rstest.setup.browser.ts deleted file mode 100644 index 064dae7a4..000000000 --- a/rstest.setup.browser.ts +++ /dev/null @@ -1,5 +0,0 @@ -// Browser pools bundle setup files into the page bundle, where node: builtins -// are an unhandled scheme. Worker isolation (rstest.setup.ts) redirects -// TMPDIR/XDG caches for Node test processes and has no browser equivalent, so -// browser projects load this empty setup instead. -export {}; diff --git a/rstest.unit.config.ts b/rstest.unit.config.ts index 763ec30b4..e966da3a4 100644 --- a/rstest.unit.config.ts +++ b/rstest.unit.config.ts @@ -10,15 +10,14 @@ import { projectionTestFiles, routeUnitTestFiles, templateTestFiles, + workspaceTestFileGlob, } 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. */ export default defineConfig({ extends: withAgentBundleRslibConfig(), - include: [ - 'packages/**/tests/**/*.test.ts', - ], + include: [workspaceTestFileGlob], exclude: [ ...fixtureProjectTestFiles, ...integrationTestFiles, From 8ee774ccae2d3a0511ef520cb11c35b869d80328 Mon Sep 17 00:00:00 2001 From: ScriptedAlchemy Date: Sat, 5 Sep 2026 00:32:26 +0000 Subject: [PATCH 2/5] test: make pool-list guard match Rstest's view; drop orphaned browser devDependencies - rstest-pool-lists.test.ts: normalize separators (Windows), prune what Rstest's default exclude prunes, and forbid test files under hidden paths so fs.globSync (no dot support) and Rstest (dot: true) agree. - Remove @rstest/browser and @rstest/browser-react from the root devDependencies: their only consumer was the deleted browser-mode runtime-playground config. --- package.json | 2 - .../tests/rstest-pool-lists.test.ts | 40 ++++++++++++++++--- pnpm-lock.yaml | 20 ---------- 3 files changed, 34 insertions(+), 28 deletions(-) diff --git a/package.json b/package.json index 36d6b6c22..413002b8e 100644 --- a/package.json +++ b/package.json @@ -70,8 +70,6 @@ "@rslib/core": "0.23.2", "@rslint/core": "0.8.2", "@rstest/adapter-rslib": "0.11.10", - "@rstest/browser": "0.11.10", - "@rstest/browser-react": "0.11.10", "@rstest/core": "0.11.10", "@rstest/playwright": "0.11.10", "@types/node": "26.4.0", diff --git a/packages/agent-bundle/tests/rstest-pool-lists.test.ts b/packages/agent-bundle/tests/rstest-pool-lists.test.ts index 336f94a7c..588e12e55 100644 --- a/packages/agent-bundle/tests/rstest-pool-lists.test.ts +++ b/packages/agent-bundle/tests/rstest-pool-lists.test.ts @@ -1,5 +1,5 @@ -import { globSync, statSync } from 'node:fs'; -import { dirname, resolve } from 'node:path'; +import { globSync, readdirSync, statSync } from 'node:fs'; +import { dirname, join, relative, resolve, sep } from 'node:path'; import { fileURLToPath } from 'node:url'; import { describe, expect, it } from '@rstest/core'; @@ -18,8 +18,13 @@ import * as poolLists from '../../../rstest.integration-tests.ts'; */ const workspaceRoot = resolve(dirname(fileURLToPath(import.meta.url)), '../../..'); +/** Workspace-relative, `/`-separated, whatever the platform hands back. */ +const toPosix = (path: string): string => path.split(sep).join('/'); + const isGlob = (entry: string): boolean => /[*?[\]{}]/u.test(entry); +const isTestFileName = (name: string): boolean => /\.(?:test|spec)\.[cm]?[jt]sx?$/u.test(name); + const isFile = (entry: string): boolean => { try { return statSync(resolve(workspaceRoot, entry)).isFile(); @@ -28,17 +33,36 @@ const isFile = (entry: string): boolean => { } }; -/** Rstest's default exclusions, so a dependency's or build output's tests never count. */ -const isBuildOrDependencyPath = (path: string): boolean => /(?:^|\/)(?:node_modules|dist)(?:\/|$)/u.test(path); +/** What Rstest's default `exclude` prunes, so a dependency's or build output's tests never count. */ +const prunedSegments: ReadonlySet = new Set(['node_modules', 'dist', '.idea', '.git', '.cache', '.output', '.temp']); + +const isPrunedPath = (path: string): boolean => toPosix(path).split('/').some((segment) => prunedSegments.has(segment)); const matchingFiles = (pattern: string): readonly string[] => globSync(pattern, { cwd: workspaceRoot, - exclude: (path: string) => isBuildOrDependencyPath(path), -}).filter(isFile).sort(); + exclude: (path: string) => isPrunedPath(path), +}).map(toPosix).filter(isFile).sort(); /** Files a pool entry collects: the entry itself when literal, its matches when a glob. */ const collectedFiles = (entry: string): readonly string[] => (isGlob(entry) ? matchingFiles(entry) : [entry]); +/** + * Rstest globs with `dot: true`; Node's `fs.globSync` never enters or returns + * a dot path and has no option to. The two views agree exactly as long as no + * test file lives under a hidden path, so this walk — which does see dot + * paths, and prunes what Rstest prunes — reports every test file that breaks + * the agreement. + */ +const hiddenTestFiles = (directory: string): readonly string[] => readdirSync(directory, { withFileTypes: true }) + .flatMap((entry) => { + if (prunedSegments.has(entry.name)) return []; + const path = join(directory, entry.name); + if (entry.isDirectory()) return hiddenTestFiles(path); + if (!entry.isFile() || !isTestFileName(entry.name)) return []; + const relativePath = toPosix(relative(workspaceRoot, path)); + return relativePath.split('/').some((segment) => segment.startsWith('.')) ? [relativePath] : []; + }); + const { workspaceTestFileGlob, ...exportedLists } = poolLists; const lists = Object.entries(exportedLists) as ReadonlyArray; @@ -52,6 +76,10 @@ it('exports the include glob plus non-empty string lists, so every pool below is } }); +it('keeps every test file on a visible path, so the glob checks below see what Rstest collects', () => { + expect(hiddenTestFiles(join(workspaceRoot, 'packages')), 'test files under a hidden path (Rstest collects them; this guard cannot)').toEqual([]); +}); + describe.each(lists)('%s', (name, entries) => { it('lists only files that exist (a moved or deleted test leaves a stale entry)', () => { const missing = entries.filter((entry) => !isGlob(entry) && !isFile(entry)); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c6efd6094..23682ff09 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -45,12 +45,6 @@ importers: '@rstest/adapter-rslib': specifier: 0.11.10 version: 0.11.10(@rslib/core@0.23.2(typescript@7.0.2))(@rstest/core@0.11.10)(typescript@7.0.2) - '@rstest/browser': - specifier: 0.11.10 - version: 0.11.10(@rstest/core@0.11.10)(playwright@1.62.1) - '@rstest/browser-react': - specifier: 0.11.10 - version: 0.11.10(@rstest/core@0.11.10)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) '@rstest/core': specifier: 0.11.10 version: 0.11.10 @@ -1363,14 +1357,6 @@ packages: typescript: optional: true - '@rstest/browser-react@0.11.10': - resolution: {integrity: sha512-LFMjeUfMmfM2HnEk/5YHGXNXLMQCwKdA0fHcNzQztYYeTVb9FB0SPwv6FrqduNXZUAyNyHryxS2TO4jDYaYWvg==} - engines: {node: ^20.19.0 || >=22.12.0} - peerDependencies: - '@rstest/core': ^0.11.0 - react: ^18.0.0 || ^19.0.0 - react-dom: ^18.0.0 || ^19.0.0 - '@rstest/browser@0.11.10': resolution: {integrity: sha512-Ic9QD8uA2aDaUaFeDgXZoEwEJerV4pwcHo2113hS/UfgpdIgi7n1ygG/tsrRUCzHPDPL3JrN+YYMcntDACAxow==} engines: {node: ^20.19.0 || >=22.12.0} @@ -4269,12 +4255,6 @@ snapshots: optionalDependencies: typescript: 7.0.2 - '@rstest/browser-react@0.11.10(@rstest/core@0.11.10)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': - dependencies: - '@rstest/core': 0.11.10 - react: 19.2.8 - react-dom: 19.2.8(react@19.2.8) - '@rstest/browser@0.11.10(@rstest/core@0.11.10)(playwright@1.62.1)': dependencies: '@jridgewell/trace-mapping': 0.3.31 From c4160395c3645fca8d6d417167fac0e35a6f37da Mon Sep 17 00:00:00 2001 From: ScriptedAlchemy Date: Sat, 5 Sep 2026 00:47:21 +0000 Subject: [PATCH 3/5] test: orphan guard accepts every Rstest test extension; drop stale citations of the deleted browser test - rstest-pool-lists.test.ts: derive the orphan set from *.{test,spec}.* filtered by the same regex Rstest's default include accepts, so .mtsx/.ctsx/.mjsx/.cjsx files are no longer invisible to the guard. - docs: regenerate the RSC runtime topology (pnpm docs:runtime-topology; it was already stale on main) and fix the fidelity-ledger citation. --- docs/architecture/rsc-runtime-workbench.md | 13 ++++++++++++- .../assets/rsc-runtime-workbench/fidelity-ledger.md | 2 +- .../agent-bundle/tests/rstest-pool-lists.test.ts | 5 +++-- 3 files changed, 16 insertions(+), 4 deletions(-) diff --git a/docs/architecture/rsc-runtime-workbench.md b/docs/architecture/rsc-runtime-workbench.md index f54e5884b..bd51a0547 100644 --- a/docs/architecture/rsc-runtime-workbench.md +++ b/docs/architecture/rsc-runtime-workbench.md @@ -49,6 +49,13 @@ packages/ src/dev/runtime-routes.ts src/dev/workbench-server.ts src/index.ts + tests/claude-hook-event-name.test.ts + tests/claude-hooks-schema.test.ts + tests/claude-plugin-validate-acceptance.test.ts + tests/claude-plugin-validation.test.ts + tests/codex-distribution.test.ts + tests/codex-hook-contract.test.ts + tests/codex-plugin-validation.test.ts tests/dev-artifact-service.test.ts tests/dev-workbench-packaging.test.ts tests/dev-workbench.test.ts @@ -68,6 +75,7 @@ packages/ tests/normalization.test.ts tests/playground-service.test.ts tests/portable-adapter.test.ts + tests/portable-plugin-validation.test.ts tests/public-api-packed.test.ts tests/public-api.test.ts tests/rsc-runtime-optional-packaging.test.ts @@ -114,12 +122,13 @@ packages/ tests/runtime-consent-dialog.test.ts tests/runtime-consent-queue.test.ts tests/runtime-contract-compile.test.ts + tests/runtime-document-atoms-disposal.test.ts tests/runtime-inspector.test.ts tests/runtime-mcp-handoff.test.ts tests/runtime-model.test.ts + tests/runtime-playground-capture-cleanup.test.ts tests/runtime-playground-capture.test.ts tests/runtime-playground-hmr.e2e.test.ts - tests/runtime-playground.browser.test.tsx tests/runtime-playground.e2e.test.ts tests/runtime-playground.test.ts tests/runtime-stage.test.ts @@ -134,6 +143,7 @@ examples/ src/build/emit-artifacts.ts src/build/serialize-definition.ts src/definition.ts + src/dev/canonical-json.ts src/dev/definition-entry.ts src/dev/environment-checkpoint-store.ts src/dev/generation-materializer.ts @@ -144,6 +154,7 @@ examples/ src/dev/serialize-inspection.ts src/flight/request-render.ts src/hook/cli.ts + src/hook/eval-probe.ts src/hook/normalize.ts src/hook/project-document.ts src/mcp/create-server.ts diff --git a/docs/assets/rsc-runtime-workbench/fidelity-ledger.md b/docs/assets/rsc-runtime-workbench/fidelity-ledger.md index 5cbb70533..c41482648 100644 --- a/docs/assets/rsc-runtime-workbench/fidelity-ledger.md +++ b/docs/assets/rsc-runtime-workbench/fidelity-ledger.md @@ -57,7 +57,7 @@ not a pass. | Typography | Compact sans-serif labels; monospace JSON/IDs/trace; small uppercase status tokens | Type hierarchy remains legible at the desktop width; code never depends on color alone | `desktop.png` visibly shows labels and monospace values, but no committed contrast or color-independence audit establishes the entire acceptance statement. | Not visually evidenced — visual hierarchy is shown; formal accessibility evidence is not. | | Palette | White canvas, near-black text, cool gray rules, cobalt active controls, green success, neutral disclaimer | Preserve contrast and restrained developer-tool palette; no gradients or marketing cards | The approved raster set visibly uses the restrained palette, but no committed contrast measurement establishes every stated accessibility property. | Not visually evidenced — visual appearance alone is insufficient for the contrast claim. | | Container model | 1px outlined panels, 4–6px corners, dense 4px rhythm, 10–16px padding/gutters | Panels align to one grid and retain accessible hit areas without card bloat | `desktop.png` shows outlined panels and the committed `styles.css` sizes Runtime controls at a 40px minimum, but no cited evidence establishes the complete grid/padding/no-bloat claim. | Not visually evidenced — only portions of the container acceptance are observed. | -| Focus/selected states | Cobalt outline/tab underline/current chip; selected Form, Tree, history, generation | Focus is visible, selected semantics are announced, and pointer-only state is not used | Keyboard focus restoration and selected states are exercised by `runtime-playground.e2e.test.ts` and `runtime-playground.browser.test.tsx`, but neither captures computed focus appearance or proves color-independent presentation. | Not visually evidenced — implementation behavior exists, but visual focus treatment is not verified. | +| Focus/selected states | Cobalt outline/tab underline/current chip; selected Form, Tree, history, generation | Focus is visible, selected semantics are announced, and pointer-only state is not used | Keyboard focus restoration and selected states are exercised by `runtime-playground.e2e.test.ts`, but it captures neither computed focus appearance nor color-independent presentation. | Not visually evidenced — implementation behavior exists, but visual focus treatment is not verified. | | Success/error states | Green dots/checks and `SUCCESS`; no error shown in the raster | Implement success and phase-labelled pale-red diagnostics; absence of an error in the concept is not evidence that error UI may be omitted | `compile-error.png` visibly records the phase-labelled AB8206 diagnostic and `recovered.png` shows its cleared text; `runtime-playground-hmr.e2e.test.ts` verifies the failure/recovery behavior. Neither cited raster nor test observes computed pale-red diagnostic styling. | Not visually evidenced — diagnostic phase/text behavior is directly observed, but the compound color-treatment acceptance is not. | | Stale/last-good states | Explicit no-stale message plus prior-generation `Last good` identity | Preserve last-good output through compile/run/App failure and distinguish stale from current | `compile-error.png` and `recovered.png`, backed by the capture contract and `runtime-playground-hmr.e2e.test.ts`, prove retention then recovery. | Verified | | 1100px continuation | Concept is native desktop only; approved continuation moves Inspector below stage | No unreadable third column; bounded trace scroll; identity labels remain separate | No approved 1100px raster or cited contract run covers this breakpoint. | Not visually evidenced — responsive evidence is limited to 1440×900 and 390×844. | diff --git a/packages/agent-bundle/tests/rstest-pool-lists.test.ts b/packages/agent-bundle/tests/rstest-pool-lists.test.ts index 588e12e55..7cd866e38 100644 --- a/packages/agent-bundle/tests/rstest-pool-lists.test.ts +++ b/packages/agent-bundle/tests/rstest-pool-lists.test.ts @@ -111,8 +111,9 @@ it('assigns every test file to at most one pool', () => { it('leaves no test file without a pool', () => { // Every `.test.ts` the include glob matches has a pool by construction (the // unit pool, unless a list claims it); anything else — another extension, - // another suffix — runs only if a list names it. - const everyTestFile = matchingFiles('packages/**/tests/**/*.{test,spec}.{ts,tsx,mts,cts,js,mjs,cjs,jsx}'); + // another suffix — runs only if a list names it. `isTestFileName` is the + // full set Rstest's default include (`*.{test,spec}.?(c|m)[jt]s?(x)`) accepts. + const everyTestFile = matchingFiles('packages/**/tests/**/*.{test,spec}.*').filter((file) => isTestFileName(file)); const collected = new Set([ ...matchingFiles(workspaceTestFileGlob), ...lists.flatMap(([, entries]) => entries.flatMap(collectedFiles)), From 201840792556fb0cc7e8e39e7357ea09865bf4dc Mon Sep 17 00:00:00 2001 From: ScriptedAlchemy Date: Sat, 5 Sep 2026 01:10:01 +0000 Subject: [PATCH 4/5] test(workbench): port the stranded browser-mode Runtime Playground coverage into pools that run runtime-playground.browser.test.tsx never had a pool. Its reset-fencing scenario was already covered by runtime-playground.e2e.test.ts; the two uncovered interactions move to where they can honestly run: - Flight tab download (client rejects a non-octet-stream response with an alert; the real route yields runtime-run-.flight.bin) -> the real- browser e2e, plus the UI consequence of real spans carrying no details. - Diagnostics span-details toggle (renders only when a span carries details, which no producer sets today) -> the SSR unit test, with a fabricated span. --- .../tests/runtime-playground.e2e.test.ts | 26 ++++++++++++++ .../tests/runtime-playground.test.ts | 34 +++++++++++++++++++ 2 files changed, 60 insertions(+) diff --git a/packages/workbench/tests/runtime-playground.e2e.test.ts b/packages/workbench/tests/runtime-playground.e2e.test.ts index b7987d13d..dadc0978e 100644 --- a/packages/workbench/tests/runtime-playground.e2e.test.ts +++ b/packages/workbench/tests/runtime-playground.e2e.test.ts @@ -165,6 +165,32 @@ e2e('renders the capability-gated Runtime sibling in the real RSC workbench', { await expect(page.getByRole('tabpanel')) .toContainText('Stored Flight could not be decoded as an Agent Document.', { timeout: browserTimeout }); + // The hook run's stored Flight is downloadable from the Flight tab. The + // client rejects any response that is not `application/octet-stream` + // and surfaces the rejection as an alert; the real route then clears the + // alert and hands the viewer a `runtime-run-.flight.bin` download. + await history.nth(1).getByRole('button').first().click(); + await page.getByRole('tab', { name: 'Flight', exact: true }).click(); + const download = page.getByRole('button', { name: 'Download Flight payload' }); + await expect(download).toBeVisible({ timeout: browserTimeout }); + const flightAlert = page.locator('.runtime-request-error[role="alert"]'); + const flightRoute = '**/api/runtime/runs/*/flight'; + await page.route(flightRoute, (route) => route.fulfill({ body: 'nope', contentType: 'text/plain', status: 200 })); + await download.click(); + await expect(flightAlert).toHaveText('Runtime Flight response is not valid.', { timeout: browserTimeout }); + await page.unroute(flightRoute); + const downloadEvent = page.waitForEvent('download'); + await download.click(); + const downloaded = await downloadEvent; + expect(downloaded.suggestedFilename()).toMatch(/^runtime-run-.+\.flight\.bin$/u); + await expect(flightAlert).toHaveCount(0, { timeout: browserTimeout }); + + // Real trace spans carry no `details`, so the Diagnostics tab renders the + // render phases without a span-details disclosure. + await page.getByRole('tab', { name: 'Diagnostics', exact: true }).click(); + await expect(page.getByLabel('Runtime render trace')).toContainText('normalize', { timeout: browserTimeout }); + await expect(page.getByRole('button', { name: /span details/u })).toHaveCount(0); + const reset = page.getByRole('button', { name: 'Reset fixture state' }); const stateVersionBeforeReset = await identity.getAttribute('data-runtime-state-version'); await reset.click(); diff --git a/packages/workbench/tests/runtime-playground.test.ts b/packages/workbench/tests/runtime-playground.test.ts index f225acdbf..4c490d00c 100644 --- a/packages/workbench/tests/runtime-playground.test.ts +++ b/packages/workbench/tests/runtime-playground.test.ts @@ -551,6 +551,40 @@ it('renders previous-provider last-good output separately from session-only runt expect(markup).toContain('"city": "London"'); }); +it('toggles span details for traced spans that carry them and renders no toggle otherwise', async () => { + const traced = run('01', { + result: Object.freeze({ + agentVisible: Object.freeze({ city: 'London' }), + state: Object.freeze({ identity: Object.freeze({ stateStoreId: 'state-a', stateVersion: 1 }) }), + trace: Object.freeze([ + Object.freeze({ details: Object.freeze({ step: 'render' }), id: 'render', phase: 'render', startedAt: '2026-08-15T12:01:00.000Z', status: 'succeeded' as const }), + Object.freeze({ id: 'flight', parentId: 'render', phase: 'flight', startedAt: '2026-08-15T12:01:00.500Z', status: 'succeeded' as const }), + ]), + tree: Object.freeze([]), + }), + }); + const controller = createRuntimePlaygroundController({ bootstrap: bootstrap({ history: Object.freeze([traced]) }), client: clientFor(), profiles }); + controller.dispatch({ tab: 'diagnostics', type: 'selection.tab' }); + const toggles = (markup: string): readonly string[] => markup.match(/']); + expect(collapsed).toContain('
  • flight'); + expect(collapsed).not.toContain('"step": "render"'); + + controller.dispatch({ spanId: 'render', type: 'trace.toggle' }); + const expanded = await renderWhenReady(createElement(RuntimePlayground, { controller })); + expect(controller.model.expandedTraceSpanIds).toEqual(['render']); + expect(toggles(expanded)).toEqual(['']); + expect(expanded).toContain('"step": "render"'); + + controller.dispatch({ spanId: 'render', type: 'trace.toggle' }); + const recollapsed = await renderWhenReady(createElement(RuntimePlayground, { controller })); + expect(controller.model.expandedTraceSpanIds).toEqual([]); + expect(toggles(recollapsed)).toEqual(['']); + expect(recollapsed).not.toContain('"step": "render"'); +}); + it('initializes all provider history items without truncating the server-owned fifty item window', () => { const history = Object.freeze(Array.from({ length: 50 }, (_, index) => run(String(50 - index).padStart(2, '0')))); const controller = createRuntimePlaygroundController({ bootstrap: bootstrap({ history }), client: clientFor(), profiles }); From 398eb3b22ef1688efba02e363b70835434f05927 Mon Sep 17 00:00:00 2001 From: ScriptedAlchemy Date: Sat, 5 Sep 2026 01:19:56 +0000 Subject: [PATCH 5/5] test(workbench): port the rejected-reset recovery path to the e2e; match span toggles order-independently The deleted browser-mode test's first scenario also proved a rejected reset focuses the request alert, hands the controls back, leaves the state version untouched, and lets the next attempt through. The e2e now stubs the first reset response (an invalid state wrapper the client refuses) and asserts exactly that before the real reset it already exercised. --- .../tests/runtime-playground.e2e.test.ts | 37 ++++++++++++++++--- .../tests/runtime-playground.test.ts | 12 ++++-- 2 files changed, 40 insertions(+), 9 deletions(-) diff --git a/packages/workbench/tests/runtime-playground.e2e.test.ts b/packages/workbench/tests/runtime-playground.e2e.test.ts index dadc0978e..f5a3624ba 100644 --- a/packages/workbench/tests/runtime-playground.e2e.test.ts +++ b/packages/workbench/tests/runtime-playground.e2e.test.ts @@ -211,14 +211,41 @@ e2e('renders the capability-gated Runtime sibling in the real RSC workbench', { await expect(confirmation).toBeHidden(); await expect(reset).toBeFocused({ timeout: browserTimeout }); expect(resetRequests).toEqual([]); + const expectedResetRequest = { + expectedGenerationId: runtimeIdentity.activeVector.runtimeGenerationId, + stateStoreId: runtimeIdentity.activeVector.stateStoreId, + }; + + // A rejected reset surfaces as a focused alert, hands the controls back, + // and leaves the state untouched; the next attempt goes through. The + // rejection is the client's own: an invalid state wrapper is refused + // before any provider identity is trusted. + const resetRoute = '**/api/runtime/state/reset'; + await page.route(resetRoute, (route) => route.fulfill({ body: '{}', contentType: 'application/json', status: 200 })); await reset.click(); await expect(confirmation).toContainText('State store'); await confirmation.getByRole('button', { name: 'Confirm' }).click(); - await expect.poll(() => resetRequests).toHaveLength(1); - expect(resetRequests).toEqual([{ - expectedGenerationId: runtimeIdentity.activeVector.runtimeGenerationId, - stateStoreId: runtimeIdentity.activeVector.stateStoreId, - }]); + const resetFailure = page.locator('.runtime-request-error[role="alert"]'); + await expect(resetFailure).toHaveText('Runtime route returned an invalid state wrapper.', { timeout: browserTimeout }); + await expect(resetFailure).toBeFocused({ timeout: browserTimeout }); + await expect(page.locator('.runtime-status')).not.toBeFocused(); + await expect(confirmation).toBeHidden(); + await expect(run).toBeEnabled(); + await expect(reset).toBeEnabled(); + await expect(page.getByLabel('Runtime surface')).toBeEnabled(); + await page.getByRole('tab', { name: 'Tree', exact: true }).click(); + await expect(resetFailure).toBeVisible(); + await expect(page.locator('.runtime-status')).not.toBeFocused(); + await expect(identity).toHaveAttribute('data-runtime-state-version', String(stateVersionBeforeReset)); + await page.unroute(resetRoute); + await expect.poll(() => resetRequests).toEqual([expectedResetRequest]); + + await reset.click(); + await expect(confirmation).toContainText('State store'); + await confirmation.getByRole('button', { name: 'Confirm' }).click(); + await expect.poll(() => resetRequests).toHaveLength(2); + expect(resetRequests).toEqual([expectedResetRequest, expectedResetRequest]); + await expect(resetFailure).toHaveCount(0, { timeout: browserTimeout }); await expect.poll(async () => identity.getAttribute('data-runtime-state-version'), { timeout: browserTimeout }).not.toBe(stateVersionBeforeReset); await expect(page.locator('.runtime-status')).toBeFocused({ timeout: browserTimeout }); diff --git a/packages/workbench/tests/runtime-playground.test.ts b/packages/workbench/tests/runtime-playground.test.ts index 4c490d00c..ab09a2cf6 100644 --- a/packages/workbench/tests/runtime-playground.test.ts +++ b/packages/workbench/tests/runtime-playground.test.ts @@ -565,23 +565,27 @@ it('toggles span details for traced spans that carry them and renders no toggle }); const controller = createRuntimePlaygroundController({ bootstrap: bootstrap({ history: Object.freeze([traced]) }), client: clientFor(), profiles }); controller.dispatch({ tab: 'diagnostics', type: 'selection.tab' }); - const toggles = (markup: string): readonly string[] => markup.match(/']); + expect(toggles(collapsed)).toEqual([{ expanded: 'false', label: 'Show span details' }]); expect(collapsed).toContain('
  • flight'); expect(collapsed).not.toContain('"step": "render"'); controller.dispatch({ spanId: 'render', type: 'trace.toggle' }); const expanded = await renderWhenReady(createElement(RuntimePlayground, { controller })); expect(controller.model.expandedTraceSpanIds).toEqual(['render']); - expect(toggles(expanded)).toEqual(['']); + expect(toggles(expanded)).toEqual([{ expanded: 'true', label: 'Hide span details' }]); expect(expanded).toContain('"step": "render"'); controller.dispatch({ spanId: 'render', type: 'trace.toggle' }); const recollapsed = await renderWhenReady(createElement(RuntimePlayground, { controller })); expect(controller.model.expandedTraceSpanIds).toEqual([]); - expect(toggles(recollapsed)).toEqual(['']); + expect(toggles(recollapsed)).toEqual([{ expanded: 'false', label: 'Show span details' }]); expect(recollapsed).not.toContain('"step": "render"'); });