Skip to content

工具框架测试策略

从 spec、registry、handler、lifecycle、dispatch trace、模型回灌和 App Server fixture 解释 Codex 工具行为如何分层验证。

基于rust-v0.150.0
CodexRustToolsTesting

工具框架测试策略 ​

Codex 工具测试不能只看“某个 handler 的单测是否通过”。一个工具结论通常横跨七层:规格是否正确、runtime 是否注册、handler 是否维护状态、Hook 后生命周期是否正确、dispatch trace 是否配对、结果是否进入下一次模型请求,以及客户端协议是否一致。当前仓库用不同夹具覆盖这些层:spec tests 检查 schema,ToolPlanProbe 检查 visible/registered/exposure,handler tests 直接构造 ToolInvocation,ToolLifecycleContributor 观察 start/finish,rollout trace 区分 Direct/Code Mode,TestCodex 验证模型回灌,TestAppServer 验证 JSON-RPC/server request。

本文面向已经读过ToolRegistry数据结构、ToolRouter解析与分派、ToolOutput与错误模型以及本系列具体工具文章的读者。本文不把所有测试文件逐一列目录,也不声称一次通过能证明真实 provider、操作系统或第三方 MCP 服务;它回答的是:面对一个工具行为结论,应该在源码哪一层建立 arrange/action/assert,如何选择夹具,失败路径如何不被 happy path 掩盖。读完后,读者可以为一个新 handler 设计从 schema 到客户端回路的最小验证矩阵。

1. 七层边界 ​

1.1 Schema层 ​

schema test 只验证 ToolSpec 的 wire shape:名称、namespace、required 字段、枚举、output schema 和 description。它不执行 handler,也不证明 runtime 已注册。

源码位置:codex-rs/core/src/tools/handlers/multi_agents_spec_tests.rs :: spawn_agent_tool_v2_requires_task_name_and_lists_visible_models

rust
let tool = create_spawn_agent_tool_v2(SpawnAgentToolOptions {
    available_models: vec![
        model_preset("visible", /*show_in_picker*/ true),
        model_preset("hidden", /*show_in_picker*/ false),
    ],
    agent_type_description: "role help".to_string(),
    expose_agent_type: true,
    hide_agent_type_model_reasoning: false,
    expose_spawn_agent_model_overrides: true,
    multi_agent_version: MultiAgentVersion::V2,
    usage_hint_text: None,
});

let ToolSpec::Function(ResponsesApiTool {
    description,
    parameters,
    output_schema,
    ..
}) = tool
else {
    panic!("spawn_agent should be a function tool");
};

assert!(properties.contains_key("task_name"));
assert!(properties.contains_key("message"));
assert_eq!(
    parameters.required.as_ref(),
    Some(&vec!["task_name".to_string(), "message".to_string()])
);
assert_eq!(
    output_schema.expect("spawn_agent output schema")["required"],
    json!(["task_name", "nickname"])
);

这个测试的输入是带 visible/hidden model 的 SpawnAgentToolOptions,动作是调用 spec factory,断言只关注 schema。它证明 V2 的 task_name/message 合约和输出字段,不证明 spawn 可以真正创建 child。

1.2 Plan层 ​

ToolPlanProbe 把一个 router 投影为四组观察值:模型可见 spec、可见名称、注册名称和 exposure。它适合验证 feature、provider capability、namespace 与 collision,因为不需要启动模型或执行工具。

源码位置:codex-rs/core/src/tools/spec_plan_tests.rs :: ToolPlanProbe

rust
struct ToolPlanProbe {
    visible_specs: Vec<ToolSpec>,
    visible_names: Vec<String>,
    namespace_functions: BTreeMap<String, Vec<String>>,
    registered_names: Vec<String>,
    exposures: BTreeMap<String, ToolExposure>,
}

impl ToolPlanProbe {
    fn from_router(router: ToolRouter) -> Self {
        let visible_specs = router.model_visible_specs().to_vec();
        let visible_names = visible_specs
            .iter()
            .map(|spec| spec.name().to_string())
            .collect::<Vec<_>>();
        let namespace_functions = visible_specs
            .iter()
            .filter_map(|spec| match spec {
                ToolSpec::Namespace(namespace) => Some((
                    namespace.name.clone(),
                    namespace.tools.iter().map(|tool| match tool {
                        ResponsesApiNamespaceTool::Function(tool) => tool.name.clone(),
                        ResponsesApiNamespaceTool::Custom(tool) => tool.name.clone(),
                    }).collect::<Vec<_>>(),
                )),
                _ => None,
            })
            .collect::<BTreeMap<_, _>>();
        let registered_tool_names = router.registered_tool_names_for_test();
        let exposures = registered_tool_names
            .iter()
            .filter_map(|name| {
                router
                    .tool_exposure_for_test(name)
                    .map(|exposure| (name.to_string(), exposure))
            })
            .collect::<BTreeMap<_, _>>();
        Self {
            visible_specs,
            visible_names,
            namespace_functions,
            registered_names: registered_tool_names
                .iter()
                .map(ToString::to_string)
                .collect(),
            exposures,
        }
    }
}

可见和注册必须同时断言。TLS028 的 Deferred Dynamic Tool 测试就是典型:tool_search visible,dynamic runtime registered,dynamic exposure Deferred。只断言 visible 会漏掉 dispatch 侧事实;只断言 registered 会漏掉模型根本看不到工具的事实。

1.3 Handler层 ​

handler unit test 直接构造 ToolInvocation,可以精确控制 Session、Turn、StepContext、payload、取消 token 和 diff tracker。它适合测试参数解析、payload 类型、弱引用生命周期、事件发射和本地状态,不需要 Responses 网络。

源码位置:codex-rs/core/src/tools/handlers/extension_tools.rs :: passes_turn_fields_and_scoped_turn_item_emitter_to_extension_call

rust
let (session, turn, rx) = make_session_and_context_with_rx().await;
let invocation = ToolInvocation {
    session,
    step_context: StepContext::for_test(Arc::clone(&turn)),
    turn,
    cancellation_token: tokio_util::sync::CancellationToken::new(),
    tracker: Arc::new(tokio::sync::Mutex::new(TurnDiffTracker::new())),
    call_id: "call-extension".to_string(),
    tool_name: codex_tools::ToolName::plain("extension_echo"),
    source: ToolCallSource::Direct,
    payload: ToolPayload::Function {
        arguments: json!({ "message": "hello" }).to_string(),
    },
};

ToolExecutor::handle(&handler, invocation)
    .await
    .expect("extension call should succeed");

测试随后断言 extension 收到的 history、model、sandbox cwd、payload 和 emitter 事件,并检查 Weak<Session>/Weak<TurnContext> 已可释放。它证明 adapter 的上下文投影,不证明某个具体 extension 的业务结果。

1.4 Core集成层 ​

TestCodex 启动真实 Core thread、真实 tool router 和 mock Responses server。测试通过 SSE 告诉模型“调用哪个工具”,再捕获第二次 request 中的 function_call_output。这是验证 handler 与 sampling/history 的关键层。

源码位置:codex-rs/core/tests/common/test_codex.rs :: TestCodex

rust
pub struct TestCodex {
    pub home: Arc<TempDir>,
    pub cwd: Arc<TempDir>,
    pub codex: Arc<CodexThread>,
    pub session_configured: SessionConfiguredEvent,
    pub config: Config,
    pub thread_manager: Arc<ThreadManager>,
    _test_env: TestEnv,
}

impl TestCodex {
    pub async fn submit_turn(&self, prompt: &str) -> Result<()> {
        self.submit_turn_with_permission_profile(prompt, PermissionProfile::Disabled)
            .await
    }
}

submit_turn 是方便入口,不应被误当成测试断言。真正的 arrange/action/assert 还包括 mount SSE sequence、提交用户输入、等待 TurnComplete、读取 mock request 并检查具体 output。

1.5 Lifecycle层 ​

ToolLifecycleContributor 观察所有 runtime 的统一执行边界。on_tool_start 发生在 PreToolUse 完成、参数重写成功之后,能读取最终 payload 和只读 conversation history;被 PreToolUse block 或 rewrite 失败的调用不会收到 start。on_tool_finish 则区分 Completed { success }、Blocked、Failed { handler_executed } 和 Aborted。

源码位置:

  • codex-rs/core/src/tools/lifecycle.rs :: notify_tool_start
  • codex-rs/ext/extension-api/src/contributors/tool_lifecycle.rs :: ToolCallOutcome
  • codex-rs/core/tests/suite/tool_lifecycle.rs :: tool_start_receives_rewritten_payload_and_post_hook_history
rust
contributor
    .on_tool_start(ToolStartInput {
        session_store: &invocation.session.services.session_extension_data,
        thread_store,
        turn_store: invocation.turn.extension_data.as_ref(),
        turn_id: invocation.turn.sub_id.as_str(),
        call_id: invocation.call_id.as_str(),
        tool_name: &invocation.tool_name,
        payload: &invocation.payload,
        conversation_history: Arc::clone(&conversation_history),
        source: extension_tool_call_source(invocation.source.clone()),
    })
    .await;

这个层面证明的是宿主观察顺序,不是单个 handler 的业务。测试应同时覆盖 rewritten payload、history 中的 Hook additional context,以及 block 时 start 不发生。

1.6 Trace层 ​

ToolDispatchTrace 记录从 registry dispatch 看到的 requester、payload 和最终消费者结果。Direct/DirectPlaintextMessage 归为 model requester,Code Mode 保存 cell id 与 runtime tool id;结果分别记录 model-facing ResponseInputItem 或 JavaScript-facing typed value。

源码位置:codex-rs/core/src/tools/tool_dispatch_trace.rs :: ToolDispatchTrace

rust
let requester = match &invocation.source {
    ToolCallSource::Direct | ToolCallSource::DirectPlaintextMessage => {
        ToolDispatchRequester::Model {
            model_visible_call_id: invocation.call_id.clone(),
        }
    }
    ToolCallSource::CodeMode {
        cell_id,
        runtime_tool_call_id,
    } => ToolDispatchRequester::CodeCell {
        runtime_cell_id: cell_id.clone(),
        runtime_tool_call_id: runtime_tool_call_id.clone(),
    },
};

trace tests 还应覆盖 unsupported tool 和 incompatible payload 的 early return,确保每个 start 都有 failed terminal record。它不是模型 history 测试,也不能替代 lifecycle item 断言。

1.7 协议层 ​

App Server 测试通过 TestAppServer 读写 JSON-RPC response、notification 和 server request。它适合验证 Core event 到公开 API 的字段映射、客户端 response 如何回到 Op,不能替代 Core handler unit。

2. Plan夹具 ​

2.1 统一输入 ​

ToolPlanInputs 把 MCP registered tools、Extension executors、Dynamic specs、ToolSuggest candidates 和 wait config 组合成一个 plan 输入。probe_with 先创建 session/turn/step,再调用 build_core_tool_registry、append_source_tools 和 ToolRouter::from_registry。

源码位置:codex-rs/core/src/tools/spec_plan_tests.rs :: probe_with

rust
async fn probe_with(
    configure_turn: impl FnOnce(&mut TurnContext),
    inputs: ToolPlanInputs,
) -> ToolPlanProbe {
    let (_session, mut turn) = make_session_and_context().await;
    configure_turn(&mut turn);
    let turn = Arc::new(turn);
    let step_context = StepContext::for_test(Arc::clone(&turn));
    let mut registry = build_core_tool_registry(
        step_context.turn.as_ref(),
        &step_context.environments,
        step_context.mcp.as_ref(),
        inputs.tool_suggest_candidates.as_ref(),
        inputs.wait_for_environment_tool_config.as_ref(),
    );
    let hosted_specs = append_source_tools(
        step_context.turn.as_ref(),
        &mut registry,
        inputs.tool_runtimes,
        inputs.extension_tool_executors,
        &inputs.dynamic_tools,
    );
    let router = ToolRouter::from_registry(
        step_context.turn.as_ref(),
        registry,
        hosted_specs,
        &Default::default(),
    );
    ToolPlanProbe::from_router(router)
}

该夹具的边界很明确:它验证计划输入到 router 的投影,不执行任何 runtime。适合测试“feature 开关改变了什么”,不适合测试“server 实际返回了什么”。

2.2 暴露矩阵 ​

对一个工具至少要比较三列:visible、registered、exposure。以下是当前测试常见的矩阵:

情形visibleregisteredexposure
Direct是是Direct/DirectModelOnly
Deferred否是Deferred/DeferredModelOnly
CodeModeOnly否是CodeModeOnly
Hidden否是Hidden
未注册否否无

ToolPlanProbe 的断言方法直接对应这三列,因此 failure message 能告诉读者到底是哪一个投影不符合预期。

2.3 冲突测试 ​

工具来源顺序测试把 MCP、Extension 和 Dynamic 放进同一个 plan,使用相同 namespace/name 形成 collision,再检查 winner 和保留下来的 namespace。严格 collision 测试还验证 config 开启 error_on_tool_collisions 时 router 返回错误。

源码位置:codex-rs/core/src/tools/spec_plan_tests.rs :: unified_tool_runtimes_preserve_source_order_and_collision_priority

rust
let expected_source_order = [
    "exec_command",
    "mcp__registry",
    "registry_extension",
    "registry_dynamic",
];
let source_order = plan
    .visible_names
    .iter()
    .map(String::as_str)
    .filter(|name| expected_source_order.contains(name))
    .collect::<Vec<_>>();
assert_eq!(source_order, expected_source_order);

plan.assert_registered_contains(&[
    "exec_command",
    "mcp__registry.lookup",
    "registry_extension.lookup",
    "registry_dynamic.lookup",
]);

测试证明的是当前注册顺序和 collision policy;不能据此推断所有外部 namespace 都会以相同顺序注册,除非它们走同一个 append_source_tools。

3. Handler夹具 ​

3.1 直接调用 ​

handler unit test 通常使用 make_session_and_context;需要观察事件时使用 make_session_and_context_with_rx。测试必须自己构造 ToolInvocation,显式提供 payload 和 call id,这样错误能定位到 handler 的输入边界。

源码位置:codex-rs/core/src/session/tests.rs :: make_session_and_context_with_rx

rust
let (session, turn, rx) = make_session_and_context_with_rx().await;
let invocation = ToolInvocation {
    session,
    step_context: StepContext::for_test(Arc::clone(&turn)),
    turn,
    cancellation_token: CancellationToken::new(),
    tracker: Arc::new(Mutex::new(TurnDiffTracker::new())),
    call_id: "call-test".to_string(),
    tool_name: ToolName::plain("extension_echo"),
    source: ToolCallSource::Direct,
    payload: ToolPayload::Function {
        arguments: json!({"message": "hello"}).to_string(),
    },
};

3.2 输入断言 ​

payload 错误、非法 JSON、空字段和 unsupported mode 应该分别测试。不要只测试一个成功 JSON,因为很多 handler 的真正边界是 ToolPayload 变体与 parser 选择。

源码位置:codex-rs/core/src/tools/handlers/mcp_resource_tests.rs :: parse_arguments_handles_empty_and_json

rust
assert!(
    parse_arguments(" \n\t").unwrap().is_none(),
    "expected None for empty arguments"
);
assert!(parse_arguments("null").unwrap().is_none());

let value = parse_arguments(r#"{"server":"figma"}"#)
    .expect("parse json")
    .expect("value present");
assert_eq!(value["server"], json!("figma"));

断言应说明 parser 的实际契约:空字符串/null 是“无参数”,非法 JSON 是 RespondToModel,而不是笼统写成“参数校验通过/失败”。

3.3 事件断言 ​

有生命周期 item 的 handler,要按顺序读取 event receiver,分别断言 started、completed 和错误状态。MCP Resource 的公共 run_resource_operation 就把 begin/end 统一到了同一夹具。

源码位置:codex-rs/core/src/tools/handlers/mcp_resource.rs :: run_resource_operation

rust
emit_tool_call_begin(session, turn, call_id, invocation.clone()).await;
let start = Instant::now();
let result = operation.await.and_then(|payload| {
    serialize_function_output(payload, turn.model_info.truncation_policy.into())
});

match result {
    Ok(output) => {
        emit_tool_call_end(
            session,
            turn,
            call_id,
            invocation,
            start.elapsed(),
            Ok(call_tool_result_from_content(&content, output.success)),
        )
        .await;
        Ok(boxed_tool_output(output))
    }
    Err(error) => {
        emit_tool_call_end(
            session,
            turn,
            call_id,
            invocation,
            start.elapsed(),
            Err(error.to_string()),
        )
        .await;
        Err(error)
    }
}

3.4 Accepted回调 ​

某些 runtime 还实现 on_tool_result_accepted。它发生在 handler 返回成功、PostToolUse 没有 block、可选 feedback 已替换模型输出之后;因此不能只在 handler unit test 中直接调用 callback,就宣称 registry 顺序正确。

源码位置:codex-rs/core/src/tools/registry.rs :: ToolRegistry::dispatch_any_with_terminal_outcome

rust
match result {
    Ok(mut result) => {
        if let Some(outcome) = post_tool_use_outcome {
            if outcome.should_block {
                let message = outcome.feedback_message.unwrap_or_else(|| {
                    "PostToolUse hook blocked the tool result".to_string()
                });
                return Err(FunctionCallError::RespondToModel(message));
            }
            if let Some(feedback_message) = outcome.feedback_message {
                result.result = Box::new(PostToolUseFeedbackOutput {
                    original: result.result,
                    model_visible: FunctionToolOutput::from_text(
                        feedback_message,
                        /*success*/ None,
                    ),
                });
            }
        }
        tool.on_tool_result_accepted(&invocation, result.result.as_ref());
        dispatch_trace.record_completed(/* ... */);
        Ok(result)
    }
    Err(err) => Err(err),
}

测试这类 runtime 时至少需要三种输入:成功并接受、PostToolUse block、handler/结果自身失败。只有第一种应触发 accepted callback。Node/CUA REPL 的 MCP 结果收集就是这一顺序的实际消费者。

4. 模型集成 ​

4.1 Mock Responses ​

Core 集成测试使用 mount_sse_once 或 mount_sse_sequence 定义模型响应。第一段 SSE 产生 function call,测试提交 user turn,第二段 SSE 关闭下一次 sampling;ResponseMock 保存所有 Responses request,供断言工具输出和可见 tools。

源码位置:codex-rs/core/tests/common/responses.rs :: ResponseMock

rust
pub fn requests(&self) -> Vec<ResponsesRequest> {
    self.requests.lock().unwrap().clone()
}

pub fn function_call_output_text(&self, call_id: &str) -> Option<String> {
    self.requests()
        .iter()
        .find_map(|req| req.function_call_output_text(call_id))
}

测试必须区分第一请求和后续请求:第一请求证明工具是否出现在 model-visible tools;后续请求证明 output 是否正确回灌。只检查最终 TurnComplete 无法证明 handler 被调用。

4.2 正常回路 ​

view_image、Dynamic Tool、MCP Tool 和 Plugin install 测试都采用同样的三段思想:模型调用 → 工具运行 → 结果回灌。不同之处在中间 owner:文件系统、外部客户端、MCP server 或 elicitation。

4.3 失败回路 ​

失败测试要检查“模型看到了什么”和“外部副作用是否发生”。例如 App-only MCP direct call 测试不仅断言 output 包含 unsupported call,还断言 MCP server 没有收到调用;Plugin install refresh 测试则区分用户 Accept 与最终 completed。

5. 协议集成 ​

5.1 App Server夹具 ​

App Server TestAppServer 提供初始化、thread start、turn start、notification 读取、server request 读取和 response 提交。动态工具测试通过它验证 item/started 与 ServerRequest::DynamicToolCall 字段一致,再提交客户端 response。

源码位置:codex-rs/app-server/tests/common/test_app_server.rs :: TestAppServerBuilder

rust
pub async fn build_initialized(self) -> anyhow::Result<TestAppServer> {
    self.build_initialized_with_timeout(DEFAULT_REQUEST_TIMEOUT)
        .await
}

5.2 Round-trip断言 ​

协议测试的 arrange 必须包括 client capability、thread configuration、mock server 和 expected notification;action 是 JSON-RPC request/response;assert 需要覆盖公开字段、request correlation 和最终模型 request。

源码位置:codex-rs/app-server/tests/suite/v2/dynamic_tools.rs :: dynamic_tool_call_round_trip_sends_text_content_items_to_model

rust
let started = wait_for_dynamic_tool_started(&mut mcp, call_id).await?;
assert_eq!(started.status, DynamicToolCallStatus::InProgress);
assert_eq!(started.namespace.as_deref(), Some(tool_namespace));
assert_eq!(started.tool, tool_name);
assert_eq!(started.arguments, tool_args);

let request = timeout(
    DEFAULT_READ_TIMEOUT,
    mcp.read_stream_until_request_message(),
)
.await??;
let (request_id, params) = match request {
    ServerRequest::DynamicToolCall { request_id, params } => (request_id, params),
    other => panic!("expected DynamicToolCall request, got {other:?}"),
};
assert_eq!(params.call_id, call_id);
assert_eq!(params.arguments, tool_args);

5.3 未连接边界 ​

App Server unknown-thread 测试证明 request processor 的错误协议,不证明 Core tool handler 的模型错误文本。两者都可能叫“thread not found”,但一个是 JSON-RPC error,一个是 function output;测试命名和断言应保持层级清晰。

6. 失败矩阵 ​

6.1 输入失败 ​

输入失败包括 unsupported payload、invalid JSON、缺少必填字段、zero/越界 timeout 和未知 target。断言应检查错误类型(通常 RespondToModel)及是否没有发出外部请求。

6.2 Gate失败 ​

gate failure 包括 feature disabled、没有 environment/server、provider 不支持 namespace、ToolSearch/Apps/Plugins 条件不满足以及 Agent Plugin budget 超限。它们通常表现为工具不在 visible/registered 集合,而不是 handler 错误。

6.3 运行失败 ​

运行失败包括 server startup/transport error、catalog revision change、client response invalid、MCP is_error、用户取消和 turn cancellation。应分别观察 lifecycle item、模型 output、日志或 pending cleanup,不把它们统称为“工具失败”。

6.4 资源清理 ​

异步 handler 测试应检查 receiver/guard/drop 后的状态:request_user_input 清 pending sender,MCP/Resource operation 发 completed failed item,Dynamic Tool drop oneshot 返回取消,App Server cancel_requests_for_thread 清空 request waiter。没有清理断言,测试只证明了 happy path。

6.5 夹具未启动 ​

测试进程退出非零不一定表示目标行为失败。应先判断是否到达业务断言:

  • could not locate binary "test_stdio_server" 表示辅助 binary 尚未构建,可先运行 cargo build -p codex-rmcp-client --bin test_stdio_server;
  • wiremock “expected requests, received 0” 或 App Server initialize timeout 表示 fixture/进程没有进入目标调用;
  • macOS 默认测试线程的 stack overflow 可用仓库测试惯例 RUST_MIN_STACK=8388608 复跑;
  • skip_if_no_network、skip_if_remote、skip_if_wine_exec 说明平台前置不满足,不能把 skip 计作行为通过。

只有复跑进入目标 assert 后,才能讨论实现正确性。记录测试结果时应区分 passed、failed at assertion、failed before assertion 和 skipped;否则会把环境故障包装成源码回归,或把未执行断言包装成通过。

源码位置:

  • codex-rs/core/tests/common/lib.rs :: stdio_server_bin
  • codex-rs/core/tests/common/lib.rs :: skip_if_no_network
  • codex-rs/app-server/tests/common/test_app_server.rs :: TestAppServerBuilder::build_initialized_with_timeout

7. 可执行矩阵 ​

7.1 新工具最小集 ​

一个新 function handler 至少需要四组基础测试;使用统一 lifecycle、trace 或 App Server 时继续增加对应层:

测试ArrangeAssert不能证明
specoptions/configToolSpec 字段runtime 注册
planfeature/source/providervisible/registered/exposure外部副作用
handlerToolInvocation/payloadoutput/event/cleanup模型下一次请求
lifecycleHook rewrite/block + contributorstart/finish/outcomehandler 业务正确性
traceDirect/Code Mode requesterterminal status/result projectionpublic protocol
integrationmock Responses + fixturecall/output/history真实 provider 质量
protocolTestAppServer + client capabilityJSON-RPC/notification/request correlationCore 内部状态全貌

若工具经过 App Server,再增加 protocol round-trip;若工具有跨进程/远端 client,再增加 transport fixture。

7.2 命令与范围 ​

在 Codex 源码仓库的 codex-rs/ 目录运行以下示例:

bash
rg -n "ToolPlanProbe|ToolInvocation|ToolLifecycleContributor|ToolDispatchTrace|TestAppServer" core app-server
cargo test -p codex-core --lib 'tools::handlers::mcp_resource::tests' -- --test-threads=1
cargo test -p codex-core --lib 'tools::spec_plan::tests::request_plugin_install_' -- --test-threads=1
RUST_MIN_STACK=8388608 cargo test -p codex-core --test all tool_start_receives_ -- --test-threads=1
cargo test -p codex-core --lib 'tools::tool_dispatch_trace::tests::' -- --test-threads=1
RUST_MIN_STACK=8388608 cargo test -p codex-app-server --test all dynamic_tool_call_round_trip_sends_text_content_items_to_model -- --test-threads=1

第一条命令帮助定位夹具;后续测试分别覆盖 helper、plan gate、lifecycle、trace 和 App Server round-trip。测试依赖 mock/fixture,不证明第三方服务、真实 OAuth、操作系统 sandbox 或模型决策质量。

8. 源码练习 ​

先选一个本系列工具,写出七个断言:ToolSpec、注册/exposure、handler 输入、lifecycle outcome、dispatch requester/result、模型回灌和公开协议。然后为每个断言指定最小测试层,不要用一个集成测试代替所有局部验证。

再比较两个失败现象:

  • 工具不在首个 tools 数组中:先查 ToolPlanProbe 的 registered/exposure,再查 ToolSearch,而不是直接查 handler;
  • 工具已经发出 function call 但下一次请求没有预期 output:检查 ResponseMock.function_call_output_text、handler 的 ToolOutput::to_response_item 和 call id 配对,而不是只看 TurnComplete。
  • 测试进程非零退出但没有目标 assertion:先检查辅助 binary、mock request count、initialize timeout、skip macro 和线程栈,再判断是不是实现回归。

这样建立的测试矩阵,才能把“源码事实”“模型可见行为”“外部副作用”和“未证明边界”分开,继续为后续 EXE、SEC、APS 专题提供可复用的阅读方法。