Codex 系统手册

App-server 集成套件 — thread、turn、review 和会话状态生命周期

stage-23.1.4.433 个文件

这一阶段像给对话服务器做全流程体检,主要发生在系统干活儿时和保存状态时。它检查新建、读取、列出、恢复、复制、归档、删除会话这些“档案柜”动作;也检查一轮对话的开始、中断、临时追加指令、权限申请、向用户追问和工具调用。还有改名、改设置、元数据、记忆开关、摘要、压缩、代码审查和安全提示等测试,确保客户端看到的通知、数据库和磁盘里的记录三边一致。

本阶段的文件33

线程发现和启动

这些测试确定线程在 rollout 支持、内存中和远程存储持久化模式下如何创建、摘要、列出、读取和恢复。

app-server/tests/suite/conversation_summary.rs源码 ↗
testtest

这份测试像是在替服务器做“查病历”的演练:给它一个对话编号或文件路径,看它能不能找到对应的对话摘要,并把编号、路径、时间、预览文字、模型来源等信息返回对。文件先造出假的对话数据,比如临时目录里的 rollout 文件,或一个内存线程仓库(一种测试用的临时存储,程序结束就清掉)。然后启动测试服务器,发送 getConversationSummary 请求,最后把服务器回的内容和预期结果逐项比较。这里还特别处理了路径标准化,因为不同系统或临时目录下,同一个文件可能有相对路径、真实路径等不同写法。最重要的是,它覆盖了几个容易出错的边角:按线程编号查、按相对 rollout 路径查、以及内存线程没有文件路径时也不能崩。

函数细节8
expected_summary48–61 ↗
fn expected_summary(conversation_id: ThreadId, path: PathBuf) -> ConversationSummary

作用:这个函数用来拼出“正确答案”的对话摘要,方便测试拿服务器返回值来对比。它把测试里固定好的预览文字、时间、模型供应商等信息装成一个 ConversationSummary。

数据流:进去的是一个对话编号和一个文件路径 → 函数把它们和测试常量,比如预览文字、创建时间、模型提供方、当前目录等组合起来 → 出来的是一份预期的 ConversationSummary,不会改动外部状态。

调用关系:它是测试里的对照表生成器。按线程编号读取 rollout 的测试,以及按相对 rollout 路径读取的测试,都会先调用它做出期望结果,然后再和服务器实际返回的摘要比较。

调用图:被 2 处调用(get_conversation_summary_by_relative_rollout_path_resolves_from_codex_home, get_conversation_summary_by_thread_id_reads_rollout);外部调用 1 个(from)。

normalized_canonical_path63–65 ↗
fn normalized_canonical_path(path: impl AsRef<Path>) -> Result<PathBuf>

作用:这个函数把路径整理成系统认可的真实绝对路径。这样测试比较路径时,不会因为相对路径、符号链接或不同写法而误判失败。

数据流:进去的是一个路径 → 它先让操作系统解析成规范路径,再转成项目使用的 AbsolutePathBuf(绝对路径类型,用来保证路径确实是绝对的)→ 出来的是普通 PathBuf 形式的规范路径;如果路径不存在或不是合法绝对路径,就返回错误。

调用关系:它是路径比较前的“统一格式”步骤。两个 rollout 相关测试会用它整理预期路径,normalized_summary_path 也会用它整理服务器返回的路径。

调用图:调用 1 个内部函数(from_absolute_path);被 3 处调用(get_conversation_summary_by_relative_rollout_path_resolves_from_codex_home, get_conversation_summary_by_thread_id_reads_rollout, normalized_summary_path);外部调用 1 个(as_ref)。

normalized_summary_path67–72 ↗
fn normalized_summary_path(mut summary: ConversationSummary) -> Result<ConversationSummary>

作用:这个函数专门整理摘要里的 path 字段,让收到的摘要更适合拿来和预期值比较。如果摘要没有路径,它会保持空路径不动。

数据流:进去的是一份 ConversationSummary → 如果里面的 path 不是空的,就调用 normalized_canonical_path 把路径变成规范绝对路径;如果是空的就跳过 → 出来的是路径已经被整理过的 ConversationSummary。

调用关系:它在测试断言前使用,属于比较结果前的清洗步骤。它把具体的路径整理工作交给 normalized_canonical_path,避免测试因为路径写法不同而失败。

调用图:调用 1 个内部函数(normalized_canonical_path)。

get_conversation_summary_by_thread_id_reads_rollout75–112 ↗
async fn get_conversation_summary_by_thread_id_reads_rollout() -> Result<()>

作用:这个测试确认:只给服务器一个对话编号时,服务器能从磁盘上的 rollout 文件里读出正确的对话摘要。rollout 可以理解成保存对话历史的文件。

数据流:开始时它创建一个临时 codex_home 目录,并写入一份假的 rollout 对话文件 → 用文件里的对话编号生成预期摘要 → 启动 TestAppServer,初始化后发送按 ThreadId 查询摘要的请求 → 读到 JSON-RPC 响应后转成 GetConversationSummaryResponse → 最后把返回的摘要路径标准化,再和预期摘要比较。

调用关系:这是端到端测试的一条主路径。它会调用 create_fake_rollout 准备测试文件,调用 expected_summary 生成正确答案,调用 normalized_canonical_path 和 normalized_summary_path 处理路径,然后通过 TestAppServer 走真实的请求和响应流程。

调用图:调用 4 个内部函数(new, expected_summary, normalized_canonical_path, from_string);外部调用 7 个(new, Integer, create_fake_rollout, rollout_path, to_response, assert_eq!, timeout)。

get_conversation_summary_by_thread_id_reads_pathless_store_thread115–196 ↗
async fn get_conversation_summary_by_thread_id_reads_pathless_store_thread() -> Result<()>

作用:这个测试确认:如果对话存在于内存线程仓库里,而且没有对应的磁盘路径,服务器也能返回一份合理摘要,不应该因为没有 path 或 cwd 就失败。

数据流:开始时它创建临时目录和一个唯一的内存仓库编号 → 写入一份 config.toml,让服务器使用这个内存线程仓库 → 在仓库里创建一个线程, metadata 里故意不放 cwd 路径 → 用 in_process::start 启动一个进程内服务器客户端 → 发送 GetConversationSummary 请求 → 把 JSON 结果解析成摘要 → 检查对话编号正确、path 和 cwd 是空路径、model_provider 被返回为预期值 → 最后关闭客户端。

调用关系:它覆盖的是比较特殊的存储路径:不经过 TestAppServer 的文件 rollout,而是直接用 in_process 启动应用服务器,并让它读取 InMemoryThreadStore。它依赖 create_config_toml_with_in_memory_thread_store 写测试配置,也用 InMemoryThreadStoreId 在测试结束时清理内存仓库编号。

调用图:调用 9 个内部函数(start, create_config_toml_with_in_memory_thread_store, default, without_managed_config_for_tests, default_for_tests, new, default, from_string, for_id);外部调用 10 个(new, default, new, new_v4, new, Integer, default, assert_eq!, default, from_value)。

get_conversation_summary_by_relative_rollout_path_resolves_from_codex_home199–232 ↗
async fn get_conversation_summary_by_relative_rollout_path_resolves_from_codex_home() -> Result<()>

作用:这个测试确认:请求里传的是相对 rollout 路径时,服务器会把它当成相对于 codex_home 的路径来找文件。这样客户端不一定非要传完整绝对路径。

数据流:开始时它在临时 codex_home 里创建假的 rollout 文件 → 从完整路径中截出相对于 codex_home 的相对路径 → 生成预期摘要,预期里的路径仍是规范后的真实路径 → 启动测试服务器并初始化 → 发送按 RolloutPath 查询摘要的请求 → 收到响应后解析并标准化返回路径 → 最后和预期摘要比较。

调用关系:它和按线程编号读取 rollout 的测试很像,但入口参数不同:这里测试的是 RolloutPath。它同样借助 create_fake_rollout 准备文件,expected_summary 准备期望结果,normalized_canonical_path 和 normalized_summary_path 负责把路径写法统一。

调用图:调用 4 个内部函数(new, expected_summary, normalized_canonical_path, from_string);外部调用 7 个(new, Integer, create_fake_rollout, rollout_path, to_response, assert_eq!, timeout)。

InMemoryThreadStoreId::drop239–241 ↗
fn drop(&mut self)

作用:这个函数在测试辅助对象被销毁时自动清理内存线程仓库。Drop 是 Rust 的自动收尾机制,类似“离开房间随手关灯”。

数据流:进去的是这个辅助对象自身,里面保存着 store_id → 当对象生命周期结束时,它调用 InMemoryThreadStore::remove_id 删除对应的内存仓库记录 → 没有返回值,但会改动全局的测试内存仓库状态,避免影响后续测试。

调用关系:它服务于内存线程仓库那条测试。get_conversation_summary_by_thread_id_reads_pathless_store_thread 创建 InMemoryThreadStoreId 后,不需要手动清理;测试结束或提前出错时,drop 会自动把清理工作交给 InMemoryThreadStore::remove_id。

调用图:调用 1 个内部函数(remove_id)。

create_config_toml_with_in_memory_thread_store244–268 ↗
fn create_config_toml_with_in_memory_thread_store(
    codex_home: &Path,
    store_id: &str,
) -> std::io::Result<()>

作用:这个函数给测试临时写一份 config.toml,告诉服务器使用指定编号的内存线程仓库。没有它,服务器启动时就不知道应该去哪个测试仓库里找线程。

数据流:进去的是 codex_home 路径和 store_id → 它把一段 TOML 配置文字拼出来,里面包含模型、审批策略、沙盒模式、内存线程仓库编号和假的模型提供方配置 → 写到 codex_home/config.toml → 成功则返回空结果,写文件失败则返回 I/O 错误。

调用关系:它只被 get_conversation_summary_by_thread_id_reads_pathless_store_thread 调用,用来准备那条测试所需的配置文件。之后服务器启动时会读取这份配置,从而连接到同一个 InMemoryThreadStore。

调用图:被 1 处调用(get_conversation_summary_by_thread_id_reads_pathless_store_thread);外部调用 3 个(join, format!, write)。

app-server/tests/suite/v2/remote_thread_store.rs源码 ↗
testtest execution

这里测试的是“线程存储”,可以理解成保存一段对话历史的仓库。正常情况下,程序可能会把对话写到本地目录;但如果配置里写了 experimental_thread_store,意思就是要改用指定的存储。这个文件用一个只在测试里存在的内存仓库来假装“远程仓库”,这样测试速度快,也不会真的碰外部服务。测试会启动一个进程内 app-server,发起 thread/start、turn/start、thread/list、thread/delete 等请求,然后检查两件事:第一,内存仓库确实收到了创建、追加、列出、删除等调用;第二,临时的 codex_home 目录里没有 sessions、archived_sessions、sqlite 等本地持久化痕迹。另一个测试还检查“冷启动后恢复线程”时,程序会复用带历史的远程读取探测,而不是走错本地路径。文件里还有一些小工具函数,用来写测试配置、启动测试服务器、删除线程、检查目录残留,以及在测试结束时清理内存仓库编号。

函数细节9
thread_delete_with_non_local_thread_store_does_not_create_local_persistence64–191 ↗
async fn thread_delete_with_non_local_thread_store_does_not_create_local_persistence() -> Result<()>

作用:这是主回归测试:它验证配置了非本地线程存储后,创建对话、进行一轮对话、列出和删除线程,都不会在本地目录留下会话或数据库文件。简单说,它是在防止“明明说好存远程,程序却偷偷写本地”。

数据流:进去的是一个临时 codex_home 目录、一个假的模型响应服务器地址、一个随机生成的内存仓库编号。它先写好配置文件,再启动 app-server,发请求创建线程、发送用户消息、等待回合完成、列出线程、删除线程,还额外创建一个未加载到当前服务里的线程再删除。最后它读取内存仓库的调用计数,确认相关操作都走了内存仓库,并扫描 codex_home,确认没有本地持久化文件出现。

调用关系:这个测试会调用 create_config_toml_with_thread_store 准备配置,调用 start_in_process_server 启动测试用 app-server,过程中用 delete_thread 做删除请求,最后把检查本地文件的工作交给 assert_no_local_persistence_artifacts。它是整个文件里最完整的一条端到端验证流程。

调用图:调用 7 个内部函数(assert_no_local_persistence_artifacts, create_config_toml_with_thread_store, delete_thread, start_in_process_server, default, from_string, for_id);外部调用 13 个(default, new, new_v4, new, bail!, Integer, default, create_mock_responses_server_repeating_assistant, assert!, assert_eq! (+3 more))。

cold_thread_resume_reuses_non_local_history_probe194–275 ↗
async fn cold_thread_resume_reuses_non_local_history_probe() -> Result<()>

作用:这个测试确认服务重启后恢复旧线程时,会去非本地线程存储里读取带历史的线程信息。它关注的是“冷启动恢复”这个容易走错路径的场景。

数据流:进去的是临时目录、假的模型服务器、随机内存仓库编号,以及用这些信息构建出来的配置。它先启动客户端,创建线程并完成一轮对话,让线程真正写进内存仓库;然后关闭服务,再用同一份配置重新启动。重启后它发起 thread/resume 请求,并比较内存仓库的 read_thread_with_history 调用次数,确认恢复动作确实多读了一次带历史的线程数据。

调用关系:它调用 create_config_toml_with_thread_store 写配置,调用 start_in_process_client 两次来模拟先运行、再重启。这个测试不追求恢复一定成功到最后,因为测试用的内存仓库没有本地路径;它真正要抓的是恢复流程有没有用到非本地历史读取。

调用图:调用 4 个内部函数(create_config_toml_with_thread_store, start_in_process_client, without_managed_config_for_tests, for_id);外部调用 13 个(new, default, new, new_v4, bail!, Integer, default, create_mock_responses_server_repeating_assistant, assert_eq!, default (+3 more))。

start_in_process_server277–289 ↗
async fn start_in_process_server(codex_home: &Path) -> Result<InProcessClientHandle>

作用:这个辅助函数用一个 codex_home 路径快速启动测试用的 app-server 客户端句柄。它把“创建配置再启动”的重复步骤包起来,方便测试主体读起来更像业务流程。

数据流:进去的是 codex_home 路径。它创建一份测试用加载配置,指定这个目录既是配置家目录也是备用工作目录,然后把配置和加载覆盖项交给 start_in_process_client。出来的是一个可以向进程内服务器发请求、收事件、关闭服务的客户端句柄。

调用关系:它被 thread_delete_with_non_local_thread_store_does_not_create_local_persistence 调用。自己不直接启动底层服务,而是先准备 Config,再把真正启动工作交给 start_in_process_client。

调用图:调用 2 个内部函数(start_in_process_client, without_managed_config_for_tests);被 1 处调用(thread_delete_with_non_local_thread_store_does_not_create_local_persistence);外部调用 3 个(new, to_path_buf, default)。

start_in_process_client291–321 ↗
async fn start_in_process_client(
    config: Arc<Config>,
    loader_overrides: LoaderOverrides,
) -> std::io::Result<InProcessClientHandle>

作用:这个辅助函数真正启动一个“进程内”的 app-server。进程内的意思是不用真的开一个外部进程,而是在测试进程里跑服务,测试可以直接发请求。

数据流:进去的是已经构建好的 Config 和加载配置时的覆盖项。它把测试需要的启动参数装进 InProcessStartArgs,比如客户端信息、测试环境管理器、反馈对象、会话来源、通道容量等,然后调用 in_process::start。出来的是 InProcessClientHandle,也就是测试和服务通信的把手。

调用关系:它被 cold_thread_resume_reuses_non_local_history_probe 直接调用,也被 start_in_process_server 间接调用。它位于所有测试启动链路的底层,负责把各种零件交给 app-server 的进程内启动函数。

调用图:调用 4 个内部函数(start, default, default_for_tests, new);被 2 处调用(cold_thread_resume_reuses_non_local_history_probe, start_in_process_server);外部调用 3 个(new, new, default)。

delete_thread323–337 ↗
async fn delete_thread(
    client: &InProcessClientHandle,
    request_id: i64,
    thread_id: String,
) -> Result<()>

作用:这个辅助函数向 app-server 发送 thread/delete 请求,并确认删除请求没有失败。它让测试里删除线程的代码更短、更清楚。

数据流:进去的是客户端句柄、请求编号和线程 ID 字符串。它组装 ThreadDelete 请求发给服务器,如果服务器返回错误,就把错误信息包装成测试失败;如果成功,就把返回的 JSON 解析成 ThreadDeleteResponse。出来的是 Ok,表示删除完成;如果任何一步失败,就返回错误。

调用关系:它被 thread_delete_with_non_local_thread_store_does_not_create_local_persistence 调用两次,一次删除当前已加载的线程,一次删除只存在于内存仓库里的线程。它把具体协议请求细节藏起来,让主测试专注验证存储路径。

调用图:调用 1 个内部函数(request);被 1 处调用(thread_delete_with_non_local_thread_store_does_not_create_local_persistence);外部调用 2 个(Integer, from_value)。

assert_no_local_persistence_artifacts339–391 ↗
fn assert_no_local_persistence_artifacts(codex_home: &Path) -> Result<()>

作用:这个检查函数确认 codex_home 目录里没有出现本地线程持久化的痕迹,比如会话目录或 SQLite 数据库文件。它相当于测试里的“现场勘查员”。

数据流:进去的是 codex_home 路径。它检查 sessions、archived_sessions 和状态数据库路径是否不存在;再扫描目录,找有没有 .sqlite、.sqlite-shm、.sqlite-wal 这类数据库相关文件;最后读取目录项,去掉允许存在的 shell_snapshots 后,确认只剩 config.toml、installation_id、skills 这些预期文件。出来的是 Ok,或者在发现多余文件时让测试失败。

调用关系:它被 thread_delete_with_non_local_thread_store_does_not_create_local_persistence 在所有操作完成后调用。它会进一步调用 codex_home_entries 收集目录内容,是判断“有没有偷偷写本地”的关键证据来源。

调用图:调用 1 个内部函数(codex_home_entries);被 1 处调用(thread_delete_with_non_local_thread_store_does_not_create_local_persistence);外部调用 3 个(assert!, assert_eq!, read_dir)。

codex_home_entries393–400 ↗
fn codex_home_entries(codex_home: &Path) -> Result<BTreeSet<String>>

作用:这个小工具读取 codex_home 目录下有哪些顶层文件或文件夹,并把名字整理成一个有序集合。它主要服务于更大的本地残留检查。

数据流:进去的是 codex_home 路径。它读取这个目录,把每个目录项的文件名转成字符串,放进 BTreeSet;BTreeSet 是一种有序集合,方便之后稳定地比较“实际有哪些”和“应该有哪些”。出来的是这组文件名,或读取目录失败时的错误。

调用关系:它只被 assert_no_local_persistence_artifacts 调用。后者用它拿到目录清单,再和允许存在的文件列表做比较。

调用图:被 1 处调用(assert_no_local_persistence_artifacts);外部调用 1 个(read_dir)。

InMemoryThreadStoreId::drop407–409 ↗
fn drop(&mut self)

作用:这个函数在 InMemoryThreadStoreId 被销毁时自动清理对应的内存仓库编号。它防止一个测试留下的内存仓库注册信息影响后面的测试。

数据流:进去的是这个结构体里保存的 store_id。对象离开作用域时,Rust 会自动调用 drop;它用 store_id 调用 InMemoryThreadStore::remove_id,把这个测试用内存仓库从全局注册处移除。出来没有返回值,但全局测试状态被清干净了。

调用关系:测试函数创建 InMemoryThreadStoreId 后不直接调用 drop,而是依靠语言自动在变量生命周期结束时触发。它把清理工作交给 InMemoryThreadStore::remove_id,是测试隔离的一道保险。

调用图:调用 1 个内部函数(remove_id)。

create_config_toml_with_thread_store412–440 ↗
fn create_config_toml_with_thread_store(
    codex_home: &Path,
    server_uri: &str,
    store_id: &str,
) -> std::io::Result<()>

作用:这个辅助函数给临时 codex_home 写入一份 config.toml,里面明确指定使用测试用的内存线程存储。没有这份配置,app-server 启动时就不会走被测试的非本地存储路径。

数据流:进去的是 codex_home 路径、假的模型服务器地址、内存仓库编号。它拼出一段 TOML 配置文本,写入 codex_home/config.toml;配置里包含模型名、永不审批、只读沙箱、experimental_thread_store 的类型和编号、mock 模型提供方地址,并关闭插件功能。出来的是写文件的结果,成功表示测试环境配置好了。

调用关系:它被两个测试函数调用,都是在启动 app-server 之前执行。它为后续 start_in_process_server 或 start_in_process_client 提供关键输入,让服务启动时选择 InMemoryThreadStore,而不是默认本地存储。

调用图:被 2 处调用(cold_thread_resume_reuses_non_local_history_probe, thread_delete_with_non_local_thread_store_does_not_create_local_persistence);外部调用 3 个(join, format!, write)。

app-server/tests/suite/v2/thread_start.rs源码 ↗
testtest

这里测试的是“开始一个新线程”这件事。线程可以理解成一次新的聊天会话,服务器收到 thread/start 请求后,要返回线程信息,还要发通知,并把模型、工作目录、权限、说明文件、项目配置等都准备好。这个文件用临时目录当假的用户配置目录,用 mock server(假的网络服务器)代替真实模型服务,这样测试稳定、不会真的联网。每个测试都启动一个 TestAppServer,发 JSON-RPC 请求(用 JSON 格式远程调用函数的一种协议),再检查响应、错误或通知是否符合约定。它特别关注边界情况:路径必须是绝对路径、空的项目说明文件不能算进去、临时线程不能暴露磁盘路径、坏掉的 MCP 服务要给出清楚状态、提升沙箱权限时是否会写入项目信任。没有这些测试,客户端和服务器之间的“线程启动合同”很容易悄悄变形,导致前端、插件或真实用户遇到难查的问题。

函数细节30
thread_start_creates_thread_and_emits_started58–200 ↗
async fn thread_start_creates_thread_and_emits_started() -> Result<()>

作用:检查最基本的开新线程流程:服务器要返回一个合法线程,并随后发出 thread/started 通知。它还确认返回字段的 JSON 名字没有变,比如线程标题字段要叫 name,新线程时是空值。

数据流:进去的是一个临时配置目录、一个假的模型服务地址,以及带模型名和来源的 thread/start 请求 → 测试启动服务器、初始化、发送请求、读取响应和通知 → 出来的是一组断言:线程有 id、有 session id、状态是空闲、来源是 user、路径尚未真正落盘,并且通知里的线程和响应里的线程一致。

调用关系:这是测试运行器直接执行的用例。它先调用 create_config_toml_without_approval_policy 写测试配置,再借助测试服务器发送请求;如果收到不该有的状态变化通知,会用 bail! 让测试失败。

调用图:调用 2 个内部函数(new, create_config_toml_without_approval_policy);外部调用 10 个(default, new, bail!, Integer, create_mock_responses_server_repeating_assistant, assert!, assert_eq!, from_value, now, timeout)。

thread_start_accepts_absolute_runtime_workspace_roots203–239 ↗
async fn thread_start_accepts_absolute_runtime_workspace_roots() -> Result<()>

作用:检查请求里传入的运行时工作区根目录如果是绝对路径,服务器会接受并原样返回。工作区根目录就是允许这次线程访问的一块文件夹范围。

数据流:进去的是一个临时当前目录和一个额外的绝对路径工作区 → 测试创建这些目录,发送包含 cwdruntime_workspace_roots 的启动请求 → 出来的是响应里的当前目录和运行时工作区列表,二者都应该是绝对路径并和输入一致。

调用关系:测试运行器执行它。它使用 create_config_toml_without_approval_policy 准备模型配置,再通过 TestAppServer 完成初始化、发请求和读响应。

调用图:调用 2 个内部函数(new, create_config_toml_without_approval_policy);外部调用 8 个(default, new, Integer, create_mock_responses_server_repeating_assistant, assert_eq!, create_dir_all, timeout, vec!)。

thread_start_excludes_profile_workspace_roots_from_runtime_workspace_roots242–280 ↗
async fn thread_start_excludes_profile_workspace_roots_from_runtime_workspace_roots() -> Result<()>

作用:确认配置文件里预设的工作区根目录,不会被错误地混进本次请求的运行时工作区根目录里。这样客户端看到的运行时范围只反映这次启动真正选择的目录。

数据流:进去的是带有 profile workspace root 的配置,以及另一个作为 cwd 的临时目录 → 服务器启动线程时会合成工作区信息 → 出来时响应里的 runtime_workspace_roots 只包含本次 cwd,不包含配置里那个 profile root。

调用关系:这个用例调用 create_config_toml_with_profile_workspace_root 写一份特殊配置,然后通过测试服务器验证 thread/start 的返回值。

调用图:调用 2 个内部函数(new, create_config_toml_with_profile_workspace_root);外部调用 6 个(default, new, Integer, create_mock_responses_server_repeating_assistant, assert_eq!, timeout)。

thread_start_rejects_unknown_environment_as_invalid_request283–316 ↗
async fn thread_start_rejects_unknown_environment_as_invalid_request() -> Result<()>

作用:检查如果请求指定了不存在的运行环境,服务器要明确拒绝,而不是默默忽略。运行环境可以理解成线程执行命令时要用的某个工作地点或执行目标。

数据流:进去的是一个环境 id 为 missing 的启动请求 → 服务器查不到这个环境 → 出来的是 JSON-RPC 错误,错误码是“无效请求”,错误消息说明 unknown turn environment id。

调用关系:测试运行器执行它。它复用普通配置 helper,然后要求 TestAppServer 读取错误响应,而不是成功响应。

调用图:调用 2 个内部函数(new, create_config_toml_without_approval_policy);外部调用 7 个(default, new, Integer, create_mock_responses_server_repeating_assistant, assert_eq!, timeout, vec!)。

thread_start_rejects_relative_environment_cwd_as_invalid_request319–350 ↗
async fn thread_start_rejects_relative_environment_cwd_as_invalid_request() -> Result<()>

作用:检查环境里的当前目录不能写成相对路径,比如 relative,必须是完整的绝对路径。这样可以避免服务器和客户端对“到底在哪个目录”理解不一致。

数据流:进去的是一个环境 id 为 local、但 cwd 是相对路径的请求 → 服务器校验路径格式 → 出来的是无效请求错误,消息指出这个 cwd 不是 POSIX 或 Windows 绝对路径。

调用关系:这个测试由测试框架运行,先用普通配置启动服务器,再发送故意错误的请求,最后只等待错误消息。

调用图:调用 2 个内部函数(new, create_config_toml_without_approval_policy);外部调用 7 个(default, new, Integer, create_mock_responses_server_repeating_assistant, assert_eq!, timeout, vec!)。

thread_start_response_includes_loaded_instruction_sources353–397 ↗
async fn thread_start_response_includes_loaded_instruction_sources() -> Result<()>

作用:确认线程启动响应会列出实际加载到的说明文件来源。说明文件这里指 AGENTS.md,里面通常放给模型看的项目规则或操作指南。

数据流:进去的是一个全局 AGENTS.md 和一个项目里的 AGENTS.md → 启动线程时服务器读取这些说明文件 → 出来的是响应中的 instruction_sources,应该同时包含全局文件和项目文件路径。

调用关系:它调用普通配置 helper,自己写入两个说明文件,并用 normalize_path_for_comparison 抹平不同系统上的路径显示差异后比较结果。

调用图:调用 2 个内部函数(new, create_config_toml_without_approval_policy);外部调用 8 个(default, new, Integer, create_mock_responses_server_repeating_assistant, assert_eq!, write, timeout, vec!)。

thread_start_response_excludes_empty_project_instruction_source400–440 ↗
async fn thread_start_response_excludes_empty_project_instruction_source() -> Result<()>

作用:确认空的项目 AGENTS.md 不会被当作有效说明来源返回。这样客户端不会误以为项目里真的有可用说明。

数据流:进去的是一个有内容的全局说明文件和一个空的项目说明文件 → 服务器启动线程并扫描说明来源 → 出来时只返回全局说明文件路径,不返回空项目文件。

调用关系:它和上一条测试类似,也使用普通配置 helper,并用 normalize_path_for_comparison 做跨平台路径比较。

调用图:调用 2 个内部函数(new, create_config_toml_without_approval_policy);外部调用 8 个(default, new, Integer, create_mock_responses_server_repeating_assistant, assert_eq!, write, timeout, vec!)。

thread_start_without_selected_environment_includes_only_global_instruction_source443–521 ↗
async fn thread_start_without_selected_environment_includes_only_global_instruction_source() -> Result<()>

作用:检查当请求明确说“不选择任何环境”时,线程只加载全局说明,不加载项目目录里的说明。这样用户没有选工作环境时,不会意外把某个项目规则塞给模型。

数据流:进去的是全局和项目两份 AGENTS.md,以及 environments: [] 的线程启动请求 → 服务器启动线程,只记录全局说明来源;随后测试再发一轮用户消息 → 出来时响应只列出全局说明,并且发给假模型的请求正文包含全局说明、不包含项目说明。

调用关系:这个用例先测试 thread/start 的返回,再继续调用 send_turn_start_request 开始一轮对话,用 mock 模型服务收到的请求内容反向验证真实加载效果。

调用图:调用 2 个内部函数(new, create_config_toml_without_approval_policy);外部调用 10 个(default, new, new, Integer, create_mock_responses_server_repeating_assistant, assert!, assert_eq!, write, timeout, vec!)。

normalize_path_for_comparison531–533 ↗
fn normalize_path_for_comparison(path: impl AsRef<Path>) -> PathBuf

作用:把路径整理成适合测试比较的样子,主要是处理 Windows 上可能出现的特殊长路径前缀。它让同一个文件路径不会因为显示形式不同而导致测试误失败。

数据流:进去的是一个路径 → 在 Windows 上会把开头的 \\?\ 去掉,其他系统基本原样转换成 PathBuf → 出来的是统一后的路径对象,不改动磁盘文件。

调用关系:它是本文件内部的小工具,被说明文件来源相关的测试调用,用来保证路径断言在不同操作系统上都稳定。

调用图:外部调用 4 个(as_ref, display, strip_prefix, from)。

thread_start_tracks_thread_initialized_analytics536–571 ↗
async fn thread_start_tracks_thread_initialized_analytics() -> Result<()>

作用:检查创建线程时会发送一条“线程已初始化”的统计事件。统计事件可以帮助产品或运维知道线程是怎么被创建的,但测试要保证它不会漏发或字段错乱。

数据流:进去的是带 ChatGPT base URL 的配置和一个用于接收统计的 mock server → 测试启动服务器、发起线程启动请求、等待统计请求到达 → 出来的是对统计 payload 的断言:事件数量是 1,线程 id、session id、模型名、来源等字段都正确。

调用关系:它调用 create_config_toml_with_chatgpt_base_url 配置网络地址,再用 analytics 测试工具 mount_analytics_capturewait_for_analytics_payload 和断言 helper 检查上报内容。

调用图:调用 6 个内部函数(new_without_managed_config, assert_basic_thread_initialized_event, mount_analytics_capture, thread_initialized_event, wait_for_analytics_payload, create_config_toml_with_chatgpt_base_url);外部调用 6 个(default, new, Integer, create_mock_responses_server_repeating_assistant, assert_eq!, timeout)。

thread_start_respects_project_config_from_cwd574–612 ↗
async fn thread_start_respects_project_config_from_cwd() -> Result<()>

作用:确认线程启动时会根据传入的当前目录读取项目自己的配置。项目配置只有在项目被信任时才应该生效。

数据流:进去的是一个带 .codex/config.toml 的工作目录,里面设置推理强度为 high,并且测试先把这个项目标记为 trusted → 启动线程时服务器从 cwd 找到并加载项目配置 → 出来时响应里的 reasoning_effort 是 High。

调用关系:它调用普通配置 helper 创建全局配置,再调用 set_project_trust_level 写入项目信任状态,然后通过 thread/start 验证配置加载结果。

调用图:调用 3 个内部函数(new, create_config_toml_without_approval_policy, set_project_trust_level);外部调用 8 个(default, new, Integer, create_mock_responses_server_repeating_assistant, assert_eq!, create_dir_all, write, timeout)。

thread_start_drops_unsupported_service_tier_id615–642 ↗
async fn thread_start_drops_unsupported_service_tier_id() -> Result<()>

作用:检查客户端传入一个不支持的服务档位 id 时,服务器不会把它原样返回。服务档位可以理解成模型服务的计费或能力等级。

数据流:进去的是 service_tierexperimental-tier-id 的启动请求 → 服务器在会话配置阶段判断这个档位不在支持范围内并丢弃 → 出来时响应里的 service_tier 是空。

调用关系:测试运行器执行它。它只需要普通配置和一次 thread/start 请求,重点验证返回字段是否被清理。

调用图:调用 2 个内部函数(new, create_config_toml_without_approval_policy);外部调用 6 个(default, new, Integer, create_mock_responses_server_repeating_assistant, assert_eq!, timeout)。

thread_start_accepts_default_service_tier645–673 ↗
async fn thread_start_accepts_default_service_tier() -> Result<()>

作用:确认默认服务档位这个特殊值会被接受并返回。这样客户端明确请求默认档位时,不会被当成未知值丢掉。

数据流:进去的是 service_tier 设置为默认请求值的启动请求 → 服务器识别这是受支持的档位 → 出来时响应仍包含这个默认服务档位字符串。

调用关系:它复用普通配置 helper,通过测试服务器发一次启动请求,和上一条“不支持档位会被丢弃”的测试形成对照。

调用图:调用 2 个内部函数(new, create_config_toml_without_approval_policy);外部调用 6 个(default, new, Integer, create_mock_responses_server_repeating_assistant, assert_eq!, timeout)。

thread_start_accepts_metrics_service_name676–701 ↗
async fn thread_start_accepts_metrics_service_name() -> Result<()>

作用:确认请求里带统计用的服务名时,线程仍然能正常创建。服务名主要用于指标归类,不应该影响创建线程的主流程。

数据流:进去的是带 service_name: my_app_server_client 的线程启动请求 → 服务器接受这个额外字段并创建线程 → 出来时响应中线程 id 不为空,说明流程成功。

调用关系:这个测试使用普通配置 helper,目标不是检查统计内容,而是确保这个参数不会让 thread/start 失败。

调用图:调用 2 个内部函数(new, create_config_toml_without_approval_policy);外部调用 6 个(default, new, Integer, create_mock_responses_server_repeating_assistant, assert!, timeout)。

thread_start_ephemeral_remains_pathless704–746 ↗
async fn thread_start_ephemeral_remains_pathless() -> Result<()>

作用:检查临时线程会被标记为临时,并且不暴露磁盘路径。临时线程可以理解成“不打算保存到会话文件”的一次性会话。

数据流:进去的是 ephemeral: true 的启动请求 → 服务器创建临时线程但不分配可见保存路径 → 出来时线程的 ephemeral 为 true,path 是空,JSON 里也明确序列化为 ephemeral: true

调用关系:它通过普通配置启动服务器,然后专门检查响应对象和原始 JSON 字段,保证类型层和线上的 JSON 合同都正确。

调用图:调用 2 个内部函数(new, create_config_toml_without_approval_policy);外部调用 7 个(default, new, Integer, create_mock_responses_server_repeating_assistant, assert!, assert_eq!, timeout)。

thread_start_fails_when_required_mcp_server_fails_to_initialize749–782 ↗
async fn thread_start_fails_when_required_mcp_server_fails_to_initialize() -> Result<()>

作用:检查必需的 MCP 服务启动失败时,线程启动必须失败。MCP 服务可以理解成模型可调用的外部工具服务;如果标成必需,缺了它就不能继续。

数据流:进去的是一份配置,里面有一个会立刻退出的必需 MCP 服务 → 服务器尝试启动线程并初始化 MCP 服务 → 出来的是错误响应,错误消息说明 required MCP servers failed to initialize,并点名 required_broken

调用关系:它调用 create_config_toml_with_required_broken_mcp 写坏服务配置,而这个 helper 又依赖 broken_mcp_transport_toml 生成跨平台的失败命令。

调用图:调用 2 个内部函数(new, create_config_toml_with_required_broken_mcp);外部调用 6 个(new, Integer, default, create_mock_responses_server_repeating_assistant, assert!, timeout)。

thread_start_emits_mcp_server_status_updated_notifications785–881 ↗
async fn thread_start_emits_mcp_server_status_updated_notifications() -> Result<()>

作用:检查可选 MCP 服务启动失败时,线程本身仍能创建,但服务器要发出“开始启动”和“启动失败”的状态通知。这样客户端可以展示工具服务的实时状态。

数据流:进去的是一份带可选坏 MCP 服务的配置 → 服务器启动线程,先返回成功响应,同时尝试启动这个可选服务 → 出来的是两个通知:optional_broken 从 starting 到 failed,失败通知里带有可读错误信息。

调用关系:它调用 create_config_toml_with_optional_broken_mcp 准备配置,再用 read_stream_until_matching_notification 等待特定通知,并把通知转换成 ServerNotification 做强类型比较。

调用图:调用 2 个内部函数(new, create_config_toml_with_optional_broken_mcp);外部调用 9 个(new, bail!, Integer, default, create_mock_responses_server_repeating_assistant, to_response, assert!, assert_eq!, timeout)。

thread_start_surfaces_cloud_config_bundle_load_errors884–963 ↗
async fn thread_start_surfaces_cloud_config_bundle_load_errors() -> Result<()>

作用:检查云端配置包加载失败时,thread/start 会把错误原因清楚地返回给客户端,尤其是登录令牌失效这种需要用户重新登录的情况。

数据流:进去的是一个 mock 云端服务器:配置包接口返回 401,刷新 token 接口也返回刷新令牌失效;同时写入一份过期的 ChatGPT 登录信息 → 服务器启动线程时尝试加载云端配置并刷新登录 → 出来的是错误响应,消息包含 failed to load configuration,错误数据里说明原因是 cloudConfigBundle、动作是 relogin、状态码是 401。

调用关系:它自己搭建 wiremock 规则,调用 create_config_toml_with_chatgpt_base_urlwrite_chatgpt_auth 准备登录状态,再用 TestAppServer::new_with_env 控制环境变量,让测试走指定的刷新 token 地址。

调用图:调用 3 个内部函数(new, new_with_env, create_config_toml_with_chatgpt_base_url);外部调用 15 个(given, start, new, new, Integer, default, create_mock_responses_server_repeating_assistant, write_chatgpt_auth, assert!, assert_eq! (+5 more))。

thread_start_with_elevated_sandbox_trusts_project_and_followup_loads_project_config966–1029 ↗
async fn thread_start_with_elevated_sandbox_trusts_project_and_followup_loads_project_config() -> Result<()>

作用:检查当用户用更高权限的沙箱启动项目时,服务器会把项目记为可信;之后再启动同一项目,就能加载该项目配置。沙箱是限制程序能访问或修改什么的安全边界。

数据流:进去的是一个有项目配置的工作目录,第一次请求设置 sandbox 为 workspace-write → 服务器成功启动并把项目信任写进用户配置;第二次不再显式提升权限启动同一目录 → 出来时响应使用项目配置里的 High 推理强度,配置文件里也出现 trusted 记录。

调用关系:它调用普通配置 helper,之后用 resolve_root_git_project_for_trustproject_trust_key 计算应该写入的项,验证信任持久化确实发生。

调用图:调用 3 个内部函数(new, create_config_toml_without_approval_policy, project_trust_key);外部调用 11 个(default, new, Integer, create_mock_responses_server_repeating_assistant, assert!, assert_eq!, resolve_root_git_project_for_trust, create_dir_all, read_to_string, write (+1 more))。

thread_start_with_nested_git_cwd_trusts_repo_root1032–1070 ↗
async fn thread_start_with_nested_git_cwd_trusts_repo_root() -> Result<()>

作用:检查如果当前目录在 Git 仓库深处,提升沙箱权限时信任的是仓库根目录,而不是那个子目录。Git 仓库根目录就是包含 .git 的项目顶层。

数据流:进去的是一个假 Git 仓库和其下的 nested/project 子目录,启动请求的 cwd 指向子目录并带 workspace-write 沙箱 → 服务器解析出仓库根目录并写入信任 → 出来时配置文件包含仓库根的信任 key,不包含子目录自己的 key。

调用关系:它使用普通配置 helper,并调用 resolve_root_git_project_for_trustproject_trust_key 来算出应该写入和不应该写入的配置键。

调用图:调用 3 个内部函数(new, create_config_toml_without_approval_policy, project_trust_key);外部调用 10 个(default, new, Integer, create_mock_responses_server_repeating_assistant, assert!, resolve_root_git_project_for_trust, create_dir, create_dir_all, read_to_string, timeout)。

thread_start_with_read_only_sandbox_does_not_persist_project_trust1073–1101 ↗
async fn thread_start_with_read_only_sandbox_does_not_persist_project_trust() -> Result<()>

作用:确认只读沙箱启动项目时,不会把项目写成可信。只读模式权限低,不应该被当成用户信任项目的信号。

数据流:进去的是一个普通工作目录和默认只读沙箱配置 → 服务器启动线程成功,但没有提升权限 → 出来时用户配置文件里没有 trust_level = "trusted",也没有该工作目录的信任记录。

调用关系:它使用普通配置 helper,发一个不带 workspace-write 的 thread/start,然后直接读取配置文件检查没有被改写。

调用图:调用 2 个内部函数(new, create_config_toml_without_approval_policy);外部调用 7 个(default, new, Integer, create_mock_responses_server_repeating_assistant, assert!, read_to_string, timeout)。

thread_start_preserves_untrusted_project_trust1104–1139 ↗
async fn thread_start_preserves_untrusted_project_trust() -> Result<()>

作用:检查如果项目已经被明确标为不可信,哪怕这次请求带更高沙箱权限,服务器也不会擅自改成可信。这保护了用户之前做出的安全选择。

数据流:进去的是一份手工写入 trust_level = untrusted 的用户配置,以及带 workspace-write 沙箱的启动请求 → 服务器启动线程时看到已有不可信记录 → 出来时配置文件内容和启动前完全一样。

调用关系:它先调用普通配置 helper,再用 toml_edit 修改配置文件,最后启动服务器并通过前后文件内容相等来验证没有覆盖用户决定。

调用图:调用 2 个内部函数(new, create_config_toml_without_approval_policy);外部调用 9 个(default, new, Integer, create_mock_responses_server_repeating_assistant, assert_eq!, read_to_string, write, timeout, value)。

thread_start_skips_trust_write_when_project_is_already_trusted1142–1188 ↗
async fn thread_start_skips_trust_write_when_project_is_already_trusted() -> Result<()>

作用:确认项目已经是可信时,再用高权限沙箱启动不会重复改写配置文件。这样可以减少不必要的磁盘写入,也避免配置格式被无故重排。

数据流:进去的是一个已标记 trusted、且有项目配置的工作目录 → 服务器启动线程并加载项目配置 → 出来时响应里的审批策略和推理强度正确,配置文件启动前后完全一致。

调用关系:它调用 set_project_trust_level 预先写入信任状态,再通过 thread/start 验证既能读项目配置,又不会重复写信任记录。

调用图:调用 3 个内部函数(new, create_config_toml_without_approval_policy, set_project_trust_level);外部调用 9 个(default, new, Integer, create_mock_responses_server_repeating_assistant, assert_eq!, create_dir_all, read_to_string, write, timeout)。

create_config_toml_without_approval_policy1190–1197 ↗
fn create_config_toml_without_approval_policy(
    codex_home: &Path,
    server_uri: &str,
) -> std::io::Result<()>

作用:写一份最常用的测试配置,但不显式设置审批策略。很多测试只关心线程启动本身,不想被审批策略干扰。

数据流:进去的是临时的 codex home 路径和 mock 模型服务器地址 → 它把审批策略参数设为空,转交给更通用的配置写入函数 → 出来是在临时目录里生成 config.toml

调用关系:这是本文件最常用的 helper,被大量 thread_start_* 测试调用;它本身只负责把请求转给 create_config_toml_with_optional_approval_policy

调用图:调用 1 个内部函数(create_config_toml_with_optional_approval_policy);被 17 处调用(thread_start_accepts_absolute_runtime_workspace_roots, thread_start_accepts_default_service_tier, thread_start_accepts_metrics_service_name, thread_start_creates_thread_and_emits_started, thread_start_drops_unsupported_service_tier_id, thread_start_ephemeral_remains_pathless, thread_start_preserves_untrusted_project_trust, thread_start_rejects_relative_environment_cwd_as_invalid_request, thread_start_rejects_unknown_environment_as_invalid_request, thread_start_respects_project_config_from_cwd (+7 more))。

create_config_toml_with_optional_approval_policy1199–1226 ↗
fn create_config_toml_with_optional_approval_policy(
    codex_home: &Path,
    server_uri: &str,
    approval_policy: Option<&str>,
) -> std::io::Result<()>

作用:按测试需要写一份基础 config.toml,可选地带审批策略。它把模型、沙箱、模型服务提供方这些启动线程必需的信息一次写好。

数据流:进去的是配置目录、mock 服务地址,以及可选审批策略字符串 → 它拼出 TOML 文本并写到 config.toml → 出来的是一个服务器能读取的测试配置文件。

调用关系:它被 create_config_toml_without_approval_policy 包装调用,是许多测试配置的底层写文件工具。

调用图:被 1 处调用(create_config_toml_without_approval_policy);外部调用 3 个(join, format!, write)。

create_config_toml_with_profile_workspace_root1228–1262 ↗
fn create_config_toml_with_profile_workspace_root(
    codex_home: &Path,
    server_uri: &str,
    profile_root: &Path,
) -> std::io::Result<()>

作用:写一份带 profile 工作区根目录的测试配置,用来验证这些预设目录不会混入运行时目录返回值。profile 可以理解成一套预设权限方案。

数据流:进去的是配置目录、mock 服务地址和一个 profile root 路径 → 它先把路径里可能影响 TOML 的字符转义,再写入包含权限配置的 config.toml → 出来的是带 permissions.dev.workspace_roots 的配置文件。

调用关系:它只被 thread_start_excludes_profile_workspace_roots_from_runtime_workspace_roots 使用,为那个测试制造特定场景。

调用图:被 1 处调用(thread_start_excludes_profile_workspace_roots_from_runtime_workspace_roots);外部调用 4 个(display, join, format!, write)。

create_config_toml_with_chatgpt_base_url1264–1290 ↗
fn create_config_toml_with_chatgpt_base_url(
    codex_home: &Path,
    server_uri: &str,
    chatgpt_base_url: &str,
) -> std::io::Result<()>

作用:写一份带 ChatGPT 后端地址的测试配置。它用于需要测试统计上报或云端配置加载的场景。

数据流:进去的是配置目录、mock 模型服务地址和 ChatGPT base URL → 它生成包含模型 provider、只读沙箱、永不审批、chatgpt_base_url 的 TOML → 出来的是对应的 config.toml 文件。

调用关系:它被统计上报测试和云端配置错误测试调用,让服务器把相关网络请求打到测试里的 mock server。

调用图:被 2 处调用(thread_start_surfaces_cloud_config_bundle_load_errors, thread_start_tracks_thread_initialized_analytics);外部调用 3 个(join, format!, write)。

create_config_toml_with_required_broken_mcp1292–1321 ↗
fn create_config_toml_with_required_broken_mcp(
    codex_home: &Path,
    server_uri: &str,
) -> std::io::Result<()>

作用:写一份包含“必需但会失败”的 MCP 服务配置。它用来测试必需工具服务坏掉时,线程启动必须失败。

数据流:进去的是配置目录和 mock 模型服务地址 → 它调用 broken_mcp_transport_toml 得到一个必然退出失败的命令,并写入 required = true 的 MCP 配置 → 出来的是一份会触发启动失败的 config.toml

调用关系:它只服务于 thread_start_fails_when_required_mcp_server_fails_to_initialize,负责把失败条件准备好。

调用图:被 1 处调用(thread_start_fails_when_required_mcp_server_fails_to_initialize);外部调用 3 个(join, format!, write)。

create_config_toml_with_optional_broken_mcp1323–1351 ↗
fn create_config_toml_with_optional_broken_mcp(
    codex_home: &Path,
    server_uri: &str,
) -> std::io::Result<()>

作用:写一份包含“可选但会失败”的 MCP 服务配置。它用来测试可选工具服务失败时,主线程能继续,但状态通知要正确。

数据流:进去的是配置目录和 mock 模型服务地址 → 它把一个必然失败的 MCP 启动命令写进配置,但不标为 required → 出来的是一份会触发 MCP 状态通知的 config.toml

调用关系:它被 thread_start_emits_mcp_server_status_updated_notifications 调用,并依赖 broken_mcp_transport_toml 生成跨平台失败命令。

调用图:被 1 处调用(thread_start_emits_mcp_server_status_updated_notifications);外部调用 3 个(join, format!, write)。

broken_mcp_transport_toml1360–1363 ↗
fn broken_mcp_transport_toml() -> &'static str

作用:返回一小段 TOML,表示一个启动后立刻失败的命令。它让测试可以稳定模拟“外部 MCP 服务启动坏了”。

数据流:进去没有参数 → 根据操作系统选择失败命令:Windows 用 cmd /C exit 1,类 Unix 系统用 /bin/sh -c exit 1 → 出来的是可嵌入配置文件的字符串。

调用关系:它被创建坏 MCP 配置的 helper 使用,是必需 MCP 失败测试和可选 MCP 状态通知测试的共同零件。

app-server/tests/suite/v2/thread_list.rs源码 ↗
testtest run

项目里会把一次次对话保存成线程记录,用户界面需要能把这些线程列出来、搜索出来、按时间排序、按模型提供方或来源筛选,还要能区分普通线程、子代理线程、归档线程等。这个测试文件就模拟各种真实情况:先在临时目录里造出假的对话记录,再启动一个测试版 app server,通过 JSON-RPC(一种用 JSON 发请求和收回应的通信格式)发起 thread/list 或 thread/search 请求,最后检查返回内容是否正确。它还覆盖一些容易出错的边角:页码游标是不是正确、最大分页数量有没有限制、更新时间是否取文件修改时间、搜索能不能匹配带引号和反斜杠的内容、状态数据库 SQLite(一种本地小数据库)和 jsonl 文件之间如何配合。没有这些测试,列表页很容易出现少数据、乱排序、筛选失效或错误游标不报错这类问题。

函数细节38
init_mcp52–56 ↗
async fn init_mcp(codex_home: &Path) -> Result<TestAppServer>

作用:启动一个测试用的 app server,并完成初始化握手。测试在真正发请求前都会先用它把服务准备好。

数据流:输入一个临时的 codex_home 路径 → 创建 TestAppServer,并等待 initialize 在限定时间内完成 → 返回可以收发测试请求的服务器对象;如果启动或初始化超时,就返回错误。

调用关系:它是大多数测试的开场步骤。各个 thread_list 和 thread_search 测试先用它拿到 mcp,再把查询、搜索或启动线程的请求交给这个 mcp 发送。

调用图:调用 1 个内部函数(new);被 28 处调用(thread_list_archived_filter, thread_list_backwards_cursor_can_seed_forward_delta_sync, thread_list_basic_empty, thread_list_created_at_tie_breaks_by_uuid, thread_list_default_sorts_by_created_at, thread_list_empty_source_kinds_defaults_to_interactive_only, thread_list_enforces_max_limit, thread_list_fetches_until_limit_or_exhausted, thread_list_filters_by_source_kind_subagent_thread_spawn, thread_list_filters_by_subagent_variant (+15 more));外部调用 1 个(timeout)。

list_threads58–76 ↗
async fn list_threads(
    mcp: &mut TestAppServer,
    cursor: Option<String>,
    limit: Option<u32>,
    providers: Option<Vec<String>>,
    source_kinds: Option<Vec<ThreadSourceKind>>,
    archive

作用:用比较省事的方式发起“列出线程”请求。测试不关心排序方式时,就用它少写一堆重复参数。

数据流:输入服务器对象、游标、数量限制、模型提供方、来源类型、是否归档 → 补上默认排序等参数 → 调用 list_threads_with_sort,最后得到 ThreadListResponse。

调用关系:它是 list_threads_with_sort 的简化包装。大量测试通过它检查普通列表、筛选、分页、归档等场景。

调用图:调用 1 个内部函数(list_threads_with_sort);被 13 处调用(thread_list_archived_filter, thread_list_basic_empty, thread_list_created_at_tie_breaks_by_uuid, thread_list_empty_source_kinds_defaults_to_interactive_only, thread_list_enforces_max_limit, thread_list_fetches_until_limit_or_exhausted, thread_list_filters_by_source_kind_subagent_thread_spawn, thread_list_filters_by_subagent_variant, thread_list_includes_git_info, thread_list_pagination_next_cursor_none_on_last_page (+3 more))。

list_threads_with_sort78–108 ↗
async fn list_threads_with_sort(
    mcp: &mut TestAppServer,
    cursor: Option<String>,
    limit: Option<u32>,
    providers: Option<Vec<String>>,
    source_kinds: Option<Vec<ThreadSourceKind>>,

作用:发起完整的“列出线程”请求,并允许指定排序字段。需要测试 created_at 和 updated_at 排序时会用它。

数据流:输入服务器对象和各种查询条件 → 组装 ThreadListParams,通过测试服务器发送 JSON-RPC 请求 → 等待对应请求编号的回应 → 把 JSON-RPC 回应转成 ThreadListResponse 返回。

调用关系:list_threads 会把默认排序的工作交给它;排序相关测试直接调用它。它位于测试和 app server 接口之间,负责把测试意图变成真实请求。

调用图:调用 2 个内部函数(read_stream_until_response_message, send_thread_list_request);被 6 处调用(list_threads, thread_list_default_sorts_by_created_at, thread_list_sort_updated_at_orders_by_mtime, thread_list_updated_at_paginates_with_cursor, thread_list_updated_at_tie_breaks_by_uuid, thread_list_updated_at_uses_mtime);外部调用 2 个(Integer, timeout)。

list_threads_for_parent110–139 ↗
async fn list_threads_for_parent(
    mcp: &mut TestAppServer,
    parent_thread_id: ThreadId,
    cursor: Option<String>,
    limit: u32,
    model_providers: Option<Vec<String>>,
    source_kinds: O

作用:专门列出某个父线程下面的子线程。它用来验证“只看这个父线程直接派生出来的线程”这个功能。

数据流:输入服务器对象、父线程 ID、游标、数量限制、模型提供方和来源类型 → 把父线程 ID 转成字符串放进请求 → 等待 thread/list 回应 → 返回 ThreadListResponse。

调用关系:它只被父子线程过滤测试使用。测试先往状态数据库里写父子关系,再用这个 helper 请求 app server,确认 app server 只返回直接子线程。

调用图:调用 2 个内部函数(read_stream_until_response_message, send_thread_list_request);被 1 处调用(thread_list_parent_filter_reads_direct_children_from_state_db);外部调用 3 个(Integer, to_string, timeout)。

create_fake_rollouts141–165 ↗
fn create_fake_rollouts(
    codex_home: &Path,
    count: usize,
    provider_for_index: F,
    timestamp_for_index: G,
    preview: &str,
) -> Result<Vec<String>>

作用:一次性造出多条假的对话记录,方便测试大分页和过滤。它避免每个测试手写几十上百次创建代码。

数据流:输入保存目录、要创建的数量、按序号决定模型提供方的函数、按序号决定时间的函数、预览文字 → 循环调用 create_fake_rollout → 返回所有新线程 ID。

调用关系:分页压力测试和过滤耗尽测试会用它批量准备数据。它把真正写假 rollout 文件的工作交给测试支持库。

调用图:被 3 处调用(thread_list_enforces_max_limit, thread_list_fetches_until_limit_or_exhausted, thread_list_stops_when_not_enough_filtered_results_exist);外部调用 2 个(with_capacity, create_fake_rollout)。

timestamp_at167–179 ↗
fn timestamp_at(
    year: i32,
    month: u32,
    day: u32,
    hour: u32,
    minute: u32,
    second: u32,
) -> (String, String)

作用:把年月日时分秒拼成测试需要的两种时间字符串。这样测试造文件名和写元数据时不会格式写错。

数据流:输入年、月、日、时、分、秒 → 格式化成文件名用的时间和 RFC3339 标准时间 → 返回这两个字符串。

调用关系:它主要配合 create_fake_rollouts 使用,让批量造数据时每条记录都有可控的时间顺序。

调用图:外部调用 1 个(format!)。

set_rollout_mtime182–190 ↗
fn set_rollout_mtime(path: &Path, updated_at_rfc3339: &str) -> Result<()>

作用:手动修改一个 rollout 文件的“最后修改时间”。测试更新时间排序时,需要把创建时间和更新时间故意设成不一样。

数据流:输入 rollout 文件路径和一个标准时间字符串 → 解析时间 → 打开文件并设置文件修改时间 → 文件本身内容不变,但系统看到的更新时间变了。

调用关系:updated_at 相关测试依赖它制造不同的文件修改时间。随后 app server 列表接口读取这些时间,测试再检查排序和返回字段。

调用图:被 5 处调用(thread_list_backwards_cursor_can_seed_forward_delta_sync, thread_list_sort_updated_at_orders_by_mtime, thread_list_updated_at_paginates_with_cursor, thread_list_updated_at_tie_breaks_by_uuid, thread_list_updated_at_uses_mtime);外部调用 3 个(parse_from_rfc3339, new, new)。

set_rollout_cwd192–210 ↗
fn set_rollout_cwd(path: &Path, cwd: &Path) -> Result<()>

作用:改掉一条假对话记录里的工作目录 cwd。cwd 可以理解为“这次对话发生在哪个项目文件夹里”。

数据流:输入 rollout 文件路径和新的目录路径 → 读取 jsonl 文件第一行的会话元数据 → 把里面的 cwd 改成新路径 → 再把文件写回去。

调用关系:cwd 筛选测试用它把不同线程放到不同目录。之后 thread/list 按目录筛选时,测试确认只有目标目录里的线程被返回。

调用图:被 1 处调用(thread_list_respects_cwd_filters);外部调用 7 个(to_path_buf, anyhow!, read_to_string, write, SessionMeta, from_str, to_string)。

thread_list_basic_empty213–234 ↗
async fn thread_list_basic_empty() -> Result<()>

作用:验证没有任何对话记录时,线程列表应该返回空数组。这个最基础的场景能防止接口凭空返回脏数据。

数据流:创建临时目录和最小配置 → 启动测试服务器 → 请求列出 mock_provider 的线程 → 检查 data 为空、next_cursor 为空。

调用关系:它使用 create_minimal_config、init_mcp 和 list_threads 串起完整请求流程,是 thread/list 最简单的端到端测试。

调用图:调用 3 个内部函数(create_minimal_config, init_mcp, list_threads);外部调用 4 个(new, assert!, assert_eq!, vec!)。

thread_list_reports_system_error_idle_flag_after_failed_turn237–327 ↗
async fn thread_list_reports_system_error_idle_flag_after_failed_turn() -> Result<()>

作用:验证一次对话轮次失败后,线程列表会显示 SystemError 状态。这样前端列表能知道这个线程不是正常空闲,而是出过系统错误。

数据流:准备一个先成功再失败的模拟模型服务 → 启动服务器 → 新建线程并跑一轮成功输入,再跑一轮会失败的输入 → 调 thread/list 找到该线程 → 检查状态是 SystemError。

调用关系:它先用 create_runtime_config 连接模拟响应服务器,再通过 thread/start、turn/start 制造失败,最后用 list_threads 检查列表接口是否反映运行状态。

调用图:调用 3 个内部函数(create_runtime_config, init_mcp, list_threads);外部调用 7 个(default, new, Integer, create_mock_responses_server_sequence, assert_eq!, timeout, vec!)。

create_minimal_config330–339 ↗
fn create_minimal_config(codex_home: &std::path::Path) -> std::io::Result<()>

作用:写一个最小可用的 config.toml 配置文件。很多测试只需要服务能启动,不需要真实模型配置。

数据流:输入 codex_home 路径 → 在里面创建 config.toml → 写入模型名和永不请求审批的策略 → 返回文件写入结果。

调用关系:它是大多数测试的准备步骤。init_mcp 启动服务器时会读取这个配置。

调用图:被 25 处调用(thread_list_archived_filter, thread_list_backwards_cursor_can_seed_forward_delta_sync, thread_list_basic_empty, thread_list_created_at_tie_breaks_by_uuid, thread_list_default_sorts_by_created_at, thread_list_empty_source_kinds_defaults_to_interactive_only, thread_list_enforces_max_limit, thread_list_fetches_until_limit_or_exhausted, thread_list_filters_by_source_kind_subagent_thread_spawn, thread_list_filters_by_subagent_variant (+15 more));外部调用 2 个(join, write)。

create_runtime_config341–362 ↗
fn create_runtime_config(codex_home: &std::path::Path, server_uri: &str) -> std::io::Result<()>

作用:写一个连接模拟模型服务的运行配置。需要真实走一遍 turn/start 的测试会用它。

数据流:输入 codex_home 和模拟服务器地址 → 写 config.toml,配置 mock_provider、base_url、API 类型和重试次数 → 返回写入结果。

调用关系:它只服务于失败轮次状态测试。该测试需要 app server 真的去请求一个可控的假模型服务。

调用图:被 1 处调用(thread_list_reports_system_error_idle_flag_after_failed_turn);外部调用 3 个(join, format!, write)。

thread_list_pagination_next_cursor_none_on_last_page365–454 ↗
async fn thread_list_pagination_next_cursor_none_on_last_page() -> Result<()>

作用:验证分页到最后一页时 next_cursor 会变成空。这样客户端知道已经没有下一页了。

数据流:创建三条线程 → 第一页按 limit=2 请求,拿到两个结果和一个游标 → 第二页带游标再请求 → 检查最后一页没有 next_cursor,并确认每条线程的基本字段正确。

调用关系:它用 create_fake_rollout 造数据,用 list_threads 走真实列表接口,重点检查分页游标的结束信号。

调用图:调用 3 个内部函数(create_minimal_config, init_mcp, list_threads);外部调用 5 个(new, create_fake_rollout, assert!, assert_eq!, vec!)。

thread_list_respects_provider_filter457–507 ↗
async fn thread_list_respects_provider_filter() -> Result<()>

作用:验证按模型提供方过滤是有效的。用户只想看某个 provider 的会话时,列表不能混进别家的记录。

数据流:创建两条不同 model_provider 的线程 → 请求只看 other_provider → 检查只返回一条,并且时间、cwd、来源等字段符合假数据。

调用关系:它通过 list_threads 发送带 provider 条件的请求,检验 app server 在扫描 rollout 后是否正确丢掉不匹配项。

调用图:调用 3 个内部函数(create_minimal_config, init_mcp, list_threads);外部调用 5 个(new, create_fake_rollout, assert_eq!, parse_from_rfc3339, vec!)。

thread_list_respects_cwd_filters510–596 ↗
async fn thread_list_respects_cwd_filters() -> Result<()>

作用:验证按工作目录 cwd 过滤可以一次匹配多个目录。项目目录筛选错了,用户会在一个项目里看到另一个项目的历史。

数据流:创建三条线程 → 把其中两条的 cwd 改成两个目标目录 → 发 thread/list 请求并传入 Many 目录过滤 → 检查只返回这两条,且顺序和目录字段正确。

调用关系:它直接构造 ThreadListParams,因为普通 helper 没带 cwd 参数;同时使用 set_rollout_cwd 改造测试数据。

调用图:调用 3 个内部函数(create_minimal_config, init_mcp, set_rollout_cwd);外部调用 10 个(new, Integer, Many, create_fake_rollout, rollout_path, assert!, assert_eq!, create_dir_all, timeout, vec!)。

thread_list_respects_search_term_filter599–700 ↗
async fn thread_list_respects_search_term_filter() -> Result<()>

作用:验证 thread/list 的 search_term 只返回内容里包含关键词的线程。它还特别覆盖 SQLite 快速路径下的搜索。

数据流:开启 sqlite 功能并创建三条线程,其中两条包含 needle → 初始化状态数据库并标记回填完成 → 先跑一次普通列表把 jsonl 修复进数据库 → 再发带 search_term 的列表请求 → 检查只返回两个匹配线程且新的在前。

调用关系:它同时碰到状态数据库、RolloutRecorder 修复和 app server 列表接口,是搜索过滤在数据库路径上的端到端验证。

调用图:调用 3 个内部函数(init_mcp, list_threads, init);外部调用 7 个(new, Integer, create_fake_rollout, assert_eq!, write, timeout, vec!)。

thread_search_returns_content_matches703–762 ↗
async fn thread_search_returns_content_matches() -> Result<()>

作用:验证专门的 thread/search 接口能找到正文内容匹配的线程,并返回命中的片段 snippet。

数据流:创建三条线程,两条含有 needle,其中一条用大写 NEEDLE → 发 thread/search 请求 → 检查大小写不敏感地返回两条,并确认 snippet 是匹配内容。

调用关系:它不走 list_threads helper,而是直接调用 send_thread_search_request,专门测试搜索接口返回结构。

调用图:调用 2 个内部函数(create_minimal_config, init_mcp);外部调用 5 个(new, Integer, create_fake_rollout, assert_eq!, timeout)。

thread_search_matches_json_escaped_content765–803 ↗
async fn thread_search_matches_json_escaped_content() -> Result<()>

作用:验证搜索能匹配带引号和反斜杠的内容。因为这些字符在 JSON 文件里会被转义,处理不好就搜不到。

数据流:创建一条内容为 quoted "needle" \ path 的线程 → 用同样的字符串发 thread/search → 检查返回一条,并且 snippet 保持原文。

调用关系:它直接测试搜索接口对 jsonl 转义内容的处理,防止搜索只匹配文件里的转义形式而不是用户看到的文本。

调用图:调用 2 个内部函数(create_minimal_config, init_mcp);外部调用 5 个(new, Integer, create_fake_rollout, assert_eq!, timeout)。

thread_search_filters_by_source_kind806–855 ↗
async fn thread_search_filters_by_source_kind() -> Result<()>

作用:验证搜索结果也能按线程来源类型过滤。比如只看 Exec 来源时,普通 CLI 线程不该出现。

数据流:创建一条 CLI 线程和一条 Exec 线程,两条都包含 needle → 发 thread/search 并指定 source_kinds 为 Exec → 检查只返回 Exec 那条。

调用关系:它结合 create_fake_rollout 和 create_fake_rollout_with_source 制造不同来源,再检查搜索接口的过滤逻辑。

调用图:调用 2 个内部函数(create_minimal_config, init_mcp);外部调用 8 个(new, Integer, create_fake_rollout, create_fake_rollout_with_source, assert_eq!, assert_ne!, timeout, vec!)。

thread_list_state_db_only_returns_sqlite_without_jsonl_repair858–981 ↗
async fn thread_list_state_db_only_returns_sqlite_without_jsonl_repair() -> Result<()>

作用:验证 use_state_db_only=true 时,列表只相信 SQLite 里的数据,不再回头扫描和修复 jsonl 文件。这个开关对快速读取和避免副作用很重要。

数据流:开启 sqlite,创建一条 jsonl 线程 → 普通列表先把它修复进数据库 → 手动把数据库里的 cwd 改成一个过期目录 → 用 state_db_only 加 cwd 过滤请求,能按数据库旧值找到它 → 再用普通扫描请求,同样 cwd 找不到它。

调用关系:它连接 app server 请求和 codex_state 数据库读写,专门区分“只读数据库”和“扫描 jsonl 并修复”的两条路径。

调用图:调用 3 个内部函数(init_mcp, from_string, init);外部调用 8 个(new, Integer, One, create_fake_rollout, assert_eq!, write, timeout, vec!)。

thread_list_parent_filter_reads_direct_children_from_state_db984–1107 ↗
async fn thread_list_parent_filter_reads_direct_children_from_state_db() -> Result<()>

作用:验证按 parent_thread_id 查询时,只返回父线程的直接子线程,而不是孙子线程。它还检查分页和来源过滤一起工作。

数据流:在状态数据库里写入父线程、两个子线程、一个孙子线程和它们的边关系 → 标记回填完成 → 分两页请求父线程的子线程 → 检查顺序、parent_thread_id 字段和空 source_kinds 的行为。

调用关系:它使用 list_threads_for_parent 调 app server;测试数据不靠 jsonl,而是直接写 codex_state,说明这个功能从状态数据库读取父子关系。

调用图:调用 6 个内部函数(create_minimal_config, init_mcp, list_threads_for_parent, new, new, init);外部调用 8 个(SubAgent, parse_from_rfc3339, new, new, assert!, assert_eq!, format!, Other)。

thread_list_parent_filter_rejects_malformed_thread_id1110–1137 ↗
async fn thread_list_parent_filter_rejects_malformed_thread_id() -> Result<()>

作用:验证 parent_thread_id 不是合法线程 ID 时,服务器会返回请求错误。错误输入不能被悄悄当成空结果。

数据流:启动空服务器 → 发 thread/list,并把 parent_thread_id 设置成 not-a-thread-id → 等待错误回应 → 检查错误码是 -32600。

调用关系:它直接发请求而不是用 helper,因为要构造非法参数并读取错误消息。

调用图:调用 2 个内部函数(create_minimal_config, init_mcp);外部调用 4 个(new, Integer, assert_eq!, timeout)。

thread_list_empty_source_kinds_defaults_to_interactive_only1140–1183 ↗
async fn thread_list_empty_source_kinds_defaults_to_interactive_only() -> Result<()>

作用:验证 source_kinds 传空数组时,会默认只返回交互式线程。这里的交互式主要指 CLI 这类用户直接开的会话。

数据流:创建一条 CLI 线程和一条 Exec 线程 → 发列表请求,source_kinds 是空数组 → 检查只返回 CLI 线程。

调用关系:它通过 list_threads 走普通列表接口,覆盖一个容易误解的参数语义:空数组不是“不筛选”,而是“只要交互式”。

调用图:调用 3 个内部函数(create_minimal_config, init_mcp, list_threads);外部调用 7 个(new, new, create_fake_rollout, create_fake_rollout_with_source, assert_eq!, assert_ne!, vec!)。

thread_list_filters_by_source_kind_subagent_thread_spawn1186–1238 ↗
async fn thread_list_filters_by_source_kind_subagent_thread_spawn() -> Result<()>

作用:验证可以只列出由 ThreadSpawn 子代理产生的线程。子代理可以理解为主线程派出去干活的小助手。

数据流:创建一条普通 CLI 线程,再创建一条来源为 SubAgent::ThreadSpawn 的线程 → 请求 source_kinds 为 SubAgentThreadSpawn → 检查只返回子代理线程,并且来源和 session_id 正确。

调用关系:它用 create_fake_rollout_with_source 制造特殊来源,再通过 list_threads 检查 app server 的来源分类映射。

调用图:调用 4 个内部函数(create_minimal_config, init_mcp, list_threads, from_string);外部调用 9 个(SubAgent, new, new_v4, create_fake_rollout, create_fake_rollout_with_source, assert!, assert_eq!, assert_ne!, vec!)。

thread_list_filters_by_subagent_variant1241–1354 ↗
async fn thread_list_filters_by_subagent_variant() -> Result<()>

作用:验证不同子代理类型可以分别筛选:Review、Compact、ThreadSpawn、Other 不会混在一起。

数据流:创建四条不同 SubAgent 变体的线程 → 分别用四种 ThreadSourceKind 请求列表 → 每次检查只返回对应那一条;Review 还检查父线程 ID 被带回来。

调用关系:它集中覆盖子代理来源的细分过滤。list_threads 是请求入口,测试支持函数负责造出不同来源的假 rollout。

调用图:调用 4 个内部函数(create_minimal_config, init_mcp, list_threads, from_string);外部调用 8 个(SubAgent, new, new_v4, create_fake_parented_rollout_with_source, create_fake_rollout_with_source, assert_eq!, Other, vec!)。

thread_list_fetches_until_limit_or_exhausted1357–1418 ↗
async fn thread_list_fetches_until_limit_or_exhausted() -> Result<()>

作用:验证服务器为了满足 limit,会继续翻内部页面直到凑够过滤后的结果。否则前几页都是不匹配数据时,用户会误以为没有更多结果。

数据流:创建 24 条线程,最新的 16 条属于跳过的 provider,较旧 8 条属于目标 provider → 请求 8 条目标 provider → 检查最终返回 8 条且没有 next_cursor。

调用关系:它用 create_fake_rollouts 批量造数据,再用 list_threads 检查 app server 在过滤后继续取下一批的能力。

调用图:调用 4 个内部函数(create_fake_rollouts, create_minimal_config, init_mcp, list_threads);外部调用 4 个(new, assert!, assert_eq!, vec!)。

thread_list_enforces_max_limit1421–1468 ↗
async fn thread_list_enforces_max_limit() -> Result<()>

作用:验证客户端请求的 limit 太大时,服务器会把它限制在最大页大小。这样一次请求不会返回过多数据拖垮接口。

数据流:创建 105 条线程 → 请求 limit=200 → 检查实际只返回 100 条,并且 next_cursor 存在表示还有更多。

调用关系:它用批量造数 helper 制造超过上限的数据量,检查 thread/list 的保护规则。

调用图:调用 4 个内部函数(create_fake_rollouts, create_minimal_config, init_mcp, list_threads);外部调用 4 个(new, assert!, assert_eq!, vec!)。

thread_list_stops_when_not_enough_filtered_results_exist1471–1531 ↗
async fn thread_list_stops_when_not_enough_filtered_results_exist() -> Result<()>

作用:验证符合过滤条件的结果不够 limit 时,服务器会正常停止,不会一直翻页或死循环。

数据流:创建 22 条线程,只有最后 7 条匹配目标 provider → 请求 10 条目标 provider → 检查返回 7 条、都匹配、next_cursor 为空。

调用关系:它和“凑够 limit”测试互补:一个看能不能继续找,一个看找不到更多时能不能停。

调用图:调用 4 个内部函数(create_fake_rollouts, create_minimal_config, init_mcp, list_threads);外部调用 4 个(new, assert!, assert_eq!, vec!)。

thread_list_includes_git_info1534–1579 ↗
async fn thread_list_includes_git_info() -> Result<()>

作用:验证列表返回会包含 Git 信息。Git 信息就是代码仓库的提交号、分支、远程地址,方便用户知道这次对话对应哪份代码。

数据流:创建一条带 commit、branch、repository_url 的线程 → 请求列表 → 找到该线程 → 检查返回的 git_info 字段被转换成 API 使用的 sha、branch、origin_url。

调用关系:它用 create_fake_rollout 写入核心协议里的 GitInfo,再通过 list_threads 检查 app server 对外协议里的字段映射。

调用图:调用 4 个内部函数(create_minimal_config, init_mcp, list_threads, new);外部调用 4 个(new, create_fake_rollout, assert_eq!, vec!)。

thread_list_default_sorts_by_created_at1582–1628 ↗
async fn thread_list_default_sorts_by_created_at() -> Result<()>

作用:验证默认列表按创建时间从新到旧排序。没有明确排序参数时,用户通常期待最新会话排最前。

数据流:创建三条不同创建时间的线程 → 不传 sort_key 请求列表 → 收集返回 ID → 检查顺序是最新、次新、最旧。

调用关系:它直接调用 list_threads_with_sort 并传 sort_key=None,专门确认默认排序规则。

调用图:调用 3 个内部函数(create_minimal_config, init_mcp, list_threads_with_sort);外部调用 4 个(new, create_fake_rollout, assert_eq!, vec!)。

thread_list_sort_updated_at_orders_by_mtime1631–1690 ↗
async fn thread_list_sort_updated_at_orders_by_mtime() -> Result<()>

作用:验证按 updated_at 排序时使用文件最后修改时间,而不是创建时间。这样最近被继续对话的线程会排前面。

数据流:创建三条线程 → 把它们的文件修改时间设成和创建时间相反的顺序 → 请求按 UpdatedAt 排序 → 检查返回顺序跟修改时间一致。

调用关系:它依赖 set_rollout_mtime 制造更新时间,再用 list_threads_with_sort 验证排序字段生效。

调用图:调用 4 个内部函数(create_minimal_config, init_mcp, list_threads_with_sort, set_rollout_mtime);外部调用 5 个(new, create_fake_rollout, rollout_path, assert_eq!, vec!)。

thread_list_updated_at_paginates_with_cursor1693–1774 ↗
async fn thread_list_updated_at_paginates_with_cursor() -> Result<()>

作用:验证按 updated_at 排序时,分页游标也能正确工作。排序方式变了,翻页边界也必须跟着变。

数据流:创建三条线程并设置不同文件修改时间 → 第一页请求 UpdatedAt、limit=2 → 拿到前两条和游标 → 第二页带游标请求 → 检查只返回最后一条且游标结束。

调用关系:它把 updated_at 排序和分页组合起来测试,防止单独排序正确但翻页重复或漏项。

调用图:调用 4 个内部函数(create_minimal_config, init_mcp, list_threads_with_sort, set_rollout_mtime);外部调用 5 个(new, create_fake_rollout, rollout_path, assert_eq!, vec!)。

thread_list_backwards_cursor_can_seed_forward_delta_sync1777–1883 ↗
async fn thread_list_backwards_cursor_can_seed_forward_delta_sync() -> Result<()>

作用:验证倒向游标 backwards_cursor 可以用来做后续增量同步。简单说,客户端先拿一页旧数据,再用这个时间点往前查新变化。

数据流:创建两条线程并设置更新时间 → 按 UpdatedAt 倒序拿最新一条,同时得到 backwards_cursor → 再创建一条更新的线程 → 用 backwards_cursor 按升序请求 → 检查返回水位线那条和新线程。

调用关系:它直接构造带 sort_direction 的 thread/list 请求,因为普通 helper 不覆盖这个参数;重点验证列表接口给同步客户端的游标语义。

调用图:调用 3 个内部函数(create_minimal_config, init_mcp, set_rollout_mtime);外部调用 7 个(new, Integer, create_fake_rollout, rollout_path, assert_eq!, timeout, vec!)。

thread_list_created_at_tie_breaks_by_uuid1886–1926 ↗
async fn thread_list_created_at_tie_breaks_by_uuid() -> Result<()>

作用:验证创建时间完全相同的线程,会用 UUID 再排序来打破平局。这样结果顺序稳定,不会每次刷新都变。

数据流:创建两条同一创建时间的线程 → 请求默认列表 → 按 UUID 倒序算出期望顺序 → 检查返回顺序一致。

调用关系:它用 list_threads 检查默认 created_at 排序的平局规则,防止相同时间导致分页不稳定。

调用图:调用 3 个内部函数(create_minimal_config, init_mcp, list_threads);外部调用 4 个(new, create_fake_rollout, assert_eq!, vec!)。

thread_list_updated_at_tie_breaks_by_uuid1929–1980 ↗
async fn thread_list_updated_at_tie_breaks_by_uuid() -> Result<()>

作用:验证更新时间相同的线程,也会用 UUID 稳定排序。更新时间排序同样需要可靠的平局规则。

数据流:创建两条线程 → 把它们的文件修改时间设成同一个时间 → 请求 UpdatedAt 排序 → 按 UUID 倒序算期望 → 检查返回顺序一致。

调用关系:它把 set_rollout_mtime 和 list_threads_with_sort 组合起来,测试 updated_at 排序的平局处理。

调用图:调用 4 个内部函数(create_minimal_config, init_mcp, list_threads_with_sort, set_rollout_mtime);外部调用 5 个(new, create_fake_rollout, rollout_path, assert_eq!, vec!)。

thread_list_updated_at_uses_mtime1983–2026 ↗
async fn thread_list_updated_at_uses_mtime() -> Result<()>

作用:验证返回字段里的 updated_at 确实来自文件修改时间。它不只是影响排序,也要显示给客户端。

数据流:创建一条线程并把文件修改时间改到几天后 → 按 UpdatedAt 请求列表 → 找到该线程 → 检查 created_at 是原创建时间,updated_at 是修改时间。

调用关系:它依赖 set_rollout_mtime 改文件属性,再通过 list_threads_with_sort 验证 app server 返回的时间字段。

调用图:调用 4 个内部函数(create_minimal_config, init_mcp, list_threads_with_sort, set_rollout_mtime);外部调用 6 个(new, create_fake_rollout, rollout_path, assert_eq!, parse_from_rfc3339, vec!)。

thread_list_archived_filter2029–2087 ↗
async fn thread_list_archived_filter() -> Result<()>

作用:验证归档过滤能区分普通线程和已归档线程。用户看普通列表时不该混入归档内容,专门看归档时也不能漏掉。

数据流:创建一条 active 线程和一条 archived 线程 → 把 archived 的 rollout 文件移动到归档目录 → 不传 archived 请求列表,检查只看到 active → archived=true 再请求,检查只看到 archived。

调用关系:它用文件移动模拟归档状态,再通过 list_threads 检查 app server 对归档目录和普通目录的扫描是否分开。

调用图:调用 3 个内部函数(create_minimal_config, init_mcp, list_threads);外部调用 7 个(new, create_fake_rollout, rollout_path, assert_eq!, create_dir_all, rename, vec!)。

thread_list_invalid_cursor_returns_error2090–2120 ↗
async fn thread_list_invalid_cursor_returns_error() -> Result<()>

作用:验证传入非法分页游标时,服务器会明确报错。坏游标如果被忽略,客户端会拿到误导性的第一页数据。

数据流:启动测试服务器 → 发 thread/list 请求,cursor 设置为 not-a-cursor → 等待错误回应 → 检查错误码和错误信息都是预期值。

调用关系:它直接读取 JSON-RPC 错误消息,专门覆盖请求参数校验,而不是成功返回路径。

调用图:调用 2 个内部函数(create_minimal_config, init_mcp);外部调用 5 个(new, Integer, assert_eq!, timeout, vec!)。

app-server/tests/suite/v2/thread_loaded_list.rs源码 ↗
testtest run

这个测试文件像是在给应用服务器做一次小体检。它先搭一个假的模型服务,让测试不用真的连外部 AI;再临时写一份配置文件,告诉应用服务器去找这个假服务。随后测试会启动一个或两个线程,也就是一段对话的工作空间,然后调用 thread_loaded_list 接口查看服务器现在“手里拿着”的线程有哪些。第一个测试检查:启动一个线程后,列表里必须正好出现这个线程。第二个测试检查分页:如果限制每页只返回 1 个线程,服务器应该先返回第一个线程和一个“下一页从这里继续”的标记,再用这个标记拿到第二个线程。这里还用了超时保护,防止服务器没回应时测试一直卡住。

函数细节4
thread_loaded_list_returns_loaded_thread_ids19–46 ↗
async fn thread_loaded_list_returns_loaded_thread_ids() -> Result<()>

作用:这个测试确认:只要启动了一个线程,调用“已加载线程列表”接口时,就能看到这个线程的 ID。它用来防止服务器忘记记录已经打开的线程。

数据流:进去的是一个临时目录和一个假的模型服务地址;测试把它们写成配置,启动测试服务器,再通过 start_thread 创建一个线程。之后它发送 thread_loaded_list 请求,读取服务器返回的 JSON-RPC 响应(JSON-RPC 是一种用 JSON 格式发请求和收回答的通信方式),把结果转成 ThreadLoadedListResponse,最后检查返回的数据里只有刚创建的那个线程 ID,并且没有下一页标记。

调用关系:它是这个文件的主测试之一。运行时会先调用 create_config_toml 准备配置,再调用 start_thread 创建线程,然后借助 TestAppServer 的发送和读取方法向服务器发请求、等回应,最后用 assert_eq! 做结果核对。

调用图:调用 3 个内部函数(new, create_config_toml, start_thread);外部调用 6 个(new, Integer, default, create_mock_responses_server_repeating_assistant, assert_eq!, timeout)。

thread_loaded_list_paginates49–100 ↗
async fn thread_loaded_list_paginates() -> Result<()>

作用:这个测试确认“已加载线程列表”支持分页。也就是说,当客户端要求每次只拿一条时,服务器会分两次把两个线程交出来,并正确告诉客户端下一页从哪里开始。

数据流:进去的是测试创建出的假服务和临时配置;测试启动服务器后连续创建两个线程,把两个线程 ID 排序成预期顺序。第一次请求设置 limit 为 1,服务器应返回第一页的一个 ID 和 next_cursor;第二次请求把这个 next_cursor 当作 cursor 传回去,服务器应返回第二个 ID,并且 next_cursor 变成 None,表示没有更多了。

调用关系:它是分页行为的主测试。它同样依赖 create_config_toml 搭好配置,依赖 start_thread 制造两个可列出的线程,然后两次调用 TestAppServer 的列表请求和响应读取能力,检查服务器的分页规则是否稳定。

调用图:调用 3 个内部函数(new, create_config_toml, start_thread);外部调用 5 个(new, Integer, create_mock_responses_server_repeating_assistant, assert_eq!, timeout)。

create_config_toml102–123 ↗
fn create_config_toml(codex_home: &Path, server_uri: &str) -> std::io::Result<()>

作用:这个小帮手负责在临时目录里写一份测试用配置文件。没有它,测试服务器就不知道该用哪个模型、该连哪个假的模型服务。

数据流:进去的是 codex_home 这个临时目录路径,以及假的模型服务 server_uri;它在目录下拼出 config.toml 文件路径,把模型名、审批策略、沙箱模式、模型提供方地址等内容写进去。出来的是写文件是否成功的结果;成功后,磁盘上多了一份测试服务器能读取的配置。

调用关系:它被两个测试函数在启动 TestAppServer 之前调用。它不直接和服务器通信,只是把测试环境的“说明书”准备好,让后面的服务器启动和线程操作能按测试设定运行。

调用图:被 2 处调用(thread_loaded_list_paginates, thread_loaded_list_returns_loaded_thread_ids);外部调用 3 个(join, format!, write)。

start_thread125–139 ↗
async fn start_thread(mcp: &mut TestAppServer) -> Result<String>

作用:这个小帮手负责向测试服务器发起“启动新线程”的请求,并把新线程的 ID 取回来。测试需要这个 ID,才能知道列表接口返回的到底是不是刚才创建的线程。

数据流:进去的是一个可操作的 TestAppServer;它发送 thread_start 请求,请求里指定模型为 gpt-5.2,然后等待服务器返回对应请求 ID 的响应。拿到 JSON-RPC 响应后,它转成 ThreadStartResponse,从里面取出 thread.id,最后把这个字符串 ID 返回给调用者。

调用关系:它被两个测试函数用来制造测试数据。它把“发启动请求、等待回应、解析线程 ID”这几步封装起来,让测试本身可以专心检查 thread_loaded_list 的结果,而不是重复写启动线程的细节。

调用图:调用 2 个内部函数(read_stream_until_response_message, send_thread_start_request);被 2 处调用(thread_loaded_list_paginates, thread_loaded_list_returns_loaded_thread_ids);外部调用 3 个(default, Integer, timeout)。

app-server/tests/suite/v2/thread_read.rs源码 ↗
testtest

这里测试的“线程”可以理解成一次聊天会话,“轮次”就是用户和助手来回交流的一段记录。文件会临时搭一个假的 Codex 家目录、假的模型服务器,或者假的内存线程仓库,然后通过 JSON-RPC(一种用 JSON 发请求和收回应的通信格式)去调用 app server。测试覆盖很多容易出错的边角:只读摘要时不能把完整对话塞回来;需要完整轮次时要保留用户输入里的文本标注;分页要能往旧记录和新记录翻;归档后的历史仍能按 id 找到;从内存仓库来的、没有磁盘文件路径的线程也能读;刚创建但还没写入第一条消息的线程要给出明确错误。文件底部是一批小工具,用来写假历史、提取文本、生成配置和清理内存仓库。

函数细节28
thread_read_returns_summary_without_turns83–135 ↗
async fn thread_read_returns_summary_without_turns() -> Result<()>

作用:检查读取线程摘要时,如果明确说不要轮次,服务器只返回线程基本信息,不返回聊天内容。这样可以保证列表页或详情页预览不会被大量历史记录拖慢。

数据流:输入是一份临时配置、一个假的历史文件和 include_turns=false 的读取请求 → 测试启动服务器,发出 thread/read 请求 → 输出是一个线程摘要,并断言 id、预览、模型提供方、路径、来源、状态等字段正确,同时 turns 为空。

调用关系:这个测试由测试框架直接运行。它先用 create_config_toml 准备配置,用外部测试工具生成假历史,再通过 TestAppServer 发请求并把响应转成 ThreadReadResponse 来检查。

调用图:调用 3 个内部函数(new, new, create_config_toml);外部调用 7 个(new, Integer, create_fake_rollout_with_text_elements, create_mock_responses_server_repeating_assistant, assert!, assert_eq!, timeout)。

thread_read_can_include_turns138–197 ↗
async fn thread_read_can_include_turns() -> Result<()>

作用:检查读取线程时如果要求 include_turns=true,服务器会把聊天轮次一起带回来。它确认用户消息和其中的文本元素没有在读历史时丢失。

数据流:输入是假历史里的用户文本和 text_elements 标注 → 服务器收到 thread/read 请求并读取完整历史 → 输出包含一个完成状态的轮次,里面有一条 UserMessage,文本和标注都与写入时一致。

调用关系:测试框架直接运行它。它依赖 create_config_toml 和外部假历史生成工具布置环境,然后通过 TestAppServer 走真实的请求/响应路径。

调用图:调用 2 个内部函数(new, create_config_toml);外部调用 8 个(new, Integer, create_fake_rollout_with_text_elements, create_mock_responses_server_repeating_assistant, assert_eq!, panic!, timeout, vec!)。

thread_turns_list_can_page_backward_and_forward200–286 ↗
async fn thread_turns_list_can_page_backward_and_forward() -> Result<()>

作用:检查轮次列表的分页能向旧记录翻,也能用游标向新记录追。游标可以理解成书签,告诉服务器“从这里继续”。

数据流:输入是三条按时间写入的用户消息 → 第一次按倒序取两条,得到 third、second 和两个游标;第二次用 next_cursor 取更旧的 first;再追加 fourth 后,用 backwards_cursor 正序取 newer 页面 → 输出证明分页方向和边界都正确。

调用关系:这个测试直接使用 append_user_message 往历史文件追加消息,再调用 thread/turns/list。它也用 turn_user_texts 把返回轮次中的用户文字抽出来,方便断言。

调用图:调用 3 个内部函数(new, append_user_message, create_config_toml);外部调用 9 个(new, Integer, create_fake_rollout_with_text_elements, create_mock_responses_server_repeating_assistant, rollout_path, assert!, assert_eq!, timeout, vec!)。

thread_turns_list_supports_requested_items_view289–354 ↗
async fn thread_turns_list_supports_requested_items_view() -> Result<()>

作用:检查轮次列表支持不同的内容视图。也就是同一个轮次,可以请求完整内容、摘要内容,或者只要外壳不加载内容。

数据流:输入是一个用户消息和两条助手消息,以及不同的 items_view 参数 → 服务器按 Full、Summary、NotLoaded 三种模式返回同一轮次 → 输出分别验证完整模式含全部助手消息,摘要模式只保留重点,不加载模式保留 id 和时间等元信息但 items 为空。

调用关系:测试框架直接运行它。它用 append_agent_message 造助手消息,用 read_single_turn_items_view 重复发列表请求,并用 turn_user_texts、turn_agent_texts 检查返回文本。

调用图:调用 4 个内部函数(new, append_agent_message, create_config_toml, read_single_turn_items_view);外部调用 8 个(new, create_fake_rollout_with_text_elements, create_mock_responses_server_repeating_assistant, rollout_path, assert!, assert_eq!, timeout, vec!)。

thread_turns_list_reads_store_history_without_rollout_path357–422 ↗
async fn thread_turns_list_reads_store_history_without_rollout_path() -> Result<()>

作用:检查没有磁盘历史文件路径的线程,也能从线程仓库里列出轮次。这样服务器不只依赖本地 rollout 文件,也支持新的存储后端。

数据流:输入是一个临时内存线程仓库、线程 id 和一条仓库里的历史消息 → 测试启动 in-process 服务器,也就是在同一进程里跑服务 → 请求 thread/turns/list 后输出一条用户文本 history from store。

调用关系:它由测试框架直接运行。它调用 create_config_toml_with_thread_store 写入内存仓库配置,用 seed_pathless_store_thread 填充数据,再通过 in_process::start 启动真实服务客户端。

调用图:调用 9 个内部函数(start, create_config_toml_with_thread_store, seed_pathless_store_thread, default, without_managed_config_for_tests, default_for_tests, new, from_string, for_id);外部调用 10 个(new, default, new, new_v4, new, Integer, default, assert_eq!, default, from_value)。

thread_read_loaded_include_turns_reads_store_history_without_rollout_path425–506 ↗
async fn thread_read_loaded_include_turns_reads_store_history_without_rollout_path() -> Result<()>

作用:检查一个已经加载在服务器里的线程,即使没有落成磁盘文件,include_turns=true 也能从线程仓库读出历史。它保护“无路径线程”的详情读取能力。

数据流:输入是内存线程仓库配置和刚 thread/start 创建的线程 → 测试把 store_history_items 追加进仓库 → 再发 thread/read include_turns=true → 输出的 thread.turns 里能看到 history from store。

调用关系:测试框架直接运行它。它不走 TestAppServer,而是用 in_process::start 拿到客户端,先发 ThreadStart,再直接操作 InMemoryThreadStore,最后发 ThreadRead。

调用图:调用 9 个内部函数(start, create_config_toml_with_thread_store, store_history_items, default, without_managed_config_for_tests, default_for_tests, new, from_string, for_id);外部调用 10 个(new, default, new, new_v4, new, Integer, default, assert_eq!, default, from_value)。

thread_list_includes_store_thread_without_rollout_path509–585 ↗
async fn thread_list_includes_store_thread_without_rollout_path() -> Result<()>

作用:检查线程列表会包含只存在于线程仓库、没有本地历史文件路径的线程。否则用户会在列表里看不到这类会话。

数据流:输入是一个被 seed_pathless_store_thread 填好的内存仓库 → 服务器收到 thread/list 请求 → 输出列表里有一个线程,path 是 None,name 是 named pathless thread。

调用关系:测试框架直接运行它。它通过 create_config_toml_with_thread_store 接通内存仓库,用 seed_pathless_store_thread 造数据,再用 in-process 客户端发 ThreadList 请求。

调用图:调用 9 个内部函数(start, create_config_toml_with_thread_store, seed_pathless_store_thread, default, without_managed_config_for_tests, default_for_tests, new, from_string, for_id);外部调用 10 个(new, default, new, new_v4, new, Integer, default, assert_eq!, default, from_value)。

thread_read_can_return_archived_threads_by_id588–633 ↗
async fn thread_read_can_return_archived_threads_by_id() -> Result<()>

作用:检查已经移动到归档目录的线程,仍然能通过线程 id 读出来。这样用户归档后,详情页或深链接不会失效。

数据流:输入是一个先创建、再被移动到 archived 目录的历史文件 → 服务器收到 thread/read 请求 → 输出线程 id 和预览正确,并且返回的 path 指向归档后的文件位置。

调用关系:测试框架直接运行它。它用 rollout_path 找到活跃历史文件,手动 rename 到归档目录,然后通过 TestAppServer 验证 thread/read 的查找逻辑。

调用图:调用 2 个内部函数(new, create_config_toml);外部调用 10 个(new, Integer, create_fake_rollout_with_text_elements, create_mock_responses_server_repeating_assistant, rollout_path, assert_eq!, create_dir_all, rename, timeout, vec!)。

thread_resume_initial_turns_page_matches_requested_turns_list_page636–704 ↗
async fn thread_resume_initial_turns_page_matches_requested_turns_list_page() -> Result<()>

作用:检查恢复线程时附带的初始轮次页,和单独调用轮次列表接口得到的页面完全一致。这样前端恢复会话时不用再猜服务器分页规则。

数据流:输入是三条用户消息和相同的分页参数 → 先调用 thread/turns/list 得到期望页面 → 再调用 thread/resume 并请求 initial_turns_page → 输出两边的页面内容相同,且 resume 主体里不重复塞完整 turns。

调用关系:测试框架直接运行它。它用 append_user_message 追加历史,再比较 ThreadTurnsListResponse 和 ThreadResumeResponse 里的 TurnsPage。

调用图:调用 3 个内部函数(new, append_user_message, create_config_toml);外部调用 10 个(default, new, Integer, create_fake_rollout_with_text_elements, create_mock_responses_server_repeating_assistant, rollout_path, assert!, assert_eq!, timeout, vec!)。

thread_turns_list_rejects_cursor_when_anchor_turn_is_rolled_back707–775 ↗
async fn thread_turns_list_rejects_cursor_when_anchor_turn_is_rolled_back() -> Result<()>

作用:检查如果游标指向的那一轮后来被回滚删除,服务器会拒绝继续用这个游标。这样可以避免客户端拿着过期书签读到混乱页面。

数据流:输入是三条消息、一次分页返回的 backwards_cursor,以及一条 thread_rolled_back 回滚事件 → 再用旧游标请求新页面 → 输出是 JSON-RPC 错误,提示 anchor turn is no longer present。

调用关系:测试框架直接运行它。它用 append_user_message 建历史,用 append_thread_rollback 写入回滚事件,然后确认 thread/turns/list 不会静默返回错误数据。

调用图:调用 4 个内部函数(new, append_thread_rollback, append_user_message, create_config_toml);外部调用 8 个(new, Integer, create_fake_rollout_with_text_elements, create_mock_responses_server_repeating_assistant, rollout_path, assert_eq!, timeout, vec!)。

thread_read_returns_forked_from_id_for_forked_threads778–825 ↗
async fn thread_read_returns_forked_from_id_for_forked_threads() -> Result<()>

作用:检查从一个线程分叉出来的新线程,在读取时会带上原线程 id。分叉像从一段聊天另开一条支线,这个字段能说明它从哪来。

数据流:输入是一个已有线程 id → 测试发 thread/fork 创建分叉线程 → 再读取分叉线程 → 输出 thread.forked_from_id 等于原线程 id。

调用关系:测试框架直接运行它。它先用假历史创建源线程,再通过 TestAppServer 调用 fork 和 read 两个接口,验证两个接口之间的信息传递。

调用图:调用 2 个内部函数(new, create_config_toml);外部调用 8 个(default, new, Integer, create_fake_rollout_with_text_elements, create_mock_responses_server_repeating_assistant, assert_eq!, timeout, vec!)。

thread_read_loaded_thread_returns_precomputed_path_before_materialization828–874 ↗
async fn thread_read_loaded_thread_returns_precomputed_path_before_materialization() -> Result<()>

作用:检查刚创建但还没写第一条用户消息的线程,也能在读取摘要时返回预先算好的文件路径。这个文件此时还不存在,但路径对客户端有用。

数据流:输入是 thread/start 创建的新线程 → 测试确认它的历史文件尚未实际创建 → 发 thread/read include_turns=false → 输出同一个线程 id、同一个预计算 path、空预览、空 turns,并且状态是 Idle。

调用关系:测试框架直接运行它。它通过 TestAppServer 先 start 再 read,专门覆盖“线程已加载但历史文件还没物化”的早期阶段。

调用图:调用 2 个内部函数(new, create_config_toml);外部调用 7 个(default, new, Integer, create_mock_responses_server_repeating_assistant, assert!, assert_eq!, timeout)。

thread_name_set_is_reflected_in_read_list_and_resume877–1032 ↗
async fn thread_name_set_is_reflected_in_read_list_and_resume() -> Result<()>

作用:检查给线程改名后,读取、列表、恢复三个接口都会显示这个名字,并且网络返回的 JSON 里真的包含 name 和 ephemeral 字段。

数据流:输入是一个已有线程和新名字 My renamed thread → 测试发 thread/set/name,等待 name updated 通知 → 再分别发 thread/read、thread/list、thread/resume → 输出三个地方都能看到新名字,序列化后的 JSON 字段也正确。

调用关系:测试框架直接运行它。它把改名接口、通知机制、读取接口、列表接口和恢复接口串在一起,确认用户可见标题在整条链路上保持一致。

调用图:调用 2 个内部函数(new, create_config_toml);外部调用 9 个(default, new, Integer, create_fake_rollout_with_text_elements, create_mock_responses_server_repeating_assistant, assert_eq!, from_value, timeout, vec!)。

thread_read_include_turns_rejects_unmaterialized_loaded_thread1035–1083 ↗
async fn thread_read_include_turns_rejects_unmaterialized_loaded_thread() -> Result<()>

作用:检查刚创建但还没有第一条用户消息的线程,不能用 include_turns=true 读取完整轮次。因为还没有实际历史文件,强行读会让语义变得含糊。

数据流:输入是一个刚 thread/start 的新线程 → 测试确认它的预计算路径还不存在 → 发 thread/read include_turns=true → 输出错误消息,说明第一条用户消息之前不可用。

调用关系:测试框架直接运行它。它和摘要读取的测试形成对照:摘要可读,但完整轮次读取要被服务器明确拒绝。

调用图:调用 2 个内部函数(new, create_config_toml);外部调用 6 个(default, new, Integer, create_mock_responses_server_repeating_assistant, assert!, timeout)。

thread_turns_list_rejects_unmaterialized_loaded_thread1086–1137 ↗
async fn thread_turns_list_rejects_unmaterialized_loaded_thread() -> Result<()>

作用:检查刚创建但还没写入历史的线程,不能调用 thread/turns/list。这样客户端不会误以为一个尚未开始的会话已经有可分页历史。

数据流:输入是一个未物化的新线程 id → 测试发 thread/turns/list → 输出错误消息,说明第一条用户消息之前不可用。

调用关系:测试框架直接运行它。它通过 TestAppServer 启动线程,再验证轮次列表接口对未落盘线程的保护。

调用图:调用 2 个内部函数(new, create_config_toml);外部调用 6 个(default, new, Integer, create_mock_responses_server_repeating_assistant, assert!, timeout)。

thread_turns_items_list_returns_unsupported1140–1170 ↗
async fn thread_turns_items_list_returns_unsupported() -> Result<()>

作用:检查 thread/turns/items/list 这个接口目前会明确返回“不支持”。这比假装成功或返回空数据更安全,客户端能据此禁用相关功能。

数据流:输入是随便给的 thread_id 和 turn_id → 服务器收到 thread/turns/items/list 请求 → 输出 JSON-RPC 错误码 -32601,消息是 not supported yet。

调用关系:测试框架直接运行它。它只验证协议层当前承诺:这个接口还没实现,必须用标准错误告诉调用者。

调用图:调用 2 个内部函数(new, create_config_toml);外部调用 5 个(new, Integer, create_mock_responses_server_repeating_assistant, assert_eq!, timeout)。

thread_read_reports_system_error_idle_flag_after_failed_turn1173–1238 ↗
async fn thread_read_reports_system_error_idle_flag_after_failed_turn() -> Result<()>

作用:检查一次模型响应失败后,读取线程摘要会报告 SystemError 状态。这样界面能告诉用户这条会话卡在系统错误,而不是误显示正常空闲。

数据流:输入是一个会返回失败 SSE 的假模型服务器和一条用户 turn/start 请求 → 服务器收到失败事件并发出 error 通知 → 再 thread/read → 输出 thread.status 为 SystemError。

调用关系:测试框架直接运行它。它用 responses 测试工具搭失败的流式响应,再通过 TestAppServer 启动线程、发起轮次、等待错误通知,最后检查读取结果。

调用图:调用 5 个内部函数(new, create_config_toml, mount_sse_once, sse_failed, start_mock_server);外部调用 6 个(default, new, Integer, assert_eq!, timeout, vec!)。

append_user_message1240–1256 ↗
fn append_user_message(path: &Path, timestamp: &str, text: &str) -> std::io::Result<()>

作用:往一个历史文件末尾追加一条假的用户消息。测试用它快速制造多轮聊天记录。

数据流:输入是文件路径、时间戳和用户文字 → 函数以追加模式打开文件,把一行 JSON 写进去 → 输出是写文件结果,成功时历史文件多了一条 user_message 事件。

调用关系:它是测试辅助工具,被分页、恢复初始页面、游标回滚等测试调用,用来搭出需要的历史场景。

调用图:被 3 处调用(thread_resume_initial_turns_page_matches_requested_turns_list_page, thread_turns_list_can_page_backward_and_forward, thread_turns_list_rejects_cursor_when_anchor_turn_is_rolled_back);外部调用 2 个(new, writeln!)。

append_agent_message1258–1274 ↗
fn append_agent_message(path: &Path, timestamp: &str, text: &str) -> anyhow::Result<()>

作用:往历史文件末尾追加一条假的助手消息。它用来测试完整视图和摘要视图里助手内容怎么被返回。

数据流:输入是文件路径、时间戳和助手文字 → 函数把 AgentMessageEvent 转成 JSON 并写入文件 → 输出是成功或失败结果,成功后文件多了一条助手消息事件。

调用关系:它被 thread_turns_list_supports_requested_items_view 调用,和用户消息一起组成一个可检查的轮次。

调用图:被 1 处调用(thread_turns_list_supports_requested_items_view);外部调用 2 个(new, writeln!)。

append_thread_rollback1276–1290 ↗
fn append_thread_rollback(path: &Path, timestamp: &str, num_turns: u32) -> std::io::Result<()>

作用:往历史文件里追加一条“线程回滚”事件,表示最近若干轮被撤销。测试用它模拟客户端拿着旧游标时历史已经变化的情况。

数据流:输入是文件路径、时间戳和要回滚的轮次数 → 函数追加一行 thread_rolled_back JSON → 输出写文件结果,成功后服务器读历史时会看到回滚事件。

调用关系:它只被 thread_turns_list_rejects_cursor_when_anchor_turn_is_rolled_back 调用,用来制造游标失效的边界条件。

调用图:被 1 处调用(thread_turns_list_rejects_cursor_when_anchor_turn_is_rolled_back);外部调用 2 个(new, writeln!)。

read_single_turn_items_view1292–1315 ↗
async fn read_single_turn_items_view(
    mcp: &mut TestAppServer,
    thread_id: &str,
    items_view: Option<TurnItemsView>,
) -> anyhow::Result<codex_app_server_protocol::Turn>

作用:用指定的 items_view 请求某个线程的轮次列表,并要求结果刚好只有一个轮次。它让同一测试里反复读取 Full、Summary、NotLoaded 更省事。

数据流:输入是 TestAppServer、线程 id 和可选视图类型 → 函数发送 thread/turns/list,请求最多 10 条、按正序返回 → 解析响应,确认只有一条,然后把这条 Turn 返回。

调用关系:它被 thread_turns_list_supports_requested_items_view 调用。内部把实际请求交给 TestAppServer,并等待对应 request id 的响应。

调用图:调用 2 个内部函数(read_stream_until_response_message, send_thread_turns_list_request);被 1 处调用(thread_turns_list_supports_requested_items_view);外部调用 3 个(Integer, assert_eq!, timeout)。

turn_user_texts1317–1331 ↗
fn turn_user_texts(turns: &[codex_app_server_protocol::Turn]) -> Vec<&str>

作用:从一组轮次里抽出每个轮次第一条用户文本,方便测试断言。它相当于把复杂返回值剪成好比较的字符串列表。

数据流:输入是一组 Turn → 函数逐个看第一个 item,如果是 UserMessage 且第一个内容是文本,就取出 text → 输出字符串切片列表,不符合条件的内容会被跳过。

调用关系:多个测试用它检查轮次顺序和内容,例如分页、摘要视图、仓库历史读取等。它不发请求,只做本地数据提取。

调用图:外部调用 1 个(iter)。

turn_agent_texts1333–1342 ↗
fn turn_agent_texts(turns: &[codex_app_server_protocol::Turn]) -> Vec<&str>

作用:从一组轮次中抽出所有助手消息文本,方便测试比较助手回复是否完整或被摘要。它把嵌套结构摊平成字符串列表。

数据流:输入是一组 Turn → 函数遍历每个轮次的每个 item,只挑 AgentMessage 的 text → 输出这些助手文本组成的列表。

调用关系:它主要配合 items_view 测试使用,帮助确认 Full 视图返回 draft 和 final,而 Summary 视图只保留预期内容。

调用图:外部调用 1 个(iter)。

InMemoryThreadStoreId::drop1349–1351 ↗
fn drop(&mut self)

作用:在测试结束时清理指定 id 的内存线程仓库。Drop 是 Rust 的自动清理钩子,变量离开作用域时会执行。

数据流:输入是结构体里保存的 store_id → drop 时调用 InMemoryThreadStore::remove_id → 输出没有返回值,但全局内存仓库中对应 id 的数据被移除。

调用关系:使用内存线程仓库的测试会创建 InMemoryThreadStoreId 变量。测试结束变量被释放时,它自动清理,避免不同测试之间互相污染。

调用图:调用 1 个内部函数(remove_id)。

seed_pathless_store_thread1354–1393 ↗
async fn seed_pathless_store_thread(
    store: &InMemoryThreadStore,
    thread_id: codex_protocol::ThreadId,
) -> Result<()>

作用:在内存线程仓库里预先创建一个没有磁盘路径的线程,并写入一条历史和一个名字。它是“仓库线程”相关测试的布景工具。

数据流:输入是 InMemoryThreadStore 和线程 id → 函数创建线程,追加 store_history_items 返回的历史,再更新元数据里的 name → 输出成功结果,仓库里多了一个可被列表和读取接口发现的线程。

调用关系:它被 thread_turns_list_reads_store_history_without_rollout_path 和 thread_list_includes_store_thread_without_rollout_path 调用。内部把具体工作交给仓库的 create_thread、append_items、update_thread_metadata。

调用图:调用 5 个内部函数(store_history_items, default, append_items, create_thread, update_thread_metadata);被 2 处调用(thread_list_includes_store_thread_without_rollout_path, thread_turns_list_reads_store_history_without_rollout_path);外部调用 2 个(default, new)。

store_history_items1395–1406 ↗
fn store_history_items() -> Vec<RolloutItem>

作用:生成一组固定的假历史项目,内容是一条用户消息 history from store。测试用它保证预期文本稳定、好比较。

数据流:输入为空 → 函数构造一个 RolloutItem::EventMsg,其中包含 UserMessageEvent → 输出一个 Vec<RolloutItem>,可直接追加到线程仓库。

调用关系:它被 seed_pathless_store_thread 和 thread_read_loaded_include_turns_reads_store_history_without_rollout_path 调用,是内存仓库历史数据的共同来源。

调用图:被 2 处调用(seed_pathless_store_thread, thread_read_loaded_include_turns_reads_store_history_without_rollout_path);外部调用 1 个(vec!)。

create_config_toml_with_thread_store1408–1430 ↗
fn create_config_toml_with_thread_store(codex_home: &Path, store_id: &str) -> std::io::Result<()>

作用:给临时 Codex 家目录写一份 config.toml,并打开实验性的内存线程仓库配置。没有它,相关测试就无法让服务器连接到指定的测试仓库。

数据流:输入是 codex_home 路径和 store_id → 函数拼出 config.toml 路径,写入模型、沙箱、mock provider 和 experimental_thread_store 配置 → 输出写文件结果。

调用关系:它被三个内存仓库相关测试调用。生成的配置随后被 ConfigBuilder 或 in-process server 读取,决定线程数据从哪个内存仓库来。

调用图:被 3 处调用(thread_list_includes_store_thread_without_rollout_path, thread_read_loaded_include_turns_reads_store_history_without_rollout_path, thread_turns_list_reads_store_history_without_rollout_path);外部调用 3 个(join, format!, write)。

create_config_toml1433–1454 ↗
fn create_config_toml(codex_home: &Path, server_uri: &str) -> std::io::Result<()>

作用:给临时 Codex 家目录写一份普通测试配置,让服务器把模型请求发到假的 mock server。这样测试不会访问真实网络或真实模型。

数据流:输入是 codex_home 路径和 mock server 地址 → 函数写入 config.toml,设置模型名、审批策略、只读沙箱、模型提供方和重试次数 → 输出写文件结果。

调用关系:大多数测试启动 TestAppServer 前都会调用它。它负责把测试环境固定住,让后续 thread/read、thread/list、thread/resume 等请求可重复、可控。

调用图:被 14 处调用(thread_name_set_is_reflected_in_read_list_and_resume, thread_read_can_include_turns, thread_read_can_return_archived_threads_by_id, thread_read_include_turns_rejects_unmaterialized_loaded_thread, thread_read_loaded_thread_returns_precomputed_path_before_materialization, thread_read_reports_system_error_idle_flag_after_failed_turn, thread_read_returns_forked_from_id_for_forked_threads, thread_read_returns_summary_without_turns, thread_resume_initial_turns_page_matches_requested_turns_list_page, thread_turns_items_list_returns_unsupported (+4 more));外部调用 3 个(join, format!, write)。

app-server/tests/suite/v2/thread_resume.rs源码 ↗
testtest run

这里测试的是 app server 的 thread resume,也就是把以前保存过的对话线程重新加载回来。对普通用户来说,这相当于“关掉应用后再打开,聊天和任务还能接着来”。文件会造临时配置、假模型服务器、假历史记录和假审批请求,然后像真实客户端一样发 JSON-RPC 请求(JSON-RPC 是一种用 JSON 格式发命令和收结果的通信方式)。它覆盖很多容易翻车的边角:没落盘的线程不能恢复,归档线程要提示先解档,远程 ChatGPT 客户端要隐藏敏感工具参数,正在运行的线程不能被错误历史覆盖,目标状态、token 用量、Git 信息和待审批请求都要恢复正确。它的重要性在于:恢复线程是用户信任系统记忆和连续工作的基础,一旦错了,就可能丢上下文、泄露数据,或让任务重复执行。

函数细节47
normalized_existing_path114–116 ↗
fn normalized_existing_path(path: impl AsRef<Path>) -> Result<PathBuf>

作用:把一个已经存在的路径整理成系统认可的绝对路径。测试里用它来避免同一个文件因为写法不同而被误认为不是同一个文件。

数据流:输入一个路径 → 先让操作系统解析出真实路径,再包装成项目里的绝对路径类型 → 输出普通的 PathBuf;它不改文件,只整理路径文字。

调用关系:它是路径比较的小帮手,被 thread_resume_defers_updated_at_until_turn_start 使用,用来确认通过文件路径恢复线程时路径能正确匹配。

调用图:调用 1 个内部函数(from_absolute_path);被 1 处调用(thread_resume_defers_updated_at_until_turn_start);外部调用 1 个(as_ref)。

wait_for_responses_request_count118–146 ↗
async fn wait_for_responses_request_count(
    server: &wiremock::MockServer,
    expected_count: usize,
) -> Result<()>

作用:等待假模型服务器收到指定数量的 /responses 请求。它用来确认测试中的模型调用次数刚刚好,没有少发或多发。

数据流:输入一个 wiremock 假服务器和期望次数 → 反复查看服务器记录的请求,数 POST 到 /responses 的请求 → 达到次数就返回成功,超过或超时就报错。

调用关系:它服务于两个“审批后继续执行”的测试,确保命令审批或文件改动审批通过后,系统确实继续请求模型,并且请求次数符合预期。

调用图:被 2 处调用(thread_resume_replays_pending_command_execution_request_approval, thread_resume_replays_pending_file_change_request_approval);外部调用 5 个(received_requests, bail!, from_millis, sleep, timeout)。

thread_resume_rejects_unmaterialized_thread149–193 ↗
async fn thread_resume_rejects_unmaterialized_thread() -> Result<()>

作用:验证刚创建但还没有任何用户消息、也还没写入历史文件的线程不能恢复。这样可以避免系统凭空恢复一个没有存档的对话。

数据流:测试先启动假模型服务和测试 app server,再创建一个线程但不发消息 → 尝试恢复这个线程 → 期望收到“找不到 rollout”的错误。

调用关系:这是独立的 Tokio 异步测试;它依赖 create_config_toml 准备配置,用 TestAppServer 模拟客户端向服务器发 thread/start 和 thread/resume。

调用图:调用 2 个内部函数(new, create_config_toml);外部调用 6 个(default, new, Integer, create_mock_responses_server_repeating_assistant, assert!, timeout)。

thread_resume_with_empty_path_uses_running_thread_id196–258 ↗
async fn thread_resume_with_empty_path_uses_running_thread_id() -> Result<()>

作用:验证恢复请求里如果给了一个空路径,系统仍然会按正在运行的 thread_id 找线程。这样客户端传了空 path 时不会把已有线程搞丢。

数据流:先创建线程并发一条消息让历史落盘 → 再用同一个 thread_id 和空 PathBuf 请求恢复 → 输出的恢复结果仍然是原来的线程 id。

调用关系:这是一个端到端测试;它通过 TestAppServer 先 start、再 turn/start、最后 thread/resume,检查恢复逻辑优先认正在运行的线程。

调用图:调用 2 个内部函数(new, create_config_toml);外部调用 8 个(default, new, new, Integer, create_mock_responses_server_repeating_assistant, assert_eq!, timeout, vec!)。

thread_resume_running_thread_uses_cached_instruction_sources261–334 ↗
async fn thread_resume_running_thread_uses_cached_instruction_sources() -> Result<()>

作用:验证正在运行的线程恢复时,会沿用启动时缓存的指令文件来源。即使 AGENTS.md 后来被删了,恢复结果也应该记得当时用过它。

数据流:测试在临时工作区写入 AGENTS.md → 启动线程并确认发现该文件 → 发消息落盘后删除文件 → 恢复线程时仍返回原来的指令来源路径。

调用关系:它通过 create_config_toml 和 TestAppServer 搭环境,重点检查 thread/start 保存下来的 instruction_sources 在 thread/resume 中继续可见。

调用图:调用 3 个内部函数(new, create_config_toml, try_from);外部调用 9 个(default, new, Integer, create_mock_responses_server_repeating_assistant, assert_eq!, remove_file, write, timeout, vec!)。

turn_start_updates_runtime_workspace_roots_for_loaded_thread337–411 ↗
async fn turn_start_updates_runtime_workspace_roots_for_loaded_thread() -> Result<()>

作用:验证给已加载线程开始新回合时,运行时工作区根目录会被更新并去重。这样工具访问文件时知道哪些目录是合法工作区。

数据流:输入一个额外目录和它的等价写法 → turn/start 把这些目录传给服务器 → resume 时返回只保留规范化后的一份目录。

调用关系:这个测试串起 thread/start、turn/start 和 thread/resume,检查 turn/start 写入的 runtime_workspace_roots 能被恢复流程读出来。

调用图:调用 2 个内部函数(new, create_config_toml);外部调用 8 个(default, new, Integer, create_mock_responses_server_repeating_assistant, assert_eq!, create_dir_all, timeout, vec!)。

thread_goal_get_rejects_unmaterialized_thread414–465 ↗
async fn thread_goal_get_rejects_unmaterialized_thread() -> Result<()>

作用:验证临时且没有落盘的线程不支持读取目标。目标是可持久化的任务计划,没有历史文件时读取它没有意义。

数据流:测试开启 goals 功能,创建一个 ephemeral 临时线程 → 发 thread/goal/get → 期望返回“临时线程不支持 goals”的错误。

调用关系:这是目标功能的防呆测试;它用 new_without_managed_config 保留手写配置,再直接发原始 JSON-RPC 方法 thread/goal/get。

调用图:调用 2 个内部函数(new_without_managed_config, create_config_toml);外部调用 9 个(default, new, Integer, create_mock_responses_server_repeating_assistant, assert!, json!, read_to_string, write, timeout)。

thread_resume_tracks_thread_initialized_analytics468–523 ↗
async fn thread_resume_tracks_thread_initialized_analytics() -> Result<()>

作用:验证恢复旧线程时会发送“线程已初始化”的分析事件。这样产品侧能区分新建线程和恢复线程。

数据流:先造一个假的历史文件并标记来源为 user → 启动带分析捕获的服务器并恢复该线程 → 读取分析上报,确认 thread_id、session_id、模型和来源都正确。

调用关系:它调用 set_thread_source_on_fake_rollout 修改假历史,再借助 analytics 测试工具 mount_analytics_capture、wait_for_analytics_payload 和 assert_basic_thread_initialized_event 校验上报。

调用图:调用 7 个内部函数(new_without_managed_config, assert_basic_thread_initialized_event, mount_analytics_capture, thread_initialized_event, wait_for_analytics_payload, create_config_toml_with_chatgpt_base_url, set_thread_source_on_fake_rollout);外部调用 8 个(default, new, Integer, create_fake_rollout, create_mock_responses_server_repeating_assistant, assert!, assert_eq!, timeout)。

set_thread_source_on_fake_rollout525–542 ↗
fn set_thread_source_on_fake_rollout(
    codex_home: &std::path::Path,
    filename_ts: &str,
    thread_id: &str,
    thread_source: &str,
) -> Result<()>

作用:给假历史文件的第一行会话元数据补上 thread_source。它让测试能模拟“这个线程来自用户”之类的真实来源信息。

数据流:输入 codex_home、文件时间戳、线程 id 和来源字符串 → 找到 rollout 文件,读第一行 JSON,改 payload.thread_source → 把修改后的内容写回文件。

调用关系:它只被 thread_resume_tracks_thread_initialized_analytics 调用,是那个分析测试准备假数据的工具。

调用图:被 1 处调用(thread_resume_tracks_thread_initialized_analytics);外部调用 6 个(rollout_path, format!, from_str, json!, read_to_string, write)。

thread_resume_returns_rollout_history545–616 ↗
async fn thread_resume_returns_rollout_history() -> Result<()>

作用:验证恢复线程时能把历史记录正确变成客户端能看的 turns 和 items。用户重新打开对话时,应该看到以前发过的消息。

数据流:测试造一个带文本元素的 fake rollout → 请求恢复 → 检查线程元数据、路径、状态、以及第一回合里的用户消息都按历史还原。

调用关系:它通过 create_fake_rollout_with_text_elements 准备历史,再用 thread/resume 验证 app server 的历史解析和协议转换结果。

调用图:调用 2 个内部函数(new, create_config_toml);外部调用 10 个(default, new, Integer, create_fake_rollout_with_text_elements, create_mock_responses_server_repeating_assistant, assert!, assert_eq!, panic!, timeout, vec!)。

thread_resume_redacts_payloads_for_chatgpt_remote_clients619–716 ↗
async fn thread_resume_redacts_payloads_for_chatgpt_remote_clients() -> Result<()>

作用:验证 ChatGPT 移动端远程客户端恢复线程时,敏感工具参数和图片生成内容会被隐藏。这样远程展示不会泄露本地工具细节或私密结果。

数据流:分别用 Android/iOS remote 客户端名恢复带敏感 MCP 工具调用和图片生成的历史 → 检查参数、结果被替换为 [redacted] 且图片项被丢弃 → 再用普通客户端确认原内容仍保留。

调用关系:它反复调用 resume_redaction_fixture 生成不同客户端视角的恢复结果,是红action规则的总体验证入口。

调用图:调用 1 个内部函数(resume_redaction_fixture);外部调用 3 个(assert!, assert_eq!, unreachable!)。

resume_redaction_fixture718–772 ↗
async fn resume_redaction_fixture(client_name: Option<&str>) -> Result<ThreadResumeResponse>

作用:准备一份带敏感工具调用和图片生成记录的恢复测试场景。调用者可以指定客户端名,观察恢复结果是否被脱敏。

数据流:输入可选 client_name → 创建假模型服务、临时 home、配置和 fake rollout,再追加敏感历史 → 初始化测试服务器并发 thread/resume → 输出 ThreadResumeResponse。

调用关系:它被 thread_resume_redacts_payloads_for_chatgpt_remote_clients 调用,内部把补历史的工作交给 append_resume_redaction_history。

调用图:调用 3 个内部函数(new, append_resume_redaction_history, create_config_toml);被 1 处调用(thread_resume_redacts_payloads_for_chatgpt_remote_clients);外部调用 6 个(default, new, Integer, create_fake_rollout, create_mock_responses_server_repeating_assistant, timeout)。

append_resume_redaction_history774–827 ↗
fn append_resume_redaction_history(
    codex_home: &Path,
    filename_ts: &str,
    meta_rfc3339: &str,
    conversation_id: &str,
) -> Result<()>

作用:往 fake rollout 文件里追加两类敏感历史:MCP 工具调用结果和图片生成结果。这样后面的恢复测试有东西可检查。

数据流:输入 home、时间戳和线程 id → 读取原 rollout 文件 → 追加序列化后的 McpToolCallEnd 和 ImageGenerationEnd 事件 → 写回文件。

调用关系:它是 resume_redaction_fixture 的数据准备步骤,专门制造需要被远程客户端隐藏的历史内容。

调用图:被 1 处调用(resume_redaction_fixture);外部调用 10 个(from_millis, rollout_path, test_absolute_path, format!, json!, ImageGenerationEnd, McpToolCallEnd, read_to_string, write, vec!)。

thread_resume_can_skip_turns_for_metadata_only_resume830–866 ↗
async fn thread_resume_can_skip_turns_for_metadata_only_resume() -> Result<()>

作用:验证恢复线程时可以只要元数据,不带完整回合历史。这样列表页或概览页可以更快加载。

数据流:测试造一份带历史的 rollout → 用 exclude_turns=true 请求恢复 → 返回线程 id 和预览等信息,但 turns 是空的。

调用关系:这是 thread/resume 的轻量模式测试,使用 fake rollout 和 TestAppServer 检查 exclude_turns 参数生效。

调用图:调用 2 个内部函数(new, create_config_toml);外部调用 9 个(default, new, new, Integer, create_fake_rollout_with_text_elements, create_mock_responses_server_repeating_assistant, assert!, assert_eq!, timeout)。

thread_resume_rejects_archived_session_by_id869–917 ↗
async fn thread_resume_rejects_archived_session_by_id() -> Result<()>

作用:验证已经归档的会话不能直接按 id 恢复,而是要提示先解档。这样避免用户误把冷存档当活跃线程打开。

数据流:测试先造 rollout,再把文件移动到 archived 目录 → 请求按 thread_id 恢复 → 期望错误信息说明会话已归档并提示 unarchive 命令。

调用关系:它使用 ARCHIVED_SESSIONS_SUBDIR 模拟真实归档位置,是恢复入口对归档状态的保护测试。

调用图:调用 2 个内部函数(new, create_config_toml);外部调用 11 个(default, new, new, Integer, create_fake_rollout_with_text_elements, create_mock_responses_server_repeating_assistant, rollout_path, assert!, create_dir_all, rename (+1 more))。

thread_resume_keeps_paused_goal_paused920–1022 ↗
async fn thread_resume_keeps_paused_goal_paused() -> Result<()>

作用:验证带目标的线程恢复后,如果目标原本是暂停状态,就不会自动继续执行。暂停就像按下暂停键,恢复窗口不应等于继续播放。

数据流:创建线程并落盘 → 设置 goal 状态为 paused → 恢复线程 → 收到目标更新通知仍是 Paused,并确认没有新的 turn/started 通知。

调用关系:它直接发 thread/goal/set 和 thread/resume,检查目标系统和线程恢复系统之间不会误触发自动继续。

调用图:调用 2 个内部函数(new_without_managed_config, create_config_toml);外部调用 13 个(default, new, bail!, Integer, create_mock_responses_server_repeating_assistant, to_response, assert!, assert_eq!, json!, read_to_string (+3 more))。

thread_goal_set_preserves_budget_limited_same_objective1025–1121 ↗
async fn thread_goal_set_preserves_budget_limited_same_objective() -> Result<()>

作用:验证同一个目标已经因为 token 预算受限时,再设置同样目标不会把状态重置成活跃。预算耗尽后不能靠重复设置绕过限制。

数据流:先创建并落盘线程 → 设置目标为 budgetLimited 且预算为 10 → 再用同样 objective 设置目标但不传状态 → 返回结果仍是 BudgetLimited,预算和用量保持。

调用关系:这是 goal/set 的状态保持测试,依赖 TestAppServer 发送原始 thread/goal/set 请求并读取 ThreadGoalSetResponse。

调用图:调用 2 个内部函数(new_without_managed_config, create_config_toml);外部调用 11 个(default, new, Integer, create_mock_responses_server_repeating_assistant, to_response, assert_eq!, json!, read_to_string, write, timeout (+1 more))。

thread_goal_set_persists_resumable_stopped_statuses1124–1208 ↗
async fn thread_goal_set_persists_resumable_stopped_statuses() -> Result<()>

作用:验证 blocked 和 usageLimited 这类可恢复的停止状态会被保存并通过通知发出来。这样客户端刷新后不会误以为目标还在跑。

数据流:创建并落盘线程 → 依次设置目标状态为 blocked、usageLimited → 检查响应和 thread/goal/updated 通知里的状态都正确。

调用关系:它是目标状态枚举的持久化测试,和恢复相关,因为这些状态之后要能被重新读取。

调用图:调用 2 个内部函数(new_without_managed_config, create_config_toml);外部调用 12 个(default, new, bail!, Integer, create_mock_responses_server_repeating_assistant, to_response, assert_eq!, json!, read_to_string, write (+2 more))。

thread_goal_set_edits_objective_without_resetting_usage1211–1317 ↗
async fn thread_goal_set_edits_objective_without_resetting_usage() -> Result<()>

作用:验证修改目标文字时,不会清掉已经累计的时间和 token 用量。改标题不应该让预算表重新归零。

数据流:创建 fake rollout 并设置目标 → 直接通过状态数据库追加 12 秒和 50 token 的用量 → 再改 objective → 返回同一个 goal_id,保留用量,并因超预算变为 BudgetLimited。

调用关系:它同时使用 app server 和 StateRuntime 状态数据库,检查 API 层改目标时不会破坏底层持久化用量。

调用图:调用 4 个内部函数(new_without_managed_config, create_config_toml, from_string, init);外部调用 10 个(new, Integer, create_fake_rollout, create_mock_responses_server_repeating_assistant, to_response, assert_eq!, json!, read_to_string, write, timeout)。

thread_goal_lifecycle_emits_analytics_and_clear_deletes_goal1320–1511 ↗
async fn thread_goal_lifecycle_emits_analytics_and_clear_deletes_goal() -> Result<()>

作用:验证目标从创建、记账、预算受限到清除的全过程都会发正确分析事件,并且清除后真的查不到目标。

数据流:测试用两段假模型响应让目标继续执行并消耗 200 token → 设置带预算目标 → 等待 created、usage_accounted、status_changed 分析事件 → 清除目标并确认 cleared 事件、get 为空、重复 clear 返回未清除。

调用关系:这是目标生命周期的大集成测试;它把模型响应、目标 API、通知、状态删除和 analytics 捕获串在一起。

调用图:调用 4 个内部函数(new_without_managed_config, mount_analytics_capture, wait_for_goal_event, create_config_toml_with_chatgpt_base_url);外部调用 12 个(default, new, Integer, create_mock_responses_server_sequence_unchecked, to_response, assert!, assert_eq!, json!, read_to_string, write (+2 more))。

thread_resume_emits_restored_token_usage_before_next_turn1514–1564 ↗
async fn thread_resume_emits_restored_token_usage_before_next_turn() -> Result<()>

作用:验证恢复带 token 用量历史的线程后,会先把已恢复的用量通知给客户端。这样 UI 在下一次对话前就能显示正确消耗。

数据流:造一个包含 token usage 的 fake rollout → 恢复线程 → 读取 thread/tokenUsage/updated 通知 → 检查总 token、输入输出 token 和上下文窗口都正确。

调用关系:它检查 thread/resume 对历史 TokenCountEvent 的重放行为,确保恢复后客户端能同步用量状态。

调用图:调用 2 个内部函数(new, create_config_toml);外部调用 8 个(default, new, Integer, create_fake_rollout_with_token_usage, create_mock_responses_server_repeating_assistant, assert_eq!, panic!, timeout)。

thread_resume_skips_restored_token_usage_when_turns_are_excluded1567–1638 ↗
async fn thread_resume_skips_restored_token_usage_when_turns_are_excluded() -> Result<()>

作用:验证 exclude_turns=true 时不会重放 token 用量通知。既然客户端没要回合历史,就不应该收到指向某个回合的用量更新。

数据流:第一次正常恢复并确认收到用量通知 → 第二次用 exclude_turns=true 恢复同一线程 → 返回 turns 为空,并等待通知超时,说明没有重放。

调用关系:它和上一个 token 用量测试成对出现,专门确认轻量恢复模式不会产生多余通知。

调用图:调用 2 个内部函数(new, create_config_toml);外部调用 9 个(default, new, Integer, create_fake_rollout_with_token_usage, create_mock_responses_server_repeating_assistant, assert!, assert_eq!, panic!, timeout)。

thread_resume_token_usage_replay_ignores_stale_interrupted_tail_turn1641–1726 ↗
async fn thread_resume_token_usage_replay_ignores_stale_interrupted_tail_turn() -> Result<()>

作用:验证如果历史末尾有一个没写完的中断回合,但 token 用量属于前一个已完成回合,恢复时不会把用量错绑到末尾回合。

数据流:先造带用量的 rollout → 手工追加一个只开始但没完成的回合和一条助手消息 → 恢复后末尾回合标为 Interrupted → 用量通知仍指向第一个完成回合。

调用关系:它直接改 rollout 文件制造半截历史,检查恢复时识别“陈旧尾巴”的逻辑。

调用图:调用 2 个内部函数(new, create_config_toml);外部调用 14 个(default, new, Integer, create_fake_rollout_with_token_usage, create_mock_responses_server_repeating_assistant, rollout_path, assert_eq!, assert_ne!, format!, json! (+4 more))。

thread_resume_token_usage_replay_can_belong_to_interrupted_turn1729–1849 ↗
async fn thread_resume_token_usage_replay_can_belong_to_interrupted_turn() -> Result<()>

作用:验证如果中断回合自己记录了 token 用量,那么恢复时用量应该归到这个中断回合。中断不等于没有消耗。

数据流:造基础 rollout → 追加一个新回合、助手消息、TokenCountEvent 和 TurnAborted → 恢复线程 → 检查用量通知的 turn_id 是这个中断回合。

调用关系:它补充前一个测试的另一种情况,确保恢复逻辑能区分“旧用量”与“中断回合自己的用量”。

调用图:调用 2 个内部函数(new, create_config_toml);外部调用 13 个(default, new, Integer, create_fake_rollout_with_token_usage, create_mock_responses_server_repeating_assistant, rollout_path, assert_eq!, format!, json!, panic! (+3 more))。

thread_resume_prefers_persisted_git_metadata_for_local_threads1852–2043 ↗
async fn thread_resume_prefers_persisted_git_metadata_for_local_threads() -> Result<()>

作用:验证本地线程恢复时优先使用已保存的 Git 元数据,而不是实时读取当前仓库分支。这样旧会话能保持当时记录的分支信息。

数据流:测试创建真实 git 仓库和 fake rollout → 初始化状态数据库并通过 metadata/update 写入 branch=feature/pr-branch → 恢复线程 → 返回 git_info.branch 是保存值而不是当前 master。

调用关系:它用 git 命令、StateRuntime 和 app server 一起验证元数据更新与恢复展示之间的关系。

调用图:调用 3 个内部函数(new, from_string, init);外部调用 14 个(default, new, new_v4, Integer, create_mock_responses_server_repeating_assistant, rollout_path, assert!, assert_eq!, new, format! (+4 more))。

thread_resume_and_read_interrupt_incomplete_rollout_turn_when_thread_is_idle2046–2161 ↗
async fn thread_resume_and_read_interrupt_incomplete_rollout_turn_when_thread_is_idle() -> Result<()>

作用:验证空闲线程的历史里如果有没完成的回合,恢复和读取都会把它标成 Interrupted。这样 UI 不会误显示它还在运行。

数据流:造一份完成历史后手工追加未完成 turn → 第一次恢复、第二次恢复、thread/read → 每次都看到线程 Idle,末尾 turn 状态为 Interrupted。

调用关系:它覆盖 thread/resume 和 thread/read 两条入口,确认两者对不完整历史的解释一致。

调用图:调用 2 个内部函数(new, create_config_toml);外部调用 13 个(default, new, new, Integer, create_fake_rollout_with_text_elements, create_mock_responses_server_repeating_assistant, rollout_path, assert_eq!, format!, json! (+3 more))。

thread_resume_defers_updated_at_until_turn_start2164–2260 ↗
async fn thread_resume_defers_updated_at_until_turn_start() -> Result<()>

作用:验证单纯恢复线程不会更新 updated_at,也不会改历史文件修改时间。只有真正开始新回合,才算线程被更新。

数据流:先准备带固定修改时间的 rollout → thread/read 记录恢复前 updated_at → thread/resume 后确认 updated_at 和文件 mtime 没变 → 再开始 turn,确认文件修改时间变新。

调用关系:它调用 setup_rollout_fixture 准备历史,并用 normalized_existing_path 测试按路径恢复;重点保护“只查看不算编辑”的行为。

调用图:调用 3 个内部函数(new, normalized_existing_path, setup_rollout_fixture);外部调用 9 个(default, new, Integer, create_mock_responses_server_repeating_assistant, assert!, assert_eq!, metadata, timeout, vec!)。

thread_resume_keeps_in_flight_turn_streaming2263–2356 ↗
async fn thread_resume_keeps_in_flight_turn_streaming() -> Result<()>

作用:验证一个客户端正在跑回合时,另一个客户端恢复同一线程不会打断流式输出。用户多窗口打开时,正在执行的任务应该继续。

数据流:primary 客户端创建并跑完种子回合 → secondary 客户端初始化 → primary 开始新回合并收到 turn/started → secondary 恢复线程 → primary 仍能收到 turn/completed。

调用关系:它模拟两个 TestAppServer 连接同一 home,检查恢复订阅不会干扰已有 in-flight turn。

调用图:调用 2 个内部函数(new, create_config_toml);外部调用 7 个(default, new, Integer, create_mock_responses_server_repeating_assistant, assert_ne!, timeout, vec!)。

thread_resume_rejects_history_when_thread_is_running2359–2475 ↗
async fn thread_resume_rejects_history_when_thread_is_running() -> Result<()>

作用:验证线程正在运行时,不能用自定义 history 覆盖它来恢复。这样防止一边执行一边被换掉上下文。

数据流:启动线程并完成种子回合 → 开始一个延迟完成的新回合 → 在运行中发带 history 的 thread/resume → 期望错误消息说明不能对 running 线程带 history 恢复。

调用关系:它用 responses mock 控制第二个模型响应延迟,并在最后调用 interrupt_turn_and_wait_for_aborted 清理运行中的回合。

调用图:调用 7 个内部函数(new, create_config_toml, mount_response_once, mount_sse_once, sse, sse_response, start_mock_server);外部调用 8 个(default, new, Integer, assert!, assert_eq!, from_millis, timeout, vec!)。

thread_resume_rejects_mismatched_path_for_running_thread_id2478–2643 ↗
async fn thread_resume_rejects_mismatched_path_for_running_thread_id() -> Result<()>

作用:验证正在运行的线程如果按 id 恢复,不能同时给一个指向别的历史文件的路径。这样避免把活跃线程和陈旧文件错配。

数据流:创建并运行线程 → 手工造另一个 stale rollout 文件 → 用运行中的 thread_id 加 stale path 请求恢复 → 期望错误包含 stale path;Windows 下还额外确认等价路径写法可被接受。

调用关系:它和运行中线程恢复逻辑直接相关,使用 mock 响应延迟保持线程处于 running,最后中断回合做清理。

调用图:调用 7 个内部函数(new, create_config_toml, mount_response_once, mount_sse_once, sse, sse_response, start_mock_server);外部调用 17 个(default, from, new, new_v4, parse_str, Integer, rollout_path, assert!, assert_eq!, format! (+7 more))。

thread_resume_rejoins_running_thread_even_with_override_mismatch2646–2778 ↗
async fn thread_resume_rejoins_running_thread_even_with_override_mismatch() -> Result<()>

作用:验证恢复正在运行的线程时,即使请求里传了不同模型或 cwd,系统也会重新加入现有线程,而不是创建或改写它。

数据流:先跑完种子回合,再启动一个延迟的新回合 → thread/resume 传入不匹配的 model、cwd 和 initial_turns_page → 返回仍使用运行中的模型,并在页面里显示正在进行的 turn。

调用关系:它测试“重新加入运行线程”的优先级,说明运行态比请求覆盖参数更权威。

调用图:调用 6 个内部函数(new, create_config_toml, mount_response_sequence, sse, sse_response, start_mock_server);外部调用 9 个(default, new, Integer, assert!, assert_eq!, panic!, from_millis, timeout, vec!)。

thread_resume_can_skip_turns_when_thread_is_running2781–2857 ↗
async fn thread_resume_can_skip_turns_when_thread_is_running() -> Result<()>

作用:验证线程即使已经加载过,也可以用 exclude_turns=true 做轻量恢复。客户端只想要元数据时不必拉完整历史。

数据流:创建线程并完成一个回合 → 第二个客户端恢复同一线程且 exclude_turns=true → 返回同一线程 id、Idle 状态和空 turns。

调用关系:它用 primary/secondary 两个测试服务器连接同一 home,检查轻量恢复对已加载线程同样有效。

调用图:调用 5 个内部函数(new, create_config_toml, mount_sse_once, sse, start_mock_server);外部调用 7 个(default, new, Integer, assert!, assert_eq!, timeout, vec!)。

thread_resume_replays_pending_command_execution_request_approval2860–2995 ↗
async fn thread_resume_replays_pending_command_execution_request_approval() -> Result<()>

作用:验证恢复正在等待命令执行审批的线程时,会重新发出同一个审批请求。这样客户端刷新后不会丢掉“是否允许运行命令”的弹窗。

数据流:模型先完成种子,再请求执行 shell 命令 → 服务器发出 CommandExecutionRequestApproval → thread/resume 后再次收到完全相同的审批请求 → 测试接受审批,任务继续完成,并确认模型请求次数为 3。

调用关系:它调用 wait_for_responses_request_count 检查审批后流程继续;重点覆盖 pending request 的重放。

调用图:调用 3 个内部函数(new, create_config_toml, wait_for_responses_request_count);外部调用 11 个(default, new, Integer, create_mock_responses_server_sequence_unchecked, assert!, assert_eq!, panic!, assert_eq!, to_value, timeout (+1 more))。

thread_resume_replays_pending_file_change_request_approval2998–3163 ↗
async fn thread_resume_replays_pending_file_change_request_approval() -> Result<()>

作用:验证恢复正在等待文件改动审批的线程时,会重新发出同一个文件审批请求。这样用户刷新后仍能决定是否应用补丁。

数据流:模型先完成种子,再返回 apply_patch 调用 → 服务器发出 FileChangeRequestApproval → thread/resume 后重放相同请求 → 测试接受审批,任务完成,并确认模型请求次数为 3。

调用关系:它和命令审批测试平行,使用 wait_for_responses_request_count 验证审批通过后模型调用链继续。

调用图:调用 3 个内部函数(new, create_config_toml, wait_for_responses_request_count);外部调用 12 个(default, new, Integer, create_mock_responses_server_sequence_unchecked, assert!, assert_eq!, panic!, from_value, to_value, create_dir (+2 more))。

thread_resume_with_overrides_defers_updated_at_until_turn_start3166–3230 ↗
async fn thread_resume_with_overrides_defers_updated_at_until_turn_start() -> Result<()>

作用:验证带模型覆盖参数恢复线程时,也不会立刻更新 updated_at 或修改历史文件。覆盖配置本身不应被当成一次新对话。

数据流:先启动线程、落盘、重启测试服务器 → 手工设置 rollout 修改时间 → 用 model override 恢复 → 确认 updated_at 和 mtime 不变 → 开始新 turn 后 mtime 才变新。

调用关系:它复用 start_materialized_thread_and_restart 和 set_rollout_mtime,补充 thread_resume_defers_updated_at_until_turn_start 的覆盖参数场景。

调用图:调用 3 个内部函数(create_config_toml, set_rollout_mtime, start_materialized_thread_and_restart);外部调用 9 个(default, new, Integer, create_mock_responses_server_repeating_assistant, assert!, assert_eq!, metadata, timeout, vec!)。

thread_resume_fails_when_required_mcp_server_fails_to_initialize3233–3268 ↗
async fn thread_resume_fails_when_required_mcp_server_fails_to_initialize() -> Result<()>

作用:验证恢复线程时,如果必需的 MCP 服务器启动失败,恢复会失败并说明是哪一个。MCP 可以理解为外部工具服务,必需工具不可用时不能假装成功。

数据流:先准备正常 rollout → 再写入一个 required=true 但命令不存在的 MCP 配置 → 请求恢复 → 收到错误,内容包含 required MCP 初始化失败和 required_broken 名称。

调用关系:它调用 setup_rollout_fixture 准备历史,再用 create_config_toml_with_required_broken_mcp 覆盖配置,测试恢复前的工具初始化检查。

调用图:调用 3 个内部函数(new, create_config_toml_with_required_broken_mcp, setup_rollout_fixture);外部调用 6 个(default, new, Integer, create_mock_responses_server_repeating_assistant, assert!, timeout)。

thread_resume_surfaces_cloud_config_bundle_load_errors3271–3360 ↗
async fn thread_resume_surfaces_cloud_config_bundle_load_errors() -> Result<()>

作用:验证恢复线程时云端配置包加载失败,会把可操作的错误信息传给客户端。比如登录令牌失效时,客户端应该知道要用户重新登录。

数据流:测试搭一个返回 401 的假 ChatGPT 后端和 oauth/token → 写入过期的 ChatGPT 登录信息 → 请求恢复 → 错误 message 包含配置加载失败,data 里包含 relogin、401 和详细说明。

调用关系:它用 wiremock 模拟云服务错误,并通过 TestAppServer::new_with_env 控制环境变量,检查错误透传而不是被吞掉。

调用图:调用 3 个内部函数(new, new_with_env, create_config_toml_with_chatgpt_base_url);外部调用 17 个(default, given, start, new, new, new, Integer, create_fake_rollout_with_text_elements, create_mock_responses_server_repeating_assistant, write_chatgpt_auth (+7 more))。

thread_resume_uses_path_over_non_running_thread_id3363–3394 ↗
async fn thread_resume_uses_path_over_non_running_thread_id() -> Result<()>

作用:验证当线程不在运行中时,如果请求同时给了错误 thread_id 和正确 path,系统会按 path 里的真实线程恢复。这样从外部文件打开历史更可靠。

数据流:先创建并落盘线程,再重启测试服务器 → thread/resume 传一个新随机 thread_id,但 path 指向真实 rollout → 返回的线程 id 是文件里的原线程 id。

调用关系:它复用 start_materialized_thread_and_restart,测试非运行态恢复时 path 的优先级。

调用图:调用 3 个内部函数(create_config_toml, start_materialized_thread_and_restart, new);外部调用 6 个(default, new, Integer, create_mock_responses_server_repeating_assistant, assert_eq!, timeout)。

thread_resume_can_load_source_by_external_path3397–3440 ↗
async fn thread_resume_can_load_source_by_external_path() -> Result<()>

作用:验证 app server 可以从当前 codex_home 之外的历史文件路径恢复线程。这样用户能打开外部保存的会话文件。

数据流:在 external_home 里造 fake rollout,而 app server 使用另一个 codex_home → thread/resume 传外部 path 和无效 id → 返回外部文件里的线程、预览、绝对路径和 Idle 状态。

调用关系:它调用 normalized_existing_path 比较路径等价性,检查恢复逻辑不只局限于默认会话目录。

调用图:调用 2 个内部函数(new, create_config_toml);外部调用 8 个(default, new, Integer, create_fake_rollout, create_mock_responses_server_repeating_assistant, rollout_path, assert_eq!, timeout)。

thread_resume_supports_history_and_overrides3443–3489 ↗
async fn thread_resume_supports_history_and_overrides() -> Result<()>

作用:验证恢复时可以显式传入新的 history,并覆盖模型和模型提供方。这样客户端可以用保存外的历史内容重建一个线程视图。

数据流:先创建并重启一个已落盘线程 → 构造一条用户消息作为 history,并指定 mock-model 和 mock_provider → 恢复后返回线程预览来自新 history,provider 也是覆盖值。

调用关系:它依赖 start_materialized_thread_and_restart 准备一个可恢复线程,测试 thread/resume 的 history 和 override 参数组合。

调用图:调用 2 个内部函数(create_config_toml, start_materialized_thread_and_restart);外部调用 8 个(default, new, Integer, create_mock_responses_server_repeating_assistant, assert!, assert_eq!, timeout, vec!)。

start_materialized_thread_and_restart3498–3570 ↗
async fn start_materialized_thread_and_restart(
    codex_home: &Path,
    seed_text: &str,
) -> Result<RestartedThreadFixture>

作用:创建一个真实落盘的测试线程,然后关闭第一个测试服务器并启动第二个。它模拟“应用重启后恢复旧线程”的场景。

数据流:输入 codex_home 和种子文本 → 第一个 TestAppServer start 线程、发送 turn 让 rollout 文件生成、读取线程信息 → 丢弃第一个服务器,启动第二个 → 输出 mcp、thread_id、rollout 路径和 updated_at。

调用关系:它是多个恢复测试的夹具,被 thread_resume_supports_history_and_overrides、thread_resume_uses_path_over_non_running_thread_id 和 thread_resume_with_overrides_defers_updated_at_until_turn_start 复用。

调用图:调用 1 个内部函数(new);被 3 处调用(thread_resume_supports_history_and_overrides, thread_resume_uses_path_over_non_running_thread_id, thread_resume_with_overrides_defers_updated_at_until_turn_start);外部调用 4 个(default, Integer, timeout, vec!)。

thread_resume_accepts_personality_override3573–3690 ↗
async fn thread_resume_accepts_personality_override() -> Result<()>

作用:验证恢复线程时可以覆盖 personality,也就是助手说话风格,并且下一次模型请求会带上风格说明。这样用户恢复旧会话后仍能切换助手语气。

数据流:启动线程并完成种子回合 → 第二个客户端用 Personality::Friendly 恢复 → 再发新 turn → 检查发给模型的 developer 输入包含 personality_spec,同时基础 instructions 仍来自历史默认值。

调用关系:它需要网络条件才运行,使用 mock SSE 序列捕获模型请求内容,测试恢复参数如何影响下一轮模型输入。

调用图:调用 5 个内部函数(new, create_config_toml, mount_sse_sequence, sse, start_mock_server);外部调用 8 个(default, new, Integer, assert!, assert_eq!, skip_if_no_network!, timeout, vec!)。

create_config_toml3693–3717 ↗
fn create_config_toml(codex_home: &std::path::Path, server_uri: &str) -> std::io::Result<()>

作用:写一个最基础的测试配置文件,让 app server 把模型请求发到假服务器。没有它,测试服务器不知道该用哪个模型提供方。

数据流:输入 codex_home 和假服务器地址 → 拼出 config.toml 路径和 TOML 文本 → 写入 model、审批策略、沙箱、feature 和 mock_provider 配置。

调用关系:它被本文件大量测试和夹具调用,是最常用的测试环境准备函数。

调用图:被 31 处调用(resume_redaction_fixture, setup_rollout_fixture, thread_goal_get_rejects_unmaterialized_thread, thread_goal_set_edits_objective_without_resetting_usage, thread_goal_set_persists_resumable_stopped_statuses, thread_goal_set_preserves_budget_limited_same_objective, thread_resume_accepts_personality_override, thread_resume_and_read_interrupt_incomplete_rollout_turn_when_thread_is_idle, thread_resume_can_load_source_by_external_path, thread_resume_can_skip_turns_for_metadata_only_resume (+15 more));外部调用 3 个(join, format!, write)。

create_config_toml_with_chatgpt_base_url3719–3748 ↗
fn create_config_toml_with_chatgpt_base_url(
    codex_home: &std::path::Path,
    server_uri: &str,
    chatgpt_base_url: &str,
) -> std::io::Result<()>

作用:写一个带 ChatGPT 后端地址的测试配置。它用于需要分析上报或云端配置加载的测试。

数据流:输入 codex_home、模型服务器地址和 chatgpt_base_url → 生成 config.toml 内容 → 写入本地 mock 模型 provider 和 ChatGPT base URL。

调用关系:它被分析事件测试和云配置错误测试调用,让同一个假服务器或指定服务器接收相关请求。

调用图:被 3 处调用(thread_goal_lifecycle_emits_analytics_and_clear_deletes_goal, thread_resume_surfaces_cloud_config_bundle_load_errors, thread_resume_tracks_thread_initialized_analytics);外部调用 3 个(join, format!, write)。

create_config_toml_with_required_broken_mcp3750–3781 ↗
fn create_config_toml_with_required_broken_mcp(
    codex_home: &std::path::Path,
    server_uri: &str,
) -> std::io::Result<()>

作用:写一个故意坏掉的必需 MCP 工具服务器配置。它用来测试恢复线程时遇到必需工具启动失败会不会正确报错。

数据流:输入 codex_home 和模型服务器地址 → 写入正常模型配置,再加一个 command 不存在且 required=true 的 mcp_servers.required_broken → 生成 config.toml。

调用关系:它只被 thread_resume_fails_when_required_mcp_server_fails_to_initialize 调用,是那个失败场景的配置制造器。

调用图:被 1 处调用(thread_resume_fails_when_required_mcp_server_fails_to_initialize);外部调用 3 个(join, format!, write)。

set_rollout_mtime3784–3792 ↗
fn set_rollout_mtime(path: &Path, updated_at_rfc3339: &str) -> Result<()>

作用:把 rollout 历史文件的修改时间设置成指定时间。测试用它来判断恢复线程有没有偷偷改文件。

数据流:输入文件路径和 RFC3339 时间字符串 → 解析成时间对象 → 用文件时间 API 设置该文件的 modified time → 文件内容不变,只改时间戳。

调用关系:它被 setup_rollout_fixture 和 thread_resume_with_overrides_defers_updated_at_until_turn_start 使用,为 updated_at/mtime 相关测试提供可控时间。

调用图:被 2 处调用(setup_rollout_fixture, thread_resume_with_overrides_defers_updated_at_until_turn_start);外部调用 3 个(new, parse_from_rfc3339, new)。

setup_rollout_fixture3800–3828 ↗
async fn setup_rollout_fixture(codex_home: &Path, server_uri: &str) -> Result<RolloutFixture>

作用:准备一份带配置、假历史、固定修改时间和多代理版本标记的 rollout 测试材料。它让多个恢复测试不用重复造同样的数据。

数据流:输入 codex_home 和模型服务器地址 → 写配置、创建 fake rollout、读取并追加 session meta、设置文件修改时间 → 输出 conversation_id、rollout_file_path 和修改前时间。

调用关系:它调用 create_config_toml、set_rollout_mtime、read_session_meta_line 和 append_rollout_item_to_path,被恢复时间戳测试和必需 MCP 失败测试复用。

调用图:调用 2 个内部函数(create_config_toml, set_rollout_mtime);被 2 处调用(thread_resume_defers_updated_at_until_turn_start, thread_resume_fails_when_required_mcp_server_fails_to_initialize);外部调用 7 个(new, create_fake_rollout_with_text_elements, rollout_path, append_rollout_item_to_path, read_session_meta_line, SessionMeta, metadata)。

线程状态变更

这些套件覆盖会改变已持久化线程状态的生命周期操作,包括归档、删除、分支、回滚和按线程更新配置。

app-server/tests/suite/v2/thread_archive.rs源码 ↗
testtest run

这里测试的是线程归档功能。可以把一个线程想成一段聊天记录,把 rollout 文件想成这段聊天真正落到硬盘上的存档。这个文件会启动一个假的应用服务器和假的模型服务,然后像真实客户端一样发送“开始线程”“发一轮消息”“归档”“取消归档”“恢复线程”等请求。它重点检查几类容易出错的情况:新建但还没写入硬盘的线程不能归档;归档父线程时,派生出来的子线程、孙线程也要一起处理;某个后代归档失败或已经丢失时,主线程不应该被拖垮;归档再恢复后,旧客户端不能继续收到不该收到的通知。辅助函数负责写测试配置、生成配置内容,以及确认两个路径在硬盘上其实指向同一个文件。

函数细节8
thread_archive_requires_materialized_rollout36–168 ↗
async fn thread_archive_requires_materialized_rollout() -> Result<()>

作用:这个测试确认:一个刚创建、还没有真正写成聊天存档文件的线程,不能被归档。只有用户真正发过一轮消息、文件落到硬盘后,归档才应该成功。

数据流:测试先创建临时目录和假模型服务,写入配置,再启动测试服务器。它创建一个线程,拿到线程编号和预期文件路径,但确认这个文件还不存在;接着发送归档请求,期望收到“找不到 rollout”的错误。然后它发一条用户消息,让线程真正写入硬盘,再次查找路径并确认路径正确。最后重新归档,收到成功响应和 thread/archived 通知,并检查原文件已经从活动目录移到归档目录。

调用关系:这是归档流程最基础的门槛测试。它会用 create_config_toml 准备服务器配置,用 assert_paths_match_on_disk 确认查到的文件就是预期文件;中间通过 TestAppServer 像客户端一样驱动应用服务器,并通过 find_thread_path_by_id_str 检查硬盘上的状态。

调用图:调用 3 个内部函数(new, assert_paths_match_on_disk, create_config_toml);外部调用 10 个(default, new, Integer, create_mock_responses_server_repeating_assistant, assert!, assert_eq!, find_thread_path_by_id_str, from_value, timeout, vec!)。

thread_archive_archives_spawned_descendants171–279 ↗
async fn thread_archive_archives_spawned_descendants() -> Result<()>

作用:这个测试确认:归档一个父线程时,它派生出来的子线程和孙线程也会一起归档。这样用户收起一棵会话分支时,不会只移动根节点,留下后代散在活动列表里。

数据流:测试先造出三个假的聊天存档文件:父、子、孙。然后在状态数据库里写入它们的派生关系,也就是“父生成子,子生成孙”。服务器启动后,它请求归档父线程。结果应该收到一次成功响应,并连续收到三个归档通知。最后它逐个检查三个线程:活动位置已经找不到,归档位置能找到。

调用关系:这个测试把文件系统里的 rollout 文件和 StateRuntime 里的父子关系连起来验证。create_config_toml 负责准备配置,create_fake_rollout 负责造假存档,StateRuntime 记录派生边;归档请求发给 TestAppServer 后,测试观察通知顺序和硬盘移动结果。

调用图:调用 4 个内部函数(new, create_config_toml, from_string, init);外部调用 9 个(new, new, Integer, create_fake_rollout, create_mock_responses_server_repeating_assistant, assert!, assert_eq!, from_value, timeout)。

thread_archive_succeeds_when_descendant_archive_fails282–417 ↗
async fn thread_archive_succeeds_when_descendant_archive_fails() -> Result<()>

作用:这个测试确认:如果归档某个后代线程时遇到文件冲突,整个父线程归档请求仍然可以成功。它防止一个坏掉的子分支把其他能归档的线程全部卡死。

数据流:测试先创建父、子、孙三个存档,并在状态数据库里写好它们的派生关系。然后它故意在归档目录里给子线程制造一个同名目录,模拟“目标位置已经被占用,子线程没法移动”的冲突。发起归档父线程后,测试期望请求本身成功,只收到父和孙的归档通知,不收到子的通知。最后它检查子线程仍留在活动位置,冲突目录还在,而父和孙已经进入归档位置。

调用关系:这个测试专门压测归档流程的容错能力。它和前一个后代归档测试类似,也用 create_config_toml、create_fake_rollout 和 StateRuntime 搭场景,但额外用文件系统制造失败条件,再通过 TestAppServer 观察服务器是否跳过失败后代、继续完成其他归档。

调用图:调用 4 个内部函数(new, create_config_toml, from_string, init);外部调用 11 个(new, new, Integer, create_fake_rollout, create_mock_responses_server_repeating_assistant, assert!, assert_eq!, find_thread_path_by_id_str, from_value, create_dir_all (+1 more))。

thread_archive_succeeds_when_spawned_descendant_is_missing420–494 ↗
async fn thread_archive_succeeds_when_spawned_descendant_is_missing() -> Result<()>

作用:这个测试确认:如果状态数据库里说有一个子线程,但硬盘上已经找不到它,归档父线程仍然应该成功。这样旧数据或残缺数据不会让用户无法归档正常存在的线程。

数据流:测试只创建父线程的假存档,然后在状态数据库里写一条父线程指向不存在子线程的派生关系。服务器启动后,它请求归档父线程。结果应该收到成功响应和父线程的归档通知。最后测试确认父线程已经从活动目录消失,并能在归档目录里找到。

调用关系:这个测试验证归档流程遇到“记录还在、文件没了”的脏数据时怎么处理。它用 create_config_toml 准备配置,用 create_fake_rollout 只创建父文件,用 StateRuntime 写入缺失后代的关系;随后交给 TestAppServer 发归档请求并检查结果。

调用图:调用 4 个内部函数(new, create_config_toml, from_string, init);外部调用 8 个(new, Integer, create_fake_rollout, create_mock_responses_server_repeating_assistant, assert!, assert_eq!, from_value, timeout)。

thread_archive_clears_stale_subscriptions_before_resume497–630 ↗
async fn thread_archive_clears_stale_subscriptions_before_resume() -> Result<()>

作用:这个测试确认:线程归档再取消归档后,旧服务器连接留下的订阅会被清掉。否则一个已经不该监听该线程的客户端,可能收到别的客户端继续聊天时的通知。

数据流:测试启动第一个客户端连接,创建线程并发一条消息,让线程有真实存档。然后启动第二个客户端连接。第一个连接把线程归档,再取消归档;第二个连接恢复这个线程,并发起新一轮对话。测试随后确认第一个连接在短时间内收不到 turn/started 通知,而第二个连接能正常收到 turn/completed 通知。

调用关系:这个测试关注的不是文件有没有移动,而是通知订阅有没有清理干净。它用 create_config_toml 搭环境,通过两个 TestAppServer 实例模拟两个客户端;归档、取消归档、恢复和发消息串起来后,检查通知只发给当前真正恢复线程的连接。

调用图:调用 2 个内部函数(new, create_config_toml);外部调用 8 个(default, new, Integer, create_mock_responses_server_repeating_assistant, assert!, assert_eq!, timeout, vec!)。

create_config_toml632–635 ↗
fn create_config_toml(codex_home: &Path, server_uri: &str) -> std::io::Result<()>

作用:这个小工具函数给每个测试写一份 config.toml 配置文件。没有它,测试服务器就不知道该用哪个假模型服务、用什么模型和运行策略。

数据流:它接收一个临时的 codex_home 目录和假服务器地址。它把目录和 config.toml 文件名拼成完整路径,再调用 config_contents 生成配置文本,最后把文本写到硬盘。结果是临时目录里多出一份测试用配置文件;如果写文件失败,就把错误返回给测试。

调用关系:所有主要测试在启动 TestAppServer 之前都会调用它。它自己不关心测试场景细节,只负责把 config_contents 产出的文字落盘,让后面的服务器初始化能读到正确设置。

调用图:调用 1 个内部函数(config_contents);被 5 处调用(thread_archive_archives_spawned_descendants, thread_archive_clears_stale_subscriptions_before_resume, thread_archive_requires_materialized_rollout, thread_archive_succeeds_when_descendant_archive_fails, thread_archive_succeeds_when_spawned_descendant_is_missing);外部调用 2 个(join, write)。

config_contents637–653 ↗
fn config_contents(server_uri: &str) -> String

作用:这个函数生成测试用的配置文件内容。它把假模型服务器的地址塞进配置里,让测试不会访问真实网络服务。

数据流:它接收一个服务器地址字符串,把它填进一段 TOML 配置文本中。输出是一整段字符串,里面指定模型名、审批策略、沙盒模式、模型提供方名称、接口地址,以及重试次数为 0 等测试设置。它不写文件,只返回文字。

调用关系:它只被 create_config_toml 调用。create_config_toml 负责写文件,它负责“配置文件里应该写什么”,两者配合完成测试服务器启动前的准备工作。

调用图:被 1 处调用(create_config_toml);外部调用 1 个(format!)。

assert_paths_match_on_disk655–660 ↗
fn assert_paths_match_on_disk(actual: &Path, expected: &Path) -> std::io::Result<()>

作用:这个小工具用来确认两个路径在硬盘上其实指向同一个文件。它避免因为相对路径、符号链接等路径写法不同,导致测试误判。

数据流:它接收两个路径:实际查到的路径和预期路径。它分别把两个路径 canonicalize,也就是转换成系统认可的绝对真实路径,然后比较两者是否相等。相等就返回成功;不相等会让测试断言失败;如果路径转换失败,则返回文件系统错误。

调用关系:它被 thread_archive_requires_materialized_rollout 使用。那个测试在 rollout 文件真正生成后,会用它确认“按线程编号查到的存档文件”和“线程启动时给出的存档路径”是同一个硬盘文件。

调用图:被 1 处调用(thread_archive_requires_materialized_rollout);外部调用 2 个(canonicalize, assert_eq!)。

app-server/tests/suite/v2/thread_delete.rs源码 ↗
testtest run

这里的“线程”可以理解成一次对话或任务记录,“rollout”就是它保存到磁盘上的记录文件。这个测试文件像是在模拟用户真实操作:先造出一些假的线程记录,再启动测试用的应用服务器,通过 JSON-RPC(一种用 JSON 格式发请求、收回复的通信方式)发送“删除线程”的命令。第一个测试检查父线程被删除时,它生出来的子线程、孙线程也要一起删,而且通知顺序是先删最底下的孙线程,再删子线程,最后删父线程,避免留下断掉的关系。第二个测试检查线程还没保存成文件时的删除行为:普通已持久化线程可以删;临时线程因为本来就不该落盘,所以删除它会返回清楚的错误,并且它仍然留在当前加载列表里。这个文件重要在于防止删除功能只删表面文件,却留下状态数据库里的脏关系,或误删还没保存好的运行中线程。

函数细节3
thread_delete_deletes_spawned_descendants27–108 ↗
async fn thread_delete_deletes_spawned_descendants() -> Result<()>

作用:这个异步测试用来确认:删除一个父线程时,它派生出来的子线程和孙线程也会被一起删除。它还确认删除通知的顺序是从最深的后代开始,最后才轮到父线程。

数据流:它先创建一个临时目录当作假的应用数据目录,再用 helper 造出父、子、孙三个假的线程保存记录。接着它把三者之间的“派生关系”写进状态数据库:父生子,子生孙。然后启动测试服务器,发送删除父线程的请求,读取服务器返回的成功响应和三条 thread/deleted 通知。最后它检查三件事:通知里的 ID 顺序是孙、子、父;磁盘上已经找不到这三个线程的保存记录;状态数据库里也不再有父线程的后代关系。

调用关系:这是整个文件里最完整的删除链路测试。它会调用 create_delete_test_rollout 准备假的磁盘数据,再通过 TestAppServer 模拟客户端和服务器通信。服务器内部真正执行删除后,这个测试用 find_thread_path_by_id_str 和状态数据库查询来验证结果没有遗漏。

调用图:调用 4 个内部函数(new, create_delete_test_rollout, from_string, init);外部调用 8 个(new, new, Integer, assert!, assert_eq!, find_thread_path_by_id_str, from_value, timeout)。

create_delete_test_rollout110–119 ↗
fn create_delete_test_rollout(codex_home: &Path, minute: u8, preview: &str) -> Result<String>

作用:这个小工具函数用来快速创建一个假的线程保存记录,给删除测试当测试材料。它把时间、预览文字和固定的 mock provider 填好,避免主测试里堆太多准备代码。

数据流:它接收一个数据目录、一个分钟数和一段预览文字。函数把分钟数拼成文件时间戳和显示时间,再交给 create_fake_rollout 去实际创建假的 rollout 文件。最后它返回新建线程的 ID 字符串,供测试继续建立关系或发送删除请求。

调用关系:它只被 thread_delete_deletes_spawned_descendants 使用。它的角色像测试里的“道具工”:主测试需要父、子、孙三个线程记录时,就通过它统一造出来,而真正写文件的细节交给外部的 create_fake_rollout。

调用图:被 1 处调用(thread_delete_deletes_spawned_descendants);外部调用 2 个(create_fake_rollout, format!)。

thread_delete_handles_live_threads_before_rollout_exists122–200 ↗
async fn thread_delete_handles_live_threads_before_rollout_exists() -> Result<()>

作用:这个异步测试检查一种边界情况:线程已经被服务器创建并加载在内存里,但磁盘上的保存文件还不存在时,删除请求是否表现正确。它也确认临时线程不能被当作可持久化线程删除。

数据流:它先在空的临时目录里启动测试服务器,然后发送“开始线程”请求,拿到一个普通线程 ID,并确认磁盘上还找不到对应 rollout 文件。随后它发送删除请求,确认服务器能正常返回成功。接着它再创建一个 ephemeral 临时线程,也就是只在当前运行期间存在、不写入长期存储的线程;对这个线程发送删除请求时,服务器应该返回 JSON-RPC 错误,错误消息说明“线程没有持久化,不能删除”。最后它请求当前已加载线程列表,确认这个临时线程仍在列表里。

调用关系:这个测试从客户端视角走完整的启动、删除、报错、列出线程流程。它依靠 TestAppServer 发送 thread/start、thread/delete 和 thread/loaded/list 请求,并用 find_thread_path_by_id_str 确认磁盘状态,用返回的 JSON-RPC 响应或错误来判断服务器行为是否符合预期。

调用图:调用 1 个内部函数(new);外部调用 9 个(default, new, Integer, default, default, assert_eq!, find_thread_path_by_id_str, format!, timeout)。

app-server/tests/suite/v2/thread_fork.rs源码 ↗
testtest run

这里不是产品代码,而是一组自动化测试。它会搭一个假的应用服务器、假的模型接口和临时配置目录,然后伪造已有的对话记录文件,再向服务器发送 thread/fork 请求。所谓“fork”,可以理解成把一段旧聊天另开一个分支:新线程要有新身份,但要知道自己从哪个旧线程来;可以复制历史,也可以不复制;可以持久保存,也可以只是临时存在。这个文件重点防止一些容易出错的地方:不能改坏原始记录;返回给客户端的字段名要稳定;该发的 thread/started 通知要发,不该提前发状态变化;令牌用量、分析埋点、云配置错误也要正确处理。它还测试了路径输入、空路径、目录路径、未落盘线程等边界情况,保证前端或插件拿到的行为一致、可预期。

函数细节14
list_threads61–83 ↗
async fn list_threads(mcp: &mut TestAppServer) -> Result<ThreadListResponse>

作用:这个小工具函数帮测试代码向测试服务器要一份线程列表。它把“发送请求、等待回复、把回复转成结构化结果”这几步包起来,避免每个测试重复写。

数据流:进去的是一个正在运行的 TestAppServer 测试服务器连接。它发送 thread/list 请求,最多等待一段固定时间,读到对应请求编号的 JSON-RPC 回复后,把回复转换成 ThreadListResponse。出来的是可直接检查的线程列表;服务器本身不会被它改动,只是被查询了一次。

调用关系:它被需要检查“列表里有没有某个线程”的测试调用,比如确认 fork 后继承名称、确认临时 fork 不出现在列表里。它把具体通信交给 send_thread_list_request 和 read_stream_until_response_message。

调用图:调用 2 个内部函数(read_stream_until_response_message, send_thread_list_request);被 2 处调用(thread_fork_ephemeral_remains_pathless_and_omits_listing, thread_fork_inherits_explicit_source_name_from_session_index);外部调用 2 个(Integer, timeout)。

thread_fork_creates_new_thread_and_emits_started86–251 ↗
async fn thread_fork_creates_new_thread_and_emits_started() -> Result<()>

作用:这个测试检查最核心的 fork 行为:从已有对话复制出一个新线程,并通知客户端“新线程已开始”。它还确认旧文件没有被改坏。

数据流:测试先创建假模型服务器、临时配置和一份假的历史对话文件,再启动测试应用服务器。随后发送 thread/fork 请求,读取返回结果,检查新线程的编号、来源、预览文本、模型提供方、状态、路径、复制出来的回合内容,以及 JSON 字段是否按协议输出。最后它继续读通知流,确认收到了 thread/started,并且通知里没有把复制历史重复塞进去。

调用关系:这是整组测试的主场景。它使用 create_config_toml 准备配置,使用测试辅助函数创建假 rollout 文件,通过 TestAppServer 发请求和读消息,用来验证服务器真正的 fork 流程和通知流程。

调用图:调用 2 个内部函数(new, create_config_toml);外部调用 18 个(default, new, bail!, Integer, create_fake_rollout, create_mock_responses_server_repeating_assistant, assert!, assert_eq!, assert_ne!, append_rollout_item_to_path (+8 more))。

thread_fork_inherits_explicit_source_name_from_session_index254–295 ↗
async fn thread_fork_inherits_explicit_source_name_from_session_index() -> Result<()>

作用:这个测试确认:如果父线程在索引里有用户改过的名字,fork 出来的线程在列表中也能显示这个名字。它防止“复制后列表标题丢失”的问题。

数据流:测试先造出一份父线程记录,再把父线程名字写进线程索引。启动服务器并发送 fork 请求后,它拿到新线程编号,再调用 list_threads 查询列表,找到新线程并检查它的 name 是否等于父线程的显式名字。

调用关系:它依赖 create_config_toml 准备测试配置,也调用 list_threads 做最终验证。它关注的是 fork 之后和线程列表索引之间的配合,而不是模型回复内容。

调用图:调用 4 个内部函数(new, create_config_toml, list_threads, from_string);外部调用 8 个(default, new, Integer, create_fake_rollout, create_mock_responses_server_repeating_assistant, assert_eq!, append_thread_name, timeout)。

thread_fork_can_load_source_by_path298–346 ↗
async fn thread_fork_can_load_source_by_path() -> Result<()>

作用:这个测试确认:即使传入的 thread_id 不是合法线程编号,只要提供了正确的历史文件路径,服务器仍然能从这个路径加载源线程并 fork。它保证客户端在只知道文件路径时也能工作。

数据流:测试创建一份假的历史对话文件,并算出它在磁盘上的路径。发送 fork 请求时故意填一个无效 thread_id,同时提供 path。服务器返回新线程后,测试检查它不是原线程、能记录 fork 来源、保留预览文本和模型提供方,并复制了历史回合。

调用关系:它使用 create_config_toml 和假 rollout 文件搭环境,然后走正常的 thread/fork 请求通道。它验证的是服务器选择“按路径加载源记录”的分支。

调用图:调用 2 个内部函数(new, create_config_toml);外部调用 9 个(default, new, Integer, create_fake_rollout, create_mock_responses_server_repeating_assistant, assert_eq!, assert_ne!, format!, timeout)。

thread_fork_emits_restored_token_usage_before_next_turn349–400 ↗
async fn thread_fork_emits_restored_token_usage_before_next_turn() -> Result<()>

作用:这个测试确认:fork 一个带有历史令牌用量的线程时,服务器会先把这些用量重新通知给客户端。令牌用量就是模型输入输出消耗的计数,常用于计费或显示上下文占用。

数据流:测试创建一份带 token usage 的假历史记录,启动服务器并 fork。拿到新线程后,它等待 thread/tokenUsage/updated 通知,再检查通知里的线程编号、回合编号、总令牌数、输入输出令牌数、缓存输入、推理输出和上下文窗口大小都恢复正确。

调用关系:它通过 create_fake_rollout_with_token_usage 准备特殊历史数据,通过 thread/fork 触发恢复流程。它验证 fork 完成后、下一次对话开始前,服务器会主动把旧用量广播出来。

调用图:调用 2 个内部函数(new, create_config_toml);外部调用 8 个(default, new, Integer, create_fake_rollout_with_token_usage, create_mock_responses_server_repeating_assistant, assert_eq!, panic!, timeout)。

thread_fork_can_exclude_turns_and_skip_restored_token_usage403–448 ↗
async fn thread_fork_can_exclude_turns_and_skip_restored_token_usage() -> Result<()>

作用:这个测试确认 excludeTurns=true 的行为:fork 时只建立新分支,不复制旧回合,也不重放旧的令牌用量通知。它防止“用户要求空分支却仍带上旧历史”的问题。

数据流:测试创建一份带令牌用量的历史记录,发送 thread/fork 请求并设置 exclude_turns。返回后检查新线程仍知道自己从哪个线程 fork 来,也保留预览文字,但 turns 为空。然后它等待 token usage 通知,期望等不到,说明服务器没有重放旧用量。

调用关系:它和令牌恢复测试形成对照:一个验证默认会恢复,一个验证排除回合时不会恢复。它使用 create_config_toml 和假模型服务器搭起同样的测试环境。

调用图:调用 2 个内部函数(new, create_config_toml);外部调用 8 个(default, new, Integer, create_fake_rollout_with_token_usage, create_mock_responses_server_repeating_assistant, assert!, assert_eq!, timeout)。

thread_fork_tracks_thread_initialized_analytics451–502 ↗
async fn thread_fork_tracks_thread_initialized_analytics() -> Result<()>

作用:这个测试确认 fork 新线程时会发送“线程初始化”的分析事件。分析事件可以理解成产品埋点,用来统计功能是否被使用、来源是什么。

数据流:测试启动带分析捕获能力的假服务器,写入带 ChatGPT 基础地址的配置,创建历史线程,然后执行 fork。拿到新线程后,它等待服务端发来的分析 payload,从里面解析 thread initialized 事件,检查线程编号、会话编号、模型、来源类型 forked、用户来源 user,以及父线程编号是否正确。

调用关系:它调用 create_config_toml_with_chatgpt_base_url 准备会触发云端/分析路径的配置,并使用 analytics 测试辅助函数挂载捕获器、等待和校验事件。它验证 fork 流程和统计上报系统的连接。

调用图:调用 6 个内部函数(new_without_managed_config, assert_basic_thread_initialized_event, mount_analytics_capture, thread_initialized_event, wait_for_analytics_payload, create_config_toml_with_chatgpt_base_url);外部调用 7 个(default, new, Integer, create_fake_rollout, create_mock_responses_server_repeating_assistant, assert_eq!, timeout)。

thread_fork_rejects_unmaterialized_thread505–547 ↗
async fn thread_fork_rejects_unmaterialized_thread() -> Result<()>

作用:这个测试确认:刚创建但还没有保存成历史记录文件的线程,不能被 fork。这样可以避免服务器从不存在的记录里复制内容。

数据流:测试先启动服务器并发送 thread/start 创建一个新线程,但不让它产生可读取的 rollout 历史文件。接着请求 fork 这个线程。结果应该不是成功回复,而是 JSON-RPC 错误,错误消息里说明找不到这个线程的 rollout。

调用关系:它先走 thread/start,再走 thread/fork,用来验证 fork 对源数据“必须已经落盘”的要求。它依赖 create_config_toml 准备普通测试配置。

调用图:调用 2 个内部函数(new, create_config_toml);外部调用 6 个(default, new, Integer, create_mock_responses_server_repeating_assistant, assert!, timeout)。

thread_fork_with_empty_path_uses_thread_id550–587 ↗
async fn thread_fork_with_empty_path_uses_thread_id() -> Result<()>

作用:这个测试确认:如果 path 字段给了一个空路径,服务器不会把它当成真正文件路径,而是退回用 thread_id 查找源线程。它防止空路径误导加载逻辑。

数据流:测试创建一份正常历史记录,发送 fork 请求时同时传入正确 thread_id 和一个空 PathBuf。返回后检查新线程的 forked_from_id 仍然是原来的 conversation_id,说明服务器按线程编号找到了源记录。

调用关系:它覆盖路径参数的边界情况。它和“按路径加载源线程”的测试互补:一个说明有效路径可用,另一个说明空路径应被忽略。

调用图:调用 2 个内部函数(new, create_config_toml);外部调用 8 个(default, new, Integer, create_fake_rollout, create_mock_responses_server_repeating_assistant, assert_eq!, new, timeout)。

thread_fork_surfaces_cloud_config_bundle_load_errors590–683 ↗
async fn thread_fork_surfaces_cloud_config_bundle_load_errors() -> Result<()>

作用:这个测试确认:fork 时如果加载云端配置失败,错误会清楚地返回给客户端,而不是变成模糊的内部错误。这里还特别检查登录失效时会提示重新登录。

数据流:测试搭一个假的云端服务器,让配置包接口返回 401,并让刷新 token 的接口也返回“刷新令牌失效”。它写入 ChatGPT 登录信息和配置,再带环境变量启动应用服务器。发送 fork 请求后,测试读取错误回复,检查消息包含“加载配置失败”,并且 error data 里有原因 cloudConfigBundle、错误类型 Auth、动作 relogin、状态码 401 和友好的说明文字。

调用关系:它使用 create_config_toml_with_chatgpt_base_url 和 write_chatgpt_auth 进入需要云配置的路径,也用 wiremock 模拟远端接口失败。它验证 fork 流程遇到配置加载失败时,错误能沿 JSON-RPC 返回给调用方。

调用图:调用 3 个内部函数(new, new_with_env, create_config_toml_with_chatgpt_base_url);外部调用 16 个(default, given, start, new, new, Integer, create_fake_rollout, create_mock_responses_server_repeating_assistant, write_chatgpt_auth, assert! (+6 more))。

thread_fork_ephemeral_remains_pathless_and_omits_listing686–837 ↗
async fn thread_fork_ephemeral_remains_pathless_and_omits_listing() -> Result<()>

作用:这个测试检查临时 fork,也就是 ephemeral=true。临时线程应该能继续对话,但不暴露磁盘路径,也不出现在线程列表里。

数据流:测试创建一份父线程记录,然后请求 ephemeral fork。它检查返回的新线程标记为 ephemeral,path 是 None,预览、状态、历史回合和序列化字段正确。接着等待 thread/started 通知,确认通知也带 ephemeral=true,但不带复制的 turns。然后调用 list_threads,确认临时线程不在列表里而父线程还在。最后它在临时线程上发起一个新 turn,并等待完成,证明临时线程虽然不入列表,仍可正常运行。

调用关系:这是临时线程行为的综合测试。它调用 list_threads 验证列表侧效果,也通过 turn/start 检查 fork 出来的临时线程能继续进入正常对话流程。

调用图:调用 3 个内部函数(new, create_config_toml, list_threads);外部调用 13 个(default, new, bail!, Integer, create_fake_rollout, create_mock_responses_server_repeating_assistant, assert!, assert_eq!, panic!, from_value (+3 more))。

pathless_ephemeral_thread_rejects_codex_home_path_after_reload840–951 ↗
async fn pathless_ephemeral_thread_rejects_codex_home_path_after_reload() -> Result<()>

作用:这个测试确认:服务器重启后,如果有人拿一个目录路径去恢复或 fork 一个原本没有路径的临时线程,服务器会立刻拒绝。这样可以避免把目录当成历史文件读取,产生难懂的系统错误。

数据流:测试先创建父线程,再启动服务器 fork 出一个无路径的临时线程,并在它上面完成一次对话。随后丢掉这个服务器实例,重新启动一个新服务器。它分别尝试用 codex_home 目录作为 path 去 resume 和 fork 那个临时线程,两个请求都应该返回错误,错误要说“path is a directory”,并且不能泄漏底层操作系统的“Is a directory”读文件错误。

调用关系:它覆盖重启之后的保护逻辑,连接了 thread/fork、turn/start、thread/resume 三条路径。它用 create_config_toml 建环境,重点验证服务器在真正读取 rollout 文件前会先检查路径是不是目录。

调用图:调用 2 个内部函数(new, create_config_toml);外部调用 9 个(default, new, Integer, create_fake_rollout, create_mock_responses_server_repeating_assistant, assert!, assert_eq!, timeout, vec!)。

create_config_toml954–975 ↗
fn create_config_toml(codex_home: &Path, server_uri: &str) -> std::io::Result<()>

作用:这个辅助函数给临时测试目录写一份最小可用的 config.toml。它让测试服务器知道用哪个假模型、哪个假提供方,以及不要真的走审批或沙箱写入。

数据流:进去的是 codex_home 目录和假模型服务器地址。函数拼出 config.toml 的路径,把模型名、审批策略、只读沙箱、模型提供方和 base_url 等内容写进去。出来的是 std::io::Result,表示写文件成功或失败;磁盘上会多出一份配置文件。

调用关系:大多数测试在启动 TestAppServer 前都会调用它。它不参与 fork 业务本身,只负责把测试环境布置成服务器能启动、且请求会打到 mock 模型服务器。

调用图:被 9 处调用(pathless_ephemeral_thread_rejects_codex_home_path_after_reload, thread_fork_can_exclude_turns_and_skip_restored_token_usage, thread_fork_can_load_source_by_path, thread_fork_creates_new_thread_and_emits_started, thread_fork_emits_restored_token_usage_before_next_turn, thread_fork_ephemeral_remains_pathless_and_omits_listing, thread_fork_inherits_explicit_source_name_from_session_index, thread_fork_rejects_unmaterialized_thread, thread_fork_with_empty_path_uses_thread_id);外部调用 3 个(join, format!, write)。

create_config_toml_with_chatgpt_base_url977–1003 ↗
fn create_config_toml_with_chatgpt_base_url(
    codex_home: &Path,
    server_uri: &str,
    chatgpt_base_url: &str,
) -> std::io::Result<()>

作用:这个辅助函数和 create_config_toml 类似,但额外写入 chatgpt_base_url。它用于那些需要模拟 ChatGPT 云端配置或分析上报的测试。

数据流:进去的是临时目录、假模型服务器地址和假的 ChatGPT 后端地址。函数生成 config.toml 内容并写入磁盘,其中既有模型提供方配置,也有 chatgpt_base_url。出来的是文件写入结果;成功后服务器启动时会使用这些测试地址。

调用关系:它被分析埋点测试和云配置错误测试调用。前者用它把分析请求导到可捕获的 mock 服务,后者用它把云配置加载导到会返回 401 的 mock 服务。

调用图:被 2 处调用(thread_fork_surfaces_cloud_config_bundle_load_errors, thread_fork_tracks_thread_initialized_analytics);外部调用 3 个(join, format!, write)。

app-server/tests/suite/v2/thread_memory_mode_set.rs源码 ↗
testtest run

这是一组自动化测试,像是在替真实用户走一遍操作流程:先准备一个临时的应用目录,写入一份只给测试用的配置文件,再启动一个假的模型服务,避免真的去联网调用 AI。测试会启动 app server,通过协议请求创建或指定一个线程,然后发送“把这个线程的记忆模式改掉”的请求。这里的“记忆模式”可以理解成聊天线程是否允许使用记忆功能的开关。最后,测试不会只看接口有没有回一句“成功”,而是直接去状态数据库里查,确认保存下来的值确实变成了 disabled 或 enabled。这个文件重要的地方在于,它同时覆盖了两种情况:一个是刚启动、已经加载到服务里的线程;另一个是磁盘上已有、后来才被操作的线程。这样可以防止代码只改了内存里的状态,或者只改了存档里的状态,造成用户下次打开时设置丢失。

函数细节4
thread_memory_mode_set_updates_loaded_thread_state24–63 ↗
async fn thread_memory_mode_set_updates_loaded_thread_state() -> Result<()>

作用:这个测试确认:当一个线程已经由服务启动并加载在运行中时,把它的记忆模式改成禁用,数据库里也会同步变成禁用。简单说,它防止“界面上改了,但真正保存处没改”的问题。

数据流:进去的是一个临时目录、一个假的模型服务地址,以及测试自己新建出来的线程。它先写配置、初始化状态数据库、启动测试服务器,再发送创建线程请求,拿到线程编号;接着发送“把这个线程记忆模式设为 Disabled”的请求;最后用线程编号去数据库查询。出来的结果是断言数据库里的 memory_mode 字段等于 disabled,同时测试过程会在临时目录里写入配置和状态数据。

调用关系:它是测试流程的主角之一。它会先请 create_config_toml 写好测试配置,再请 init_state_db 准备状态数据库;之后通过 TestAppServer 模拟客户端和服务器对话,并用协议响应解析函数拿到线程信息。它验证的是“已加载线程”的那条路径,也就是服务正在认识这个线程时,设置请求能不能落到持久化状态里。

调用图:调用 4 个内部函数(new, create_config_toml, init_state_db, from_string);外部调用 6 个(default, new, Integer, create_mock_responses_server_repeating_assistant, assert_eq!, timeout)。

thread_memory_mode_set_updates_stored_thread_state66–103 ↗
async fn thread_memory_mode_set_updates_stored_thread_state() -> Result<()>

作用:这个测试确认:对一个已经存放在磁盘里的旧线程,也能修改记忆模式,并且最后保存的是最新一次设置。它特别检查先禁用再启用后,数据库最终应该是启用。

数据流:进去的是临时应用目录、假的模型服务地址,以及 create_fake_rollout 预先造出来的旧线程记录。它写配置、初始化数据库、创建一个假存档线程,再启动测试服务器;随后对同一个线程连续发送两次记忆模式设置请求,先 Disabled 后 Enabled;最后读取数据库。出来的结果是数据库里这个线程的 memory_mode 变成 enabled,说明后一次设置覆盖了前一次设置。

调用关系:它和另一个测试使用同样的辅助函数 create_config_toml 和 init_state_db,但测试对象不同:它不是先通过服务器新建线程,而是借助 create_fake_rollout 做出一个已存在的线程。这样它覆盖的是“已有存档线程被重新设置”的流程,确保服务器在收到设置请求时能找到旧线程并更新状态。

调用图:调用 4 个内部函数(new, create_config_toml, init_state_db, from_string);外部调用 6 个(new, Integer, create_fake_rollout, create_mock_responses_server_repeating_assistant, assert_eq!, timeout)。

init_state_db105–111 ↗
async fn init_state_db(codex_home: &Path) -> Result<Arc<StateRuntime>>

作用:这个辅助函数负责给测试准备状态数据库。可以把它理解成测试开场前先把账本建好,并标记为已经完成初始化补课。

数据流:进去的是测试用的 codex_home 路径。它把路径交给 StateRuntime::init,创建或打开这个目录下的状态数据库,并指定测试用的 provider 名字 mock_provider;然后调用 mark_backfill_complete,表示历史数据补填已经完成。出来的是一个可以共享使用的 StateRuntime,测试后面用它直接检查线程记忆模式是否真的写进数据库。

调用关系:它被两个测试函数在开头调用,因为两个测试都需要一个可查询的状态数据库。它不直接参与发送请求,而是搭好底层存储环境,并在测试结束前提供检查入口。

调用图:调用 1 个内部函数(init);被 2 处调用(thread_memory_mode_set_updates_loaded_thread_state, thread_memory_mode_set_updates_stored_thread_state);外部调用 1 个(to_path_buf)。

create_config_toml113–138 ↗
fn create_config_toml(codex_home: &Path, server_uri: &str) -> std::io::Result<()>

作用:这个辅助函数在临时目录里写一份测试专用的 config.toml 配置文件。没有它,测试服务器就不知道该用哪个模型、哪个假服务地址,以及是否启用 SQLite 状态存储。

数据流:进去的是 codex_home 路径和假的模型服务 server_uri。它在这个目录下拼出 config.toml 文件路径,把模型名、审批策略、沙盒模式、测试 provider、SQLite 功能开关、以及 mock provider 的 base_url 等内容写进去。出来的是一次文件写入结果;成功后,测试服务器启动时就会读取这份配置。

调用关系:它被两个测试函数在启动 TestAppServer 之前调用。它把 create_mock_responses_server_repeating_assistant 提供的假服务地址写进配置里,让后续服务器请求模型时走测试替身,而不是访问真实外部服务。

调用图:被 2 处调用(thread_memory_mode_set_updates_loaded_thread_state, thread_memory_mode_set_updates_stored_thread_state);外部调用 3 个(join, format!, write)。

app-server/tests/suite/v2/thread_metadata_update.rs源码 ↗
testtest run

这个测试文件模拟一个真实的 app-server:先建一个临时的 Codex 主目录,写入测试配置,再启动测试服务器,通过 JSON-RPC(一种用 JSON 发请求、收回复的通信格式)发送“开始线程、读取线程、恢复线程、更新元数据”等请求。它重点盯住线程里的 gitInfo,也就是这段对话对应的 Git 仓库信息。测试覆盖了几类容易出错的情况:正常改分支名;空更新要被拒绝;临时线程不能改元数据;旧的 rollout 文件存在但 SQLite(一种本地小数据库)里少了记录时,系统要能修复;归档线程也要能修复;已有的 Git 字段还能被清空。可以把它理解成一套验收清单:用户点了“更新线程 Git 信息”后,不管线程是新建的、旧文件恢复的,还是被归档的,服务器都不能把线程状态、预览文字、创建时间弄坏。

函数细节9
thread_metadata_update_patches_git_branch_and_returns_updated_thread39–131 ↗
async fn thread_metadata_update_patches_git_branch_and_returns_updated_thread() -> Result<()>

作用:验证最常见的成功场景:给一个新线程补上 Git 分支名后,接口立刻返回更新后的线程,并且之后再读取也能看到同样的分支名。

数据流:进去的是一个临时主目录、一个假的模型服务地址,以及一次“把分支改成 feature/sidebar-pr”的请求。测试先写配置、启动测试服务器、创建线程,再发送元数据更新请求。出来的是更新后的线程对象;测试确认线程编号和会话编号没变,gitInfo 里多了分支名,线程仍是空闲状态,并且 JSON 返回内容和后续读取结果都一致。

调用关系:这是该文件的基础正向用例。它先用 create_config_toml 准备配置,再借助假的模型响应服务器和 TestAppServer 跑完整请求流程,并用 timeout 防止测试卡死。它不调用本文件里的数据库辅助函数,因为这里验证的是新线程的正常更新路径。

调用图:调用 2 个内部函数(new, create_config_toml);外部调用 6 个(default, new, Integer, create_mock_responses_server_repeating_assistant, assert_eq!, timeout)。

thread_metadata_update_rejects_empty_git_info_patch134–177 ↗
async fn thread_metadata_update_rejects_empty_git_info_patch() -> Result<()>

作用:验证服务端不会接受“什么字段都没改”的 Git 元数据更新。这样可以避免客户端发了一个看似成功、其实没有任何意义的请求。

数据流:进去的是一个新建线程和一个 gitInfo 里 sha、branch、origin_url 都没有提供的更新请求。测试启动服务器并创建线程后发送这个空补丁。出来的不是成功响应,而是 JSON-RPC 错误;测试检查错误消息必须说明“gitInfo 至少要包含一个字段”。

调用关系:它和正常更新测试走相同的启动和建线程流程,也调用 create_config_toml、假的响应服务器和 timeout。不同点是它故意发坏请求,然后读取错误消息,确认服务端的参数校验在真正改数据前就拦住了请求。

调用图:调用 2 个内部函数(new, create_config_toml);外部调用 6 个(default, new, Integer, create_mock_responses_server_repeating_assistant, assert_eq!, timeout)。

thread_metadata_update_rejects_ephemeral_thread180–228 ↗
async fn thread_metadata_update_rejects_ephemeral_thread() -> Result<()>

作用:验证临时线程不能更新元数据。临时线程可以理解成“不打算长期保存的草稿对话”,给它改持久化元数据没有意义,也容易造成存储状态混乱。

数据流:进去的是一个带 ephemeral=true 标记的新线程,以及一个试图写入 Git 分支名的请求。测试创建这种临时线程后发送更新。出来的是错误响应;测试确认错误码是无效请求,并且错误消息明确指出这个临时线程不支持元数据更新,还带上了线程 ID。

调用关系:它沿用 create_config_toml 和测试服务器搭建流程,但在线程启动参数里打开 ephemeral。随后它不期待成功结果,而是等待错误消息,用来证明线程类型检查发生在更新落库之前。

调用图:调用 2 个内部函数(new, create_config_toml);外部调用 6 个(default, new, Integer, create_mock_responses_server_repeating_assistant, assert_eq!, timeout)。

thread_metadata_update_repairs_missing_sqlite_row_for_stored_thread231–281 ↗
async fn thread_metadata_update_repairs_missing_sqlite_row_for_stored_thread() -> Result<()>

作用:验证只有历史 rollout 文件、但 SQLite 数据库里缺少线程记录时,更新元数据能顺便把数据库记录补回来。rollout 文件可以理解成线程的磁盘存档。

数据流:进去的是一个临时主目录、一份手工造出的旧线程 rollout 文件、一个已初始化但缺记录的状态数据库,以及一个写入 Git 分支名的请求。测试启动服务器后直接更新这个旧线程。出来的是一个完整线程:ID 正确,预览文字和创建时间从旧文件里恢复,gitInfo 被写成新分支。

调用关系:这个用例调用 init_state_db 先准备 SQLite 状态库,再用 create_fake_rollout 造一个磁盘上的线程文件。它验证的不是普通新线程,而是“数据库和文件不同步”时,metadata update 这条路径会触发修复,而不是直接失败。

调用图:调用 3 个内部函数(new, create_config_toml, init_state_db);外部调用 6 个(new, Integer, create_fake_rollout, create_mock_responses_server_repeating_assistant, assert_eq!, timeout)。

thread_metadata_update_repairs_loaded_thread_without_resetting_summary284–361 ↗
async fn thread_metadata_update_repairs_loaded_thread_without_resetting_summary() -> Result<()>

作用:验证线程已经被恢复加载过之后,如果数据库记录又丢了,更新元数据仍能修复记录,并且不会把已有的摘要信息重置掉。

数据流:进去的是一个假 rollout 文件、一个状态数据库、一次 reconcile_rollout 生成的数据库记录、一次恢复线程请求,以及之后人为删除数据库记录的操作。测试再发送 Git 分支更新。出来的是更新后的线程;它保留原来的预览文字和创建时间,同时写入新的分支名。

调用关系:这个测试比前一个更接近复杂真实场景:先用 init_state_db 建库,用 reconcile_rollout 把磁盘线程同步进库,再通过 resume 请求把线程加载到服务器内存中,随后故意 delete_thread 删除库记录。最后的 metadata update 必须把缺失记录修好,而不是把已加载线程的摘要弄丢。

调用图:调用 5 个内部函数(new, create_config_toml, init_state_db, from_string, reconcile_rollout);外部调用 8 个(default, new, Integer, create_fake_rollout, create_mock_responses_server_repeating_assistant, rollout_path, assert_eq!, timeout)。

thread_metadata_update_repairs_missing_sqlite_row_for_archived_thread364–424 ↗
async fn thread_metadata_update_repairs_missing_sqlite_row_for_archived_thread() -> Result<()>

作用:验证线程文件被移动到归档目录后,即使 SQLite 记录缺失,更新 Git 元数据也能找到它并修复。归档线程就像放进“旧档案柜”的对话。

数据流:进去的是一个假 rollout 文件、一个已初始化的状态数据库,以及手动把该文件移动到 archived sessions 目录的文件操作。测试启动服务器后,对这个归档线程发送分支更新请求。出来的是成功响应,线程 ID、预览文字、创建时间都正确,gitInfo 也变成新的归档分支名。

调用关系:它调用 init_state_db、create_fake_rollout 和 rollout_path,并用文件系统的创建目录、重命名操作模拟归档。这个用例证明元数据更新逻辑不只会看普通会话目录,也能处理归档目录里的历史线程。

调用图:调用 3 个内部函数(new, create_config_toml, init_state_db);外部调用 9 个(new, Integer, create_fake_rollout, create_mock_responses_server_repeating_assistant, rollout_path, assert_eq!, create_dir_all, rename, timeout)。

thread_metadata_update_can_clear_stored_git_fields427–486 ↗
async fn thread_metadata_update_can_clear_stored_git_fields() -> Result<()>

作用:验证已经保存过的 Git 信息可以被清空。比如用户不想让线程继续关联某个提交、分支或远程仓库时,接口要能把这些字段真正删掉。

数据流:进去的是一个带有提交号、分支名、仓库地址的假 rollout 文件,以及一个把 sha、branch、origin_url 都设置为 None 的更新请求。测试启动服务器并发送清空请求。出来的更新结果里 gitInfo 变成 None;随后再次读取线程,结果仍然是 None,说明清空被保存下来了。

调用关系:它用 create_fake_rollout 先制造一个“已经有 Git 信息”的旧线程,用 init_state_db 准备状态库,再通过 metadata update 发出显式清空字段的补丁。最后它还调用 read 请求复查,证明这不是只改了返回值,而是持久状态也被改了。

调用图:调用 4 个内部函数(new, create_config_toml, init_state_db, new);外部调用 6 个(new, Integer, create_fake_rollout, create_mock_responses_server_repeating_assistant, assert_eq!, timeout)。

init_state_db488–494 ↗
async fn init_state_db(codex_home: &Path) -> Result<Arc<StateRuntime>>

作用:为测试创建并准备一个 SQLite 状态数据库。它还把回填标记为完成,避免测试启动时把“历史数据同步”这件事搅进主要断言里。

数据流:进去的是 Codex 临时主目录路径。函数用这个路径初始化 StateRuntime,也就是测试用的状态数据库运行对象,然后标记 backfill complete,意思是“旧数据补录已经做完”。出来的是一个可共享的数据库句柄 Arc<StateRuntime>;Arc 可以理解成多人共用同一个对象的安全引用。

调用关系:它是多个修复类测试的准备工具,被 thread_metadata_update_repairs_missing_sqlite_row_for_stored_thread、thread_metadata_update_repairs_loaded_thread_without_resetting_summary、thread_metadata_update_repairs_missing_sqlite_row_for_archived_thread 和 thread_metadata_update_can_clear_stored_git_fields 调用。它把数据库环境搭好,让这些测试能专心检查元数据更新和缺记录修复。

调用图:调用 1 个内部函数(init);被 4 处调用(thread_metadata_update_can_clear_stored_git_fields, thread_metadata_update_repairs_loaded_thread_without_resetting_summary, thread_metadata_update_repairs_missing_sqlite_row_for_archived_thread, thread_metadata_update_repairs_missing_sqlite_row_for_stored_thread);外部调用 1 个(to_path_buf)。

create_config_toml496–521 ↗
fn create_config_toml(codex_home: &Path, server_uri: &str) -> std::io::Result<()>

作用:给每个测试写一份最小可用的 config.toml 配置文件。没有它,测试服务器不知道用哪个模型、哪个假服务地址,也不知道要打开 SQLite 功能。

数据流:进去的是 Codex 临时主目录路径和假模型服务器的地址。函数拼出 config.toml 的文件路径,把模型名、审批策略、只读沙盒、mock provider 地址、重试次数、sqlite=true 等配置写进去。出来的是一次文件写入结果;成功后测试服务器启动时就会读取这份配置。

调用关系:它被本文件所有测试用例调用,是测试启动前的共同铺垫。各测试先创建假的模型响应服务器,拿到地址后交给 create_config_toml;随后 TestAppServer 才能按这份配置连接假服务并启用数据库相关行为。

调用图:被 7 处调用(thread_metadata_update_can_clear_stored_git_fields, thread_metadata_update_patches_git_branch_and_returns_updated_thread, thread_metadata_update_rejects_empty_git_info_patch, thread_metadata_update_rejects_ephemeral_thread, thread_metadata_update_repairs_loaded_thread_without_resetting_summary, thread_metadata_update_repairs_missing_sqlite_row_for_archived_thread, thread_metadata_update_repairs_missing_sqlite_row_for_stored_thread);外部调用 3 个(join, format!, write)。

app-server/tests/suite/v2/thread_name_websocket.rs源码 ↗
testtest execution

这个测试文件模拟了一个很真实的场景:两个客户端同时连着 app-server,其中一个人把某个聊天线程改名,另一个人也应该马上收到通知。WebSocket 可以理解成一根一直开着的双向电话线,客户端和服务器可以随时互发消息。这里还用 JSON-RPC(一种用 JSON 包装“请求、响应、通知”的通信格式)来发送命令。测试分两种情况:一种是线程先被恢复加载过,再改名;另一种是线程还没加载,只是磁盘上已有记录,也直接改名。两种情况下都要验证三件事:发起改名的客户端收到成功响应和更新通知,另一个客户端也收到同样通知,本地旧格式的线程名字也被改掉。最后测试会确认没有多余消息乱发,并杀掉测试用服务器进程,避免留下后台程序。

函数细节6
thread_name_updated_broadcasts_for_loaded_threads33–96 ↗
async fn thread_name_updated_broadcasts_for_loaded_threads() -> Result<()>

作用:这个测试检查“已经加载过的线程”改名后,服务器是否会把改名通知广播给所有 WebSocket 客户端。它防止出现只有改名的人看见新名字、其他打开页面的人还停留在旧名字的问题。

数据流:它先准备一个假的回答服务器、临时的配置目录和一份假的聊天记录,然后启动 WebSocket app-server。接着连上两个客户端,分别初始化;第一个客户端先恢复这个线程,让服务器把它加载进来,再发送 thread/name/set 改名请求。测试读取第一个客户端的响应和通知,也读取第二个客户端收到的通知,逐一确认线程 id 和新名字正确;最后检查磁盘里的旧线程名也变成新名字,并确认两个客户端都没有再收到多余消息。测试结束后会停止服务器进程。

调用关系:这是一个顶层测试用例,会串起很多测试辅助工具:用 create_rollout 准备聊天记录,用 initialize_both_clients 完成两个客户端的握手,用发送和读取消息的辅助函数模拟真实 WebSocket 通信,再把通知交给 assert_thread_name_updated 检查内容,把本地存储交给 assert_legacy_thread_name 检查。

调用图:调用 12 个内部函数(assert_no_message, connect_websocket, create_config_toml, read_notification_for_method, read_response_and_notification_for_method, read_response_for_id, send_request, spawn_websocket_server, assert_legacy_thread_name, assert_thread_name_updated (+2 more));外部调用 6 个(default, from_millis, new, create_mock_responses_server_repeating_assistant, assert_eq!, to_value)。

thread_name_updated_broadcasts_for_not_loaded_threads99–148 ↗
async fn thread_name_updated_broadcasts_for_not_loaded_threads() -> Result<()>

作用:这个测试检查“还没有被服务器加载的线程”也能被改名,并且同样会通知所有 WebSocket 客户端。它覆盖的是更容易漏掉的情况:记录在磁盘上存在,但当前内存里还没打开。

数据流:它创建临时配置和一份假的聊天记录,但不先恢复这个线程。服务器启动后,两个 WebSocket 客户端连接并初始化;第一个客户端直接发送 thread/name/set 改名请求。测试随后确认第一个客户端拿到成功响应和更新通知,第二个客户端也收到同样的 thread/name/updated 通知;然后再去本地旧记录里查,确认名字已经写入磁盘。最后它等待一小段时间,确认没有多余消息,并关闭服务器进程。

调用关系:它和 loaded 版本的测试走同一套辅助流程,但故意跳过 thread/resume 这一步。这样可以证明改名功能不依赖线程已经加载到内存里;它会调用 create_rollout、initialize_both_clients、assert_thread_name_updated 和 assert_legacy_thread_name 来完成准备、通信和校验。

调用图:调用 11 个内部函数(assert_no_message, connect_websocket, create_config_toml, read_notification_for_method, read_response_and_notification_for_method, send_request, spawn_websocket_server, assert_legacy_thread_name, assert_thread_name_updated, create_rollout (+1 more));外部调用 4 个(from_millis, new, create_mock_responses_server_repeating_assistant, to_value)。

initialize_both_clients150–157 ↗
async fn initialize_both_clients(ws1: &mut WsClient, ws2: &mut WsClient) -> Result<()>

作用:这个辅助函数负责让两个 WebSocket 客户端先完成初始化握手。简单说,就是让服务器知道“这两个客户端已经准备好了,可以开始正式收发业务消息”。

数据流:它接收两个可写可读的 WebSocket 客户端连接。它先给第一个客户端发送 initialize 请求,并在规定时间内等到对应响应;再对第二个客户端做同样的事。如果两个响应都及时回来,就返回成功;如果超时或响应读取失败,就把错误交给测试。

调用关系:两个顶层测试在真正发改名请求前都会调用它。它把初始化的细节封装起来,内部把发送初始化请求的工作交给 send_initialize_request,把读取指定响应的工作交给 read_response_for_id,并用 timeout 防止测试卡死。

调用图:调用 2 个内部函数(read_response_for_id, send_initialize_request);被 2 处调用(thread_name_updated_broadcasts_for_loaded_threads, thread_name_updated_broadcasts_for_not_loaded_threads);外部调用 1 个(timeout)。

create_rollout159–169 ↗
fn create_rollout(codex_home: &std::path::Path, filename_ts: &str) -> Result<String>

作用:这个辅助函数用来在临时目录里造一份假的聊天线程记录。测试需要有一个真实可查的线程 id,才能模拟“给已有聊天改名”。

数据流:它接收 Codex 的临时主目录和一个用于文件名的时间戳。然后调用测试支持库,写入一份包含用户消息、时间、模型提供者等信息的假聊天记录。完成后返回这份记录对应的线程 id,后续请求会拿这个 id 去恢复或改名。

调用关系:两个顶层测试都会先调用它来准备测试数据。它不直接参与 WebSocket 通信,而是负责把“测试世界里的磁盘状态”搭好,底层创建记录的具体工作交给 create_fake_rollout_with_text_elements。

调用图:被 2 处调用(thread_name_updated_broadcasts_for_loaded_threads, thread_name_updated_broadcasts_for_not_loaded_threads);外部调用 2 个(new, create_fake_rollout_with_text_elements)。

assert_thread_name_updated171–181 ↗
fn assert_thread_name_updated(
    notification: JSONRPCNotification,
    thread_id: &str,
    thread_name: &str,
) -> Result<()>

作用:这个断言函数检查服务器发来的 thread/name/updated 通知内容是否正确。它确保通知里说的是同一个线程,并且新名字就是测试刚设置的名字。

数据流:它接收一条 JSON-RPC 通知、期望的线程 id 和期望的新名字。它先从通知的 params 字段里解析出 ThreadNameUpdatedNotification 结构;然后比较通知里的 thread_id 是否等于期望 id,thread_name 是否等于期望名字。全部一致就返回成功,不一致就让测试失败。

调用关系:两个顶层测试都会用它分别检查发起改名的客户端和旁观客户端收到的通知。它站在消息校验这一环,前面由 read_response_and_notification_for_method 或 read_notification_for_method 读出通知,后面由它判断通知内容是不是可信。

调用图:被 2 处调用(thread_name_updated_broadcasts_for_loaded_threads, thread_name_updated_broadcasts_for_not_loaded_threads);外部调用 2 个(assert_eq!, from_value)。

assert_legacy_thread_name183–196 ↗
async fn assert_legacy_thread_name(
    codex_home: &Path,
    conversation_id: &str,
    expected_name: &str,
) -> Result<()>

作用:这个断言函数检查旧的本地线程记录里,线程名字是否也已经改成新名字。它防止只给在线客户端发了通知,却忘了把名字真正保存起来。

数据流:它接收 Codex 主目录、线程 id 字符串和期望名字。它先把字符串形式的 id 转成 ThreadId 类型,再调用 find_thread_name_by_id 到本地记录里查这个线程名;最后比较查到的名字是否等于期望名字。匹配就成功,不匹配或读取失败就让测试失败。

调用关系:两个顶层测试在确认 WebSocket 通知之后都会调用它。它负责验证持久化结果,也就是“改名有没有落到磁盘上”;查询底层记录的工作交给 find_thread_name_by_id。

调用图:调用 1 个内部函数(from_string);被 2 处调用(thread_name_updated_broadcasts_for_loaded_threads, thread_name_updated_broadcasts_for_not_loaded_threads);外部调用 1 个(assert_eq!)。

app-server/tests/suite/v2/thread_rollback.rs源码 ↗
testtest execution

这个测试把真实的大模型换成一个假的模型服务器,让测试结果稳定、可重复。它先临时写一份配置文件,启动测试用的 app server,再创建一个线程,连续发送两次用户输入。随后它调用 thread/rollback,要求删掉最后 1 轮。测试会检查返回的线程里只剩第一轮、状态回到空闲、用户消息内容没被改坏,还特别检查接口返回的 JSON 里,线程标题 name 没有时必须明确是 null。最后它再 resume(重新打开/恢复)同一个线程,确认第二轮确实已经从保存记录里消失,而不是只在内存里临时隐藏。

函数细节2
thread_rollback_drops_last_turns_and_persists_to_rollout26–182 ↗
async fn thread_rollback_drops_last_turns_and_persists_to_rollout() -> Result<()>

作用:这是主测试用例,用来模拟一个完整场景:开线程、发两轮消息、回滚最后一轮、再重新恢复线程。它的目的就是确认回滚功能既改对了返回结果,也真的写进了持久记录。

数据流:进去的是一组固定的假模型回复、一个临时目录和测试服务器配置;它用这些启动测试版 app server,创建线程并发送“First”和“Second”两轮输入;然后发出回滚请求,要求删掉最后 1 轮。出来的是一串断言结果:回滚后的线程只剩第一轮,状态是 Idle,sessionId 没变,name 在 JSON 里是 null;再恢复线程后,看到的历史仍然只剩第一轮,说明删除已经保存成功。

调用关系:它是这个文件的核心测试流程。它先调用 create_config_toml 准备配置,再借助测试工具创建假模型服务、启动 TestAppServer,并通过发送 thread/start、turn/start、thread/rollback、thread/resume 这些请求来推动整个流程。测试中的 assert_eq! 和 panic! 用来在结果不符合预期时立刻报错。

调用图:调用 2 个内部函数(new, create_config_toml);外部调用 8 个(default, new, Integer, create_mock_responses_server_sequence_unchecked, assert_eq!, panic!, timeout, vec!)。

create_config_toml184–205 ↗
fn create_config_toml(codex_home: &std::path::Path, server_uri: &str) -> std::io::Result<()>

作用:这个小工具函数给测试临时写一份 config.toml 配置文件。没有它,测试服务器就不知道该使用哪个模型、连接哪个假的模型接口。

数据流:进去的是临时的 codex_home 路径和假模型服务器地址;它在这个目录下拼出 config.toml 文件路径,把模型名、审批策略、沙箱模式、模型提供方地址等内容填进 TOML 文本;出来的是写文件的结果,成功就生成配置文件,失败就把文件系统错误交还给调用者。

调用关系:它只被 thread_rollback_drops_last_turns_and_persists_to_rollout 调用,属于测试启动前的准备步骤。它内部只是做三件事:用 join 拼路径,用 format! 把服务器地址塞进配置模板,再用 std::fs::write 写到磁盘。

调用图:被 1 处调用(thread_rollback_drops_last_turns_and_persists_to_rollout);外部调用 3 个(join, format!, write)。

app-server/tests/suite/v2/thread_settings_update.rs源码 ↗
testtest execution

这个文件像一套验收清单,模拟一个客户端连接应用服务器,再用假的模型服务接收请求。它重点测试 thread/settings/update 这个能力:改设置时不能偷偷启动模型请求;改完后要发出 thread/settings/updated 通知;下一轮对话要带着新的模型、服务档位或工作目录去请求模型。它还测了几个容易出错的边界:对话正在进行时也能发更新通知;把 service_tier 传成 null 时要恢复默认行为;sandboxPolicy 和 permissions 这两种权限写法不能混用。文件后半部分是一些小工具函数,负责启动线程、启动一轮用户消息、读取通知、读取线程内容、查看假模型服务收到的请求。整体作用是防止“设置界面看起来改了,但真正发给模型的请求没变”这种隐蔽问题。

函数细节14
thread_settings_update_emits_notification_and_updates_future_turns37–95 ↗
async fn thread_settings_update_emits_notification_and_updates_future_turns() -> Result<()>

作用:测试更新线程的模型和服务档位后,服务器会发出设置已更新的通知,并且下一轮对话真的使用这些新设置。它还确认单纯改设置不会立刻触发一次模型请求。

数据流:进去的是一个临时配置目录、一个假的模型响应服务器,以及从模型目录里挑出的可用模型和服务档位 → 测试启动应用服务器和线程,发送设置更新,再启动一轮文字对话 → 出来的是多项断言:先没有模型请求,之后收到更新通知,线程里有一轮对话,假模型服务器收到的请求体里包含新模型和新服务档位。

调用关系:这是本文件的核心场景测试。它先用 create_config_toml 准备配置,用 start_thread 开线程,用 send_thread_settings_update 改设置,用 start_text_turn 发起后续对话,再通过 read_thread_settings_updated、read_thread_with_turns 和 received_response_bodies 检查服务器对外表现是否正确。

调用图:调用 9 个内部函数(new, create_config_toml, read_thread_settings_updated, read_thread_with_turns, received_response_bodies, send_thread_settings_update, service_tier_model_and_tier_id, start_text_turn, start_thread);外部调用 8 个(default, new, create_mock_responses_server_sequence_unchecked, write_models_cache, assert!, assert_eq!, timeout, vec!)。

thread_settings_update_cwd_retargets_default_environment98–148 ↗
async fn thread_settings_update_cwd_retargets_default_environment() -> Result<()>

作用:测试更新线程的 cwd,也就是当前工作目录后,下一次发给模型的环境信息会改到新目录。cwd 可以理解成程序“站在哪个文件夹里干活”。

数据流:进去的是一个假的模型服务器、一个临时的 codex_home 配置目录和一个临时 workspace 工作目录 → 测试启动线程,把线程 cwd 更新成 workspace,再发起一轮文字对话 → 出来的是对模型请求内容的检查:发给模型看的 environment_context 里必须出现新的 cwd 路径。

调用关系:这个测试复用 create_config_toml、start_thread、send_thread_settings_update、read_thread_settings_updated 和 start_text_turn。它还借助 mock 响应记录请求内容,用来确认设置更新不只是存在服务器内部,而是真的影响了发给模型的上下文。

调用图:调用 9 个内部函数(new, create_config_toml, read_thread_settings_updated, send_thread_settings_update, start_text_turn, start_thread, mount_sse_once, sse, start_mock_server);外部调用 6 个(default, new, assert!, assert_eq!, timeout, vec!)。

thread_settings_update_while_turn_is_active_emits_notification151–190 ↗
async fn thread_settings_update_while_turn_is_active_emits_notification() -> Result<()>

作用:测试在一轮对话还没结束时更新线程设置,服务器仍然会发出设置已更新通知。它防止“正在忙就吞掉设置更新消息”的问题。

数据流:进去的是一个会故意延迟两秒才完成的假模型响应 → 测试启动线程和一轮对话,等收到 turn/started 后立刻更新模型设置 → 出来的是确认收到 thread/settings/updated 通知,并且通知里的模型变成 mock-model-4,最后这一轮对话正常完成。

调用关系:这个测试把 start_text_turn 放在设置更新之前,制造“对话进行中”的状态。之后用 send_thread_settings_update 发更新,用 read_thread_settings_updated 验证通知,再等待 turn/completed 确认流程没有被更新动作打坏。

调用图:调用 9 个内部函数(new, create_config_toml, read_thread_settings_updated, send_thread_settings_update, start_text_turn, start_thread, mount_response_sequence, sse_response, start_mock_server);外部调用 7 个(default, from_secs, new, create_final_assistant_message_sse_response, assert_eq!, timeout, vec!)。

thread_settings_update_null_service_tier_uses_default193–261 ↗
async fn thread_settings_update_null_service_tier_uses_default() -> Result<()>

作用:测试 service_tier 先被设置成某个档位,再被传 null 清掉时,服务器会回到默认服务档位行为。这里的服务档位可以理解成模型服务的速度或优先级选项。

数据流:进去的是假模型服务器、临时配置目录,以及一个带服务档位的模型 → 测试先把线程设置为指定模型和指定服务档位,再发送 service_tier 为 null 的更新 → 出来的是通知中显示服务档位回到默认值,后续模型请求体里不再显式带 service_tier 字段。

调用关系:这个测试和第一个测试很像,但重点在“清空设置”。它用 service_tier_model_and_tier_id 找可测数据,用 send_thread_settings_update 连续发两次更新,用 read_thread_settings_updated 看两次通知,最后用 received_response_bodies 查看真正发给模型的请求是否不再带 service_tier。

调用图:调用 8 个内部函数(new, create_config_toml, read_thread_settings_updated, received_response_bodies, send_thread_settings_update, service_tier_model_and_tier_id, start_text_turn, start_thread);外部调用 8 个(default, new, create_mock_responses_server_sequence_unchecked, write_models_cache, assert!, assert_eq!, timeout, vec!)。

thread_settings_update_rejects_sandbox_policy_with_permissions264–292 ↗
async fn thread_settings_update_rejects_sandbox_policy_with_permissions() -> Result<()>

作用:测试如果一次设置更新里同时传 sandboxPolicy 和 permissions,服务器会拒绝。sandboxPolicy 是沙箱安全策略,permissions 是权限字符串;这两套写法同时出现会让权限含义变混乱。

数据流:进去的是临时配置、假模型服务器和一个已启动线程 → 测试发送一个同时包含 SandboxPolicy::DangerFullAccess 和 permissions 的设置更新请求 → 出来的是 JSON-RPC 错误响应,错误消息明确说 permissions cannot be combined with sandboxPolicy

调用关系:这个测试直接调用 TestAppServer 的 send_thread_settings_update_request,而不是使用成功路径的 send_thread_settings_update,因为它预期会失败。它先用 create_config_toml 和 start_thread 搭好环境,再读取指定 request id 对应的错误消息。

调用图:调用 3 个内部函数(new, create_config_toml, start_thread);外部调用 7 个(default, new, new, Integer, create_mock_responses_server_sequence_unchecked, assert_eq!, timeout)。

turn_start_settings_override_emits_thread_settings_updated295–342 ↗
async fn turn_start_settings_override_emits_thread_settings_updated() -> Result<()>

作用:测试启动一轮对话时临时带上的模型设置,也会让线程设置更新并发出通知。也就是说,不只专门的设置更新接口会改设置,发起对话时的覆盖参数也会触发同样的同步消息。

数据流:进去的是临时配置、假模型服务器和一个新线程 → 测试发送 turn/start 请求,并在请求里指定 model 为 mock-model-3 → 出来的是成功创建一轮对话,同时收到 thread/settings/updated 通知,通知里的线程模型也变成 mock-model-3。

调用关系:这个测试先用 start_thread 创建线程,再直接调用 send_turn_start_request 发送带设置覆盖的对话请求。随后它用 read_thread_settings_updated 检查通知,并等待 turn/completed 确认整个对话流程走完。

调用图:调用 4 个内部函数(new, create_config_toml, read_thread_settings_updated, start_thread);外部调用 9 个(default, new, Integer, create_mock_responses_server_sequence_unchecked, to_response, assert!, assert_eq!, timeout, vec!)。

send_thread_settings_update344–356 ↗
async fn send_thread_settings_update(
    mcp: &mut TestAppServer,
    params: ThreadSettingsUpdateParams,
) -> Result<()>

作用:这是测试里的小帮手,用来发送一次线程设置更新,并确认服务器返回的是成功响应。它把“发请求、等响应、把响应转成具体类型”这几步包装起来,避免每个测试重复写。

数据流:进去的是一个测试服务器连接 mcp 和 ThreadSettingsUpdateParams 设置参数 → 它发送 thread/settings/update 请求,拿到 request id,再等待对应的 JSON-RPC 响应;JSON-RPC 是一种用 JSON 表示请求和响应的通信格式 → 出来时没有返回业务数据,只要函数成功结束,就表示响应能被解析成 ThreadSettingsUpdateResponse。

调用关系:多个成功路径测试都会调用它。它把活交给 TestAppServer 的 send_thread_settings_update_request 和 read_stream_until_response_message,最后用 to_response 把通用响应拆成设置更新响应。失败场景不会用它,因为失败场景要读错误消息。

调用图:调用 2 个内部函数(read_stream_until_response_message, send_thread_settings_update_request);被 4 处调用(thread_settings_update_cwd_retargets_default_environment, thread_settings_update_emits_notification_and_updates_future_turns, thread_settings_update_null_service_tier_uses_default, thread_settings_update_while_turn_is_active_emits_notification);外部调用 3 个(Integer, to_response, timeout)。

start_text_turn358–377 ↗
async fn start_text_turn(mcp: &mut TestAppServer, thread_id: String) -> Result<()>

作用:这是测试里的小帮手,用来在线程里启动一轮最简单的用户文字对话。它保证服务器真的创建了一个 turn,也就是一次用户输入到助手回答的对话轮次。

数据流:进去的是测试服务器连接和线程 id → 它构造一条内容为 hello 的用户文本,发送 turn/start 请求,等待对应响应,并解析成 TurnStartResponse → 出来时函数本身只返回成功或失败,但会断言新建的 turn id 不能为空。

调用关系:需要触发模型请求的测试都会调用它。它依赖 TestAppServer 发送 turn/start 并读取响应;后续测试通常再等待 turn/completed 通知,或查看假模型服务器收到的请求体。

调用图:调用 2 个内部函数(read_stream_until_response_message, send_turn_start_request);被 4 处调用(thread_settings_update_cwd_retargets_default_environment, thread_settings_update_emits_notification_and_updates_future_turns, thread_settings_update_null_service_tier_uses_default, thread_settings_update_while_turn_is_active_emits_notification);外部调用 6 个(default, Integer, to_response, assert!, timeout, vec!)。

start_thread379–392 ↗
async fn start_thread(mcp: &mut TestAppServer) -> Result<ThreadStartResponse>

作用:这是测试里的小帮手,用来新建一个线程,也就是一段独立的对话会话。它默认用 mock-model 作为初始模型。

数据流:进去的是测试服务器连接 → 它发送 thread/start 请求,参数里带一个默认模型名 mock-model,然后等待对应 JSON-RPC 响应 → 出来的是 ThreadStartResponse,里面包含新线程的信息,后续测试会拿线程 id 继续操作。

调用关系:几乎所有测试都先调用它,因为更新设置、启动对话、读取线程都需要已有线程。它把创建线程的细节封装起来,让每个测试专注于自己要验证的行为。

调用图:调用 2 个内部函数(read_stream_until_response_message, send_thread_start_request);被 6 处调用(thread_settings_update_cwd_retargets_default_environment, thread_settings_update_emits_notification_and_updates_future_turns, thread_settings_update_null_service_tier_uses_default, thread_settings_update_rejects_sandbox_policy_with_permissions, thread_settings_update_while_turn_is_active_emits_notification, turn_start_settings_override_emits_thread_settings_updated);外部调用 4 个(default, Integer, to_response, timeout)。

read_thread_with_turns394–410 ↗
async fn read_thread_with_turns(
    mcp: &mut TestAppServer,
    thread_id: &str,
) -> Result<ThreadReadResponse>

作用:这是测试里的小帮手,用来读取某个线程,并要求返回里面的对话轮次。它用于确认线程里到底记录了几轮对话。

数据流:进去的是测试服务器连接和线程 id → 它发送 thread/read 请求,并把 include_turns 设为 true,表示要一起拿到 turn 列表 → 出来的是 ThreadReadResponse,测试可以检查线程内容和轮次数量。

调用关系:当前只有 thread_settings_update_emits_notification_and_updates_future_turns 调用它。那个测试在启动一轮对话后用它确认线程确实保存了一轮 turn。

调用图:调用 2 个内部函数(read_stream_until_response_message, send_thread_read_request);被 1 处调用(thread_settings_update_emits_notification_and_updates_future_turns);外部调用 3 个(Integer, to_response, timeout)。

read_thread_settings_updated412–424 ↗
async fn read_thread_settings_updated(
    mcp: &mut TestAppServer,
) -> Result<ThreadSettingsUpdatedNotification>

作用:这是测试里的小帮手,用来等待并读取 thread/settings/updated 通知。通知可以理解成服务器主动告诉客户端:“这个线程的设置变了”。

数据流:进去的是测试服务器连接 → 它等待名为 thread/settings/updated 的通知消息,取出 notification.params,并把 JSON 数据解析成 ThreadSettingsUpdatedNotification → 出来的是结构化的通知内容,包括线程 id 和最新线程设置。

调用关系:大多数测试都用它检查设置更新是否被广播出来。它不发请求,只监听服务器流里的通知,是验证“客户端能不能及时知道设置变化”的关键工具。

调用图:调用 1 个内部函数(read_stream_until_notification_message);被 5 处调用(thread_settings_update_cwd_retargets_default_environment, thread_settings_update_emits_notification_and_updates_future_turns, thread_settings_update_null_service_tier_uses_default, thread_settings_update_while_turn_is_active_emits_notification, turn_start_settings_override_emits_thread_settings_updated);外部调用 2 个(from_value, timeout)。

received_response_bodies426–438 ↗
async fn received_response_bodies(server: &wiremock::MockServer) -> Result<Vec<Value>>

作用:这是测试里的小帮手,用来查看假的模型服务器收到了哪些 /responses 请求,以及每个请求的 JSON 正文。它让测试能确认真正发给模型服务的参数是什么。

数据流:进去的是 wiremock 的假服务器 → 它读取服务器记录过的所有请求,筛选路径以 /responses 结尾的请求,再把每个请求体解析成 JSON 值 → 出来的是一个 JSON 列表,测试会在里面查 model、service_tier 等字段。

调用关系:它被用在检查“后续模型请求是否真的带上新设置”的测试里。前面的设置更新和对话启动动作会让服务器产生请求,这个函数负责把这些请求拿出来给断言使用。

调用图:被 2 处调用(thread_settings_update_emits_notification_and_updates_future_turns, thread_settings_update_null_service_tier_uses_default);外部调用 2 个(received_requests, new)。

service_tier_model_and_tier_id440–446 ↗
fn service_tier_model_and_tier_id() -> Result<(String, String)>

作用:这是测试里的小帮手,用来从内置模型目录里挑一个既会显示给用户、又带服务档位的模型。这样测试不用把某个具体模型名写死。

数据流:进去时没有外部参数 → 它读取 all_model_presets 返回的所有模型预设,找出 show_in_picker 为真且 service_tiers 不为空的模型 → 出来的是这个模型的 id 和第一个服务档位的 id;如果找不到,就返回带说明的错误。

调用关系:它被服务档位相关的两个测试调用。这样测试数据跟随模型目录变化,只要目录里仍有可选服务档位模型,测试就能继续工作。

调用图:调用 1 个内部函数(all_model_presets);被 2 处调用(thread_settings_update_emits_notification_and_updates_future_turns, thread_settings_update_null_service_tier_uses_default)。

create_config_toml448–458 ↗
fn create_config_toml(codex_home: &std::path::Path, server_uri: &str) -> std::io::Result<()>

作用:这是测试里的小帮手,用来在临时 codex_home 目录里写一份指向假模型服务器的配置文件。没有它,测试服务器就不知道该把模型请求发到哪里。

数据流:进去的是 codex_home 路径和假模型服务器地址 → 它调用 write_mock_responses_config_toml 写入配置,指定 mock_provider、compact 等测试用设置 → 出来的是文件写入结果;成功后,后续 TestAppServer 会按这份配置连接假服务器。

调用关系:所有测试启动应用服务器前都会调用它。它属于测试环境搭建步骤,负责把真实外部模型服务替换成可控的 mock 服务,让测试稳定、快速、不依赖网络。

调用图:被 6 处调用(thread_settings_update_cwd_retargets_default_environment, thread_settings_update_emits_notification_and_updates_future_turns, thread_settings_update_null_service_tier_uses_default, thread_settings_update_rejects_sandbox_policy_with_permissions, thread_settings_update_while_turn_is_active_emits_notification, turn_start_settings_override_emits_thread_settings_updated);外部调用 2 个(default, write_mock_responses_config_toml)。

app-server/tests/suite/v2/thread_unarchive.rs源码 ↗
testtest

这个测试文件像是在给“会话归档箱”做验收。用户把一个对话归档后,系统会把它从正常会话目录移到归档位置;取消归档时,就应该把它搬回来,并告诉客户端“这个会话已经恢复了”。第一个测试启动一个临时的应用服务器和假的模型服务,真的创建会话、发一轮消息、归档、再取消归档,然后检查文件是否从归档目录搬回原目录,更新时间是否被刷新,状态是否正确,返回的 JSON 字段是否符合对外约定。第二个测试关注另一种情况:会话存在于内存里的线程存储中,没有磁盘路径。它确认取消归档不会因为“没有文件可搬”就弄丢名称、父会话等重要信息。文件里还带了一些小工具函数,用来写测试配置、比较路径,以及在测试结束时清理内存存储,避免影响别的测试。

函数细节7
thread_unarchive_moves_rollout_back_into_sessions_directory57–198 ↗
async fn thread_unarchive_moves_rollout_back_into_sessions_directory() -> Result<()>

作用:这个测试确认:一个已经写到磁盘上的会话被归档后,再取消归档时,实际文件会从归档位置搬回正常会话目录。它还检查服务器返回给客户端的数据和通知是否符合约定。

数据流:进去的是一个临时目录、一个假的模型响应服务器,以及通过测试客户端发出的“开始会话、开始一轮对话、归档、取消归档”请求。它先让服务器生成真实的会话文件,再确认文件在正常目录里;接着归档它,确认文件到了归档目录;然后故意把归档文件的修改时间改得很旧,再取消归档。出来的结果是多组断言:恢复后的会话文件回到原位置,归档位置不再有文件,返回的会话更新时间变新,状态是未加载,并且未命名会话在 JSON 里明确写成 name: null

调用关系:这是本文件的主测试之一。它会调用 create_config_toml 准备测试配置,调用 assert_paths_match_on_disk 确认找到的路径和原始路径真的是同一个磁盘文件。它通过 TestAppServer 模拟真实客户端和服务器交互,所以覆盖的是完整链路,而不只是某个小函数。

调用图:调用 3 个内部函数(new, assert_paths_match_on_disk, create_config_toml);外部调用 14 个(default, from_secs, new, new, new, Integer, create_mock_responses_server_repeating_assistant, assert!, assert_eq!, find_archived_thread_path_by_id_str (+4 more))。

thread_unarchive_preserves_pathless_store_metadata201–293 ↗
async fn thread_unarchive_preserves_pathless_store_metadata() -> Result<()>

作用:这个测试确认:如果会话存在内存存储里、没有磁盘文件路径,取消归档也应该保留它的元数据。这里的元数据包括会话名字、来源关系和是否有路径等信息。

数据流:进去的是一个临时配置目录、一个临时内存线程存储编号,以及手工创建的一条会话记录。测试先把这条会话写进内存存储,并给它设置名字和父会话信息;然后启动一个进程内服务器,发送“取消归档”请求。出来的是服务器返回的会话对象,测试检查它的 id 没变、path 仍然是空、forked_from_id 仍然存在、name 也仍然是原来的名字。最后它关闭客户端并让清理对象删除这份内存存储。

调用关系:这是本文件的第二个主测试,专门补上“没有磁盘文件可移动”的场景。它调用 create_config_toml_with_in_memory_thread_store 写入使用内存存储的配置,并依赖 InMemoryThreadStoreId::drop 在测试结束时清理全局内存状态,避免污染后续测试。

调用图:调用 9 个内部函数(start, create_config_toml_with_in_memory_thread_store, default, without_managed_config_for_tests, default_for_tests, new, default, from_string, for_id);外部调用 10 个(new, default, new, new_v4, new, Integer, default, assert_eq!, default, from_value)。

create_config_toml295–298 ↗
fn create_config_toml(codex_home: &Path, server_uri: &str) -> std::io::Result<()>

作用:这个小工具函数给测试用的临时 Codex 主目录写一份配置文件。没有这份配置,测试服务器就不知道该用哪个模型提供方、该把请求发到哪个假的服务地址。

数据流:进去的是临时目录路径和假的模型服务器地址。它调用 config_contents 生成配置文字,把文件名拼成 config.toml,然后写到磁盘。出来的结果是一个写文件结果:成功时临时目录里多了一份配置,失败时返回文件系统错误。

调用关系:它服务于 thread_unarchive_moves_rollout_back_into_sessions_directory。主测试先用它搭好服务器运行环境,然后才启动 TestAppServer 做真实的归档和取消归档流程。

调用图:调用 1 个内部函数(config_contents);被 1 处调用(thread_unarchive_moves_rollout_back_into_sessions_directory);外部调用 2 个(join, write)。

InMemoryThreadStoreId::drop305–307 ↗
fn drop(&mut self)

作用:这是一个自动清理函数。测试里创建了带编号的内存线程存储,测试结束时它会把这份存储删掉,防止残留数据影响别的测试。

数据流:进去的是这个清理对象里保存的 store_id。对象被销毁时,它把这个编号交给 InMemoryThreadStore::remove_id,让全局内存存储表移除对应内容。出来没有普通返回值,但全局测试状态被清理干净了。

调用关系:它不是被主测试手动调用的,而是 Rust 的 Drop 机制自动调用;Drop 可以理解成“对象离场时自动执行的收尾动作”。thread_unarchive_preserves_pathless_store_metadata 创建 InMemoryThreadStoreId 后,等测试函数结束,这个 drop 就负责善后。

调用图:调用 1 个内部函数(remove_id)。

create_config_toml_with_in_memory_thread_store310–334 ↗
fn create_config_toml_with_in_memory_thread_store(
    codex_home: &Path,
    store_id: &str,
) -> std::io::Result<()>

作用:这个小工具函数写一份特殊测试配置,让应用服务器使用指定编号的内存线程存储,而不是普通的磁盘会话文件。这样测试才能覆盖“没有路径的会话”这种情况。

数据流:进去的是临时 Codex 主目录和内存存储编号。它把编号填进一段 TOML 配置文字里,拼出 config.toml 路径,然后写到磁盘。出来的结果是写文件成功或失败;成功后,服务器启动时会按这份配置连接到同一个内存存储。

调用关系:它只被 thread_unarchive_preserves_pathless_store_metadata 使用。这个测试先创建内存存储并塞入会话,再用这份配置启动服务器,确保服务器看到的就是刚才准备好的那条会话。

调用图:被 1 处调用(thread_unarchive_preserves_pathless_store_metadata);外部调用 3 个(join, format!, write)。

config_contents336–352 ↗
fn config_contents(server_uri: &str) -> String

作用:这个函数生成普通归档测试要用的配置文本。它把假的模型服务器地址填进去,让测试不会真的访问外部网络或真实模型服务。

数据流:进去的是假的服务器地址。它把地址插入一段 TOML 配置模板,配置模型名、审批策略、沙箱模式、模型提供方地址和重试次数等。出来的是一整段字符串,之后会被写成 config.toml 文件。

调用关系:它被 create_config_toml 调用,属于准备测试环境的下游小零件。主测试不直接关心配置字符串怎么拼,只通过 create_config_toml 得到可启动的测试服务器环境。

调用图:被 1 处调用(create_config_toml);外部调用 1 个(format!)。

assert_paths_match_on_disk354–359 ↗
fn assert_paths_match_on_disk(actual: &Path, expected: &Path) -> std::io::Result<()>

作用:这个小断言函数用来确认两条路径在磁盘上指向同一个真实位置。它避免因为相对路径、符号链接或路径写法不同,导致测试误判。

数据流:进去的是实际找到的路径和预期路径。它先把两者都转成规范路径,也就是系统眼里的真实绝对位置,然后比较它们是否完全相同。出来的结果是:相同则返回成功,不同则让测试失败;如果路径无法解析,则返回文件系统错误。

调用关系:它被 thread_unarchive_moves_rollout_back_into_sessions_directory 调用,用在会话刚创建并写入磁盘之后。主测试用它确认“根据会话 id 找到的文件”确实就是服务器返回的那个会话文件。

调用图:被 1 处调用(thread_unarchive_moves_rollout_back_into_sessions_directory);外部调用 2 个(canonicalize, assert_eq!)。

app-server/tests/suite/v2/thread_unsubscribe.rs源码 ↗
testtest execution

这里测试的是 app-server 的 v2 线程退订行为。线程可以理解成一段对话,订阅就是客户端还在听这个对话的实时消息;退订就是客户端说“我先不听了”。这些测试确认:退订后,线程不会立刻从内存里消失,而是会等空闲超时;如果线程里还有一轮回答正在跑,退订也不能把它取消;如果刚失败过,失败状态也要保留下来,之后恢复线程还能看到;如果已经退订过,再退一次要明确告诉客户端“本来就没订阅”。文件用临时目录写配置,用假的模型服务器返回固定内容,再通过 JSON-RPC(一种用 JSON 发请求和收回应答的通信格式)模拟真实客户端和服务器对话。

函数细节7
thread_unsubscribe_keeps_thread_loaded_until_idle_timeout40–86 ↗
async fn thread_unsubscribe_keeps_thread_loaded_until_idle_timeout() -> Result<()>

作用:这个测试确认:客户端退订线程后,服务器不会马上把线程卸载或关闭。这样客户端短时间内还可以查到这个线程,避免刚退订就找不到的尴尬。

数据流:它先启动一个假的模型服务器,再写入测试配置,启动测试版 app-server,并创建一个新线程。接着发送退订请求,检查返回状态是“已退订”。然后它故意等很短时间,确认没有收到“线程已关闭”的通知。最后请求“当前已加载线程列表”,确认刚才那个线程还在列表里,且没有下一页数据。

调用关系:这是一个独立的集成测试。它依赖 create_config_toml 准备配置,依赖 start_thread 创建线程,然后通过 TestAppServer 发送退订和列表查询请求,验证服务器在退订后的短时间窗口里行为正确。

调用图:调用 3 个内部函数(new, create_config_toml, start_thread);外部调用 7 个(new, Integer, default, create_mock_responses_server_repeating_assistant, assert!, assert_eq!, timeout)。

thread_unsubscribe_during_turn_keeps_turn_running89–243 ↗
async fn thread_unsubscribe_during_turn_keeps_turn_running() -> Result<()>

作用:这个测试确认:如果线程正在生成回答,尤其是正在等待一个工具调用结果,客户端退订不能让这一轮任务停掉。退订只是不再听消息,不等于取消工作。

数据流:它先准备一个流式假的模型服务器:第一次响应要求调用一个动态工具,第二次响应给出最终回答。测试启动线程和一轮用户输入,等服务器发出工具调用,并用 wait_for_dynamic_tool_started 找到对应的工具开始通知。随后它读取服务器发给客户端的工具调用请求,确认参数正确。接着发送退订请求,确认状态是“已退订”,并确认短时间内没有线程关闭通知。最后它把工具结果回传给服务器,服务器继续发第二次模型请求并完成最终回答。

调用关系:这是文件里最复杂的主流程测试。它使用 create_config_toml 建配置,使用 wait_for_dynamic_tool_started 等到工具调用真正开始,还和 start_streaming_sse_server 创建的流式假服务器配合,证明退订不会打断还没结束的 turn(一轮对话处理)。

调用图:调用 4 个内部函数(new, create_config_toml, wait_for_dynamic_tool_started, start_streaming_sse_server);外部调用 13 个(default, new, Integer, assert!, assert_eq!, json!, panic!, to_string, to_value, create_dir (+3 more))。

thread_unsubscribe_preserves_cached_status_before_idle_unload246–335 ↗
async fn thread_unsubscribe_preserves_cached_status_before_idle_unload() -> Result<()>

作用:这个测试确认:线程发生错误后,即使客户端退订,服务器也不能忘记这个错误状态。这样之后恢复线程时,客户端还能看到真实状态,而不是被错误地改成正常。

数据流:它启动一个假的模型服务器,并让模型响应一次失败。然后写配置、启动测试服务器、创建线程,再启动一轮会失败的输入。收到错误通知后,它读取线程,确认状态是 SystemError,也就是系统错误。接着退订线程,确认已退订,并确认短时间内线程没有关闭。最后它恢复这个线程,检查恢复出来的线程状态仍然是 SystemError。

调用关系:这个测试用 create_config_toml 配好模型地址,用 start_thread 创建基础线程,再借助 mock SSE 失败响应制造错误场景。它验证退订流程不会覆盖或清空线程缓存里的状态。

调用图:调用 6 个内部函数(new, create_config_toml, start_thread, mount_sse_once, sse_failed, start_mock_server);外部调用 7 个(default, new, Integer, assert!, assert_eq!, timeout, vec!)。

thread_unsubscribe_reports_not_subscribed_before_idle_unload338–379 ↗
async fn thread_unsubscribe_reports_not_subscribed_before_idle_unload() -> Result<()>

作用:这个测试确认:同一个线程退订两次时,第二次要返回“未订阅”,而不是假装又退订成功。这样客户端能清楚知道当前订阅状态。

数据流:它启动假的模型服务器,写配置,启动测试服务器,并创建线程。第一次发送退订请求后,读取响应并确认状态是 Unsubscribed,表示刚刚成功退订。随后对同一个线程再发一次退订请求,读取响应并确认状态是 NotSubscribed,表示服务器知道这个客户端已经不在订阅了。

调用关系:这是退订状态判断的边界测试。它复用 create_config_toml 和 start_thread 做准备,然后连续调用两次退订接口,专门检查服务器对重复操作的回答是否准确。

调用图:调用 3 个内部函数(new, create_config_toml, start_thread);外部调用 5 个(new, Integer, create_mock_responses_server_repeating_assistant, assert_eq!, timeout)。

wait_for_dynamic_tool_started381–397 ↗
async fn wait_for_dynamic_tool_started(
    mcp: &mut TestAppServer,
    call_id: &str,
) -> Result<ItemStartedNotification>

作用:这个辅助函数会一直读取通知,直到看到指定的动态工具调用已经开始。动态工具可以理解成模型让客户端帮忙执行的一个小功能,比如这里的等待工具。

数据流:它拿到测试服务器连接和一个工具调用 id。进入循环后,不断读取 item/started 通知;如果通知没有参数,就跳过;如果有参数,就把 JSON 数据转成 ItemStartedNotification。只有当通知里的条目是 DynamicToolCall,并且 id 和目标 call_id 一样时,它才返回这个开始通知。

调用关系:它只被 thread_unsubscribe_during_turn_keeps_turn_running 使用。那个测试需要先确认工具调用已经卡在等待客户端响应的阶段,才能再发送退订请求,验证退订不会中断这个等待中的流程。

调用图:调用 1 个内部函数(read_stream_until_notification_message);被 1 处调用(thread_unsubscribe_during_turn_keeps_turn_running);外部调用 2 个(matches!, from_value)。

create_config_toml399–420 ↗
fn create_config_toml(codex_home: &std::path::Path, server_uri: &str) -> std::io::Result<()>

作用:这个辅助函数给每个测试写一份最小可用的配置文件,让 app-server 知道该用哪个假模型服务器。没有它,测试服务器就不知道请求该发到哪里。

数据流:它接收一个 codex_home 目录和假的模型服务器地址。然后在这个目录下拼出 config.toml 路径,把模型名、审批策略、沙箱模式、供应商名称、base_url、重试次数等测试配置写进去。结果是磁盘上多出一份配置文件;如果写文件失败,就返回系统错误。

调用关系:四个测试都会先调用它。它不参与断言,只负责铺好测试环境,让 TestAppServer 启动时读到一致、可控的模型配置。

调用图:被 4 处调用(thread_unsubscribe_during_turn_keeps_turn_running, thread_unsubscribe_keeps_thread_loaded_until_idle_timeout, thread_unsubscribe_preserves_cached_status_before_idle_unload, thread_unsubscribe_reports_not_subscribed_before_idle_unload);外部调用 3 个(join, format!, write)。

start_thread422–436 ↗
async fn start_thread(mcp: &mut TestAppServer) -> Result<String>

作用:这个辅助函数负责创建一个新线程,并把新线程的 id 返回给测试。这样各个测试不用重复写启动线程和解析响应的样板代码。

数据流:它拿到测试服务器连接,发送 thread/start 请求,指定模型为 mock-model。然后等待对应请求 id 的 JSON-RPC 响应,把响应转换成 ThreadStartResponse,取出里面的 thread.id 返回。它不会自己检查业务结果,只负责把“创建线程”这一步做完。

调用关系:它被三个测试复用:检查退订后仍加载、检查错误状态保留、检查重复退订。测试先靠它拿到 thread_id,后续所有读取、退订、恢复操作都围绕这个 id 展开。

调用图:调用 2 个内部函数(read_stream_until_response_message, send_thread_start_request);被 3 处调用(thread_unsubscribe_keeps_thread_loaded_until_idle_timeout, thread_unsubscribe_preserves_cached_status_before_idle_unload, thread_unsubscribe_reports_not_subscribed_before_idle_unload);外部调用 3 个(default, Integer, timeout)。

回合执行和交互

这些测试跟踪线程内的活动执行,从回合开始到中断、引导、注入历史、结构化输出,以及由模型驱动的客户端交互。

app-server/tests/suite/v2/client_metadata.rs源码 ↗
testtest

这个测试文件像一套“快递抽查”:客户端把一段对话请求交给 app-server,app-server 再转发给模拟的 Responses API,测试就检查包裹外面贴的标签是不是对。这里的“metadata(元数据)”就是额外说明,比如 fiber_run_id、origin、turn_id、thread_id、parent_thread_id 等,不是用户正文,但对追踪问题很关键。文件会启动假的后端服务器,写一份临时配置,让测试用的 app-server 连到假服务器;然后分别模拟开始线程、开始一轮对话、fork 旧线程、恢复子任务线程、发起 review、以及中途 steer(给正在进行的回合追加指导)。最后它读取假服务器收到的 HTTP 请求头或 WebSocket 请求体,确认这些追踪信息没有丢、没有串、也没有被旧值覆盖。

函数细节10
turn_start_forwards_client_metadata_to_responses_request_v241–123 ↗
async fn turn_start_forwards_client_metadata_to_responses_request_v2() -> Result<()>

作用:这个测试确认:客户端在 turn/start 请求里带上的自定义元数据,会被 app-server 放进发给 Responses API 的请求头里。同时它还确认服务端会补上 turn_id、session_id、installation_id、window_id 这些追踪字段。

数据流:测试先启动一个假的 Responses API,再写临时配置,让 app-server 使用这个假地址。接着它创建一个新线程,发送一轮用户输入,并附带 fiber_run_id、origin、thread_source 等客户端元数据。app-server 处理后会向假服务器发请求;测试读取那次请求里的 x-codex-turn-metadata 头,把 JSON 字符串解析出来,检查进去的客户端字段还在,同时服务端补充的回合、会话、安装和窗口信息也存在。

调用关系:这是最基础的 HTTP 转发场景。它会用 create_config_toml 准备配置,用测试服务器工具挂载一次 SSE 响应,再通过 TestAppServer 发送 thread/start 和 turn/start。最后借助 parse_json_header 读懂请求头里的 JSON,验证 app-server 到 Responses API 这条链路没有丢 metadata。

调用图:调用 5 个内部函数(new, create_config_toml, mount_sse_once, sse, start_mock_server);外部调用 10 个(default, from, new, Integer, default, assert!, assert_eq!, skip_if_no_network!, timeout, vec!)。

turn_start_sends_fork_lineage_in_turn_metadata_for_thread_fork_v2126–200 ↗
async fn turn_start_sends_fork_lineage_in_turn_metadata_for_thread_fork_v2() -> Result<()>

作用:这个测试确认:从旧线程 fork 出来的新线程,在发起新一轮对话时,会把“我是从哪个线程复制出来的”写进元数据。这样排查问题时能看出对话的血缘关系。

数据流:测试先造出一个假的已保存线程,再启动 app-server,并调用 fork_fake_rollout_thread 让 app-server 从这个旧线程 fork 出新线程。随后它在新线程里发送 turn/start。假 Responses API 收到请求后,测试解析 x-codex-turn-metadata,检查里面的 forked_from_thread_id 等于旧线程 id,thread_id 等于新线程 id,turn_id 等于这次新回合的 id。

调用关系:它覆盖的是“线程 fork 后再开始对话”的路径。测试本身负责搭环境和断言,fork 动作交给 fork_fake_rollout_thread,配置写入交给 create_config_toml,请求头解析交给 parse_json_header。

调用图:调用 6 个内部函数(new, create_config_toml, fork_fake_rollout_thread, mount_sse_once, sse, start_mock_server);外部调用 8 个(default, new, Integer, create_fake_rollout, assert_eq!, skip_if_no_network!, timeout, vec!)。

review_start_sends_parent_lineage_in_turn_metadata_for_thread_fork_v2203–300 ↗
async fn review_start_sends_parent_lineage_in_turn_metadata_for_thread_fork_v2() -> Result<()>

作用:这个测试确认:在 fork 出来的线程上发起 review(审查任务)时,app-server 会把 review 子任务和父线程的关系写对。这里的 review 会作为一个子代理请求发送,不应该继续沿用 forked_from_thread_id。

数据流:测试先准备一个假的旧线程,fork 出新线程,然后对这个新线程发起 review/start。假的 Responses API 返回一段符合 review 格式的结果。测试读取实际发出的请求:先确认请求头 x-openai-subagent 是 review,说明这是审查子任务;再解析 x-codex-turn-metadata,确认没有 forked_from_thread_id,而是有 parent_thread_id 指向 review 所属的线程,同时这次真正发给模型的 review 请求使用了一个单独的线程 id,并且 window_id 也和这个请求线程对应。

调用关系:它检查的是 review 子代理路径,比普通 turn/start 多一层“父线程和子任务线程”的关系。它复用 create_config_toml 创建配置,复用 fork_fake_rollout_thread 造 fork 场景,再通过 review/start 触发子代理请求,最后用 parse_json_header 检查 app-server 传给 Responses API 的标签是否符合子任务语义。

调用图:调用 6 个内部函数(new, create_config_toml, fork_fake_rollout_thread, mount_sse_once, sse, start_mock_server);外部调用 9 个(new, Integer, create_fake_rollout, assert!, assert_eq!, json!, skip_if_no_network!, timeout, vec!)。

turn_start_sends_other_subagent_lineage_after_cold_thread_resume_v2303–398 ↗
async fn turn_start_sends_other_subagent_lineage_after_cold_thread_resume_v2() -> Result<()>

作用:这个测试确认:一个已经保存在磁盘上的子代理线程,在 app-server 冷启动后被恢复,再继续对话时,仍然会带上正确的父线程 id 和子代理类型。换句话说,重启服务不能把线程身世忘掉。

数据流:测试先造一个带父线程 id 的假子代理 rollout,并把它标成 Other("guardian") 这种子代理来源。然后启动 app-server,调用 thread/resume 恢复这个线程,先检查恢复出来的线程确实带有 parent_thread_id 和 source。接着发送 turn/start。假服务器收到请求后,测试解析元数据,确认 parent_thread_id 是原父线程,subagent_kind 是 guardian,thread_id 是恢复的子代理线程,并且没有错误地出现 forked_from_thread_id。

调用关系:它覆盖“从磁盘恢复旧线程后再继续”的冷启动场景。造数据由 create_fake_parented_rollout_with_source 完成,配置由 create_config_toml 完成,app-server 先走 thread/resume,再走 turn/start,最后测试通过 parse_json_header 验证 Responses API 请求里的 lineage 信息。

调用图:调用 5 个内部函数(new, create_config_toml, mount_sse_once, sse, start_mock_server);外部调用 12 个(new, default, new, Integer, SubAgent, create_fake_parented_rollout_with_source, assert!, assert_eq!, Other, skip_if_no_network! (+2 more))。

turn_steer_updates_client_metadata_on_follow_up_responses_request_v2401–525 ↗
async fn turn_steer_updates_client_metadata_on_follow_up_responses_request_v2() -> Result<()>

作用:这个测试确认:一次对话进行中,如果客户端用 turn/steer 追加指导并带了新的元数据,后续发给 Responses API 的请求会使用新的元数据,而不是一直沿用 turn/start 的旧值。

数据流:测试搭了一个会返回两次响应的假服务器:第一次响应故意延迟,方便中途插入 steer。它先创建线程并用 turn/start 发起对话,带上 fiber_run_id=fiber-start-123。等第一条后端请求已经发出后,它发送 turn/steer,带上新的 fiber_run_id=fiber-steer-456 和 origin=gaas。最后测试拿到假服务器记录的两次请求:第一条应有旧 metadata,第二条应有新 metadata;两条都应指向同一个 turn_id。

调用关系:它检查的是长回合中的“追加指导”路径。它用 create_config_toml 配置假后端,用 mount_response_sequence 安排两次后端响应,用 wait_for_request_count 等第一条请求真的到达后再发 steer。这个测试保证 app-server 在同一个 turn 内更新 metadata 的时机是正确的。

调用图:调用 7 个内部函数(new, create_config_toml, wait_for_request_count, mount_response_sequence, sse, sse_response, start_mock_server);外部调用 10 个(default, from, new, Integer, default, assert_eq!, skip_if_no_network!, from_secs, timeout, vec!)。

turn_start_forwards_client_metadata_to_responses_websocket_request_body_v2528–623 ↗
async fn turn_start_forwards_client_metadata_to_responses_websocket_request_body_v2() -> Result<()>

作用:这个测试确认:当 Responses API 走 WebSocket(长连接通信方式)而不是普通 HTTP 时,客户端元数据也会被正确放进请求体里的 client_metadata,而不是丢失。WebSocket 可以理解成一条一直开着的电话线,不是每次都重新打电话。

数据流:测试启动一个假的 WebSocket 服务器,并让配置声明支持 WebSocket。app-server 初始化后会先发一个 warmup 请求,再在用户 turn/start 时发真正的 response.create 请求。测试等待并读取这两个 WebSocket 请求的 JSON 内容,确认 warmup 是 generate=false 的预热请求,真正请求接在 warmup 的 previous_response_id 后面;然后从 request["client_metadata"]["x-codex-turn-metadata"] 解析出 JSON,检查客户端传入的 fiber_run_id、origin,以及服务端补上的 turn_id、session_id、window_id 都在。

调用关系:它是普通 HTTP metadata 测试的 WebSocket 版本。配置仍由 create_config_toml 写入,但 supports_websockets 设为 true;后端由 start_websocket_server 提供。测试通过 TestAppServer 发 thread/start 和 turn/start,再直接检查 WebSocket 服务器记录到的请求体。

调用图:调用 3 个内部函数(new, create_config_toml, start_websocket_server);外部调用 10 个(default, from, new, Integer, default, assert!, assert_eq!, skip_if_no_network!, timeout, vec!)。

create_config_toml625–651 ↗
fn create_config_toml(
    codex_home: &Path,
    server_uri: &str,
    supports_websockets: bool,
) -> std::io::Result<()>

作用:这个辅助函数给每个测试写一份临时的 config.toml,让 app-server 知道该连哪个假的模型服务,以及这个服务是否支持 WebSocket。没有它,测试就可能误连真实服务或用错通信方式。

数据流:它接收一个临时目录路径、假服务器地址、以及 supports_websockets 这个开关。然后它拼出 config.toml 文件路径,把模型名、审批策略、沙盒模式、provider 地址、重试次数、wire_api 和 WebSocket 支持情况写进文件。结果是在临时目录里出现一份完整配置;如果写文件失败,就返回系统 IO 错误。

调用关系:几乎所有测试都会先调用它,因为 TestAppServer 启动时需要读取这份配置。它不参与请求验证本身,只负责把测试环境布置成“app-server 会连到假 Responses API”。

调用图:被 6 处调用(review_start_sends_parent_lineage_in_turn_metadata_for_thread_fork_v2, turn_start_forwards_client_metadata_to_responses_request_v2, turn_start_forwards_client_metadata_to_responses_websocket_request_body_v2, turn_start_sends_fork_lineage_in_turn_metadata_for_thread_fork_v2, turn_start_sends_other_subagent_lineage_after_cold_thread_resume_v2, turn_steer_updates_client_metadata_on_follow_up_responses_request_v2);外部调用 3 个(join, format!, write)。

fork_fake_rollout_thread653–670 ↗
async fn fork_fake_rollout_thread(
    mcp: &mut TestAppServer,
    source_thread_id: String,
) -> Result<ThreadForkResponse>

作用:这个辅助函数把“让 app-server 从一个已有假线程 fork 出新线程”这几步包起来,避免每个测试重复写同样的请求和等待代码。

数据流:它拿到一个正在运行的 TestAppServer 和一个源线程 id。然后发送 thread/fork 请求,指定源线程和线程来源,等待 app-server 返回对应的 JSON-RPC 响应,再把响应转换成 ThreadForkResponse。输出就是新 fork 出来的线程信息;过程中会通过 app-server 改变测试服务里的线程状态。

调用关系:它被两个需要 fork 场景的测试调用:普通 fork 后 turn/start 的测试,以及 fork 后 review/start 的测试。它把底层的 send_thread_fork_request、等待响应和 to_response 转换封装起来,让上层测试只关心 fork 结果。

调用图:调用 2 个内部函数(read_stream_until_response_message, send_thread_fork_request);被 2 处调用(review_start_sends_parent_lineage_in_turn_metadata_for_thread_fork_v2, turn_start_sends_fork_lineage_in_turn_metadata_for_thread_fork_v2);外部调用 3 个(default, Integer, timeout)。

parse_json_header672–674 ↗
fn parse_json_header(value: &str) -> serde_json::Value

作用:这个小工具把请求头里的 JSON 字符串变成可以按字段读取的 JSON 值。测试用它来检查 x-codex-turn-metadata 里面到底写了什么。

数据流:它接收一个字符串,比如请求头里的 metadata 内容。然后调用 JSON 解析器把字符串转成 serde_json::Value。解析成功就返回这个 JSON 值;如果字符串不是合法 JSON,测试会直接失败,并提示 metadata header 应该包含有效 JSON。

调用关系:多个测试在拿到假服务器记录的请求后都会用它。它的位置很靠后:app-server 已经发完请求,测试已经读到请求头或请求体里的 metadata 字符串,然后才调用它把字符串拆开检查。

调用图:外部调用 1 个(from_str)。

wait_for_request_count676–690 ↗
async fn wait_for_request_count(
    request_log: &core_test_support::responses::ResponseMock,
    expected: usize,
) -> Result<()>

作用:这个辅助函数会一直等,直到假 Responses API 至少收到了指定数量的请求。它主要用来避免测试跑太快,在后端请求还没到时就继续下一步。

数据流:它接收一个请求日志对象和期望的请求数量。函数在最多 DEFAULT_READ_TIMEOUT 的时间内循环检查 request_log.requests().len(),如果数量够了就返回成功;如果还不够,就每 10 毫秒睡一下再看。超过时间还不够,就返回超时错误。

调用关系:它被 turn_steer_updates_client_metadata_on_follow_up_responses_request_v2 使用。那个测试必须先确认第一条 Responses API 请求已经发出,再发送 turn/steer,这样才能可靠地区分第一条请求使用旧 metadata、第二条请求使用新 metadata。

调用图:调用 1 个内部函数(requests);被 1 处调用(turn_steer_updates_client_metadata_on_follow_up_responses_request_v2);外部调用 3 个(from_millis, sleep, timeout)。

app-server/tests/suite/v2/dynamic_tools.rs源码 ↗
testtest execution

动态工具可以理解成“临时给模型的一盒彩色笔”:客户端在开启对话线程时告诉服务器有哪些工具,模型之后就能按名字调用这些工具。这个测试文件就是防止这条链路出错。它会启动一个假的模型服务,再启动真实的测试版 app-server,通过 JSON-RPC(一种用 JSON 发请求和响应的通信格式)和服务器对话。测试覆盖几件关键事:老格式的工具会被整理成新格式;不合法的工具配置会被拒绝;模型发起工具调用时,app-server 会通知客户端执行;客户端把文字或图片等结果返回后,服务器会把这些结果包装成模型能懂的 function_call_output。文件末尾还有一些小帮手,用来读取假模型收到的请求、查找工具、等待工具开始和完成的通知,以及写测试配置文件。

函数细节12
thread_start_normalizes_legacy_dynamic_tools_into_model_request49–161 ↗
async fn thread_start_normalizes_legacy_dynamic_tools_into_model_request() -> Result<()>

作用:这个测试确认旧版写法的动态工具,在开启线程后会被服务器转换成模型接口需要的新格式。这样老客户端不用立刻改代码,也不会把错误形状的工具说明发给模型。

数据流:进去的是一组旧格式 dynamicTools、一个假的模型响应服务器、以及临时配置目录。测试先写配置并启动 TestAppServer,然后发起 thread/start 和 turn/start,最后去假模型服务器收到的请求里找 tools 字段。出来的结果是断言:普通工具被变成 function,带 namespace 的工具被变成 namespace,隐藏工具不会暴露给模型。

调用关系:它是完整流程测试的开头场景之一。它会调用 create_config_toml 准备配置,用 create_mock_responses_server_sequence_unchecked 准备假模型,用 responses_bodies 取回模型请求,再用 find_tool 从请求里挑出要检查的工具。

调用图:调用 4 个内部函数(new, create_config_toml, find_tool, responses_bodies);外部调用 8 个(default, new, Integer, create_mock_responses_server_sequence_unchecked, assert_eq!, json!, timeout, vec!)。

thread_start_rejects_hidden_dynamic_tools_without_namespace164–200 ↗
async fn thread_start_rejects_hidden_dynamic_tools_without_namespace() -> Result<()>

作用:这个测试确认“隐藏加载”的动态工具如果没有放进命名空间,会被服务器拒绝。命名空间可以理解成工具的文件夹;没有文件夹,服务器就不知道该怎么安全地延迟展示它。

数据流:进去的是一个 defer_loading 为 true、但没有 namespace 的工具定义。测试启动服务器后发 thread/start。出来的不是正常线程,而是一个 JSON-RPC 错误;测试检查错误码是 -32600,并且错误信息里提到工具名和 namespace。

调用关系:它验证输入校验这一关。它会用 create_config_toml 建测试配置,用 MockServer 提供假地址,用 TestAppServer 发请求并等待错误响应。

调用图:调用 2 个内部函数(new, create_config_toml);外部调用 10 个(default, start, new, Integer, assert!, assert_eq!, json!, Function, timeout, vec!)。

thread_start_rejects_invalid_dynamic_tool_inputs203–317 ↗
async fn thread_start_rejects_invalid_dynamic_tool_inputs() -> Result<()>

作用:这个测试集中检查几种坏的 dynamicTools 输入都会被拒绝。它防止客户端把新旧格式混在一起、传空命名空间、或重复命名空间,导致后面模型调用时含义不清。

数据流:进去的是多组故意写错的 JSON 工具配置,以及每组应该出现的错误提示关键词。测试循环发送 thread/start。每次出来都应该是 -32600 的请求错误,并且错误消息要包含对应原因。

调用关系:它像一张“坏输入清单”。它同样先通过 create_config_toml 和 TestAppServer 搭好环境,然后逐个 raw request 直接投喂 JSON,确认服务器入口层会挡住问题数据。

调用图:调用 2 个内部函数(new, create_config_toml);外部调用 7 个(start, new, Integer, assert!, assert_eq!, json!, timeout)。

dynamic_tool_call_round_trip_sends_text_content_items_to_model321–551 ↗
async fn dynamic_tool_call_round_trip_sends_text_content_items_to_model() -> Result<()>

作用:这个测试跑通一次完整的动态工具调用:模型要求调用工具,服务器转给客户端,客户端返回文字结果,服务器再把结果发回模型。它证明这条来回通道没有断。

数据流:进去的是一个假模型的两段流式响应:第一段要求调用 codex_app/demo_tool,第二段结束回合;还有客户端声明的命名空间工具和工具返回的文字 dynamic-ok。测试启动线程和回合,等待 item/started 通知,读取服务器发给客户端的 DynamicToolCall 请求,发送 DynamicToolCallResponse,再等待 item/completed。最后检查假模型收到的请求:工具定义格式正确,后续请求里有对应 call_id 的 function_call_output,内容是 dynamic-ok。

调用关系:这是本文件最完整的主流程测试之一。它会用 wait_for_dynamic_tool_started 和 wait_for_dynamic_tool_completed 观察服务器通知,用 responses_bodies 和 find_tool 检查发给模型的请求,并用 FunctionCallOutputPayload::from_text 对比最终送回模型的内容。

调用图:调用 7 个内部函数(new, create_config_toml, find_tool, responses_bodies, wait_for_dynamic_tool_completed, wait_for_dynamic_tool_started, from_text);外部调用 13 个(default, new, Integer, create_mock_responses_server_sequence_unchecked, assert!, assert_eq!, json!, panic!, Namespace, to_string (+3 more))。

dynamic_tool_call_round_trip_sends_content_items_to_model555–745 ↗
async fn dynamic_tool_call_round_trip_sends_content_items_to_model() -> Result<()>

作用:这个测试确认动态工具返回的不只是一段文字,也可以是结构化内容,比如文字加图片。它保证这些内容会按模型接口要求转换,图片还会补上默认清晰度。

数据流:进去的是一个模型发出的工具调用、一个客户端工具定义、以及客户端返回的两个内容项:input_text 和 input_image。测试启动线程和回合,等待工具开始,读取 DynamicToolCall 请求,回传内容项,再等待工具完成。出来的检查包括:完成通知里保留了原始内容项;发给模型的 function_call_output 里,图片带上 detail: high;解析后的 payload 内容和预期一致。

调用关系:它和文字版 round trip 测试互补,专门覆盖多内容项和图片路径。它调用 wait_for_dynamic_tool_started、wait_for_dynamic_tool_completed 等等待工具生命周期事件,再用 responses_bodies 读取假模型实际收到的请求。

调用图:调用 5 个内部函数(new, create_config_toml, responses_bodies, wait_for_dynamic_tool_completed, wait_for_dynamic_tool_started);外部调用 12 个(default, new, Integer, create_mock_responses_server_sequence_unchecked, assert_eq!, json!, panic!, Function, to_string, to_value (+2 more))。

responses_bodies747–761 ↗
async fn responses_bodies(server: &MockServer) -> Result<Vec<Value>>

作用:这个小工具函数把假模型服务器收到的 /responses 请求正文都取出来。测试需要靠它查看 app-server 到底把什么发给了模型。

数据流:进去的是 MockServer,也就是测试里的假模型服务。它读取这个服务器收到的所有请求,筛选 URL 路径以 /responses 结尾的请求,把每个请求体当 JSON 解析。出来的是一组 JSON 值;如果拿不到请求或请求体不是 JSON,就返回错误。

调用关系:它被多个测试用来做“事后查账”:thread_start_normalizes_legacy_dynamic_tools_into_model_request 用它看工具定义,两个 dynamic_tool_call_round_trip 测试用它看工具调用结果是否送回模型。

调用图:被 3 处调用(dynamic_tool_call_round_trip_sends_content_items_to_model, dynamic_tool_call_round_trip_sends_text_content_items_to_model, thread_start_normalizes_legacy_dynamic_tools_into_model_request);外部调用 1 个(received_requests)。

find_tool763–771 ↗
fn find_tool(body: &'a Value, name: &str) -> Option<&'a Value>

作用:这个小工具函数在一次模型请求的 tools 列表里按名字找工具。它让测试不用手写一堆 JSON 查找代码。

数据流:进去的是一个 JSON 请求体和要找的工具名。它先取出 body.tools 数组,再逐个看每个工具的 name 字段。出来的是找到的那个 JSON 工具引用;如果没有 tools、不是数组、或名字对不上,就返回 None。

调用关系:它服务于检查工具定义的测试。thread_start_normalizes_legacy_dynamic_tools_into_model_request 用它找旧格式转换后的工具,dynamic_tool_call_round_trip_sends_text_content_items_to_model 用它找命名空间工具。

调用图:被 2 处调用(dynamic_tool_call_round_trip_sends_text_content_items_to_model, thread_start_normalizes_legacy_dynamic_tools_into_model_request);外部调用 1 个(get)。

function_call_output_payload773–776 ↗
fn function_call_output_payload(body: &Value, call_id: &str) -> Option<FunctionCallOutputPayload>

作用:这个函数从模型请求里抽出某次工具调用的输出,并把它解析成项目里的 FunctionCallOutputPayload 类型。也就是把原始 JSON 变成更好比较的结构化数据。

数据流:进去的是一个 JSON 请求体和 call_id。它先把查找原始 output 的工作交给 function_call_output_raw_output,然后尝试用 serde_json 解析成 FunctionCallOutputPayload。出来的是解析成功后的 payload;找不到或解析失败就返回 None。

调用关系:它站在测试断言前一层,负责把原始模型请求变成可比较的数据。它依赖 function_call_output_raw_output 找到具体 JSON,再由测试拿结果和期望值对比。

调用图:调用 1 个内部函数(function_call_output_raw_output)。

function_call_output_raw_output778–789 ↗
fn function_call_output_raw_output(body: &Value, call_id: &str) -> Option<Value>

作用:这个函数在模型请求的 input 列表里,找到指定 call_id 对应的 function_call_output 原始 output。它适合用来检查 JSON 长什么样。

数据流:进去的是一个 JSON 请求体和工具调用编号 call_id。它读取 body.input 数组,找到 type 等于 function_call_output 且 call_id 匹配的条目,然后取出它的 output 字段并复制出来。找不到就返回 None。

调用关系:它是 function_call_output_payload 的底层查找器,也被测试用来直接比较原始 JSON。这样既能检查语义解析后的 payload,也能检查发给模型的字面 JSON 是否正确。

调用图:被 1 处调用(function_call_output_payload);外部调用 1 个(get)。

wait_for_dynamic_tool_started791–809 ↗
async fn wait_for_dynamic_tool_started(
    mcp: &mut TestAppServer,
    call_id: &str,
) -> Result<ItemStartedNotification>

作用:这个异步函数一直等到服务器发出某个动态工具调用“开始了”的通知。测试用它确认模型的工具调用已经被 app-server 记录并公布出来。

数据流:进去的是 TestAppServer 连接和目标 call_id。它循环读取 item/started 通知,每次用超时时间防止测试卡死;如果通知没有参数就跳过,有参数就解析成 ItemStartedNotification,并检查里面是不是目标 DynamicToolCall。出来的是匹配的开始通知。

调用关系:它被两个 round trip 测试调用,位置在启动回合之后、读取客户端工具请求之前。它只负责等正确事件,不执行工具;后续测试会继续读取 ServerRequest::DynamicToolCall。

调用图:调用 1 个内部函数(read_stream_until_notification_message);被 2 处调用(dynamic_tool_call_round_trip_sends_content_items_to_model, dynamic_tool_call_round_trip_sends_text_content_items_to_model);外部调用 3 个(matches!, from_value, timeout)。

wait_for_dynamic_tool_completed811–829 ↗
async fn wait_for_dynamic_tool_completed(
    mcp: &mut TestAppServer,
    call_id: &str,
) -> Result<ItemCompletedNotification>

作用:这个异步函数一直等到服务器发出某个动态工具调用“完成了”的通知。测试用它确认客户端返回结果后,服务器已经把工具状态更新完。

数据流:进去的是 TestAppServer 连接和目标 call_id。它循环读取 item/completed 通知,带超时保护;解析通知参数后,只接受 item 是目标 DynamicToolCall 的那一条。出来的是匹配的完成通知,里面包含状态、内容项、成功标记和耗时等信息。

调用关系:它被两个 round trip 测试调用,位置在测试向服务器发送 DynamicToolCallResponse 之后。它帮助测试确认工具调用生命周期从 InProgress 走到了 Completed。

调用图:调用 1 个内部函数(read_stream_until_notification_message);被 2 处调用(dynamic_tool_call_round_trip_sends_content_items_to_model, dynamic_tool_call_round_trip_sends_text_content_items_to_model);外部调用 3 个(matches!, from_value, timeout)。

create_config_toml831–852 ↗
fn create_config_toml(codex_home: &Path, server_uri: &str) -> std::io::Result<()>

作用:这个函数给每个测试写一份最小可用的 config.toml 配置文件。没有它,测试版 app-server 不知道该用哪个模型地址,也就无法连到假模型服务器。

数据流:进去的是临时的 codex_home 路径和假模型服务器地址。它拼出 config.toml 文件路径,把模型名、审批策略、沙箱模式、模型供应商、base_url、重试次数等内容写进去。出来的是写文件的结果;成功则磁盘上多了一份测试配置,失败则返回 IO 错误。

调用关系:它是所有测试启动 app-server 前的准备步骤。多个测试先创建临时目录和假服务器,再调用 create_config_toml,随后 TestAppServer::new 才能按这份配置启动并把请求发到 MockServer。

调用图:被 5 处调用(dynamic_tool_call_round_trip_sends_content_items_to_model, dynamic_tool_call_round_trip_sends_text_content_items_to_model, thread_start_normalizes_legacy_dynamic_tools_into_model_request, thread_start_rejects_hidden_dynamic_tools_without_namespace, thread_start_rejects_invalid_dynamic_tool_inputs);外部调用 3 个(join, format!, write)。

app-server/tests/suite/v2/memory_reset.rs源码 ↗
testtest run

这个测试文件像一次彩排:先搭一个临时的 Codex 家目录,写入测试配置,启动一份状态数据库,再故意塞进去一些“旧记忆”文件和数据库里的记忆记录。接着它启动测试版 app server,通过 JSON-RPC(一种用 JSON 格式发请求、收回应的通信方式)调用 memory/reset。调用结束后,测试会检查三件事:数据库里的阶段一记忆输出已经没了;磁盘上的 memories 目录也空了;但原来的线程记录还在,并且它的记忆模式仍是 enabled。也就是说,这个文件验证的是“清理记忆”要清得彻底,但不能把用户会话历史这类基础信息一起误删。辅助函数负责准备配置、初始化数据库、造一条假的记忆处理结果,让主测试能专注检查重置接口的真实效果。

函数细节4
memory_reset_clears_memory_files_and_rows_preserves_threads23–69 ↗
async fn memory_reset_clears_memory_files_and_rows_preserves_threads() -> Result<()>

作用:这是主测试。它模拟一个已经有旧记忆文件和记忆数据库记录的环境,然后调用 memory/reset,确认旧记忆被清空,但线程信息没有被破坏。

数据流:进去的是一个全新的临时目录,测试先往里面写配置、建数据库、放旧的 MEMORY.md 和 rollout summary 文件,并通过辅助函数塞入一条假的记忆结果。然后它启动测试服务器,发送 memory/reset 请求,读取返回结果。出来的结果是:接口返回成功;数据库里查不到旧的阶段一记忆输出;线程的记忆模式仍然是 enabled;磁盘上的 memories 目录已经没有剩余文件。

调用关系:它是整个文件的导演。开头调用 create_config_toml 准备配置,调用 init_state_db 准备状态数据库,再调用 seed_stage1_output 制造测试用的旧记忆数据。随后它用 TestAppServer 启动服务并发起请求,最后用断言检查重置功能有没有做到“删记忆、不删线程”。

调用图:调用 4 个内部函数(new, create_config_toml, init_state_db, seed_stage1_output);外部调用 8 个(new, Integer, assert!, assert_eq!, create_dir_all, read_dir, write, timeout)。

seed_stage1_output71–119 ↗
async fn seed_stage1_output(state_db: &Arc<StateRuntime>, codex_home: &Path) -> Result<ThreadId>

作用:这个函数用来给测试数据库预先塞一条假的“记忆生成结果”。没有这一步,memory/reset 就没有东西可删,测试也就不能证明它真的有效。

数据流:进去的是状态数据库和临时家目录路径。它生成当前时间、一个假的线程 ID、一个假的 worker ID,然后创建线程元数据并写入数据库。接着它模拟一个记忆处理任务被领取、成功完成,并写入 raw memory 和 rollout summary 这两份记忆内容,最后安排一次全局合并任务。出来的是刚创建的线程 ID,供主测试稍后确认这个线程没有被 reset 删掉。

调用关系:它只被主测试 memory_reset_clears_memory_files_and_rows_preserves_threads 调用,作用是布置“案发现场”。它会使用状态库里的 memories 子系统来领取任务、标记成功、排队合并,让数据库看起来像真的跑过一次记忆流程。

调用图:调用 2 个内部函数(from_string, new);被 1 处调用(memory_reset_clears_memory_files_and_rows_preserves_threads);外部调用 6 个(join, to_path_buf, now, new_v4, bail!, assert!)。

init_state_db121–127 ↗
async fn init_state_db(codex_home: &Path) -> Result<Arc<StateRuntime>>

作用:这个函数创建测试用的状态数据库,并把必要的初始化标记补齐。它让后面的测试能像面对一套已经准备好的真实数据库一样操作。

数据流:进去的是临时家目录路径。它用这个路径初始化 StateRuntime,也就是测试里的状态数据库运行对象,并指定 mock_provider 作为假的模型提供方。随后它标记回填已经完成。出来的是一个可共享的数据库句柄,主测试和造数据函数都可以拿它读写状态。

调用关系:它被主测试在早期调用,位置在写配置之后、塞测试数据之前。它不直接参与 reset 请求,而是为 seed_stage1_output 和后续断言提供同一个数据库基础。

调用图:调用 1 个内部函数(init);被 1 处调用(memory_reset_clears_memory_files_and_rows_preserves_threads);外部调用 1 个(to_path_buf)。

create_config_toml129–151 ↗
fn create_config_toml(codex_home: &Path) -> std::io::Result<()>

作用:这个函数给临时家目录写一份测试专用的 config.toml 配置文件。没有这份配置,测试服务器可能不知道该用哪个模型、哪些功能要打开,也就启动不起来或行为不稳定。

数据流:进去的是临时家目录路径。它在这个目录下生成 config.toml,并写入一套固定配置:使用 mock-model,禁止审批,启用 sqlite 功能,配置一个不会真正联网成功的 mock_provider。出来的是写文件是否成功的结果;它会在磁盘上留下这份配置文件。

调用关系:它是主测试最早调用的准备步骤之一。它不碰数据库,也不发请求,只负责把测试服务器启动所需的配置摆好,让 TestAppServer 后面能按预期运行。

调用图:被 1 处调用(memory_reset_clears_memory_files_and_rows_preserves_threads);外部调用 2 个(join, write)。

app-server/tests/suite/v2/output_schema.rs源码 ↗
testtest run

这是一组自动化测试,专门盯住 v2 接口里的 output_schema。output_schema 可以理解成“请按这个表格格式回答”的规则,这里用的是 JSON Schema(一种描述 JSON 数据长什么样的格式)。测试会先启动一个假的模型服务器,就像搭了一个临时柜台,专门接收 app-server 发出去的请求;再写一份临时配置,让 app-server 以为模型服务就在这个假服务器上。随后测试通过 TestAppServer 启动真实的应用服务器流程,创建一个 thread(对话线程),再发起 turn(一次用户提问和模型回答的回合)。第一个测试确认:带上的 schema 最终出现在发给模型的请求里的 text.format 字段中,并且被包装成严格的 json_schema 格式。第二个测试确认:第一回合带 schema,第二回合不带时,第二次请求里不会残留上一次的格式要求。这个文件重要在于防止“格式要求丢失”或“格式要求串场”,否则用户可能拿不到想要的结构化答案,或者下一次普通聊天也被错误地强制成 JSON。

函数细节3
turn_start_accepts_output_schema_v221–101 ↗
async fn turn_start_accepts_output_schema_v2() -> Result<()>

作用:这个测试确认 v2 的 turn_start 请求能接受 output_schema,并且服务器会把它正确塞进发给模型服务的请求里。简单说,它检查“用户要求按某种 JSON 格式回答”这件事有没有真的传到下游。

数据流:进去的是一个临时配置目录、一个假的模型服务器地址、一个新建的对话线程,以及一份描述答案必须包含 answer 字符串字段的 JSON Schema。函数先启动假模型服务器并准备一段假的流式回答,再启动测试用 app-server,创建线程,发送带 output_schema 的 turn_start 请求。出来的结果是:测试读取模型服务器实际收到的请求体,检查其中的 text.format 是否正好等于预期包装后的 schema;如果不一致,测试失败。它还会等待 turn/completed 通知,确保这一回合真的跑完了。

调用关系:这是一个独立的异步测试入口。它会调用 start_mock_server 搭假模型服务,用 sse 和 mount_sse_once 准备一次假的模型响应,用 create_config_toml 写测试配置,再通过 TestAppServer 与应用服务器交互。create_config_toml 在这里负责把服务器指向假模型服务,这样测试可以检查真实发出的请求而不用访问真正的外部模型。

调用图:调用 5 个内部函数(new, create_config_toml, mount_sse_once, sse, start_mock_server);外部调用 8 个(default, new, Integer, assert_eq!, json!, skip_if_no_network!, timeout, vec!)。

turn_start_output_schema_is_per_turn_v2104–214 ↗
async fn turn_start_output_schema_is_per_turn_v2() -> Result<()>

作用:这个测试确认 output_schema 是“一回合一设置”,不会从上一回合自动带到下一回合。也就是说,第一次要求 JSON 格式回答,不代表第二次也必须按这个格式回答。

数据流:进去的是同一个临时 app-server、同一个假模型服务器、一个对话线程,以及两次 turn_start 请求:第一次带 output_schema,第二次明确不带。函数先让假模型服务器接收第一回合,检查第一份请求体里有 text.format;然后再挂载第二次假的模型响应,发送第二回合请求,并检查第二份请求体里没有 /text/format。出来的结果是两个断言:第一回合格式要求存在,第二回合格式要求不存在;如果 schema 被错误地记住并复用,测试就会失败。

调用关系:这是另一个异步测试入口,流程比前一个更像“连续对话”的真实场景。它同样调用 start_mock_server、sse、mount_sse_once 和 create_config_toml 来搭测试环境;不同点是它连续发两次 turn_start,请假模型服务器分别记录两次请求,用来证明 output_schema 没有在服务器内部串到下一次。

调用图:调用 5 个内部函数(new, create_config_toml, mount_sse_once, sse, start_mock_server);外部调用 8 个(default, new, Integer, assert_eq!, json!, skip_if_no_network!, timeout, vec!)。

create_config_toml216–237 ↗
fn create_config_toml(codex_home: &Path, server_uri: &str) -> std::io::Result<()>

作用:这个辅助函数给测试写一份最小可用的 config.toml 配置文件。它的作用是告诉 app-server:模型名字是什么、不要审批、只读沙箱、以及模型服务地址就是测试里启动的假服务器。

数据流:进去的是一个临时的 codex_home 路径和假模型服务器的地址。函数把路径拼成 codex_home/config.toml,然后把一段 TOML 配置文本写进去,其中 base_url 会填成传入的 server_uri 加上 /v1。出来的结果是磁盘上多了一份配置文件;如果写文件失败,就返回 std::io::Result 里的错误。

调用关系:它不是测试主体,而是两个测试共同使用的搭台工具。turn_start_accepts_output_schema_v2 和 turn_start_output_schema_is_per_turn_v2 在启动 TestAppServer 之前都会先调用它,因为 app-server 启动时需要读取配置;没有这份配置,测试服务器就不知道应该把模型请求发到哪个假服务上。

调用图:被 2 处调用(turn_start_accepts_output_schema_v2, turn_start_output_schema_is_per_turn_v2);外部调用 3 个(join, format!, write)。

app-server/tests/suite/v2/plan_item.rs源码 ↗
testtest run

这个测试文件像一次排练:先搭一个假的模型服务器,让它按测试要求返回固定内容;再启动真实的 app server 测试实例;然后发起一个“计划模式”的对话回合。重点检查两种情况:如果模型回复里有 <proposed_plan> 这段特殊标记,服务器应该把中间的计划文字抽出来,一边流式发出计划增量,一边在完成列表里生成一个 ThreadItem::Plan;同时普通助手消息也不能丢。如果回复里没有这个标记,就不应该凭空造出计划项。文件里还有几个小帮手:写临时配置、启动计划回合、收集通知、等待假服务器收到请求。这样测试既接近真实运行,又不会真的依赖外部模型服务。

函数细节6
plan_mode_uses_proposed_plan_block_for_plan_item39–98 ↗
async fn plan_mode_uses_proposed_plan_block_for_plan_item() -> Result<()>

作用:这是主测试之一,用来确认计划模式能识别模型回复里的 <proposed_plan> 块,并把里面的内容单独发成计划项。它还检查普通助手消息不会因为抽取计划而消失。

数据流:进去的是一段人为准备好的假模型回复,里面包含前言、<proposed_plan> 包住的计划、以及后记。测试把这段回复放进假服务器,写好临时配置,启动测试用 app server,再发起计划模式回合。之后它收集服务器发出的通知,确认计划增量拼起来正好是计划正文,完成项里有正确的 Plan,并且回合状态是完成;同时它还确认假模型接口只被请求了一次。

调用关系:它是整条测试流程的驾驶员。它先调用 create_config_toml 准备配置,再用 start_plan_mode_turn 开始一次计划回合,随后交给 collect_turn_notifications 收集服务器通知,最后用 wait_for_responses_request_count 确认后端请求次数符合预期。

调用图:调用 5 个内部函数(new, collect_turn_notifications, create_config_toml, start_plan_mode_turn, wait_for_responses_request_count);外部调用 8 个(new, create_mock_responses_server_sequence_unchecked, assert!, assert_eq!, format!, skip_if_no_network!, timeout, vec!)。

plan_mode_without_proposed_plan_does_not_emit_plan_item101–128 ↗
async fn plan_mode_without_proposed_plan_does_not_emit_plan_item() -> Result<()>

作用:这是另一个主测试,用来确认服务器不会过度解读普通回复。也就是说,只有明确出现 <proposed_plan> 标记时,才应该生成计划项。

数据流:进去的是一段普通的假模型回复,比如 “Done”,没有计划标记。测试启动同样的临时环境并发起计划模式回合,然后收集所有通知。出来的检查结果应该是:完成项里没有 ThreadItem::Plan,计划增量列表也是空的,并且模型接口只被调用一次。

调用关系:它复用了和正向测试相同的基础设施:create_config_toml 负责写配置,start_plan_mode_turn 负责开回合,collect_turn_notifications 负责收消息,wait_for_responses_request_count 负责确认假服务器确实收到了预期数量的请求。它和前一个测试一起证明“该生成时生成,不该生成时不生成”。

调用图:调用 5 个内部函数(new, collect_turn_notifications, create_config_toml, start_plan_mode_turn, wait_for_responses_request_count);外部调用 6 个(new, create_mock_responses_server_sequence_unchecked, assert!, skip_if_no_network!, timeout, vec!)。

start_plan_mode_turn130–170 ↗
async fn start_plan_mode_turn(mcp: &mut TestAppServer) -> Result<codex_app_server_protocol::Turn>

作用:这个辅助函数负责帮测试创建一个线程,并在这个线程里启动一次“计划模式”的回合。测试用它避免每个用例都重复写一大段启动对话的代码。

数据流:进去的是一个已经启动好的 TestAppServer,也就是测试里的 app server 客户端。函数先发送 thread/start 请求创建对话线程,并等待对应响应;拿到线程 id 后,它构造 CollaborationMode,其中 ModeKind::Plan 表示计划模式,再发送 turn/start 请求,输入文字是 “Plan this”。最后它等待回合启动响应,把里面的 Turn 返回给调用者。

调用关系:它被两个主测试调用,位置在配置和初始化之后、收集通知之前。它把“创建线程”和“启动计划回合”这两个步骤包装成一个动作,内部依赖 TestAppServer 的发送请求和读取响应能力,并用 timeout 防止测试卡死。

调用图:调用 3 个内部函数(read_stream_until_response_message, send_thread_start_request, send_turn_start_request);被 2 处调用(plan_mode_uses_proposed_plan_block_for_plan_item, plan_mode_without_proposed_plan_does_not_emit_plan_item);外部调用 4 个(default, Integer, timeout, vec!)。

collect_turn_notifications172–221 ↗
async fn collect_turn_notifications(
    mcp: &mut TestAppServer,
) -> Result<(
    Vec<ThreadItem>,
    Vec<ThreadItem>,
    Vec<PlanDeltaNotification>,
    TurnCompletedNotification,
)>

作用:这个辅助函数负责从服务器的消息流里捞出本次回合的重要通知。它像一个分拣员,把“项目开始”“项目完成”“计划文字增量”“回合完成”分别放到不同篮子里。

数据流:进去的是 TestAppServer 的可变引用,函数会不断读取下一条 JSON-RPC 消息。JSON-RPC 可以理解成一种用 JSON 包装请求、响应、通知的通信格式。它只关心通知消息:看到 item/started 就解析成 ItemStartedNotification,看到 item/completed 就保存完成项,看到 item/plan/delta 就保存计划增量,看到 turn/completed 就解析完成通知并立刻返回。出来的是四样东西:开始项列表、完成项列表、计划增量列表、以及回合完成通知。

调用关系:它被两个主测试在启动回合后调用,是测试观察服务器行为的窗口。它内部调用 read_next_message 持续读消息,并用 serde_json::from_value 把原始 JSON 参数变成具体通知类型;一旦收到 turn/completed,就说明这轮对话可以结束检查了。

调用图:调用 1 个内部函数(read_next_message);被 2 处调用(plan_mode_uses_proposed_plan_block_for_plan_item, plan_mode_without_proposed_plan_does_not_emit_plan_item);外部调用 3 个(new, from_value, timeout)。

wait_for_responses_request_count223–251 ↗
async fn wait_for_responses_request_count(
    server: &MockServer,
    expected_count: usize,
) -> Result<()>

作用:这个辅助函数用来等待假模型服务器收到指定数量的 /responses 请求。它确保测试不只是看到了本地通知,也确认服务器真的向模型接口发起了预期次数的调用。

数据流:进去的是 wiremock 的 MockServer 和期望的请求数量。函数在一个超时时间内反复查看假服务器记录下来的请求,只统计方法是 POST、路径以 /responses 结尾的请求。如果数量正好等于期望值,就返回成功;如果超过了,就直接报错;如果还没到,就睡 10 毫秒再看一次。

调用关系:它在两个主测试的后半段被调用,用来给网络请求一点异步完成的时间。它不参与生成计划项本身,而是守住测试的外部行为:一次回合应该只打到假模型接口一次,不能偷偷多请求。

调用图:被 2 处调用(plan_mode_uses_proposed_plan_block_for_plan_item, plan_mode_without_proposed_plan_does_not_emit_plan_item);外部调用 5 个(received_requests, bail!, from_millis, sleep, timeout)。

create_config_toml253–290 ↗
fn create_config_toml(codex_home: &Path, server_uri: &str) -> std::io::Result<()>

作用:这个辅助函数给临时测试目录写一份 config.toml 配置文件,让 app server 知道该用哪个模型、哪个假服务地址,以及要打开协作模式功能。

数据流:进去的是临时的 codex_home 路径和假模型服务器地址。函数先把 CollaborationModes 这个功能开关写成配置文本,再生成完整的 TOML 配置:模型名是 mock-model,审批策略是 never,沙箱是只读,模型提供方指向传入的 server_uri,并使用 responses 接口。最后它把这些内容写到 codex_home/config.toml,成功时返回空结果,失败时返回文件写入错误。

调用关系:它被两个主测试在启动 TestAppServer 之前调用,是测试环境能跑起来的前置准备。没有它,服务器不知道要连测试用的假模型服务,也可能没有打开计划模式所需的协作模式功能。

调用图:被 2 处调用(plan_mode_uses_proposed_plan_block_for_plan_item, plan_mode_without_proposed_plan_does_not_emit_plan_item);外部调用 4 个(from, join, format!, write)。

app-server/tests/suite/v2/request_permissions.rs源码 ↗
testtest run

这个测试文件像是在排练一次完整对话:先搭一个假的模型服务,让它假装回答“我需要更多文件权限”;再启动真实的测试版应用服务器;然后像客户端一样发起线程和一轮对话。测试会检查服务器是不是发出了正确的权限审批请求,比如请求属于哪个线程、哪一轮对话、为什么要权限、想写哪些路径。接着测试假装用户批准其中一部分权限,并继续监听服务器消息。它特别关心两个结果:第一,服务器要先发出“这个请求已经处理完了”的通知;第二,之后任务才能结束。这样可以防止权限功能表面能用、实际消息顺序或内容却不对的问题。文件里还会临时写一份 config.toml 配置文件,把服务器指向假的模型服务,并打开权限请求工具开关。

函数细节2
request_permissions_round_trip24–155 ↗
async fn request_permissions_round_trip() -> Result<()>

作用:这是主测试函数,验证一次权限申请从发出、被客户端批准、到服务器通知已解决并完成任务的完整来回流程。有人改权限请求协议、消息顺序或配置时,这个测试能及时发现有没有破坏功能。

数据流:进去的是测试临时目录、假的模型响应,以及测试客户端发出的线程启动和对话启动请求。函数先创建临时配置,把模型请求导向假的服务器;再启动测试应用服务器,并发起一轮用户消息。假的模型返回“需要权限”后,函数读取服务器发给客户端的权限审批请求,检查里面的线程、轮次、原因、路径和文件写权限是否正确。然后它模拟客户端批准一部分权限,把审批结果发回服务器。最后它继续读消息,确认先收到 serverRequest/resolved,也就是“权限请求已处理”的通知,再收到 turn/completed,也就是这一轮对话结束。

调用关系:它是这个文件的核心入口,由测试框架 tokio 在测试运行时自动调用。它自己会调用 create_config_toml 准备配置文件,也会借助测试支持工具启动假模型服务、启动 TestAppServer、发送请求、读取 JSON-RPC 消息。整个故事里,它扮演“测试导演”:安排环境、触发流程、检查每一步有没有按约定发生。

调用图:调用 2 个内部函数(new, create_config_toml);外部调用 12 个(default, Integer, create_mock_responses_server_sequence, to_response, assert!, assert_eq!, panic!, from_value, to_value, new (+2 more))。

create_config_toml157–181 ↗
fn create_config_toml(codex_home: &std::path::Path, server_uri: &str) -> std::io::Result<()>

作用:这个辅助函数负责给测试写一份临时配置文件,让应用服务器知道该用哪个假模型服务,以及要打开权限请求功能。没有这份配置,测试服务器可能会连到错误的地方,或者根本不会走权限请求这条路。

数据流:进去的是一个临时的 Codex 主目录路径和假的模型服务器地址。函数把路径拼成 config.toml 文件名,然后写入一段 TOML 配置文本。写完之后,这个临时目录里就有了测试服务器启动时会读取的配置:模型叫 mock-model,审批策略是不可信环境,沙箱是只读,模型提供方指向传入的 server_uri,并启用 request_permissions_tool。

调用关系:它只被 request_permissions_round_trip 调用,发生在测试服务器启动之前。它不参与后面的消息收发,只负责把“舞台布置好”:让后续启动的 TestAppServer 读取到正确配置,从而把模型请求发给假服务器,并触发权限申请流程。

调用图:被 1 处调用(request_permissions_round_trip);外部调用 3 个(join, format!, write)。

app-server/tests/suite/v2/request_user_input.rs源码 ↗
testtest run

这份测试像一次完整彩排:先搭一个假的模型服务,让它返回一段流式响应,里面包含一个叫 request_user_input 的工具调用,也就是“请用户选择/确认”。然后测试启动真实的应用服务器,开一个线程,再发起一轮对话。服务器读到模型的工具调用后,应该通过 JSON-RPC(一种用 JSON 格式来回发请求和响应的通信方式)向客户端发出 ToolRequestUserInput 请求。测试会检查问题内容、线程号、轮次号、自动处理时间等信息都对。接着测试假装客户端回答“yes”,再确认服务器先发出“这个请求已解决”的通知,然后才宣布本轮对话完成。这样能保证用户确认流程不是只看起来能跑,而是真的从模型到服务器、再到客户端、再回到服务器完整走通。

函数细节3
create_request_user_input_sse_response_with_auto_resolution26–51 ↗
fn create_request_user_input_sse_response_with_auto_resolution(
    call_id: &str,
    auto_resolution_ms: u64,
) -> anyhow::Result<String>

作用:这个函数造出一段假的模型流式响应,用来模拟模型要求服务器向用户提问。它还带上一个自动处理时间,测试服务器能不能正确读到这个字段。

数据流:进去的是一次工具调用的编号 call_id 和自动处理毫秒数 auto_resolution_ms。函数把一个问题、两个选项和自动处理时间包装成 JSON 字符串,再塞进一串 SSE(服务器持续推送事件,像一条不断吐消息的水管)事件里。出来的是一整段文本形式的假响应,后面的测试会把它喂给假的模型服务。

调用关系:它在测试开始时被 request_user_input_round_trip 使用,用来准备第一段模型回复。它内部借助 json! 拼出数据、to_string 转成字符串,再交给 responses::sse 等测试工具拼成流式事件。

调用图:调用 1 个内部函数(sse);外部调用 3 个(json!, to_string, vec!)。

request_user_input_round_trip54–161 ↗
async fn request_user_input_round_trip() -> Result<()>

作用:这是主测试,验证“模型请求用户输入 → 服务器转发给客户端 → 客户端回答 → 服务器通知已解决并完成对话”这条链路能完整跑通。它关心的是端到端效果,而不是某个小函数本身。

数据流:一开始它创建临时配置目录和假的模型响应:第一段让模型要求用户确认,第二段让模型最终回复 done。然后它写入测试配置,启动测试服务器,初始化通信,创建线程,并发起一轮带用户文本的对话。接着它读取服务器发来的用户输入请求,检查请求里的线程、轮次、问题编号和自动处理时间是否正确。之后它把用户答案发回服务器,再不断读取消息,直到看到 serverRequest/resolved 通知,并确认这个通知出现在 turn/completed 之前。最后如果所有检查都通过,就返回成功;任何等待超时、内容不对或顺序不对都会让测试失败。

调用关系:它是这个文件的核心流程。它先调用 create_request_user_input_sse_response_with_auto_resolution 准备模型的假输出,再调用 create_config_toml 写配置;运行中还会使用 TestAppServer、create_mock_responses_server_sequence、to_response 等测试支撑工具,分别负责启动假服务、和应用服务器通信、把通用响应拆成具体结果。

调用图:调用 2 个内部函数(new, create_config_toml);外部调用 12 个(default, Integer, create_mock_responses_server_sequence, to_response, assert!, assert_eq!, panic!, from_value, json!, new (+2 more))。

create_config_toml162–183 ↗
fn create_config_toml(codex_home: &std::path::Path, server_uri: &str) -> std::io::Result<()>

作用:这个函数给测试写一份最小配置文件,让应用服务器知道该连哪个假的模型服务、用哪个模型、用什么请求方式。没有它,服务器可能会去找真实服务,测试就不可控了。

数据流:进去的是测试用的 codex_home 目录和假的模型服务地址 server_uri。函数在这个目录下拼出 config.toml 路径,把模型名、审批策略、沙箱模式、模型提供方地址、重试次数等内容格式化成文本,然后写到磁盘。出来的是写文件的结果:成功就是配置已准备好,失败就把文件系统错误交回给调用者。

调用关系:它由 request_user_input_round_trip 在启动测试服务器之前调用。它不处理对话,只负责把测试环境“指路牌”放好,让后面的服务器启动和请求流程都走向假的模型服务。

调用图:被 1 处调用(request_user_input_round_trip);外部调用 3 个(join, format!, write)。

app-server/tests/suite/v2/thread_inject_items.rs源码 ↗
testtest run

这个测试文件验证的是一个很容易出错的场景:客户端不只是正常发用户消息,还可以把一段已经整理好的“模型回复记录”直接插进某个对话线程里。这里的“线程”可以理解成一段持续的聊天记录;“注入 items”就像在聊天记录本中补贴一张便签。测试会启动一个假的模型服务器,让它按预设内容返回结果,然后启动真实的测试版 app server,通过 JSON-RPC(一种用 JSON 发请求和收回复的通信格式)创建线程、注入内容、开启对话轮次。第一个测试确认:注入的回复项会写进本地 rollout history,也就是可恢复的历史记录里;并且下一次发给模型时,它排在系统初始上下文之后、用户新问题之前。第二个测试确认:如果先完成一轮对话,再注入内容,那么第一次模型请求里不会提前出现它,第二次才会出现。文件里还有两个小工具:一个写测试配置文件,一个在模型输入里找某段文字的位置。

函数细节4
thread_inject_items_adds_raw_response_items_to_thread_history27–136 ↗
async fn thread_inject_items_adds_raw_response_items_to_thread_history() -> Result<()>

作用:这是第一个主测试,用来确认“注入的原始回复项”不只是接口返回成功,而是真的进入了线程历史,并且会在下一次模型请求里按正确位置出现。它防止系统出现“看起来注入成功,但重启或下一轮对话时丢了”的问题。

数据流:测试先准备一个假的模型服务器和临时配置目录,再启动测试用 app server。接着它创建一个新线程,把一条 assistant 回复项转成 JSON 后注入线程。然后它读取线程磁盘上的历史记录,检查这条内容确实被保存了。最后它发起一轮用户输入“Hello”,取出假模型服务器收到的输入列表,检查顺序变成:标准环境上下文在前,注入内容在中间,用户新问题在后。

调用关系:这个测试是整条链路的演练者:它会调用 create_config_toml 写好测试配置,让 TestAppServer 启动时连到假模型服务器;它用 mount_sse_once 和 sse 准备一次假的流式模型回复;它用 RolloutRecorder::get_rollout_history 检查落盘历史;最后交给 response_item_text_position 在模型输入里找关键文字的位置,用来判断顺序是否正确。

调用图:调用 7 个内部函数(new, create_config_toml, response_item_text_position, mount_sse_once, sse, start_mock_server, get_rollout_history);外部调用 8 个(default, new, Integer, assert!, panic!, to_value, timeout, vec!)。

thread_inject_items_adds_raw_response_items_after_a_turn139–253 ↗
async fn thread_inject_items_adds_raw_response_items_after_a_turn() -> Result<()>

作用:这是第二个主测试,用来确认在一轮对话已经结束之后再注入内容,也会影响后续对话,但不会倒流到已经发生过的模型请求里。它重点防止时间顺序错乱。

数据流:测试先启动假模型服务器,并准备两次模型回复。它创建线程后先发送“First turn”,让第一轮对话正常完成。之后它把一条 assistant 回复项注入到同一个线程,再发送“Second turn”。最后它查看假模型服务器收到的两次请求:第一次请求不应该包含注入内容,第二次请求应该包含注入内容。

调用关系:这个测试同样依赖 create_config_toml 搭好测试配置,并用 mount_sse_sequence 准备两次连续的假流式回复。它不检查磁盘历史,而是通过 response_mock.requests() 查看模型端实际收到的请求,从外部结果判断注入功能在“已有历史”中的位置是否正确。

调用图:调用 5 个内部函数(new, create_config_toml, mount_sse_sequence, sse, start_mock_server);外部调用 8 个(default, new, Integer, assert!, assert_eq!, to_value, timeout, vec!)。

create_config_toml255–276 ↗
fn create_config_toml(codex_home: &Path, server_uri: &str) -> std::io::Result<()>

作用:这个小工具函数负责给测试临时写一份 config.toml 配置文件。没有它,测试版 app server 就不知道该用哪个模型、连到哪个假的模型服务器、以及测试时不要重试。

数据流:它接收一个临时的 codex_home 目录路径和假模型服务器地址。它在这个目录下拼出 config.toml 文件路径,然后写入一段配置文本:模型名是 mock-model,审批策略是不询问,沙盒是只读,模型提供方指向传入的 server_uri。结果是磁盘上多出一份测试专用配置文件;如果写文件失败,就把系统的 IO 错误返回出去。

调用关系:两个主测试在启动 TestAppServer 之前都会调用它。它相当于测试开场前摆好路牌:让后续的 app server 请求不要跑到真实模型服务,而是准确打到测试控制的 mock server。

调用图:被 2 处调用(thread_inject_items_adds_raw_response_items_after_a_turn, thread_inject_items_adds_raw_response_items_to_thread_history);外部调用 3 个(join, format!, write)。

response_item_text_position278–291 ↗
fn response_item_text_position(items: &[Value], needle: &str) -> Option<usize>

作用:这个小工具函数在一串模型输入 JSON 里查找哪一项包含指定文字。它主要用来判断不同内容在模型请求中的先后顺序。

数据流:它接收一个 JSON 值列表和要寻找的文字。它逐项查看每个 JSON 里的 content 数组,再看 content 里的 text 字段是否包含目标文字。如果找到了,就返回这一项在列表里的位置编号;如果全部没找到,就返回空值。

调用关系:第一个主测试用它分别找到环境上下文、“Injected assistant context”和用户输入“Hello”的位置。测试随后比较这些位置,确认注入内容没有插到错误的地方。

调用图:被 1 处调用(thread_inject_items_adds_raw_response_items_to_thread_history);外部调用 1 个(iter)。

app-server/tests/suite/v2/thread_status.rs源码 ↗
testtest run

这个测试文件像一个小型验收现场:它先搭一个假的模型服务,让服务器以为自己正在和真实 AI 后端通信;再启动一个临时的 app server,模拟客户端发起线程和一轮对话。重点检查的是 JSON-RPC 通知(一种客户端和服务器互发消息的格式)里的 thread/status/changed。第一个测试确认线程从运行中变成空闲等结束状态时,客户端能看到状态变化;这能避免界面一直显示“没动静”或“还在跑”。第二个测试确认客户端如果在初始化时选择屏蔽这种通知,服务器会尊重这个选择,不把无用消息塞给它。文件末尾的配置生成函数负责写一份临时 config.toml,把服务器指向假的模型接口,保证测试稳定、可重复,不依赖外网或真实模型。

函数细节3
thread_status_changed_emits_runtime_updates25–129 ↗
async fn thread_status_changed_emits_runtime_updates() -> Result<()>

作用:这个测试确认服务器会在对话运行期间发出“线程正在活动”的通知,并在这一轮对话结束后再发出“线程已空闲或已结束”的通知。它防止客户端界面拿不到运行状态,从而无法正确显示进度。

数据流:一开始,它创建一个临时目录当作配置目录,再准备一个假的模型响应“done”,并启动假的响应服务器。然后它写入测试配置,启动 TestAppServer,初始化连接。接着它发起一个线程,再往这个线程里发一条用户输入。之后它不断读取服务器发来的 JSON-RPC 消息,筛选出 thread/status/changed,只看当前线程的状态:如果看到 Active,就记下“确实运行过”;如果在运行后又看到 IdleSystemErrorNotLoaded,就记下“运行后确实结束/退出活动状态”。最后它用断言确认这两个阶段都出现过,并等待 turn/completed,表示这一轮对话完整结束。

调用关系:这是整条状态通知流程的正向测试。它会调用 create_config_toml 准备配置,借助 create_mock_responses_server_sequence 搭假后端,用 TestAppServer::new_with_env 启动被测服务器,再通过测试工具发送 thread start 和 turn start 请求。服务器返回的响应会交给 to_response 转成具体结构,后续读取到的通知则由测试自己判断是否符合预期。

调用图:调用 2 个内部函数(new_with_env, create_config_toml);外部调用 10 个(default, new, Integer, create_mock_responses_server_sequence, to_response, assert!, from_value, now, timeout, vec!)。

thread_status_changed_can_be_opted_out132–217 ↗
async fn thread_status_changed_can_be_opted_out() -> Result<()>

作用:这个测试确认客户端可以选择不接收 thread/status/changed 通知。也就是说,如果客户端初始化时声明“这种消息别发给我”,服务器就应该过滤掉它。

数据流:它先和前一个测试一样,创建临时配置目录、假的模型响应和假的后端服务器,并写入配置。然后它启动 TestAppServer,但初始化时传入客户端信息和能力声明,其中 opt_out_notification_methods 明确写着不接收 thread/status/changed。初始化成功后,它创建线程、发送一轮用户输入,并等待 turn/completed,确认对话已经跑完。最后它只等 500 毫秒去找 thread/status/changed 通知:如果超时,说明通知确实被过滤,这是正确结果;如果真的读到了这种通知,测试就直接失败。

调用关系:这是状态通知流程的反向测试,专门检查“退出订阅”是否生效。它同样调用 create_config_toml 准备配置,用 create_mock_responses_server_sequence 提供假模型服务,用 TestAppServer::new 启动服务器。和正向测试不同的是,它通过 initialize_with_capabilities 告诉服务器要屏蔽某类通知,然后用读取通知的测试工具确认服务器没有越界发送。

调用图:调用 2 个内部函数(new, create_config_toml);外部调用 9 个(default, new, bail!, Integer, create_mock_responses_server_sequence, to_response, from_millis, timeout, vec!)。

create_config_toml219–243 ↗
fn create_config_toml(codex_home: &std::path::Path, server_uri: &str) -> std::io::Result<()>

作用:这个辅助函数给测试临时写一份 config.toml 配置文件。它把 app server 的模型提供方指向测试里的假服务器,这样测试不会调用真实外部服务。

数据流:输入是一个临时目录路径和假的服务器地址。函数先在目录下拼出 config.toml 文件路径,再把模型名、审批策略、沙箱模式、功能开关,以及模型提供方的 base_url 等内容拼成 TOML 文本,最后写到磁盘。结果是临时目录里多了一份服务器启动时能读取的配置文件;如果写文件失败,就把系统错误返回给调用方。

调用关系:它是两个测试共同使用的准备步骤。thread_status_changed_emits_runtime_updatesthread_status_changed_can_be_opted_out 都会先启动假模型服务器,再把假服务器地址交给这个函数写进配置。后面的 TestAppServer 启动时会读取这份配置,从而把所有模型请求导向可控的测试后端。

调用图:被 2 处调用(thread_status_changed_can_be_opted_out, thread_status_changed_emits_runtime_updates);外部调用 3 个(join, format!, write)。

app-server/tests/suite/v2/turn_interrupt.rs源码 ↗
testtest execution

这个测试文件模拟了一个真实用户和应用服务器对话的过程。这里的“一轮 turn”可以理解成用户发一句话后,系统开始处理并可能调用命令的那一段工作。测试会先启动一个临时的应用服务器,再用假的模型服务器返回预设内容,比如“去执行一个很久才结束的 sleep 命令”。然后它通过 JSON-RPC(一种用 JSON 传请求和响应的通信格式)发起线程、开始 turn、发送中断请求,并检查服务器回来的响应和通知。重点有三件事:正在跑的命令被中断后,turn 状态必须变成 Interrupted;已经 Completed 的 turn 再中断必须报错;如果命令还卡在“是否允许执行”的审批请求上,中断时也要把这个请求标记为已解决,避免客户端一直等着。辅助函数 create_config_toml 会给每个测试写一份临时配置,让服务器连接到假的模型服务,而不是外部真实服务。

函数细节4
turn_interrupt_aborts_running_turn32–130 ↗
async fn turn_interrupt_aborts_running_turn() -> Result<()>

作用:这个测试确认:当一轮对话正在执行一个长时间命令时,客户端发送中断请求后,服务器真的会把这轮任务停掉,并报告它是“已中断”。这能防止用户点了停止,但后台命令还偷偷跑着。

数据流:测试先准备临时目录、配置文件和一个假的模型服务器;假的模型服务器会让应用去执行一个会睡眠很久的命令。接着测试启动应用服务器,创建线程,开始一轮对话,拿到 turn 的编号。等待命令开始后,它发送 turn_interrupt 请求。最后它读取服务器通知,确认通知里的 thread_id 对得上,并且 turn.status 从运行中变成了 Interrupted。

调用关系:它是这一组测试里验证“正常中断正在运行任务”的主场景。它会调用 create_config_toml 写测试配置,也会使用 TestAppServer 和 mock responses server 搭出一套临时环境;真正的判断点来自服务器返回的 JSON-RPC 响应和 turn/completed 通知。

调用图:调用 2 个内部函数(new, create_config_toml);外部调用 11 个(default, new, Integer, create_mock_responses_server_sequence_unchecked, assert_eq!, from_value, create_dir, from_secs, sleep, timeout (+1 more))。

turn_interrupt_rejects_completed_turn133–207 ↗
async fn turn_interrupt_rejects_completed_turn() -> Result<()>

作用:这个测试确认:如果一轮对话已经正常结束,再去中断它应该被拒绝。这样可以避免客户端或用户误操作,把已经完成的历史结果改成奇怪的状态。

数据流:测试先创建临时配置和假的模型服务器;假的模型服务器只返回一句最终助手消息,让 turn 很快完成。测试启动服务器、创建线程、开始 turn,并等到 turn/completed 通知,确认状态是 Completed。之后它再发送中断请求,预期不再收到成功响应,而是收到 JSON-RPC 错误,并检查错误码是“无效请求”。

调用关系:它覆盖的是中断功能的边界情况:中断只能用于还没结束的 turn。它同样通过 create_config_toml 准备环境,通过 TestAppServer 发请求和读消息;和前一个测试不同,它期待服务器走错误返回路径,而不是成功中断路径。

调用图:调用 2 个内部函数(new, create_config_toml);外部调用 10 个(default, new, Integer, create_mock_responses_server_sequence_unchecked, assert_eq!, from_value, create_dir, from_millis, timeout, vec!)。

turn_interrupt_resolves_pending_command_approval_request210–328 ↗
async fn turn_interrupt_resolves_pending_command_approval_request() -> Result<()>

作用:这个测试确认:如果一轮对话正在等用户批准某个命令,中断这轮对话时,那个等待批准的请求也会被收掉。否则客户端界面可能还挂着一个没人能处理的“是否允许执行”弹窗。

数据流:测试先设置一个更严格的环境:命令执行需要用户批准,沙箱模式也更保守。假的模型服务器返回一个想执行长时间命令的响应。测试启动应用服务器、创建线程、开始 turn,然后读取到服务器发来的命令审批请求,并检查这个请求属于正确的 thread 和 turn。随后测试发送中断请求。成功响应后,它继续读取 serverRequest/resolved 通知,确认刚才那个审批请求已被解决;最后再读取 turn/completed,确认 turn 状态是 Interrupted。

调用关系:它测试的是中断和“命令审批”这两个机制交叉时的行为。它由假的模型响应触发审批请求,再由 turn_interrupt 请求触发清理;过程中依赖 create_config_toml 把 approval_policy 和 sandbox_mode 写成会要求审批的配置。

调用图:调用 2 个内部函数(new, create_config_toml);外部调用 10 个(default, new, Integer, create_mock_responses_server_sequence, assert_eq!, panic!, from_value, create_dir, timeout, vec!)。

create_config_toml331–358 ↗
fn create_config_toml(
    codex_home: &std::path::Path,
    server_uri: &str,
    approval_policy: &str,
    sandbox_mode: &str,
) -> std::io::Result<()>

作用:这个辅助函数给测试写一份临时的 config.toml 配置文件。它让应用服务器使用假的模型服务,并按测试需要设置审批策略和沙箱模式。

数据流:它接收 codex_home 目录、假的模型服务器地址、审批策略字符串和沙箱模式字符串。然后它在 codex_home 下面拼出 config.toml 路径,把这些值填进一段配置文本里,最后写入磁盘。结果是测试服务器启动时会读到这份配置,并连接到 mock provider,而不是外部真实模型服务。

调用关系:三个测试都会先调用它来准备环境。它不参与断言,也不直接发请求;它像测试开场前摆好的路牌,告诉 TestAppServer 接下来该连哪里、该用什么安全规则。

调用图:被 3 处调用(turn_interrupt_aborts_running_turn, turn_interrupt_rejects_completed_turn, turn_interrupt_resolves_pending_command_approval_request);外部调用 3 个(join, format!, write)。

app-server/tests/suite/v2/turn_start.rs源码 ↗
testtest run

在这个系统里,“thread”可以理解成一条聊天会话,“turn”就是这条会话里用户发一次话、模型回一次应的过程。这个测试文件搭了很多假的模型服务器和临时配置目录,然后让测试客户端像真实前端一样去调用 app-server。它验证的不是某个小函数,而是完整流程:启动线程、发起一轮对话、把内容送到模型、收到模型的流式返回、触发命令执行或文件修改审批、发出前端通知,最后正确结束。它还覆盖很多容易出事故的边界情况,比如空输入不能伪造空消息,超长输入要被拒绝,本地图片要转成模型能看懂的格式,危险权限不能悄悄放开,补丁修改必须先问用户。可以把它看成机场安检清单:每条测试都在确认一个入口不会把不该放行的东西放进去,也不会漏掉该通知用户的事情。

函数细节44
body_contains108–112 ↗
fn body_contains(req: &wiremock::Request, text: &str) -> bool

作用:检查一个模拟网络请求的正文里,是否包含某段文字。测试用它来确认 app-server 真的把预期提示词、模型参数或工具信息发给了假模型服务器。

数据流:进去的是一个收到的 HTTP 请求和要查找的文字 → 它把请求里的字节内容按 UTF-8 文本解出来,再做包含判断 → 出来一个真假值,不改动任何外部状态。

调用关系:它是很多模型请求匹配和断言的小工具,后面的子代理、补丁流式更新等测试会用它判断某个模拟响应该不该匹配这次请求。它只依赖标准的字符串解码,不再把工作交给项目里的其他函数。

调用图:外部调用 1 个(from_utf8)。

run_local_image_turn114–177 ↗
async fn run_local_image_turn(detail: Option<ImageDetail>) -> Result<Vec<Value>>

作用:跑一遍“用户发送本地图片”的完整对话流程,并把模型服务器收到的图片输入取回来。两个图片细节相关测试共用它,避免重复搭环境。

数据流:进去的是可选的图片清晰度设置 → 它启动假模型服务器,写临时配置,启动测试 app-server,创建线程,写入一个极小 PNG 文件,再用这个图片发起 turn/start → 出来的是假模型服务器实际收到的 input_image 列表,同时临时目录里会产生配置和图片文件。

调用关系:它被 turn_start_defaults_local_image_detail_to_highturn_start_forwards_custom_local_image_detail 调用。流程中它会调用 create_config_toml 准备配置,最后把收集请求的活交给 received_response_input_images

调用图:调用 3 个内部函数(new, create_config_toml, received_response_input_images);被 2 处调用(turn_start_defaults_local_image_detail_to_high, turn_start_forwards_custom_local_image_detail);外部调用 9 个(default, default, new, Integer, create_mock_responses_server_sequence_unchecked, assert!, write, timeout, vec!)。

received_response_input_images179–214 ↗
async fn received_response_input_images(server: &wiremock::MockServer) -> Result<Vec<Value>>

作用:从假模型服务器收到的所有请求里,挑出真正发给模型的图片输入片段。测试用它看图片有没有被正确包装成模型 API 需要的格式。

数据流:进去的是 wiremock 假服务器 → 它读取服务器记录的请求,筛出路径以 /responses 结尾的请求,解析 JSON,找到 message 内容里的 input_image 项 → 出来是一组图片 JSON 片段,不修改服务器。

调用关系:它只被 run_local_image_turn 调用,是图片测试的“验货员”。它依赖假服务器保存的请求记录,不参与发起 turn/start。

调用图:被 1 处调用(run_local_image_turn);外部调用 2 个(received_requests, new)。

turn_start_with_empty_input_runs_model_request217–316 ↗
async fn turn_start_with_empty_input_runs_model_request() -> Result<()>

作用:验证空输入也能启动一次模型请求,但不能凭空制造一条空的用户消息。这样前端发空 turn 时,后端流程仍正常,历史里也不会多出奇怪的空消息。

数据流:进去的是临时配置和一个会返回 Done 的假模型服务器 → 测试启动 app-server,创建线程,用空 input 发起 turn/start,读取开始和完成通知,再检查发给模型的 JSON → 结果是确认 turn 有 id、状态从进行中到完成,并且模型输入里没有空用户 message。

调用关系:它直接用测试服务器和 create_config_toml 搭环境,是 turn/start 基础行为的端到端测试。它不被其他函数调用,也不调用本文件的业务辅助函数以外的复杂逻辑。

调用图:调用 2 个内部函数(new, create_config_toml);外部调用 11 个(default, default, new, new, Integer, create_mock_responses_server_sequence_unchecked, assert!, assert_eq!, from_value, timeout (+1 more))。

turn_start_additional_context_flows_to_model_input319–393 ↗
async fn turn_start_additional_context_flows_to_model_input() -> Result<()>

作用:确认用户随 turn/start 附带的额外上下文,会真的进入模型输入。额外上下文就像前端告诉模型“当前标签页还有这些材料”。

数据流:进去的是一段普通用户文本和一个名为 custom_source 的额外上下文 → 测试创建线程并发起 turn/start,然后读取假模型服务器收到的请求正文 → 出来是断言正文里包含包起来的 <external_custom_source>source value</external_custom_source>

调用关系:它用 create_config_toml 准备模型地址,然后通过测试 app-server 发请求。它验证的是 app-server 到模型请求之间的传递链路。

调用图:调用 2 个内部函数(new, create_config_toml);外部调用 9 个(default, default, from, new, Integer, create_mock_responses_server_sequence_unchecked, assert!, timeout, vec!)。

turn_start_sends_originator_header396–470 ↗
async fn turn_start_sends_originator_header() -> Result<()>

作用:验证客户端身份会作为 originator 请求头发给模型服务。这样下游服务能知道请求来自哪个产品客户端,比如 VS Code 插件。

数据流:进去的是带名称、标题、版本的客户端信息 → 测试初始化 app-server,创建线程,发起 turn/start,等 turn 完成,再读取假模型服务器收到的所有请求头 → 出来是确认每个请求的 originator 都等于测试客户端名。

调用关系:它依赖 create_config_toml 打开相关配置,并用 app-server 的初始化入口传入客户端信息。它是客户端信息传播链路的检查点。

调用图:调用 2 个内部函数(new, create_config_toml);外部调用 9 个(from, default, new, Integer, create_mock_responses_server_sequence_unchecked, assert!, assert_eq!, timeout, vec!)。

turn_start_emits_user_message_item_with_text_elements473–561 ↗
async fn turn_start_emits_user_message_item_with_text_elements() -> Result<()>

作用:确认用户消息里的文本标注信息会跟着 item/started 通知发给前端。文本标注可以理解成一段文字上的备注或引用范围。

数据流:进去的是一条带 client_user_message_id 和 TextElement 的用户文本 → 测试发起 turn/start,不断读取 item/started 通知,直到拿到 UserMessage → 出来是确认通知里的客户端消息 id、文本和 text_elements 都和输入一致。

调用关系:它用普通 turn/start 流程触发服务端通知,是前端消息展示数据是否完整的测试。它依赖 create_config_toml 和测试服务器,但没有被其他函数复用。

调用图:调用 2 个内部函数(new, create_config_toml);外部调用 10 个(from, default, new, Integer, create_mock_responses_server_sequence_unchecked, assert_eq!, panic!, from_value, timeout, vec!)。

turn_start_emits_thread_scoped_warning_notification_for_trimmed_skills564–675 ↗
async fn turn_start_emits_thread_scoped_warning_notification_for_trimmed_skills() -> Result<()>

作用:验证当技能说明太多、塞不进模型上下文时,系统会发出带 thread_id 的警告,并把过长技能说明裁掉。这样用户知道模型没看到完整技能列表。

数据流:进去的是一个被故意改小上下文窗口的模型缓存,以及两个测试技能文件 → 测试启动隔离 HOME 的 app-server,发起 turn/start,读取 warning 通知和模型请求正文 → 出来是确认警告属于当前线程,说明被裁剪,并且模型请求里没有具体技能描述。

调用关系:它会调用 write_test_skill 写测试技能,也用 create_config_toml 准备环境。它覆盖的是技能注入、预算裁剪、前端警告三者的组合流程。

调用图:调用 3 个内部函数(new_with_env, create_config_toml, write_test_skill);外部调用 18 个(from, default, new, Integer, default, create_mock_responses_server_sequence_unchecked, write_models_cache, assert!, assert_eq!, format! (+8 more))。

turn_start_sends_service_tier_id_to_model_request678–745 ↗
async fn turn_start_sends_service_tier_id_to_model_request() -> Result<()>

作用:确认前端指定的服务档位 id 会被放进发给模型的请求里。服务档位可以理解成同一个模型的不同服务等级。

数据流:进去的是模型目录里一个带 service tier 的模型和选中的 tier id → 测试创建线程,发起 turn/start 时填入 service_tier,然后检查假模型服务器收到的 JSON → 出来是确认 service_tier 字段等于指定 id。

调用关系:它从内置模型预设里找测试对象,用 mock Responses API 接收请求。它测试 turn/start 参数到模型请求字段的直连关系。

调用图:调用 6 个内部函数(new, create_config_toml, all_model_presets, mount_sse_once, sse, start_mock_server);外部调用 8 个(default, default, new, Integer, write_models_cache, assert_eq!, timeout, vec!)。

thread_start_omits_empty_instruction_overrides_from_model_request748–836 ↗
async fn thread_start_omits_empty_instruction_overrides_from_model_request() -> Result<()>

作用:验证空字符串形式的指令覆盖不会被当成真实指令发给模型。这样不会因为前端传了空值,就让模型收到空的 developer 消息或空 instructions。

数据流:进去的是 thread/start 上的空 base_instructions、空 developer_instructions 和一个配置覆盖 → 测试创建线程后发起 turn/start,检查模型请求 JSON → 出来是确认没有 instructions 字段,也没有空文本的 developer 输入。

调用关系:虽然名字以 thread_start 开头,它实际检查的是线程启动参数如何影响后续 turn/start 的模型请求。它独立运行,靠 mock Responses API 验证结果。

调用图:调用 5 个内部函数(new, create_config_toml, mount_sse_once, sse, start_mock_server);外部调用 10 个(default, default, from, new, new, Integer, assert_eq!, json!, timeout, vec!)。

turn_start_tracks_turn_event_analytics839–973 ↗
async fn turn_start_tracks_turn_event_analytics() -> Result<()>

作用:确认一次 turn 的统计埋点会被正确记录,包括线程、模型、图片数量、重试次数、耗时和 token 计数。埋点就是系统给分析后台发的运行记录。

数据流:进去的是一个第一次返回 500、第二次成功的假模型服务器,以及一个图片输入 → 测试开启 analytics 捕获,发起 turn/start,等待完成后读取 codex_turn_event → 出来是确认事件里的各种字段和预期一致,包括一次重试和两次采样请求。

调用关系:它调用 mount_analytics_capture 安装埋点接收器,并用 wait_for_analytics_event 等事件出现。它验证 turn/start 主流程和分析上报流程的交汇点。

调用图:调用 5 个内部函数(new_without_managed_config, mount_analytics_capture, wait_for_analytics_event, mount_response_sequence, start_mock_server);外部调用 11 个(default, from, new, Integer, write_mock_responses_config_toml_with_chatgpt_base_url, assert!, assert_eq!, read_to_string, write, timeout (+1 more))。

turn_profile_tracks_blocking_tool_and_follow_up_sampling976–1077 ↗
async fn turn_profile_tracks_blocking_tool_and_follow_up_sampling() -> Result<()>

作用:验证当模型要求用户输入、系统等待用户回答时,这段等待会被算进工具阻塞时间,并且后续还会继续采样模型。采样可以理解成向模型要下一步输出。

数据流:进去的是先请求用户输入、再返回 Done 的假模型响应序列 → 测试发起 turn/start,收到服务端工具请求后故意等一小会再回复,最后读取埋点 → 出来是确认工具阻塞时间大于 0、采样次数为 2、状态完成。

调用关系:它和 turn_start_tracks_turn_event_analytics 一样使用 analytics 捕获,但重点是用户交互工具造成的等待时间。它通过 app-server 的 server request/response 机制模拟前端回答。

调用图:调用 3 个内部函数(new_without_managed_config, mount_analytics_capture, wait_for_analytics_event);外部调用 12 个(default, new, Integer, create_mock_responses_server_sequence, write_mock_responses_config_toml_with_chatgpt_base_url, assert_eq!, json!, panic!, from_millis, sleep (+2 more))。

turn_start_accepts_text_at_limit_with_mention_item1080–1140 ↗
async fn turn_start_accepts_text_at_limit_with_mention_item() -> Result<()>

作用:验证刚好达到最大文本长度的输入仍然可以通过,即使旁边还有一个 mention 项。mention 可以理解成用户提到某个应用或资源。

数据流:进去的是长度正好等于 MAX_USER_INPUT_TEXT_CHARS 的文本和一个 Mention 输入 → 测试发起 turn/start 并读取响应与完成通知 → 出来是确认 turn 状态进入进行中并最终完成。

调用关系:它使用 create_config_toml 和假模型服务器跑完整流程。它和超长输入测试形成一正一反的边界检查。

调用图:调用 2 个内部函数(new, create_config_toml);外部调用 8 个(from, default, new, Integer, create_mock_responses_server_sequence_unchecked, assert_eq!, timeout, vec!)。

turn_start_rejects_combined_oversized_text_input1143–1216 ↗
async fn turn_start_rejects_combined_oversized_text_input() -> Result<()>

作用:验证多段文本加起来超过最大长度时,turn/start 会在开始前被拒绝。这样可以防止一次请求把服务器或模型上下文撑爆。

数据流:进去的是两段合计超过上限的文本 → 测试发起 turn/start,读取 JSON-RPC 错误,检查错误码、最大字符数和实际字符数 → 出来是确认没有 turn/started 通知,说明流程没有真正启动。

调用关系:它不需要真的连模型,因为错误应该在本地参数校验阶段发生。它和 turn_start_accepts_text_at_limit_with_mention_item 一起验证输入大小边界。

调用图:调用 2 个内部函数(new, create_config_toml);外部调用 9 个(from, default, new, Integer, assert!, assert_eq!, from_millis, timeout, vec!)。

turn_start_rejects_invalid_permission_selection_before_starting_turn1219–1291 ↗
async fn turn_start_rejects_invalid_permission_selection_before_starting_turn() -> Result<()>

作用:验证当前管理配置不允许的危险权限,会在 turn 开始前被拒绝。这样用户或前端不能绕过受管策略把沙箱改成完全访问。

数据流:进去的是一个受管配置 sandbox_mode = read-only 和 turn/start 里要求 danger-full-access 的权限配置 → 测试发起请求并读取错误 → 出来是确认错误信息说明策略冲突,并且没有 turn/started 通知。

调用关系:它用 create_config_toml 加手写 managed_config.toml 构造受限环境。它测试权限选择校验发生在真正启动 turn 之前。

调用图:调用 2 个内部函数(new, create_config_toml);外部调用 10 个(from, default, new, Integer, assert!, assert_eq!, write, from_millis, timeout, vec!)。

turn_start_rejects_unknown_environment_before_starting_turn1294–1358 ↗
async fn turn_start_rejects_unknown_environment_before_starting_turn() -> Result<()>

作用:验证 turn/start 指定不存在的运行环境 id 时,会立刻报错而不是继续执行。运行环境可以理解成命令和工具要在哪台机器或哪个工作目录里跑。

数据流:进去的是一个 environment_id 为 missing 的环境参数 → 测试创建线程后发起 turn/start,读取错误响应 → 出来是确认错误说未知环境,并且没有 turn/started 通知。

调用关系:它使用会重复返回 Done 的假模型服务器,但理论上不应走到模型请求阶段。它检查环境选择的前置校验。

调用图:调用 2 个内部函数(new, create_config_toml);外部调用 10 个(default, default, new, Integer, create_mock_responses_server_repeating_assistant, assert!, assert_eq!, from_millis, timeout, vec!)。

turn_start_emits_notifications_and_accepts_model_override1361–1502 ↗
async fn turn_start_emits_notifications_and_accepts_model_override() -> Result<()>

作用:验证正常 turn/start 会发出开始和完成通知,并且第二轮可以临时覆盖模型。模型覆盖就是这一次 turn 用不同模型,不一定改变整条线程默认模型。

数据流:进去的是同一线程上的两次用户输入,第二次带 model override → 测试依次发起两个 turn/start,读取响应、turn/started、turn/completed → 出来是确认每个 turn 都有不同 id,状态正确,items 仍是未加载视图。

调用关系:它是基础通知行为和模型覆盖路径的综合测试。它用 create_config_toml 和假模型响应序列支撑两次 turn。

调用图:调用 2 个内部函数(new, create_config_toml);外部调用 11 个(from, default, new, Integer, create_mock_responses_server_sequence_unchecked, assert!, assert_eq!, assert_ne!, from_value, timeout (+1 more))。

turn_start_accepts_collaboration_mode_override_v21505–1587 ↗
async fn turn_start_accepts_collaboration_mode_override_v2() -> Result<()>

作用:验证 turn/start 里的协作模式设置会优先决定模型和工具说明。协作模式可以理解成“这轮让模型按默认、计划等模式工作”的一组临时设置。

数据流:进去的是线程默认模型、turn 上的普通模型覆盖,以及 collaboration_mode 里的模型和推理强度 → 测试发起 turn/start 后查看模型请求 → 出来是确认最终发给模型的是协作模式里的模型,并且工具说明文字出现。

调用关系:它使用 mock Responses API 检查实际请求。它说明在参数优先级上,协作模式能覆盖普通 turn 参数。

调用图:调用 5 个内部函数(new, create_config_toml, mount_sse_once, sse, start_mock_server);外部调用 9 个(default, default, new, Integer, assert!, assert_eq!, skip_if_no_network!, timeout, vec!)。

turn_start_uses_thread_feature_overrides_for_request_user_input_tool_description_v21590–1673 ↗
async fn turn_start_uses_thread_feature_overrides_for_request_user_input_tool_description_v2() -> Result<()>

作用:验证线程级功能开关会影响 request_user_input 工具的说明文字。也就是说,不只是全局配置,thread/start 传入的特性覆盖也会改变后续 turn。

数据流:进去的是 thread/start 配置 features.default_mode_request_user_input = true,以及一次带协作模式的 turn/start → 测试检查模型请求正文 → 出来是确认工具说明里包含“只在 Default 或 Plan 模式可用”的提示。

调用关系:它和协作模式测试类似,但重点是线程配置覆盖会被后续 turn/start 读取。它通过模型请求文本验证工具描述生成结果。

调用图:调用 5 个内部函数(new, create_config_toml, mount_sse_once, sse, start_mock_server);外部调用 10 个(default, default, from, new, Integer, assert!, json!, skip_if_no_network!, timeout, vec!)。

turn_start_accepts_personality_override_v21676–1750 ↗
async fn turn_start_accepts_personality_override_v2() -> Result<()>

作用:验证 turn/start 可以临时指定人格,比如 Friendly,并且会把人格说明发给模型。人格就是系统给模型的一套说话风格和行为偏好。

数据流:进去的是开启 Personality 功能的配置和一次带 Personality::Friendly 的 turn/start → 测试读取模型请求里的 developer 文本 → 出来是确认其中包含 <personality_spec> 人格更新内容。

调用关系:它使用 create_config_toml 打开人格功能,并通过 Responses API mock 查看请求。它覆盖 turn 级人格覆盖进入模型提示词的路径。

调用图:调用 5 个内部函数(new, create_config_toml, mount_sse_once, sse, start_mock_server);外部调用 9 个(from, default, new, Integer, assert!, eprintln!, skip_if_no_network!, timeout, vec!)。

turn_start_change_personality_mid_thread_v21753–1863 ↗
async fn turn_start_change_personality_mid_thread_v2() -> Result<()>

作用:验证同一条线程中,第一轮不改人格、第二轮再改人格时,只有第二轮模型请求带人格更新。这样中途切换风格不会污染之前请求。

数据流:进去的是同一线程上的两次 turn/start,第二次带 Friendly 人格 → 测试收集两次模型请求的 developer 文本 → 出来是确认第一次没有 <personality_spec>,第二次有。

调用关系:它复用了完整线程和 turn 流程,但用两次 mock SSE 响应观察状态变化。它是人格状态“中途改变”场景的专项测试。

调用图:调用 5 个内部函数(new, create_config_toml, mount_sse_sequence, sse, start_mock_server);外部调用 9 个(from, default, new, Integer, assert!, assert_eq!, skip_if_no_network!, timeout, vec!)。

turn_start_uses_migrated_pragmatic_personality_without_override_v21866–1954 ↗
async fn turn_start_uses_migrated_pragmatic_personality_without_override_v2() -> Result<()>

作用:验证启动时从旧历史迁移出来的 Pragmatic 人格,会在没有 turn 级覆盖时自动用于模型指令。迁移就是把旧格式或旧默认行为改写成新配置。

数据流:进去的是一个伪造的历史 rollout 文件和开启 Personality 的配置 → app-server 初始化时写入人格配置和迁移标记,随后 turn/start 不传 personality → 出来是确认模型 instructions 里包含 Pragmatic 模板文本。

调用关系:它调用外部辅助创建假历史,并检查 PERSONALITY_MIGRATION_FILENAME 是否存在。它连接了启动迁移逻辑和后续 turn/start 指令生成。

调用图:调用 5 个内部函数(new, create_config_toml, mount_sse_once, sse, start_mock_server);外部调用 12 个(from, default, new, Integer, create_fake_rollout, assert!, assert_eq!, skip_if_no_network!, read_to_string, timeout (+2 more))。

turn_start_defaults_local_image_detail_to_high1957–1967 ↗
async fn turn_start_defaults_local_image_detail_to_high() -> Result<()>

作用:验证本地图片没有指定清晰度时,默认按 high 发给模型。这样图片不会因为缺省参数变成低质量输入。

数据流:进去的是没有 detail 的本地图片场景 → 它调用 run_local_image_turn 跑完整流程并拿回模型收到的图片片段 → 出来是确认只有一张图片,且 detail 字段是 high

调用关系:它是 run_local_image_turn 的一个简单调用者,专门检查默认值。真正搭环境和收集请求的工作都交给辅助函数。

调用图:调用 1 个内部函数(run_local_image_turn);外部调用 1 个(assert_eq!)。

turn_start_forwards_custom_local_image_detail1970–1980 ↗
async fn turn_start_forwards_custom_local_image_detail() -> Result<()>

作用:验证用户显式指定的本地图片清晰度会原样传给模型。比如指定 original,就不能被后端改成 high。

数据流:进去的是 detail 为 Original 的本地图片场景 → 它调用 run_local_image_turn → 出来是确认模型收到的图片 detail 是 original

调用关系:它和默认清晰度测试共用 run_local_image_turn。两者一同确认图片 detail 的默认值和覆盖值都正确。

调用图:调用 1 个内部函数(run_local_image_turn);外部调用 1 个(assert_eq!)。

turn_start_exec_approval_toggle_v21983–2137 ↗
async fn turn_start_exec_approval_toggle_v2() -> Result<()>

作用:验证命令执行审批可以按 turn 设置切换:第一轮需要用户批准,第二轮设置永不询问后就不再弹审批。命令审批是防止模型随便在电脑上跑命令的安全关卡。

数据流:进去的是两轮会触发 shell 命令的模型响应,初始审批策略为 untrusted → 第一轮测试收到 CommandExecutionRequestApproval,批准后等完成;第二轮 turn/start 指定 approval_policy=never 和危险沙箱 → 出来是确认第二轮没有审批请求并能完成。

调用关系:它用模型响应序列模拟两次命令调用,并通过 JSON-RPC request/response 模拟前端批准。它覆盖 turn 级安全策略覆盖是否立即生效。

调用图:调用 2 个内部函数(new, create_config_toml);外部调用 13 个(default, default, new, Integer, create_mock_responses_server_sequence, assert!, assert_eq!, panic!, from_value, to_value (+3 more))。

turn_start_exec_approval_decline_v22140–2282 ↗
async fn turn_start_exec_approval_decline_v2() -> Result<()>

作用:验证用户拒绝命令执行审批时,命令 item 会以 Declined 状态结束,并且不会产生输出或退出码。这样拒绝是真的拦住执行,而不是只改了显示状态。

数据流:进去的是一个会要求运行 python 的模型响应和 untrusted 审批策略 → 测试发起 turn/start,等到命令 item 开始,收到审批请求后回复 Decline,再等 item/completed → 出来是确认命令状态为 Declined,exit_code 和输出为空。

调用关系:它使用 app-server 的服务端审批请求机制。和批准测试不同,它重点检查拒绝后的 item 状态和副作用。

调用图:调用 2 个内部函数(new, create_config_toml);外部调用 15 个(default, default, new, Integer, create_mock_responses_server_sequence, assert!, assert_eq!, panic!, from_value, to_value (+5 more))。

turn_start_updates_sandbox_and_cwd_between_turns_v22285–2454 ↗
async fn turn_start_updates_sandbox_and_cwd_between_turns_v2() -> Result<()>

作用:验证同一线程的不同 turn 可以更新沙箱和当前工作目录。当前工作目录就是命令默认在哪个文件夹里执行。

数据流:进去的是两个不同的工作目录和两轮会触发 echo 命令的模型响应 → 第一轮设置 workspace-write 和 first_cwd,第二轮设置 danger-full-access 和 second_cwd → 出来是确认第二轮命令 item 的 cwd 是 second_cwd,显示命令也符合第二轮内容。

调用关系:它用 format_with_current_shell_display 生成平台相关的命令显示文本。它检查 turn 级环境更新不会被上一轮残留状态污染。

调用图:调用 2 个内部函数(new, create_config_toml);外部调用 14 个(default, default, new, Integer, create_mock_responses_server_sequence, format_with_current_shell_display, assert_eq!, matches!, from_value, skip_if_no_network! (+4 more))。

turn_start_permission_profile_rebinds_runtime_workspace_roots_between_turns2458–2604 ↗
async fn turn_start_permission_profile_rebinds_runtime_workspace_roots_between_turns() -> Result<()>

作用:验证权限配置里的动态工作区根目录,会在每轮 turn 按最新 runtime_workspace_roots 重新绑定。简单说,:workspace_roots 这个占位符不能一直指向旧目录。

数据流:进去的是一个权限 profile,其中 :workspace_roots 有写权限,以及两轮分别传入 old-root 和 new-root → 测试检查两次模型请求里的权限说明 → 出来是确认第一轮只含旧目录,第二轮只含新目录。

调用关系:它只在 Unix 上运行,因为路径和沙箱行为更稳定。它直接手写 config.toml,而不是用 create_config_toml,重点检查权限说明材料化过程。

调用图:调用 4 个内部函数(new, mount_sse_sequence, start_mock_server, from_absolute_path);外部调用 11 个(default, new, Integer, assert!, assert_eq!, format!, skip_if_no_network!, create_dir, write, timeout (+1 more))。

turn_start_resolves_sticky_thread_local_environment_and_turn_overrides2607–2659 ↗
async fn turn_start_resolves_sticky_thread_local_environment_and_turn_overrides() -> Result<()>

作用:验证线程上“粘住”的本地环境选择,以及 turn 上临时环境覆盖,能组合出正确结果。粘住的意思是 thread/start 选了环境后,后续 turn 默认沿用。

数据流:进去的是一个带 local 和空环境组合的多组测试用例 → 它启动 app-server 和线程环境配置,然后逐个调用 run_environment_selection_case → 出来是每个用例都能启动并完成 turn。

调用关系:它是环境选择场景的外层循环,真正每个 case 的 thread/start、turn/start 和通知检查交给 run_environment_selection_case

调用图:调用 3 个内部函数(new, create_config_toml, run_environment_selection_case);外部调用 6 个(default, new, create_mock_responses_server_repeating_assistant, create_dir, write, timeout)。

run_environment_selection_case2667–2740 ↗
async fn run_environment_selection_case(
    mcp: &mut TestAppServer,
    workspace: &Path,
    case: EnvironmentSelectionCase,
) -> Result<()>

作用:执行一个具体的环境选择用例,确认这一组 sticky 环境和 turn 环境能让 turn 正常开始并完成。

数据流:进去的是测试 app-server、工作目录和一个 EnvironmentSelectionCase → 它用 environment_params 生成线程和 turn 的环境参数,创建线程,发起 turn/start,读取 started/completed 通知 → 出来是确认通知里的 turn id 匹配,并清空消息缓冲。

调用关系:它只被 turn_start_resolves_sticky_thread_local_environment_and_turn_overrides 调用,是那个批量测试里的单用例执行器。它把环境参数生成交给 environment_params

调用图:调用 6 个内部函数(clear_message_buffer, read_stream_until_notification_message, read_stream_until_response_message, send_thread_start_request, send_turn_start_request, environment_params);被 1 处调用(turn_start_resolves_sticky_thread_local_environment_and_turn_overrides);外部调用 8 个(default, to_path_buf, to_string_lossy, Integer, assert_eq!, from_value, timeout, vec!)。

environment_params2742–2751 ↗
fn environment_params(ids: Option<&[&str]>, cwd: &Path) -> Option<Vec<TurnEnvironmentParams>>

作用:把一组环境 id 转成 turn/thread 请求需要的环境参数结构。没有 id 时返回 None,有 id 时每个 id 都带上同一个工作目录。

数据流:进去的是可选的 id 列表和 cwd 路径 → 它把每个字符串 id 包装成 TurnEnvironmentParams,并把 cwd 转成绝对路径 → 出来是可选的环境参数列表,不改动文件系统。

调用关系:它被 run_environment_selection_case 调用,用来避免在每个测试分支里重复写参数组装代码。

调用图:被 1 处调用(run_environment_selection_case)。

turn_start_file_change_approval_v22754–2920 ↗
async fn turn_start_file_change_approval_v2() -> Result<()>

作用:验证模型要求应用补丁改文件时,系统会先发文件修改审批;用户同意后才真正写入文件。补丁就是一段描述“新增、删除、修改哪些行”的文本。

数据流:进去的是一个会新增 README.md 的 apply_patch 模型响应和 untrusted 策略 → 测试发起 turn/start,等 FileChange item 开始,收到审批请求,回复 Accept,等完成 → 出来是确认 item 完成、文件被写成预期内容,并且 serverRequest/resolved 先于 item 完成出现。

调用关系:它通过模型响应触发 apply_patch 工具,再通过 JSON-RPC 审批机制模拟前端同意。它是文件修改安全链路的主测试。

调用图:调用 2 个内部函数(new, create_config_toml);外部调用 17 个(default, default, new, Integer, create_mock_responses_server_sequence, assert!, assert_eq!, panic!, assert_eq!, from_value (+7 more))。

turn_start_does_not_stream_apply_patch_change_updates_without_feature_v22923–3018 ↗
async fn turn_start_does_not_stream_apply_patch_change_updates_without_feature_v2() -> Result<()>

作用:验证未开启补丁流式更新功能时,即使模型分段输出补丁,也不会给前端发送 patchUpdated 通知。这样旧客户端不会突然收到不认识的新事件。

数据流:进去的是一个分两段输出 apply_patch 内容的模型响应,但配置没有打开 ApplyPatchStreamingEvents → 测试发起 turn/start 并等完成 → 出来是确认待处理通知里没有 item/fileChange/patchUpdated

调用关系:它和开启功能的流式更新测试是一组对照。它使用 create_config_toml 的默认 feature flags 来确认功能开关真的生效。

调用图:调用 2 个内部函数(new, create_config_toml);外部调用 10 个(default, default, new, Integer, create_mock_responses_server_sequence, assert!, skip_if_no_network!, create_dir, timeout, vec!)。

turn_start_streams_apply_patch_change_updates_v23021–3177 ↗
async fn turn_start_streams_apply_patch_change_updates_v2() -> Result<()>

作用:验证开启补丁流式更新功能后,模型边输出补丁,app-server 边把可解析出的文件变化通知给前端。这样前端可以实时显示“将要改哪些文件”。

数据流:进去的是打开 ApplyPatchStreamingEvents 的配置、支持 freeform apply_patch 的模型缓存,以及分段补丁响应 → 测试发起 turn/start,连续读取 item/fileChange/patchUpdated → 出来是确认通知带正确 thread_id、turn_id、item_id,并且 live.txt 的 diff 最终变成完整内容。

调用关系:它是 turn_start_does_not_stream_apply_patch_change_updates_without_feature_v2 的正向版本。它还故意放了一个非 apply_patch 函数调用,确保系统不会把无关工具的参数当补丁流。

调用图:调用 2 个内部函数(new, create_config_toml);外部调用 19 个(from, default, new, new, Integer, create_mock_responses_server_sequence, write_models_cache, assert!, assert_eq!, from (+9 more))。

turn_start_emits_spawn_agent_item_with_model_metadata_v23180–3420 ↗
async fn turn_start_emits_spawn_agent_item_with_model_metadata_v2() -> Result<()>

作用:验证模型调用 spawn_agent 创建子代理时,前端会看到一个协作代理工具 item,并包含子代理的 prompt、模型和推理强度。子代理可以理解成主模型派出去干活的“助手”。

数据流:进去的是父 turn 会调用 spawn_agent、子 turn 会返回完成、父 turn 后续完成的三段假响应 → 测试发起父 turn,读取 spawn item started 和 completed,再删除父线程并检查子线程也被删除 → 出来是确认 spawn item 元数据正确,删除线程会连带删除后代,loaded list 为空。

调用关系:它用多个 mount_sse_once_match 按请求内容匹配父、子、后续请求。它覆盖多代理创建、通知、线程树删除三个相关流程。

调用图:调用 5 个内部函数(new, create_config_toml, mount_sse_once_match, sse, start_mock_server);外部调用 15 个(from, default, new, new, Integer, default, assert!, assert_eq!, json!, from_value (+5 more))。

direct_input_to_multi_agent_v2_subagent_is_rejected3423–3548 ↗
async fn direct_input_to_multi_agent_v2_subagent_is_rejected() -> Result<()>

作用:验证 multi-agent v2 的子代理线程不能被 app-server 客户端直接发 turn/start 或 turn/steer。子代理应由父代理流程驱动,不能被外部随意插话。

数据流:进去的是一个父代理生成子代理的模型响应 → 测试拿到子线程 id 后,分别对它直接发 turn/start 和 turn/steer → 出来是两次都收到 INVALID_REQUEST 错误,错误信息说明不允许直接输入。

调用关系:它先通过 spawn_agent 跑出真实子线程,再检查拒绝路径。它和子代理正常创建测试互补:能创建,但不能被前端直接驱动。

调用图:调用 5 个内部函数(new, create_config_toml, mount_sse_once_match, sse, start_mock_server);外部调用 12 个(from, default, new, Integer, to_response, write_models_cache, assert_eq!, json!, from_value, to_string (+2 more))。

turn_start_emits_spawn_agent_item_with_effective_role_model_metadata_v23551–3737 ↗
async fn turn_start_emits_spawn_agent_item_with_effective_role_model_metadata_v2() -> Result<()>

作用:验证子代理指定 agent_type 后,显示给前端的模型元数据使用角色配置里的最终生效模型,而不是 spawn 调用里原始请求的模型。角色配置可以理解成预设岗位的默认模型和推理强度。

数据流:进去的是一个 custom 角色配置文件,里面写了 ROLE_MODEL 和 ROLE_REASONING_EFFORT,同时 spawn 参数里也写了另一个模型 → 测试发起父 turn,等 spawn completed item → 出来是确认 item 里的 model 和 reasoning_effort 是角色配置的值。

调用关系:它和 turn_start_emits_spawn_agent_item_with_model_metadata_v2 类似,但多了自定义 agent 角色配置。它检查“最终生效配置”会反映到前端通知。

调用图:调用 5 个内部函数(new, create_config_toml, mount_sse_once_match, sse, start_mock_server);外部调用 16 个(from, default, new, Integer, assert!, assert_eq!, format!, json!, from_value, to_string (+6 more))。

turn_start_file_change_approval_accept_for_session_persists_v23740–3920 ↗
async fn turn_start_file_change_approval_accept_for_session_persists_v2() -> Result<()>

作用:验证文件修改审批选择“本会话都允许”后,后续同一会话里的相关补丁不再重复询问。这样用户同意一次后,不会被同类操作反复打断。

数据流:进去的是两轮补丁响应,第一轮新增 README.md,第二轮修改它 → 第一轮收到审批后回复 AcceptForSession,确认文件创建;第二轮发起后直接等 item 完成,不应再收到审批请求 → 出来是文件内容最终变成 updated line。

调用关系:它是文件修改审批持久化策略的测试,基于 turn_start_file_change_approval_v2 覆盖的基础审批流程继续扩展。

调用图:调用 2 个内部函数(new, create_config_toml);外部调用 14 个(default, default, new, Integer, create_mock_responses_server_sequence, assert_eq!, panic!, from_value, to_value, skip_if_no_network! (+4 more))。

turn_start_file_change_approval_decline_v23923–4075 ↗
async fn turn_start_file_change_approval_decline_v2() -> Result<()>

作用:验证用户拒绝文件修改审批时,补丁不会被应用,FileChange item 会以 Declined 结束。这样拒绝修改能真正保护工作区文件。

数据流:进去的是一个新增 README.md 的补丁响应和 untrusted 策略 → 测试发起 turn/start,收到 FileChangeRequestApproval 后回复 Decline,等待 item 完成 → 出来是确认状态为 Declined,并且 README.md 不存在。

调用关系:它和文件修改批准测试是一正一反。它通过检查磁盘文件不存在,确认拒绝不是只影响 UI,而是真的阻止写入。

调用图:调用 2 个内部函数(new, create_config_toml);外部调用 16 个(default, default, new, Integer, create_mock_responses_server_sequence, assert!, assert_eq!, panic!, assert_eq!, from_value (+6 more))。

command_execution_notifications_include_process_id4079–4213 ↗
async fn command_execution_notifications_include_process_id() -> Result<()>

作用:验证统一执行工具发出的命令执行通知里包含进程 id,并且开始和完成通知里的进程 id 一致。进程 id 是操作系统给正在运行程序的编号。

数据流:进去的是一个会触发 unified exec 命令的模型响应,以及 danger-full-access 沙箱配置 → 测试发起 turn/start,读取 CommandExecution 的 started 和 completed item → 出来是确认开始时有 process_id,完成时同一个 process_id 仍在,状态和退出码合理。

调用关系:它用 create_config_toml_with_sandbox 打开 UnifiedExec 并设置沙箱。Windows 上忽略,因为进程 id 行为差异会影响稳定性。

调用图:调用 2 个内部函数(new, create_config_toml_with_sandbox);外部调用 12 个(from, default, new, Integer, create_mock_responses_server_sequence, assert!, assert_eq!, from_value, skip_if_no_network!, timeout (+2 more))。

turn_start_with_elevated_override_does_not_persist_project_trust4216–4274 ↗
async fn turn_start_with_elevated_override_does_not_persist_project_trust() -> Result<()>

作用:验证某一轮临时提升沙箱权限,不会把项目永久标记为受信任。这样一次性的高权限操作不会悄悄改变用户配置。

数据流:进去的是一个工作区和一次带 DangerFullAccess 沙箱覆盖的 turn/start → 测试等 turn 完成后读取 config.toml → 出来是确认配置里没有写入 trust_level = trusted,也没有记录该工作区路径。

调用关系:它使用普通模型成功响应,只关注配置文件是否被意外修改。它检查 turn 级权限覆盖和项目信任持久化之间的边界。

调用图:调用 2 个内部函数(new, create_config_toml);外部调用 9 个(from, default, new, Integer, create_mock_responses_server_sequence_unchecked, assert!, read_to_string, timeout, vec!)。

create_config_toml4277–4290 ↗
fn create_config_toml(
    codex_home: &Path,
    server_uri: &str,
    approval_policy: &str,
    feature_flags: &BTreeMap<Feature, bool>,
) -> std::io::Result<()>

作用:给测试临时目录写一个最常用的 app-server 配置文件,指向假模型服务器,默认沙箱是 read-only。大多数测试用它快速搭好可运行环境。

数据流:进去的是 codex_home 路径、假服务器地址、审批策略和功能开关表 → 它把沙箱模式固定为 read-only,再转交给 create_config_toml_with_sandbox 写文件 → 出来是一个 config.toml 文件或写文件错误。

调用关系:它被大量 turn/start 测试调用,是测试夹具里的默认配置入口。需要自定义沙箱时,测试会直接调用 create_config_toml_with_sandbox

调用图:调用 1 个内部函数(create_config_toml_with_sandbox);被 31 处调用(direct_input_to_multi_agent_v2_subagent_is_rejected, run_local_image_turn, thread_start_omits_empty_instruction_overrides_from_model_request, turn_start_accepts_collaboration_mode_override_v2, turn_start_accepts_personality_override_v2, turn_start_accepts_text_at_limit_with_mention_item, turn_start_additional_context_flows_to_model_input, turn_start_change_personality_mid_thread_v2, turn_start_does_not_stream_apply_patch_change_updates_without_feature_v2, turn_start_emits_notifications_and_accepts_model_override (+15 more))。

create_config_toml_with_sandbox4292–4338 ↗
fn create_config_toml_with_sandbox(
    codex_home: &Path,
    server_uri: &str,
    approval_policy: &str,
    feature_flags: &BTreeMap<Feature, bool>,
    sandbox_mode: &str,
) -> std::io::Result<()

作用:写入可自定义沙箱模式的测试配置文件。它把模型、审批策略、功能开关和 mock provider 地址都放进 config.toml。

数据流:进去的是配置目录、服务器地址、审批策略、功能开关和沙箱模式字符串 → 它把 Feature 枚举转换成配置键,拼成 TOML 文本并写到磁盘 → 出来是写文件成功或错误,同时磁盘上多了 config.toml

调用关系:它被 create_config_toml 包装,也被需要 danger-full-access 等特殊沙箱的测试直接调用。它是本文件所有测试环境配置的底层写入函数。

调用图:被 2 处调用(command_execution_notifications_include_process_id, create_config_toml);外部调用 4 个(new, join, format!, write)。

write_test_skill4340–4347 ↗
fn write_test_skill(codex_home: &Path, name: &str) -> std::io::Result<()>

作用:在临时 codex_home 里写一个最小可用的测试技能文件。技能可以理解成给模型看的某项能力说明。

数据流:进去的是 codex_home 路径和技能名 → 它创建 skills/<name> 目录,并写入带 name、description 和正文的 SKILL.md → 出来是文件系统里出现这个测试技能,或返回写入错误。

调用关系:它只被 turn_start_emits_thread_scoped_warning_notification_for_trimmed_skills 调用,用来制造技能数量和内容,触发技能裁剪警告。

调用图:被 1 处调用(turn_start_emits_thread_scoped_warning_notification_for_trimmed_skills);外部调用 4 个(join, format!, create_dir_all, write)。

app-server/tests/suite/v2/turn_steer.rs源码 ↗
testtest execution

这个文件不写正式功能,而是扮演“质检员”。它启动一个临时的测试版 app server,再用假的后端响应服务器来模拟模型和命令执行,避免真的连外网或跑不可控的服务。测试重点是:只有当前真的有活跃回合时才能追加输入;追加的文字不能超过长度上限;成功追加后,服务器要返回正在被追加的回合 ID;如果用户只传额外上下文、没有真正输入,服务器必须拒绝,而且不能偷偷把这些上下文塞进下一次请求里。文件还会检查埋点事件,也就是系统记录的统计日志,确认成功或失败的原因被正确记录。整体像是在给“行驶中的车临时打方向盘”做安全检查:车没开不能打,方向盘指令不能乱传,失败原因也要记清楚。

函数细节4
turn_steer_requires_active_turn40–105 ↗
async fn turn_steer_requires_active_turn() -> Result<()>

作用:这个测试确认:如果没有正在进行的回合,客户端不能发送 turn steer。这样可以防止用户把追加输入塞到一个不存在或已经结束的任务里。

数据流:测试先创建临时目录和假的响应服务器,再启动测试版 app server。它创建一个新线程,但不启动任何 turn,然后发送一个带有假的 expected_turn_id 的 steer 请求。结果应该是一个 JSON-RPC 错误,也就是接口层返回的标准错误;随后测试读取埋点事件,确认结果是 rejected,拒绝原因是 no_active_turn,并且没有 accepted_turn_id。

调用关系:它用 new_without_managed_config 启动测试服务器,用 mount_analytics_capture 挂上埋点收集,用 create_mock_responses_server_sequence 和配置写入函数准备假的后端地址。最后通过 wait_for_analytics_event 等待服务器发出的统计事件,检查这次拒绝不仅发生了,而且被正确记录。

调用图:调用 3 个内部函数(new_without_managed_config, mount_analytics_capture, wait_for_analytics_event);外部调用 9 个(default, new, Integer, create_mock_responses_server_sequence, write_mock_responses_config_toml_with_chatgpt_base_url, assert_eq!, create_dir, timeout, vec!)。

turn_steer_rejects_oversized_text_input108–217 ↗
async fn turn_steer_rejects_oversized_text_input() -> Result<()>

作用:这个测试确认:正在进行的回合虽然可以被追加输入,但追加文字不能超过系统允许的最大长度。这样可以避免超大输入拖垮服务,或让后续模型请求变得不可控。

数据流:测试先准备一个会执行 sleep 的假模型响应,让 turn 保持一段时间的活跃状态。它启动线程和 turn,等收到 turn/started 通知后,构造一段比 MAX_USER_INPUT_TEXT_CHARS 多 1 个字符的文本作为 steer 输入。服务器应返回 INVALID_PARAMS_ERROR_CODE,并在错误数据里说明输入太长、最大字符数和实际字符数。最后测试主动中断这个还在运行的 turn,避免测试留下后台任务。

调用关系:它通过 create_mock_responses_server_sequence_unchecked 和 create_shell_command_sse_response 制造一个“正在忙”的回合,再用 TestAppServer 发送 thread start、turn start、turn steer 这些 JSON-RPC 请求。这个测试主要盯住输入校验层,失败后不会把工作交给模型流程继续处理。

调用图:调用 2 个内部函数(new_without_managed_config, mount_analytics_capture);外部调用 9 个(default, new, Integer, create_mock_responses_server_sequence_unchecked, write_mock_responses_config_toml_with_chatgpt_base_url, assert_eq!, create_dir, timeout, vec!)。

turn_steer_returns_active_turn_id220–363 ↗
async fn turn_steer_returns_active_turn_id() -> Result<()>

作用:这个测试确认:当 turn steer 成功时,服务器会明确告诉客户端它追加到了哪一个正在运行的 turn。客户端拿到这个 ID 后,就不会误以为输入进了别的回合。

数据流:测试准备一个先 sleep、再返回最终助手消息的假后端。它启动线程和 turn,等 turn/started 后发送 steer 请求,请求里的 expected_turn_id 就是当前 turn 的 ID。服务器返回 TurnSteerResponse,里面的 turn_id 应等于当前 turn。接着测试继续监听 item/started 通知,确认那条追加的用户消息真的进入了线程,并且 client_user_message_id 和文本内容都对。之后它检查埋点事件,确认 result 是 accepted,accepted_turn_id 也是当前 turn,最后等待 turn/completed。

调用关系:它串起了完整的成功路径:测试服务器启动、假模型响应、线程创建、回合启动、追加输入、通知流验证、埋点验证、回合完成。它调用 wait_for_analytics_event 来确认业务动作之外的统计记录也正确,是对 turn steer 正常工作链路的一次端到端检查。

调用图:调用 3 个内部函数(new_without_managed_config, mount_analytics_capture, wait_for_analytics_event);外部调用 10 个(default, new, Integer, create_mock_responses_server_sequence_unchecked, write_mock_responses_config_toml_with_chatgpt_base_url, assert_eq!, from_value, create_dir, timeout, vec!)。

turn_steer_rejects_context_only_input_without_merging_context366–480 ↗
async fn turn_steer_rejects_context_only_input_without_merging_context() -> Result<()>

作用:这个测试确认:turn steer 不能只带“额外上下文”而不带用户输入。更重要的是,被拒绝的额外上下文不能偷偷混进后续发给模型的请求里。

数据流:测试启动一个会先执行 sleep、再完成的回合。然后它发送 steer 请求,input 是空数组,但 additional_context 里带了 browser_info。服务器应该返回错误,错误消息是 input must not be empty。等原本的 turn 完成后,测试查看假后端收到的 /responses 请求正文,确认里面没有出现用 external_browser_info 包起来的 tab one,也就是这份上下文没有被合并进去。

调用关系:它使用 HashMap 构造额外上下文,用 create_mock_responses_server_sequence_unchecked 准备假后端,再通过 TestAppServer 驱动真实的 JSON-RPC 流程。这个测试把重点放在“拒绝后不应产生副作用”:即使请求带了上下文,只要输入为空被拒绝,后面的模型请求也不能受到污染。

调用图:调用 2 个内部函数(new_without_managed_config, mount_analytics_capture);外部调用 12 个(default, from, new, new, Integer, create_mock_responses_server_sequence_unchecked, write_mock_responses_config_toml_with_chatgpt_base_url, assert!, assert_eq!, create_dir (+2 more))。

审查、压缩和导入会话

这些套件覆盖基于线程的高级生命周期流程,包括审查执行、上下文压缩、外部代理导入,以及活动会话期间的安全/状态信号。

app-server/tests/suite/v2/compaction.rs源码 ↗
testtest run

聊天越聊越长,模型能记住的内容会超过上限,所以系统需要把旧对话“压缩”成摘要,像把一大摞会议记录整理成一页纪要。这个测试文件专门检查这条流程是否可靠。它会启动一个假的模型服务器,让它按顺序吐出预设回复;再启动真实的 app-server 测试进程,创建线程,连续发送几轮消息,逼近或超过 token(可以粗略理解为模型读文字时的计量单位)限制。测试会观察 JSON-RPC 通知(客户端和服务端用 JSON 传消息的一种协议),确认压缩开始时会发 item/started,结束时会发 item/completed,而且两条通知指向同一个压缩项目。文件还分别覆盖本地压缩、远端压缩、手动触发压缩,以及线程 ID 错误时必须拒绝请求这些情况。这样能保证用户在长对话里不会因为压缩流程坏掉而丢上下文或看到错乱状态。

函数细节11
auto_compaction_local_emits_started_and_completed_items51–107 ↗
async fn auto_compaction_local_emits_started_and_completed_items() -> Result<()>

作用:这个测试确认:当对话太长、系统选择本地方式自动压缩时,客户端能收到“压缩开始”和“压缩完成”两条通知。它还检查这两条通知属于同一个聊天线程、同一个压缩项目。

数据流:进去的是一个假的模型服务器、临时配置目录和几条预设模型回复;测试把 token 数量设置得足够触发自动压缩,然后启动 app-server、创建线程、连续发三条用户消息。之后它从服务端消息流里等到上下文压缩的开始通知和完成通知,最后比对线程 ID 和压缩项目 ID,确认前后对应。

调用关系:这是本文件的主场景之一。它先用 start_mock_server、sse、mount_sse_sequence 准备假的模型回复,再用 start_thread 创建会话,用 send_turn_and_wait 推动多轮聊天,最后把检查工作交给 wait_for_context_compaction_started 和 wait_for_context_compaction_completed。

调用图:调用 8 个内部函数(new, send_turn_and_wait, start_thread, wait_for_context_compaction_completed, wait_for_context_compaction_started, mount_sse_sequence, sse, start_mock_server);外部调用 8 个(default, new, write_mock_responses_config_toml, assert_eq!, skip_if_no_network!, timeout, unreachable!, vec!)。

auto_compaction_remote_emits_started_and_completed_items110–249 ↗
async fn auto_compaction_remote_emits_started_and_completed_items() -> Result<()>

作用:这个测试确认:当系统走远端压缩接口时,也会正确发出“压缩开始”和“压缩完成”通知。它还额外检查真正打到了 /v1/responses/compact,并且请求头里的元数据写得对。

数据流:进去的是假的普通回复、假的远端压缩接口返回值、临时配置和一份模拟登录凭据;测试启动 app-server 后创建线程并发三轮消息。系统触发压缩后,测试读取压缩通知,同时查看假服务器记录到的请求:普通聊天请求应带 turn 元数据但不带 compaction 字段,压缩请求应标明这是自动、因上下文限制触发、发生在本轮前的压缩。

调用关系:这是远端压缩路径的完整验收测试。它和本地压缩测试一样调用 start_thread、send_turn_and_wait、wait_for_context_compaction_started、wait_for_context_compaction_completed,但还会用 mount_compact_json_once 模拟远端压缩接口,并用 parse_json_header 解析请求头中的 JSON 元数据。

调用图:调用 10 个内部函数(new, new_with_env, send_turn_and_wait, start_thread, wait_for_context_compaction_completed, wait_for_context_compaction_started, mount_compact_json_once, mount_sse_sequence, sse, start_mock_server);外部调用 11 个(from, new, write_chatgpt_auth, write_mock_responses_config_toml, assert!, assert_eq!, json!, skip_if_no_network!, timeout, unreachable! (+1 more))。

thread_compact_start_triggers_compaction_and_returns_empty_response252–305 ↗
async fn thread_compact_start_triggers_compaction_and_returns_empty_response() -> Result<()>

作用:这个测试确认:客户端手动发起“开始压缩线程”的请求时,服务端会真的启动压缩,并且请求本身能得到一个正常但没有额外内容的响应。它保证手动按钮或命令不会只是假装成功。

数据流:进去的是一个假模型服务器、一段压缩摘要回复和测试配置;测试启动 app-server,创建一个线程,然后发送 thread compact start 请求。它先等这个请求收到正常响应,再继续等 item/started 和 item/completed 通知,最后确认通知的线程 ID 正确、开始和完成对应同一个压缩项目。

调用关系:这是手动压缩入口的测试。它用 start_thread 先建立有效线程,直接通过 TestAppServer 发送压缩请求,然后复用 wait_for_context_compaction_started 和 wait_for_context_compaction_completed 来确认后台压缩流程真的跑完。

调用图:调用 7 个内部函数(new, start_thread, wait_for_context_compaction_completed, wait_for_context_compaction_started, mount_sse_sequence, sse, start_mock_server);外部调用 9 个(default, new, Integer, write_mock_responses_config_toml, assert_eq!, skip_if_no_network!, timeout, unreachable!, vec!)。

thread_compact_start_rejects_invalid_thread_id308–341 ↗
async fn thread_compact_start_rejects_invalid_thread_id() -> Result<()>

作用:这个测试确认:如果客户端传来的线程 ID 连格式都不像合法 ID,服务端必须拒绝。这样可以避免把明显错误的请求继续往后处理,造成难懂的异常。

数据流:进去的是一个临时配置和字符串 not-a-thread-id;测试启动 app-server 后发送手动压缩请求。服务端返回 JSON-RPC 错误,测试检查错误码是无效请求,并且错误消息里说明了 invalid thread id。

调用关系:这是手动压缩的防呆测试之一。它不需要创建线程,也不会进入真正压缩流程;它只验证请求入口处的校验逻辑会挡住格式错误的线程 ID。

调用图:调用 2 个内部函数(new, start_mock_server);外部调用 8 个(default, new, Integer, write_mock_responses_config_toml, assert!, assert_eq!, skip_if_no_network!, timeout)。

thread_compact_start_rejects_unknown_thread_id344–377 ↗
async fn thread_compact_start_rejects_unknown_thread_id() -> Result<()>

作用:这个测试确认:即使线程 ID 格式看起来合法,只要服务端并没有这个线程,也必须拒绝压缩。它防止客户端误操作或拿旧 ID 请求时影响不存在的会话。

数据流:进去的是一个看起来像 UUID 的线程 ID 和测试配置;测试启动 app-server 后直接发送压缩请求。服务端返回 JSON-RPC 错误,测试检查错误码是无效请求,并且错误消息里说明 thread not found。

调用关系:这是手动压缩的另一道防线测试。它和 invalid thread id 测试很像,但验证的是“格式合法但查不到线程”的情况,确保错误不会被误当成正常压缩任务。

调用图:调用 2 个内部函数(new, start_mock_server);外部调用 8 个(default, new, Integer, write_mock_responses_config_toml, assert!, assert_eq!, skip_if_no_network!, timeout)。

start_thread379–393 ↗
async fn start_thread(mcp: &mut TestAppServer) -> Result<String>

作用:这个辅助函数帮测试创建一个新的聊天线程,并把新线程的 ID 拿回来。测试后面发消息、压缩,都要靠这个 ID 找到同一个聊天。

数据流:进去的是一个可操作的 TestAppServer;函数发送 thread/start 请求,指定使用 mock-model,然后等待对应请求 ID 的响应。它把 JSON 响应转换成 ThreadStartResponse,取出里面的 thread.id,最后把这个字符串返回给调用者。

调用关系:它是多个测试的准备步骤,被自动本地压缩、自动远端压缩和手动压缩测试调用。它把“创建线程并解析响应”这段重复动作包起来,让主测试只关心后续聊天和压缩。

调用图:调用 2 个内部函数(read_stream_until_response_message, send_thread_start_request);被 3 处调用(auto_compaction_local_emits_started_and_completed_items, auto_compaction_remote_emits_started_and_completed_items, thread_compact_start_triggers_compaction_and_returns_empty_response);外部调用 3 个(default, Integer, timeout)。

send_turn_and_wait395–419 ↗
async fn send_turn_and_wait(
    mcp: &mut TestAppServer,
    thread_id: &str,
    text: &str,
) -> Result<String>

作用:这个辅助函数给指定线程发送一条用户消息,并等到这一轮对话真正完成。它避免测试在模型回复还没结束时就急着检查压缩结果。

数据流:进去的是 TestAppServer、线程 ID 和用户输入文本;函数把文本包装成 turn/start 请求发出去,等待启动响应,解析出 turn.id。随后它调用 wait_for_turn_completed 等到这一轮完成通知,最后返回这轮对话的 ID。

调用关系:它被两个自动压缩测试反复调用,用来把对话长度一步步推高。它自己负责发起 turn 请求,并把“等待完成”的细活交给 wait_for_turn_completed。

调用图:调用 3 个内部函数(read_stream_until_response_message, send_turn_start_request, wait_for_turn_completed);被 2 处调用(auto_compaction_local_emits_started_and_completed_items, auto_compaction_remote_emits_started_and_completed_items);外部调用 4 个(default, Integer, timeout, vec!)。

wait_for_turn_completed421–434 ↗
async fn wait_for_turn_completed(mcp: &mut TestAppServer, turn_id: &str) -> Result<()>

作用:这个辅助函数一直监听服务端通知,直到看到指定对话轮次完成。它像在门口等快递,只有单号对上了才算等到。

数据流:进去的是 TestAppServer 和目标 turn_id;函数循环读取 turn/completed 通知,把通知参数解析成 TurnCompletedNotification。每次都检查通知里的 turn.id,只有等于目标 ID 时才返回成功,否则继续等下一条。

调用关系:它只被 send_turn_and_wait 调用,是发送一轮消息后的收尾检查。这样上层测试不用自己处理通知流里可能出现的其他完成消息。

调用图:调用 1 个内部函数(read_stream_until_notification_message);被 1 处调用(send_turn_and_wait);外部调用 2 个(from_value, timeout)。

wait_for_context_compaction_started436–451 ↗
async fn wait_for_context_compaction_started(
    mcp: &mut TestAppServer,
) -> Result<ItemStartedNotification>

作用:这个辅助函数等待“上下文压缩开始”的通知。它帮测试确认服务端已经把压缩任务当作一个可见的线程项目发给客户端。

数据流:进去的是 TestAppServer;函数循环读取 item/started 通知,把参数解析成 ItemStartedNotification。它检查通知里的 item 是否是 ContextCompaction,如果是就返回这条通知,否则继续等。

调用关系:它被本地自动压缩、远端自动压缩和手动压缩测试调用。它专门筛选压缩开始事件,让主测试不用关心消息流里其他类型的 item/started。

调用图:调用 1 个内部函数(read_stream_until_notification_message);被 3 处调用(auto_compaction_local_emits_started_and_completed_items, auto_compaction_remote_emits_started_and_completed_items, thread_compact_start_triggers_compaction_and_returns_empty_response);外部调用 2 个(from_value, timeout)。

wait_for_context_compaction_completed453–468 ↗
async fn wait_for_context_compaction_completed(
    mcp: &mut TestAppServer,
) -> Result<ItemCompletedNotification>

作用:这个辅助函数等待“上下文压缩完成”的通知。它用来确认压缩不只是开始了,而且已经走完整个流程。

数据流:进去的是 TestAppServer;函数循环读取 item/completed 通知,把参数解析成 ItemCompletedNotification。它检查完成的项目是否是 ContextCompaction,是的话返回通知,否则继续等待。

调用关系:它和 wait_for_context_compaction_started 成对使用,被三个压缩成功场景调用。主测试先等开始、再等完成,然后比较两条通知中的 ID 是否一致。

调用图:调用 1 个内部函数(read_stream_until_notification_message);被 3 处调用(auto_compaction_local_emits_started_and_completed_items, auto_compaction_remote_emits_started_and_completed_items, thread_compact_start_triggers_compaction_and_returns_empty_response);外部调用 2 个(from_value, timeout)。

parse_json_header470–472 ↗
fn parse_json_header(value: &str) -> serde_json::Value

作用:这个小工具把 HTTP 请求头里的 JSON 字符串解析成 JSON 值,方便测试检查里面的字段。它主要服务于远端压缩测试里的元数据检查。

数据流:进去的是一个字符串形式的请求头值;函数用 JSON 解析器把它转换成 serde_json::Value。如果字符串不是合法 JSON,测试会直接失败并提示 turn metadata should be JSON;成功时返回可按字段读取的 JSON 对象。

调用关系:它被远端压缩测试在检查 x-codex-turn-metadata 请求头时使用。普通聊天请求和压缩请求都会经过它解析,之后测试再比较 request_kind、turn_id、window_id 和 compaction 等字段。

调用图:外部调用 1 个(from_str)。

app-server/tests/suite/v2/external_agent_config.rs源码 ↗
testtest run

这个文件不负责真正导入数据,而是扮演用户来测试服务器。它会临时造出假的用户目录、假的 Claude 配置、假的插件市场、假的历史会话文件,再启动一个测试版 App Server,通过 JSON-RPC(一种用 JSON 发请求、收响应的通信格式)向服务器发送“检测外部配置”“导入外部配置”“读取线程”等请求。测试重点是:同步导入结束后必须发完成通知;坏配置不能悄悄通过;本地插件要被安装并启用;历史会话要变成 Codex 里的线程;重复导入不能制造重复会话;大历史会话在继续对话前要先压缩摘要。这里还特别测了后台导入:请求可以先返回,真正的耗时导入稍后完成并通知用户。没有这些测试,外部迁移功能很容易出现“看似成功但没记录”“重复导入”“导入后不能继续聊”等隐蔽问题。

函数细节12
assert_import_response38–41 ↗
fn assert_import_response(response: ExternalAgentConfigImportResponse) -> String

作用:这是一个小检查工具,用来确认导入请求真的返回了一个非空的导入编号。导入编号就像快递单号,后面的进度通知和完成通知都靠它对上同一次导入。

数据流:进去的是服务器返回的导入响应 → 它检查里面的 import_id 不是空字符串 → 出来的是这个 import_id,供后续断言通知、历史记录和导入结果是否属于同一批任务。

调用关系:多个导入测试在拿到响应后都会先调用它。它本身只做最基础的验票工作,不继续调用复杂逻辑;如果这里失败,说明服务器连“给导入任务编号”这一步都没做好。

调用图:被 8 处调用(external_agent_config_import_accepts_detected_session_payload_after_restart, external_agent_config_import_compacts_huge_session_before_first_follow_up, external_agent_config_import_creates_session_rollouts, external_agent_config_import_returns_before_background_session_import_finishes, external_agent_config_import_sends_completion_notification_after_pending_plugins_finish, external_agent_config_import_sends_completion_notification_for_local_plugins, external_agent_config_import_sends_completion_notification_for_sync_only_import, external_agent_config_import_skips_already_imported_session_versions);外部调用 1 个(assert!)。

external_agent_config_import_sends_completion_notification_for_sync_only_import44–158 ↗
async fn external_agent_config_import_sends_completion_notification_for_sync_only_import() -> Result<()>

作用:这个测试确认只导入配置这种“同步完成”的任务时,服务器会先返回导入编号,再发送进度通知和完成通知,并把结果写进历史记录。

数据流:它先创建临时的 Codex 目录和 SQLite 状态目录 → 启动测试服务器并发送 externalAgentConfig/import 请求,只要求导入 CONFIG → 读取响应、进度通知、完成通知 → 再打开状态数据库检查成功和失败明细是否被保存 → 最后调用读取导入历史的接口确认用户以后还能查到这次导入。

调用关系:测试运行器启动它后,它用 TestAppServer 模拟客户端,调用 assert_import_response 验证导入编号,并通过 StateRuntime 读取数据库。它覆盖的是最基础、最理想的导入链路:请求、通知、落库、历史查询都要通。

调用图:调用 3 个内部函数(new_with_env, assert_import_response, init);外部调用 8 个(new, Integer, to_response, assert!, assert_eq!, from_value, json!, timeout)。

external_agent_config_import_returns_error_for_failed_sync_import161–200 ↗
async fn external_agent_config_import_returns_error_for_failed_sync_import() -> Result<()>

作用:这个测试确认如果现有 config.toml 是坏的,配置导入会明确失败,而不是假装成功或写坏用户配置。

数据流:它先在临时目录里写入一个 Claude settings.json,又故意写一个语法错误的 config.toml → 启动测试服务器 → 发送导入 CONFIG 的请求 → 等待 JSON-RPC 错误响应 → 检查错误码是内部错误,并且错误信息提到现有配置无效。

调用关系:它由测试运行器直接执行,主要和 TestAppServer 通信。和成功路径测试不同,它不调用 assert_import_response,因为这里预期没有正常导入响应,而是服务器应该返回错误。

调用图:调用 1 个内部函数(new_with_env);外部调用 8 个(new, Integer, assert!, assert_eq!, json!, create_dir_all, write, timeout)。

external_agent_config_import_sends_completion_notification_for_local_plugins203–315 ↗
async fn external_agent_config_import_sends_completion_notification_for_local_plugins() -> Result<()>

作用:这个测试确认从外部配置里导入本地插件后,插件会出现在插件列表里,并且处于已安装、已启用状态。

数据流:它先搭出一个假的本地插件市场目录,写入 marketplace.json 和 plugin.json,再在 Claude settings.json 里声明启用这个插件 → 启动服务器并发送 PLUGINS 导入请求 → 拿到导入编号并等待完成通知 → 再调用插件列表接口 → 检查 sample 插件已经安装且启用。

调用关系:测试运行器执行它;它用 assert_import_response 连接“导入响应”和“完成通知”;完成后把流程交给插件列表接口来验证真实效果,而不是只看导入通知。

调用图:调用 2 个内部函数(new_with_env, assert_import_response);外部调用 11 个(new, Integer, to_response, assert!, assert_eq!, from_value, json!, to_string_pretty, create_dir_all, write (+1 more))。

external_agent_config_import_sends_completion_notification_after_pending_plugins_finish318–381 ↗
async fn external_agent_config_import_sends_completion_notification_after_pending_plugins_finish() -> Result<()>

作用:这个测试确认插件导入即使需要走后台流程,也最终会发完成通知。它用一个无效的非本地插件来源,避免测试时真的去联网克隆代码。

数据流:它写入一个包含外部插件市场的 Claude settings.json,其中 source 故意无效 → 启动测试服务器 → 发送导入 PLUGINS 的请求 → 读取正常响应拿到 import_id → 等待 completed 通知并确认通知里的 import_id 对得上。

调用关系:它由测试运行器调用,使用 TestAppServer 发请求、读通知,并用 assert_import_response 检查导入编号。它关注的是“后台导入也必须有最终交代”,不检查插件是否真正可用。

调用图:调用 2 个内部函数(new_with_env, assert_import_response);外部调用 9 个(new, Integer, to_response, assert_eq!, from_value, json!, create_dir_all, write, timeout)。

external_agent_config_import_creates_session_rollouts384–598 ↗
async fn external_agent_config_import_creates_session_rollouts() -> Result<()>

作用:这个测试确认外部历史会话导入后,会变成 Codex 里的线程,而且这个线程可以继续聊天。它是在验证“搬家后还能接着住”,不是只把文件复制过去。

数据流:它先启动一个假的模型响应服务器,再写入测试用 config.toml 和一份 Claude session.jsonl 历史会话,里面有用户消息、助手消息和标题 → 调用 detect 找到可迁移会话 → 调用 import 导入 → 检查完成通知里的成功项包含源文件和新线程编号 → 列出线程,确认预览和标题正确 → 读取线程,确认里面有导入标记 → 恢复线程并发起 follow up → 最后确认新一轮助手回复来自假服务器。

调用关系:它串起 detect、import、thread/list、thread/read、thread/resume、turn/start 这些接口,是本文件里最完整的端到端测试之一。它调用 create_config_toml 准备模型配置,调用 assert_import_response 对齐导入任务。

调用图:调用 3 个内部函数(new_with_env, assert_import_response, create_config_toml);外部调用 15 个(default, new, Integer, create_mock_responses_server_repeating_assistant, to_response, assert_eq!, now, panic!, from_value, json! (+5 more))。

external_agent_config_import_does_not_initialize_required_mcp601–691 ↗
async fn external_agent_config_import_does_not_initialize_required_mcp() -> Result<()>

作用:这个测试确认导入历史会话时,不会顺手启动配置里标成必需但已经坏掉的 MCP 服务器。MCP 可以理解为外接工具服务;这里要防止导入被无关工具拖垮。

数据流:它先写入正常模型配置,又追加一个 required=true 但命令不存在的 MCP 服务 → 准备一份外部会话文件 → 启动测试服务器 → 直接发送 SESSIONS 导入请求 → 等完成通知 → 再列出线程,确认会话仍然成功导入了一条。

调用关系:测试运行器调用它;它用 create_config_toml 准备基础配置,但不使用 assert_import_response,因为这里主要关心请求能返回、导入能完成、坏 MCP 不被初始化。它验证导入流程和工具初始化流程之间保持隔离。

调用图:调用 2 个内部函数(new_with_env, create_config_toml);外部调用 11 个(new, Integer, create_mock_responses_server_repeating_assistant, to_response, assert_eq!, now, json!, create_dir_all, read_to_string, write (+1 more))。

external_agent_config_import_accepts_detected_session_payload_after_restart694–782 ↗
async fn external_agent_config_import_accepts_detected_session_payload_after_restart() -> Result<()>

作用:这个测试名字强调“重启后仍接受检测到的会话载荷”,实际是在确认显式传入的 SESSIONS 导入数据能被服务器接受并导入成功。

数据流:它创建临时目录、模型配置、项目目录和一份 Claude 会话文件 → 启动服务器 → 发送包含 sessions 明细的 externalAgentConfig/import 请求 → 读取导入响应和完成通知 → 最后列出线程,确认只导入出一条线程。

调用关系:它由测试运行器在多线程 Tokio 环境里执行。它调用 create_config_toml 设置假模型服务,调用 assert_import_response 检查导入编号,然后通过线程列表接口验证结果落到了系统里。

调用图:调用 3 个内部函数(new_with_env, assert_import_response, create_config_toml);外部调用 11 个(new, Integer, create_mock_responses_server_repeating_assistant, to_response, assert_eq!, now, from_value, json!, create_dir_all, write (+1 more))。

external_agent_config_import_skips_already_imported_session_versions785–874 ↗
async fn external_agent_config_import_skips_already_imported_session_versions() -> Result<()>

作用:这个测试确认同一份外部会话被导入两次时,系统不会重复创建两条线程。它防止用户反复点导入后历史记录变成一堆重复内容。

数据流:它先准备一份可检测的外部会话 → 调用 detect 拿到迁移项目 → 连续两次用同一批 detected.items 发起 import → 每次都等待响应和完成通知 → 最后列出线程,确认数据库里仍然只有一条线程。

调用关系:它由测试运行器调用,使用 create_config_toml 和假模型服务器准备环境,循环里用 assert_import_response 验证每次导入都有编号。它测试的是导入逻辑里的“去重”能力。

调用图:调用 3 个内部函数(new_with_env, assert_import_response, create_config_toml);外部调用 11 个(new, Integer, create_mock_responses_server_repeating_assistant, to_response, assert_eq!, now, from_value, json!, create_dir_all, write (+1 more))。

external_agent_config_import_returns_before_background_session_import_finishes878–1013 ↗
async fn external_agent_config_import_returns_before_background_session_import_finishes() -> Result<()>

作用:这个 Unix 专用测试确认会话导入可以先返回响应,把耗时读取放到后台继续做。它还确认如果同一份会话导入两次,后台完成通知会分别发出,但最终不会产生重复线程。

数据流:它先准备一份会话并通过 detect 取得迁移项目 → 然后把会话文件换成 FIFO(先进先出的管道文件,可用来故意卡住读取)→ 发起第一次 import,确认 5 秒内先拿到响应 → 短时间内确认还没有 completed 通知 → 再发起第二次相同 import,也拿到响应 → 接着两次往 FIFO 写入会话内容,解除后台读取阻塞 → 收到两个完成通知并核对两个 import_id → 最后列出线程,确认只有一条。

调用关系:它只在 Unix 系统编译运行,因为 mkfifo 是 Unix 工具。它调用 create_config_toml、assert_import_response,并使用 Tokio 异步写文件来控制后台导入何时继续。它专门测试“前台快速响应”和“后台最终完成”之间的配合。

调用图:调用 3 个内部函数(new_with_env, assert_import_response, create_config_toml);外部调用 18 个(from_secs, new, new, Integer, create_mock_responses_server_repeating_assistant, to_response, assert!, assert_eq!, now, new (+8 more))。

external_agent_config_import_compacts_huge_session_before_first_follow_up1016–1189 ↗
async fn external_agent_config_import_compacts_huge_session_before_first_follow_up() -> Result<()>

作用:这个测试确认导入超大的历史会话后,第一次继续聊天前会先做压缩摘要,避免把过长上下文直接塞给模型。

数据流:它启动一个假的响应服务器,并安排两次模型响应:第一次返回 LOCAL_SUMMARY,第二次返回 follow-up answer → 写入带自动压缩限制的配置 → 创建一份包含超长用户消息和助手消息的外部会话 → detect 并 import 这份会话 → 恢复线程并发送 follow up → 等对话完成 → 查看假模型服务器收到的两个请求:第一个应包含摘要提示而不包含 follow up,第二个应包含 follow up 和 LOCAL_SUMMARY。

调用关系:测试运行器在多线程 Tokio 环境中执行它。它用响应测试工具 mount_sse_sequence 安排模型流式返回,用 write_mock_responses_config_toml 写配置,用 assert_import_response 对齐导入任务。它连接了“会话导入”和“后续对话压缩”两个子系统。

调用图:调用 4 个内部函数(new_with_env, assert_import_response, mount_sse_sequence, start_mock_server);外部调用 15 个(default, default, new, Integer, to_response, write_mock_responses_config_toml, assert!, assert_eq!, now, from_value (+5 more))。

create_config_toml1191–1211 ↗
fn create_config_toml(codex_home: &std::path::Path, server_uri: &str) -> std::io::Result<()>

作用:这是测试用的小工具,用来在临时 Codex 目录里写一份最小可用的 config.toml。没有它,很多会话导入测试就没法让服务器知道该连哪个假模型服务。

数据流:进去的是临时 Codex 目录路径和假模型服务器地址 → 它拼出 config.toml 文件路径,并把模型名、审批策略、沙箱模式、模型供应商和 base_url 写进去 → 出来的是写文件的结果,成功表示测试服务器启动后能读到这份配置。

调用关系:多个会话相关测试在启动 TestAppServer 前调用它。它不参与请求流程本身,只负责布置测试环境,让后面的导入、恢复线程、继续对话都能使用同一个假模型服务。

调用图:被 5 处调用(external_agent_config_import_accepts_detected_session_payload_after_restart, external_agent_config_import_creates_session_rollouts, external_agent_config_import_does_not_initialize_required_mcp, external_agent_config_import_returns_before_background_session_import_finishes, external_agent_config_import_skips_already_imported_session_versions);外部调用 3 个(join, format!, write)。

app-server/tests/suite/v2/review.rs源码 ↗
testtest run

这个测试文件专门盯住 app server 的 review/start 接口,也就是“请系统开始做一次代码审查”。它会启动一个假的应用服务器,再接一个假的模型服务,避免真的联网调用 AI。测试会模拟几种真实情况:审查某个提交、在原线程里展示审查结果、另开线程做审查、审查时触发 shell 命令审批,以及用户传了空分支名、空提交号、空说明时必须报错。这里用 JSON-RPC(用 JSON 格式来发请求、收响应的一种通信约定)检查服务器吐出的响应和通知。几个辅助函数负责搭好临时配置、开默认线程、让线程先跑一轮。没有这些测试,审查功能即使悄悄改坏了,比如结果没显示、线程用错、错误输入被放行,也可能到用户手里才暴露。

函数细节10
review_start_runs_review_turn_and_emits_code_review_item38–153 ↗
async fn review_start_runs_review_turn_and_emits_code_review_item() -> Result<()>

作用:这个测试确认:启动一次内联代码审查后,服务器会真的跑一个审查回合,并把模型给出的审查意见变成用户能看到的审查条目。

数据流:进去的是一段假的模型返回内容,里面有一条代码问题、文件路径和行号;测试把它挂到临时配置里的假模型服务上,然后启动服务器、创建线程、发送 review/start 请求。之后它读取服务器返回和后续通知,检查回合状态是进行中,用户消息文字像“commit 1234567: Tidy UI colors”,并确认先看到“进入审查模式”,再看到“退出审查模式”且正文包含问题标题和代码位置。

调用关系:它是审查主流程的验收测试。它先调用 create_config_toml 写配置,再调用 start_default_thread 建一个普通线程,然后通过 TestAppServer 发送审查请求,并持续读取 JSON-RPC 通知来验证服务器内部流程没有断。

调用图:调用 3 个内部函数(new, create_config_toml, start_default_thread);外部调用 8 个(new, Integer, create_mock_responses_server_repeating_assistant, assert!, assert_eq!, json!, from_value, timeout)。

review_start_exec_approval_item_id_matches_command_execution_item157–251 ↗
async fn review_start_exec_approval_item_id_matches_command_execution_item() -> Result<()>

作用:这个测试确认:审查过程中如果模型要求执行命令,而且当前策略需要用户批准,那么“审批请求里的条目编号”和“实际命令执行条目的编号”必须是同一个。

数据流:进去的是一组假的模型流式回复:先要求执行 git rev-parse HEAD,再给最终回答。测试把审批策略设成 untrusted,也就是不信任命令、需要确认;启动审查后,它读取服务器发给客户端的命令审批请求,检查审批参数里的 item_id 是 review-call-1,又继续等到命令执行条目出现,确认它的 id 也一样。最后测试模拟用户批准,让流程结束。

调用关系:它覆盖的是审查流程和命令审批流程的交叉点。它调用 create_config_toml_with_approval_policy 来制造需要审批的环境,调用 start_default_thread 准备线程,然后等服务器主动发出 ServerRequest,最后把批准结果回给服务器。这个测试当前被标记为忽略,因为注释说明它还不稳定。

调用图:调用 3 个内部函数(new, create_config_toml_with_approval_policy, start_default_thread);外部调用 9 个(new, Integer, create_mock_responses_server_sequence, assert_eq!, panic!, from_value, json!, timeout, vec!)。

review_start_rejects_empty_base_branch254–285 ↗
async fn review_start_rejects_empty_base_branch() -> Result<()>

作用:这个测试确认:如果用户要求审查某个基础分支,但分支名只有空格,服务器必须拒绝,而不是继续跑。

数据流:进去的是一个 BaseBranch 审查目标,branch 字段是几个空格。测试启动假服务器和线程后发送 review/start 请求,然后读取错误响应。出来的结果应该是 JSON-RPC 错误,错误码是 -32600,并且错误消息里说明 branch must not be empty。

调用关系:它测试输入校验这一关。它和其他拒绝空输入的测试一样,先用 create_config_toml 和 start_default_thread 搭环境,再把坏请求交给服务器,看服务器是否在真正审查前就拦住。

调用图:调用 3 个内部函数(new, create_config_toml, start_default_thread);外部调用 6 个(new, Integer, create_mock_responses_server_repeating_assistant, assert!, assert_eq!, timeout)。

review_start_with_detached_delivery_returns_new_thread_id289–372 ↗
async fn review_start_with_detached_delivery_returns_new_thread_id() -> Result<()>

作用:这个测试确认:如果审查方式选择 Detached,也就是“另开一个地方跑审查”,服务器应该返回一个新的审查线程编号,而不是沿用原线程。

数据流:进去的是一个自定义审查说明“detached review”,delivery 设置为 Detached。测试先创建原线程,并调用 materialize_thread_rollout 让原线程已有一次完整展开;然后发起审查请求。它检查返回的 review_thread_id 和原 thread_id 不一样,审查回合里用户消息正确,并继续读通知,确认新线程是通过 thread/started 正式介绍出来的,而不是先发一个不合适的状态变更通知。

调用关系:它验证审查流程里的“新线程承载审查”分支。它调用 create_config_toml 配置假模型,调用 start_default_thread 建原线程,再调用 materialize_thread_rollout 让线程状态更接近真实使用,最后观察服务器通知顺序是否符合协议。

调用图:调用 4 个内部函数(new, create_config_toml, materialize_thread_rollout, start_default_thread);外部调用 10 个(new, bail!, Integer, create_mock_responses_server_repeating_assistant, assert_eq!, assert_ne!, json!, from_value, now, timeout)。

review_start_rejects_empty_commit_sha375–407 ↗
async fn review_start_rejects_empty_commit_sha() -> Result<()>

作用:这个测试确认:如果用户说要审查某个提交,但提交号是空白字符,服务器必须报错。

数据流:进去的是一个 Commit 审查目标,sha 字段只有制表符,title 为空。测试启动环境、创建线程、发送请求,然后读取错误响应。出来的结果应该是错误码 -32600,并且错误消息包含 sha must not be empty。

调用关系:它和 base branch 的空值测试类似,都是守住 review/start 的入口。它依赖 create_config_toml 搭好假模型配置,依赖 start_default_thread 拿到合法线程编号,然后专门检查坏 sha 不会进入后续审查流程。

调用图:调用 3 个内部函数(new, create_config_toml, start_default_thread);外部调用 6 个(new, Integer, create_mock_responses_server_repeating_assistant, assert!, assert_eq!, timeout)。

review_start_rejects_empty_custom_instructions410–444 ↗
async fn review_start_rejects_empty_custom_instructions() -> Result<()>

作用:这个测试确认:如果用户选择自定义审查,但说明文字只有换行,服务器必须拒绝。

数据流:进去的是一个 Custom 审查目标,instructions 字段只有空行。测试发出 review/start 后不等正常响应,而是等错误响应。出来的结果应该是错误码 -32600,错误消息里包含 instructions must not be empty。

调用关系:它覆盖自定义审查目标的输入校验。它调用 create_config_toml 和 start_default_thread 准备最小可运行环境,再把无效说明交给服务器,确保服务器在启动模型调用前就返回清楚的错误。

调用图:调用 3 个内部函数(new, create_config_toml, start_default_thread);外部调用 6 个(new, Integer, create_mock_responses_server_repeating_assistant, assert!, assert_eq!, timeout)。

start_default_thread446–465 ↗
async fn start_default_thread(mcp: &mut TestAppServer) -> Result<String>

作用:这个辅助函数帮测试快速创建一个默认线程,并返回线程编号。这样每个测试不用重复写同一段“开线程、等响应、等通知”的样板代码。

数据流:进去的是一个可操作的 TestAppServer。函数向它发送 thread/start 请求,指定模型为 mock-model;然后等待对应响应,把响应里的 ThreadStartResponse 解析出来;接着再等 thread/started 通知,确认服务器真的宣布线程已开始。出来的是 thread.id,也就是后面发审查请求要用的线程编号。

调用关系:它被几乎所有审查测试调用,是这些测试的前置步骤。它自己把活交给 TestAppServer 的 send_thread_start_request、read_stream_until_response_message 和 read_stream_until_notification_message,并用 timeout 防止测试无限卡住。

调用图:调用 3 个内部函数(read_stream_until_notification_message, read_stream_until_response_message, send_thread_start_request);被 6 处调用(review_start_exec_approval_item_id_matches_command_execution_item, review_start_rejects_empty_base_branch, review_start_rejects_empty_commit_sha, review_start_rejects_empty_custom_instructions, review_start_runs_review_turn_and_emits_code_review_item, review_start_with_detached_delivery_returns_new_thread_id);外部调用 3 个(default, Integer, timeout)。

materialize_thread_rollout467–490 ↗
async fn materialize_thread_rollout(mcp: &mut TestAppServer, thread_id: &str) -> Result<()>

作用:这个辅助函数让一个已有线程先跑完一轮普通对话,好让后面的 detached review 测试看起来更像真实场景。

数据流:进去的是 TestAppServer 和一个线程编号。函数发送 turn/start 请求,用户输入是“materialize rollout”;然后等待这个请求的响应,再等待 turn/completed 通知。出来没有新数据返回,但服务器里的这个线程已经经历过一次完整回合。

调用关系:它只被 review_start_with_detached_delivery_returns_new_thread_id 调用。它位于 detached review 测试的准备阶段,用来先把原线程“落地”一次,再检查另开审查线程时的行为。

调用图:调用 3 个内部函数(read_stream_until_notification_message, read_stream_until_response_message, send_turn_start_request);被 1 处调用(review_start_with_detached_delivery_returns_new_thread_id);外部调用 4 个(default, Integer, timeout, vec!)。

create_config_toml492–494 ↗
fn create_config_toml(codex_home: &std::path::Path, server_uri: &str) -> std::io::Result<()>

作用:这个小函数给测试写一份默认配置文件,默认不需要审批命令。它让测试服务器知道该用哪个假模型服务。

数据流:进去的是临时的 codex_home 路径和假模型服务器地址。函数不自己拼完整文件内容,而是把审批策略固定成 never,然后转交给 create_config_toml_with_approval_policy。出来的是写文件是否成功的结果。

调用关系:它是多数测试的配置快捷入口。普通审查和输入校验测试都调用它;如果测试需要特殊审批策略,就绕过它直接调用 create_config_toml_with_approval_policy。

调用图:调用 1 个内部函数(create_config_toml_with_approval_policy);被 5 处调用(review_start_rejects_empty_base_branch, review_start_rejects_empty_commit_sha, review_start_rejects_empty_custom_instructions, review_start_runs_review_turn_and_emits_code_review_item, review_start_with_detached_delivery_returns_new_thread_id)。

create_config_toml_with_approval_policy496–524 ↗
fn create_config_toml_with_approval_policy(
    codex_home: &std::path::Path,
    server_uri: &str,
    approval_policy: &str,
) -> std::io::Result<()>

作用:这个函数真正把测试用的 config.toml 写到临时目录里,并允许指定命令审批策略。它是搭建假运行环境的关键一步。

数据流:进去的是配置目录、假模型服务地址和 approval_policy 字符串。函数在目录下定位 config.toml,拼出一段 TOML 配置文本:模型名是 mock-model,沙箱是只读,模型提供方指向假服务器的 /v1,重试次数设为 0。出来的是 std::fs::write 的结果;成功时磁盘上多了一份测试配置文件。

调用关系:它被 create_config_toml 包装调用,也被命令审批测试直接调用。后续 TestAppServer::new 会读取这份配置,所以它决定了测试里的服务器连接哪个假模型、是否需要命令审批。

调用图:被 2 处调用(create_config_toml, review_start_exec_approval_item_id_matches_command_execution_item);外部调用 3 个(join, format!, write)。

app-server/tests/suite/v2/safety_check_downgrade.rs源码 ↗
testtest execution

这个测试文件把真实服务器外部依赖换成一个假的 OpenAI 接口,就像排练时用假收银台模拟付款流程。测试先写一份临时配置,让 app-server 去连这个假接口;然后启动一个测试服务器,创建对话线程,再发起一轮用户输入。假接口会返回几种特殊情况:实际模型和请求模型不一致、网络安全策略拒绝、模型验证元数据、审核元数据等。测试会读取服务器通过 JSON-RPC(一种用 JSON 格式传请求、响应和通知的通信方式)发回来的消息,检查它们是不是被翻译成了清楚的类型化通知,比如“model/rerouted”“model/verification”“turn/moderationMetadata”或“error”。它还特别检查旧式的 Warning 用户消息没有混进来,确保 v2 协议走的是结构化通知,而不是把警告塞进聊天内容里。

函数细节11
openai_model_header_mismatch_emits_model_rerouted_notification_v237–99 ↗
async fn openai_model_header_mismatch_emits_model_rerouted_notification_v2() -> Result<()>

作用:这个测试确认:如果上游接口的响应头里说实际用了另一个模型,服务器会通知客户端“模型被改路由了”。这能防止客户端以为自己用的是请求的模型,实际上却用了别的模型。

数据流:测试先搭一个假接口,让它返回正常的流式回答,但在 OpenAI-Model 响应头里写入实际模型。然后它写临时配置、启动测试 app-server、创建线程、发起一轮输入。最后它收集服务器通知,期待得到一个 ModelReroutedNotification,里面包含原模型、实际模型、线程 ID、轮次 ID 和高风险网络安全活动这个原因。

调用关系:它是一个完整场景测试。前半段调用 start_mock_server、sse、sse_response、mount_response_once 准备假上游;中间调用 create_config_toml 和 TestAppServer::new 搭测试环境;最后把读取和检查通知的活交给 collect_turn_notifications_and_validate_no_warning_item。

调用图:调用 7 个内部函数(new, collect_turn_notifications_and_validate_no_warning_item, create_config_toml, mount_response_once, sse, sse_response, start_mock_server);外部调用 8 个(default, new, Integer, to_response, assert_eq!, skip_if_no_network!, timeout, vec!)。

cyber_policy_response_emits_typed_error_notification_v2102–169 ↗
async fn cyber_policy_response_emits_typed_error_notification_v2() -> Result<()>

作用:这个测试确认:如果上游直接返回 cyber_policy 错误,服务器会发一个带明确类型的错误通知,而不是误报成模型改路由。这样客户端能知道这是安全策略拒绝,不是普通失败。

数据流:测试先让假接口返回 HTTP 400,并在 JSON 错误体里写明 code 是 cyber_policy。接着启动测试服务器,创建线程并发送一轮会触发该错误的输入。之后它读取通知,期待收到 ErrorNotification,其中错误信息标记为 CodexErrorInfo::CyberPolicy,同时 will_retry 是 false,并且没有 model/rerouted 通知。

调用关系:它复用了 create_config_toml 来生成测试配置,使用假上游模拟错误响应,再调用 collect_cyber_policy_error_and_validate_no_reroute 专门等待 cyber policy 错误并排除错误的改路由通知。

调用图:调用 5 个内部函数(new, collect_cyber_policy_error_and_validate_no_reroute, create_config_toml, mount_response_once, start_mock_server);外部调用 10 个(default, new, new, Integer, to_response, assert_eq!, json!, skip_if_no_network!, timeout, vec!)。

response_model_field_mismatch_emits_model_rerouted_notification_v2_when_header_matches_requested172–243 ↗
async fn response_model_field_mismatch_emits_model_rerouted_notification_v2_when_header_matches_requested() -> Result<()>

作用:这个测试确认:即使 HTTP 响应头看起来是请求的模型,只要流式事件内部声明的实际模型不同,服务器也会发现并发出模型改路由通知。它防的是只看表面响应头、漏掉正文里真实模型信息的问题。

数据流:测试让假接口的响应头写请求模型,但在 response.created 事件里的 headers.OpenAI-Model 写服务器实际模型。随后创建测试配置和测试服务器,发起线程和一轮输入。读取完通知后,测试要求得到 ModelReroutedNotification,说明服务器能从流式正文中识别真实模型,并把 from_model 和 to_model 填对。

调用关系:它和第一个改路由测试很像,但改成检查流式事件里的模型字段。通知读取和“不能出现旧式 warning 项”的验证仍然交给 collect_turn_notifications_and_validate_no_warning_item。

调用图:调用 7 个内部函数(new, collect_turn_notifications_and_validate_no_warning_item, create_config_toml, mount_response_once, sse, sse_response, start_mock_server);外部调用 8 个(default, new, Integer, to_response, assert_eq!, skip_if_no_network!, timeout, vec!)。

model_verification_emits_typed_notification_and_warning_v2246–311 ↗
async fn model_verification_emits_typed_notification_and_warning_v2() -> Result<()>

作用:这个测试确认:如果上游返回模型验证元数据,服务器会发出结构化的 model/verification 通知。这里的验证表示模型具备某种受信能力,例如可信网络安全访问。

数据流:测试准备一段流式响应,其中包含 model verification metadata,并带上 trusted_access_for_cyber 这个标记。然后启动测试服务器、创建线程、发送输入。最后读取通知,期待得到 ModelVerificationNotification,里面的 verifications 被转换成 ModelVerification::TrustedAccessForCyber,并且不会出现 warning 或 model/rerouted。

调用关系:它用 responses::ev_model_verification_metadata 构造特殊事件,用 create_config_toml 配好假接口地址,再把消息收集和严格检查交给 collect_model_verification_notifications_and_validate_no_warning_item。

调用图:调用 7 个内部函数(new, collect_model_verification_notifications_and_validate_no_warning_item, create_config_toml, mount_response_once, sse, sse_response, start_mock_server);外部调用 8 个(default, new, Integer, to_response, assert_eq!, skip_if_no_network!, timeout, vec!)。

turn_moderation_metadata_emits_typed_notification_v2314–392 ↗
async fn turn_moderation_metadata_emits_typed_notification_v2() -> Result<()>

作用:这个测试确认:如果上游在一轮回答里返回审核元数据,服务器会把它作为 turn/moderationMetadata 通知发给客户端。这样客户端可以用结构化数据决定怎么展示,而不是从聊天文字里猜。

数据流:测试让假接口的流式响应中包含 response.metadata 事件,里面有 openai_chatgpt_moderation_metadata,并写着 presentation 是 inline。然后它创建配置、启动服务器、创建线程并发起输入。最后它等待 turn/moderationMetadata 通知,把 params 解析成 TurnModerationMetadataNotification,并检查线程、轮次和元数据内容完全符合预期。

调用关系:它和其他测试共享假上游、临时配置、线程启动和轮次启动流程,但没有使用专门的收集辅助函数,而是直接调用 read_stream_until_notification_message 等待指定方法名的通知。

调用图:调用 6 个内部函数(new, create_config_toml, mount_response_once, sse, sse_response, start_mock_server);外部调用 9 个(default, new, Integer, to_response, assert_eq!, from_value, skip_if_no_network!, timeout, vec!)。

collect_turn_notifications_and_validate_no_warning_item394–434 ↗
async fn collect_turn_notifications_and_validate_no_warning_item(
    mcp: &mut TestAppServer,
) -> Result<ModelReroutedNotification>

作用:这个辅助函数不断读取一轮对话里的通知,直到这一轮完成,并取出 model/rerouted 通知。它同时检查服务器没有把安全提示伪装成普通用户消息。

数据流:输入是一个正在运行的 TestAppServer。函数循环读取下一条 JSON-RPC 消息,只关心通知;遇到 model/rerouted 就解析并暂存,遇到 item/started 或 item/completed 就检查其中的 ThreadItem 不是以“Warning: ”开头的用户消息;遇到 turn/completed 时返回之前收集到的改路由通知。如果到结束还没看到改路由通知,就返回错误。

调用关系:它被两个模型不一致场景测试调用。测试主体负责制造场景,它负责像验票员一样逐条查看服务器吐出的通知,并用 is_warning_user_message_item 判断有没有不该出现的旧式警告消息。

调用图:调用 1 个内部函数(read_next_message);被 2 处调用(openai_model_header_mismatch_emits_model_rerouted_notification_v2, response_model_field_mismatch_emits_model_rerouted_notification_v2_when_header_matches_requested);外部调用 3 个(assert!, from_value, timeout)。

collect_model_verification_notifications_and_validate_no_warning_item436–485 ↗
async fn collect_model_verification_notifications_and_validate_no_warning_item(
    mcp: &mut TestAppServer,
) -> Result<ModelVerificationNotification>

作用:这个辅助函数专门等待 model/verification 通知,并确认这个场景里没有 warning、没有 model/rerouted,也没有旧式警告用户消息。它保证“模型验证”不会被混淆成其他安全事件。

数据流:输入是测试服务器连接。函数循环读取消息,看到 model/verification 就解析保存;看到 warning 或 model/rerouted 就立刻报错;看到 item/started 或 item/completed 就检查项目不是 Warning 用户消息;看到 turn/completed 时,如果已经拿到验证通知就返回,否则报错。

调用关系:它只被 model_verification_emits_typed_notification_and_warning_v2 调用。上层测试制造包含验证元数据的上游响应,它负责确认服务器输出的通知种类和顺序符合 v2 协议期望。

调用图:调用 1 个内部函数(read_next_message);被 1 处调用(model_verification_emits_typed_notification_and_warning_v2);外部调用 4 个(bail!, assert!, from_value, timeout)。

collect_cyber_policy_error_and_validate_no_reroute487–518 ↗
async fn collect_cyber_policy_error_and_validate_no_reroute(
    mcp: &mut TestAppServer,
) -> Result<ErrorNotification>

作用:这个辅助函数等待网络安全策略错误通知,并确保服务器没有错误地发出模型改路由通知。它用来区分“请求被安全策略拒绝”和“模型被换了”这两种完全不同的情况。

数据流:输入是测试服务器连接。函数不断读取通知;遇到 error 时解析成 ErrorNotification,如果其中 codex_error_info 是 CyberPolicy 就保存;遇到 model/rerouted 就直接报错;遇到 turn/completed 时返回已保存的 cyber policy 错误。如果一轮结束前没收到该错误,也返回错误。

调用关系:它被 cyber_policy_response_emits_typed_error_notification_v2 调用。场景测试负责让假上游返回 cyber_policy,函数负责在服务器输出流中验证它被正确翻译成错误通知。

调用图:调用 1 个内部函数(read_next_message);被 1 处调用(cyber_policy_response_emits_typed_error_notification_v2);外部调用 3 个(bail!, from_value, timeout)。

warning_text_from_item520–529 ↗
fn warning_text_from_item(item: &ThreadItem) -> Option<&str>

作用:这个小函数检查一个线程项目是不是用户消息,并从里面找出以“Warning: ”开头的文本。它帮助测试发现旧版那种把警告塞进聊天内容里的行为。

数据流:输入是一个 ThreadItem。函数先看它是不是 UserMessage;如果不是,就返回 None。若是用户消息,就遍历其中的输入内容,找到第一段以“Warning: ”开头的文本并返回这段文字的引用;如果没有,也返回 None。

调用关系:它不直接被测试主流程调用,而是被 is_warning_user_message_item 包了一层。真正的通知收集函数通过后者来判断 item/started 和 item/completed 里有没有不该出现的警告消息。

调用图:被 1 处调用(is_warning_user_message_item)。

is_warning_user_message_item531–533 ↗
fn is_warning_user_message_item(item: &ThreadItem) -> bool

作用:这个函数把“有没有 Warning 用户消息”简化成一个 true/false 判断。测试代码用它来写清楚:这些 v2 通知场景不应该生成旧式警告聊天项。

数据流:输入是一个 ThreadItem。它调用 warning_text_from_item 尝试找警告文本;如果找到了就返回 true,没找到就返回 false。它不修改任何数据。

调用关系:它服务于两个收集辅助函数:collect_turn_notifications_and_validate_no_warning_item 和 collect_model_verification_notifications_and_validate_no_warning_item。辅助函数在检查 item 通知时用它快速判断是否违规。

调用图:调用 1 个内部函数(warning_text_from_item)。

create_config_toml535–560 ↗
fn create_config_toml(codex_home: &std::path::Path, server_uri: &str) -> std::io::Result<()>

作用:这个函数为每个测试写一份临时 config.toml,让 app-server 去连接假接口,而不是连接真实服务。没有它,测试就很难稳定复现这些安全场景。

数据流:输入是临时的 codex_home 路径和假服务器地址。函数拼出 config.toml 文件路径,把模型名、审批策略、沙箱模式、功能开关、mock provider 的 base_url 和重试次数等内容写进去。输出是写文件是否成功;成功后,临时目录里多了一份可被测试服务器读取的配置文件。

调用关系:它被所有五个测试场景调用。每个测试先启动假上游拿到地址,再调用它写配置,之后 TestAppServer::new 才能按这份配置启动并把请求发到假上游。

调用图:被 5 处调用(cyber_policy_response_emits_typed_error_notification_v2, model_verification_emits_typed_notification_and_warning_v2, openai_model_header_mismatch_emits_model_rerouted_notification_v2, response_model_field_mismatch_emits_model_rerouted_notification_v2_when_header_matches_requested, turn_moderation_metadata_emits_typed_notification_v2);外部调用 3 个(join, format!, write)。