多智能体、协作与远程环境套件
这一阶段像一套“多人协作演练”,属于幕后把关,不是正式干活儿本身。它检查系统能不能安全地让主助手派出子助手:说明文字别误导,子助手数量不能无限膨胀;委托审查时,批准命令、改文件这类关键消息要传回主流程;子助手的启动、结束、通知和配置继承不能乱;批量 CSV 任务要能派发、回写和停止;远程环境也要确认连得对、文件写在对的机器上、权限拦得住。
代理生成基础
这些测试确立如何向模型呈现多代理生成,以及如何在运行时强制执行嵌套执行容量。
core/tests/suite/spawn_agent_description.rs源码 ↗
这个测试文件只在非 Windows 系统上运行。它像一次“彩排”:先搭一个假的服务器,假装远端模型列表里有一个可见模型和一个隐藏模型;再启动一套测试用的 Codex 会话,并打开协作功能。然后它让系统提交一句“hello”,截获发给服务器的请求,从请求里取出 spawn_agent 工具的 description,也就是给模型看的工具说明。接着逐条检查:可见模型要被写进去,隐藏模型不能出现;默认推理强度、可选推理强度、服务档位要写清楚;还要明确告诉模型,除非用户要求或有明确理由,否则不要擅自派生子代理,也不要随便改模型。简单说,这个文件是在防止工具说明“教坏”模型,避免它过度委派任务或暴露不该显示的模型。
spawn_agent_description35–40 ↗
fn spawn_agent_description(body: &Value) -> Option<String>
作用:这个小工具函数从一次请求的 JSON 内容里,把 multi_agent_v1.spawn_agent 这个工具的说明文字取出来。测试要检查的重点就在这段文字,所以先把它方便地挖出来。
数据流:输入是一份 JSON 请求内容 → 它先用 namespace_child_tool 找到指定命名空间(一组工具的分类名)下面名叫 spawn_agent 的工具 → 再读取这个工具里的 description 字段 → 如果字段存在且是文字,就返回这段文字;否则返回空。
调用关系:主测试 spawn_agent_description_lists_visible_models_and_reasoning_efforts 在拿到模拟服务器收到的请求后调用它。它把“从大块请求里找目标说明文字”的细活交给 namespace_child_tool,让主测试可以专心判断说明内容是否正确。
调用图:调用 1 个内部函数(namespace_child_tool);被 1 处调用(spawn_agent_description_lists_visible_models_and_reasoning_efforts)。
test_model_info42–91 ↗
fn test_model_info(
slug: &str,
display_name: &str,
description: &str,
visibility: ModelVisibility,
default_reasoning_level: ReasoningEffort,
supported_reasoning_levels: Vec<Re
作用:这个函数快速造出一份测试用的模型资料。测试需要假装服务器返回不同模型,它就像填一张标准表格,把模型名、说明、可见性、推理强度和服务档位塞进去。
数据流:输入是模型的代号、显示名、描述、是否可见、默认推理强度、支持的推理强度列表、服务档位列表 → 它把这些值放进 ModelInfo 结构里,并给其他不关心的字段填上测试用默认值,例如输入类型、上下文窗口、截断策略等 → 输出一份完整的 ModelInfo,可以直接放进假的模型接口响应里。
调用关系:主测试用它创建两个模型:一个应该展示给用户看的可见模型,另一个应该被隐藏的模型。它内部会调用默认值相关函数,比如 default_input_modalities 和截断策略的 bytes,只是为了让这份模型资料完整、能被系统接受。
调用图:调用 2 个内部函数(bytes, default_input_modalities);外部调用 2 个(default, new)。
wait_for_model_available93–105 ↗
async fn wait_for_model_available(manager: &SharedModelsManager, slug: &str)
作用:这个异步函数会等到某个远端模型真的出现在模型管理器里。因为模型列表可能需要一点时间刷新,测试不能刚启动就马上断言,否则容易因为“还没刷新完”而误失败。
数据流:输入是共享的模型管理器和要等待的模型代号 → 它在最多 2 秒内反复向管理器请求在线模型列表 → 如果发现目标模型,就直接结束;如果一直没出现,就让测试失败并提示超时;每次没找到都会短暂睡 25 毫秒再试。
调用关系:主测试在提交用户消息前调用它,确保 visible-model 已经被系统识别。它把查询工作交给模型管理器的 list_models,把等待交给 sleep,作用是让后面的请求稳定包含正确的模型信息。
调用图:被 1 处调用(spawn_agent_description_lists_visible_models_and_reasoning_efforts);外部调用 6 个(from_millis, from_secs, now, list_models, panic!, sleep)。
spawn_agent_description_lists_visible_models_and_reasoning_efforts108–236 ↗
async fn spawn_agent_description_lists_visible_models_and_reasoning_efforts() -> Result<()>
作用:这是核心测试。它验证 spawn_agent 的说明文字会列出可见模型和推理强度,但不会泄露隐藏模型,也不会鼓励模型随便派生子代理或随便切换模型。
数据流:测试开始后先启动假的服务器 → 给服务器挂上一次模型列表响应,里面有可见模型和隐藏模型 → 再挂上一次假的流式响应,让会话能正常结束 → 然后构建测试用 Codex,会打开协作功能,并允许显示 spawn_agent 元数据 → 等待可见模型刷新出来 → 提交一轮用户输入 → 从模拟服务器收到的请求中取出 spawn_agent 说明文字 → 用一连串断言检查说明文字应该包含什么、不应该包含什么 → 如果全部符合,测试成功返回;如果有一条不符合,测试失败并打印实际说明文字。
调用关系:它是这个文件的主角,由测试框架在运行测试时自动调用。它会调用 start_mock_server 搭假服务器,调用 mount_models_once 和 mount_sse_once 安排假响应,调用 test_codex 创建测试会话,调用 wait_for_model_available 等模型准备好,最后调用 spawn_agent_description 把要检查的说明文字取出来。
调用图:调用 8 个内部函数(mount_models_once, mount_sse_once, sse, start_mock_server, test_codex, spawn_agent_description, wait_for_model_available, create_dummy_chatgpt_auth_for_testing);外部调用 2 个(assert!, vec!)。
core/tests/suite/agent_execution.rs源码 ↗
这个测试文件像是在搭一个小剧场,模拟模型服务一边返回消息,一边要求系统启动新的代理。它先准备一个假的服务器,让第一次用户请求触发“启动第一个 worker”,第一个 worker 又尝试启动第二个 worker。测试重点是:系统设置了同一个会话最多只能同时有 2 条代理执行线,也就是主任务加第一个 worker 已经占满名额,所以第二个 worker 应该被挡住。文件里的两个小工具函数用来检查发给假服务器的请求内容:一个看请求里有没有某段文字,另一个看请求里有没有某次函数调用的结果。最后的测试会启用协作和新版多代理功能,提交第一条提示词,然后等待第二次启动失败的结果,并确认失败原因是“代理线程达到上限”。这保证了嵌套启动代理时,大家共用同一套容量限制,而不是各算各的。
body_contains19–22 ↗
fn body_contains(request: &wiremock::Request, text: &str) -> bool
作用:这个小函数用来判断一次网络请求的正文里,是否包含指定文字。测试用它来分辨“这次请求是不是我们等的那一次”。
数据流:输入是一条假的网络请求和一段要查找的文字。它把请求正文当成 JSON 读出来,再把 JSON 转成字符串去查找那段文字;如果能读懂并且找到了,就返回 true,否则返回 false。它不会改动请求,只是检查。
调用关系:主测试在给假服务器登记预期回应时会用到它。假服务器收到请求后,会调用这个判断条件,只有请求内容像“第一条提示词”或“第一个 worker 的任务”时,才返回对应的模拟模型事件。
has_function_call_output24–36 ↗
fn has_function_call_output(request: &wiremock::Request, call_id: &str) -> bool
作用:这个小函数用来判断一次请求里,是否已经带上某个函数调用的执行结果。这里主要用来确认系统有没有把“启动代理”的结果回传给模型。
数据流:输入是一条请求和一个调用编号 call_id。它把请求正文解析成 JSON,找到里面的 input 数组,再逐项寻找类型为 function_call_output、并且 call_id 匹配的项目;找到了就返回 true,找不到或 JSON 解析失败就返回 false。它只读数据,不修改任何东西。
调用关系:主测试用它区分不同阶段的请求:比如第二个 worker 的启动结果回来之后,假服务器才应该给出“blocked”这样的后续消息;第一个 worker 的启动结果回来之后,才给出“spawned”。它相当于测试剧本里的“等这个回执出现再演下一幕”。
子代理协作流程
这些文件覆盖父子协调、委托事件呈现,以及协作会话中的邮箱式通知。
core/tests/suite/codex_delegate.rs源码 ↗
这组测试把真实网络服务换成一个假的服务器,让它按脚本吐出一串 SSE 事件。SSE 可以理解成服务器一条条推送消息的水管。测试会启动一个 Codex 会话,让它进入代码审查模式,内部再派出一个“子助手”去做审查。重点看两件事:第一,子助手如果想执行危险命令,或者想应用补丁改文件,主流程是否会收到审批请求;主流程给出同意或拒绝后,子助手是否能继续走完。第二,旧格式的推理文本增量不会被重复当成新内容上报。前两个测试目前被标记为 ignore,也就是默认不跑,因为相关能力还没完全做好;第三个测试会实际检查旧事件兼容问题。
codex_delegate_forwards_exec_approval_and_proceeds_on_approval29–117 ↗
async fn codex_delegate_forwards_exec_approval_and_proceeds_on_approval()
作用:这个测试验证:子助手想执行一个需要批准的 shell 命令时,审批请求会被转交给父级 Codex 会话;父级批准后,整个审查流程能正常结束。shell 命令就是让系统终端执行的命令,这里用一个危险的删除命令来模拟需要人工确认的场景。
数据流:进去的是一段假的服务端事件:先告诉系统“模型想调用 shell_command”,参数里带着要执行的命令和需要提升权限的沙箱设置;然后再准备第二段事件,表示审查结果已经生成。测试启动假服务器,把这些事件挂上去,再创建一个要求按需审批的 Codex 会话。之后它提交一次 Review 操作,等待进入审查模式,抓到 ExecApprovalRequest 审批请求,再用这个请求里的审批编号提交 Approved。最后它等待退出审查模式和 TurnComplete,说明流程从“子助手请求执行”变成“父级批准后正常收尾”。
调用关系:它是这份测试文件里检查“执行命令审批转发”的用例。它会先用 start_mock_server 建假服务器,用 sse 和 mount_sse_sequence 安排模型返回内容,用 test_codex 建测试会话,再用 wait_for_event 一步步等系统发出进入审查、请求审批、退出审查、回合完成这些事件。如果等到的事件不是预期类型,测试会 panic,表示委托审批链路坏了。
调用图:调用 4 个内部函数(mount_sse_sequence, sse, start_mock_server, test_codex);外部调用 5 个(wait_for_event, panic!, json!, skip_if_no_network!, vec!)。
codex_delegate_forwards_patch_approval_and_proceeds_on_decision123–197 ↗
async fn codex_delegate_forwards_patch_approval_and_proceeds_on_decision()
作用:这个测试验证:子助手想应用补丁改文件时,补丁审批请求会传到父级 Codex 会话;父级即使拒绝,子助手也能收到决定并继续把流程结束。补丁可以理解成一张“要怎么改文件”的清单。
数据流:进去的是两段假的模型推送:第一段说模型调用 apply_patch 工具,想新增 delegated.txt;第二段说模型最后给出了审查结果。测试把权限设成只读,这样改文件必须先问人。它提交 Review 后,等待进入审查模式,再等待 ApplyPatchApprovalRequest。拿到请求后,它用请求里的 call_id 提交 Denied,表示父级拒绝这次改文件。之后测试继续等退出审查模式和 TurnComplete,结果是系统没有卡死,子助手正确收到了拒绝决定并走完了审查。
调用关系:它是这份文件里检查“改文件审批转发”的用例。和执行命令的测试类似,它依赖 start_mock_server、sse、mount_sse_sequence 搭出假模型服务,依赖 test_codex 创建受限权限的会话,依赖 wait_for_event 观察系统事件。它把补丁审批这条支路单独拎出来,确保委托审查不只会转发命令审批,也会转发文件修改审批。
调用图:调用 4 个内部函数(mount_sse_sequence, sse, start_mock_server, test_codex);外部调用 5 个(wait_for_event, panic!, json!, skip_if_no_network!, vec!)。
codex_delegate_ignores_legacy_deltas200–242 ↗
async fn codex_delegate_ignores_legacy_deltas()
作用:这个测试验证:委托审查时,旧格式的推理摘要增量不会被错误地重复上报。推理增量可以理解成模型一边思考一边吐出的片段;如果旧新两套格式都算一遍,用户可能会看到重复内容。
数据流:进去的是一段假的 SSE 推送:创建响应、加入一个 reasoning item、再发送一个旧式 reasoning summary text delta,最后完成响应。测试启动假服务器和 Codex 会话,提交一次 Review。随后它不断读取事件,遇到 ReasoningContentDelta 就计数,遇到 TurnComplete 就停止。最后用 assert_eq 检查计数必须等于 1,也就是只保留一条新的推理内容增量,没有把旧式增量额外算进去。
调用关系:它是当前文件里唯一默认会跑的测试,用来保护事件兼容行为。它通过 start_mock_server 和 mount_sse_sequence 提供固定的模型事件,通过 test_codex 启动被测会话,通过 wait_for_event 连续消费系统发出的事件,最后用 assert_eq 给出明确判断:委托层应该忽略会造成重复的 legacy delta。
调用图:调用 4 个内部函数(mount_sse_sequence, sse, start_mock_server, test_codex);外部调用 4 个(assert_eq!, wait_for_event, skip_if_no_network!, vec!)。
core/tests/suite/subagent_notifications.rs源码 ↗
这份测试文件围绕一个实际问题:主代理可以派生出子代理帮忙干活,但子代理不是普通对话,它有自己的启动、停止、提示词、模型设置、权限和消息信箱。如果这些环节错了,用户可能看不到子代理结果,子代理可能拿不到父会话上下文,或者本该只给子代理的钩子误作用到主会话。文件用 mock server(假的模型服务器,用来装作真实 AI 服务)模拟模型返回工具调用、流式响应和错误。它还临时写入 hook 脚本(外部小程序,在特定事件发生时被调用),再读取日志确认调用顺序和内容。整体像搭了一个小剧场:父代理发起任务,模型要求 spawn_agent,系统创建子线程,子线程请求模型,结束后把通知或 agent_message 送回父线程,测试逐步检查每个环节是否正确。
body_contains64–82 ↗
fn body_contains(req: &wiremock::Request, text: &str) -> bool
作用:检查一次发给假服务器的请求正文里,是否包含某段文字。它还照顾到正文可能被 zstd 压缩的情况,避免测试因为看不懂压缩内容而误判。
数据流:进去的是一个 HTTP 请求和要查找的文字 → 它先看请求头里有没有 zstd 压缩标记,有就解压,没有就直接用原始字节,再尝试转成 UTF-8 字符串 → 出来的是 true 或 false,表示正文里有没有这段文字,不改动请求本身。
调用关系:很多测试用它当“筛子”,在 mock server 收到请求时判断这是不是自己要拦截的那一次请求。它内部会新建读取游标并在需要时调用解压函数。
调用图:调用 1 个内部函数(new);外部调用 1 个(decode_all)。
has_subagent_notification84–88 ↗
fn has_subagent_notification(req: &ResponsesRequest) -> bool
作用:判断一次模型请求里,用户消息是否带上了子代理通知标记。这个标记说明父代理已经知道子代理完成或有新消息。
数据流:进去的是解析好的 ResponsesRequest → 它取出角色为 user 的输入文本,逐条查找 <subagent_notification> → 出来的是布尔值,表示有没有子代理通知。
调用关系:测试“后续回合是否带通知”时会用它检查请求内容。它依赖 ResponsesRequest 提供的 message_input_texts 方法来抽出用户文本。
调用图:调用 1 个内部函数(message_input_texts)。
tool_parameter_description90–97 ↗
fn tool_parameter_description(tool: &Value, parameter_name: &str) -> Option<String>
作用:从一个工具的 JSON 描述里,取出某个参数的说明文字。测试用它确认 spawn_agent 工具是否把角色限制写清楚了。
数据流:进去的是工具 JSON 和参数名 → 它沿着 parameters、properties、参数名、description 这几层往下找,并把字符串复制出来 → 出来是说明文字,找不到就返回空值。
调用关系:只在 spawn_agent_tool_description_mentions_role_locked_settings 里使用。那个测试先拿到工具搜索结果,再交给它提取 agent_type 参数说明。
调用图:被 1 处调用(spawn_agent_tool_description_mentions_role_locked_settings);外部调用 1 个(get)。
role_block99–111 ↗
fn role_block(description: &str, role_name: &str) -> Option<String>
作用:从一大段角色说明文字里,截出某个角色自己的那一小块。这样测试可以精确比较 custom 角色的说明有没有写对。
数据流:进去的是完整说明文本和角色名 → 它找到形如 角色名: { 的开头,然后一直收集到下一个角色块之前 → 出来是这个角色的说明片段,找不到就返回空值。
调用关系:只被 spawn_agent_tool_description_mentions_role_locked_settings 调用。它接在 tool_parameter_description 后面,把参数说明进一步切成某个角色的局部内容。
调用图:被 1 处调用(spawn_agent_tool_description_mentions_role_locked_settings);外部调用 2 个(format!, vec!)。
write_home_skill113–119 ↗
fn write_home_skill(codex_home: &Path, dir: &str, name: &str, description: &str) -> Result<()>
作用:在临时的 Codex home 目录里写一个假 skill 文件。skill 可以理解成给代理额外看的技能说明,测试用它确认关闭技能说明后,父代理和子代理都不会收到这些内容。
数据流:进去的是 home 路径、技能目录名、技能名和描述 → 它创建 skills 子目录,写入一个带 YAML 头部的 SKILL.md → 出来是成功或错误,同时磁盘上多了一个测试技能文件。
调用关系:skills_toggle_skips_instructions_for_parent_and_spawned_child 通过测试构建器的预构建钩子间接使用它,先布置假技能,再启动测试实例。
调用图:外部调用 4 个(join, format!, create_dir_all, write)。
write_subagent_lifecycle_hooks121–262 ↗
fn write_subagent_lifecycle_hooks(
home: &Path,
stop_prompts: &[&str],
subagent_stop_matcher: &str,
) -> Result<()>
作用:给测试环境写一整套生命周期 hook 脚本和 hooks.json 配置。hook 就像门铃:会话开始、子代理开始、用户提交提示、子代理结束、普通停止时,系统会运行对应脚本。
数据流:进去的是临时 home 目录、哪些 SubagentStop 要拦截并要求继续的提示词、以及匹配哪个子代理类型 → 它生成多个 Python 脚本,把收到的输入写成 jsonl 日志,有的脚本还会输出额外上下文或阻止结束 → 出来是成功或错误,同时磁盘上写好脚本和 hooks.json。
调用关系:子代理启动和停止相关测试通过 pre_build_hook 调用它。后续测试再用 read_hook_log 和 wait_for_hook_log 检查这些脚本到底有没有被系统调用。
read_hook_log264–274 ↗
fn read_hook_log(home: &Path, filename: &str) -> Result<Vec<serde_json::Value>>
作用:读取某个 hook 脚本写下的日志文件,把每一行 JSON 变成程序能检查的数据。没有日志文件时,它返回空列表,而不是报错。
数据流:进去的是 home 路径和日志文件名 → 它拼出完整路径,若文件不存在就返回空数组;若存在就逐行读取、跳过空行、解析 JSON → 出来是一组 JSON 值,或者读取/解析错误。
调用关系:wait_for_hook_log 会反复调用它等待日志出现。subagent_stop_replaces_stop_and_skips_internal_subagents 也直接调用它,检查普通 Stop hook 没有被子代理结束误触发。
调用图:被 2 处调用(subagent_stop_replaces_stop_and_skips_internal_subagents, wait_for_hook_log);外部调用 3 个(join, new, read_to_string)。
wait_for_hook_log276–295 ↗
async fn wait_for_hook_log(
home: &Path,
filename: &str,
expected_len: usize,
) -> Result<Vec<serde_json::Value>>
作用:等待某个 hook 日志至少出现指定数量的记录。因为测试里很多事情是异步发生的,不能刚提交任务就立刻读文件。
数据流:进去的是 home 路径、日志文件名和期望条数 → 它在最多 2 秒内循环调用 read_hook_log,数量够了就返回日志,不够就短暂睡眠再试 → 出来是日志数组,超时则返回错误。
调用关系:子代理启动和停止测试用它等待 hook 脚本真正跑完。它把“异步等待文件变化”的细节藏起来,让测试主体只关心结果。
调用图:调用 1 个内部函数(read_hook_log);被 2 处调用(subagent_start_replaces_session_start_and_injects_context, subagent_stop_replaces_stop_and_skips_internal_subagents);外部调用 5 个(from_millis, from_secs, now, bail!, sleep)。
wait_for_spawned_thread_id297–312 ↗
async fn wait_for_spawned_thread_id(test: &TestCodex) -> Result<String>
作用:等待系统真的创建出一个新的子线程,并拿到它的线程 ID。子代理在代码里表现为一个新线程,所以这个 ID 是后续检查它配置和日志的钥匙。
数据流:进去的是 TestCodex 测试实例 → 它反复查看 thread_manager 里的线程 ID 列表,找出不是父会话 ID 的那个 → 出来是子线程 ID 字符串,超时则报错。
调用关系:通用搭建函数 setup_turn_one_with_custom_spawned_child 用它确认 spawn_agent 已经生效。subagent_start_replaces_session_start_and_injects_context 也用它把 hook 日志里的 agent_id 和真实子线程对上。
调用图:被 2 处调用(setup_turn_one_with_custom_spawned_child, subagent_start_replaces_session_start_and_injects_context);外部调用 5 个(from_millis, from_secs, now, bail!, sleep)。
wait_for_requests314–328 ↗
async fn wait_for_requests(
mock: &core_test_support::responses::ResponseMock,
) -> Result<Vec<ResponsesRequest>>
作用:等待某个 mock 响应记录器捕捉到至少一条请求。它解决异步测试里的常见问题:请求可能还在路上,不能马上断言。
数据流:进去的是 ResponseMock 记录器 → 它在最多 2 秒内循环读取已记录请求,非空就返回;一直没有就报错 → 出来是请求列表,不修改服务器状态。
调用关系:大量测试都会用它确认父代理或子代理确实向假模型服务器发了请求。setup_turn_one_with_custom_spawned_child 也用它等待子请求,从而保证后续通知已写入。
调用图:调用 1 个内部函数(requests);被 8 处调用(encrypted_multi_agent_v2_spawn_sends_agent_message_to_child, plaintext_multi_agent_v2_completion_sends_agent_message, setup_turn_one_with_custom_spawned_child, skills_toggle_skips_instructions_for_parent_and_spawned_child, spawned_multi_agent_v2_child_inherits_parent_developer_context, subagent_notification_is_included_without_wait, subagent_start_replaces_session_start_and_injects_context, subagent_stop_replaces_stop_and_skips_internal_subagents);外部调用 5 个(from_millis, from_secs, now, bail!, sleep)。
setup_turn_one_with_spawned_child330–345 ↗
async fn setup_turn_one_with_spawned_child(
server: &MockServer,
child_response_delay: Option<Duration>,
) -> Result<(TestCodex, String)>
作用:快速搭出“父代理第一回合派生一个普通子代理”的测试场景。它是更复杂搭建函数的简化包装,减少重复代码。
数据流:进去的是 mock server 和可选的子响应延迟 → 它准备默认 spawn_agent 参数,只包含子代理提示词,然后调用 setup_turn_one_with_custom_spawned_child → 出来是测试实例和子线程 ID。
调用关系:subagent_notification_is_included_without_wait 使用它来先制造一个已经发生过的子代理任务,再测试下一回合是否带通知。真正的服务器布置和等待逻辑交给 setup_turn_one_with_custom_spawned_child。
调用图:调用 1 个内部函数(setup_turn_one_with_custom_spawned_child);被 1 处调用(subagent_notification_is_included_without_wait);外部调用 1 个(json!)。
setup_turn_one_with_custom_spawned_child347–449 ↗
async fn setup_turn_one_with_custom_spawned_child(
server: &MockServer,
spawn_args: serde_json::Value,
child_response_delay: Option<Duration>,
wait_for_parent_notification: bool,
c
作用:搭建一个可定制的“父代理调用 spawn_agent,子代理执行任务,父代理继续完成”的完整场景。它是本文件很多测试的基础舞台。
数据流:进去的是 mock server、spawn_agent 参数、可选子响应延迟、是否等待父通知、以及修改测试配置的闭包 → 它挂载父回合响应、子回合响应和父回合后续响应,启用协作功能,启动 TestCodex 并提交父提示词,必要时等待子请求和父 rollout 文件里出现通知 → 出来是测试实例、子线程 ID 和子请求记录器。
调用关系:setup_turn_one_with_spawned_child 和 spawn_child_and_capture_snapshot 都调用它。它又把具体网络模拟交给 mount_sse_once_match、mount_response_once_match 等辅助函数,把异步等待交给 wait_for_requests 和 wait_for_spawned_thread_id。
调用图:调用 7 个内部函数(mount_response_once_match, mount_sse_once_match, sse, sse_response, test_codex, wait_for_requests, wait_for_spawned_thread_id);被 2 处调用(setup_turn_one_with_spawned_child, spawn_child_and_capture_snapshot);外部调用 8 个(from_millis, from_secs, now, bail!, to_string, read_to_string, sleep, vec!)。
spawn_child_and_capture_snapshot451–473 ↗
async fn spawn_child_and_capture_snapshot(
server: &MockServer,
spawn_args: serde_json::Value,
configure_test: impl FnOnce(
core_test_support::test_codex::TestCodexBuilder,
) -
作用:派生一个子代理后,直接读取它启动时的配置快照。测试用它确认子代理到底用了哪个模型、哪种推理强度。
数据流:进去的是 mock server、spawn_agent 参数和测试配置修改闭包 → 它先调用 setup_turn_one_with_custom_spawned_child 创建子代理,再把子线程 ID 转成 ThreadId,向 thread_manager 取出线程并读取 config_snapshot → 出来是 ThreadConfigSnapshot。
调用关系:模型覆盖相关的两个测试调用它:一个检查请求里的 model/reasoning_effort 能覆盖父设置,另一个检查角色配置能反过来锁定这些设置。
调用图:调用 2 个内部函数(setup_turn_one_with_custom_spawned_child, from_string);被 2 处调用(spawn_agent_requested_model_and_reasoning_override_inherited_settings_without_role, spawn_agent_role_overrides_requested_model_and_reasoning_settings)。
subagent_start_replaces_session_start_and_injects_context476–598 ↗
async fn subagent_start_replaces_session_start_and_injects_context() -> Result<()>
作用:测试子代理启动时应该触发 SubagentStart hook,而不是像普通会话那样再触发 SessionStart;同时确认 SubagentStart 返回的额外上下文会送进子代理请求。
数据流:进去没有普通业务输入,测试自己启动 mock server、写 hook、配置协作功能 → 它模拟父代理调用 spawn_agent,要求子请求同时包含子提示词和 hook 注入的上下文,然后读取各 hook 日志 → 出来是测试通过或失败;它会在临时目录写脚本和日志。
调用关系:这是一个顶层 tokio 测试。它使用 write_subagent_lifecycle_hooks 布置 hook,用 wait_for_requests 等待子请求,用 wait_for_hook_log 和 wait_for_spawned_thread_id 验证 hook 输入里的 agent_id、agent_type 和父子提示词区分。
调用图:调用 7 个内部函数(mount_sse_once_match, sse, start_mock_server, test_codex, wait_for_hook_log, wait_for_requests, wait_for_spawned_thread_id);外部调用 6 个(assert_eq!, assert_ne!, json!, to_string, skip_if_no_network!, vec!)。
subagent_stop_replaces_stop_and_skips_internal_subagents601–803 ↗
async fn subagent_stop_replaces_stop_and_skips_internal_subagents() -> Result<()>
作用:测试子代理结束时走 SubagentStop hook,而不是普通 Stop hook;如果 SubagentStop 要求继续,子代理会再跑一轮;内部系统用的子代理则不应触发这些用户可见的子代理停止 hook。
数据流:进去没有外部参数,测试自己布置 mock server、hook 脚本和协作配置 → 它让子代理第一次完成后被 SubagentStop 拦住并追加继续提示,再完成第二次;随后手动启动一个内部 review 子代理 → 出来是断言结果,确认日志条数、transcript 路径、最后助手消息、普通 Stop hook 和内部子代理行为都正确。
调用关系:这是本文件最完整的生命周期测试。它使用 write_subagent_lifecycle_hooks 生成会阻止第一次结束的脚本,用 wait_for_hook_log 等待日志,用 read_hook_log 对比前后状态,并通过 thread_manager.start_thread_with_options 手动制造内部子代理。
调用图:调用 9 个内部函数(mount_sse_once_match, sse, start_mock_server, local_selections, test_codex, turn_permission_fields, read_hook_log, wait_for_hook_log, wait_for_requests);外部调用 11 个(default, new, SubAgent, assert!, assert_eq!, assert_ne!, wait_for_event_match, json!, to_string, skip_if_no_network! (+1 more))。
subagent_notification_is_included_without_wait806–829 ↗
async fn subagent_notification_is_included_without_wait() -> Result<()>
作用:测试即使用户没有显式等待子代理,下一次父代理请求里也会带上子代理通知。这样父代理不会装作不知道子代理已经发生过什么。
数据流:进去没有业务参数,测试先搭出一个已派生并完成的子代理,再提交第二个父提示词 → 它检查第二回合发给模型的请求里是否包含 <subagent_notification> → 出来是测试通过或失败。
调用关系:它调用 setup_turn_one_with_spawned_child 搭好第一回合,再用 wait_for_requests 等待第二回合请求,最后用 has_subagent_notification 判断通知是否存在。
调用图:调用 5 个内部函数(mount_sse_once_match, sse, start_mock_server, setup_turn_one_with_spawned_child, wait_for_requests);外部调用 3 个(assert!, skip_if_no_network!, vec!)。
spawned_child_receives_forked_parent_context832–926 ↗
async fn spawned_child_receives_forked_parent_context() -> Result<()>
作用:测试当 spawn_agent 带 fork_context: true 时,子代理能收到父会话之前的上下文。也就是说,子代理不是从空白开始,而是带着父代理的旧聊天记录出发。
数据流:进去没有外部参数,测试先提交一个种子回合留下父上下文,再提交一个会派生子代理的回合 → 它在服务器收到的子请求里寻找种子提示词,并确认没有把 spawn_agent 调用 ID 这种工具内部信息混进去 → 出来是测试通过或失败。
调用关系:这是顶层 tokio 测试。它自己用 mount_sse_once_match 布置种子回合、派生回合、子回合和父后续回合,并用 body_contains 在收到的请求里查证上下文是否被 fork。
调用图:调用 4 个内部函数(mount_sse_once_match, sse, start_mock_server, test_codex);外部调用 10 个(from_millis, from_secs, now, bail!, assert!, json!, to_string, skip_if_no_network!, sleep, vec!)。
spawn_agent_requested_model_and_reasoning_override_inherited_settings_without_role929–952 ↗
async fn spawn_agent_requested_model_and_reasoning_override_inherited_settings_without_role() -> Result<()>
作用:测试在没有指定角色配置时,spawn_agent 请求里的 model 和 reasoning_effort 可以覆盖从父代理继承来的设置。
数据流:进去没有外部参数,测试启动 mock server,用 spawn 参数指定请求模型和推理强度 → 它派生子代理并读取子线程配置快照 → 出来是断言结果,确认子代理用了请求里指定的模型和推理强度。
调用关系:它调用 spawn_child_and_capture_snapshot 完成派生和配置读取,自己只负责准备参数和比较快照。
调用图:调用 2 个内部函数(start_mock_server, spawn_child_and_capture_snapshot);外部调用 3 个(assert_eq!, json!, skip_if_no_network!)。
spawned_multi_agent_v2_child_inherits_parent_developer_context955–1025 ↗
async fn spawned_multi_agent_v2_child_inherits_parent_developer_context() -> Result<()>
作用:测试 MultiAgentV2 模式下,子代理会继承父代理的开发者指令。开发者指令可以理解成系统给代理的额外工作规则。
数据流:进去没有外部参数,测试启用协作和 MultiAgentV2,并给父配置设置 developer_instructions → 它让父代理派生子代理,然后检查子请求正文同时包含父开发者指令和子提示词 → 出来是测试通过或失败。
调用关系:它自己搭建 mock 响应,用 wait_for_requests 等待子代理请求,再通过请求对象的 body_contains_text 做断言。
调用图:调用 5 个内部函数(mount_sse_once_match, sse, start_mock_server, test_codex, wait_for_requests);外部调用 5 个(assert!, json!, to_string, skip_if_no_network!, vec!)。
encrypted_multi_agent_v2_spawn_sends_agent_message_to_child1028–1105 ↗
async fn encrypted_multi_agent_v2_spawn_sends_agent_message_to_child() -> Result<()>
作用:测试 MultiAgentV2 下,如果派生任务内容是加密的,系统会把它包装成 agent_message 发给子代理,而不是当普通明文提示词塞进去。
数据流:进去没有外部参数,测试准备一个加密字符串作为 spawn_agent message → 它模拟父代理调用 spawn_agent,等待子请求,然后提取类型为 agent_message 的输入 → 出来是断言结果,确认 author、recipient、任务头文字和 encrypted_content 都符合预期。
调用关系:这是顶层 tokio 测试。它用 mount_sse_once_match 匹配包含 agent_message 的子请求,用 wait_for_requests 等待请求,然后用 inputs_of_type 检查消息结构。
调用图:调用 5 个内部函数(mount_sse_once_match, sse, start_mock_server, test_codex, wait_for_requests);外部调用 4 个(assert_eq!, json!, to_string, vec!)。
plaintext_multi_agent_v2_completion_sends_agent_message1116–1242 ↗
async fn plaintext_multi_agent_v2_completion_sends_agent_message(
scenario: CompletionScenario,
) -> Result<()>
作用:测试 MultiAgentV2 下,子代理完成后会把最终答案作为 agent_message 投递回父代理;如果子代理流式响应中断,也会把错误包装成可读消息交回去。
数据流:进去的是测试参数 CompletionScenario,表示子代理正常完成还是终止错误 → 它布置父派生、子执行、父后续 wait_agent 的多段假响应;正常时 payload 是 child done,错误时 payload 是错误说明;最后检查父代理后续请求里收到的 agent_message → 出来是测试通过或失败。
调用关系:这是一个带 test_case 的参数化 tokio 测试,同一套流程跑两种场景。它用 mount_response_once_match 模拟延迟的子响应,用 wait_for_requests 等待关键请求,并检查最终投递给父代理的消息结构。
调用图:调用 7 个内部函数(mount_response_once_match, mount_sse_once_match, sse, sse_response, start_mock_server, test_codex, wait_for_requests);外部调用 6 个(from_secs, assert_eq!, format!, json!, to_string, vec!)。
skills_toggle_skips_instructions_for_parent_and_spawned_child1245–1322 ↗
async fn skills_toggle_skips_instructions_for_parent_and_spawned_child() -> Result<()>
作用:测试当配置关闭 skill instructions 时,父代理和派生出的子代理都不会收到技能说明。这样用户关掉开关后,不会有隐藏的技能文本继续影响模型。
数据流:进去没有外部参数,测试先写入一个假技能文件,再设置 include_skill_instructions 为 false 并启用协作/MultiAgentV2 → 它提交会派生子代理的提示词,分别检查父请求和子请求正文 → 出来是断言结果,确认都不含 <skills_instructions> 和技能名。
调用关系:它通过 write_home_skill 布置假技能,用 spawn_turn.single_request 检查父请求,用 wait_for_requests 检查子请求。
调用图:调用 5 个内部函数(mount_sse_once_match, sse, start_mock_server, test_codex, wait_for_requests);外部调用 5 个(assert!, json!, to_string, skip_if_no_network!, vec!)。
spawn_agent_role_overrides_requested_model_and_reasoning_settings1325–1364 ↗
async fn spawn_agent_role_overrides_requested_model_and_reasoning_settings() -> Result<()>
作用:测试如果子代理指定了一个带固定配置的角色,那么角色里的模型和推理强度会压过 spawn_agent 请求里临时写的设置。
数据流:进去没有外部参数,测试创建 custom 角色配置文件,里面写死模型和推理强度,同时 spawn 参数里故意传另一组值 → 它派生子代理并读取配置快照 → 出来是断言结果,确认最终采用角色配置。
调用关系:它调用 spawn_child_and_capture_snapshot 做派生和快照读取,配置阶段还向 config.agent_roles 注册 custom 角色。
调用图:调用 2 个内部函数(start_mock_server, spawn_child_and_capture_snapshot);外部调用 3 个(assert_eq!, json!, skip_if_no_network!)。
spawn_agent_tool_description_mentions_role_locked_settings1367–1437 ↗
async fn spawn_agent_tool_description_mentions_role_locked_settings() -> Result<()>
作用:测试工具搜索返回的 spawn_agent 说明里,会明确告诉模型某个角色的模型和推理强度已被锁定、不能修改。这能减少模型误以为自己还能覆盖角色设置的情况。
数据流:进去没有外部参数,测试配置 custom 角色和工具搜索响应流程 → 它提交提示词触发 tool_search,取出第二次请求里的工具搜索输出,找到 multi_agent_v1.spawn_agent,再抽取 agent_type 参数说明中的 custom 角色块 → 出来是断言结果,确认说明文字精确写明 locked settings。
调用关系:它调用 namespace_child_tool 找到工具,再用 tool_parameter_description 提取参数说明,最后用 role_block 截出 custom 块并比较完整文本。
调用图:调用 6 个内部函数(mount_sse_sequence, namespace_child_tool, start_mock_server, test_codex, role_block, tool_parameter_description);外部调用 3 个(assert_eq!, skip_if_no_network!, vec!)。
代理任务编排
此组测试由 CSV 驱动的任务生成、结果处理,以及代理支持的工作项的取消行为。
core/tests/suite/agent_jobs.rs源码 ↗
这个文件像一套演练脚本:先造一个临时的模型服务器,再让系统以为自己正在和真实模型对话。测试会准备 CSV 文件,比如几行待处理的数据,然后模拟模型调用 spawn_agents_on_csv 这个工具去启动批量任务。每个子任务再由假的响应器返回 report_agent_job_result,表示“这个条目处理完了”。这样做的好处是,不用真的连外部模型,也能稳定检查整条链路。文件里有两个假响应器:AgentJobsResponder 用来模拟正常完成;StopAfterFirstResponder 会在第一个子任务后要求停止,用来验证系统不会继续乱跑。辅助函数负责解压请求体、从请求里找出任务 ID 和条目 ID、简单拆 CSV。重点是:这些测试不只看有没有输出文件,还会查数据库里的任务状态,确保系统内部记录和导出的 CSV 都一致。
AgentJobsResponder::new31–37 ↗
fn new(spawn_args_json: String) -> Self
作用:创建一个“正常完成版”的假模型响应器。测试用它来告诉假服务器:第一次请求时要返回一个启动 CSV 批量任务的工具调用。
数据流:进去的是一段已经转成字符串的工具参数 JSON → 它把这段参数保存起来,并把“是否见过主请求”和“子任务调用次数”初始化为 0 或 false → 出来的是一个可挂到 mock server 上的 AgentJobsResponder。
调用关系:多个正常流程测试会先调用它,然后把生成的响应器交给 wiremock 的 Mock。之后系统发 POST 请求到 /responses 时,真正干活的是 AgentJobsResponder::respond。
调用图:被 3 处调用(report_agent_job_result_rejects_wrong_thread, spawn_agents_on_csv_dedupes_item_ids, spawn_agents_on_csv_runs_and_exports);外部调用 2 个(new, new)。
StopAfterFirstResponder::new47–53 ↗
fn new(spawn_args_json: String, worker_calls: Arc<AtomicUsize>) -> Self
作用:创建一个“处理第一个子任务后就喊停”的假模型响应器。它专门用来测试批量任务收到停止信号后,会不会乖乖停止后续条目。
数据流:进去的是启动任务的 JSON 参数,以及一个共享计数器 → 它保存参数,初始化“是否见过主请求”,并保留这个计数器用来记录 worker 被叫了几次 → 出来的是 StopAfterFirstResponder。
调用关系:只有停止场景测试 spawn_agents_on_csv_stop_halts_future_items 会创建它。创建后挂到 mock server,后续请求由 StopAfterFirstResponder::respond 决定返回正常结果还是带 stop 的结果。
调用图:被 1 处调用(spawn_agents_on_csv_stop_halts_future_items);外部调用 1 个(new)。
StopAfterFirstResponder::respond57–98 ↗
fn respond(&self, request: &wiremock::Request) -> ResponseTemplate
作用:这是“停止版”假服务器的核心回复逻辑。它会看系统发来的请求像不像工具结果、像不像子任务请求,或者是不是第一次主请求,然后返回不同的假模型事件。
数据流:进去的是一次 HTTP 请求 → 它先用 decode_body_bytes 取出真实请求体,再解析成 JSON;如果发现这是工具调用结果,就返回一个空的完成响应;如果发现这是某个子任务,就递增 worker_calls,给第一个子任务返回带 stop=true 的 report_agent_job_result;如果是第一次主请求,就返回 spawn_agents_on_csv;其他情况只返回普通完成 → 出来的是一段 SSE 响应,SSE 可以理解成服务器一条条推送事件的格式。
调用关系:它由 wiremock 在测试服务器收到 /responses 请求时自动调用。它会借助 has_function_call_output 判断是不是工具结果,借助 extract_job_and_item 找出子任务身份,再用 sse 和 sse_response 包装成系统能读懂的模型流式响应。
调用图:调用 5 个内部函数(sse, sse_response, decode_body_bytes, extract_job_and_item, has_function_call_output);外部调用 6 个(swap, format!, json!, from_slice, to_string, vec!)。
AgentJobsResponder::respond102–143 ↗
fn respond(&self, request: &wiremock::Request) -> ResponseTemplate
作用:这是“正常完成版”假服务器的核心回复逻辑。它模拟模型先启动一个 CSV 批量任务,然后每个子任务都正常上报结果。
数据流:进去的是一次 HTTP 请求 → 它解码并解析请求体;如果请求里已经包含工具结果,就返回完成事件;如果请求是在处理某个子任务,就生成一个唯一的 call-worker 编号,并返回 report_agent_job_result;如果这是第一次主对话请求,就返回 spawn_agents_on_csv;其他请求只返回默认完成 → 出来的是模拟模型服务的 SSE 响应。
调用关系:它由 wiremock 自动触发,是三个正常测试的假模型。它依赖 decode_body_bytes 处理可能被压缩的请求,依赖 has_function_call_output 避免重复触发工具,依赖 extract_job_and_item 识别 worker 请求。
调用图:调用 5 个内部函数(sse, sse_response, decode_body_bytes, extract_job_and_item, has_function_call_output);外部调用 6 个(swap, format!, json!, from_slice, to_string, vec!)。
decode_body_bytes146–163 ↗
fn decode_body_bytes(request: &wiremock::Request) -> Vec<u8>
作用:取出请求体的真实字节内容。如果请求体被 zstd 压缩过,它会先解压;没有压缩就直接返回原内容。
数据流:进去的是 wiremock 收到的请求 → 它查看 content-encoding 头;如果里面写着 zstd,就尝试解压 body,失败时退回原 body;如果没有 zstd,就直接克隆原 body → 出来的是可以拿去解析 JSON 的字节数组。
调用关系:两个 respond 函数都会先调用它,因为测试里的请求可能带压缩。它相当于门口的拆包员,先把包裹拆开,后面的 JSON 解析和内容判断才不会看错。
调用图:调用 1 个内部函数(new);被 2 处调用(respond, respond);外部调用 1 个(decode_all)。
has_function_call_output165–173 ↗
fn has_function_call_output(body: &Value) -> bool
作用:判断请求里是不是已经包含了“工具调用的输出”。这能帮助假响应器知道:系统是在回报工具执行结果,而不是又要发起新工具。
数据流:进去的是解析后的 JSON 请求体 → 它查看 input 数组里有没有 type 等于 function_call_output 的项目 → 出来的是 true 或 false,不改动任何数据。
调用关系:两个 respond 函数都会用它做第一层分流。如果是工具输出,响应器只返回完成事件,避免测试中无限重复触发工具调用。
extract_job_and_item175–196 ↗
fn extract_job_and_item(body: &Value) -> Option<(String, String)>
作用:从模型请求文本里找出当前子任务的 job_id 和 item_id。没有这一步,假服务器就不知道该给哪个任务条目回报结果。
数据流:进去的是 JSON 请求体 → 它先用 message_input_texts 收集输入文本,再把 instructions 也拼进去;如果文本里没有“正在处理一个 agent job 条目”的标记,就返回 None;如果有,就用正则表达式从文本里抓 Job ID 和 Item ID → 出来的是一对字符串,或者表示没找到的 None。
调用关系:两个 respond 函数在判断 worker 请求时调用它。它把系统发给子 agent 的自然语言提示变成结构化的任务编号,然后 respond 才能生成 report_agent_job_result。
调用图:调用 1 个内部函数(message_input_texts);被 2 处调用(respond, respond);外部调用 2 个(new, get)。
message_input_texts198–211 ↗
fn message_input_texts(body: &Value) -> Vec<String>
作用:从请求 JSON 里抽出用户消息中的纯文本片段。它只拿真正的 input_text,跳过其他类型的内容。
数据流:进去的是 JSON 请求体 → 它找到 input 数组,再筛选 type 为 message 的项目;接着进入 content 数组,只保留 type 为 input_text 的片段,并取出 text 字段 → 出来的是一组字符串。
调用关系:extract_job_and_item 会调用它来收集可搜索的文本。它像筛子一样先把无关 JSON 结构筛掉,让后面找 Job ID 和 Item ID 更简单。
调用图:被 1 处调用(extract_job_and_item);外部调用 2 个(get, new)。
parse_simple_csv_line213–215 ↗
fn parse_simple_csv_line(line: &str) -> Vec<String>
作用:把一行非常简单的 CSV 按逗号拆成多个字段。这里的测试数据没有复杂引号,所以不需要完整 CSV 解析器。
数据流:进去的是一行文本 → 它直接按逗号分割,并把每段变成字符串 → 出来的是字段列表。
调用关系:spawn_agents_on_csv_dedupes_item_ids 用它读取输出 CSV 的表头和行内容,找到 item_id 列并检查重复 ID 是否被改成唯一值。
调用图:被 1 处调用(spawn_agents_on_csv_dedupes_item_ids)。
report_agent_job_result_rejects_wrong_thread218–281 ↗
async fn report_agent_job_result_rejects_wrong_thread() -> Result<()>
作用:测试系统会拒绝“错误线程”上报的子任务结果。也就是说,只有真正负责这个条目的 worker 才能写入结果,别人不能冒名顶替。
数据流:进去的是测试运行环境 → 它启动假服务器,打开 SpawnCsv 和 Sqlite 功能,写入一个只有一条数据的 CSV,挂上 AgentJobsResponder,然后提交一轮对话让任务跑起来;跑完后它读输出 CSV 找 job_id,再查数据库找对应 item,最后故意用一个假的 thread_id 上报结果 → 出来的是断言:数据库返回 accepted=false,说明错误线程没有被接受。
调用关系:这是一个完整端到端测试。它调用 start_mock_server 和 test_codex 搭环境,调用 AgentJobsResponder::new 准备假模型,并间接触发 AgentJobsResponder::respond。最后直接访问 state_db 来验证底层保护规则。
调用图:调用 3 个内部函数(start_mock_server, test_codex, new);外部调用 9 个(given, assert!, assert_eq!, read_to_string, write, json!, to_string, method, path_regex)。
spawn_agents_on_csv_runs_and_exports284–323 ↗
async fn spawn_agents_on_csv_runs_and_exports() -> Result<()>
作用:测试最基础的批量流程:系统能读 CSV,派发多个子任务,收集结果,并导出新的 CSV 文件。
数据流:进去的是测试运行环境 → 它准备两行输入 CSV,配置启动工具参数,启动假服务器和测试 Codex,提交“run batch job”;假响应器模拟每个子任务都返回结果 → 出来的是一个输出 CSV,测试断言里面有 result_json、item_id 以及结果内容。
调用关系:这是正常路径的主测试。它创建 AgentJobsResponder,然后让系统通过 mock server 走完整的 spawn_agents_on_csv 和 report_agent_job_result 流程,最后只从导出文件角度确认结果真的落地。
调用图:调用 3 个内部函数(start_mock_server, test_codex, new);外部调用 8 个(given, assert!, read_to_string, write, json!, to_string, method, path_regex)。
spawn_agents_on_csv_dedupes_item_ids326–382 ↗
async fn spawn_agents_on_csv_dedupes_item_ids() -> Result<()>
作用:测试输入 CSV 里如果有重复的 item ID,系统会自动改成不重复的 ID。这样数据库和结果文件不会因为两个条目同名而混乱。
数据流:进去的是测试运行环境 → 它写入两行 id 都是 foo 的 CSV,指定 id_column 为 id,然后启动任务;任务结束后读取输出 CSV,用 parse_simple_csv_line 找到 item_id 列,收集所有 item_id,排序去重 → 出来的是断言:最终有两个不同 ID,分别是 foo 和 foo-2。
调用关系:它和正常流程一样使用 AgentJobsResponder 模拟模型,但额外检查 ID 去重规则。parse_simple_csv_line 在这里帮它从输出文件里定位和读取 item_id。
调用图:调用 4 个内部函数(start_mock_server, test_codex, new, parse_simple_csv_line);外部调用 10 个(given, new, assert!, assert_eq!, read_to_string, write, json!, to_string, method, path_regex)。
spawn_agents_on_csv_stop_halts_future_items385–444 ↗
async fn spawn_agents_on_csv_stop_halts_future_items() -> Result<()>
作用:测试子任务上报 stop=true 后,批量任务会停止继续处理后面的条目。这个场景很重要,因为用户或模型可能发现继续跑没有意义,需要及时刹车。
数据流:进去的是测试运行环境 → 它准备三行 CSV,并把 max_concurrency 设为 1,意思是一次只跑一个,便于观察停止行为;StopAfterFirstResponder 在第一个 worker 返回结果时附带 stop=true;任务结束后,测试读取输出 CSV、查数据库里的任务状态和进度,并读取 worker_calls 计数 → 出来的是断言:任务状态是 Cancelled,总数为 3,完成 1 个,剩下 2 个仍待处理,worker 只被调用 1 次。
调用关系:这是停止逻辑的端到端测试。它调用 StopAfterFirstResponder::new 创建特殊假服务器响应器,后续请求由 StopAfterFirstResponder::respond 控制。测试最后通过数据库进度和计数器一起确认:系统不是只改了状态,而是真的没有继续派发后续条目。
调用图:调用 3 个内部函数(start_mock_server, test_codex, new);外部调用 10 个(new, new, given, assert_eq!, read_to_string, write, json!, to_string, method, path_regex)。
远程环境执行
这些测试验证远程环境中的隔离执行,包括文件系统访问、环境选择、权限和沙盒行为。
core/tests/suite/remote_env.rs源码 ↗
这份测试像一套“远程施工验收清单”。它会启动假的模型服务器,让模型“假装”发出命令或补丁,然后检查 Codex 是否把这些操作送到选中的环境:本地还是远程。文件还重点检查沙箱,也就是一圈安全围栏:哪些目录能读、哪些目录能写、符号链接(一种指向别处的快捷方式)会不会被错误跟随。测试里有一些小工具函数,用来创建只读或可写的沙箱、等待审批事件、直接进 Docker 容器准备测试文件。整体目标不是测试某个小算法,而是确认远程文件系统、命令执行、权限审批、apply_patch 补丁工具这些零件在真实流程里能安全配合。
unified_exec_test61–71 ↗
async fn unified_exec_test(server: &wiremock::MockServer) -> Result<TestCodex>
作用:搭一个开启“统一执行工具”的测试版 Codex。统一执行工具可以把命令执行统一走同一套接口,方便测试本地和远程命令路由。
数据流:进去的是一个假的模型服务器 → 它从 test_codex 创建测试构建器,打开 UnifiedExec 功能开关,并要求同时准备远程和本地环境 → 出来的是一个可用于后续测试的 TestCodex,里面已经连好 mock 服务器和两种执行环境。
调用关系:它是多个命令路由测试的准备步骤。exec_command_routes_to_selected_remote_environment 和 apply_patch_intercepted_exec_command_routes_to_selected_remote_environment 先调用它搭好环境,再让模型发 exec_command,看命令是否真的跑到远程。
调用图:调用 1 个内部函数(test_codex);被 2 处调用(apply_patch_intercepted_exec_command_routes_to_selected_remote_environment, exec_command_routes_to_selected_remote_environment)。
submit_turn_with_approval_and_environments73–110 ↗
async fn submit_turn_with_approval_and_environments(
test: &TestCodex,
prompt: &str,
environments: Vec<TurnEnvironmentSelection>,
) -> Result<()>
作用:向 Codex 提交一轮用户输入,并同时指定可用环境和审批策略。它用来模拟“用户允许模型在多个环境里工作,但敏感操作要先问人”的场景。
数据流:进去的是测试实例、提示词、环境列表 → 它把这些环境包装成 TurnEnvironmentSelections,再提交一条 UserInput,并设置按需审批、用户审核、只读沙箱等线程设置 → 出来没有返回业务数据,但 Codex 会开始处理这一轮对话。
调用关系:它把测试从“准备阶段”推进到“模型开始行动”。remote_request_permissions_grant_unblocks_later_remote_exec 用它触发权限申请流程,apply_patch_approvals_are_remembered_per_environment 用它反复触发补丁审批流程。
调用图:调用 1 个内部函数(new);被 2 处调用(apply_patch_approvals_are_remembered_per_environment, remote_request_permissions_grant_unblocks_later_remote_exec);外部调用 3 个(default, new_read_only_policy, vec!)。
expect_patch_approval112–132 ↗
async fn expect_patch_approval(
test: &TestCodex,
expected_call_id: &str,
) -> ApplyPatchApprovalRequestEvent
作用:等待 Codex 发出一次补丁审批请求,并确认请求的调用编号就是预期的那个。它帮助测试证明系统确实在改文件前停下来问用户。
数据流:进去的是测试实例和期望的 call_id → 它监听事件,直到看到补丁审批请求或这一轮结束 → 如果拿到正确审批请求,就返回审批内容;如果提前结束或编号不对,就让测试失败。
调用关系:它服务于 apply_patch_approvals_are_remembered_per_environment。那个测试先让模型发补丁,再用它确认 Codex 没有擅自修改文件,而是先发来了审批。
调用图:被 1 处调用(apply_patch_approvals_are_remembered_per_environment);外部调用 3 个(assert_eq!, wait_for_event, panic!)。
wait_for_completion_without_patch_approval134–150 ↗
async fn wait_for_completion_without_patch_approval(test: &TestCodex)
作用:等待一轮任务结束,并确保过程中没有出现补丁审批请求。它用来验证某些审批被记住后,后续同环境操作不需要重复打扰用户。
数据流:进去的是测试实例 → 它等待补丁审批或任务完成两类事件 → 如果任务直接完成就算通过;如果等到补丁审批,就立刻让测试失败。
调用关系:它在 apply_patch_approvals_are_remembered_per_environment 的最后阶段使用。前面用户已经批准过远程环境的补丁操作,后面再次远程打补丁时,这个函数确认不会再弹审批。
调用图:被 1 处调用(apply_patch_approvals_are_remembered_per_environment);外部调用 2 个(wait_for_event, panic!)。
remote_test_env_can_connect_and_use_filesystem153–185 ↗
async fn remote_test_env_can_connect_and_use_filesystem() -> Result<()>
作用:检查远程测试环境是否真的能连上,并且远程文件系统能写、读、删文件。它是最基础的远程环境冒烟测试。
数据流:进去没有业务参数 → 它先确认配置了远程环境,再创建测试环境,拿到远程文件系统,在远程工作目录写入一个文件,读回来比对内容,最后删除 → 出来是测试通过或失败。
调用关系:这是独立测试,不依赖本文件的辅助函数。它直接调用 test_env 和 get_remote_test_env,是后续更复杂远程测试能成立的前提检查。
调用图:调用 2 个内部函数(test_env, from_path);外部调用 2 个(assert_eq!, get_remote_test_env)。
remote_test_env_exposes_target_shell_to_model188–229 ↗
async fn remote_test_env_exposes_target_shell_to_model() -> Result<()>
作用:确认模型能看到远程环境实际使用的 shell,也就是命令解释器,比如 bash 或 PowerShell。这样模型才会写出适合目标环境的命令。
数据流:进去没有业务参数 → 它启动 mock 模型服务器,返回一段简单完成消息;然后提交一轮请求,抓取发给模型的环境上下文,检查其中的 shell 标签是否符合当前远程环境类型 → 出来是断言通过或失败。
调用关系:它使用 start_mock_server、mount_sse_once 和 test_codex 搭出一次模型交互。这个测试不执行真实命令,而是验证 Codex 在请求模型前塞进去的环境说明是否正确。
调用图:调用 4 个内部函数(mount_sse_once, sse, start_mock_server, test_codex);外部调用 5 个(assert_eq!, get_remote_test_env, test_environment, unreachable!, vec!)。
absolute_path231–233 ↗
fn absolute_path(path: PathBuf) -> AbsolutePathBuf
作用:把普通路径转换成“确定是绝对路径”的类型。绝对路径就是从根目录开始的完整地址,避免相对路径造成安全判断歧义。
数据流:进去的是 PathBuf 路径 → 它尝试转换成 AbsolutePathBuf,如果不是绝对路径就直接报错让测试失败 → 出来的是带有“已确认绝对”的路径对象。
调用关系:它是沙箱相关测试的小帮手。read_only_sandbox 和 workspace_write_sandbox 用它保证沙箱根目录明确;remote_test_env_remove_removes_symlink_not_target 也用它处理符号链接路径的元数据检查。
调用图:调用 1 个内部函数(try_from);被 3 处调用(read_only_sandbox, remote_test_env_remove_removes_symlink_not_target, workspace_write_sandbox)。
read_only_sandbox235–246 ↗
fn read_only_sandbox(readable_root: PathBuf) -> FileSystemSandboxContext
作用:创建一个只允许读取指定目录的文件系统沙箱。沙箱可以理解成安全围栏,围栏外的文件不该被读到。
数据流:进去的是允许读取的目录 → 它先把目录确认成绝对路径,再生成一个文件系统策略:该路径只有读权限,网络也受限制 → 出来的是 FileSystemSandboxContext,后续读文件时可带上它做权限检查。
调用关系:它被两个远程读取测试使用。remote_test_env_sandboxed_read_allows_readable_root 用它证明围栏内能读;remote_test_env_sandboxed_read_rejects_symlink_parent_dotdot_escape 用它证明不能借符号链接和 .. 路径逃出围栏。
调用图:调用 4 个内部函数(absolute_path, from_permission_profile, from_runtime_permissions, restricted);被 2 处调用(remote_test_env_sandboxed_read_allows_readable_root, remote_test_env_sandboxed_read_rejects_symlink_parent_dotdot_escape);外部调用 1 个(vec!)。
workspace_write_sandbox248–259 ↗
fn workspace_write_sandbox(writable_root: PathBuf) -> FileSystemSandboxContext
作用:创建一个只允许写指定目录的文件系统沙箱。它用来测试写入、删除、复制等操作是否只发生在被允许的工作区里。
数据流:进去的是允许写的目录 → 它确认目录是绝对路径,再生成写权限策略,并限制网络 → 出来的是可传给远程文件系统操作的沙箱上下文。
调用关系:它服务于符号链接相关测试。remote_test_env_remove_removes_symlink_not_target 用它检查删除链接时不会删掉链接指向的外部文件;remote_test_env_copy_preserves_symlink_source 用它检查复制链接时保留链接本身。
调用图:调用 4 个内部函数(absolute_path, from_permission_profile, from_runtime_permissions, restricted);被 2 处调用(remote_test_env_copy_preserves_symlink_source, remote_test_env_remove_removes_symlink_not_target);外部调用 1 个(vec!)。
assert_normalized_path_rejected261–278 ↗
fn assert_normalized_path_rejected(error: &std::io::Error)
作用:检查一次路径被拒绝时,错误类型和错误信息是否合理。这里的“路径归一化”是指把 link、.. 等路径花样整理成真实目标后再判断权限。
数据流:进去的是一个输入输出错误 → 它查看错误种类和文字;如果是找不到、无效输入或权限拒绝,并且信息符合预期,就通过;如果出现奇怪错误,就让测试失败 → 不产生新数据,只做判断。
调用关系:它被 remote_test_env_sandboxed_read_rejects_symlink_parent_dotdot_escape 调用。那个测试故意构造逃出沙箱的路径,随后用这个函数确认系统是以合理方式拒绝的。
调用图:被 1 处调用(remote_test_env_sandboxed_read_rejects_symlink_parent_dotdot_escape);外部调用 4 个(assert!, kind, to_string, panic!)。
remote_exec280–295 ↗
fn remote_exec(script: &str) -> Result<()>
作用:直接在远程 Docker 容器里执行一段 shell 脚本,用来准备或清理测试现场。Docker 容器可以理解成一台隔离的小机器。
数据流:进去的是 shell 脚本文本 → 它读取远程测试环境配置,找到 Docker 容器名,运行 docker exec 执行脚本,并检查退出状态 → 成功时无额外输出,失败时打印标准输出和错误输出并让测试失败。
调用关系:它是一些底层文件系统测试的布景工具。符号链接和目录结构需要在远程 POSIX 系统里精确创建,所以 remote_test_env_sandboxed_read_rejects_symlink_parent_dotdot_escape、remote_test_env_remove_removes_symlink_not_target、remote_test_env_copy_preserves_symlink_source 都会调用它。
调用图:被 3 处调用(remote_test_env_copy_preserves_symlink_source, remote_test_env_remove_removes_symlink_not_target, remote_test_env_sandboxed_read_rejects_symlink_parent_dotdot_escape);外部调用 3 个(assert!, new, get_remote_test_env)。
exec_command_routing_output297–327 ↗
async fn exec_command_routing_output(
test: &TestCodex,
server: &wiremock::MockServer,
call_id: &str,
arguments: Value,
environments: Option<Vec<TurnEnvironmentSelection>>,
) -> Re
作用:模拟模型发起 exec_command,并取回这个命令的输出文本。它让测试能专心检查命令到底跑在哪个环境。
数据流:进去的是测试实例、mock 服务器、调用编号、命令参数和可选环境列表 → 它让 mock 服务器先返回一次函数调用,再返回完成消息;然后提交用户请求;最后从 mock 记录中取出该函数调用的输出 → 出来是一段命令输出字符串。
调用关系:它被 exec_command_routes_to_selected_remote_environment 调用。外层测试负责准备本地和远程的不同 marker 文件,它负责驱动模型调用并把执行结果拿回来给外层断言。
调用图:调用 2 个内部函数(mount_sse_sequence, submit_turn_with_environments);被 1 处调用(exec_command_routes_to_selected_remote_environment);外部调用 1 个(vec!)。
exec_command_routes_to_selected_remote_environment330–404 ↗
async fn exec_command_routes_to_selected_remote_environment() -> Result<()>
作用:验证当模型明确指定远程环境执行命令时,命令真的在远程目录运行,而不是误跑到本机目录。
数据流:进去没有业务参数 → 它跳过不适用环境,启动 mock 服务器,搭好统一执行测试实例;然后分别准备本地和远程目录,各放一个内容不同的 marker 文件;接着让模型执行 cat marker.txt 且指定远程环境 → 输出应包含远程内容、不包含本地内容,最后清理远程目录。
调用关系:它先调用 unified_exec_test 建立带统一执行工具的 Codex,再调用 exec_command_routing_output 驱动一次模型函数调用。这个测试是远程命令路由的核心验收。
调用图:调用 6 个内部函数(start_mock_server, local, exec_command_routing_output, unified_exec_test, from_abs_path, from_path);外部调用 10 个(from, new, assert!, get_remote_test_env, format!, write, json!, skip_if_no_network!, skip_if_wine_exec!, vec!)。
remote_request_permissions_grant_unblocks_later_remote_exec407–610 ↗
async fn remote_request_permissions_grant_unblocks_later_remote_exec() -> Result<()>
作用:验证模型先申请远程写权限、用户批准后,后面的远程命令就能写文件,而且不会再被执行审批卡住。
数据流:进去没有业务参数 → 它开启统一执行、执行审批和权限申请工具;准备本地和远程目录;让 mock 模型先调用 request_permissions 请求远程写目录,再调用 exec_command 写入文件;测试收到权限请求后提交批准响应 → 最后检查权限响应传回模型、命令输出正确、远程文件被写入、本地没有被误写,并清理远程目录。
调用关系:它调用 submit_turn_with_approval_and_environments 来启动带审批策略的一轮任务。它还通过 mount_sse_sequence 安排模型的两步动作,随后用事件等待和 Op::RequestPermissionsResponse 模拟用户批准,是权限申请与远程执行联动的完整流程测试。
调用图:调用 7 个内部函数(mount_sse_sequence, start_mock_server, test_codex, submit_turn_with_approval_and_environments, from_read_write_roots, from_abs_path, from_path);外部调用 14 个(from, new, default, assert!, assert_eq!, get_remote_test_env, wait_for_event, format!, create_dir, panic! (+4 more))。
apply_patch_freeform_routes_to_selected_remote_environment613–698 ↗
async fn apply_patch_freeform_routes_to_selected_remote_environment() -> Result<()>
作用:验证模型用自由格式的 apply_patch 补丁,并在补丁里写明远程环境时,文件会创建在远程而不是本地。
数据流:进去没有业务参数 → 它准备本地临时目录和远程目录;mock 模型返回一个 apply_patch 工具调用,补丁内容包含远程环境 ID 和新增文件;提交任务后读取远程文件内容,并确认本地没有同名文件 → 最后删除远程目录。
调用关系:它直接使用 test_codex 和 mount_sse_sequence 搭出模型补丁调用。它不通过统一 exec,而是测试 apply_patch 自己的环境选择能力。
调用图:调用 4 个内部函数(mount_sse_sequence, start_mock_server, test_codex, from_path);外部调用 9 个(from, new, assert!, assert_eq!, get_remote_test_env, format!, skip_if_no_network!, skip_if_wine_exec!, vec!)。
apply_patch_approvals_are_remembered_per_environment701–885 ↗
async fn apply_patch_approvals_are_remembered_per_environment() -> Result<()>
作用:验证补丁审批是按环境记住的:批准本地不等于批准远程,批准远程后远程后续补丁才可以免再次审批。
数据流:进去没有业务参数 → 它开启按需审批,准备本地和远程环境,以及同一个目标路径;mock 模型依次发本地补丁、远程补丁、远程更新补丁;测试分别等待本地和远程审批并提交“本会话批准”;第三次远程补丁则应直接完成无审批 → 最后检查本地和远程文件内容,并清理文件和目录。
调用关系:它把 submit_turn_with_approval_and_environments、expect_patch_approval 和 wait_for_completion_without_patch_approval 串起来,形成一段完整审批记忆故事。这个测试特别防止“某个环境批准过,另一个环境也被误放行”的安全漏洞。
调用图:调用 7 个内部函数(mount_sse_sequence, start_mock_server, test_codex, expect_patch_approval, submit_turn_with_approval_and_environments, wait_for_completion_without_patch_approval, from_path);外部调用 10 个(from, new, assert_eq!, get_remote_test_env, wait_for_event, format!, remove_file, skip_if_no_network!, skip_if_wine_exec!, vec!)。
apply_patch_intercepted_exec_command_routes_to_selected_remote_environment888–983 ↗
async fn apply_patch_intercepted_exec_command_routes_to_selected_remote_environment() -> Result<()>
作用:验证模型通过 exec_command 运行 apply_patch 命令时,系统拦截到补丁后仍然按指定远程环境执行。也就是说,补丁藏在命令里也不能跑错地方。
数据流:进去没有业务参数 → 它准备本地和远程目录,mock 模型发出 exec_command,命令内容是 apply_patch here-doc,并指定远程环境 ID → Codex 应识别并应用补丁到远程目录;测试读取远程文件确认内容,同时确认本地没有被创建,最后清理远程目录。
调用关系:它调用 unified_exec_test 启用统一执行工具,再用 mount_sse_sequence 模拟 exec_command。它补上了 apply_patch 通过命令通道进入时的路由测试。
调用图:调用 4 个内部函数(mount_sse_sequence, start_mock_server, unified_exec_test, from_path);外部调用 9 个(from, new, assert!, assert_eq!, get_remote_test_env, format!, skip_if_no_network!, skip_if_wine_exec!, vec!)。
remote_test_env_sandboxed_read_allows_readable_root986–1034 ↗
async fn remote_test_env_sandboxed_read_allows_readable_root() -> Result<()>
作用:验证远程沙箱允许读取被明确授权的目录。它证明安全围栏不是把所有操作都挡掉,而是该放行的能放行。
数据流:进去没有业务参数 → 它准备远程目录和文件,创建一个只读沙箱授权该目录,然后带着沙箱读取文件 → 读到的内容应等于写入内容,最后删除远程目录。
调用关系:它调用 read_only_sandbox 生成只读权限,再通过 test_env 拿到远程文件系统执行读写删。它是后面“逃逸路径被拒绝”测试的正向对照。
调用图:调用 3 个内部函数(test_env, read_only_sandbox, from_path);外部调用 6 个(from, assert_eq!, get_remote_test_env, format!, skip_if_no_network!, skip_if_wine_exec!)。
remote_test_env_sandboxed_read_rejects_symlink_parent_dotdot_escape1037–1070 ↗
async fn remote_test_env_sandboxed_read_rejects_symlink_parent_dotdot_escape() -> Result<()>
作用:验证远程沙箱不会被“符号链接 + ..”这种路径技巧绕过。符号链接像快捷方式,.. 表示上一级目录,组合不当可能让人从允许目录偷偷跳到外面。
数据流:进去没有业务参数 → 它用 remote_exec 在远程建出 allowed、outside、secret 文件和一个指向 outside 的链接;然后请求读取 allowed/link/../secret.txt,并带上只允许读 allowed 的沙箱 → 读取应失败,失败原因再交给 assert_normalized_path_rejected 检查,最后清理远程目录。
调用关系:它依赖 remote_exec 精确搭建 POSIX 符号链接场景,依赖 read_only_sandbox 设置围栏,依赖 assert_normalized_path_rejected 判断拒绝是否符合预期。
调用图:调用 5 个内部函数(test_env, assert_normalized_path_rejected, read_only_sandbox, remote_exec, from_path);外部调用 6 个(from, bail!, get_remote_test_env, format!, skip_if_no_network!, skip_if_wine_exec!)。
remote_test_env_remove_removes_symlink_not_target1073–1141 ↗
async fn remote_test_env_remove_removes_symlink_not_target() -> Result<()>
作用:验证在远程环境删除符号链接时,只删除链接本身,不删除它指向的外部文件。这能防止用户以为只是删快捷方式,结果把真正文件删掉。
数据流:进去没有业务参数 → 它用 remote_exec 创建一个外部文件和一个指向它的链接;创建只允许写 allowed 目录的沙箱;在沙箱内删除链接 → 之后检查链接不存在,但外部文件内容仍然是 outside;最后清理整个测试目录。
调用关系:它调用 workspace_write_sandbox 建立写权限边界,也调用 absolute_path 处理路径元数据检查。remote_exec 负责创建符号链接现场,远程文件系统 API 负责执行受沙箱约束的删除。
调用图:调用 6 个内部函数(test_env, absolute_path, remote_exec, workspace_write_sandbox, from_abs_path, from_path);外部调用 7 个(from, assert!, assert_eq!, get_remote_test_env, format!, skip_if_no_network!, skip_if_wine_exec!)。
remote_test_env_copy_preserves_symlink_source1144–1217 ↗
async fn remote_test_env_copy_preserves_symlink_source() -> Result<()>
作用:验证复制远程符号链接时,复制的是链接本身,而不是把链接指向的文件内容复制成普通文件。这样才能保留文件系统语义。
数据流:进去没有业务参数 → 它用 remote_exec 创建外部文件和指向它的源链接;创建只允许写 allowed 目录的沙箱;把源链接复制成新链接;然后直接在 Docker 容器里用 readlink 查看新链接指向哪里 → 新链接应仍指向原来的外部文件,最后清理测试目录。
调用关系:它使用 workspace_write_sandbox 限定复制操作范围,用 remote_exec 准备链接结构,再用 docker readlink 做底层验证。这个测试覆盖远程文件系统 copy 操作对符号链接的处理。
调用图:调用 4 个内部函数(test_env, remote_exec, workspace_write_sandbox, from_path);外部调用 8 个(from, assert!, assert_eq!, new, get_remote_test_env, format!, skip_if_no_network!, skip_if_wine_exec!)。