Skip to content

Commit afa430b

Browse files
fix(test): mount conventional providers for rendered scripts after the G1 rebase
- prepareScriptRenderHost mounts src/providers/* with the script invocation and a fresh process identity per run, as the generated scripts/<name>.mjs render worker does; context.providers substitutes a fixture map - a plain script opens no request scope and now rejects any context - the fixture's tooling-summary rendered script joins the script inventory - the test registry is version 5: main's 4 added providerLoaders, this lane adds manifest.scripts
1 parent d6185c3 commit afa430b

6 files changed

Lines changed: 77 additions & 30 deletions

File tree

‎.changeset/script-dispatch-and-workbench-surface.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,4 @@
33
"create-agent-bundle": patch
44
---
55

6-
Add `runScript`, `scriptJson`, `scriptNdjson`, and `inspectWorkbenchSurface` to `agent-bundle/test`, and move the `cli-tool` template onto the routed CLI. `runScript` (the `script-dispatch` proof level) runs a conventional `src/scripts/*` module through its generated executable's contract: a rendered `.tsx` script through the rendered-script shell with piped Markdown, TTY, `--json`, and `--ndjson` output, a plain `.ts` script as a Node process of its own with the `main` envelope, `process.exit`, exit code, stdout, stderr, optional `stdin`, and the compiled `agent-bundle/meta` identity (no `AB4760` outside a compiled surface); `testManifest().scripts` lists only the compiled scripts that ship — a nested (`AB4808`) or configuration-conflicting (`AB4809`) conventional script is never a `runScript` target — and every failure names the script route, execution form, and proof level. `inspectWorkbenchSurface` (the `workbench-surface` proof level) returns the route manifest, grouped route catalog, state declaration, lifecycle-replay fixtures, and page availability the Workbench would show for a project, without a browser or dev server, and reports `manifest-unavailable` with the compiler's error diagnostics (for example `AB4100`) for a project the compiler rejects. `ScriptRouteProps` types rendered script components. `create-agent-bundle`'s `cli-tool` template replaces the hand-written `src/cli.ts` with a routed `src/cli/greet.ts` command and a conventional `src/scripts/hello.ts`, proved by a generated projection pool at the `cli-dispatch` and `script-dispatch` levels. (#398)
6+
Add `runScript`, `scriptJson`, `scriptNdjson`, and `inspectWorkbenchSurface` to `agent-bundle/test`, and move the `cli-tool` template onto the routed CLI. `runScript` (the `script-dispatch` proof level) runs a conventional `src/scripts/*` module through its generated executable's contract: a rendered `.tsx` script through the rendered-script shell with piped Markdown, TTY, `--json`, and `--ndjson` output and the project's conventional `src/providers/*` mounted with the `script` invocation, a plain `.ts` script as a Node process of its own with the `main` envelope, `process.exit`, exit code, stdout, stderr, optional `stdin`, and the compiled `agent-bundle/meta` identity (no `AB4760` outside a compiled surface); `testManifest().scripts` lists only the compiled scripts that ship — a nested (`AB4808`) or configuration-conflicting (`AB4809`) conventional script is never a `runScript` target — and every failure names the script route, execution form, and proof level. `inspectWorkbenchSurface` (the `workbench-surface` proof level) returns the route manifest, grouped route catalog, state declaration, lifecycle-replay fixtures, and page availability the Workbench would show for a project, without a browser or dev server, and reports `manifest-unavailable` with the compiler's error diagnostics (for example `AB4100`) for a project the compiler rejects. `ScriptRouteProps` types rendered script components. `create-agent-bundle`'s `cli-tool` template replaces the hand-written `src/cli.ts` with a routed `src/cli/greet.ts` command and a conventional `src/scripts/hello.ts`, proved by a generated projection pool at the `cli-dispatch` and `script-dispatch` levels. (#398)

‎packages/agent-bundle/README.md‎

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -420,13 +420,15 @@ kind, and the module provenance.
420420
Conventional request context providers (`src/providers/*`, see
421421
[entry conventions](../../docs/entry-conventions.md#request-context-providers-power-tier))
422422
are mounted automatically for every manifest-backed helper — `renderRoute`,
423-
`renderRouteEvents`, `invokeCli`, and the in-memory MCP helpers — exactly as the
423+
`renderRouteEvents`, `invokeCli`, `runScript` (rendered scripts), and the
424+
in-memory MCP helpers — exactly as the
424425
generated request scopes mount them: discovered from the compiled manifest,
425426
executed once per request in the same deterministic key order, handed the same
426427
surface-specific `invocation` (`tool`, `event`, `cli`, `script`), and failing the
427428
request closed when a factory throws. `providers.processLifetime` is scoped the
428-
way the artifact scopes it: each `invokeCli` call and each `renderRoute` render
429-
is a fresh simulated executable (hit 1, new `instanceId`), while one open
429+
way the artifact scopes it: each `invokeCli` call, each `runScript` run, and
430+
each `renderRoute` render is a fresh simulated executable (hit 1, new
431+
`instanceId`), while one open
430432
`openInMemoryMcpServer` session shares a single identity across every request
431433
it handles, like the artifact's warm Flight worker. Pass `context.providers` to opt out: an explicit map is mounted
432434
verbatim and no conventional provider runs, which is how a test stubs a provider
@@ -548,10 +550,11 @@ Node's own type transform, lowers the `.tsx` and `.jsx` helpers a plain
548550
script imports with the bundler's SWC — the same lowering the generated
549551
executable was built with — and serves `agent-bundle/meta` as the identity the build stamps from the
550552
manifest's `plugin`. Explicit `scripts:` configuration entries are bundled
551-
entries rather than routes and stay with the packed level. As with every
552-
harness call, `context` (with its provider fixtures) becomes mandatory once
553-
the project declares providers; a plain script accepts the fixtures and no
554-
other request-scope override. `stdin` pipes input to a plain script (omitted,
553+
entries rather than routes and stay with the packed level. A rendered script
554+
mounts the project's conventional providers with the `script` invocation the
555+
generated executable passes (`context.providers` substitutes a fixture map, as
556+
everywhere); a plain script opens no request scope, so it accepts no `context`
557+
at all. `stdin` pipes input to a plain script (omitted,
555558
it reads end-of-file at once); `process.execArgv` is empty as under plain
556559
`node`; an aborted `signal` sends SIGTERM and, should the script trap it,
557560
kills the process after a one-second grace before the run rejects. A rendered

‎packages/agent-bundle/src/test/render.ts‎

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -812,6 +812,8 @@ export interface PrepareScriptRenderHostOptions {
812812
readonly name: string;
813813
/** Receives the completed document's value, exactly what the rendered-script shell validated. */
814814
readonly onComplete: (value: unknown) => void;
815+
/** The script executable's process identity; a generated `scripts/<name>.mjs` is a fresh process per run. */
816+
readonly processLifetime: ProviderProcessLifetime;
815817
readonly provenance: RenderedRouteProvenance;
816818
readonly signal: AbortSignal;
817819
}
@@ -895,8 +897,18 @@ export const prepareScriptRenderHost = async (
895897
componentProps: (request) => ({ argv, signal: request.signal }),
896898
contextProgress: context.progress,
897899
renderer,
898-
requestInit: (request) => {
900+
requestInit: async (request) => {
899901
const root = process.cwd();
902+
// The generated script's render worker hands its providers the
903+
// `script` invocation with the path-derived name, never the route id.
904+
const providers = await mountProviders({
905+
explicit: context.providers,
906+
invocation,
907+
manifest: options.manifest,
908+
processHit: claimProcessHit(options.processLifetime),
909+
provenance: options.provenance,
910+
signal: request.signal,
911+
});
900912
return {
901913
capabilities: {
902914
command: renderer.unavailable(),
@@ -908,6 +920,7 @@ export const prepareScriptRenderHost = async (
908920
workspace: renderer.available({ root }, 'derived'),
909921
...context,
910922
...state.context,
923+
providers,
911924
invocation: {
912925
kind: 'script',
913926
operationId: options.provenance.routeId,

‎packages/agent-bundle/src/test/script.ts‎

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import { metaModuleSpecifier } from '../build/meta.ts';
3232
import { runGeneratedRenderedScript } from '../cli-entry.ts';
3333
import type { CliRenderedEvent } from '../cli-entry.ts';
3434
import { testMetaModuleSource } from '../rstest/meta-module.ts';
35+
import { createProviderProcessLifetime } from '../routes/provider-execution.ts';
3536
import { AgentTestError, captured } from './errors.ts';
3637
import {
3738
SCRIPT_DISPATCH_PROOF_LEVEL,
@@ -67,12 +68,11 @@ export interface RunScriptOptionsBase {
6768

6869
/**
6970
* Run options. `context` carries the request-scope overrides for a rendered
70-
* script over the runtime's request contract and is required once the project
71-
* declares providers (see {@link RenderRouteContextInit}): the generated
72-
* executable mounts `src/providers/*` before the component renders, and the
73-
* harness installs the fixtures instead. A plain script has no request scope;
74-
* it accepts the provider fixtures the contract demands and nothing else of
75-
* `context`.
71+
* script over the runtime's request contract (see {@link RenderRouteContextInit}):
72+
* omitted, the harness mounts the project's `src/providers/*` with the
73+
* `script` invocation exactly as the generated executable does before the
74+
* component renders, and `context.providers` substitutes a fixture map. A
75+
* plain script has no request scope and accepts no `context`.
7676
*/
7777
export type RunScriptOptions = RunScriptOptionsBase & RenderRouteContextInit;
7878

@@ -403,12 +403,10 @@ const rejectRenderedOnlyOptions = (
403403
script: TestableScriptDescriptor,
404404
options: RunScriptOptions,
405405
): void => {
406-
// Provider fixtures are what the options contract demands of every call in
407-
// a project that declares providers; a plain script cannot use them, but
408-
// supplying them is not a mistake. Any other request-scope override is.
409-
const contextOverrides = Object.keys(options.context ?? {}).filter((key) => key !== 'providers');
406+
// A plain script opens no request scope: no providers are mounted for it,
407+
// so there is nothing for `context` — fixtures included — to override.
410408
const offending = [
411-
...(contextOverrides.length === 0 ? [] : ['context']),
409+
...(options.context === undefined ? [] : ['context']),
412410
...(options.tty === undefined ? [] : ['tty']),
413411
];
414412
if (offending.length === 0) return;
@@ -567,6 +565,9 @@ export const runScript = async (
567565
manifest,
568566
name: script.name,
569567
onComplete: (completed) => { value = completed; },
568+
// Each generated `scripts/<name>.mjs` is a process of its own: its
569+
// providers see hit 1 of a fresh identity on every run.
570+
processLifetime: createProviderProcessLifetime(),
570571
provenance,
571572
signal,
572573
});

‎packages/agent-bundle/tests/projection/script-dispatch.test.ts‎

Lines changed: 31 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ describe('the compiled script inventory', () => {
3030
['identity', false],
3131
['stalled', true],
3232
['summary', true],
33+
['tooling-summary', true],
3334
]);
3435
});
3536

@@ -38,7 +39,7 @@ describe('the compiled script inventory', () => {
3839

3940
expect(error).toBeInstanceOf(AgentTestError);
4041
expect((error as AgentTestError).code).toBe('script-not-found');
41-
expect((error as AgentTestError).message).toContain('compiled: badge, banner, blank, broken, checksum, constant, identity, stalled, summary');
42+
expect((error as AgentTestError).message).toContain('compiled: badge, banner, blank, broken, checksum, constant, identity, stalled, summary, tooling-summary');
4243
expect((error as AgentTestError).message).toContain(`project root: ${testManifest().projectRoot}`);
4344
});
4445
});
@@ -82,7 +83,7 @@ describe('rendered scripts at the script dispatch level', () => {
8283
expect(run).toMatchObject({
8384
kind: 'rendered',
8485
name: 'summary',
85-
provenance: { execution: 'rendered-shell', proofLevel: 'script-dispatch', scripts: ['badge', 'banner', 'blank', 'broken', 'checksum', 'constant', 'identity', 'stalled', 'summary'] },
86+
provenance: { execution: 'rendered-shell', proofLevel: 'script-dispatch', scripts: ['badge', 'banner', 'blank', 'broken', 'checksum', 'constant', 'identity', 'stalled', 'summary', 'tooling-summary'] },
8687
routeId: 'script:summary',
8788
});
8889
});
@@ -177,6 +178,32 @@ describe('rendered scripts at the script dispatch level', () => {
177178
expect(run.value).toBeUndefined();
178179
}, 5000);
179180

181+
it('mounts the conventional providers for a rendered script with the script invocation, as the generated executable does', async () => {
182+
type Summary = { arguments: number; keys: string[]; libraryTooling: unknown };
183+
const first = await runScript('tooling-summary', ['--json', 'a.mp4', '--fast']);
184+
const second = await runScript('tooling-summary', ['--json', 'b.mp4']);
185+
186+
expect(first.exitCode).toBe(0);
187+
// The generated script passes `name: 'tooling-summary'`, never the route id.
188+
expect(scriptJson(first)).toEqual({
189+
arguments: 2,
190+
keys: ['libraryTooling', 'processLifetime'],
191+
libraryTooling: { kind: 'script', surface: 'tooling-summary', tool: 'ffprobe 6.1' },
192+
});
193+
expect((scriptJson(second) as Summary).arguments).toBe(1);
194+
195+
// An explicit fixture map is mounted verbatim: nothing under
196+
// src/providers/ runs for this invocation.
197+
const stubbed = await runScript('tooling-summary', ['--json', 'c.mp4'], {
198+
context: { providers: { libraryTooling: { tool: 'stub' }, processLifetime: { hits: 7, instanceId: 'fixture', pid: 0 } } },
199+
});
200+
expect(scriptJson(stubbed)).toEqual({
201+
arguments: 1,
202+
keys: ['libraryTooling', 'processLifetime'],
203+
libraryTooling: { tool: 'stub' },
204+
});
205+
});
206+
180207
it('rejects conflicting output flags at the shell boundary', async () => {
181208
const run = await runScript('summary', ['--json', '--ndjson']);
182209

@@ -372,12 +399,8 @@ describe('plain scripts at the script dispatch level', () => {
372399
expect(modelless.stderr).toContain('produced no plugin model');
373400
});
374401

375-
it('accepts provider fixtures on a plain script, as the options contract may demand them, and nothing else of context', async () => {
376-
const withProviders = await runScript('checksum', ['ab'], { context: { providers: {} } });
377-
expect(withProviders.exitCode).toBe(0);
378-
expect(withProviders.stdout).toBe('Fixture checksum: 2\n');
379-
380-
const error = await runScript('checksum', ['ab'], { context: { providers: {}, invocation: { surface: 'x' } } })
402+
it('rejects context on a plain script: it opens no request scope, so no providers are mounted for it', async () => {
403+
const error = await runScript('checksum', ['ab'], { context: { providers: {} } })
381404
.catch((thrown: unknown) => thrown);
382405
expect(error).toBeInstanceOf(AgentTestError);
383406
expect((error as AgentTestError).code).toBe('invalid-input');

‎packages/agent-bundle/tests/test-harness-manifest.test.ts‎

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,13 @@ describe('the compiled test manifest', () => {
190190
routeId: 'script:summary',
191191
source: resolve(fixtureRoot, 'src/scripts/summary.tsx'),
192192
},
193+
{
194+
name: 'tooling-summary',
195+
relativePath: 'src/scripts/tooling-summary.tsx',
196+
rendered: true,
197+
routeId: 'script:tooling-summary',
198+
source: resolve(fixtureRoot, 'src/scripts/tooling-summary.tsx'),
199+
},
193200
]);
194201
expect(manifest.apps).toEqual({
195202
panel: {
@@ -390,8 +397,8 @@ describe('the compiled test manifest', () => {
390397
// `banner` is claimed by configuration (AB4809) and `release/verify` is
391398
// nested (AB4808): neither becomes a scripts/<name>.mjs executable, so
392399
// neither is a script-dispatch target.
393-
expect(projected.scripts.map((script) => script.name)).toEqual(['badge', 'blank', 'broken', 'checksum', 'constant', 'identity', 'stalled', 'summary']);
394-
expect(manifest.scripts.map((script) => script.name)).toEqual(['badge', 'banner', 'blank', 'broken', 'checksum', 'constant', 'identity', 'stalled', 'summary']);
400+
expect(projected.scripts.map((script) => script.name)).toEqual(['badge', 'blank', 'broken', 'checksum', 'constant', 'identity', 'stalled', 'summary', 'tooling-summary']);
401+
expect(manifest.scripts.map((script) => script.name)).toEqual(['badge', 'banner', 'blank', 'broken', 'checksum', 'constant', 'identity', 'stalled', 'summary', 'tooling-summary']);
395402
});
396403

397404
it('rejects a shared app name whose compile-relevant declaration differs', async () => {

0 commit comments

Comments
 (0)