工具框架测试策略
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
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
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
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
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_startcodex-rs/ext/extension-api/src/contributors/tool_lifecycle.rs :: ToolCallOutcomecodex-rs/core/tests/suite/tool_lifecycle.rs :: tool_start_receives_rewritten_payload_and_post_hook_history
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
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
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。以下是当前测试常见的矩阵:
| 情形 | visible | registered | exposure |
|---|---|---|---|
| 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
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
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
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
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
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
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
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
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_bincodex-rs/core/tests/common/lib.rs :: skip_if_no_networkcodex-rs/app-server/tests/common/test_app_server.rs :: TestAppServerBuilder::build_initialized_with_timeout
7. 可执行矩阵
7.1 新工具最小集
一个新 function handler 至少需要四组基础测试;使用统一 lifecycle、trace 或 App Server 时继续增加对应层:
| 测试 | Arrange | Assert | 不能证明 |
|---|---|---|---|
| spec | options/config | ToolSpec 字段 | runtime 注册 |
| plan | feature/source/provider | visible/registered/exposure | 外部副作用 |
| handler | ToolInvocation/payload | output/event/cleanup | 模型下一次请求 |
| lifecycle | Hook rewrite/block + contributor | start/finish/outcome | handler 业务正确性 |
| trace | Direct/Code Mode requester | terminal status/result projection | public protocol |
| integration | mock Responses + fixture | call/output/history | 真实 provider 质量 |
| protocol | TestAppServer + client capability | JSON-RPC/notification/request correlation | Core 内部状态全貌 |
若工具经过 App Server,再增加 protocol round-trip;若工具有跨进程/远端 client,再增加 transport fixture。
7.2 命令与范围
在 Codex 源码仓库的 codex-rs/ 目录运行以下示例:
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 专题提供可复用的阅读方法。
