From 29b0ce0f7f08f3c1eb80881e4bc2d9dfd88f46b9 Mon Sep 17 00:00:00 2001 From: ScriptedAlchemy Date: Tue, 1 Sep 2026 21:02:36 +0000 Subject: [PATCH] fix(test): honest registry version gate, binary-faithful CLI cwd, README field Post-merge review findings from #190: bump AGENT_TEST_REGISTRY_VERSION to 2 so registries missing the required cliCommands/plugin manifest fields fail the existing version gate with a recoverable AgentTestError instead of crashing inside invokeCli or the MCP helpers; derive the invokeCli request workspace from process.cwd() to match the generated executable's semantics instead of manifest.projectRoot; correct the README example to read call.structuredContent (the McpToolInvocation field) rather than call.result. --- .changeset/honest-harness-registry-cwd.md | 7 ++++++ packages/agent-bundle/README.md | 2 +- packages/agent-bundle/src/test/cli.ts | 2 +- packages/agent-bundle/src/test/registry.ts | 2 +- .../tests/projection/cli-dispatch.test.ts | 24 +++++++++++++++++++ .../tests/test-harness-manifest.test.ts | 23 ++++++++++++++++++ 6 files changed, 57 insertions(+), 3 deletions(-) create mode 100644 .changeset/honest-harness-registry-cwd.md diff --git a/.changeset/honest-harness-registry-cwd.md b/.changeset/honest-harness-registry-cwd.md new file mode 100644 index 000000000..96460ce76 --- /dev/null +++ b/.changeset/honest-harness-registry-cwd.md @@ -0,0 +1,7 @@ +--- +"agent-bundle": patch +--- + +Reject stale consumer test registries before projection helpers read fields +they do not contain, and derive CLI dispatch workspace context from the +invocation working directory to match generated executables. diff --git a/packages/agent-bundle/README.md b/packages/agent-bundle/README.md index ea9bf8a9a..6f47af39f 100644 --- a/packages/agent-bundle/README.md +++ b/packages/agent-bundle/README.md @@ -252,7 +252,7 @@ import { cliJson, expectEvents, invokeCli, invokeMcpTool } from 'agent-bundle/te // mcp-in-memory: the generated server projects the document to protocol content. const call = await invokeMcpTool('summarize', { input: { title: 'Dune' } }); -expect(call.result.structuredContent).toEqual({ chapters: 24 }); +expect(call.structuredContent).toEqual({ chapters: 24 }); // cli-dispatch: the routed CLI resolves the command, parses argv, and maps the exit code. const run = await invokeCli(['library', 'audit', './books', '--max-files', '8']); diff --git a/packages/agent-bundle/src/test/cli.ts b/packages/agent-bundle/src/test/cli.ts index db925db74..29f4a6196 100644 --- a/packages/agent-bundle/src/test/cli.ts +++ b/packages/agent-bundle/src/test/cli.ts @@ -181,7 +181,7 @@ export const invokeCli = async ( } catch (error) { throw new CliInputError(error instanceof Error ? error.message : String(error)); } - const root = manifest.projectRoot; + const root = process.cwd(); const result = await runtime.runAgentRequest({ capabilities: { command: runtime.unavailable(), diff --git a/packages/agent-bundle/src/test/registry.ts b/packages/agent-bundle/src/test/registry.ts index b7cd4618b..5a6baf7d0 100644 --- a/packages/agent-bundle/src/test/registry.ts +++ b/packages/agent-bundle/src/test/registry.ts @@ -14,7 +14,7 @@ export const AGENT_TEST_REGISTRY_SYMBOL_KEY = 'agent-bundle/test-route-registry' const REGISTRY_SYMBOL = Symbol.for(AGENT_TEST_REGISTRY_SYMBOL_KEY); -export const AGENT_TEST_REGISTRY_VERSION = 1; +export const AGENT_TEST_REGISTRY_VERSION = 2; export interface AgentTestRouteRegistry { /** Lazy loaders keyed by compiled route id, so a test only compiles the routes it renders. */ diff --git a/packages/agent-bundle/tests/projection/cli-dispatch.test.ts b/packages/agent-bundle/tests/projection/cli-dispatch.test.ts index 5f0a7a6de..1b8ed71d4 100644 --- a/packages/agent-bundle/tests/projection/cli-dispatch.test.ts +++ b/packages/agent-bundle/tests/projection/cli-dispatch.test.ts @@ -1,5 +1,6 @@ import { describe, expect, it } from '@rstest/core'; +import { agent } from '@agent-bundle/runtime'; import { cliJson, invokeCli } from '../../src/test/cli.ts'; /** @@ -84,4 +85,27 @@ describe('the CLI dispatch level', () => { expect(run.exitCode).toBe(0); expect(progress.map((update) => update.message)).toEqual(['reading inventory', 'inventory ready']); }); + + it('derives the request workspace from the invocation cwd like the generated binary', async () => { + const invocationCwd = process.cwd(); + let observed: { readonly projectRoot: string | null; readonly workspace: string | null } | undefined; + const run = await invokeCli(['inventory', 'fiction'], { + context: { + progress: { + report: async () => { + const context = await agent(); + observed = { + projectRoot: context.capabilities.projectRoot.state === 'available' + ? context.capabilities.projectRoot.value.root + : null, + workspace: context.workspace.state === 'available' ? context.workspace.value.root : null, + }; + }, + }, + }, + }); + + expect(invocationCwd).not.toBe(run.provenance.projectRoot); + expect(observed).toEqual({ projectRoot: invocationCwd, workspace: invocationCwd }); + }); }); diff --git a/packages/agent-bundle/tests/test-harness-manifest.test.ts b/packages/agent-bundle/tests/test-harness-manifest.test.ts index ee576b79b..b4094ff31 100644 --- a/packages/agent-bundle/tests/test-harness-manifest.test.ts +++ b/packages/agent-bundle/tests/test-harness-manifest.test.ts @@ -5,6 +5,7 @@ import { describe, expect, it } from '@rstest/core'; import { routeTestSetupSource } from '../src/rstest/setup-module.ts'; import { AgentTestError } from '../src/test/errors.ts'; +import { invokeCli } from '../src/test/cli.ts'; import { compileTestManifest, testManifestFromRouteGraph } from '../src/test/manifest.ts'; import { AGENT_TEST_REGISTRY_SYMBOL_KEY, @@ -144,6 +145,28 @@ describe('the generated route registry', () => { expect(() => testManifest()).toThrow('Incompatible Agent Bundle test registry version'); }); }); + + it('refuses a version-1 manifest before a helper reads fields that version did not carry', async () => { + const versionOneManifest = Object.fromEntries( + Object.entries(manifest).filter(([key]) => key !== 'cliCommands' && key !== 'plugin'), + ); + const error = await withRealmRegistry( + { loaders: {}, manifest: versionOneManifest, version: 1 }, + async () => invokeCli(['--help']).catch((thrown: unknown) => thrown), + ); + + expect(error).toBeInstanceOf(AgentTestError); + expect((error as AgentTestError).code).toBe('manifest-unavailable'); + expect((error as AgentTestError).message).toContain('found 1'); + expect((error as AgentTestError).message).toContain('Install one agent-bundle version'); + }); + + it('accepts a registry carrying the current manifest version', async () => { + await withRealmRegistry( + { loaders: {}, manifest, version: AGENT_TEST_REGISTRY_VERSION }, + () => expect(testManifest()).toBe(manifest), + ); + }); }); describe('route loaders and the manifest that produced them', () => {