MCP server 可执行文件集成测试
这一阶段是给 codex MCP server 这个可执行程序做端到端体检,属于幕后保证质量的测试部分。all.rs 和 mod.rs 像总开关,把整套测试串起来。common/lib.rs 提供公共工具箱;mcp_process.rs 真正启动服务器子进程,用 JSON-RPC 这种“按格式传纸条”的方式对话;mock_model_server.rs 和 responses.rs 假扮外部 AI,并吐出预设流式回复。最后 codex_tool.rs 检查危险命令、代码补丁和用户指令传递是否都按规矩走。
测试套件入口点
这些文件组装集成测试二进制文件,并公开测试工具运行的套件模块。
mcp-server/tests/all.rs源码 ↗
这个文件本身不写具体测试,它更像一本试卷的目录页。项目里的集成测试放在 tests/suite/ 目录中,而这里用 mod suite; 告诉 Rust 测试工具:请把那个测试模块也编进来一起跑。文件开头的 #![allow(clippy::expect_used)] 是给代码检查工具 Clippy 的说明:测试代码里允许使用 expect,也就是“如果这里失败,就直接报出指定错误信息”。这在测试里很常见,因为测试本来就希望失败时立刻说明哪里不对。没有这个文件,测试模块可能不会被这个集成测试二进制统一收进来,开发者运行测试时就不一定能覆盖整套测试。
mcp-server/tests/suite/mod.rs源码 ↗
这个文件本身几乎不写具体测试,它更像一本书的目录页。mod codex_tool; 的意思是:把同一测试套件里的 codex_tool 模块接进来。Rust 里模块就像文件夹或章节,只有被声明出来,编译器才知道要把它算作当前测试包的一部分。所以它的价值不在于“做事”,而在于“把测试组织起来”。当开发者运行这组测试时,Rust 会先读到这个入口,再顺着它去加载 codex_tool 里的测试内容。这样测试代码可以拆成多个文件,避免全部挤在一个大文件里,也让后续新增测试更清楚。
共享集成测试工具
这些通用工具提供类型化 JSON-RPC 解码、子进程 MCP 传输、模拟模型服务,以及各测试共用的预设流式响应。
mcp-server/tests/common/lib.rs源码 ↗
测试 MCP 服务时,很多测试都会做类似的事:启动一个测试用的 MCP 进程,准备一个假的模型服务器,伪造几种服务器返回的消息,再把收到的 JSON-RPC 响应变成测试真正想检查的 Rust 类型。这个文件就像测试目录里的前台接待,把这些常用零件统一摆出来,别的测试文件只要引用这里就能用。它还提供了 to_response,专门把通用的 JSON-RPC 响应拆开,取出里面的 result,再转换成调用方指定的具体数据类型。JSON-RPC 可以理解成一种“用 JSON 写的请求和回复格式”,而这里的转换就是把信封里的内容拿出来,翻译成测试能直接比较的对象。
to_response18–24 ↗
fn to_response(
response: JsonRpcResponse<serde_json::Value>,
) -> anyhow::Result<T>
作用:这个函数把一个通用的 JSON-RPC 回复转换成测试想要的具体类型。测试拿到回复后,不用手动拆 JSON、转类型,直接调用它就能得到更好检查的数据。
数据流:输入是一份 JsonRpcResponse,里面的 result 还是比较通用的 JSON 值。函数先用 to_value 把 result 变成标准 JSON 值,再用 from_value 按调用方要求的类型 T 反序列化,也就是把 JSON 翻译成 Rust 里的具体结构。成功时输出这个具体对象;如果 JSON 格式不对或类型对不上,就返回错误,不会偷偷给出错误数据。
调用关系:它是测试公共工具箱里的一个小帮手。测试代码在收到 MCP 服务返回的 JSON-RPC 响应后会用它做最后一步转换;它自己不决定响应内容,只把转换工作交给 serde_json::to_value 和 serde_json::from_value 这两个 JSON 序列化/反序列化工具来完成。
调用图:外部调用 2 个(from_value, to_value)。
mcp-server/tests/common/mcp_process.rs源码 ↗
测试 MCP 服务器时,光调用内部函数不够,因为真正出问题的地方常常在“进程怎么启动、协议消息怎么收发、退出时有没有清干净”。这个文件把这些麻烦事包成 McpProcess。它会找到 codex-mcp-server 可执行文件,带着 CODEX_HOME 等环境变量启动它,把服务器的标准输入当作“写信口”,标准输出当作“收信口”。双方说的是 JSON-RPC,也就是一种用 JSON 包装请求、响应和通知的通用消息格式。它还能完成 MCP 初始化握手,发送 codex 工具调用,等待指定响应或通知。最后在对象销毁时,它会主动杀掉子进程并短暂等待,避免测试结束后留下僵尸进程,导致偶发失败。
McpProcess::new47–49 ↗
async fn new(codex_home: &Path) -> anyhow::Result<Self>
作用:用最普通的方式启动一个测试用 MCP 服务器。测试只需要给它一个 Codex 的家目录,不需要关心环境变量细节。
数据流:输入一个 codex_home 路径 → 它把这个路径交给 McpProcess::new_with_env,并且不额外修改环境变量 → 返回一个已经连好标准输入和标准输出的 McpProcess,或者返回启动失败的错误。
调用关系:这是最常用的入口。codex_tool_passes_base_instructions 和 create_mcp_process 这类测试辅助会调用它;它自己不直接启动进程,而是把具体工作交给 McpProcess::new_with_env。
调用图:被 2 处调用(codex_tool_passes_base_instructions, create_mcp_process);外部调用 1 个(new_with_env)。
McpProcess::new_with_env56–111 ↗
async fn new_with_env(
codex_home: &Path,
env_overrides: &[(&str, Option<&str>)],
) -> anyhow::Result<Self>
作用:启动一个 MCP 服务器子进程,并允许测试临时改掉或删除某些环境变量。这样测试可以模拟不同运行环境,比如某个变量不存在时服务器会怎样。
数据流:输入 codex_home 和一组环境变量覆盖规则 → 它找到 codex-mcp-server 程序,设置标准输入、标准输出、标准错误,写入 CODEX_HOME 和 RUST_LOG,再应用额外环境变量改动 → 启动子进程,取出它的输入输出管道,把标准错误转发到测试日志,最后返回 McpProcess。
调用关系:McpProcess::new 会把默认启动流程交给它。它是整个测试连接的搭建者,后面的 initialize、send_jsonrpc_message、read_jsonrpc_message 都依赖它保存下来的 stdin、stdout 和子进程句柄。
调用图:外部调用 7 个(new, new, piped, new, cargo_bin, eprintln!, spawn)。
McpProcess::initialize114–185 ↗
async fn initialize(&mut self) -> anyhow::Result<()>
作用:和刚启动的 MCP 服务器做初始化握手。就像客户端先自我介绍、确认协议版本,再等服务器回一句“我准备好了”。
数据流:它读取当前请求编号和客户端能力信息 → 组装 initialize 请求,声明支持表单 elicitation(可以理解为服务器向客户端要补充信息的能力),发送给服务器 → 读取服务器响应,检查协议版本、服务器名称、工具能力和 user agent 是否符合预期 → 再发送 initialized 通知作为确认。
调用关系:测试通常在启动 McpProcess 后先调用它,确保服务器进入可用状态。它通过 McpProcess::send_jsonrpc_message 发消息,通过 McpProcess::read_jsonrpc_message 收响应,并使用 originator 等信息拼出预期的 user agent。
调用图:调用 3 个内部函数(originator, read_jsonrpc_message, send_jsonrpc_message);外部调用 15 个(fetch_add, default, new, new, new, new, Notification, Request, bail!, Number (+5 more))。
McpProcess::send_codex_tool_call189–204 ↗
async fn send_codex_tool_call(
&mut self,
params: CodexToolCallParam,
) -> anyhow::Result<i64>
作用:向服务器发起一次 codex 工具调用。对测试来说,这就是模拟客户端请求服务器执行 Codex 的核心功能。
数据流:输入 CodexToolCallParam,也就是一次 codex 工具调用需要的参数 → 它把参数转成 JSON 对象,包装成 MCP 的 tools/call 请求,并指定工具名为 codex → 返回这次请求使用的编号,方便之后把响应或通知对上号。
调用关系:它是更友好的专用接口,内部把通用请求细节交给 McpProcess::send_request。测试如果要触发 codex 工具行为,通常会通过它开始。
调用图:调用 1 个内部函数(send_request);外部调用 3 个(new, to_value, unreachable!)。
McpProcess::send_request206–220 ↗
async fn send_request(
&mut self,
method: &str,
params: Option<serde_json::Value>,
) -> anyhow::Result<i64>
作用:发送一条通用 JSON-RPC 请求,并自动分配请求编号。请求编号像取餐号,用来把之后回来的响应和这次请求配对。
数据流:输入方法名和可选参数 → 它从 next_request_id 里取一个新编号,组装成 JSON-RPC 2.0 请求消息 → 调用 McpProcess::send_jsonrpc_message 写给服务器 → 返回刚才分配的请求编号。
调用关系:McpProcess::send_codex_tool_call 会调用它来发送 tools/call。它位于“具体业务请求”和“底层写入管道”之间,把请求编号和 JSON-RPC 外壳补齐。
调用图:调用 1 个内部函数(send_jsonrpc_message);被 1 处调用(send_codex_tool_call);外部调用 4 个(fetch_add, new, Request, Number)。
McpProcess::send_response222–233 ↗
async fn send_response(
&mut self,
id: RequestId,
result: serde_json::Value,
) -> anyhow::Result<()>
作用:给服务器回一条 JSON-RPC 响应。某些场景里服务器会反过来向客户端发请求,测试客户端就需要用这个函数答复它。
数据流:输入要回复的请求 id 和结果 JSON → 它把它们包装成 JSON-RPC response 消息 → 通过 McpProcess::send_jsonrpc_message 写入服务器的标准输入。
调用关系:它和 send_request 是一对:send_request 是测试主动问服务器,send_response 是服务器问测试时测试作答。底层发送动作仍由 McpProcess::send_jsonrpc_message 完成。
调用图:调用 1 个内部函数(send_jsonrpc_message);外部调用 1 个(Response)。
McpProcess::send_jsonrpc_message235–245 ↗
async fn send_jsonrpc_message(
&mut self,
message: JsonRpcMessage<CustomRequest, serde_json::Value, CustomNotification>,
) -> anyhow::Result<()>
作用:把一条 JSON-RPC 消息真正写进服务器的标准输入。它是所有发送动作最后都会经过的“信封投递口”。
数据流:输入一个已经组装好的 JSON-RPC 消息 → 它先打印调试日志,再把消息序列化成一行 JSON 文本,写入 stdin,追加换行,并 flush(强制送出去,避免卡在缓冲区)→ 成功时没有额外结果,失败时返回错误。
调用关系:initialize、send_request、send_response 都调用它。上层函数决定发什么,它负责把消息按服务器能读懂的格式送到进程里。
调用图:被 3 处调用(initialize, send_request, send_response);外部调用 4 个(flush, write_all, eprintln!, to_string)。
McpProcess::read_jsonrpc_message247–257 ↗
async fn read_jsonrpc_message(
&mut self,
) -> anyhow::Result<JsonRpcMessage<CustomRequest, serde_json::Value, CustomNotification>>
作用:从服务器标准输出读出一条 JSON-RPC 消息。它是所有接收动作的基础。
数据流:它从 stdout 读取一整行文本 → 把这行 JSON 解析成请求、响应或通知这类结构化消息 → 打印调试日志,并把解析后的消息返回给调用者。
调用关系:initialize 用它读取初始化响应;几个 read_stream_until_* 函数也反复调用它,在消息流里寻找自己关心的那一条。
调用图:被 4 处调用(initialize, read_stream_until_legacy_task_complete_notification, read_stream_until_request_message, read_stream_until_response_message);外部调用 3 个(read_line, new, eprintln!)。
McpProcess::read_stream_until_request_message259–282 ↗
async fn read_stream_until_request_message(
&mut self,
) -> anyhow::Result<JsonRpcRequest<CustomRequest>>
作用:一直读服务器输出,直到遇到一条“服务器发来的请求”。它会跳过普通通知,但遇到不该出现的响应或错误会立刻失败。
数据流:没有额外输入 → 它循环调用 McpProcess::read_jsonrpc_message,一条条看服务器输出 → 如果是通知就记录后继续等;如果是请求就返回;如果是错误或响应,就返回测试失败的错误。
调用关系:当测试预期服务器会反向询问客户端时,会用它等待那次请求。它把底层读消息函数包成了“只等请求”的专用等待器。
调用图:调用 1 个内部函数(read_jsonrpc_message);外部调用 2 个(bail!, eprintln!)。
McpProcess::read_stream_until_response_message284–309 ↗
async fn read_stream_until_response_message(
&mut self,
request_id: RequestId,
) -> anyhow::Result<JsonRpcResponse<serde_json::Value>>
作用:一直读服务器输出,直到等到指定请求编号对应的响应。这样测试不会被别的通知干扰。
数据流:输入一个 request_id → 它循环读取 JSON-RPC 消息,通知会被忽略并记录;如果读到响应,就检查 id 是否和目标一致 → 匹配时返回这个响应;如果读到请求或错误,就认为流程不符合预期并失败。
调用关系:测试在调用 send_request 或 send_codex_tool_call 得到编号后,可以用它等待对应结果。它依赖 McpProcess::read_jsonrpc_message 持续读取消息流。
调用图:调用 1 个内部函数(read_jsonrpc_message);外部调用 2 个(bail!, eprintln!)。
McpProcess::read_stream_until_legacy_task_complete_notification313–353 ↗
async fn read_stream_until_legacy_task_complete_notification(
&mut self,
) -> anyhow::Result<JsonRpcNotification<CustomNotification>>
作用:等待旧格式的“任务完成”通知。这里的旧格式是 method 为 codex/event,并且 params.msg.type 等于 task_complete。
数据流:没有额外输入 → 它循环读取服务器消息,只接受通知 → 对每条通知检查方法名和内部字段;匹配 task_complete 时返回该通知,不匹配就忽略;如果读到请求、响应或错误,就让测试失败。
调用关系:一些测试需要确认一次 Codex 任务真的结束了,但服务器可能还会夹杂发出别的通知。这个函数专门负责在通知流里筛出旧版任务完成事件,底层读取仍交给 McpProcess::read_jsonrpc_message。
调用图:调用 1 个内部函数(read_jsonrpc_message);外部调用 2 个(bail!, eprintln!)。
McpProcess::drop357–383 ↗
fn drop(&mut self)
作用:在 McpProcess 被丢弃时清理子进程,避免测试结束后 codex-mcp-server 还活着。它是防止测试偶发泄漏报警的重要保险。
数据流:输入是即将被销毁的 McpProcess 自身 → 它先请求杀掉子进程,然后在最多 5 秒内反复询问操作系统进程是否已经退出,中间短暂睡眠避免空转 → 如果进程退出、查询出错或超时,就结束清理流程。
调用关系:这是 Rust 的 Drop 清理钩子,不由测试手动调用,而是在 McpProcess 生命周期结束时自动运行。它补强了启动时设置的 kill_on_drop,因为 Tokio 的自动杀进程只是尽力而为,不能保证立刻清干净。
调用图:外部调用 6 个(start_kill, try_wait, sleep, from_millis, from_secs, now)。
mcp-server/tests/common/mock_model_server.rs源码 ↗
真实模型服务不适合直接拿来跑自动测试:它可能慢、贵、不稳定,还会受网络影响。这个文件用 wiremock(一个用来伪装 HTTP 服务的测试工具)启动一个本地假服务器,只监听 /v1/responses 这个接口,并且只接受 POST 请求。测试传进来一组字符串作为“模型回复”,服务器每收到一次请求,就按顺序吐出下一条回复。这里的 SeqResponder 像一个排队发号的柜台:它用原子计数器(一种能安全记录次数的数字,避免并发时数错)记住当前是第几次调用,然后取对应的回复返回。返回内容的类型被设置成 text/event-stream,也就是模拟流式事件数据,贴近真实模型接口的行为。这样测试既可控,又能验证代码确实发了预期次数的请求。
create_mock_responses_server13–30 ↗
async fn create_mock_responses_server(responses: Vec<String>) -> MockServer
作用:创建一个测试用的假模型服务器,并把传入的多条回复排好队。测试代码会用它来代替真实的 /v1/responses 接口。
数据流:输入是一组字符串,每个字符串代表一次假模型回复。函数先启动一个本地 mock 服务器,再把这些回复放进 SeqResponder,并设置规则:只有 POST 到 /v1/responses 的请求才会被它接住;预期请求次数等于回复数量。最后输出这个已经挂好规则的 MockServer,供测试把程序指向它。
调用关系:它通常在测试开始时被调用,负责把测试环境搭起来。它会借助 wiremock 的 start、given、method、path 等工具设置服务器和匹配规则;之后真正收到请求时,回应工作会交给 SeqResponder::respond。
SeqResponder::respond38–47 ↗
fn respond(&self, _: &wiremock::Request) -> ResponseTemplate
作用:在假服务器收到一次请求时,取出下一条预设回复并返回。它保证多次请求会按准备好的顺序拿到不同内容。
数据流:输入是一次 HTTP 请求,但这里不需要读取请求内容。函数先把内部调用次数加一,并拿到这次调用的编号;再用这个编号从回复列表里取对应字符串。如果没有对应回复,就说明测试请求次数超出了预期,会直接报错。最后它返回一个 HTTP 200 响应,内容类型是 text/event-stream,正文就是那条预设回复。
调用关系:它不是测试直接调用的函数,而是被 wiremock 在请求到达时自动调用。create_mock_responses_server 先把它装到 mock 服务器上;之后每个匹配 /v1/responses 的 POST 请求都会进入这里,由它生成具体的假模型响应。
调用图:外部调用 2 个(fetch_add, new)。
mcp-server/tests/common/responses.rs源码 ↗
测试里常常需要模拟一个助手的回答,比如“我要执行这条 shell 命令”“这是最终文本回复”“我要应用一段补丁”。这个文件就是专门做这种模拟数据的。SSE 是 Server-Sent Events 的缩写,可以理解成服务器一条一条往外吐消息的格式,像流水账一样记录“回复开始了”“调用某个工具了”“回复结束了”。这里的几个函数会把命令、工作目录、超时时间、补丁内容等信息转成 JSON 字符串,再包进一串 SSE 事件里。这样测试代码拿到的就是一份看起来很像真实服务返回的数据,可以检查服务器后续处理是否正确。它的重要点是:测试不依赖真实网络和真实模型,结果更稳定,也更容易覆盖各种场景。
create_shell_command_sse_response6–24 ↗
fn create_shell_command_sse_response(
command: Vec<String>,
workdir: Option<&Path>,
timeout_ms: Option<u64>,
call_id: &str,
) -> anyhow::Result<String>
作用:造一段“助手要求执行 shell 命令”的假 SSE 回复。测试需要确认系统能正确识别并执行命令时,会用到它。
数据流:输入是一组命令参数、可选工作目录、可选超时时间,以及一次工具调用的编号。它先用 shlex::try_join 把命令参数安全地拼成一行 shell 命令,再用 json! 和 serde_json::to_string 把命令、目录、超时包装成 JSON 文本;接着用 format! 生成回复编号,最后把“回复创建”“函数调用 shell_command”“回复完成”三段事件交给 responses::sse 拼成一整段 SSE 字符串。输出是这段可供测试使用的模拟响应;如果命令拼接或 JSON 转换失败,就返回错误。
调用关系:它位于测试准备阶段,通常由测试代码在需要模拟“模型要求跑命令”时调用。它自己不执行命令,只是把事件格式拼好;具体的 SSE 外壳交给 core_test_support::responses 里的 sse 和事件构造函数来生成。
调用图:调用 1 个内部函数(sse);外部调用 5 个(format!, json!, to_string, try_join, vec!)。
create_final_assistant_message_sse_response26–33 ↗
fn create_final_assistant_message_sse_response(message: &str) -> anyhow::Result<String>
作用:造一段“助手给出最终文字回答”的假 SSE 回复。测试只关心普通文本回复,而不是工具调用时,会用到它。
数据流:输入是一段最终要显示给用户的文字。它固定使用 resp-final 作为回复编号,依次生成“回复创建”“助手消息”“回复完成”三类事件,再交给 responses::sse 合成一段 SSE 字符串。输出是这段模拟的最终回答流;这个函数本身没有复杂转换,正常情况下直接返回成功结果。
调用关系:它是测试里最简单的响应生成器,用来模拟模型不调用工具、直接说完话的情况。它把事件拼装的细节交给 core_test_support::responses,测试代码只需要提供消息内容。
create_apply_patch_sse_response35–47 ↗
fn create_apply_patch_sse_response(
patch_content: &str,
call_id: &str,
) -> anyhow::Result<String>
作用:造一段“助手要求通过 shell 命令应用补丁”的假 SSE 回复。测试需要模拟修改文件的场景时,会用到它。
数据流:输入是一段补丁内容和一次工具调用编号。它先用 format! 把补丁包成一条 apply_patch <<'EOF' ... EOF 形式的 shell 命令,这种写法像把一整段文本递给命令处理;然后用 json! 和 serde_json::to_string 把命令放进 JSON 参数里;再生成回复编号,最后拼出“回复创建”“调用 shell_command”“回复完成”的 SSE 字符串。输出是模拟的流式工具调用回复;如果 JSON 序列化失败,就返回错误。
调用关系:它服务于测试中的“文件补丁”场景。虽然表面上是在模拟 apply_patch,但在事件里仍然表现为一次 shell_command 工具调用;SSE 包装仍由 core_test_support::responses 完成,这个函数主要把补丁内容变成合适的命令参数。
调用图:调用 1 个内部函数(sse);外部调用 4 个(format!, json!, to_string, vec!)。
Codex 工具端到端测试
此测试模块使用共享测试工具,根据模拟后端响应和审批流程验证完整的 MCP codex 工具行为。
mcp-server/tests/suite/codex_tool.rs源码 ↗
这个文件像一套“验收演练”。测试不会真的去调用线上模型,而是启动一个假的模型服务器,让它按剧本返回“我要执行命令”“我要改文件”“最终回答”等消息。然后测试再启动真实的 MCP 进程,通过 JSON-RPC(一种用 JSON 发请求和响应的通信格式)给它发送 codex 工具请求。重点是看 MCP 遇到可能有风险的动作时,会不会先发出 elicitation,也就是“请用户批准”的请求;用户批准后,命令或补丁是不是真的执行;最后原来的工具调用是不是能拿到正确回答。文件里还会临时创建目录和配置文件,避免污染真实环境。整体作用是防止安全审批、补丁应用、指令传递这些核心链路悄悄坏掉。
test_shell_command_approval_triggers_elicitation40–53 ↗
async fn test_shell_command_approval_triggers_elicitation()
作用:这是“执行普通 shell 命令前必须先征求批准”的测试入口。shell 命令就是在系统命令行里跑的命令,比如创建文件;这个测试确认不受信任的命令不会被 MCP 偷偷执行。
数据流:它先读取环境变量,看看当前是不是在禁止联网的 Codex 沙箱里;如果是,就打印提示并跳过。否则它把真正的测试工作交给 shell_command_approval_triggers_elicitation,如果那边出错,就让测试失败并显示明确原因。
调用关系:测试框架会直接运行这个函数。它自己只做环境检查和错误包装,核心流程交给 shell_command_approval_triggers_elicitation,这样内部函数可以用更方便的错误返回方式。
调用图:调用 1 个内部函数(shell_command_approval_triggers_elicitation);外部调用 2 个(var, println!)。
shell_command_approval_triggers_elicitation55–186 ↗
async fn shell_command_approval_triggers_elicitation() -> anyhow::Result<()>
作用:这个函数完整演练一次:模型要求 Codex 执行一个不受信任的命令,MCP 先向客户端请求批准;客户端批准后,命令才真正运行,并创建出文件。
数据流:它先建一个临时工作目录,准备一个会创建文件的命令;Windows 和其他系统用不同命令。然后它启动带假模型服务器的 MCP 进程,让假模型返回“执行这个命令”的流式响应。接着它发送一次 codex 工具调用,读取 MCP 发出的审批请求,检查请求内容是否和预期一致。随后它模拟用户批准,继续读取任务完成通知和最终响应,最后检查目标文件确实被创建。
调用关系:test_shell_command_approval_triggers_elicitation 会调用它。它依赖 create_mcp_process 搭好测试环境,并用 create_expected_elicitation_request_params 拼出应该收到的审批请求内容;审批通过后,MCP 继续完成原来的 codex 工具调用。
调用图:调用 1 个内部函数(create_mcp_process);被 1 处调用(test_shell_command_approval_triggers_elicitation);外部调用 11 个(default, new, Number, assert!, assert_eq!, cfg!, format_with_current_shell, to_value, try_join, timeout (+1 more))。
create_expected_elicitation_request_params188–214 ↗
fn create_expected_elicitation_request_params(
command: Vec<String>,
workdir: &Path,
codex_mcp_tool_call_id: String,
codex_event_id: String,
thread_id: codex_protocol::ThreadId,
)
作用:这个辅助函数生成“执行命令审批请求”的标准答案,方便测试拿实际收到的 JSON 去对比。它的作用像考试里的参考答案。
数据流:它接收命令、工作目录、原始工具调用编号、Codex 事件编号和会话线程编号。它把这些信息整理成一段给用户看的提示语,比如“允许 Codex 在某目录运行某命令吗?”,再解析命令内容,最后序列化成 JSON 值返回。
调用关系:shell_command_approval_triggers_elicitation 在收到 MCP 的审批请求后调用它,用它生成预期参数。函数内部会调用命令解析工具 parse_command,因为审批请求里不只要原始命令,还要带上解析后的命令信息。
调用图:调用 1 个内部函数(parse_command);外部调用 4 个(to_path_buf, format!, json!, to_value)。
test_patch_approval_triggers_elicitation219–230 ↗
async fn test_patch_approval_triggers_elicitation()
作用:这是“应用代码补丁前必须先征求批准”的测试入口。它确认 Codex 想改文件时,MCP 会先问用户,而不是直接动手。
数据流:它先检查是否处在禁止联网的沙箱环境;如果是,就打印提示并跳过。否则它调用 patch_approval_triggers_elicitation 执行真正测试,若内部返回错误,就让测试失败并说明补丁审批流程不符合预期。
调用关系:测试框架会直接运行这个函数。它和 shell 命令测试入口的结构类似,只负责跳过条件和错误处理,具体流程交给 patch_approval_triggers_elicitation。
调用图:调用 1 个内部函数(patch_approval_triggers_elicitation);外部调用 2 个(var, println!)。
patch_approval_triggers_elicitation232–350 ↗
async fn patch_approval_triggers_elicitation() -> anyhow::Result<()>
作用:这个函数演练一次完整的“申请修改文件、等待批准、批准后真正改文件”的流程。它防止补丁审批这条安全链路失效。
数据流:它先在 Windows 上跳过,因为当前 Windows 下的 PowerShell 补丁命令不会被解析成补丁审批。其他系统上,它创建临时目录和测试文件,写入原始内容,再准备一个把内容改掉的补丁。随后启动 MCP 和假模型服务器,让假模型返回 apply_patch 请求。函数发送 codex 工具调用后,读取 MCP 发来的审批请求,检查里面列出的文件改动是否正确。然后它模拟用户批准,等待原始工具调用完成,最后读取文件,确认内容真的从原始内容变成了修改后内容。
调用关系:test_patch_approval_triggers_elicitation 会调用它。它用 create_mcp_process 启动测试用 MCP,用 create_expected_patch_approval_elicitation_request_params 生成预期审批参数;批准响应发回 MCP 后,MCP 才继续应用补丁并返回最终回答。
调用图:调用 1 个内部函数(create_mcp_process);被 1 处调用(test_patch_approval_triggers_elicitation);外部调用 14 个(default, from, new, new, Number, assert_eq!, cfg!, format!, json!, to_value (+4 more))。
test_codex_tool_passes_base_instructions353–361 ↗
async fn test_codex_tool_passes_base_instructions()
作用:这是“用户传给 codex 工具的基础指令和开发者指令会正确送到模型请求里”的测试入口。它保证上层传入的行为要求不会在 MCP 转发过程中丢失。
数据流:它先用测试辅助宏检查网络条件;如果不能跑就跳过。然后调用 codex_tool_passes_base_instructions 执行真正检查,出错时让测试失败并给出清楚说明。
调用关系:测试框架会直接运行它。它不像前两个测试那样检查审批,而是把流程交给 codex_tool_passes_base_instructions,专门验证模型请求里的指令内容。
调用图:调用 1 个内部函数(codex_tool_passes_base_instructions);外部调用 1 个(skip_if_no_network!)。
codex_tool_passes_base_instructions363–446 ↗
async fn codex_tool_passes_base_instructions() -> anyhow::Result<()>
作用:这个函数确认 base_instructions 和 developer_instructions 没有被 MCP 吃掉,而是进入了发给模型服务的请求。前者可以理解为“系统总原则”,后者是“开发者补充要求”。
数据流:它先启动假模型服务器,并让它最终回复“Enjoy!”。然后创建临时 Codex 配置,启动 MCP 进程并完成初始化。接着它发送一个 codex 工具调用,里面带上基础指令和开发者指令。函数读取 MCP 返回给客户端的最终响应,确认内容正确;然后查看假模型服务器实际收到的请求,检查 instructions 字段是否以基础指令开头,也检查输入消息里是否包含权限说明和开发者指令。
调用关系:test_codex_tool_passes_base_instructions 调用它。它自己直接创建假模型服务器,并调用 create_config_toml 写测试配置;MCP 运行后,假服务器记录请求,函数再回头检查 MCP 是否把指令原样传了过去。
调用图:调用 2 个内部函数(new, create_config_toml);被 1 处调用(test_codex_tool_passes_base_instructions);外部调用 8 个(default, new, Number, assert!, assert_eq!, create_mock_responses_server, timeout, vec!)。
create_expected_patch_approval_elicitation_request_params448–475 ↗
fn create_expected_patch_approval_elicitation_request_params(
changes: HashMap<PathBuf, FileChange>,
grant_root: Option<PathBuf>,
reason: Option<String>,
codex_mcp_tool_call_id: String
作用:这个辅助函数生成“补丁审批请求”的标准 JSON,用来和 MCP 实际发出的请求做精确对比。它保证测试看的不是大概对,而是字段、内容都对。
数据流:它接收文件变更列表、可选的授权根目录、可选原因、原始工具调用编号、事件编号和线程编号。它先拼出给用户看的审批提示;如果有原因,就把原因放在前面。然后它把补丁相关信息、调用编号和线程信息装进 PatchApprovalElicitRequestParams,最后转成 JSON 返回。
调用关系:patch_approval_triggers_elicitation 在检查实际审批请求时会调用它。它不启动进程、不读写文件,只负责把“期望中的补丁审批请求”组装出来。
create_mcp_process489–500 ↗
async fn create_mcp_process(responses: Vec<String>) -> anyhow::Result<McpHandle>
作用:这个辅助函数一键搭好测试用 MCP 环境:假模型服务器、临时配置目录、真实 MCP 进程都在这里启动好。这样多个测试不用重复写同样的启动代码。
数据流:它接收一组假模型要返回的流式响应文本。它先启动 mock server,也就是假服务器;再创建临时 Codex home 目录,写入指向假服务器的配置文件;接着启动 McpProcess 并等待初始化完成。最后它返回一个 McpHandle,里面同时保存 MCP 进程、假服务器和临时目录,避免测试还没结束这些资源就被提前释放。
调用关系:shell_command_approval_triggers_elicitation 和 patch_approval_triggers_elicitation 都会调用它。它内部会调用 create_config_toml 写配置,然后把初始化好的进程交还给测试,让测试继续发送工具调用和读取响应。
调用图:调用 2 个内部函数(new, create_config_toml);被 2 处调用(patch_approval_triggers_elicitation, shell_command_approval_triggers_elicitation);外部调用 3 个(new, create_mock_responses_server, timeout)。
create_config_toml505–528 ↗
fn create_config_toml(codex_home: &Path, server_uri: &str) -> std::io::Result<()>
作用:这个函数写出一份测试专用的 Codex 配置文件,让 MCP 使用假模型服务器,并启用会触发审批的安全策略。没有它,测试可能会连到真实服务,或者走不到要测的审批路径。
数据流:它接收 Codex home 目录和假服务器地址。它在该目录下生成 config.toml,内容指定模型名、审批策略、沙箱策略、模型提供商地址、重试次数等。写入成功就返回成功;如果磁盘写入失败,就返回系统 I/O 错误。
调用关系:create_mcp_process 和 codex_tool_passes_base_instructions 都会调用它。它是启动 MCP 前的准备步骤,确保 MCP 进程读到的是测试控制下的配置,而不是开发者机器上的真实配置。
调用图:被 2 处调用(codex_tool_passes_base_instructions, create_mcp_process);外部调用 3 个(join, format!, write)。