For AI agents: the complete documentation index is available at https://scriptedalchemy.github.io/agent-bundle/zh/llms.txt, the full documentation bundle is available at https://scriptedalchemy.github.io/agent-bundle/zh/llms-full.txt, and this page is available as Markdown at https://scriptedalchemy.github.io/agent-bundle/zh/guide/development/testing.md.
  • 简体中文
  • 测试

    路由模块通过框架来测试,而不是通过手写的打包器配置。承载该 harness 的有两个子路径,且两者都是选择 加入的:@rstest/corereact 是可选 peer 依赖,因此从不测试路由的项目两者都不会安装。渲染还需要 @agent-bundle/runtime,只要项目拥有路由模块就已经拥有它——生成的入口以同样的方式导入它。

    配置辅助函数

    agent-bundle/rstest 会编译项目一次——与构建执行的路由图编译完全相同,但不构建产物——并返回一个普通的 Rstest 配置对象,其中携带测试清单、路由加载器、React 的 react-server 解析条件以及自动 JSX 运行时:

    // rstest.route-unit.config.ts
    import { defineConfig } from '@rstest/core';
    import { agentBundleRstest } from 'agent-bundle/rstest';
    
    export default defineConfig(await agentBundleRstest());

    route-unit 测试默认匹配 tests/route-unit/**/*.test.{ts,tsx},并且需要自己独立的 Rstest 运行, 因为渲染一个路由要求整个 worker 进程启用 Node 的 react-server 条件。请把它们排除在项目普通的 rstest 运行之外。

    渲染一个路由

    agent-bundle/test 提供这些辅助函数。renderRoute 执行一个路由——通过编译后的 route id,或直接导入 模块——它经过真实的渲染器与真实的请求存储,并解析为最终的 Agent Document:

    import { 
    const expectDocument: (subject: DocumentSubject) => DocumentAssertions
    expectDocument
    ,
    const renderRoute: (target: RenderRouteTarget, options?: RenderRouteOptions | undefined) => Promise<RenderedRoute>

    Renders one route through the real Agent renderer and returns its final Agent Document. The route component executes inside a real request scope, its output is encoded as React Flight, and the runtime's own final-only dispatcher decodes it — the harness owns no second rendering path.

    This is the route-unit proof level: no transport is opened, no browser surface is compiled, and no host artifact is built.

    renderRoute
    } from 'agent-bundle/test';
    export const
    const summarizes: () => Promise<void>
    summarizes
    = async ():
    interface Promise<T>

    Represents the completion of an asynchronous operation

    Promise
    <void> => {
    const {
    const document: AgentDocument

    The final Agent Document the real renderer produced.

    document
    } = await
    function renderRoute(target: RenderRouteTarget, options?: RenderRouteOptions | undefined): Promise<RenderedRoute>

    Renders one route through the real Agent renderer and returns its final Agent Document. The route component executes inside a real request scope, its output is encoded as React Flight, and the runtime's own final-only dispatcher decodes it — the harness owns no second rendering path.

    This is the route-unit proof level: no transport is opened, no browser surface is compiled, and no host artifact is built.

    renderRoute
    ('tool:library/summarize', {
    RenderRouteOptionsBase.input?: unknown

    The route's input: tool input, event payload, or script input.

    input
    : {
    title: string
    title
    : 'Dune' },
    });
    function expectDocument(subject: DocumentSubject): DocumentAssertions
    expectDocument
    (
    const document: AgentDocument

    The final Agent Document the real renderer produced.

    document
    )
    .
    DocumentAssertions.toHaveStatus: (status: AgentDocumentStatus) => DocumentAssertions
    toHaveStatus
    ('success')
    .
    DocumentAssertions.toContainMarkdown: (text: string) => DocumentAssertions

    Asserts a Markdown node contains text.

    toContainMarkdown
    ('Dune')
    .
    DocumentAssertions.toHaveValue: (value: unknown) => DocumentAssertions

    Asserts the document's structured value equals value (JSON structural equality). undefined asserts the document emitted no value at all, which null does not satisfy.

    toHaveValue
    ({
    chapters: number
    chapters
    : 24 });
    };

    renderRoute 接受 inputargs(CLI 路由)、请求 context 覆盖(包括一个 context.progress 上报器)、渲染 limits 以及一个 signal。它返回文档、路由上报的请求作用域进度、解析出的 provenance, 以及由路由自己的 resultSchema 解析后的取值。无论调用方是否提供自己的上报器,进度都会被记录。

    testManifest() 暴露编译后的路由清单,因此一个测试套件可以在进程内遍历每个路由,而不必为每个路由付出 一次构建的代价。任何失败——未知路由、被拒绝的路由种类、被拒的输入、渲染错误——都会指明 route id、target 种类与模块 provenance。

    针对 Agent Document 契约的匹配器:toHaveStatustoContainMarkdowntoContainTexttoHaveValuetoHaveErrortoHaveNodeKinds

    这就是 route-unit 证明级别,且仅此而已:它证明一个路由模块渲染出了它所声称的文档。它不是关于 MCP 传输、打包产物或浏览器表面的证据。

    证明级别

    这些级别是刻意分开的。每个辅助函数都会把自己承载的级别写进 provenance,并在每次失败时打印它,因为 某一级别的通过绝不是另一级别的凭据。

    级别辅助函数它证明什么
    route-unitrenderRouterenderRouteEvents一个路由模块渲染出了它所声称的文档——以及渲染事件流。
    mcp-in-memoryopenInMemoryMcpServerinvokeMcpToolreadMcpResourcegetMcpPromptlistMcpSurfacerunContractMatrix真实生成式 MCP 服务器的协议契约,经由 SDK 的内存内传输。
    dev-epochrunDevEpochContractMatrix通过 Workbench 会话服务打开的、锁定到某个 epoch 的生成式 stdio 进程;调用方拥有 epoch 租约与进程生命周期,MCP App 路由被覆盖(表面加 ui:// 扫描)。
    cli-dispatchinvokeClicliJsoncliNdjson一个普通或渲染式 argv 向量在路由式 CLI 自己的 shell 中被解析并执行——包括渲染式 Markdown、显式 TTY、JSON 与 NDJSON 模式——全部在进程内完成。
    script-dispatchrunScriptscriptJsonscriptNdjson一个约定式 src/scripts/* 模块按其生成可执行文件的契约运行,无需打包:渲染式 .tsx 脚本在进程内经由渲染式脚本外壳运行(管道 Markdown、TTY、--json--ndjson,并挂载项目的约定式 provider),普通 .ts 脚本则作为独立 Node 进程经由 main 封套运行——真实的 process.exit、退出码、stdout、stderr 与可选的 stdintestManifest().scripts 列出实际发布的脚本;嵌套(AB4808)或冲突(AB4809)的脚本绝不会成为目标。
    workbench-surfaceinspectWorkbenchSurfacedev 服务器会交给 Workbench 的本项目内容——路由清单、分组路由目录、state 声明、生命周期回放 fixture、页面可用性——来自同一次编译器处理,无需浏览器也无需 dev 服务器;被编译器拒绝的项目报告 manifest-unavailable 及其 error 诊断。
    packed-stdioopenPackedMcpServerrunPackedContractMatrix已构建产物的生成入口作为真实进程通过 stdio 运行。
    packed-deleted-sourceremoveProjectSourceopenPackedMcpServer({ deletedSource })runPackedContractMatrix在项目源码与配置被移除并核实缺失之后,打包后的 stdio 进程仍然可以运行。
    host-installopenInstalledHostMcpServerrunInstalledHostContractMatrix一份已构建的捆绑包被投放到隔离的宿主根目录、以输出的宿主格式被发现,并从已安装布局中启动。

    在这九个级别之外还有两个并列级别,共十一个。agent-bundle/test/browser 为浏览器安全的 browser-app 级别 提供 mountBrowserApp,用于在真实浏览器页面中把生产编译的 MCP App HTML 挂载到产品桥接层之上; 而 simulated 复用不带 sessionEvidence 的已安装宿主辅助函数 openInstalledHostMcpServer —— 一份输出的捆绑包被直接投放到隔离的宿主形状根目录并在没有宿主自有安装的情况下启动,它比 host-install 更弱。

    import { 
    const cliJson: (invocation: CliInvocation) => unknown

    The parsed canonical JSON line a successful command wrote to stdout.

    cliJson
    ,
    const cliNdjson: (invocation: CliInvocation) => readonly CliRenderedEvent[]

    The ordered render events a successful --ndjson invocation wrote to stdout.

    cliNdjson
    ,
    const invokeCli: (argv: readonly string[], options?: InvokeCliOptions | undefined) => Promise<CliInvocation>

    Dispatches one argv vector through the routed CLI shell in this process and returns its exit code, streams, and validated result.

    This is the cli-dispatch proof level. Nothing is spawned.

    invokeCli
    ,
    const invokeMcpTool: (tool: string, options?: McpInvocationOptions | undefined) => Promise<McpToolInvocation>

    Calls one compiled tool through the real protocol and returns the projected result. mcp-in-memory level: protocol contract proof, not process proof.

    invokeMcpTool
    } from 'agent-bundle/test';
    export const
    const proofs: () => Promise<Record<string, unknown>>
    proofs
    = async ():
    interface Promise<T>

    Represents the completion of an asynchronous operation

    Promise
    <
    type Record<K extends keyof any, T> = { [P in K]: T; }

    Construct a type with a set of properties K of type T

    Record
    <string, unknown>> => {
    // mcp-in-memory: the generated server projects the document to protocol content. const
    const call: McpToolInvocation
    call
    = await
    function invokeMcpTool(tool: string, options?: McpInvocationOptions | undefined): Promise<McpToolInvocation>

    Calls one compiled tool through the real protocol and returns the projected result. mcp-in-memory level: protocol contract proof, not process proof.

    invokeMcpTool
    ('summarize', {
    input?: unknown
    input
    : {
    title: string
    title
    : 'Dune' } });
    // cli-dispatch, plain .ts route: resolve argv, execute, and map the exit code. const
    const run: CliInvocation
    run
    = await
    function invokeCli(argv: readonly string[], options?: InvokeCliOptions | undefined): Promise<CliInvocation>

    Dispatches one argv vector through the routed CLI shell in this process and returns its exit code, streams, and validated result.

    This is the cli-dispatch proof level. Nothing is spawned.

    invokeCli
    (['library', 'audit', './books', '--max-files', '8']);
    // cli-dispatch, rendered .tsx route: exercise the shell's rendered output modes. const
    const rendered: CliInvocation
    rendered
    = await
    function invokeCli(argv: readonly string[], options?: InvokeCliOptions | undefined): Promise<CliInvocation>

    Dispatches one argv vector through the routed CLI shell in this process and returns its exit code, streams, and validated result.

    This is the cli-dispatch proof level. Nothing is spawned.

    invokeCli
    (['library', 'report', './books', '--ndjson']);
    const
    const events: readonly CliRenderedEvent[]
    events
    =
    function cliNdjson(invocation: CliInvocation): readonly CliRenderedEvent[]

    The ordered render events a successful --ndjson invocation wrote to stdout.

    cliNdjson
    (
    const rendered: CliInvocation
    rendered
    );
    // An explicit TTY proves the in-place progress path rather than the piped one. const
    const tty: CliInvocation
    tty
    = await
    function invokeCli(argv: readonly string[], options?: InvokeCliOptions | undefined): Promise<CliInvocation>

    Dispatches one argv vector through the routed CLI shell in this process and returns its exit code, streams, and validated result.

    This is the cli-dispatch proof level. Nothing is spawned.

    invokeCli
    (['library', 'report', './books'], {
    InvokeCliOptionsBase.tty?: boolean | undefined

    Selects interactive rendered output explicitly. Generated binaries use process.stdout.isTTY; the in-process harness defaults to piped output.

    tty
    : true });
    return {
    exitCode: number
    exitCode
    :
    const run: CliInvocation
    run
    .
    CliInvocation.exitCode: number

    The process exit code the routed shell mapped: 0 success (or the result's exitCode under config.exitCode: 'result'), 1 execution failure, 2 usage or input-validation failure.

    exitCode
    ,
    finalEvent: "shell" | "progress" | "replace" | "error" | "complete" | undefined
    finalEvent
    :
    const events: readonly CliRenderedEvent[]
    events
    .
    ReadonlyArray<CliRenderedEvent>.at(index: number): CliRenderedEvent | undefined

    Returns the item located at the specified index.

    @paramindex The zero-based index of the desired code unit. A negative index will count back from the last item.
    at
    (-1)?.
    type: "shell" | "progress" | "replace" | "error" | "complete" | undefined
    type
    ,
    inPlaceProgress: boolean
    inPlaceProgress
    :
    const tty: CliInvocation
    tty
    .
    CliInvocation.stdout: string

    Everything the shell wrote to stdout, including rendered Markdown, TTY, JSON, or NDJSON output.

    stdout
    .
    String.includes(searchString: string, position?: number): boolean

    Returns true if searchString appears as a substring of the result of converting this object to a String, at one or more positions that are greater than or equal to position; otherwise, returns false.

    @paramsearchString search string@paramposition If position is undefined, 0 is assumed, so as to search all of the String.
    includes
    ('\r\u001B[2K'),
    scanned: unknown
    scanned
    :
    function cliJson(invocation: CliInvocation): unknown

    The parsed canonical JSON line a successful command wrote to stdout.

    cliJson
    (
    const run: CliInvocation
    run
    ),
    structured: unknown
    structured
    :
    const call: McpToolInvocation
    call
    .
    McpToolInvocation.structuredContent?: unknown
    structuredContent
    ,
    }; };

    expectEvents 针对渲染事件流做断言。toContainSequence 对序列是宽容的——多出一个 progressreplace 帧是合法的,不会把一次本该通过的渲染判红——而缺帧、乱序或序号回退仍然会失败。 toHaveMonotonicSequencetoCompleteOncetoHaveProgresstoHaveNoErrors 覆盖契约的其余部分。

    进程证据刻意很昂贵

    在打包级别中,只有 packed-stdio 及其严格更强的升级版 packed-deleted-source 属于进程证据:打包一次、 安装一次、构建一次、移除并核实源码缺失一次、启动一次,然后在这一个会话内遍历每个路由的断言。dev-epoch 是另一种形态的进程证据——Workbench 自己的、锁定到 epoch 的生成式 stdio 进程,而非打包后的产物——因此 dev-epoch 通过并不能说明打包会交付什么。deleted-source 的 旅程还会从生成式服务器读取内嵌的 MCP App 资源;它不证明原生宿主的安装或派发,也不证明那种把产物复制 到别处的安装方式。

    host-install 是独立的、关于已安装布局的进程证据。它的确定性适配器模拟通道是无条件运行的,可用的 Claude 与 Codex 二进制文件还会证明它们各自公开的安装路径,而 Cursor 会显式记录它那不可用的、非交互式 宿主会话表面。

    契约矩阵

    契约矩阵是框架自有的生成式插件线上契约测试套件。三个入口共享同一份实现;边界差异是显式的能力标记,而 不是分叉的检查逻辑。项目只需提供 fixture ——合法输入、为每个内存内工具路由声明的 resultCompat 策略、 可选的 previousResults 负载、可选的 cancellation case,以及一个可选的、带声明式预期的确定性生命 周期状态转换驱动。

    runContractMatrixmcp-in-memory 通过 SDK 的内存内传输,对真实的生成式服务器打开一个真实的 MCP 客户端并运行完整矩阵。它证明:线上表面相对编译器清单的完整性、fixture 覆盖率、成功路径的调用扫描、 经由每个工具路由自己的 resultSchema 的 JSON 序列化往返、在序列化负载上声明的 additive 或 closed 兼容行为、当前 schema 对旧版服务器负载的接受度、对由所公布的 listTools 输入 JSON Schema 派生出的 负面输入的拒绝,以及调用中途取消的卫生性。内存内传输可能在不做序列化的情况下传递结构化取值,因此矩阵 会在校验之前用一次显式的 JSON.parse(JSON.stringify(...)) 往返来填上这个缺口。MCP App 在表面注册上 被报告为 not-applicable,因为内存内级别并不注册它们。

    runPackedContractMatrixpacked-stdio / packed-deleted-source 针对一个已经打开的打包会话 运行——那一次打包旅程自己拥有会话的打开与关闭。它为以下内容提供进程 stdio 证据:表面完整性(包括 listResources 中编译后的 MCP App 资源 URI)、fixture 覆盖率、成功路径扫描、对所公布输入 schema 的 拒绝,以及客户端侧的取消卫生性。它无法加载项目路由模块,因为源码可能已被删除并核实缺失,所以序列化 往返、兼容性探测与版本偏移检查——包括它们逐生命周期阶段的变体——都会以诚实的理由被报告为 not-applicable。打包后的服务器在返回之前会用自己内置的 resultSchema 校验每个工具结果;一次成功的 扫描调用就是那份证据。

    runInstalledHostContractMatrixhost-install 针对 openInstalledHostMcpServer 返回的、已经 打开的会话运行。打开器会从已安装的根目录读取宿主输出的 MCP 文档,核实清单、组件路径、资源路径与钩子 路径以及产物文件摘要,启动那条已安装的命令,并从真实的 MCP initialize 结果中观察运行中的版本。它的 报告分别记录源码、已构建产物、已安装产物与运行中进程的版本,任何取值缺失或不一致都会失败关闭。元数据 记录被观察到的宿主二进制版本、适配器修订号、清单与 schema 摘要,以及框架版本。以模块为后端的检查仍然 诚实地是 not-applicable,因为加载项目模块会跨回源码与构建树。

    import { 
    const runContractMatrix: (options: ContractMatrixOptions) => Promise<ContractMatrixReport>

    Runs the contract matrix against one compiled MCP server at the mcp-in-memory proof level. Returns a per-route report; throws one aggregated AgentTestError with code contract-violation when any check failed (never first-failure-only).

    runContractMatrix
    } from 'agent-bundle/test';
    export const
    const matrix: () => Promise<void>
    matrix
    = async ():
    interface Promise<T>

    Represents the completion of an asynchronous operation

    Promise
    <void> => {
    await
    function runContractMatrix(options: ContractMatrixOptions): Promise<ContractMatrixReport>

    Runs the contract matrix against one compiled MCP server at the mcp-in-memory proof level. Returns a per-route report; throws one aggregated AgentTestError with code contract-violation when any check failed (never first-failure-only).

    runContractMatrix
    ({
    ContractMatrixOptions.fixtures: Readonly<Record<string, ContractRouteFixture>>

    Route id -> fixture. Every compiled tool, prompt, and resource route on the server must be covered. App routes are not registered at mcp-in-memory; entries for them are accepted and ignored.

    fixtures
    : {
    'tool:library/summarize': {
    ContractRouteFixture.input?: unknown

    Valid input for the sweep invocation (tools/prompts; resources need none).

    input
    : {
    title: string
    title
    : 'Dune' },
    ContractRouteFixture.previousResults?: readonly unknown[] | undefined

    Serialized payloads captured from previous server versions; each must parse under the CURRENT resultSchema (previous-server + current-client skew).

    previousResults
    : [{
    chapters: number
    chapters
    : 24 }],
    ContractRouteFixture.resultCompat?: ResultCompatPolicy | undefined

    Declared serialized-result compatibility policy. REQUIRED for tool routes.

    resultCompat
    : 'additive',
    }, }, }); };

    打包与已安装宿主这两个入口接受同样的 fixture 形状,外加它们所针对的会话:runPackedContractMatrix 需要那个已打开的打包会话,以及在移除源码之前编译出的清单;而 runInstalledHostContractMatrix 需要 openInstalledHostMcpServer 返回的会话以及同一份清单。

    生命周期 fixture

    生命周期 fixture 会在矩阵那一个已打开的客户端上重放 unknown → queued → running → first-progress → repeated-progress → terminal。框架会校验每个阶段的 结构化内容与渲染输出、additive 与 closed 兼容性、结算之前的实时进度、日志累积、声明的通知、幂等的 提交重放,以及带类型的预算拒绝。

    调用方提供的同存储 restart 回调会在该边界上补充持久性证据;没有它时,这项检查诚实地是 not-applicable。打包侧的调用方应把该回调接到既有打包旅程的重启上,而不是另建一条打包、构建与安装的 路径。生命周期 fixture 可选的 state.catalog 断言会把它声明的 id 与 lifetime 固定到同一次挂载状态 重放所使用的编译器清单上。

    事件路由与运行时身份

    当编译后的清单包含事件路由时,打包与已安装宿主这两个边界会在顺序矩阵事件之前以及整个过程中,采样只读的 事件运行时状态。如果预热的 instanceId 发生变化、产物 epoch 漂移,或者可用性降级为 runtime-restartedruntime-unavailableruntime-instance-identity 检查就会失败。内存内运行 以及不含事件路由的编译产物,会诚实地把运行时身份报告为 not-applicable

    没有任何矩阵边界证明浏览器 App HTML 或产物重建重放。

    当所公布的输入 schema 声明了 additionalProperties: false 时,普通的 z.object 工具路由仍可能在不 触发协议失败的情况下剥掉未知键。当其他生成的负面输入仍然证明了拒绝路径时,负面输入检查会记录这种容忍。

    失败的矩阵会抛出一个聚合后的 AgentTestError,错误码为 contract-violation,其中指明每个失败的路由、 每项失败的检查,以及这次运行实际承载的证明级别标签。

    下一步

    • 评测 —— 追问智能体是否根本抵达了插件的那个表面。
    • 产物校验 —— 为已构建捆绑包把关的那些检查。