App-server 集成套件 — 认证、配置、发现和核心 RPC 表面
这一阶段像给 app-server 做一轮“总验收”,重点不在具体业务,而在客户端刚连上服务器后能不能安全、正确地使用基础能力。认证和账号测试先守住登录、退出、令牌刷新和限额入口;初始化、严格配置、配置读写和实验开关测试确保启动时读到的设置不乱。模型、权限、协作模式等列表接口负责告诉客户端“有哪些可选项”。文件、进程、Windows 沙箱和远程控制测试则检查服务器能不能按规矩操作本机资源,并在出错或受限制时及时拦住。
认证和账户流程
这些套件覆盖端到端认证入口、账户会话管理,以及应用服务器公开的账户关联限速操作。
app-server/tests/suite/auth.rs源码 ↗
这个测试文件像一套“门禁检查清单”。它会临时造一个干净的 Codex 配置目录,启动测试版 app-server,然后通过 JSON-RPC(一种用 JSON 发请求、收回复的通信格式)问服务器:现在是谁登录了?能不能返回 token?有些测试先用 API key 登录,有些假装有 ChatGPT token,有些用假的网络服务器模拟刷新 token 成功或失败。重点不只是看能不能登录,还要看敏感信息什么时候必须隐藏:比如个人访问令牌不能直接返回,刷新失败后也不能把旧 access token 交出去。文件里几个小 helper 负责写测试配置和完成 API key 登录,后面的测试函数则覆盖不同场景,确保认证规则、配置开关、强制登录方式和 token 刷新失败后的行为都安全可靠。
create_config_toml_custom_provider32–63 ↗
fn create_config_toml_custom_provider(
codex_home: &Path,
requires_openai_auth: bool,
) -> std::io::Result<()>
作用:给测试用的临时目录写一份自定义模型供应商配置。它特别用来测试“这个供应商需不需要 OpenAI 登录”这个开关会不会影响鉴权状态。
数据流:输入是一个临时的 codex_home 路径,以及 requires_openai_auth 这个真假开关 → 它拼出 config.toml 文件路径,把模型名、沙箱设置、供应商地址和可选的 requires_openai_auth 行写成 TOML 配置文本 → 输出是写文件的成功或失败结果,同时磁盘上多了一份测试配置。
调用关系:它被 get_auth_status_with_api_key_when_auth_not_required 调用。那个测试先靠它造出“供应商不要求 OpenAI 登录”的环境,再启动测试服务器,确认即使输入了 API key,状态接口也不会把它当作需要展示的认证方式。
调用图:被 1 处调用(get_auth_status_with_api_key_when_auth_not_required);外部调用 3 个(join, format!, write)。
create_config_toml65–78 ↗
fn create_config_toml(codex_home: &Path) -> std::io::Result<()>
作用:写一份最基础的测试配置文件。多数认证测试都先用它准备一个稳定、简单、不受真实用户配置影响的运行环境。
数据流:输入是测试临时目录路径 → 它在这个目录下找到或创建 config.toml,并写入固定模型、永不审批、关闭 shell_snapshot 等测试设置 → 输出是文件写入结果,之后服务器启动时会读取这份配置。
调用关系:它是这个文件里最常用的准备步骤,被没有登录、API key、个人访问令牌、ChatGPT token 刷新失败和恢复等多个测试调用。测试函数先让它铺好“干净房间”,再启动 TestAppServer 做真正的认证检查。
调用图:被 8 处调用(get_auth_status_no_auth, get_auth_status_omits_token_after_permanent_refresh_failure, get_auth_status_omits_token_after_proactive_refresh_failure, get_auth_status_returns_token_after_proactive_refresh_recovery, get_auth_status_with_api_key, get_auth_status_with_api_key_no_include_token, get_auth_status_with_api_key_refresh_requested, get_auth_status_with_personal_access_token_omits_token);外部调用 2 个(join, write)。
create_config_toml_forced_login80–94 ↗
fn create_config_toml_forced_login(codex_home: &Path, forced_method: &str) -> std::io::Result<()>
作用:写一份带“强制登录方式”的测试配置。它用来确认如果系统规定只能用某种方式登录,另一种登录方式会被拒绝。
数据流:输入是临时目录路径和 forced_method 字符串,比如 chatgpt → 它把 forced_login_method 写进 config.toml → 输出是写文件结果,并改变后续服务器启动时采用的登录规则。
调用关系:它只服务于 login_api_key_rejected_when_forced_chatgpt。那个测试用它把登录方式锁成 ChatGPT,然后再尝试 API key 登录,检查服务器是否返回明确的拒绝错误。
调用图:被 1 处调用(login_api_key_rejected_when_forced_chatgpt);外部调用 3 个(join, format!, write)。
login_with_api_key_via_request96–107 ↗
async fn login_with_api_key_via_request(mcp: &mut TestAppServer, api_key: &str) -> Result<()>
作用:帮测试通过 app-server 的正式请求通道完成一次 API key 登录。这样后续测试不用重复写发送请求、等回复、检查结果这套步骤。
数据流:输入是一个正在运行的 TestAppServer 和要登录的 api_key → 它发送“用 API key 登录”的请求,等待对应请求编号的 JSON-RPC 回复,把通用回复转成 LoginAccountResponse,并确认结果确实是 ApiKey 登录 → 输出是成功或错误;成功后测试服务器内部已经记住这个 API key 登录状态。
调用关系:它被多个 API key 场景测试调用。上层测试负责先启动服务器,它负责把“登录”这一步做完;它内部把活交给 TestAppServer 的发送和读取方法,再用 to_response 把服务器回复拆成测试能比较的结构。
调用图:调用 2 个内部函数(read_stream_until_response_message, send_login_account_api_key_request);被 4 处调用(get_auth_status_with_api_key, get_auth_status_with_api_key_no_include_token, get_auth_status_with_api_key_refresh_requested, get_auth_status_with_api_key_when_auth_not_required);外部调用 4 个(Integer, to_response, assert_eq!, timeout)。
get_auth_status_no_auth110–134 ↗
async fn get_auth_status_no_auth() -> Result<()>
作用:测试完全没有认证信息时,服务器应该老实回答“没有登录”。这能防止环境变量或默认配置意外让测试环境看起来像已登录。
数据流:它创建临时目录并写基础配置,同时明确清掉 OPENAI_API_KEY → 启动并初始化 TestAppServer,然后发送 get_auth_status 请求,要求包含 token 但不刷新 token → 读回结果后确认 auth_method 和 auth_token 都是空。
调用关系:这是由 Tokio 异步测试运行器直接执行的测试。它先调用 create_config_toml 准备配置,再通过 TestAppServer 发请求和读回复,最后用断言检查服务器在无凭据场景下的表现。
调用图:调用 2 个内部函数(new_with_env, create_config_toml);外部调用 5 个(new, Integer, to_response, assert_eq!, timeout)。
get_auth_status_with_api_key137–162 ↗
async fn get_auth_status_with_api_key() -> Result<()>
作用:测试用户用 API key 登录后,鉴权状态接口能正确报告登录方式,并在被允许时返回这个 key。
数据流:它创建临时配置并启动服务器 → 调用 login_with_api_key_via_request 写入 sk-test-key 登录状态 → 发送 get_auth_status,并设置 include_token 为 true、refresh_token 为 false → 结果应显示 AuthMode::ApiKey,token 应该等于 sk-test-key。
调用关系:测试运行器执行它时,它把配置准备交给 create_config_toml,把登录步骤交给 login_with_api_key_via_request,自己负责询问状态接口并验证最终回复。
调用图:调用 3 个内部函数(new, create_config_toml, login_with_api_key_via_request);外部调用 5 个(new, Integer, to_response, assert_eq!, timeout)。
get_auth_status_with_personal_access_token_omits_token165–220 ↗
async fn get_auth_status_with_personal_access_token_omits_token() -> Result<()>
作用:测试使用个人访问令牌时,服务器能识别这种登录方式,但不会把令牌原文返回给调用方。个人访问令牌是能代表用户访问服务的秘密,泄露风险很高。
数据流:它先写基础配置,再启动一个假的 HTTP 服务器模拟 whoami 接口,并要求请求里带 Bearer at-test-token → 启动 TestAppServer 时放入 CODEX_ACCESS_TOKEN 和认证 API 地址 → 查询 auth status 后,期望返回 PersonalAccessToken,但 auth_token 必须是空,同时标记需要 OpenAI 认证 → 最后验证假的接口确实被调用过。
调用关系:它由测试运行器触发。它除了使用 create_config_toml 和 TestAppServer,还用 wiremock 搭了一个假外部认证服务;服务器启动和状态查询过程中会访问这个假服务来确认 token 身份。
调用图:调用 2 个内部函数(new_with_env, create_config_toml);外部调用 12 个(given, start, new, new, Integer, to_response, assert_eq!, json!, timeout, header (+2 more))。
get_auth_status_with_api_key_when_auth_not_required223–253 ↗
async fn get_auth_status_with_api_key_when_auth_not_required() -> Result<()>
作用:测试当当前模型供应商明确“不需要 OpenAI 登录”时,即使用户提交了 API key,状态接口也不应该把它当成当前需要的认证状态返回。
数据流:它用 create_config_toml_custom_provider 写入一个 requires_openai_auth 为 false 的供应商配置 → 启动服务器并通过 API key 登录 → 查询 auth status,要求包含 token → 返回结果应没有 auth_method、没有 auth_token,并明确 requires_openai_auth 是 false。
调用关系:这个测试依赖 create_config_toml_custom_provider 制造特殊配置,也依赖 login_with_api_key_via_request 完成登录。它检查的是配置层面的开关会不会正确压过已有登录信息。
调用图:调用 3 个内部函数(new, create_config_toml_custom_provider, login_with_api_key_via_request);外部调用 5 个(new, Integer, to_response, assert_eq!, timeout)。
get_auth_status_with_api_key_no_include_token256–281 ↗
async fn get_auth_status_with_api_key_no_include_token() -> Result<()>
作用:测试如果请求里没有说“请返回 token”,服务器就算知道用户用 API key 登录了,也不能把密钥返回出去。
数据流:它写基础配置、启动服务器、用 sk-test-key 登录 → 构造 get_auth_status 参数时让 include_token 为 None,也就是线上 JSON 里不带这个字段 → 收到回复后,认证方式应是 ApiKey,但 auth_token 必须为空。
调用关系:它复用 create_config_toml 和 login_with_api_key_via_request 做准备。真正要验证的是状态接口的默认安全行为:没明确要秘密,就不要给秘密。
调用图:调用 3 个内部函数(new, create_config_toml, login_with_api_key_via_request);外部调用 6 个(new, Integer, to_response, assert!, assert_eq!, timeout)。
get_auth_status_with_api_key_refresh_requested284–315 ↗
async fn get_auth_status_with_api_key_refresh_requested() -> Result<()>
作用:测试 API key 登录时,即使请求要求刷新 token,也不会破坏正常结果。API key 本身不像 OAuth token 那样需要刷新,所以这里主要确认服务器能平稳处理这个参数。
数据流:它创建配置并启动服务器 → 登录 API key → 查询 auth status 时同时设置 include_token 为 true、refresh_token 为 true → 返回应仍然是 ApiKey,token 是 sk-test-key,并显示 requires_openai_auth 为 true。
调用关系:它通过 create_config_toml 和 login_with_api_key_via_request 搭好 API key 场景,然后把 refresh_token 这个额外开关交给服务器处理,最后确认结果没有被错误刷新逻辑影响。
调用图:调用 3 个内部函数(new, create_config_toml, login_with_api_key_via_request);外部调用 5 个(new, Integer, to_response, assert_eq!, timeout)。
get_auth_status_omits_token_after_permanent_refresh_failure318–396 ↗
async fn get_auth_status_omits_token_after_permanent_refresh_failure() -> Result<()>
作用:测试 ChatGPT 登录的刷新 token 永久失败后,服务器仍能说用户是 ChatGPT 登录,但不能再返回旧 access token。这样可以避免已经不可信的旧令牌继续被使用。
数据流:它先写基础配置,并在文件里放入一组过期的 ChatGPT 凭据:旧 access token、旧 refresh token、账号信息 → 启动假的刷新接口,让它对 /oauth/token 返回 401 和 refresh_token_reused 错误 → 启动服务器并请求 auth status,要求刷新且返回 token → 结果应是 Chatgpt,但 auth_token 为空;第二次再问也应得到同样结果,并且假服务器只按预期收到一次刷新请求。
调用关系:它由测试运行器执行,准备配置靠 create_config_toml,准备凭据靠 write_chatgpt_auth,外部刷新服务靠 wiremock 模拟。它重点验证服务器遇到“永久失败”后会记住这个失败状态,不反复泄露或重复刷新。
调用图:调用 3 个内部函数(new, new_with_env, create_config_toml);外部调用 13 个(given, start, new, new, Integer, to_response, write_chatgpt_auth, assert_eq!, format!, json! (+3 more))。
get_auth_status_omits_token_after_proactive_refresh_failure399–463 ↗
async fn get_auth_status_omits_token_after_proactive_refresh_failure() -> Result<()>
作用:测试即使调用方没有主动要求刷新,服务器发现 ChatGPT token 太久没刷新而主动刷新失败时,也不能返回旧 token。
数据流:它写入基础配置和一份 ChatGPT 凭据,并把 last_refresh 设置成 9 天前,表示该主动刷新了 → 假刷新接口返回 401 refresh_token_reused → 启动服务器后发送 get_auth_status,include_token 为 true 但 refresh_token 为 false → 因为服务器主动刷新失败,结果应显示 Chatgpt,但 auth_token 为空。
调用关系:这个测试把时间信息交给 ChatGptAuthFixture,把失败的网络刷新交给 wiremock 模拟。它检查的是服务器内部的“主动刷新”路径,而不是用户显式要求刷新的路径。
调用图:调用 3 个内部函数(new, new_with_env, create_config_toml);外部调用 15 个(days, given, start, new, new, now, Integer, to_response, write_chatgpt_auth, assert_eq! (+5 more))。
get_auth_status_returns_token_after_proactive_refresh_recovery466–563 ↗
async fn get_auth_status_returns_token_after_proactive_refresh_recovery() -> Result<()>
作用:测试一次刷新失败后,如果磁盘上的 ChatGPT 凭据后来被新的有效 token 替换,服务器能恢复并重新返回可用 token。它防止系统被一次失败永久卡死。
数据流:它先写入一份 9 天前刷新的旧 ChatGPT 凭据,并让假刷新接口返回失败 → 第一次请求 auth status 且要求刷新,结果应没有 token → 然后它直接把凭据文件改成 recovered-access-token 和新的刷新时间 → 第二次查询不要求刷新,服务器应读到新凭据并返回 recovered-access-token。
调用关系:它串起了 create_config_toml、write_chatgpt_auth、wiremock 和 TestAppServer。前半段验证失败会隐藏 token,后半段验证凭据恢复后服务器不会继续沿用失败状态,而是重新接受文件里的新登录信息。
调用图:调用 3 个内部函数(new, new_with_env, create_config_toml);外部调用 15 个(days, given, start, new, new, now, Integer, to_response, write_chatgpt_auth, assert_eq! (+5 more))。
login_api_key_rejected_when_forced_chatgpt566–588 ↗
async fn login_api_key_rejected_when_forced_chatgpt() -> Result<()>
作用:测试配置强制使用 ChatGPT 登录时,API key 登录会被拒绝,并给出清楚的错误提示。
数据流:它创建临时目录,用 create_config_toml_forced_login 写入 forced_login_method = chatgpt → 启动并初始化服务器 → 发送 API key 登录请求 → 读取 JSON-RPC 错误回复,确认错误消息是“API key login is disabled. Use ChatGPT login instead.”
调用关系:这是测试运行器直接执行的负面测试。它先用配置 helper 锁住登录方式,再通过 TestAppServer 尝试违规登录,最后检查服务器是否把错误通过 JSON-RPC 错误通道返回。
调用图:调用 2 个内部函数(new, create_config_toml_forced_login);外部调用 4 个(new, Integer, assert_eq!, timeout)。
app-server/tests/suite/v2/account.rs源码 ↗
这个文件不是真正给用户运行的功能代码,而是专门用来“试机器”的测试。它会临时建一个假的 Codex 主目录,写入最小配置文件,再启动测试版 app server,通过 JSON-RPC(一种用 JSON 发请求和收回复的通信格式)模拟客户端操作。它覆盖 API Key 登录、ChatGPT 浏览器登录、设备码登录、外部传入 ChatGPT token、Amazon Bedrock 账号识别、退出登录、查询账号状态等场景。遇到需要联网的地方,它用 wiremock 搭一个假服务器,提前规定“这个接口返回成功”或“返回 401 未授权”。这样测试既快又稳定。重要的是,这些测试不只看请求是否成功,还检查 auth.json 是否创建或删除、账号更新通知是否发出、失败时是否不会误报成功。它保护的是登录系统的边界行为:没有这些测试,改动登录流程时很容易把取消登录、刷新失败、工作区限制等细节弄坏。
create_config_toml81–142 ↗
fn create_config_toml(codex_home: &Path, params: CreateConfigTomlParams) -> std::io::Result<()>
作用:给每个测试临时写一份最小可用的 config.toml 配置文件。这样测试不用依赖用户机器上的真实配置,也能按需要打开或关闭某些登录限制。
数据流:输入是临时目录路径和一组可选参数,比如强制登录方式、允许的工作区、模型服务地址。它把这些参数拼成 TOML 配置文本,写到临时目录里的 config.toml。结果是测试服务器启动时会读到这份专门为当前测试准备的配置。
调用关系:很多测试一开始都会调用它来搭舞台。它只负责写配置,后面的 TestAppServer 会读取这份配置并表现出不同的登录规则。
调用图:被 27 处调用(account_read_refresh_token_is_noop_in_external_mode, external_auth_refresh_error_fails_turn, external_auth_refresh_invalid_access_token_fails_turn, external_auth_refresh_mismatched_workspace_fails_turn, external_auth_refreshes_on_unauthorized, get_account_no_auth, get_account_omits_chatgpt_after_permanent_refresh_failure, get_account_when_auth_not_required, get_account_with_api_key, get_account_with_aws_provider (+15 more));外部调用 4 个(join, new, format!, write)。
mock_device_code_usercode144–154 ↗
async fn mock_device_code_usercode(server: &MockServer, interval_seconds: u64)
作用:模拟 ChatGPT 设备码登录的第一步成功:服务器给出用户要输入的短码。设备码登录可以理解为“电脑显示一个码,让你去网页上输入”。
数据流:输入是假 HTTP 服务器和轮询间隔秒数。它在假服务器上注册一个 POST /api/accounts/deviceauth/usercode 响应,返回 device_auth_id、user_code 和 interval。结果是测试里的 app server 会以为真实登录服务已经给了一个设备码。
调用关系:设备码成功、失败后通知、可取消这几类测试会先用它。它只模拟第一步,后续 token 成功或失败由其他 mock 函数接着安排。
调用图:被 3 处调用(login_account_chatgpt_device_code_can_be_cancelled, login_account_chatgpt_device_code_failure_notifies_without_account_update, login_account_chatgpt_device_code_succeeds_and_notifies);外部调用 5 个(given, new, json!, method, path)。
mock_device_code_usercode_failure156–162 ↗
async fn mock_device_code_usercode_failure(server: &MockServer, status: u16)
作用:模拟设备码登录一开始就失败,比如服务端说这个功能没开。用它验证 app server 不会在启动失败时误发“登录完成”。
数据流:输入是假 HTTP 服务器和要返回的 HTTP 状态码。它注册 usercode 接口,让这个接口只返回指定错误状态。输出是一个会失败的假登录服务环境。
调用关系:login_account_chatgpt_device_code_returns_error_when_disabled 会调用它,专门检查设备码入口失败时的错误消息、没有通知、也不会写 auth.json。
调用图:被 1 处调用(login_account_chatgpt_device_code_returns_error_when_disabled);外部调用 4 个(given, new, method, path)。
mock_device_code_token_success164–174 ↗
async fn mock_device_code_token_success(server: &MockServer)
作用:模拟设备码登录轮询成功:用户已经在网页上输入了码,登录服务返回继续换 token 所需的授权信息。
数据流:输入是假 HTTP 服务器。它注册 POST /api/accounts/deviceauth/token,返回 authorization_code、code_challenge 和 code_verifier。结果是 app server 可以进入下一步,用这些信息去换真正的 OAuth token。
调用关系:它服务于设备码登录成功测试,通常接在 mock_device_code_usercode 后面,再由 mock_device_code_oauth_token 提供最终 token。
调用图:被 1 处调用(login_account_chatgpt_device_code_succeeds_and_notifies);外部调用 5 个(given, new, json!, method, path)。
mock_device_code_token_failure176–182 ↗
async fn mock_device_code_token_failure(server: &MockServer, status: u16)
作用:模拟设备码登录轮询失败。用来确认用户没完成登录、服务报错或测试取消时,app server 不会错误地写入账号。
数据流:输入是假 HTTP 服务器和 HTTP 状态码。它让 /api/accounts/deviceauth/token 返回这个错误状态。结果是登录流程在轮询阶段失败或一直不可用。
调用关系:设备码失败通知测试和设备码取消测试会调用它。它配合 mock_device_code_usercode,让测试能走到“已经拿到用户码,但后面没成功”的阶段。
调用图:被 2 处调用(login_account_chatgpt_device_code_can_be_cancelled, login_account_chatgpt_device_code_failure_notifies_without_account_update);外部调用 4 个(given, new, method, path)。
mock_device_code_oauth_token184–194 ↗
async fn mock_device_code_oauth_token(server: &MockServer, id_token: &str)
作用:模拟 OAuth token 接口成功返回登录凭证。OAuth 可以简单理解为“第三方登录换票据”的流程。
数据流:输入是假 HTTP 服务器和一个 id_token。它注册 POST /oauth/token,返回 id_token、access_token 和 refresh_token。结果是 app server 可以保存 ChatGPT 登录信息并识别邮箱、套餐等账号资料。
调用关系:设备码登录成功测试会在 usercode 和 token 轮询都安排好之后调用它,完成整条假登录链路。
调用图:被 1 处调用(login_account_chatgpt_device_code_succeeds_and_notifies);外部调用 5 个(given, new, json!, method, path)。
logout_account_removes_auth_and_notifies197–254 ↗
async fn logout_account_removes_auth_and_notifies() -> Result<()>
作用:测试退出账号时,服务器会删掉本地登录文件,并通知客户端账号已经变成未登录。
数据流:先在临时目录写配置并预先保存一个 API Key 登录文件。然后启动测试服务器,发送退出请求。最后读取响应、账号更新通知、磁盘文件和 get account 结果,确认 auth.json 被删除,账号为空。
调用关系:这是测试运行器直接执行的测试。它用 create_config_toml 准备配置,用 login_with_api_key 制造已登录状态,再通过 TestAppServer 发请求和收通知。
调用图:调用 3 个内部函数(new_with_env, create_config_toml, default);外部调用 9 个(new, Integer, default, to_response, assert!, assert_eq!, bail!, login_with_api_key, timeout)。
set_auth_token_updates_account_and_notifies257–331 ↗
async fn set_auth_token_updates_account_and_notifies() -> Result<()>
作用:测试客户端直接传入 ChatGPT token 后,服务器会立刻更新账号信息并发出通知。这类模式用于外部宿主已经帮用户登录好的场景。
数据流:它创建配置、模型缓存和一个假的 id_token,token 里带邮箱、套餐和工作区。然后发送 chatgpt auth tokens 登录请求。返回应为成功,通知里应显示 ChatgptAuthTokens,get account 应能读到对应邮箱和 Pro 套餐。
调用关系:测试运行器执行它。它依赖 create_config_toml、encode_id_token 和 TestAppServer,验证外部 token 登录入口和账号查询入口能连起来。
调用图:调用 3 个内部函数(new, new_with_env, create_config_toml);外部调用 11 个(default, start, new, Integer, encode_id_token, to_response, write_models_cache, assert_eq!, bail!, format! (+1 more))。
account_read_refresh_token_is_noop_in_external_mode334–409 ↗
async fn account_read_refresh_token_is_noop_in_external_mode() -> Result<()>
作用:测试外部 token 登录模式下,即使查询账号时要求刷新 token,服务器也不会主动向客户端请求刷新。因为外部模式的 token 生命周期由外部客户端负责。
数据流:它先用外部 ChatGPT token 登录,再发送 get account 请求,并把 refresh_token 设为 true。结果仍然返回当前账号;随后短时间等待,确认没有出现 account/chatgptAuthTokens/refresh 请求。
调用关系:测试运行器执行它。它和 set_auth_token_updates_account_and_notifies 类似,但重点检查“不会多做刷新”这个边界行为。
调用图:调用 3 个内部函数(new, new_with_env, create_config_toml);外部调用 10 个(default, from_millis, new, Integer, encode_id_token, to_response, write_models_cache, assert!, assert_eq!, timeout)。
respond_to_refresh_request411–434 ↗
async fn respond_to_refresh_request(
mcp: &mut TestAppServer,
access_token: &str,
chatgpt_account_id: &str,
chatgpt_plan_type: Option<&str>,
) -> Result<()>
作用:帮测试回复服务器发来的“请刷新 ChatGPT token”请求。它让刷新成功场景不用重复写同一段收请求、检查原因、发响应的代码。
数据流:输入是测试服务器连接、要返回的新 access token、工作区 ID 和可选套餐。它读取下一条服务器请求,确认这是因为 Unauthorized 未授权触发的刷新请求,然后把新 token 信息作为 JSON-RPC 响应发回去。
调用关系:external_auth_refreshes_on_unauthorized 会调用它。它处在模型请求收到 401 之后、服务器重试之前,扮演客户端补发新 token 的角色。
调用图:调用 2 个内部函数(read_stream_until_request_message, send_response);被 1 处调用(external_auth_refreshes_on_unauthorized);外部调用 4 个(assert_eq!, bail!, to_value, timeout)。
external_auth_refreshes_on_unauthorized438–556 ↗
async fn external_auth_refreshes_on_unauthorized() -> Result<()>
作用:测试外部 ChatGPT token 调用模型时如果遇到 401 未授权,服务器会向客户端要新 token,然后用新 token 重试成功。
数据流:它让假模型服务第一次返回 401,第二次返回正常流式结果。先用旧 token 登录,再启动线程和一轮对话。服务器发出刷新请求后,测试用 respond_to_refresh_request 回新 token。最后检查假服务收到两次请求,第一次带旧 token,第二次带新 token。
调用关系:测试运行器执行它。它把账号登录、模型请求、401 处理、客户端刷新、请求重试这几段流程串成一个完整故事。
调用图:调用 6 个内部函数(new, new_with_env, create_config_toml, respond_to_refresh_request, mount_response_sequence, sse);外部调用 13 个(default, start, new, new, Integer, encode_id_token, to_response, write_models_cache, assert_eq!, format! (+3 more))。
external_auth_refresh_error_fails_turn560–673 ↗
async fn external_auth_refresh_error_fails_turn() -> Result<()>
作用:测试 token 刷新请求如果被客户端明确返回错误,那么当前对话轮次会失败,而不是继续用坏 token 硬跑。
数据流:假模型服务返回 401。测试启动对话后读到服务器的刷新请求,但故意发送 JSON-RPC 错误。之后读取 turn/completed 通知,确认这一轮状态是 Failed,并且带有错误信息。
调用关系:测试运行器执行它。它覆盖 external_auth_refreshes_on_unauthorized 的反面分支:服务器把刷新工作交给客户端,但客户端说失败时,服务器必须把对话标成失败。
调用图:调用 4 个内部函数(new, new_with_env, create_config_toml, mount_response_sequence);外部调用 16 个(default, start, new, new, Integer, encode_id_token, to_response, write_models_cache, assert!, assert_eq! (+6 more))。
external_auth_refresh_mismatched_workspace_fails_turn677–797 ↗
async fn external_auth_refresh_mismatched_workspace_fails_turn() -> Result<()>
作用:测试刷新回来的 token 如果属于不允许的工作区,对话必须失败。这样可以防止用户被悄悄切到错误的组织或工作区。
数据流:配置里只允许一个工作区。测试先用允许的工作区 token 登录,模型请求收到 401 后,刷新响应却返回另一个不允许的工作区。服务器收到后拒绝继续,最终 turn/completed 显示失败。
调用关系:测试运行器执行它。它在刷新链路中额外检查工作区限制,和 create_config_toml 写入的 forced workspace 配置配合。
调用图:调用 4 个内部函数(new, new_with_env, create_config_toml, mount_response_sequence);外部调用 17 个(default, start, new, new, Integer, encode_id_token, to_response, write_models_cache, assert!, assert_eq! (+7 more))。
external_auth_refresh_invalid_access_token_fails_turn801–914 ↗
async fn external_auth_refresh_invalid_access_token_fails_turn() -> Result<()>
作用:测试刷新返回的 access token 如果格式不对,对话会失败。access token 在这里像身份证,连身份证格式都不对就不能放行。
数据流:它设置模型服务返回 401,随后在刷新响应里给一个 not-a-jwt 的坏 token。服务器尝试解析失败,最终对话完成通知里的状态是 Failed,并带错误。
调用关系:测试运行器执行它。它验证刷新流程不只相信客户端给的字符串,还会检查 token 是否真能被识别。
调用图:调用 4 个内部函数(new, new_with_env, create_config_toml, mount_response_sequence);外部调用 17 个(default, start, new, new, Integer, encode_id_token, to_response, write_models_cache, assert!, assert_eq! (+7 more))。
login_account_api_key_succeeds_and_notifies917–962 ↗
async fn login_account_api_key_succeeds_and_notifies() -> Result<()>
作用:测试 API Key 登录成功时,服务器会返回成功、保存 auth.json,并发出登录完成和账号更新两类通知。
数据流:它创建配置并启动服务器,发送 API Key 登录请求。之后读取登录响应、account/login/completed 通知、account/updated 通知,并检查本地 auth.json 是否存在。
调用关系:测试运行器执行它。它验证最基本的 API Key 登录路径,是后面强制登录方式测试的对照组。
调用图:调用 2 个内部函数(new, create_config_toml);外部调用 9 个(new, Integer, default, to_response, assert!, assert_eq!, bail!, assert_eq!, timeout)。
login_account_api_key_rejected_when_forced_chatgpt965–992 ↗
async fn login_account_api_key_rejected_when_forced_chatgpt() -> Result<()>
作用:测试配置强制只能用 ChatGPT 登录时,API Key 登录会被拒绝,并给出明确提示。
数据流:它写入 forced_login_method = chatgpt 的配置,启动服务器后发送 API Key 登录请求。输出不是成功响应,而是 JSON-RPC 错误,错误文本要求改用 ChatGPT 登录。
调用关系:测试运行器执行它。它依靠 create_config_toml 设置限制,检查服务器是否尊重管理员或产品配置。
调用图:调用 2 个内部函数(new, create_config_toml);外部调用 5 个(default, new, Integer, assert_eq!, timeout)。
login_account_chatgpt_rejected_when_forced_api995–1020 ↗
async fn login_account_chatgpt_rejected_when_forced_api() -> Result<()>
作用:测试配置强制只能用 API Key 登录时,ChatGPT 登录会被拒绝,并给出明确提示。
数据流:它写入 forced_login_method = api 的配置,启动服务器后发起 ChatGPT 登录。结果应收到 JSON-RPC 错误,提示 ChatGPT 登录被禁用,应使用 API Key。
调用关系:测试运行器执行它。它和 login_account_api_key_rejected_when_forced_chatgpt 是一对,分别检查两种强制登录方向。
调用图:调用 2 个内部函数(new, create_config_toml);外部调用 5 个(default, new, Integer, assert_eq!, timeout)。
login_account_chatgpt_device_code_returns_error_when_disabled1023–1076 ↗
async fn login_account_chatgpt_device_code_returns_error_when_disabled() -> Result<()>
作用:测试设备码登录入口不可用时,服务器会直接返回错误,不会误发登录完成通知,也不会写入登录文件。
数据流:它让假登录服务的 usercode 接口返回 404,启动服务器并发起设备码登录。结果读取到错误消息,短暂等待后确认没有 account/login/completed 通知,也没有 auth.json。
调用关系:测试运行器执行它。它调用 mock_device_code_usercode_failure 来制造“功能没开”的登录服务。
调用图:调用 3 个内部函数(new_with_env, create_config_toml, mock_device_code_usercode_failure);外部调用 9 个(default, from_millis, start, new, Integer, write_models_cache, assert!, format!, timeout)。
login_account_chatgpt_device_code_succeeds_and_notifies1079–1160 ↗
async fn login_account_chatgpt_device_code_succeeds_and_notifies() -> Result<()>
作用:测试设备码登录完整成功路径:返回用户码,后台轮询拿到授权,换到 token,保存账号并通知客户端。
数据流:它依次设置 usercode 成功、轮询 token 成功、OAuth token 成功。发起设备码登录后,先收到包含验证网址和用户码的响应,再收到登录成功通知和账号更新通知,最后确认 auth.json 被创建。
调用关系:测试运行器执行它。它把 mock_device_code_usercode、mock_device_code_token_success 和 mock_device_code_oauth_token 串起来,模拟真实设备码登录全过程。
调用图:调用 6 个内部函数(new, new_with_env, create_config_toml, mock_device_code_oauth_token, mock_device_code_token_success, mock_device_code_usercode);外部调用 12 个(default, start, new, Integer, encode_id_token, to_response, write_models_cache, assert!, assert_eq!, bail! (+2 more))。
login_account_chatgpt_device_code_failure_notifies_without_account_update1163–1235 ↗
async fn login_account_chatgpt_device_code_failure_notifies_without_account_update() -> Result<()>
作用:测试设备码登录已经开始但后续失败时,只发登录失败通知,不发账号更新,也不保存账号。
数据流:它让 usercode 成功返回用户码,但让轮询 token 接口返回 500。登录请求本身先返回 login_id,随后后台流程失败,测试读取到 account/login/completed 且 success=false;再确认没有 account/updated,auth.json 也不存在。
调用关系:测试运行器执行它。它用 mock_device_code_usercode 和 mock_device_code_token_failure 组合出“开始成功、完成失败”的情况。
调用图:调用 4 个内部函数(new_with_env, create_config_toml, mock_device_code_token_failure, mock_device_code_usercode);外部调用 12 个(default, from_millis, start, new, Integer, to_response, write_models_cache, assert!, assert_eq!, bail! (+2 more))。
login_account_chatgpt_device_code_can_be_cancelled1238–1319 ↗
async fn login_account_chatgpt_device_code_can_be_cancelled() -> Result<()>
作用:测试正在进行的设备码登录可以被取消。取消后应告诉客户端失败结束,并且不更新账号。
数据流:它先启动设备码登录,拿到 login_id。随后发送 cancel login 请求,期望返回 Canceled。后台登录完成通知显示 success=false 且有错误;短暂等待后确认没有账号更新通知,也没有 auth.json。
调用关系:测试运行器执行它。它用 mock_device_code_usercode 设置较长轮询间隔,再用取消请求打断登录流程。
调用图:调用 4 个内部函数(new_with_env, create_config_toml, mock_device_code_token_failure, mock_device_code_usercode);外部调用 12 个(default, from_millis, start, new, Integer, to_response, write_models_cache, assert!, assert_eq!, bail! (+2 more))。
login_account_chatgpt_start_can_be_cancelled1324–1385 ↗
async fn login_account_chatgpt_start_can_be_cancelled() -> Result<()>
作用:测试普通 ChatGPT 浏览器登录刚启动后可以取消。普通登录会开本地回调端口,所以这个测试串行运行,避免多个测试抢同一个端口。
数据流:它发起 ChatGPT 登录,收到 login_id 和 auth_url,并检查 auth_url 指向 localhost 回调。然后发送取消请求,读取登录完成通知,确认 success=false 且有错误;再确认没有账号更新通知。
调用关系:测试运行器执行它,并用 serial(login_port) 保证独占登录端口。它主要检查取消逻辑,而不是完成真实浏览器登录。
调用图:调用 2 个内部函数(new, create_config_toml);外部调用 9 个(from_millis, new, Integer, default, to_response, assert!, bail!, assert_eq!, timeout)。
login_account_chatgpt_uses_debug_oauth_overrides1390–1437 ↗
async fn login_account_chatgpt_uses_debug_oauth_overrides() -> Result<()>
作用:测试调试用环境变量能改写 ChatGPT OAuth 登录地址和 client_id。这样开发或测试环境可以指向非正式登录服务。
数据流:它启动服务器时设置 CLIENT_ID_OVERRIDE_ENV_VAR 和登录 issuer 环境变量。发起 ChatGPT 登录后解析 auth_url,确认域名变成指定地址,query 参数里的 client_id 也变成指定值。最后取消这次登录。
调用关系:测试运行器执行它,并串行占用登录端口。它通过 TestAppServer::new_with_env 把环境变量传给服务器。
调用图:调用 2 个内部函数(new_with_env, create_config_toml);外部调用 8 个(new, parse, Integer, default, to_response, assert_eq!, bail!, timeout)。
set_auth_token_cancels_active_chatgpt_login1442–1506 ↗
async fn set_auth_token_cancels_active_chatgpt_login() -> Result<()>
作用:测试如果用户正在进行浏览器 ChatGPT 登录,但客户端突然直接设置了外部 token,旧的登录尝试会被取消清理。
数据流:它先启动普通 ChatGPT 登录并拿到 login_id。然后发送外部 token 登录请求,服务器成功切换到 ChatgptAuthTokens 并发账号更新。最后再尝试取消旧 login_id,结果应是 NotFound,说明旧登录已被清掉。
调用关系:测试运行器执行它,并串行占用登录端口。它连接了普通登录和外部 token 登录两条路径,检查二者不会同时留下活动状态。
调用图:调用 3 个内部函数(new, new, create_config_toml);外部调用 8 个(new, Integer, default, encode_id_token, to_response, assert_eq!, bail!, timeout)。
login_account_chatgpt_includes_forced_workspace_query_param1511–1540 ↗
async fn login_account_chatgpt_includes_forced_workspace_query_param() -> Result<()>
作用:测试配置只允许某个 ChatGPT 工作区时,登录链接里会带上这个工作区 ID。这样用户登录时就会被引导到正确组织。
数据流:它写入 forced_chatgpt_workspace_id 为单个 ID,启动服务器并发起 ChatGPT 登录。返回的 auth_url 必须包含 allowed_workspace_id=这个 ID。
调用关系:测试运行器执行它,并串行占用登录端口。它用 create_config_toml 写入工作区限制,再检查登录 URL 是否携带限制信息。
调用图:调用 2 个内部函数(new, create_config_toml);外部调用 7 个(default, new, Integer, to_response, assert!, bail!, timeout)。
login_account_chatgpt_includes_forced_workspace_allowlist_query_param1545–1584 ↗
async fn login_account_chatgpt_includes_forced_workspace_allowlist_query_param() -> Result<()>
作用:测试配置允许多个 ChatGPT 工作区时,登录链接会把这些 ID 作为允许列表传出去。
数据流:它写入两个 allowed workspace ID,启动服务器并发起 ChatGPT 登录。然后解析 auth_url,收集 allowed_workspace_id 参数,确认里面是两个 ID 用逗号连在一起。
调用关系:测试运行器执行它,并串行占用登录端口。它检查单工作区限制之外的多工作区允许列表情况。
调用图:调用 2 个内部函数(new, create_config_toml);外部调用 9 个(default, new, parse, Integer, to_response, assert_eq!, bail!, timeout, vec!)。
get_account_no_auth1587–1616 ↗
async fn get_account_no_auth() -> Result<()>
作用:测试没有任何登录信息时,查询账号会返回空账号,同时仍能告诉客户端当前模型服务是否要求 OpenAI 登录。
数据流:它写入 requires_openai_auth=true 的配置,但不写 auth.json,也清掉环境里的 OPENAI_API_KEY。查询账号后,结果应是 account=None,requires_openai_auth=true。
调用关系:测试运行器执行它。它是账号查询的空状态基准,用来确保未登录不会被误判成已登录。
调用图:调用 2 个内部函数(new_with_env, create_config_toml);外部调用 6 个(default, new, Integer, to_response, assert_eq!, timeout)。
get_account_with_api_key1619–1660 ↗
async fn get_account_with_api_key() -> Result<()>
作用:测试用 API Key 登录后,查询账号能返回 ApiKey 类型账号。
数据流:它启动服务器,先发送 API Key 登录并确认成功,再发送 get account 请求。结果应包含 Account::ApiKey,同时保留配置里的 requires_openai_auth=true。
调用关系:测试运行器执行它。它先走 login_account_api_key_succeeds_and_notifies 所覆盖的登录路径,再验证查询接口看到同一份状态。
调用图:调用 2 个内部函数(new, create_config_toml);外部调用 6 个(default, new, Integer, to_response, assert_eq!, timeout)。
get_account_when_auth_not_required1663–1694 ↗
async fn get_account_when_auth_not_required() -> Result<()>
作用:测试模型服务不要求 OpenAI 登录时,查询账号会明确返回 requires_openai_auth=false。
数据流:它写入 requires_openai_auth=false 的配置,启动服务器后直接查询账号。结果应是 account=None,requires_openai_auth=false。
调用关系:测试运行器执行它。它专门验证配置项会反映到账号查询结果里,而不是默认总要求登录。
调用图:调用 2 个内部函数(new, create_config_toml);外部调用 6 个(default, new, Integer, to_response, assert_eq!, timeout)。
get_account_with_aws_provider1697–1737 ↗
async fn get_account_with_aws_provider() -> Result<()>
作用:测试使用 Amazon Bedrock 且凭据由 AWS 自己管理时,查询账号会返回对应的 Bedrock 账号来源。
数据流:它写入 amazon-bedrock provider,并配置 AWS profile 和 region。启动服务器后查询账号,结果应是 Account::AmazonBedrock,credential_source 为 AwsManaged,且不要求 OpenAI 登录。
调用关系:测试运行器执行它。它通过 create_config_toml 的 extra_provider_config 设置 AWS 管理凭据场景。
调用图:调用 2 个内部函数(new, create_config_toml);外部调用 6 个(default, new, Integer, to_response, assert_eq!, timeout)。
get_account_with_managed_bedrock_provider1740–1782 ↗
async fn get_account_with_managed_bedrock_provider() -> Result<()>
作用:测试使用 Codex 托管的 Bedrock API Key 时,查询账号能识别凭据来源是 CodexManaged。
数据流:它写入 amazon-bedrock provider 配置,并用 login_with_bedrock_api_key 在临时目录保存 Bedrock API Key。启动服务器后查询账号,结果应显示 AmazonBedrock 且 credential_source 为 CodexManaged。
调用关系:测试运行器执行它。它覆盖和 get_account_with_aws_provider 不同的 Bedrock 凭据来源。
调用图:调用 3 个内部函数(new, create_config_toml, default);外部调用 7 个(default, new, Integer, to_response, assert_eq!, login_with_bedrock_api_key, timeout)。
get_account_with_chatgpt1785–1827 ↗
async fn get_account_with_chatgpt() -> Result<()>
作用:测试已有 ChatGPT 登录文件时,查询账号能读出邮箱和套餐。
数据流:它写入 requires_openai_auth=true 的配置,再用测试夹具写入 ChatGPT auth.json,里面有邮箱 user@example.com 和 pro 套餐。启动服务器查询账号,结果应返回 Account::Chatgpt 和 Pro 套餐。
调用关系:测试运行器执行它。它不走登录流程,而是直接准备本地登录文件,验证服务器启动后读取账号资料的能力。
调用图:调用 3 个内部函数(new, new_with_env, create_config_toml);外部调用 7 个(default, new, Integer, to_response, write_chatgpt_auth, assert_eq!, timeout)。
get_account_omits_chatgpt_after_permanent_refresh_failure1830–1911 ↗
async fn get_account_omits_chatgpt_after_permanent_refresh_failure() -> Result<()>
作用:测试 ChatGPT refresh token 已经永久失效时,查询账号不应继续显示这个旧账号。这样用户不会看到一个其实不能用的登录状态。
数据流:它写入一份过期较久的 ChatGPT 登录信息,并让假 OAuth 刷新接口返回 401 refresh_token_reused。先调用 get auth status 触发刷新失败,再查询账号。结果应是 account=None,同时假服务器验证刷新接口确实被调用过。
调用关系:测试运行器执行它。它用 REFRESH_TOKEN_URL_OVERRIDE_ENV_VAR 把刷新请求导向假服务器,检查永久刷新失败后的账号隐藏逻辑。
调用图:调用 3 个内部函数(new, new_with_env, create_config_toml);外部调用 16 个(days, default, given, start, new, new, now, Integer, to_response, write_chatgpt_auth (+6 more))。
get_account_with_chatgpt_missing_plan_claim_returns_unknown1914–1954 ↗
async fn get_account_with_chatgpt_missing_plan_claim_returns_unknown() -> Result<()>
作用:测试 ChatGPT token 里没有套餐信息时,查询账号不会报错,而是把套餐标成 Unknown。
数据流:它写入一份只有邮箱、没有 plan_type 的 ChatGPT auth。启动服务器查询账号后,结果应返回这个邮箱,plan_type 为 Unknown。
调用关系:测试运行器执行它。它和 get_account_with_chatgpt 类似,但专门覆盖 token 缺少套餐字段的兼容情况。
调用图:调用 3 个内部函数(new, new_with_env, create_config_toml);外部调用 7 个(default, new, Integer, to_response, write_chatgpt_auth, assert_eq!, timeout)。
app-server/tests/suite/v2/rate_limits.rs源码 ↗
这个文件像一套验收清单,专门检查限额相关接口。它会启动一个临时的测试服务器,用临时目录假装用户的配置目录,再用 wiremock(一个假的 HTTP 服务器,用来冒充真正后端)模拟 ChatGPT 后端的回答。测试分两大块:第一块是读取账号使用限额,确认没登录、只用 API key 登录、用 ChatGPT 登录时分别会发生什么;第二块是发送“请给我加额度”的提醒邮件,确认没权限时会报清楚的错,正常时会向后端 POST 正确内容,后端说冷却中时会变成友好的状态,后端炸了时会返回内部错误。辅助函数负责做 API key 登录,以及把测试后端地址写进配置文件。没有这些测试,改动服务器时很容易把鉴权、错误提示或后端数据映射悄悄弄坏。
get_account_rate_limits_requires_auth39–62 ↗
async fn get_account_rate_limits_requires_auth() -> Result<()>
作用:这个测试确认:如果用户完全没有账号认证,就不能读取账号限额。它防止服务器在没有身份信息时误把限额信息放出去。
数据流:进去的是一个全新的临时目录,并且环境里明确没有 OPENAI_API_KEY。测试启动 TestAppServer,完成初始化后发送“读取账号限额”的 JSON-RPC 请求(JSON-RPC 是一种用 JSON 包装请求和回应的通信格式)。服务器返回错误后,测试检查错误编号是“无效请求”,错误文字也必须说明需要 Codex 账号认证。
调用关系:它直接创建测试服务器并等待初始化,然后通过服务器测试工具发请求、读错误消息。它不调用本文件的辅助登录函数,因为这里刻意模拟“完全没登录”的情况。
调用图:调用 1 个内部函数(new_with_env);外部调用 4 个(new, Integer, assert_eq!, timeout)。
get_account_rate_limits_requires_chatgpt_auth65–89 ↗
async fn get_account_rate_limits_requires_chatgpt_auth() -> Result<()>
作用:这个测试确认:只有 API key 登录还不够,读取账号限额必须是 ChatGPT 账号登录。这样可以避免把两种不同登录方式混在一起用错。
数据流:进去的是临时配置目录和一个假 API key。测试先启动服务器,再调用 login_with_api_key 完成 API key 登录,接着发送读取限额请求。出来的是一条 JSON-RPC 错误,测试检查它的请求编号对得上,错误码是“无效请求”,错误消息明确写着需要 ChatGPT authentication。
调用关系:它位于“鉴权边界”测试中,先把登录步骤交给 login_with_api_key,再自己检查读取限额接口的拒绝行为。
调用图:调用 2 个内部函数(new, login_with_api_key);外部调用 4 个(new, Integer, assert_eq!, timeout)。
get_account_rate_limits_returns_snapshot92–268 ↗
async fn get_account_rate_limits_returns_snapshot() -> Result<()>
作用:这个测试确认:ChatGPT 登录有效时,服务器能从后端拿到限额数据,并把复杂的后端格式整理成客户端协议里的快照。它防止限额百分比、重置时间、套餐类型、额外限额等字段在转换时丢失或算错。
数据流:进去的是一个写好 ChatGPT token、账号 ID 和套餐类型的临时用户目录,以及一个假的后端服务器地址。测试让假后端在 GET /api/codex/usage 时返回一大段限额 JSON。应用服务器收到客户端读取限额请求后,会带上 authorization 和 chatgpt-account-id 请求假后端,再把后端字段转换成 GetAccountRateLimitsResponse。最后测试把响应拆出来,和手写的期望结果逐项比较。
调用关系:它用 write_chatgpt_base_url 把应用服务器指向假后端,用 wiremock 安排后端响应,再通过 TestAppServer 走完整的 JSON-RPC 流程。它是这个文件里最全面的“成功路径”测试。
调用图:调用 3 个内部函数(new, new_with_env, write_chatgpt_base_url);外部调用 14 个(given, start, new, new, Integer, to_response, write_chatgpt_auth, assert_eq!, parse_from_rfc3339, json! (+4 more))。
send_add_credits_nudge_email_requires_auth271–298 ↗
async fn send_add_credits_nudge_email_requires_auth() -> Result<()>
作用:这个测试确认:完全没登录时,不能发送“提醒工作区负责人加额度”的邮件请求。它防止陌生请求触发真实通知。
数据流:进去的是一个干净的临时目录,并且没有 OPENAI_API_KEY。测试启动服务器后发送 send_add_credits_nudge_email 请求,请求里带的是 credit_type 为 Credits。服务器没有执行后端通知,而是返回 JSON-RPC 错误。测试检查错误码和错误文字,确认它说的是需要 Codex 账号认证才能通知工作区负责人。
调用关系:它和读取限额的未登录测试类似,直接使用 TestAppServer 发请求并读取错误,不经过任何登录辅助函数。
调用图:调用 1 个内部函数(new_with_env);外部调用 4 个(new, Integer, assert_eq!, timeout)。
send_add_credits_nudge_email_requires_chatgpt_auth301–329 ↗
async fn send_add_credits_nudge_email_requires_chatgpt_auth() -> Result<()>
作用:这个测试确认:只用 API key 登录时,也不能发送加额度提醒邮件。因为这个动作依赖 ChatGPT 账号和工作区身份,不是普通 API key 能代表的。
数据流:进去的是临时目录和一个假 API key。测试先启动服务器并调用 login_with_api_key 登录,再发送 credit_type 为 UsageLimit 的提醒邮件请求。服务器返回错误,测试确认错误码是“无效请求”,错误文字说明需要 ChatGPT authentication 才能通知工作区负责人。
调用关系:它复用 login_with_api_key 制造“已登录但不是 ChatGPT 登录”的场景,然后专门检查提醒邮件接口的权限判断。
调用图:调用 2 个内部函数(new, login_with_api_key);外部调用 4 个(new, Integer, assert_eq!, timeout)。
send_add_credits_nudge_email_posts_expected_body333–378 ↗
async fn send_add_credits_nudge_email_posts_expected_body() -> Result<()>
作用:这个测试确认:正常 ChatGPT 登录后,服务器真的会向后端发送正确的提醒邮件请求。它重点检查 HTTP 方法、路径、身份头和请求正文都对。
数据流:进去的是写好 ChatGPT token 和账号 ID 的临时目录,以及一个假的后端服务器。测试配置假后端期待收到 POST /api/codex/accounts/send_add_credits_nudge_email,并要求请求头里有 Bearer token 和账号 ID,正文里有 credit_type: usage_limit。客户端发出 JSON-RPC 请求后,服务器转发到假后端;假后端返回 200,最终客户端收到状态 Sent。
调用关系:它用 write_chatgpt_base_url 把后端地址改成 MockServer,再由 wiremock 检查请求是否完全符合预期。这个测试在 Windows 上被跳过,因为同样场景已由 Linux 和 macOS 的持续集成覆盖。
调用图:调用 3 个内部函数(new, new_with_env, write_chatgpt_base_url);外部调用 14 个(given, start, new, new, Integer, to_response, write_chatgpt_auth, assert_eq!, json!, timeout (+4 more))。
send_add_credits_nudge_email_maps_cooldown382–422 ↗
async fn send_add_credits_nudge_email_maps_cooldown() -> Result<()>
作用:这个测试确认:后端用 429 表示“太频繁了,冷却中”时,应用服务器会把它翻译成客户端能直接理解的 CooldownActive 状态,而不是粗暴报错。
数据流:进去的是有效的 ChatGPT 登录信息和一个假后端。假后端对提醒邮件 POST 返回 HTTP 429。测试发出提醒请求后,服务器收到 429,不把它当成崩溃,而是生成 SendAddCreditsNudgeEmailResponse,并把 status 设为 CooldownActive。最后测试验证这个状态。
调用关系:它和正常发送测试走同一条请求链路,也依赖 write_chatgpt_base_url 指向假后端;区别是后端响应被安排成冷却状态,用来检查错误码到业务状态的映射。
调用图:调用 3 个内部函数(new, new_with_env, write_chatgpt_base_url);外部调用 11 个(given, start, new, new, Integer, to_response, write_chatgpt_auth, assert_eq!, timeout, method (+1 more))。
send_add_credits_nudge_email_surfaces_backend_failure426–475 ↗
async fn send_add_credits_nudge_email_surfaces_backend_failure() -> Result<()>
作用:这个测试确认:如果后端真的失败了,比如返回 500,服务器会把问题作为内部错误告诉客户端,并带上有用但不过度暴露的数据。它防止失败被误报成发送成功或冷却中。
数据流:进去的是有效 ChatGPT 登录信息和一个假后端。假后端对提醒邮件 POST 返回 500,并带有文本 boom。测试发送提醒请求后读取 JSON-RPC 错误,检查请求编号匹配、错误码是内部错误、错误消息包含“failed to notify workspace owner”,并确认没有额外 error data。
调用关系:它通过 write_chatgpt_base_url 和 MockServer 构造后端故障场景,再让 TestAppServer 跑完整转发流程。它补上了正常、冷却之外的真实失败分支。
调用图:调用 3 个内部函数(new, new_with_env, write_chatgpt_base_url);外部调用 11 个(given, start, new, new, Integer, write_chatgpt_auth, assert!, assert_eq!, timeout, method (+1 more))。
login_with_api_key477–488 ↗
async fn login_with_api_key(mcp: &mut TestAppServer, api_key: &str) -> Result<()>
作用:这个辅助函数帮测试用 API key 登录一次。它的作用不是测试登录本身,而是方便其他测试制造“已经用 API key 登录,但不是 ChatGPT 登录”的状态。
数据流:进去的是一个可操作的 TestAppServer 和一串 API key。函数向服务器发送 API key 登录请求,等待对应请求编号的响应,把 JSON-RPC 响应转成 LoginAccountResponse,然后确认结果正好是 ApiKey 登录成功。它不返回登录对象,只在成功时返回空结果,失败时把错误传出去。
调用关系:它被 get_account_rate_limits_requires_chatgpt_auth 和 send_add_credits_nudge_email_requires_chatgpt_auth 调用。调用者用它先铺好测试状态,再检查目标接口是否仍然要求 ChatGPT 认证。
调用图:调用 2 个内部函数(read_stream_until_response_message, send_login_account_api_key_request);被 2 处调用(get_account_rate_limits_requires_chatgpt_auth, send_add_credits_nudge_email_requires_chatgpt_auth);外部调用 4 个(Integer, to_response, assert_eq!, timeout)。
write_chatgpt_base_url490–493 ↗
fn write_chatgpt_base_url(codex_home: &Path, base_url: &str) -> std::io::Result<()>
作用:这个辅助函数把测试用的 ChatGPT 后端地址写进配置文件。这样应用服务器不会访问真实后端,而是访问测试里搭好的假服务器。
数据流:进去的是用户目录路径 codex_home 和一个 base_url 字符串。函数拼出 config.toml 的路径,把一行 chatgpt_base_url = "..." 写进去。出来的是文件写入结果;成功后,这个临时配置目录就会让服务器使用指定的后端地址。
调用关系:它被所有需要假后端的成功或后端错误测试调用,包括读取限额和发送提醒邮件的多种场景。它是测试隔离外部网络依赖的关键小工具。
调用图:被 4 处调用(get_account_rate_limits_returns_snapshot, send_add_credits_nudge_email_maps_cooldown, send_add_credits_nudge_email_posts_expected_body, send_add_credits_nudge_email_surfaces_backend_failure);外部调用 3 个(join, format!, write)。
app-server/tests/suite/v2/rate_limit_reset_credits.rs源码 ↗
这个测试文件像一套验收清单,盯着“rate limit reset credit(限流重置额度,也就是用户可以用来清掉一段限流等待的机会)”是否被正确使用。它会启动一个测试版应用服务器,再用临时目录假装用户的配置和登录信息。需要模拟真正的 ChatGPT 后端时,它会开一个假的 HTTP 服务器,让测试自己规定后端返回什么。文件重点检查几件事:没有正确的 ChatGPT 登录不能用;空的 idempotency key(幂等键,用来避免同一个请求被重复扣额度)会被拒绝;后端说 reset、no_credit 等不同结果时,应用服务器会翻译成协议里的正确结果;后端失败或超时时,服务器会返回清楚的 JSON-RPC 错误(JSON-RPC 是一种用 JSON 包装请求和响应的通信格式)。还有一个重要点:即使消耗额度请求超时,也不能把账号认证队列卡死,后面的账号请求仍然要能继续得到响应。
consume_rate_limit_reset_credit_requires_chatgpt_auth39–66 ↗
async fn consume_rate_limit_reset_credit_requires_chatgpt_auth() -> Result<()>
作用:这个测试确认:使用限流重置额度必须是真正的 ChatGPT 账号登录,只有普通 API Key 不够。这样可以避免把只属于 ChatGPT 账号的权益错误地开放给别的认证方式。
数据流:它先创建一个空的临时用户目录并启动测试服务器,在没有登录的状态下发送消耗额度请求,读回错误并检查错误码和提示文字。然后它用 API Key 登录,再发一次同类请求,确认这次仍然被拒绝,而且错误信息明确说需要 ChatGPT 认证。
调用关系:它先通过 initialized_app_server 把测试服务器准备好;中间用 login_with_api_key 模拟 API Key 登录;发送请求时交给 send_consume_reset_credit,读取失败结果时交给 read_error_response。整个故事是在验证入口处的认证拦截是不是足够严格。
调用图:调用 4 个内部函数(initialized_app_server, login_with_api_key, read_error_response, send_consume_reset_credit);外部调用 2 个(new, assert_eq!)。
consume_account_rate_limit_reset_credit_maps_backend_outcomes69–121 ↗
async fn consume_account_rate_limit_reset_credit_maps_backend_outcomes() -> Result<()>
作用:这个测试确认:真正的后端返回不同文字代码时,应用服务器会把它们翻译成客户端协议里约定好的结果。这样客户端不用直接理解后端内部用的字符串。
数据流:它先准备带 ChatGPT 登录信息的临时环境和一个假的后端服务器。然后为 reset、nothing_to_reset、no_credit、already_redeemed 这些后端返回分别设置模拟响应。接着启动应用服务器,逐个发送带不同幂等键的消耗请求,最后检查返回的 outcome 是否等于预期的协议枚举值。
调用关系:它依赖 chatgpt_test_context 准备登录凭据和假的后端地址,再用 initialized_app_server 启动服务器。测试里的 wiremock 负责假装后端,consume_reset_credit 负责完成“发送请求并读正常响应”这一整步。
调用图:调用 2 个内部函数(chatgpt_test_context, initialized_app_server);外部调用 8 个(given, new, assert_eq!, json!, body_json, header, method, path)。
consume_account_rate_limit_reset_credit_rejects_empty_idempotency_key124–140 ↗
async fn consume_account_rate_limit_reset_credit_rejects_empty_idempotency_key() -> Result<()>
作用:这个测试确认:幂等键不能为空。幂等键就像请求编号,少了它就没法可靠判断一次扣额度是不是重复请求。
数据流:它先准备一个已经有 ChatGPT 登录的测试环境并启动服务器。然后发送一个 idempotency_key 为空字符串的消耗请求。服务器返回错误后,测试检查错误码是“无效请求”,错误信息也明确说明 idempotencyKey 不能为空。
调用关系:它用 chatgpt_test_context 和 initialized_app_server 搭好环境,然后直接通过测试服务器发送请求,最后用 read_error_response 读取并验证失败结果。这个测试主要站在参数校验这一关口。
调用图:调用 3 个内部函数(chatgpt_test_context, initialized_app_server, read_error_response);外部调用 2 个(new, assert_eq!)。
consume_account_rate_limit_reset_credit_surfaces_backend_failure143–165 ↗
async fn consume_account_rate_limit_reset_credit_surfaces_backend_failure() -> Result<()>
作用:这个测试确认:如果后端服务返回失败,应用服务器不会装作成功,而是把失败变成客户端能看到的内部错误。这样问题不会被悄悄吞掉。
数据流:它先准备 ChatGPT 登录环境和假的后端服务器,并让假的后端在消耗额度接口上返回 HTTP 500 和文本 boom。随后启动应用服务器,发送一次消耗请求。读到错误响应后,它检查错误码是内部错误,并且错误消息里包含“消耗限流重置失败”的意思。
调用关系:它通过 chatgpt_test_context 准备后端假服务,通过 initialized_app_server 启动应用,通过 send_consume_reset_credit 触发实际请求,再通过 read_error_response 收集错误。wiremock 在这里扮演出故障的远端后端。
调用图:调用 4 个内部函数(chatgpt_test_context, initialized_app_server, read_error_response, send_consume_reset_credit);外部调用 6 个(given, new, assert!, assert_eq!, method, path)。
consume_timeout_releases_account_auth_queue168–214 ↗
async fn consume_timeout_releases_account_auth_queue() -> Result<()>
作用:这个测试确认:消耗额度请求如果超时,不会一直占住账号认证相关的处理通道。换句话说,一个慢请求不能把后面的账号请求堵死。
数据流:它先准备 ChatGPT 登录环境,并让假的后端故意延迟 1 秒才返回。然后启动测试服务器时把消耗额度请求超时设置成 100 毫秒。它发出一个会超时的消耗请求,又立刻发出一个获取账号信息的请求。最后它确认第一个请求返回“消耗超时”,第二个请求也能正常得到自己的错误响应,而不是一直卡住。
调用关系:它用 chatgpt_test_context 建好测试账号和假后端,但不用普通的 initialized_app_server,而是直接调用 new_with_env 注入特殊超时环境变量。发送消耗请求交给 send_consume_reset_credit,读取后续账号错误交给 read_error_response。这个测试特别关注并发排队和超时清理。
调用图:调用 4 个内部函数(new_with_env, chatgpt_test_context, read_error_response, send_consume_reset_credit);外部调用 9 个(given, new, Integer, assert_eq!, json!, from_secs, timeout, method, path)。
chatgpt_test_context216–228 ↗
async fn chatgpt_test_context() -> Result<(TempDir, MockServer)>
作用:这个辅助函数准备一套“看起来已经登录 ChatGPT”的测试环境。很多测试都需要同样的登录文件和假后端地址,所以把这一步集中在这里。
数据流:它创建一个临时目录作为用户主目录,在里面写入一份 ChatGPT 认证信息,包括访问令牌、账号 ID 和套餐类型。然后启动一个假的 HTTP 后端服务器,并把这个假服务器的网址写进配置文件。最后返回临时目录和假服务器,供测试继续使用。
调用关系:它被多个测试开头调用,用来统一搭建前置条件。内部调用 write_chatgpt_auth 写认证文件,调用 MockServer::start 启动假后端,再把写配置这件事交给 write_chatgpt_base_url。
调用图:调用 2 个内部函数(new, write_chatgpt_base_url);被 4 处调用(consume_account_rate_limit_reset_credit_maps_backend_outcomes, consume_account_rate_limit_reset_credit_rejects_empty_idempotency_key, consume_account_rate_limit_reset_credit_surfaces_backend_failure, consume_timeout_releases_account_auth_queue);外部调用 3 个(start, new, write_chatgpt_auth)。
initialized_app_server230–234 ↗
async fn initialized_app_server(codex_home: &Path) -> Result<TestAppServer>
作用:这个辅助函数启动并初始化测试版应用服务器。它把重复的“创建服务器、等待初始化完成”步骤包起来,避免每个测试都写一遍。
数据流:它接收一个临时用户目录路径,用这个目录启动 TestAppServer,并明确清掉 OPENAI_API_KEY 环境变量,避免外部真实环境影响测试。然后它等待服务器初始化完成,超过默认时间就失败。成功后返回可以继续收发请求的服务器对象。
调用关系:它被需要普通启动流程的测试调用。内部依赖 TestAppServer::new_with_env 创建服务器,并用 timeout 给初始化过程加上等待上限,防止测试无限挂住。
调用图:调用 1 个内部函数(new_with_env);被 4 处调用(consume_account_rate_limit_reset_credit_maps_backend_outcomes, consume_account_rate_limit_reset_credit_rejects_empty_idempotency_key, consume_account_rate_limit_reset_credit_surfaces_backend_failure, consume_rate_limit_reset_credit_requires_chatgpt_auth);外部调用 1 个(timeout)。
consume_reset_credit236–242 ↗
async fn consume_reset_credit(
mcp: &mut TestAppServer,
idempotency_key: &str,
) -> Result<ConsumeAccountRateLimitResetCreditResponse>
作用:这个辅助函数把“发送消耗限流重置额度请求,并读取成功响应”合成一步。测试只关心结果时,用它会更清楚。
数据流:它接收一个可变的测试服务器和一个幂等键。先把幂等键交给 send_consume_reset_credit 发出请求,得到请求编号;再用 read_response 等待这个编号对应的正常响应,并把响应转成指定的结果类型返回。
调用关系:它作为更高层的测试小工具使用,里面串起 send_consume_reset_credit 和 read_response。比如映射后端结果的测试,就用它来专注比较 outcome,而不用关心收发细节。
调用图:调用 2 个内部函数(read_response, send_consume_reset_credit)。
send_consume_reset_credit244–251 ↗
async fn send_consume_reset_credit(mcp: &mut TestAppServer, idempotency_key: &str) -> Result<i64>
作用:这个辅助函数只负责发出“消耗限流重置额度”的请求。它不等最终结果,只返回这次请求的编号,方便之后按编号读取响应或错误。
数据流:它接收测试服务器和幂等键,把字符串包装成 ConsumeAccountRateLimitResetCreditParams 参数对象,然后调用服务器的发送方法。发送成功后返回一个整数请求 ID,后续读取响应时用这个 ID 找回对应消息。
调用关系:它被多个测试和 consume_reset_credit 调用,是测试里触发目标接口的统一出口。真正的请求发送动作交给 TestAppServer 的 send_consume_account_rate_limit_reset_credit_request。
调用图:调用 1 个内部函数(send_consume_account_rate_limit_reset_credit_request);被 4 处调用(consume_account_rate_limit_reset_credit_surfaces_backend_failure, consume_rate_limit_reset_credit_requires_chatgpt_auth, consume_reset_credit, consume_timeout_releases_account_auth_queue)。
read_response253–263 ↗
async fn read_response(mcp: &mut TestAppServer, request_id: i64) -> Result<T>
作用:这个辅助函数按请求编号读取一条成功响应,并把 JSON-RPC 包装拆成测试想要的具体类型。它让测试不用重复写等待和反序列化代码。
数据流:它接收测试服务器和请求 ID。先等待服务器流里出现这个请求 ID 对应的成功响应,并给等待过程加默认超时。拿到 JSONRPCResponse 后,再通过 to_response 转成调用方指定的类型,最后返回这个具体结果。
调用关系:它被 consume_reset_credit 使用,承担“读成功结果”的收尾工作。底层读取交给 read_stream_until_response_message,格式转换交给 to_response,timeout 负责避免测试卡死。
调用图:调用 1 个内部函数(read_stream_until_response_message);被 1 处调用(consume_reset_credit);外部调用 3 个(Integer, to_response, timeout)。
read_error_response265–272 ↗
async fn read_error_response(mcp: &mut TestAppServer, request_id: i64) -> Result<JSONRPCError>
作用:这个辅助函数按请求编号读取一条错误响应。很多测试要确认服务器是否拒绝请求、报错文字是否正确,所以它把这件事统一封装起来。
数据流:它接收测试服务器和请求 ID。然后等待服务器流里出现这个请求 ID 对应的错误消息,并使用默认超时时间保护测试。拿到 JSONRPCError 后直接返回给测试做断言。
调用关系:它被认证失败、参数错误、后端失败、超时释放队列等测试使用。它把具体的流读取交给 read_stream_until_error_message,把请求 ID 包成 JSON-RPC 使用的 RequestId::Integer。
调用图:调用 1 个内部函数(read_stream_until_error_message);被 4 处调用(consume_account_rate_limit_reset_credit_rejects_empty_idempotency_key, consume_account_rate_limit_reset_credit_surfaces_backend_failure, consume_rate_limit_reset_credit_requires_chatgpt_auth, consume_timeout_releases_account_auth_queue);外部调用 2 个(Integer, timeout)。
login_with_api_key274–281 ↗
async fn login_with_api_key(mcp: &mut TestAppServer, api_key: &str) -> Result<()>
作用:这个辅助函数模拟用户用 API Key 登录,并确认登录确实成功。它用于测试“API Key 登录不等于 ChatGPT 登录”这个边界。
数据流:它接收测试服务器和一个 API Key 字符串。先把 API Key 登录请求发给服务器,拿到请求 ID;再读取这个请求的成功响应;最后确认响应类型是 ApiKey 登录成功。如果不是,测试立刻失败。
调用关系:它只被 consume_rate_limit_reset_credit_requires_chatgpt_auth 调用。这个测试先靠它切换到 API Key 登录状态,再验证限流重置额度接口仍然拒绝访问。
调用图:调用 1 个内部函数(send_login_account_api_key_request);被 1 处调用(consume_rate_limit_reset_credit_requires_chatgpt_auth);外部调用 1 个(assert_eq!)。
write_chatgpt_base_url283–288 ↗
fn write_chatgpt_base_url(codex_home: &Path, base_url: &str) -> std::io::Result<()>
作用:这个辅助函数把 ChatGPT 后端地址写进测试配置文件。这样应用服务器会访问测试里的假后端,而不是误连真实服务。
数据流:它接收临时用户目录路径和一个基础网址。然后在这个目录下生成 config.toml 文件,写入一行 chatgpt_base_url 配置。写入成功就返回成功,写文件失败就把系统错误传回去。
调用关系:它被 chatgpt_test_context 调用,是搭建假后端环境的最后一步。chatgpt_test_context 先启动 MockServer 得到地址,再让这个函数把地址落到配置里,供应用服务器启动时读取。
调用图:被 1 处调用(chatgpt_test_context);外部调用 3 个(join, format!, write)。
初始化和配置界面
这些测试验证启动时初始化行为、严格配置解析、运行时配置 RPC,以及由功能/配置派生的能力公开。
app-server/tests/suite/strict_config.rs源码 ↗
这个测试像是在模拟一个用户把配置文件写错了:它先临时建一个假的 CODEX_HOME 目录,再往里面放一个 config.toml,里面只有一个程序不认识的字段 foo。然后它启动真正的 codex-app-server 命令,并加上 --strict-config,意思是“配置必须严格检查,不能有陌生字段”。同时它把监听功能关掉,避免测试真的开网络端口。测试期待这个程序启动失败,并且错误输出里明确写着 unknown configuration field foo。这样做很重要:如果以后有人改代码时不小心让严格配置失效,这个测试会立刻失败,提醒开发者“错误配置不能被放过”。
strict_config_rejects_unknown_config_fields_for_standalone_app_server7–33 ↗
fn strict_config_rejects_unknown_config_fields_for_standalone_app_server() -> Result<()>
作用:这个测试函数检查独立运行的 app server 是否会在严格配置模式下拒绝未知配置字段。它的价值是确保用户写错配置时能尽早看到明确错误,而不是程序假装没事继续运行。
数据流:进去时没有外部参数,测试自己创建一个临时目录,当作程序的家目录;它写入一个带有未知字段 foo 的 config.toml;接着通过 cargo_bin 找到 codex-app-server 可执行文件,用 Command 启动它,并设置 CODEX_HOME 和托管配置路径这两个环境变量;程序运行结束后,测试读取退出状态和标准错误输出。最后它要求结果必须是启动失败,并且错误信息必须包含 unknown configuration field foo;如果不符合,测试就失败。
调用关系:这个函数由 Rust 的测试框架在跑测试时自动调用。它把准备测试环境的活儿交给 tempfile::TempDir::new,把写配置文件交给 std::fs::write,把寻找可执行文件交给 codex_utils_cargo_bin::cargo_bin,把启动进程交给 std::process::Command::new,最后用 String::from_utf8 把程序的错误输出转成能检查的文字,并用 assert! 判断结果是否符合预期。
调用图:外部调用 6 个(from_utf8, new, assert!, new, cargo_bin, write)。
app-server/tests/suite/v2/initialize.rs源码 ↗
这个文件像一套开机体检表。app server 启动后,客户端会先发一个 initialize 请求,告诉服务器“我是谁、我支持什么能力”。这些测试会启动一个临时的测试服务器,写一份临时 config.toml 配置文件,再用假模型服务替代真实网络调用,避免测试受外部环境影响。它重点检查几件事:客户端名字会不会正确进入 user agent;某些内部探测客户端不会误改来源标识;环境变量能不能强制覆盖来源标识;非法客户端名会不会被拒绝;客户端声明不想收某类通知时,服务器是否真的不发;以及后续 turn 完成时写出的通知文件里是否带上初始化时的客户端名。辅助函数负责生成测试配置和安全拼接 TOML 字符串。没有这些测试,初始化协议改坏了可能不会马上发现,客户端和服务器之间就容易出现“你以为我知道,其实我没收到”的问题。
initialize_uses_client_info_name_as_originator30–63 ↗
async fn initialize_uses_client_info_name_as_originator() -> Result<()>
作用:这个测试确认:普通客户端在初始化时传来的 clientInfo.name,会被服务器当作来源标识的一部分。简单说,VS Code 插件说“我是 codex_vscode”,服务器返回的 user agent 也应该从这个名字开始。
数据流:它先准备一个空的假模型响应服务和一个临时的 codex_home 目录,再写入测试配置文件。随后启动 TestAppServer,发送带有 name、title、version 的初始化请求。服务器返回 JSON-RPC 响应后,测试把响应拆成 InitializeResponse,检查 user_agent、codex_home、platform_family 和 platform_os 是否符合预期;如果响应不是成功响应,测试直接失败。
调用关系:这是初始化流程的基础测试之一。它会用 create_config_toml 准备配置,用测试服务器发送 initialize 请求,再依赖协议解析工具把通用响应转成 InitializeResponse,最后用断言守住“客户端名字会成为来源标识”这个约定。
调用图:调用 3 个内部函数(new, create_config_toml, try_from);外部调用 7 个(new, new, bail!, create_mock_responses_server_sequence_unchecked, assert!, assert_eq!, timeout)。
initialize_probe_does_not_override_originator66–90 ↗
async fn initialize_probe_does_not_override_originator() -> Result<()>
作用:这个测试确认:名叫 codex_app_server_daemon 的内部探测客户端,不应该把真正的来源标识改掉。也就是说,它只是来探路的,不应该把服务器身份伪装成自己。
数据流:它创建假模型服务、临时目录和配置文件,然后启动测试 app server。接着发送一个 clientInfo.name 为 codex_app_server_daemon 的初始化请求。收到成功响应后,测试读取里面的 user_agent,并确认它仍然以 codex_cli_rs 开头,而不是以这个探测客户端名开头。
调用关系:它和普通客户端测试形成对照:普通客户端名会影响来源标识,但这个内部 daemon 名不会。它同样依赖 create_config_toml 搭建测试环境,并通过 TestAppServer 的初始化接口走完整协议。
调用图:调用 2 个内部函数(new, create_config_toml);外部调用 6 个(new, new, bail!, create_mock_responses_server_sequence_unchecked, assert!, timeout)。
initialize_codex_backend_does_not_override_originator93–117 ↗
async fn initialize_codex_backend_does_not_override_originator() -> Result<()>
作用:这个测试确认:名叫 codex-backend 的客户端也不会覆盖服务器默认来源标识。它防止后端内部组件把对外识别信息改乱。
数据流:它先创建假服务、临时配置和测试 app server。然后发送一个 clientInfo.name 为 codex-backend 的初始化请求。服务器返回后,测试解析 InitializeResponse,只关心 user_agent,并断言它仍然以 codex_cli_rs 开头。
调用关系:它和 initialize_probe_does_not_override_originator 一起保护内部客户端的特殊规则。测试过程仍然通过 create_config_toml 建好环境,再用初始化请求触发真实服务器逻辑。
调用图:调用 2 个内部函数(new, create_config_toml);外部调用 6 个(new, new, bail!, create_mock_responses_server_sequence_unchecked, assert!, timeout)。
initialize_respects_originator_override_env_var120–160 ↗
async fn initialize_respects_originator_override_env_var() -> Result<()>
作用:这个测试确认:如果设置了 CODEX_INTERNAL_ORIGINATOR_OVERRIDE 环境变量,服务器会优先使用这个值作为来源标识。它用来保证内部或测试环境需要强制指定身份时能生效。
数据流:它创建临时 codex_home,并计算这个目录的真实绝对路径。写好配置后,它用 new_with_env 启动测试服务器,同时注入 CODEX_INTERNAL_ORIGINATOR_OVERRIDE。随后发送普通客户端初始化请求。返回成功响应后,测试检查 user_agent 是否以环境变量里的值开头,同时检查返回的 codex_home 和操作系统信息是否正确。
调用关系:它验证的是初始化身份选择里的最高优先级规则:环境变量覆盖客户端上报的名字。它会调用 create_config_toml 准备配置,并通过 TestAppServer 的带环境变量启动方式模拟真实运行环境。
调用图:调用 3 个内部函数(new_with_env, create_config_toml, try_from);外部调用 7 个(new, new, bail!, create_mock_responses_server_sequence_unchecked, assert!, assert_eq!, timeout)。
initialize_rejects_invalid_client_name163–195 ↗
async fn initialize_rejects_invalid_client_name() -> Result<()>
作用:这个测试确认:客户端名字如果包含不能放进 HTTP 头的字符,比如回车符,初始化会被拒绝。这样可以避免坏名字污染后续网络请求或日志。
数据流:它搭好假模型服务和临时配置,并明确清掉 CODEX_INTERNAL_ORIGINATOR_OVERRIDE,避免环境变量绕过检查。然后发送 clientInfo.name 为 bad\rname 的初始化请求。服务器应该返回 JSON-RPC 错误,而不是成功响应;测试检查错误码是 -32600,错误消息说明这个名字不是合法 HTTP header value,且没有额外 data。
调用关系:它站在初始化入口处做“门卫”测试:名字不合格就不能进系统。它仍然复用 create_config_toml 和 TestAppServer,但预期结果是错误响应,而不是 InitializeResponse。
调用图:调用 2 个内部函数(new_with_env, create_config_toml);外部调用 6 个(new, new, bail!, create_mock_responses_server_sequence_unchecked, assert_eq!, timeout)。
initialize_opt_out_notification_methods_filters_notifications198–259 ↗
async fn initialize_opt_out_notification_methods_filters_notifications() -> Result<()>
作用:这个测试确认:客户端初始化时如果声明不想收到某些通知,服务器之后真的会过滤掉这些通知。这里检查的是 thread/started 通知不会被发给客户端。
数据流:它先启动测试服务器,并在初始化请求里带上 capabilities,也就是客户端能力声明,其中 opt_out_notification_methods 包含 thread/started。初始化成功后,它发送创建线程的请求。读取消息流时,它等待对应请求的响应;如果中途看到 thread/started 通知,就立刻失败。拿到线程创建响应后,它又短暂等待一次,确认后面也没有漏发 thread/started。
调用关系:它覆盖的是初始化和后续消息推送之间的联动。initialize_with_capabilities 先把客户端“不想收什么”告诉服务器,send_thread_start_request 再触发本来可能产生通知的动作,读取循环则验证过滤规则确实生效。
调用图:调用 2 个内部函数(new, create_config_toml);外部调用 11 个(new, new, bail!, Integer, default, create_mock_responses_server_sequence_unchecked, to_response, assert!, from_millis, timeout (+1 more))。
turn_start_notify_payload_includes_initialize_client_name262–336 ↗
async fn turn_start_notify_payload_includes_initialize_client_name() -> Result<()>
作用:这个测试确认:初始化时传入的客户端名字,会继续出现在后续 turn 完成后的通知载荷里。换句话说,服务器不只是初始化时知道“谁来了”,后面发通知时也会记得。
数据流:它先准备一个会返回最终助手消息“Done”的假模型服务,再创建临时目录和一个 notify.json 文件路径。配置文件里额外写入 notify 命令,让服务器在事件发生时把通知内容捕获到文件。服务器初始化时传入客户端名 xcode。之后测试创建线程、启动一轮对话、等待 turn/completed 通知,再等 notify.json 出现。最后读取这个 JSON 文件,确认里面的 client 字段等于 xcode。
调用关系:这是从初始化一直走到一次完整对话结束的集成测试。它用 create_config_toml_with_extra 加入通知捕获配置,用 toml_basic_string 安全写入路径字符串,然后通过 thread/start、turn/start 和通知读取串起完整流程。
调用图:调用 2 个内部函数(new, create_config_toml_with_extra);外部调用 15 个(default, from_secs, new, Integer, default, create_mock_responses_server_sequence_unchecked, to_response, assert_eq!, cargo_bin, format! (+5 more))。
create_config_toml339–345 ↗
fn create_config_toml(
codex_home: &Path,
server_uri: &str,
approval_policy: &str,
) -> std::io::Result<()>
作用:这个辅助函数用来快速生成测试需要的 config.toml。调用者只要给 codex_home、假模型服务地址和审批策略,它就会写出一份可用配置。
数据流:它接收一个目录路径、模型服务地址和 approval_policy 字符串。自己不直接写文件,而是把 extra 留空,转交给 create_config_toml_with_extra。结果是指定目录下会出现一份基础测试配置文件,返回写文件成功或失败的信息。
调用关系:大多数初始化测试都会先调用它,像铺好临时舞台一样。它把通用配置生成工作交给 create_config_toml_with_extra,避免每个测试重复写同一大段 TOML 内容。
调用图:调用 1 个内部函数(create_config_toml_with_extra);被 6 处调用(initialize_codex_backend_does_not_override_originator, initialize_opt_out_notification_methods_filters_notifications, initialize_probe_does_not_override_originator, initialize_rejects_invalid_client_name, initialize_respects_originator_override_env_var, initialize_uses_client_info_name_as_originator)。
create_config_toml_with_extra347–378 ↗
fn create_config_toml_with_extra(
codex_home: &Path,
server_uri: &str,
approval_policy: &str,
extra: &str,
) -> std::io::Result<()>
作用:这个辅助函数负责真正写出 config.toml,并允许额外插入一段配置。它适合普通测试,也适合需要加 notify 等特殊设置的测试。
数据流:它接收 codex_home 目录、假模型服务地址、审批策略和 extra 配置文本。它把目标文件路径拼成 codex_home/config.toml,然后用 format 生成 TOML 内容,里面指定 mock-model、只读沙盒、mock provider、关闭重试等测试用设置,最后写入磁盘。输出是标准的写文件结果;成功时磁盘上多出配置文件,失败时返回 IO 错误。
调用关系:create_config_toml 会把基础情况转交给它;需要额外通知配置的 turn_start_notify_payload_includes_initialize_client_name 会直接调用它。它是这些测试能稳定启动 app server 的配置来源。
调用图:被 2 处调用(create_config_toml, turn_start_notify_payload_includes_initialize_client_name);外部调用 3 个(join, format!, write)。
toml_basic_string380–382 ↗
fn toml_basic_string(value: &str) -> String
作用:这个小工具把普通字符串包装成 TOML 基本字符串,并转义反斜杠和双引号。它主要用来安全地把命令路径、文件路径写进 config.toml。
数据流:它接收一个原始字符串。函数先把里面的反斜杠变成两个反斜杠,再把双引号加上转义,最后在两边补上双引号。输出是可以放进 TOML 配置里的字符串文本,不会因为路径里有特殊字符而把配置格式弄坏。
调用关系:它在通知捕获测试里被用来生成 notify 配置项。create_config_toml_with_extra 负责写整份配置,而 toml_basic_string 负责把其中的字符串值处理得安全可靠。
调用图:外部调用 1 个(format!)。
app-server/tests/suite/v2/config_rpc.rs源码 ↗
这个文件测试的是配置 RPC。RPC 可以简单理解成“让另一个进程帮我办事的请求”,这里用的是 JSON-RPC,也就是用 JSON 格式发请求、收结果。测试会临时创建一个假的 Codex 主目录,写入 config.toml 或 requirements.toml,然后启动一个测试版 app-server,向它发送“读配置”“写配置”“批量写配置”等请求。它重点确认几件事:最终生效的配置值对不对;每个值来自哪一层配置,比如用户配置、系统配置、项目配置;写配置时会不会覆盖正确文件;版本冲突和非法旧格式会不会被拒绝。没有这些测试,配置层级一旦改坏,用户可能看到错误模型、错误沙盒权限,甚至把设置写到不该写的地方。
write_config44–49 ↗
fn write_config(codex_home: &TempDir, contents: &str) -> Result<()>
作用:这是测试里的小帮手,用来把一段文字写成临时的 config.toml 配置文件。很多测试都先靠它摆好“测试现场”。
数据流:进去的是一个临时 Codex 主目录和配置文件内容 → 它把内容写到这个目录下的 config.toml → 出来的是成功或失败结果,同时磁盘上多了或更新了这个配置文件。
调用关系:它被大量配置读写测试调用。测试先用它准备用户配置,再启动 TestAppServer,让后面的 RPC 请求读到这些内容。
调用图:被 17 处调用(config_batch_write_applies_multiple_edits, config_batch_write_rejects_legacy_profile_tables, config_batch_write_updates_multiple_desktop_settings, config_read_accepts_forced_chatgpt_workspace_id_list, config_read_accepts_legacy_forced_chatgpt_workspace_id, config_read_after_pipelined_write_sees_written_value, config_read_ignores_bool_web_search_tool_config, config_read_includes_apps, config_read_includes_desktop_settings, config_read_includes_nested_web_search_tool_config (+7 more));外部调用 2 个(path, write)。
config_requirements_read_includes_allow_remote_control52–76 ↗
async fn config_requirements_read_includes_allow_remote_control() -> Result<()>
作用:这个测试确认服务器能读出 requirements.toml 里的 allow_remote_control 设置。这个设置关系到是否允许远程控制,读错会影响安全边界。
数据流:进去的是一个临时目录,里面写了 allow_remote_control = false → 测试启动服务器并发送“读取要求配置”的请求 → 出来的是响应对象,测试检查 allow_remote_control 确实是 false。
调用关系:它由异步测试运行器启动,自己创建 TestAppServer,初始化后发起配置要求读取请求,再用 to_response 把 JSON-RPC 响应转成强类型结果来检查。
调用图:调用 1 个内部函数(new);外部调用 6 个(new, Integer, to_response, assert_eq!, write, timeout)。
config_read_returns_effective_and_layers79–123 ↗
async fn config_read_returns_effective_and_layers() -> Result<()>
作用:这个测试确认普通配置读取不仅返回最终生效值,还能返回这些值来自哪些配置层。这样用户界面才能告诉用户“这个设置是谁定的”。
数据流:进去的是写有 model 和 sandbox_mode 的临时 config.toml → 服务器读取配置并要求带上 layers → 出来的是最终配置、来源信息和层列表,测试检查 model 生效且来源是用户文件。
调用关系:它先调用 write_config 准备文件,再通过 TestAppServer 发读配置请求,最后把层级顺序交给 assert_layers_user_then_optional_system 检查。
调用图:调用 4 个内部函数(new, assert_layers_user_then_optional_system, write_config, try_from);外部调用 5 个(new, Integer, to_response, assert_eq!, timeout)。
config_read_includes_tools126–196 ↗
async fn config_read_includes_tools() -> Result<()>
作用:这个测试确认 tools.web_search 这种工具配置能被正确读出来。它避免“网页搜索工具的细项写了但服务器看不见”的问题。
数据流:进去的是包含 web_search context_size 和 allowed_domains 的配置文件 → 服务器读取后解析成工具配置结构 → 出来的是包含 Low 上下文大小和 example.com 白名单的结果,并带有每个字段的来源。
调用关系:它使用 write_config 准备工具配置,向 TestAppServer 发读请求,然后用 assert_layers_user_then_optional_system 继续确认配置层级顺序没乱。
调用图:调用 4 个内部函数(new, assert_layers_user_then_optional_system, write_config, try_from);外部调用 5 个(new, Integer, to_response, assert_eq!, timeout)。
config_read_accepts_legacy_forced_chatgpt_workspace_id199–234 ↗
async fn config_read_accepts_legacy_forced_chatgpt_workspace_id() -> Result<()>
作用:这个测试确认旧写法 forced_chatgpt_workspace_id = "一个值" 仍然能被接受。这样老用户升级后,原来的配置不会突然失效。
数据流:进去的是一个只写了单个 workspace id 的配置文件 → 服务器读取并兼容旧格式 → 出来的是 Single 形式的 workspace id,测试确认值没有丢。
调用关系:它由测试运行器执行,调用 write_config 写旧格式配置,再通过 TestAppServer 读取配置并检查兼容解析结果。
调用图:调用 2 个内部函数(new, write_config);外部调用 6 个(new, Integer, to_response, assert_eq!, format!, timeout)。
config_read_accepts_forced_chatgpt_workspace_id_list237–276 ↗
async fn config_read_accepts_forced_chatgpt_workspace_id_list() -> Result<()>
作用:这个测试确认 forced_chatgpt_workspace_id 也支持列表形式。这样一个配置可以指定多个 ChatGPT 工作区 ID。
数据流:进去的是包含两个 workspace id 的数组配置 → 服务器读取并解析成多个 ID → 出来的是 Multiple 列表,测试检查两个 ID 顺序和值都正确。
调用关系:它和旧格式测试互补:同样通过 write_config 和 TestAppServer 走完整配置读取流程,但检查的是新列表格式。
调用图:调用 2 个内部函数(new, write_config);外部调用 6 个(new, Integer, to_response, assert_eq!, format!, timeout)。
config_read_includes_nested_web_search_tool_config279–324 ↗
async fn config_read_includes_nested_web_search_tool_config() -> Result<()>
作用:这个测试确认更复杂的网页搜索配置也能读出来,包括搜索上下文大小、允许域名和地理位置。它防止嵌套配置被解析漏掉。
数据流:进去的是带有 tools.web_search 子表和 location 对象的配置 → 服务器把 TOML 配置解析成结构化工具设置 → 出来的是 High 上下文、example.com 域名和美国纽约时区等位置字段。
调用关系:它调用 write_config 准备嵌套配置,启动 TestAppServer 后发读请求,最后直接检查返回的 WebSearchToolConfig。
调用图:调用 2 个内部函数(new, write_config);外部调用 5 个(new, Integer, to_response, assert_eq!, timeout)。
config_read_ignores_bool_web_search_tool_config327–356 ↗
async fn config_read_ignores_bool_web_search_tool_config() -> Result<()>
作用:这个测试确认旧的或错误的布尔写法 tools.web_search = true 不会被当成新的详细配置。这样可以避免把类型不对的配置误读成有效设置。
数据流:进去的是 web_search 被写成 true 的配置文件 → 服务器读取时识别它不是期望的详细对象 → 出来的是 web_search 为 None,表示没有可用的新式配置。
调用关系:它用 write_config 制造这个边界情况,通过 TestAppServer 读取,再检查服务器选择忽略而不是误解析。
调用图:调用 2 个内部函数(new, write_config);外部调用 5 个(new, Integer, to_response, assert_eq!, timeout)。
config_read_includes_apps359–472 ↗
async fn config_read_includes_apps() -> Result<()>
作用:这个测试确认 apps 配置能正确读取,包括默认应用设置和单个应用 app1 的设置。它保护的是多应用场景下的权限和默认行为。
数据流:进去的是包含 apps._default 和 apps.app1 的配置 → 服务器解析出默认配置、单应用配置以及字段来源 → 出来的是 AppsConfig,其中 app1 被禁用、审批人和工具审批模式都符合文件内容。
调用关系:它调用 write_config 准备应用配置,读回后检查多个字段来源,最后用 assert_layers_user_then_optional_system 确认用户层和系统层顺序。
调用图:调用 4 个内部函数(new, assert_layers_user_then_optional_system, write_config, try_from);外部调用 5 个(new, Integer, to_response, assert_eq!, timeout)。
config_read_includes_desktop_settings475–518 ↗
async fn config_read_includes_desktop_settings() -> Result<()>
作用:这个测试确认桌面端专用设置会被保留下来并返回。桌面设置可能不是核心配置类型,但前端仍需要原样读到它们。
数据流:进去的是 desktop 表,里面有主题、头像 ID 和工作区宽度等设置 → 服务器读取配置时把这些值作为 JSON 样的数据带回 → 出来的是 desktop 字段,测试逐项确认内容正确。
调用关系:它通过 write_config 写桌面设置,再让 TestAppServer 执行配置读取,用返回结果验证桌面端数据没有被过滤掉。
调用图:调用 2 个内部函数(new, write_config);外部调用 5 个(new, Integer, to_response, assert_eq!, timeout)。
config_read_includes_project_layers_for_cwd521–564 ↗
async fn config_read_includes_project_layers_for_cwd() -> Result<()>
作用:这个测试确认当读取配置时传入当前工作目录 cwd,服务器会把该项目的 .codex/config.toml 也算进去。这样不同项目可以有自己的配置。
数据流:进去的是用户配置、项目目录里的 .codex/config.toml,以及一个“项目可信”的记录 → 服务器按 cwd 找到项目配置层并合并 → 出来的是项目里设置的 model_reasoning_effort 生效,来源标成 Project。
调用关系:它先用 write_config 写用户配置,再调用 set_project_trust_level 标记项目可信,然后通过 TestAppServer 读带 cwd 的配置。
调用图:调用 4 个内部函数(new, write_config, set_project_trust_level, try_from);外部调用 7 个(new, Integer, to_response, assert_eq!, create_dir_all, write, timeout)。
config_read_includes_system_layer_and_overrides567–690 ↗
async fn config_read_includes_system_layer_and_overrides() -> Result<()>
作用:这个测试确认托管配置文件可以覆盖用户配置,同时没有被覆盖的用户配置仍然保留。它很重要,因为企业或系统管理员可能要强制指定某些设置。
数据流:进去的是用户 config.toml 和一个通过环境变量指定的 managed_config.toml → 服务器读取两层配置并按优先级合并 → 出来的是 model 和 approval_policy 来自托管文件,sandbox_mode 和 network_access 仍来自用户文件,层级顺序也被检查。
调用关系:它调用 write_config 写用户配置,再用 TestAppServer::new_with_env 带上托管配置路径启动服务器,最后把层级检查交给 assert_layers_managed_user_then_optional_system。
调用图:调用 4 个内部函数(new_with_env, assert_layers_managed_user_then_optional_system, write_config, try_from);外部调用 9 个(new, Integer, test_path_buf_with_windows, to_response, assert!, assert_eq!, format!, write, timeout)。
config_value_write_replaces_value693–756 ↗
async fn config_value_write_replaces_value() -> Result<()>
作用:这个测试确认单个配置值可以被替换写入。它还检查服务器返回的写入文件路径和版本控制信息是否合理。
数据流:进去的是 model = gpt-old 的配置文件 → 测试先读出当前版本,再发送把 model 改成 gpt-new 的写请求 → 出来的是写入成功响应,随后再次读取能看到 gpt-new。
调用关系:它先用 write_config 准备旧值,通过 TestAppServer 先读再写再读;中间用 resolve_path_against_base 验证写到的确是默认 config.toml。
调用图:调用 3 个内部函数(new, write_config, resolve_path_against_base);外部调用 7 个(new, Integer, to_response, assert!, assert_eq!, json!, timeout)。
config_value_write_updates_desktop_settings759–800 ↗
async fn config_value_write_updates_desktop_settings() -> Result<()>
作用:这个测试确认单值写入也能更新 desktop 下面的桌面端设置。它保证前端改主题这类操作可以落到配置文件里。
数据流:进去的是空配置文件和一个写入 desktop.appearanceTheme = dark 的请求 → 服务器把这个嵌套字段写进配置 → 出来的是写入成功,随后读取时 desktop.appearanceTheme 是 dark。
调用关系:它用 write_config 创建空配置,调用 TestAppServer 的单值写接口,再走一次读配置接口验证结果。
调用图:调用 2 个内部函数(new, write_config);外部调用 6 个(new, Integer, to_response, assert_eq!, json!, timeout)。
config_read_after_pipelined_write_sees_written_value803–849 ↗
async fn config_read_after_pipelined_write_sees_written_value() -> Result<()>
作用:这个测试确认如果写请求和读请求几乎连续发出,后面的读请求能看到刚写进去的值。它防止请求排队或并发时读到旧配置。
数据流:进去的是 model = gpt-old 的文件 → 测试先发送写 model = gpt-new,再立刻发送读请求 → 出来的是写成功,并且读响应里的 model 已经是 gpt-new。
调用关系:它通过 TestAppServer 模拟“流水线式”连续请求,检查服务器内部处理顺序能保证写后读一致。
调用图:调用 2 个内部函数(new, write_config);外部调用 6 个(new, Integer, to_response, assert_eq!, json!, timeout)。
config_value_write_rejects_version_conflict852–888 ↗
async fn config_value_write_rejects_version_conflict() -> Result<()>
作用:这个测试确认当客户端拿着过期版本号去写配置时,服务器会拒绝。这样可以避免两个人或两个界面同时改配置时互相覆盖。
数据流:进去的是一个已有 model 的配置文件,以及 expected_version = sha256:stale 的写请求 → 服务器发现版本不匹配 → 出来的是 JSON-RPC 错误,错误码是 configVersionConflict。
调用关系:它用 write_config 准备文件,通过 TestAppServer 发单值写请求,但这次期待的是错误响应而不是成功响应。
调用图:调用 2 个内部函数(new, write_config);外部调用 5 个(new, Integer, assert_eq!, json!, timeout)。
config_batch_write_applies_multiple_edits891–954 ↗
async fn config_batch_write_applies_multiple_edits() -> Result<()>
作用:这个测试确认批量写入可以一次应用多个配置改动。比如同时打开 workspace-write 沙盒,并写入沙盒的详细限制。
数据流:进去的是空配置文件和两个编辑项:sandbox_mode、sandbox_workspace_write → 服务器一次性写入这些改动 → 出来的是写入成功,随后读取能看到沙盒模式、可写目录和网络开关都正确。
调用关系:它调用 write_config 准备空文件,通过 TestAppServer 发批量写请求,再读回配置确认多个字段一起生效。
调用图:调用 3 个内部函数(new, write_config, resolve_path_against_base);外部调用 8 个(new, Integer, test_tmp_path_buf, to_response, assert!, assert_eq!, timeout, vec!)。
config_batch_write_rejects_legacy_profile_tables957–1016 ↗
async fn config_batch_write_rejects_legacy_profile_tables() -> Result<()>
作用:这个测试确认批量写入不会允许修改旧式 profiles 表。它防止新写入接口把已经不该用的旧配置结构继续扩大。
数据流:进去的是已有 profiles."team.prod" 的配置文件,以及试图改 profiles 和新增 items 的批量编辑 → 服务器校验失败并拒绝整批写入 → 出来的是 configValidationError,原文件里的旧 model 没变,也没有新增 items。
调用关系:它用 write_config 制造旧格式配置,通过 TestAppServer 发批量写请求并期待错误;随后直接读磁盘文件,确认失败时没有半写入。
调用图:调用 2 个内部函数(new, write_config);外部调用 8 个(new, Integer, assert!, assert_eq!, read_to_string, timeout, from_str, vec!)。
config_batch_write_updates_multiple_desktop_settings1019–1080 ↗
async fn config_batch_write_updates_multiple_desktop_settings() -> Result<()>
作用:这个测试确认批量写入可以一次更新多个 desktop 设置。它对应桌面端一次保存多个偏好项的场景。
数据流:进去的是空配置文件和两个 desktop 编辑项:头像 ID、workspace 对象 → 服务器把两项都写入配置文件 → 出来的是写入成功,读回时两个桌面设置都存在且值正确。
调用关系:它走的是 TestAppServer 的批量写接口,之后再用读配置接口验证,和单值桌面写入测试一起覆盖两种写法。
调用图:调用 2 个内部函数(new, write_config);外部调用 6 个(new, Integer, to_response, assert_eq!, timeout, vec!)。
assert_layers_user_then_optional_system1082–1106 ↗
fn assert_layers_user_then_optional_system(
layers: &[codex_app_server_protocol::ConfigLayer],
user_file: AbsolutePathBuf,
) -> Result<()>
作用:这是一个断言帮手,用来检查配置层顺序是不是“用户配置,然后系统配置”,前面可能还有一个由设备管理带来的托管层。层顺序错了,最终覆盖规则就会错。
数据流:进去的是服务器返回的层列表和预期用户配置文件路径 → 它先跳过可能存在的 MDM 托管层,再检查列表长度、用户层位置和系统层位置 → 出来的是测试通过或断言失败。
调用关系:它被多个读取配置的测试调用,用来复用同一套层级顺序检查逻辑,避免每个测试重复写这些判断。
调用图:被 3 处调用(config_read_includes_apps, config_read_includes_tools, config_read_returns_effective_and_layers);外部调用 3 个(assert!, assert_eq!, matches!)。
assert_layers_managed_user_then_optional_system1108–1137 ↗
fn assert_layers_managed_user_then_optional_system(
layers: &[codex_app_server_protocol::ConfigLayer],
managed_file: AbsolutePathBuf,
user_file: AbsolutePathBuf,
) -> Result<()>
作用:这是另一个层级顺序检查帮手,专门用于有托管配置文件的场景。它确认顺序是“托管文件、用户配置、系统配置”,前面同样可能有 MDM 层。
数据流:进去的是层列表、托管配置文件路径和用户配置文件路径 → 它跳过可选 MDM 层后,逐层核对托管、用户、系统的位置 → 出来的是测试通过或断言失败。
调用关系:它被 config_read_includes_system_layer_and_overrides 调用,帮助那个测试确认托管配置确实排在用户配置前面,从而能覆盖用户值。
调用图:被 1 处调用(config_read_includes_system_layer_and_overrides);外部调用 3 个(assert!, assert_eq!, matches!)。
app-server/tests/suite/v2/experimental_feature_list.rs源码 ↗
这个测试文件像一套验收清单,模拟真实用户启动应用服务器、读取实验功能、修改功能开关、再读取配置确认结果。这里的“实验功能”就是还没完全稳定、可能需要单独打开或关闭的功能。测试会搭临时目录当作用户的配置目录,避免污染真实机器;也会搭假的网络服务,模拟 ChatGPT 后端返回团队策略。它重点检查几件事:功能列表要带上名称、阶段、说明和默认开关;团队策略能把 apps、plugins 这类功能禁用;某个线程如果属于一个项目,要能读到项目里的 .codex/config.toml;不存在的线程 ID 要报清楚的错;设置功能开关时,只能改允许远程控制的功能,不能覆盖用户自己写在配置文件里的决定。最后几个小 helper 把“发请求、等响应、转成具体结果”包起来,让每个测试只关心行为本身。
experimental_feature_list_returns_feature_metadata_with_stage43–102 ↗
async fn experimental_feature_list_returns_feature_metadata_with_stage() -> Result<()>
作用:这个测试确认服务器返回的实验功能列表是完整的:每个功能都要有名字、阶段、默认是否开启,以及该显示给用户看的说明。没有这个保障,界面可能会显示错功能,或者把开发中、已稳定、已废弃的功能混在一起。
数据流:进去的是一个空的临时配置目录和默认配置构造器;测试启动一个测试服务器,发出“列出实验功能”的请求;然后把服务器返回的数据和代码里登记的 FEATURES 功能清单逐项对比,确认阶段、文案、当前开关和默认开关都一致。出来的结果是测试通过,或者在任何字段不一致时失败。
调用关系:它在测试流程一开始验证最基础的列表接口。它会创建临时环境,使用测试服务器的初始化流程,并借助超时保护避免测试卡死;最后用断言把服务器实际响应和预期响应对齐。
调用图:调用 2 个内部函数(new, with_managed_config_path_for_tests);外部调用 5 个(new, default, assert_eq!, default, timeout)。
experimental_feature_list_marks_apps_and_plugins_disabled_by_workspace_policy105–160 ↗
async fn experimental_feature_list_marks_apps_and_plugins_disabled_by_workspace_policy() -> Result<()>
作用:这个测试确认团队或工作区策略可以强制关掉 apps 和 plugins,即使这些功能默认是开启的。它防止企业策略失效,避免用户看到或使用被组织禁用的功能。
数据流:进去的是临时用户目录、假的 ChatGPT 后端地址、假的登录凭据和一条模拟的账号设置接口返回;测试服务器启动后会去读取这份账号策略;测试再请求实验功能列表,检查 apps 和 plugins 的 enabled 变成 false,但 default_enabled 仍然保持 true。也就是说,默认值没变,只是被策略临时压住了。
调用关系:它把 wiremock 这类假服务器当成“假后端”,先准备团队策略响应,再启动 TestAppServer。服务器初始化和功能列表请求发生后,测试读取响应并只关注 apps、plugins 两项,确认远端策略真的参与了最终判断。
调用图:调用 2 个内部函数(new, new_without_managed_config);外部调用 13 个(given, start, new, new, default, write_chatgpt_auth, assert!, format!, write, timeout (+3 more))。
experimental_feature_list_resolves_thread_project_config163–227 ↗
async fn experimental_feature_list_resolves_thread_project_config() -> Result<()>
作用:这个测试确认功能列表在指定线程时,会使用这个线程所在项目的配置。也就是说,同一个用户在不同项目里看到的实验功能开关可以不同。
数据流:进去的是一个假的模型服务、一个临时用户目录、一个临时工作区,以及工作区里的 .codex/config.toml;测试先把 memories 功能写成 true,再启动服务器并在这个工作区里创建线程;随后带着 thread_id 请求实验功能列表,最后检查 memories 是否确实开启。输出是确认项目级配置被正确读到了。
调用关系:它先用 create_mock_responses_server_repeating_assistant 搭一个会固定回复的假模型服务,再用 TestAppServer 启动应用服务器。创建线程后,列表请求把 thread_id 带回服务器,让服务器能沿着线程找到项目目录和项目配置。
调用图:调用 1 个内部函数(new_without_managed_config);外部调用 8 个(default, new, create_mock_responses_server_repeating_assistant, assert!, format!, create_dir_all, write, timeout)。
experimental_feature_list_rejects_unknown_thread_id230–258 ↗
async fn experimental_feature_list_rejects_unknown_thread_id() -> Result<()>
作用:这个测试确认如果客户端拿一个不存在的线程 ID 来问实验功能列表,服务器不会假装成功,而是返回明确错误。这样调用方能知道是自己传错了线程,而不是拿到一份误导性的配置。
数据流:进去的是一个临时用户目录和一个固定写死、并不存在的线程 ID;测试启动服务器后发出带 thread_id 的功能列表请求;服务器返回 JSON-RPC 错误,测试检查错误码是 -32600,并且错误信息里包含这个找不到的线程 ID。出来的是一次被正确拒绝的请求。
调用关系:它使用 TestAppServer 建立最小服务器环境,再通过超时等待错误响应。这个测试站在接口边界上,专门验证“坏输入”不会被静默吞掉。
调用图:调用 1 个内部函数(new);外部调用 5 个(new, Integer, assert!, assert_eq!, timeout)。
experimental_feature_enablement_set_applies_to_global_and_thread_config_reads261–295 ↗
async fn experimental_feature_enablement_set_applies_to_global_and_thread_config_reads() -> Result<()>
作用:这个测试确认通过接口设置的实验功能开关,会反映到后续读取配置的结果里。不管读取的是全局配置,还是带项目目录的配置,都应该能看到这个由接口写入的开关。
数据流:进去的是临时用户目录、一个临时项目目录,以及想设置的 auth_elicitation = true;测试调用 set_experimental_feature_enablement 发出设置请求,拿到服务器回显;然后分别读取不带 cwd 和带 cwd 的配置,检查 features.auth_elicitation 都是 true。出来的是确认设置能被配置读取接口看见。
调用关系:它是多个设置类测试中的基础用例。它把真正发设置请求的细节交给 set_experimental_feature_enablement,把读取配置的细节交给 read_config,自己只检查“设置以后读出来是否一致”。
调用图:调用 3 个内部函数(new, read_config, set_experimental_feature_enablement);外部调用 5 个(from, new, assert_eq!, create_dir_all, timeout)。
experimental_feature_enablement_set_does_not_override_user_config298–330 ↗
async fn experimental_feature_enablement_set_does_not_override_user_config() -> Result<()>
作用:这个测试确认接口设置的开关不会盖掉用户手写在 config.toml 里的明确选择。用户配置优先,这能避免服务器把用户已经关掉的功能偷偷打开。
数据流:进去的是一份用户配置文件,里面写着 memories = false,以及接口请求里想把 memories 设置为 true;测试启动服务器后发设置请求,服务器会回显它收到了 true;但再读取配置时,最终结果仍然是用户文件里的 false。出来的是确认“用户自己写的配置”优先级更高。
调用关系:它先直接往临时目录写 config.toml,再通过 set_experimental_feature_enablement 模拟接口设置,最后通过 read_config 看最终配置。这个测试和上一条形成对照:接口设置能生效,但不能压过用户明示配置。
调用图:调用 3 个内部函数(new, read_config, set_experimental_feature_enablement);外部调用 5 个(from, new, assert_eq!, write, timeout)。
experimental_feature_enablement_set_only_updates_named_features333–405 ↗
async fn experimental_feature_enablement_set_only_updates_named_features() -> Result<()>
作用:这个测试确认每次设置功能开关时,只改请求里点名的功能,不会顺手清掉以前设置过的其他功能。这样多次局部更新才安全。
数据流:进去的是两轮设置:第一轮把 mentions_v2 打开,第二轮只提交 auth_elicitation、memories、remote_plugin 和 tool_suggest;测试检查第二轮响应只包含第二轮点名的功能;再读配置,确认第一轮的 mentions_v2 还在,同时第二轮的几个值也都保存正确。出来的是一份累计后的配置状态。
调用关系:它连续调用 set_experimental_feature_enablement 两次,再用 read_config 做最终核对。这个测试保证设置接口像“只改表格里被圈出来的几格”,而不是每次把整张表重写。
调用图:调用 3 个内部函数(new, read_config, set_experimental_feature_enablement);外部调用 4 个(from, new, assert_eq!, timeout)。
experimental_feature_enablement_set_allows_remote_control408–423 ↗
async fn experimental_feature_enablement_set_allows_remote_control() -> Result<()>
作用:这个测试确认 remote_control 这个功能允许通过该接口被设置。它防止合法的远程控制开关被误判成不能改的功能。
数据流:进去的是 remote_control = false 这一项设置;测试启动服务器,发送设置请求;服务器返回同样的 enablement 映射,测试确认没有被过滤掉。出来的是一次成功保存的远程控制开关请求。
调用关系:它把请求发送交给 set_experimental_feature_enablement,自己只检查返回值。它是“哪些功能允许被接口控制”这组规则里的正向用例。
调用图:调用 2 个内部函数(new, set_experimental_feature_enablement);外部调用 4 个(from, new, assert_eq!, timeout)。
experimental_feature_enablement_set_empty_map_is_no_op426–456 ↗
async fn experimental_feature_enablement_set_empty_map_is_no_op() -> Result<()>
作用:这个测试确认提交空的功能开关表不会造成任何改动。空请求应该像“什么都没说”,不能把已有设置清空。
数据流:进去的第一步是把 mentions_v2 设置为 true;第二步提交一个空的 BTreeMap,也就是没有任何功能名;服务器返回空结果;最后读取配置,确认 mentions_v2 仍然是 true。出来的是证明空更新不会破坏原状态。
调用关系:它先用 set_experimental_feature_enablement 建立一个已有状态,再用同一个 helper 发空更新,最后用 read_config 验证状态还在。这个测试保护的是接口的“不操作”语义。
调用图:调用 3 个内部函数(new, read_config, set_experimental_feature_enablement);外部调用 5 个(from, new, new, assert_eq!, timeout)。
experimental_feature_enablement_set_ignores_invalid_features459–486 ↗
async fn experimental_feature_enablement_set_ignores_invalid_features() -> Result<()>
作用:这个测试确认设置接口会忽略不允许通过它修改的功能名,以及完全不存在的功能名。这样客户端乱传一堆字段时,服务器不会把不该改的东西写进去。
数据流:进去的是一张包含 apps、connectors、plugins、unknown_feature 等多项的开关表,其中只有 auth_elicitation 是允许被这个接口接收的;测试发送请求后,检查服务器返回的 enablement 只剩 auth_elicitation = true。出来的是一份被过滤后的结果。
调用关系:它通过 set_experimental_feature_enablement 发起设置请求,用断言验证过滤规则。它和 remote_control 的正向测试配合起来,说明接口既有允许列表,也会拒绝不合规项目。
调用图:调用 2 个内部函数(new, set_experimental_feature_enablement);外部调用 4 个(from, new, assert_eq!, timeout)。
set_experimental_feature_enablement488–498 ↗
async fn set_experimental_feature_enablement(
mcp: &mut TestAppServer,
enablement: BTreeMap<String, bool>,
) -> Result<ExperimentalFeatureEnablementSetResponse>
作用:这是测试里的小帮手,用来向测试服务器发送“设置实验功能开关”的请求,并把返回结果转成好用的结构。它让多个测试不用重复写发请求和读响应的样板代码。
数据流:进去的是一个可变的 TestAppServer 和一张功能名到开关值的表;它把这张表包进 ExperimentalFeatureEnablementSetParams,发给服务器,拿到 request_id;然后调用 read_response 等服务器回信,并把回信解析成 ExperimentalFeatureEnablementSetResponse。出来的是服务器确认接受的那部分开关表。
调用关系:它被六个设置类测试调用:包括检查全局和项目配置读取、用户配置优先、只更新点名功能、允许 remote_control、空更新无操作、忽略非法功能。它自己把“等待并解析响应”的活交给 read_response。
调用图:调用 2 个内部函数(send_experimental_feature_enablement_set_request, read_response);被 6 处调用(experimental_feature_enablement_set_allows_remote_control, experimental_feature_enablement_set_applies_to_global_and_thread_config_reads, experimental_feature_enablement_set_does_not_override_user_config, experimental_feature_enablement_set_empty_map_is_no_op, experimental_feature_enablement_set_ignores_invalid_features, experimental_feature_enablement_set_only_updates_named_features)。
read_config500–508 ↗
async fn read_config(mcp: &mut TestAppServer, cwd: Option<String>) -> Result<ConfigReadResponse>
作用:这是测试里的读配置帮手,用来向服务器发“读取配置”的请求。测试需要确认某个功能开关最后到底以什么值出现在配置里,就会用它。
数据流:进去的是一个 TestAppServer 和可选的 cwd,也就是当前项目目录;它发送 ConfigReadParams,其中 include_layers 为 false,表示只要最终合成后的配置,不要每一层来源;拿到 request_id 后调用 read_response 等待并解析。出来的是 ConfigReadResponse,里面有测试要检查的 config。
调用关系:它被多个设置类测试调用,用来验证设置后的最终效果。它不自己解析底层消息,而是把通用的“等响应并转换类型”交给 read_response。
调用图:调用 2 个内部函数(send_config_read_request, read_response);被 4 处调用(experimental_feature_enablement_set_applies_to_global_and_thread_config_reads, experimental_feature_enablement_set_does_not_override_user_config, experimental_feature_enablement_set_empty_map_is_no_op, experimental_feature_enablement_set_only_updates_named_features)。
read_response510–517 ↗
async fn read_response(mcp: &mut TestAppServer, request_id: i64) -> Result<T>
作用:这是最底层的响应读取帮手:给它一个请求编号,它就等服务器发回对应响应,并把通用 JSON-RPC 响应转成测试需要的具体类型。JSON-RPC 可以理解成一种用 JSON 包装请求和回复的通信格式。
数据流:进去的是 TestAppServer 和 request_id;它把 request_id 包成 RequestId::Integer,然后在默认 30 秒超时时间内等待服务器流里出现对应响应;拿到 JSONRPCResponse 后交给 to_response 反序列化,也就是把 JSON 数据变成 Rust 里的具体结构。出来的是调用方指定的类型 T。
调用关系:它被 set_experimental_feature_enablement 和 read_config 调用,是这些测试共享的“收信员”。有了它,上层测试不用关心流式读取、请求编号匹配、超时和 JSON 转结构这些细节。
调用图:调用 1 个内部函数(read_stream_until_response_message);被 2 处调用(read_config, set_experimental_feature_enablement);外部调用 3 个(Integer, to_response, timeout)。
app-server/tests/suite/v2/model_provider_capabilities_read.rs源码 ↗
这个测试文件像是在替用户问服务器一句:“你现在接的模型供应商,支持工具命名空间、图片生成、网页搜索吗?”它会启动一个临时的测试服务器,并用临时目录当作配置家目录,避免污染真实用户环境。第一个测试不写配置,检查默认供应商应该三个能力都支持。第二个测试先写入一个 config.toml,把供应商改成 amazon-bedrock,再检查图片生成和网页搜索会被关掉。测试里用超时保护,防止服务器没回应时一直卡住;也把服务器返回的 JSON-RPC 响应转成具体结果,再和预期值比较。
read_default_provider_capabilities17–39 ↗
async fn read_default_provider_capabilities() -> Result<()>
作用:这个测试检查在没有特别配置模型供应商时,服务器返回的默认能力是否正确。它证明默认情况下工具命名空间、图片生成和网页搜索都应该是可用的。
数据流:进去的是一个新建的临时目录,里面没有额外配置;测试用它启动 TestAppServer,并完成初始化。之后它发送一次读取模型供应商能力的请求,等待指定请求编号对应的 JSON-RPC 响应,把响应转换成 ModelProviderCapabilitiesReadResponse。最后它把收到的三个布尔值和预期结果比较;如果不一致,测试失败;如果一致,就返回成功。
调用关系:它在测试运行器执行异步测试时被调用。它先通过 TestAppServer::new 搭起临时服务器,再用 timeout 给初始化和读响应加上最长等待时间;收到响应后交给 to_response 做格式转换,最后用 assert_eq! 判断服务器行为是否符合默认供应商的约定。
调用图:调用 1 个内部函数(new);外部调用 5 个(new, Integer, to_response, assert_eq!, timeout)。
read_amazon_bedrock_provider_capabilities42–69 ↗
async fn read_amazon_bedrock_provider_capabilities() -> Result<()>
作用:这个测试检查当配置里的模型供应商是 amazon-bedrock 时,服务器是否返回 Bedrock 对应的能力限制。它保证系统不会误把图片生成和网页搜索说成可用。
数据流:进去的是一个新建的临时目录;测试先在里面写入 config.toml,把 model_provider 设置成 amazon-bedrock。然后它启动并初始化 TestAppServer,发送读取能力的请求,等待对应请求编号的 JSON-RPC 响应,再把响应转成具体结构。出来的是一次断言结果:namespace_tools 应为 true,image_generation 和 web_search 应为 false;同时它改动了临时目录里的配置文件。
调用关系:它同样由测试运行器启动,但比默认场景多了一步 std::fs::write,用来提前准备配置。之后流程交给 TestAppServer::new 启动服务器,用 timeout 防卡死,用 RequestId::Integer 锁定要等的响应,用 to_response 解析结果,最后用 assert_eq! 验证 amazon-bedrock 这条配置路径是否生效。
调用图:调用 1 个内部函数(new);外部调用 6 个(new, Integer, to_response, assert_eq!, write, timeout)。
发现和列表 API
这些套件测试只读发现端点,用于枚举内置和已配置的服务器可见资源与预设。
app-server/tests/suite/v2/collaboration_mode_list.rs源码 ↗
这个测试像一个真实客户端一样启动 app-server,然后通过 MCP 测试工具去问服务器:“你支持哪些协作模式?”协作模式可以理解成几套预设的工作方式,比如不同的模型、推理力度等。测试先创建一个临时的 Codex 主目录,避免污染真实环境;再启动测试服务器并完成初始化;接着发送“列出协作模式”的请求,等待服务器回信。为了防止持续集成环境太慢导致测试卡死,它给启动和等待响应都加了 60 秒超时。拿到返回后,测试会把服务器给出的列表和代码里定义的内置默认预设逐项比较,包括名称、模式、模型和推理强度。这里不只是看“有没有数据”,还要求顺序稳定、内容完全一致,所以能很好地守住这个 API 的默认行为。
list_collaboration_modes_returns_presets29–59 ↗
async fn list_collaboration_modes_returns_presets() -> Result<()>
作用:这个测试函数检查服务器的协作模式列表接口是否按预期返回默认预设。有人改服务器接口、默认预设或返回顺序时,它能提醒开发者有没有破坏对外承诺。
数据流:进去的是一个临时目录和默认的列表请求参数;函数用临时目录启动测试服务器,初始化后发送请求,再读取指定请求编号对应的 JSON-RPC 响应。随后它把响应转换成协作模式列表,把本地内置预设整理成同样的格式,最后比较两边是否完全相同;如果相同就测试通过,如果不同就失败并显示差异。
调用关系:它是 Tokio 异步测试框架在跑测试时直接调用的测试入口。执行时先通过 TestAppServer::new 搭起测试服务器,再用 default 生成默认请求参数,用 timeout 包住初始化和读响应以免无限等待;拿到结果后,它调用 builtin_collaboration_mode_presets 取得标准答案,并用 assert_eq! 做最终核对。
调用图:调用 2 个内部函数(new, builtin_collaboration_mode_presets);外部调用 5 个(new, Integer, default, assert_eq!, timeout)。
app-server/tests/suite/v2/model_list.rs源码 ↗
这个测试文件像是在给“模型菜单”做验收。用户打开应用时,前端需要知道有哪些 AI 模型可以选;如果这里错了,用户可能看不到该用的模型,看到不该出现的模型,或者翻页时漏数据。文件先把内部的 ModelPreset(项目里的模型预设)转换成接口要返回的 Model,方便拿来和服务器真实返回值比较。然后它启动一个临时测试服务器,准备假的模型缓存或假的 ChatGPT 登录信息,再发送 model/list 请求。测试覆盖几件关键事:一次取很多时能拿到全部可见模型;include_hidden 打开时能看到隐藏模型;ChatGPT 模式下远端 /models 目录才是准绳;limit 很小时分页能一页页拿完;cursor(翻页用的书签)乱填时会返回标准错误。整体上,它保证“模型列表”这个入口既准确又稳定。
model_from_preset33–76 ↗
fn model_from_preset(preset: &ModelPreset) -> Model
作用:把项目内部的模型预设,改写成接口返回给客户端的模型格式。测试用它来造“应该返回什么”的标准答案。
数据流:进去的是一个 ModelPreset,也就是内部保存的一份模型说明;函数逐项取出模型编号、显示名、描述、升级信息、推理强度选项、服务档位等字段,并做少量格式转换;出来的是一个 Model,可以直接和 app-server 返回的 JSON-RPC 结果比较。它不会改原来的预设,只是做一份新对象。
调用关系:它是这些测试里的“翻译器”。expected_visible_models 会用它批量生成期望列表;远端模型目录测试也会用它把远端模型转成可比较的返回格式。这样测试不用手写一大串重复的模型字段。
expected_visible_models78–93 ↗
fn expected_visible_models() -> Vec<Model>
作用:算出在普通测试登录条件下,接口应该显示哪些模型。它相当于测试里的“正确答案生成器”。
数据流:它先读取测试用的全部模型预设;再按认证方式过滤掉当前不该出现的模型;接着标记哪个模型应该是默认模型;最后只留下 show_in_picker 为真的模型,并用 model_from_preset 转成接口返回格式。输出是一组可见 Model。
调用关系:它把 all_model_presets、filter_by_auth、mark_default_by_picker_visibility 这些底层规则串起来,模拟真实服务器挑模型的逻辑。list_models_returns_all_models_with_large_limit 和 list_models_pagination_works 会拿它的结果去检查服务器返回值是否一致。
调用图:调用 3 个内部函数(all_model_presets, filter_by_auth, mark_default_by_picker_visibility);被 2 处调用(list_models_pagination_works, list_models_returns_all_models_with_large_limit)。
list_models_returns_all_models_with_large_limit96–127 ↗
async fn list_models_returns_all_models_with_large_limit() -> Result<()>
作用:测试当客户端一次请求足够大的数量时,model/list 会返回全部可见模型,而且不再给下一页游标。
数据流:它创建一个临时目录当作假的 Codex 主目录,写入模型缓存,启动测试 app-server,并完成初始化;然后发送 limit=100、没有 cursor、不要隐藏模型的请求;服务器返回后,它把响应解成 ModelListResponse,和 expected_visible_models 算出的标准列表逐项比较,并确认 next_cursor 为空。
调用关系:这是最基础的成功路径测试。它先通过 TestAppServer 搭好服务器,再用 send_list_models_request 发请求,随后等 read_stream_until_response_message 收到对应请求编号的响应。expected_visible_models 提供期望结果,assert_eq 和 assert 用来判定是否通过。
调用图:调用 2 个内部函数(new, expected_visible_models);外部调用 6 个(new, Integer, write_models_cache, assert!, assert_eq!, timeout)。
list_models_uses_chatgpt_remote_catalog_as_source_of_truth162–270 ↗
async fn list_models_uses_chatgpt_remote_catalog_as_source_of_truth() -> Result<()>
作用:测试在 ChatGPT 登录模式下,服务器会相信远端 /models 接口返回的模型目录,而不是只用本地缓存或内置列表。
数据流:它先启动一个假的 HTTP 服务器,并挂上一次性的 /models 响应,里面只有一个远端专属模型;然后创建临时配置,把 openai_base_url 指向这个假服务器,并写入假的 ChatGPT 登录凭据;启动 app-server 时还清掉 OPENAI_API_KEY,确保走 ChatGPT 认证路径。接着发送 model/list 请求,把返回结果和由远端模型转换出的期望 Model 比较,最后确认假 /models 只被请求了一次。
调用关系:这是更接近真实网络场景的测试。MockServer 扮演 OpenAI/ChatGPT 后端,mount_models_once 准备远端模型目录,write_chatgpt_auth 准备登录状态,TestAppServer::new_with_env 启动带特定环境变量的服务器。服务器请求远端目录后,测试再用 mark_default_by_picker_visibility 和 model_from_preset 拼出标准答案来对照。
调用图:调用 4 个内部函数(new, new_with_env, mount_models_once, mark_default_by_picker_visibility);外部调用 12 个(start, new, Integer, write_chatgpt_auth, assert!, assert_eq!, format!, json!, from_value, write (+2 more))。
list_models_pagination_works273–319 ↗
async fn list_models_pagination_works() -> Result<()>
作用:测试 model/list 的分页能正常工作:每次只取 1 个,也能靠 next_cursor 一页页取完整个列表。
数据流:它先准备模型缓存并启动服务器,再用 expected_visible_models 得到完整标准列表;然后循环发送 limit=1 的请求,每次把上一次返回的 next_cursor 当作下一次请求的 cursor;每页都要求正好返回 1 个模型,并把这些模型累加起来。直到服务器不再给 next_cursor 时,它检查累加结果和完整标准列表完全一样;如果循环很多次还没结束,就直接让测试失败。
调用关系:它专门检查分页协议。send_list_models_request 每次发一页请求,read_stream_until_response_message 取回这一页;next_cursor 像书签一样把下一页接上。expected_visible_models 仍然提供最终应取到的完整列表。
调用图:调用 2 个内部函数(new, expected_visible_models);外部调用 7 个(new, new, Integer, write_models_cache, assert_eq!, panic!, timeout)。
list_models_rejects_invalid_cursor322–347 ↗
async fn list_models_rejects_invalid_cursor() -> Result<()>
作用:测试客户端传入错误的 cursor 时,服务器不会乱返回数据,而是给出明确的“无效请求”错误。
数据流:它准备模型缓存、启动并初始化测试服务器;然后发送一个 cursor="invalid" 的 model/list 请求;这次它等待的不是正常响应,而是错误消息。拿到错误后,它检查错误属于同一个请求编号,错误码是 JSON-RPC 的 invalid request 代码 -32600,错误文字也明确写着 invalid cursor: invalid。
调用关系:这是 model/list 的防呆测试。它通过 TestAppServer 发出坏请求,再用 read_stream_until_error_message 等服务器报错,确保错误路径也符合协议,不会让客户端收到含糊或错误格式的结果。
调用图:调用 1 个内部函数(new);外部调用 5 个(new, Integer, write_models_cache, assert_eq!, timeout)。
app-server/tests/suite/v2/permission_profile_list.rs源码 ↗
这个测试文件关心的是一个很实际的问题:客户端问服务器“现在有哪些权限方案可选?”时,服务器回答得对不对。它会临时造出假的用户配置目录和项目目录,写入测试用的 config.toml,然后启动一个 TestAppServer,也就是专门给测试用的应用服务器。测试会发起 permission profile list 请求,再检查返回结果是不是包含内置的三个权限方案,以及用户或项目自己配置的方案。它还检查两件容易出错的事:一是项目里的 .codex/config.toml 只有在项目被标记为可信时才会被发现;二是列表分页是否正确,比如每页只要 3 条时,下一页游标要接得上。最后的 read_response 是一个小帮手,负责等服务器回信并把 JSON-RPC 响应转成测试能比较的 Rust 数据。
permission_profile_list_returns_builtin_and_configured_profiles23–85 ↗
async fn permission_profile_list_returns_builtin_and_configured_profiles() -> Result<()>
作用:这个测试确认:服务器列权限配置档时,会同时返回系统内置的权限方案和用户在全局配置文件里写的自定义方案。这样可以防止用户配置被漏掉,或者内置安全选项消失。
数据流:它先创建一个临时的 codex home 目录,并写入 config.toml,里面定义了 dev 和 audit 两个权限方案。然后它启动测试服务器并完成初始化,发送一个不带分页、不指定当前目录的列表请求。最后它读回服务器响应,检查返回列表必须按预期包含三个内置方案,再加上 audit 和 dev 两个自定义方案,并且没有下一页。
调用关系:这是直接验证权限列表接口的测试入口之一。它用 TestAppServer::new 搭起测试服务器,用 timeout 防止测试一直卡住,发出请求后把读取和解析响应的工作交给 read_response,最后用 assert_eq! 比较实际结果和期望结果。
调用图:调用 1 个内部函数(new);外部调用 4 个(new, assert_eq!, write, timeout)。
permission_profile_list_resolves_project_profiles_and_paginates88–163 ↗
async fn permission_profile_list_resolves_project_profiles_and_paginates() -> Result<()>
作用:这个测试确认:当请求带上某个项目目录时,服务器能找到该项目自己的权限配置档,并且列表分页能正常工作。分页就像书本翻页,一次只拿一部分,后面用游标继续拿。
数据流:它先创建临时的用户目录和工作区目录,在用户配置里只设置默认权限,在项目的 .codex/config.toml 里定义 project 权限方案。接着它把这个项目标记为可信,这样服务器才应该读取项目配置。服务器启动后,测试先请求每页 3 条,应该只拿到三个内置方案,并得到 next_cursor 为 3;再用这个游标请求第二页,应该拿到 project 方案,并且 next_cursor 变成空,表示没有更多了。
调用关系:这个测试覆盖了“项目配置发现”和“分页”两个流程的配合。它会调用 set_project_trust_level 准备可信项目状态,用 create_dir_all 和 write 准备磁盘上的配置文件,然后通过测试服务器发两次列表请求,每次都用 read_response 收回并解析响应。
调用图:调用 2 个内部函数(new, set_project_trust_level);外部调用 5 个(new, assert_eq!, create_dir_all, write, timeout)。
permission_profile_list_discovers_project_profiles_without_default_selection166–221 ↗
async fn permission_profile_list_discovers_project_profiles_without_default_selection() -> Result<()>
作用:这个测试确认:即使全局配置里没有把项目权限方案设成默认值,服务器也仍然能发现项目目录里的权限方案。也就是说,“被列出来”和“被选为默认”不是一回事。
数据流:它创建一个临时工作区,在工作区的 .codex/config.toml 里写入 project 权限方案,但不写全局 default_permissions。随后它把这个工作区标记为可信,启动测试服务器,并发送带 cwd 的列表请求。服务器返回后,测试检查结果应包含三个内置方案和项目里的 project 方案,没有下一页。
调用关系:这个测试专门补上一个边界情况:项目权限方案不能因为没有被设为默认就被漏掉。它和前一个测试一样依赖 set_project_trust_level 让项目配置可读,依赖 TestAppServer 发请求,并把响应读取交给 read_response。
调用图:调用 2 个内部函数(new, set_project_trust_level);外部调用 5 个(new, assert_eq!, create_dir_all, write, timeout)。
read_response223–233 ↗
async fn read_response(
mcp: &mut TestAppServer,
request_id: i64,
) -> Result<T>
作用:这是测试里的通用小帮手,用来等服务器返回指定请求的响应,并把原始 JSON-RPC 响应转换成具体的返回类型。JSON-RPC 是一种用 JSON 格式表达请求和响应的通信约定。
数据流:它收到一个测试服务器对象和请求编号。它用这个编号包装成 RequestId,等待服务器消息流里出现对应响应,并加上超时时间,避免测试无限等待。拿到 JSONRPCResponse 后,它调用 to_response 把里面的数据反序列化成调用者想要的类型 T,最后把这个结构化结果交回去。
调用关系:它服务于本文件里的各个测试用例。测试函数负责搭环境和发请求;read_response 负责收信、匹配请求编号、转换数据。它内部调用 read_stream_until_response_message 读取服务器流,再调用 to_response 做类型转换。
调用图:调用 1 个内部函数(read_stream_until_response_message);外部调用 3 个(Integer, to_response, timeout)。
执行和环境 RPC
这些测试覆盖文件系统访问、进程执行和 Windows 沙箱设置的核心操作 RPC 界面。
app-server/tests/suite/v2/fs.rs源码 ↗
这个测试文件关心的是“外部客户端通过 app-server 操作本地文件”这条路是否可靠。它会先建一个临时文件夹,当成安全的测试世界,然后启动一个测试版服务器,通过 JSON-RPC(一种用 JSON 发请求、收响应的通信格式)发送 fs/readFile、fs/writeFile、fs/copy、fs/watch 等请求。测试不只看成功场景,也特意检查坏输入:比如相对路径、无效 base64、没开启本地文件系统、复制目录却没声明递归等。文件还覆盖一些容易出错的边角:符号链接、命名管道、原子替换文件、监听后取消监听。整体作用就像给“文件系统遥控器”做体检:按钮按下去后,真实磁盘、返回消息和错误提示都必须符合预期。
initialized_mcp39–43 ↗
async fn initialized_mcp(codex_home: &TempDir) -> Result<TestAppServer>
作用:启动一个测试用的 app-server,并等它完成初始化。很多测试都先调用它,因为服务器没准备好就发文件请求,会让测试结果不可靠。
数据流:输入一个临时的 codex_home 目录 → 用这个目录创建 TestAppServer,再在规定时间内等待 initialize 完成 → 输出一个已经能接收请求的测试服务器对象。
调用关系:它是大多数测试的开场步骤。各个 fs 测试先找它拿到可用服务器,之后再调用服务器对象发送读写、复制、监听等请求。
调用图:调用 1 个内部函数(new);被 15 处调用(fs_copy_ignores_unknown_special_files_in_recursive_copy, fs_copy_preserves_symlinks_in_recursive_copy, fs_copy_rejects_copying_directory_into_descendant, fs_copy_rejects_directory_without_recursive, fs_copy_rejects_standalone_fifo_source, fs_get_metadata_reports_symlink, fs_get_metadata_returns_only_used_fields, fs_methods_cover_current_fs_utils_surface, fs_methods_reject_relative_paths, fs_watch_allows_missing_file_targets (+5 more));外部调用 2 个(path, timeout)。
expect_error_message45–57 ↗
async fn expect_error_message(
mcp: &mut TestAppServer,
request_id: i64,
expected_message: &str,
) -> Result<()>
作用:等待某个请求返回错误,并确认错误文字正好是预期的那一句。它让测试不只是知道“失败了”,还知道“为什么失败”。
数据流:输入测试服务器、请求编号和期望错误文本 → 从服务器消息流里等对应请求的错误响应 → 比较实际错误消息和期望消息,不一致就让测试失败。
调用关系:它被多个负面测试复用,比如本地文件系统关闭、相对路径被拒绝、复制不支持的文件类型、监听相对路径等场景。
调用图:调用 1 个内部函数(read_stream_until_error_message);被 4 处调用(fs_copy_rejects_standalone_fifo_source, fs_methods_reject_relative_paths, fs_methods_return_error_when_local_environment_is_disabled, fs_watch_rejects_relative_paths);外部调用 3 个(Integer, assert_eq!, timeout)。
absolute_path59–66 ↗
fn absolute_path(path: PathBuf) -> AbsolutePathBuf
作用:把普通路径转换成项目协议要求的绝对路径类型。它先确认路径真的是绝对路径,避免测试自己传错数据。
数据流:输入一个 PathBuf 路径 → 检查它是不是绝对路径,再转换成 AbsolutePathBuf → 输出可放进文件系统请求参数里的绝对路径对象。
调用关系:几乎所有正常的文件系统请求都会用它包装路径。这样测试发送给 app-server 的路径格式和正式协议保持一致。
调用图:调用 1 个内部函数(try_from);被 14 处调用(fs_copy_ignores_unknown_special_files_in_recursive_copy, fs_copy_preserves_symlinks_in_recursive_copy, fs_copy_rejects_copying_directory_into_descendant, fs_copy_rejects_directory_without_recursive, fs_copy_rejects_standalone_fifo_source, fs_get_metadata_reports_symlink, fs_get_metadata_returns_only_used_fields, fs_methods_cover_current_fs_utils_surface, fs_methods_return_error_when_local_environment_is_disabled, fs_watch_allows_missing_file_targets (+4 more));外部调用 1 个(assert!)。
fs_get_metadata_returns_only_used_fields69–120 ↗
async fn fs_get_metadata_returns_only_used_fields() -> Result<()>
作用:检查 fs/getMetadata 返回的文件信息只包含客户端真正会用的字段。这样可以避免接口偷偷暴露多余、不稳定或没人需要的数据。
数据流:先在临时目录写入 note.txt → 启动服务器并请求这个文件的元数据 → 检查响应 JSON 只有创建时间、修改时间、是否目录、是否文件、是否符号链接这些字段,并确认文件判断和时间值合理。
调用关系:这个测试使用 initialized_mcp 准备服务器,用 absolute_path 包装文件路径,用 to_response 把服务器响应转成 FsGetMetadataResponse 后再断言结果。
调用图:调用 2 个内部函数(absolute_path, initialized_mcp);外部调用 7 个(new, Integer, to_response, assert!, assert_eq!, write, timeout)。
fs_methods_return_error_when_local_environment_is_disabled123–142 ↗
async fn fs_methods_return_error_when_local_environment_is_disabled() -> Result<()>
作用:确认当本地文件系统能力被关掉时,文件接口不会继续访问磁盘。这样可以防止配置禁用后仍然泄露或修改本地文件。
数据流:创建临时目录和一个目标路径 → 用环境变量把执行服务器地址设成 none,启动测试服务器 → 发送读文件请求 → 期望收到“local filesystem is not configured”错误。
调用关系:它不走 initialized_mcp,而是用 new_with_env 特意改启动环境;随后把错误校验交给 expect_error_message。
调用图:调用 3 个内部函数(new_with_env, absolute_path, expect_error_message);外部调用 2 个(new, timeout)。
fs_get_metadata_reports_symlink146–171 ↗
async fn fs_get_metadata_reports_symlink() -> Result<()>
作用:在 Unix 系统上检查元数据接口能识别符号链接。符号链接可以理解成文件系统里的“快捷方式”,它既指向文件,又有自己的类型特征。
数据流:创建真实文件,再创建指向它的符号链接 → 启动服务器,请求符号链接路径的元数据 → 输出响应应显示它是文件、不是目录,同时也是符号链接。
调用关系:这个测试只在 Unix 上运行。它通过 initialized_mcp 访问服务器,用 absolute_path 提供路径,用 to_response 解析结果。
调用图:调用 2 个内部函数(absolute_path, initialized_mcp);外部调用 7 个(new, Integer, to_response, assert_eq!, write, symlink, timeout)。
fs_methods_cover_current_fs_utils_surface174–322 ↗
async fn fs_methods_cover_current_fs_utils_surface() -> Result<()>
作用:用一条完整流程检查主要文件系统接口是否都能工作:建目录、写文件、读文件、复制文件、复制目录、列目录、删除目录。它像一次端到端演练。
数据流:在临时目录中规划源目录、嵌套目录和目标路径 → 通过服务器依次创建目录、写入两个文件、读取文件内容、复制单个文件、递归复制目录、读取目录列表、删除复制出的目录 → 每一步都检查磁盘内容或响应是否符合预期。
调用关系:这是覆盖面最大的正向测试。它依赖 initialized_mcp 启动服务器,反复用 absolute_path 生成请求参数,并用 to_response 读取 readFile 和 readDirectory 的结构化结果。
调用图:调用 2 个内部函数(absolute_path, initialized_mcp);外部调用 6 个(new, Integer, to_response, assert!, assert_eq!, timeout)。
fs_write_file_accepts_base64_bytes325–364 ↗
async fn fs_write_file_accepts_base64_bytes() -> Result<()>
作用:确认写文件接口能接受任意字节,而不只是普通文字。接口用 base64(一种把二进制数据变成安全文本的编码)传输文件内容。
数据流:准备包含 0、1、2、255 的字节数组 → 编成 base64 后通过 fs/writeFile 写入 → 直接从磁盘读回确认字节没变,再通过 fs/readFile 读回并确认返回的 base64 也一致。
调用关系:它测试写入和读取两个接口的二进制通道。初始化和路径包装分别交给 initialized_mcp 与 absolute_path,响应解析交给 to_response。
调用图:调用 2 个内部函数(absolute_path, initialized_mcp);外部调用 5 个(new, Integer, to_response, assert_eq!, timeout)。
fs_write_file_rejects_invalid_base64367–393 ↗
async fn fs_write_file_rejects_invalid_base64() -> Result<()>
作用:确认写文件接口会拒绝不合法的 base64 内容。否则服务器可能写入错误数据,或者在解码时出现不可控问题。
数据流:准备一个文件路径和明显非法的字符串“%%%” → 发送 fs/writeFile 请求 → 等待错误响应,并检查错误消息以“需要有效 base64”开头。
调用关系:这是写文件接口的负面测试。它用 initialized_mcp 启动服务器,用 absolute_path 包装路径,然后直接读取错误响应并检查提示。
调用图:调用 2 个内部函数(absolute_path, initialized_mcp);外部调用 4 个(new, Integer, assert!, timeout)。
fs_methods_reject_relative_paths396–517 ↗
async fn fs_methods_reject_relative_paths() -> Result<()>
作用:确认所有文件系统接口都拒绝相对路径。相对路径会依赖当前工作目录,容易造成访问位置不清楚甚至越权。
数据流:先准备一个真实的绝对路径文件 → 启动服务器 → 对 readFile、writeFile、createDirectory、getMetadata、readDirectory、remove、copy 等接口故意发送相对路径 → 每个请求都应返回同一句“需要绝对路径”的错误。
调用关系:它通过 send_raw_request 绕过正常类型包装,故意发送坏 JSON 参数;每个错误都交给 expect_error_message 做统一校验。
调用图:调用 2 个内部函数(expect_error_message, initialized_mcp);外部调用 3 个(new, json!, write)。
fs_copy_rejects_directory_without_recursive520–544 ↗
async fn fs_copy_rejects_directory_without_recursive() -> Result<()>
作用:确认复制目录时必须明确开启 recursive。recursive 的意思是“连同里面的子目录和文件一起处理”。
数据流:创建一个源目录 → 启动服务器 → 发送 fs/copy,请求复制目录但 recursive=false → 收到错误,说明复制目录必须 recursive=true。
调用关系:这是复制接口的安全检查。它用 initialized_mcp 和 absolute_path 走正常请求通道,然后直接比较错误消息。
调用图:调用 2 个内部函数(absolute_path, initialized_mcp);外部调用 5 个(new, Integer, assert_eq!, create_dir_all, timeout)。
fs_copy_rejects_copying_directory_into_descendant547–571 ↗
async fn fs_copy_rejects_copying_directory_into_descendant() -> Result<()>
作用:确认不能把一个目录复制到它自己里面。否则就像把箱子复制进箱子自身,可能造成无限套娃或混乱结果。
数据流:创建 source/nested 目录结构 → 请求把 source 递归复制到 source/nested/copy → 服务器应拒绝,并返回“不能复制到自身或子孙目录”的错误。
调用关系:它专门验证 fs/copy 的防呆规则。测试先由 initialized_mcp 启动服务器,再用 absolute_path 构造源和目标路径。
调用图:调用 2 个内部函数(absolute_path, initialized_mcp);外部调用 5 个(new, Integer, assert_eq!, create_dir_all, timeout)。
fs_copy_preserves_symlinks_in_recursive_copy575–603 ↗
async fn fs_copy_preserves_symlinks_in_recursive_copy() -> Result<()>
作用:在 Unix 系统上确认递归复制目录时会保留符号链接本身,而不是把链接指向的内容硬塞过去。
数据流:创建 source/nested,并在 source 下创建指向 nested 的符号链接 nested-link → 通过 fs/copy 递归复制 source 到 copied → 检查 copied/nested-link 仍然是符号链接,而且目标仍是 nested。
调用关系:这个 Unix 专用测试验证复制目录时的链接语义。它通过 initialized_mcp 发复制请求,用系统的 symlink_metadata 和 read_link 检查磁盘结果。
调用图:调用 2 个内部函数(absolute_path, initialized_mcp);外部调用 8 个(new, Integer, assert!, assert_eq!, create_dir_all, symlink_metadata, symlink, timeout)。
fs_copy_ignores_unknown_special_files_in_recursive_copy607–644 ↗
async fn fs_copy_ignores_unknown_special_files_in_recursive_copy() -> Result<()>
作用:在 Unix 系统上确认递归复制目录时会跳过不认识的特殊文件,比如命名管道。命名管道不是普通文件,复制它可能卡住或带来奇怪行为。
数据流:创建源目录,放入普通 note.txt,再用 mkfifo 创建 named-pipe → 递归复制整个目录 → 检查普通文件被复制了,而 named-pipe 没有出现在目标目录。
调用关系:它验证 fs/copy 面对特殊文件时的宽容策略。若 mkfifo 命令失败,测试会立即报错;否则通过 initialized_mcp 发送复制请求并检查结果。
调用图:调用 2 个内部函数(absolute_path, initialized_mcp);外部调用 9 个(new, bail!, Integer, assert!, assert_eq!, new, create_dir_all, write, timeout)。
fs_copy_rejects_standalone_fifo_source648–676 ↗
async fn fs_copy_rejects_standalone_fifo_source() -> Result<()>
作用:在 Unix 系统上确认如果直接把命名管道当作复制源,服务器会拒绝。因为接口只承诺支持普通文件、目录和符号链接。
数据流:在临时目录中创建一个 named-pipe → 启动服务器 → 请求把这个特殊文件复制到另一个路径 → 期望收到“不支持此类文件”的错误。
调用关系:它和递归复制跳过特殊文件的测试互补:目录里遇到特殊文件可以跳过,但用户直接点名复制特殊文件必须报错。错误检查交给 expect_error_message。
调用图:调用 3 个内部函数(absolute_path, expect_error_message, initialized_mcp);外部调用 3 个(new, bail!, new)。
fs_watch_directory_reports_changed_child_paths_and_unwatch_stops_notifications679–745 ↗
async fn fs_watch_directory_reports_changed_child_paths_and_unwatch_stops_notifications() -> Result<()>
作用:检查监听目录时,目录里子文件变化能被报告;同时确认取消监听后不再收到通知。监听就像给目录装了门铃,文件变了就响。
数据流:创建 repo/.git/FETCH_HEAD → 请求服务器监听 .git 目录 → 修改 FETCH_HEAD → 如果测试环境收到了 fs/changed 通知,就检查 watchId 和 changed_paths 是否正确 → 清空残留通知后发送 unwatch → 再改另一个文件,确认短时间内不会再收到通知。
调用关系:它使用 maybe_fs_changed_notification 读取可选通知,因为不同测试沙箱中文件监听可能不稳定;取消监听后则明确要求不能再出现通知。
调用图:调用 3 个内部函数(absolute_path, initialized_mcp, maybe_fs_changed_notification);外部调用 9 个(from_millis, new, Integer, to_response, assert!, assert_eq!, create_dir_all, write, timeout)。
fs_watch_file_reports_atomic_replace_events748–785 ↗
async fn fs_watch_file_reports_atomic_replace_events() -> Result<()>
作用:检查监听单个文件时,即使用“原子替换”的方式改文件,也能报告变化。原子替换就是先写临时文件,再一下子改名覆盖原文件。
数据流:创建 .git/HEAD 文件 → 请求监听这个文件 → 调用 replace_file_atomically 替换内容 → 如果收到了 fs/changed 通知,就检查通知里的 watchId 和变化路径正是 HEAD。
调用关系:它关注文件监听对常见编辑方式的兼容性。文件替换动作交给 replace_file_atomically,通知读取交给 maybe_fs_changed_notification。
调用图:调用 4 个内部函数(absolute_path, initialized_mcp, maybe_fs_changed_notification, replace_file_atomically);外部调用 7 个(new, Integer, to_response, assert_eq!, create_dir_all, write, timeout)。
fs_watch_allows_missing_file_targets788–824 ↗
async fn fs_watch_allows_missing_file_targets() -> Result<()>
作用:确认可以监听一个暂时还不存在的文件。很多真实场景里文件稍后才会被创建,比如 Git 的某些状态文件。
数据流:创建 .git 目录但不创建 FETCH_HEAD → 请求监听这个不存在的 FETCH_HEAD 路径 → 之后用原子替换方式创建它 → 如果有通知,就检查通知指向这个新出现的文件。
调用关系:它验证 fs/watch 不要求目标文件一开始就存在。创建文件动作复用 replace_file_atomically,通知解析复用 maybe_fs_changed_notification。
调用图:调用 4 个内部函数(absolute_path, initialized_mcp, maybe_fs_changed_notification, replace_file_atomically);外部调用 6 个(new, Integer, to_response, assert_eq!, create_dir_all, timeout)。
fs_watch_rejects_relative_paths827–845 ↗
async fn fs_watch_rejects_relative_paths() -> Result<()>
作用:确认监听接口也拒绝相对路径。监听路径如果不明确,服务器可能看错地方或者暴露不该看的目录。
数据流:启动服务器 → 用原始 JSON 请求发送 path=relative-path 的 fs/watch → 等待错误响应,确认提示是绝对路径反序列化失败。
调用关系:它和其他相对路径拒绝测试保持同一规则,只是专门覆盖 fs/watch。错误断言交给 expect_error_message。
调用图:调用 2 个内部函数(expect_error_message, initialized_mcp);外部调用 2 个(new, json!)。
fs_changed_notification847–852 ↗
fn fs_changed_notification(notification: JSONRPCNotification) -> Result<FsChangedNotification>
作用:把服务器发来的 fs/changed 通知解析成测试里好比较的结构。它相当于把一封 JSON 消息拆成清楚的字段。
数据流:输入一个 JSONRPCNotification → 取出 params 字段,如果没有就报错 → 把 params 转成 FsChangedNotification → 输出包含 watchId 和 changed_paths 的结构。
调用关系:它只被 maybe_fs_changed_notification 调用。监听类测试不会直接解析原始通知,而是通过这个小函数拿到结构化结果。
调用图:被 1 处调用(maybe_fs_changed_notification)。
maybe_fs_changed_notification854–866 ↗
async fn maybe_fs_changed_notification(
mcp: &mut TestAppServer,
) -> Result<Option<FsChangedNotification>>
作用:尝试在较短时间内读取一次文件变化通知;有就返回,没有也不算失败。这样测试能适应文件监听在某些沙箱环境里不稳定的现实。
数据流:输入测试服务器 → 在 OPTIONAL_FS_CHANGE_TIMEOUT 内等待 fs/changed 通知 → 等到了就用 fs_changed_notification 解析并返回 Some,超时就返回 None。
调用关系:它被三个监听测试使用。它把“不一定收到系统事件”的复杂性包起来,让测试既能验证通知格式,又不会因为操作系统没发事件而整套失败。
调用图:调用 2 个内部函数(read_stream_until_notification_message, fs_changed_notification);被 3 处调用(fs_watch_allows_missing_file_targets, fs_watch_directory_reports_changed_child_paths_and_unwatch_stops_notifications, fs_watch_file_reports_atomic_replace_events);外部调用 1 个(timeout)。
replace_file_atomically868–881 ↗
fn replace_file_atomically(path: &PathBuf, contents: &str) -> Result<()>
作用:用接近真实编辑器或 Git 的方式替换文件内容:先写临时文件,再改名成目标文件。这样可以测试监听器是否能捕捉这种常见变化。
数据流:输入目标路径和新内容 → 写入同名但扩展名为 lock 的临时文件;在 Windows 上先尝试删除旧文件;最后把临时文件重命名为目标路径 → 磁盘上的目标文件变成新内容。
调用关系:它被监听文件和监听缺失文件的测试调用,用来制造“原子替换”事件;随后这些测试通过 maybe_fs_changed_notification 看服务器是否报告变化。
调用图:被 2 处调用(fs_watch_allows_missing_file_targets, fs_watch_file_reports_atomic_replace_events);外部调用 4 个(with_extension, remove_file, rename, write)。
app-server/tests/suite/v2/process_exec.rs源码 ↗
这份测试像是在给“远程控制本地命令执行”这件事做体检。测试会先启动一个临时的测试版 app-server,再通过测试连接向它发送“启动进程”或“杀掉进程”的请求。它检查几件关键事:启动请求应该很快返回,而不是等命令跑完才回复;命令结束后,服务器应该另外发出一条“进程已退出”的通知,里面带退出码、标准输出和错误输出;如果配置里禁用了本地环境,启动命令必须失败;如果设置了输出长度上限,服务器只能保存前面一部分,并明确标记“被截断了”;如果杀掉长时间运行的进程,退出通知也要能正确到达。文件里还做了 Windows 和类 Unix 系统的命令差异处理,避免测试只在某一种系统上能跑。几个辅助函数则像测试夹具:负责搭好服务器、组装请求参数、等待退出通知、等待临时文件出现。
process_spawn_returns_before_exit_and_emits_exit_notification24–104 ↗
async fn process_spawn_returns_before_exit_and_emits_exit_notification() -> Result<()>
作用:这个测试确认“启动进程”的请求会先返回成功,然后等进程真的结束时再发一条退出通知。这样可以防止服务器把一个长命令当成同步任务,一直卡着不回话。
数据流:它先创建一个临时目录和测试服务器,再准备一个小命令:命令一启动就写一个探针文件,然后等待测试创建释放文件,最后输出 stdout 和 stderr 并退出。测试把这些参数发给服务器,先收到一个空的成功响应;接着等探针文件出现,证明进程已经启动;再写入释放文件让进程继续跑完;最后读取“process/exited”通知,检查进程名、退出码、输出内容和截断标记都正确。
调用关系:这是完整流程测试之一。它用 initialized_mcp 搭好测试环境,用 process_spawn_params 生成基础启动参数,用 wait_for_file 避免靠猜时间等待进程启动,最后交给 read_process_exited 读取并解析退出通知。它验证的是启动、异步返回、退出通知这条主链路。
调用图:调用 4 个内部函数(initialized_mcp, process_spawn_params, read_process_exited, wait_for_file);外部调用 7 个(from, new, Integer, assert_eq!, cfg!, write, vec!)。
process_spawn_returns_error_when_local_environment_is_disabled107–131 ↗
async fn process_spawn_returns_error_when_local_environment_is_disabled() -> Result<()>
作用:这个测试确认当本地执行环境被禁用时,服务器不会真的启动进程,而是返回清楚的错误。它保护的是安全边界:配置说不能执行本地命令,就必须坚决不能执行。
数据流:它先建临时目录和假的响应服务器,写入配置,并额外把执行服务器地址环境变量设成 none,让本地环境不可用。然后启动测试版 app-server 并完成初始化。接着它发送一个看起来正常的启动进程请求。结果不是成功响应,而是一条错误响应,错误信息必须是“local environment is not configured”。
调用关系:这个测试没有走 initialized_mcp,因为它要故意用特殊环境变量启动服务器。它仍然复用 process_spawn_params 来组装请求参数,然后通过测试连接读取错误消息。它覆盖的是“配置禁止执行”这条失败路径。
调用图:调用 3 个内部函数(new_with_env, create_config_toml, process_spawn_params);外部调用 7 个(new, new, Integer, create_mock_responses_server_sequence_unchecked, assert_eq!, timeout, vec!)。
process_spawn_reports_buffered_output_cap_reached134–180 ↗
async fn process_spawn_reports_buffered_output_cap_reached() -> Result<()>
作用:这个测试确认服务器在收集进程输出时,会遵守输出字节上限,并告诉调用者输出被截断了。这样可以避免一个命令打印太多内容,把内存或消息撑爆。
数据流:它先搭好测试服务器,再准备一个会向标准输出写 abcde、向标准错误写 12345 的短命令。测试发送启动请求时,把 output_bytes_cap 设置为 3。服务器先返回启动成功;等进程退出后,测试读取退出通知,应该只看到 stdout 是 abc、stderr 是 123,并且两个 cap_reached 标记都为 true。
调用关系:它复用 initialized_mcp 建环境,复用 process_spawn_params 生成启动参数,再覆盖输出上限设置。最后通过 read_process_exited 拿到退出通知。它专门验证“输出缓存限制”这个细节是否正确接入进程执行流程。
调用图:调用 3 个内部函数(initialized_mcp, process_spawn_params, read_process_exited);外部调用 5 个(new, Integer, assert_eq!, cfg!, vec!)。
process_kill_terminates_running_process183–231 ↗
async fn process_kill_terminates_running_process() -> Result<()>
作用:这个测试确认服务器能杀掉一个还在运行的进程,并且之后会发出退出通知。没有这个能力,用户启动了卡住的命令后就可能收不回来。
数据流:它先启动测试服务器,再创建一个会睡 30 秒的命令。发送启动请求后,服务器返回成功,说明进程已经被接管。然后测试发送 kill 请求,带上同一个 process_handle。kill 请求也应该返回成功。最后测试读取退出通知,确认通知属于这个进程,退出码不是 0,输出为空,并且没有触发输出截断。
调用关系:它走的是“启动进程再杀掉”的流程。initialized_mcp 负责准备服务器,process_spawn_params 负责组装启动请求,read_process_exited 负责读取杀掉之后产生的退出通知。这个测试把 process/spawn 和 process/kill 两个接口串起来检查。
调用图:调用 3 个内部函数(initialized_mcp, process_spawn_params, read_process_exited);外部调用 7 个(new, Integer, assert!, assert_eq!, assert_ne!, cfg!, vec!)。
initialized_mcp233–239 ↗
async fn initialized_mcp(codex_home: &Path) -> Result<(MockServer, TestAppServer)>
作用:这个辅助函数负责准备一个可以用来跑测试的 app-server 连接。它把重复的开服、写配置、初始化握手这些步骤收在一起,避免每个测试都写一遍。
数据流:它接收一个临时的 codex_home 路径。先启动一个假的响应服务器,再在临时目录里写入配置文件,让 app-server 知道该连哪里;然后创建 TestAppServer,并在规定时间内完成 initialize 初始化。最后返回假的响应服务器和已经初始化好的测试连接。
调用关系:它被多个正常路径测试调用:启动后退出、输出截断、杀进程。它内部会调用 create_config_toml 写配置,调用 TestAppServer::new 启动测试服务器,并用 timeout 防止初始化卡死。它是这些测试的共同起跑线。
调用图:调用 2 个内部函数(new, create_config_toml);被 3 处调用(process_kill_terminates_running_process, process_spawn_reports_buffered_output_cap_reached, process_spawn_returns_before_exit_and_emits_exit_notification);外部调用 3 个(new, create_mock_responses_server_sequence_unchecked, timeout)。
process_spawn_params241–258 ↗
fn process_spawn_params(
process_handle: String,
cwd: &Path,
command: Vec<String>,
) -> Result<ProcessSpawnParams>
作用:这个辅助函数把“启动一个进程”需要的参数打包成标准请求对象。测试用它来保证大部分默认设置一致,只在关心的地方单独改。
数据流:它接收进程标识、工作目录和命令数组。它把工作目录转换成绝对路径类型,然后填进 ProcessSpawnParams;同时设置默认值:不开伪终端,不流式传 stdin/stdout/stderr,不设置输出上限,不设置超时,不额外传环境变量,也不指定窗口大小。最后返回这个参数对象。
调用关系:所有发送启动进程请求的测试都会用它。正常测试会在它生成的默认参数上覆盖少量字段,比如环境变量或输出上限。它不直接发送请求,只负责把请求材料准备好。
调用图:调用 1 个内部函数(try_from);被 4 处调用(process_kill_terminates_running_process, process_spawn_reports_buffered_output_cap_reached, process_spawn_returns_before_exit_and_emits_exit_notification, process_spawn_returns_error_when_local_environment_is_disabled)。
read_process_exited260–268 ↗
async fn read_process_exited(mcp: &mut TestAppServer) -> Result<ProcessExitedNotification>
作用:这个辅助函数专门等待并读取“进程已退出”的通知,然后把通知内容变成测试能直接比较的结构。它让测试不用关心底层 JSON 消息怎么拆。
数据流:它接收一个可变的 TestAppServer 连接。先从消息流里等到名为 process/exited 的通知,再取出通知参数;如果没有参数,就给出上下文错误。然后它把 JSON 参数反序列化成 ProcessExitedNotification,返回给调用者。
调用关系:它被三个需要检查进程最终结果的测试调用。测试只关心退出码、输出和截断标记,所以把“读消息”和“解析 JSON”这两步交给它做。它位于测试断言之前,是从服务器消息流到 Rust 结构体的转换点。
调用图:调用 1 个内部函数(read_stream_until_notification_message);被 3 处调用(process_kill_terminates_running_process, process_spawn_reports_buffered_output_cap_reached, process_spawn_returns_before_exit_and_emits_exit_notification);外部调用 1 个(from_value)。
wait_for_file270–278 ↗
async fn wait_for_file(path: &Path) -> Result<()>
作用:这个辅助函数等待某个文件出现,用来确认子进程已经真的启动并执行到某一步。它比固定睡几秒更稳,因为机器快慢不同时也不容易误判。
数据流:它接收一个文件路径。在默认超时时间内,它每隔 20 毫秒检查一次这个路径是否存在;如果文件出现,就结束等待;如果一直没出现,就返回带说明的超时错误。
调用关系:它只被 process_spawn_returns_before_exit_and_emits_exit_notification 使用。那个测试让子进程先写探针文件,wait_for_file 就负责等这个信号,再继续释放子进程退出。它的作用是把测试从“不靠谱的时间猜测”改成“看到明确证据再继续”。
调用图:被 1 处调用(process_spawn_returns_before_exit_and_emits_exit_notification);外部调用 4 个(from_millis, exists, sleep, timeout)。
app-server/tests/suite/v2/windows_sandbox_setup.rs源码 ↗
这个文件不是正式功能代码,而是一组自动化测试。可以把它理解成给 app server 做体检的两项检查:第一项检查正常情况,测试程序先搭一个假的后端服务和临时配置,再启动测试版服务器,发送“开始设置 Windows 沙箱”的请求。服务器应该先回复“已开始”,之后再主动发一条“setupCompleted”通知,说明沙箱设置流程走完了。第二项检查异常情况:如果请求里传入的是相对路径,比如“relative-root”,而不是完整的绝对路径,服务器必须拒绝它,并返回 JSON-RPC 错误。JSON-RPC 是一种用 JSON 格式传请求和响应的通信约定。这里用超时保护每一步,避免测试卡死;用临时目录避免污染真实用户环境。
windows_sandbox_setup_start_emits_completion_notification21–64 ↗
async fn windows_sandbox_setup_start_emits_completion_notification() -> Result<()>
作用:这个测试验证:当客户端请求开始 Windows 沙箱设置时,服务器会先确认“启动了”,然后再发出“设置完成”的通知。它保证正常流程不是只收到了请求就结束,而是真的会把完成状态告诉客户端。
数据流:进去的是一个临时的 Codex 主目录、一个假的响应服务器地址,以及一个 Windows 沙箱启动请求,模式是非管理员权限运行,工作目录为空。测试先写好模拟配置,再启动 TestAppServer,并完成初始化;随后发送 setupStart 请求,读取对应的 JSON-RPC 响应,把响应内容转成 WindowsSandboxSetupStartResponse,检查 started 为真;最后继续从消息流里等 windowsSandbox/setupCompleted 通知,把通知参数解析出来,并检查通知里的模式仍然是 Unelevated。出来的结果是测试通过或失败;过程中只改动临时目录和测试服务器状态。
调用关系:这个函数是测试框架在运行测试时直接调用的异步测试用例。它会借助 create_mock_responses_server_sequence_unchecked 搭假服务,用 write_mock_responses_config_toml 写测试配置,用 TestAppServer 启动服务器,用 send_windows_sandbox_setup_start_request 发请求,再用 read_stream_until_response_message 和 read_stream_until_notification_message 等待服务器回话,最后用 to_response 把通用响应拆成具体业务响应。
调用图:调用 1 个内部函数(new);外部调用 11 个(new, new, new, Integer, create_mock_responses_server_sequence_unchecked, to_response, write_mock_responses_config_toml, assert!, assert_eq!, from_value (+1 more))。
windows_sandbox_setup_start_rejects_relative_cwd67–91 ↗
async fn windows_sandbox_setup_start_rejects_relative_cwd() -> Result<()>
作用:这个测试验证:Windows 沙箱设置请求不能接受相对工作目录。这样可以避免服务器在不明确的位置执行沙箱相关操作,减少路径混乱或安全风险。
数据流:进去的是一个新的临时 Codex 主目录和一条原始 JSON-RPC 请求,请求方法是 windowsSandbox/setupStart,参数里 mode 是 unelevated,cwd 是 relative-root 这种相对路径。测试启动并初始化 TestAppServer,然后直接发送这条原始请求;接着等待与该请求编号对应的错误响应。出来的是一个错误对象,测试检查它的错误码是 -32600,也就是“无效请求”,并且错误消息里包含 Invalid request。
调用关系:这个函数也是由测试框架直接运行的异步测试用例。它不像正常流程测试那样使用专门的封装请求方法,而是用 send_raw_request 故意构造一条不合格请求,以便绕过客户端侧的安全封装,直接检查服务器自己的校验是否可靠。随后它把工作交给 read_stream_until_error_message,专门等服务器返回错误。
调用图:调用 1 个内部函数(new);外部调用 6 个(new, Integer, assert!, assert_eq!, json!, timeout)。
远程控制集成
此套件专注于应用服务器的远程控制注册、配对、状态和托管客户端工作流。
app-server/tests/suite/v2/remote_control.rs源码 ↗
远程控制涉及本机应用服务器、配置文件、持久化状态、登录凭据,以及一个远端后台服务。这里的测试不会真的连线上后台,而是在本机临时开一个假的 HTTP 服务,假装自己是远程控制后台。测试先准备临时目录、认证信息和配置,再启动测试版应用服务器,通过 JSON-RPC(一种用 JSON 发请求和收回应的协议)发“启用远程控制”“读取状态”“开始配对”“列出客户端”等请求,然后检查返回值、状态通知、数据库里的开关是否正确。文件里还特意测试了受管 requirements.toml 禁止远程控制时,所有相关 RPC 都必须被拒绝。几个 Backend 结构体就是小型假后台:有的故意卡住注册流程,有的返回配对码,有的返回客户端列表。这样可以把复杂的远程控制流程拆成可控的小剧场,确保真实代码在各种边界情况下不出岔子。
EnvVarGuard::set69–75 ↗
fn set(key: &'static str, value: &OsStr) -> Self
作用:临时设置一个环境变量,并记住它原来的值。测试结束后可以自动恢复,避免这个测试污染后面的测试。
数据流:进去的是环境变量名和值;它先读取原来的值,再把新值写进当前进程环境;出来的是一个 EnvVarGuard 小对象,里面保存了变量名和旧值。
调用关系:一些需要假装不同运行环境的测试会先调用它,比如显式设置 CODEX_HOME。它本身只做设置,真正的恢复工作交给 EnvVarGuard::drop 在对象离开作用域时完成。
调用图:被 26 处调用(explicit_remote_control_startup_fails_when_disabled_by_requirements, remote_stdio_env_var_source_does_not_copy_local_env, stdio_server_propagates_explicit_local_env_var_source, stdio_server_propagates_whitelisted_env_vars, streamable_http_with_oauth_round_trip_impl, windows_elevated_enforces_deny_read_and_protects_setup_marker, windows_restricted_token_rejects_exact_and_glob_deny_read_policy, agent_identity_authapi_base_url_prefers_env_value, assert_agent_identity_plan_alias, auth_manager_rejects_env_personal_access_token_workspace_mismatch (+15 more));外部调用 2 个(set_var, var_os)。
EnvVarGuard::drop79–86 ↗
fn drop(&mut self)
作用:在 EnvVarGuard 被丢弃时,把环境变量恢复成测试前的样子。这样测试用过的临时环境不会留在进程里。
数据流:进去的是 EnvVarGuard 里保存的变量名和旧值;如果旧值存在,就写回旧值;如果旧值本来不存在,就删除这个环境变量;没有普通返回值,只改变进程环境。
调用关系:它由 Rust 的 Drop 机制自动调用,也就是变量离开作用域时自动执行。它和 EnvVarGuard::set 配成一对,像“借东西”和“还东西”。
调用图:外部调用 2 个(remove_var, set_var)。
remote_control_preference89–98 ↗
async fn remote_control_preference(
state_db: &StateRuntime,
websocket_url: &str,
) -> Result<Option<bool>>
作用:从状态数据库里读取某个远程控制登记记录里的启用偏好。测试用它确认“持久启用/禁用”有没有真的写进数据库。
数据流:进去的是状态数据库和 websocket 地址;它按地址、账号和默认客户端名查登记记录;出来的是 remote_control_enabled 这个可选布尔值,比如 Some(true)、Some(false) 或没有值。
调用关系:持久化偏好相关测试会调用它。它把底层数据库查询 get_remote_control_enrollment 包成更好读的检查步骤。
调用图:外部调用 1 个(get_remote_control_enrollment)。
wait_for_response100–106 ↗
async fn wait_for_response(mcp: &mut TestAppServer, request_id: i64) -> Result<JSONRPCResponse>
作用:等待测试服务器返回某个请求编号对应的成功响应。它给等待加了超时,防止测试卡死。
数据流:进去的是测试服务器对象和请求编号;它把编号包装成 JSON-RPC 请求 ID,然后在固定时间内读流;出来的是 JSONRPCResponse,或者超时报错。
调用关系:多个测试在发出远程控制请求后用它收结果。它把 TestAppServer 的读流能力和 tokio timeout 串起来,让测试代码不用重复写等待逻辑。
调用图:调用 1 个内部函数(read_stream_until_response_message);被 3 处调用(disable_waits_for_in_flight_durable_enable, pairing_start_works_after_ephemeral_enable, rpc_updates_durable_preference_but_ephemeral_does_not);外部调用 2 个(Integer, timeout)。
assert_remote_control_disabled_by_requirements108–123 ↗
async fn assert_remote_control_disabled_by_requirements(
mcp: &mut TestAppServer,
request_id: i64,
) -> Result<()>
作用:检查某个远程控制请求是否因为受管规则禁止而失败。它确认错误码和错误消息都对。
数据流:进去的是测试服务器和请求编号;它等待这个编号对应的错误响应;然后检查错误码是 -32600,消息是“remote control is disabled by managed requirements”;成功时不返回数据,只说明断言通过。
调用关系:managed_requirements_reject_all_remote_control_rpcs 用它逐个检查所有远程控制 RPC。它把重复的错误检查收成一个小工具。
调用图:调用 1 个内部函数(read_stream_until_error_message);被 1 处调用(managed_requirements_reject_all_remote_control_rpcs);外部调用 3 个(Integer, assert_eq!, timeout)。
managed_requirements_reject_all_remote_control_rpcs126–180 ↗
async fn managed_requirements_reject_all_remote_control_rpcs() -> Result<()>
作用:测试当 requirements.toml 明确写着不允许远程控制时,所有远程控制相关 RPC 都必须被拒绝。这样可以防止企业或管理员策略被绕过。
数据流:它创建临时 CODEX_HOME,写入 allow_remote_control = false,启动测试服务器并初始化;先确认服务器发出的状态通知是 Disabled;再发送启用、禁用、读状态、配对、客户端列表和撤销等请求;最后逐个确认都返回同一个禁止错误。
调用关系:这是测试运行器直接执行的异步测试。它调用 TestAppServer 启动服务器,并把每个请求的错误检查交给 assert_remote_control_disabled_by_requirements。
调用图:调用 2 个内部函数(new, assert_remote_control_disabled_by_requirements);外部调用 5 个(new, assert_eq!, from_value, write, timeout)。
managed_requirements_allow_remote_control_true_does_not_enable_or_block_it183–202 ↗
async fn managed_requirements_allow_remote_control_true_does_not_enable_or_block_it() -> Result<()>
作用:测试 allow_remote_control = true 只是“允许使用”,不会自动打开远程控制,也不会阻止读状态。它确认默认状态仍然是关闭。
数据流:它写入允许远程控制的 requirements.toml,启动并初始化服务器;发送状态读取请求;把 JSON-RPC 响应转成 RemoteControlStatusReadResponse;最后确认状态是 Disabled。
调用关系:由测试运行器执行。它主要覆盖“允许”和“启用”不是一回事,使用 TestAppServer 发请求,用 to_response 解析响应。
调用图:调用 1 个内部函数(new);外部调用 6 个(new, Integer, to_response, assert_eq!, write, timeout)。
explicit_remote_control_startup_fails_when_disabled_by_requirements206–249 ↗
async fn explicit_remote_control_startup_fails_when_disabled_by_requirements() -> Result<()>
作用:测试如果管理员规则禁止远程控制,但启动参数又要求显式启用远程控制,服务器启动必须失败。这样能防止命令行参数越过受管策略。
数据流:它准备临时目录、禁止规则、Unix socket 地址和 CODEX_HOME 环境变量;调用 run_main_with_transport_options 尝试以远程控制启用模式启动;出来的结果应该是 InvalidInput 错误,并且 socket 文件不应被创建。
调用关系:由测试运行器执行,并标记 serial,意思是串行跑,避免环境变量等全局状态互相干扰。它使用 EnvVarGuard::set 临时设置 CODEX_HOME,核心启动动作交给 run_main_with_transport_options。
调用图:调用 3 个内部函数(from_listen_url, set, with_managed_config_path_for_tests);外部调用 10 个(new, default, assert!, assert_eq!, run_main_with_transport_options, format!, current_exe, write, timeout, default)。
listen_off_honors_persisted_remote_control_enable252–276 ↗
async fn listen_off_honors_persisted_remote_control_enable() -> Result<()>
作用:测试即使命令行写了 --listen off,只要数据库里之前保存过“远程控制已启用”,服务器仍会尝试连远程控制后台。它保证用户的持久偏好不会被 listen off 意外覆盖。
数据流:它启动一个假远程控制监听器,算出 websocket 地址;在状态库里预写一条 remote_control_enabled = true 的登记;然后用 --listen off 启动测试服务器;最后等待假后台收到连接。
调用关系:由测试运行器执行。它用 configured_remote_control_listener 准备假后台配置,用 StateRuntime 写入持久状态,再通过 TestAppServer::new_with_args 启动服务器。
调用图:调用 3 个内部函数(new_with_args, configured_remote_control_listener, init);外部调用 3 个(new, format!, timeout)。
listen_off_ignores_persisted_enable_when_disabled_by_requirements279–324 ↗
async fn listen_off_ignores_persisted_enable_when_disabled_by_requirements() -> Result<()>
作用:测试管理员禁止远程控制时,即使数据库里保存过启用偏好,也不能连接远程后台。它还确认原来的数据库记录不会被偷偷改掉。
数据流:它准备假后台和 allow_remote_control = false;写入一条 remote_control_enabled = true 的登记;用 --listen off 启动服务器;服务器应很快失败退出,假后台不应收到连接;最后再读数据库确认偏好仍是 Some(true)。
调用关系:由测试运行器执行。它把假监听器、状态库和测试服务器组合起来,重点验证受管规则优先级高于持久偏好。
调用图:调用 3 个内部函数(new_with_args, configured_remote_control_listener, init);外部调用 7 个(from_millis, new, assert!, assert_eq!, format!, write, timeout)。
listen_off_exits_without_persisted_remote_control_enable327–358 ↗
async fn listen_off_exits_without_persisted_remote_control_enable() -> Result<()>
作用:测试 --listen off 且没有可用的远程控制启用偏好时,服务器会退出而不是无意义地常驻。它覆盖“没有监听入口,也没有远程控制入口”的情况。
数据流:它分别测试没有偏好和偏好为 false 两种情况;每次都准备临时目录和假监听器,必要时写入数据库;再用 --listen off 启动服务器;最后确认进程退出且不是成功状态。
调用关系:由测试运行器执行。它用循环覆盖两个相近场景,借助 configured_remote_control_listener 和 StateRuntime 构造环境。
调用图:调用 3 个内部函数(new_with_args, configured_remote_control_listener, init);外部调用 4 个(new, assert!, format!, timeout)。
remote_control_disable_returns_disabled_status361–380 ↗
async fn remote_control_disable_returns_disabled_status() -> Result<()>
作用:测试发送“禁用远程控制”请求时,服务器返回的状态确实是 Disabled。即使之前没有真正连上远端,也要能给出完整状态信息。
数据流:它准备临时目录和假远程监听器,启动并初始化服务器;发送 disable 请求;读取响应并转成 RemoteControlDisableResponse;检查状态、服务器名、环境 ID 和安装 ID。
调用关系:由测试运行器执行。它通过 TestAppServer 发 JSON-RPC 请求,使用 configured_remote_control_listener 提供可用的远端配置。
调用图:调用 2 个内部函数(new, configured_remote_control_listener);外部调用 6 个(new, Integer, to_response, assert!, assert_eq!, timeout)。
remote_control_status_read_returns_disabled_status383–401 ↗
async fn remote_control_status_read_returns_disabled_status() -> Result<()>
作用:测试刚启动、没有启用远程控制时,读取状态会返回 Disabled。它确认默认状态清楚且包含必要标识信息。
数据流:它创建临时目录,启动并初始化服务器;发送状态读取请求;把响应转成 RemoteControlStatusReadResponse;检查状态是 Disabled、环境 ID 为空、服务器名和安装 ID 不为空。
调用关系:由测试运行器执行。它只依赖 TestAppServer,不需要假后台,因为只是读本地默认状态。
调用图:调用 1 个内部函数(new);外部调用 6 个(new, Integer, to_response, assert!, assert_eq!, timeout)。
remote_control_enable_returns_connecting_status404–434 ↗
async fn remote_control_enable_returns_connecting_status() -> Result<()>
作用:测试启用远程控制时,服务器会先向后台登记,登记完成后返回 Connecting 状态。它还确认响应不会在后台登记完成前过早返回。
数据流:它启动一个会卡住登记响应的 BlockingRemoteControlBackend;发 enable 请求;确认假后台收到了 enroll 请求;短时间内应收不到响应;然后放行登记响应;最后收到 Connecting 状态和 environment-id。
调用关系:由测试运行器执行。它依赖 BlockingRemoteControlBackend::start、wait_for_enroll_request 和 complete_enrollment 来精确控制后台何时回复。
调用图:调用 2 个内部函数(new, start);外部调用 7 个(from_millis, new, Integer, to_response, assert!, assert_eq!, timeout)。
disable_waits_for_in_flight_durable_enable437–465 ↗
async fn disable_waits_for_in_flight_durable_enable() -> Result<()>
作用:测试如果“持久启用”还在进行中,此时发“禁用”请求,禁用会等启用流程收尾后再完成。这样能避免状态库被两个操作打乱。
数据流:它启动会阻塞登记的假后台和状态库;发 enable 后等后台收到登记请求但不回复;再发 disable,并确认短时间内没有响应;放行登记后等待 disable 响应;最后确认返回 Disabled,并且数据库偏好变成 Some(false)。
调用关系:由测试运行器执行。它用 BlockingRemoteControlBackend 控制竞态时机,用 wait_for_response 等待最终响应,用 remote_control_preference 验证数据库结果。
调用图:调用 4 个内部函数(new, start, wait_for_response, init);外部调用 6 个(from_millis, new, Integer, to_response, assert_eq!, timeout)。
rpc_updates_durable_preference_but_ephemeral_does_not468–526 ↗
async fn rpc_updates_durable_preference_but_ephemeral_does_not() -> Result<()>
作用:测试普通启用/禁用会修改数据库里的长期偏好,而 ephemeral(临时)启用/禁用不会。它区分“这次先开/关一下”和“以后都记住”。
数据流:它启动假后台和状态库;先发普通 enable,完成登记后确认偏好为 true;再发临时 disable,确认偏好仍是 true;普通 disable 后偏好变 false;普通 enable 后变 true;普通 disable 后再变 false;最后临时 enable 不改变 false。
调用关系:由测试运行器执行。它反复用 TestAppServer 发不同 RPC,用 wait_for_response 等响应,用 remote_control_preference 检查持久状态。
调用图:调用 4 个内部函数(new, start, wait_for_response, init);外部调用 3 个(new, assert_eq!, timeout)。
remote_control_status_read_returns_connecting_status_after_enable529–561 ↗
async fn remote_control_status_read_returns_connecting_status_after_enable() -> Result<()>
作用:测试启用远程控制并完成登记后,再读取状态应该看到 Connecting。它确认状态读取能反映最近的启用流程。
数据流:它启动阻塞式假后台和测试服务器;发 enable,等登记请求出现,然后放行登记响应;收到 enable 响应后再发状态读取;最后检查状态是 Connecting,environment-id 正确,标识字段不为空。
调用关系:由测试运行器执行。它先通过 BlockingRemoteControlBackend 模拟后台登记,再用状态读取 RPC 验证服务器内部状态已更新。
调用图:调用 2 个内部函数(new, start);外部调用 6 个(new, Integer, to_response, assert!, assert_eq!, timeout)。
remote_control_pairing_start_returns_pairing_artifacts564–658 ↗
async fn remote_control_pairing_start_returns_pairing_artifacts() -> Result<()>
作用:测试启用远程控制后,开始配对会返回配对码、手动配对码、环境 ID 和过期时间;随后查询配对状态能显示已被领取。它保证手机等客户端配对流程能拿到需要展示给用户的信息。
数据流:它启动 PairingRemoteControlBackend 和测试服务器;先 enable 并等登记完成和状态通知;再发 pairing start,检查响应里没有暴露 serverId,并确认配对码内容;接着分别用 pairing_code 和 manual_pairing_code 查询状态,两次都应返回 claimed = true。
调用关系:由测试运行器执行。它把假后台的配对接口交给 PairingRemoteControlBackend::start 模拟,测试服务器负责把 JSON-RPC 请求转成后台 HTTP 请求。
调用图:调用 2 个内部函数(new, start);外部调用 5 个(new, Integer, to_response, assert_eq!, timeout)。
pairing_start_works_after_ephemeral_enable661–696 ↗
async fn pairing_start_works_after_ephemeral_enable() -> Result<()>
作用:测试临时启用远程控制后,也能开始配对。也就是说,不写入长期偏好的启用方式仍然应该能完成当前会话的配对。
数据流:它启动配对假后台和测试服务器;发送 ephemeral enable 并等响应;再发送 pairing start;后台收到 enroll 请求,服务器返回配对码、手动码、环境 ID 和过期时间。
调用关系:由测试运行器执行。它使用 PairingRemoteControlBackend 模拟远端登记和配对,用 wait_for_response 等待临时启用完成。
调用图:调用 3 个内部函数(new, start, wait_for_response);外部调用 5 个(new, Integer, to_response, assert_eq!, timeout)。
remote_control_client_management_works_while_disabled699–757 ↗
async fn remote_control_client_management_works_while_disabled() -> Result<()>
作用:测试即使远程控制连接状态是 disabled,也仍然可以列出和撤销远程客户端。这个功能更像账号管理,不要求当前服务器已经连上远控通道。
数据流:它启动客户端管理假后台和测试服务器;先发 clients list 请求,带 cursor、limit、排序等参数;检查返回的客户端信息和 next_cursor;再发 revoke 请求;最后确认假后台实际收到了正确的 GET 和 DELETE HTTP 请求。
调用关系:由测试运行器执行。它使用 ClientManagementRemoteControlBackend::start 模拟后台列表和撤销接口,最后用 wait_for_requests 检查请求路径是否正确。
调用图:调用 2 个内部函数(new, start);外部调用 5 个(new, Integer, to_response, assert_eq!, timeout)。
ClientManagementRemoteControlBackend::start772–814 ↗
async fn start(codex_home: &std::path::Path) -> Result<Self>
作用:启动一个专门测试“客户端列表和撤销”的假远程后台。它会接受两个 HTTP 请求:先返回一个客户端列表,再接受一次撤销。
数据流:进去的是临时 CODEX_HOME 路径;它创建监听器并写好测试配置和认证;后台任务先读取列表请求并返回 JSON,再读取撤销请求并返回 204;出来的是 Backend 对象,里面有接收请求记录的通道和后台任务句柄。
调用关系:remote_control_client_management_works_while_disabled 调用它。它内部使用 configured_remote_control_listener、read_http_request、respond_with_json 和 respond_with_status,把真实网络交互缩成可检查的假服务。
调用图:调用 4 个内部函数(configured_remote_control_listener, read_http_request, respond_with_json, respond_with_status);被 1 处调用(remote_control_client_management_works_while_disabled);外部调用 4 个(channel, json!, spawn, vec!)。
ClientManagementRemoteControlBackend::wait_for_requests816–821 ↗
async fn wait_for_requests(&mut self) -> Result<Vec<String>>
作用:等待客户端管理假后台把收到的请求行交出来。测试用它确认应用服务器访问了正确的 URL 和 HTTP 方法。
数据流:进去的是这个 Backend 自身;它取出只能用一次的接收通道并等待后台任务发来结果;出来的是请求行列表,比如 GET 列表和 DELETE 撤销。
调用关系:remote_control_client_management_works_while_disabled 在所有 RPC 完成后调用它。它是后台任务和测试断言之间的“取证口”。
BlockingRemoteControlBackend::start825–872 ↗
async fn start(codex_home: &std::path::Path) -> Result<Self>
作用:启动一个会故意卡住登记响应的假远程后台。测试用它检查应用服务器在后台没回复时是否会正确等待。
数据流:进去的是临时 CODEX_HOME;它创建监听器,生成 websocket URL,建立两个一次性通道:一个报告收到 enroll 请求,一个等待测试命令再回复;后台收到登记请求后先通知测试,等 complete_enrollment 放行后才返回登记 JSON,并继续接受 websocket 连接。
调用关系:启用、禁用等待、状态读取和持久偏好测试都会调用它。它内部依赖 configured_remote_control_listener、read_enroll_request 和 respond_with_json。
调用图:调用 3 个内部函数(configured_remote_control_listener, read_enroll_request, respond_with_json);被 4 处调用(disable_waits_for_in_flight_durable_enable, remote_control_enable_returns_connecting_status, remote_control_status_read_returns_connecting_status_after_enable, rpc_updates_durable_preference_but_ephemeral_does_not);外部调用 4 个(format!, channel, json!, spawn)。
BlockingRemoteControlBackend::wait_for_enroll_request874–880 ↗
async fn wait_for_enroll_request(&mut self) -> Result<String>
作用:等待假后台收到远程控制登记请求,并把请求行交给测试检查。它保证测试知道服务器确实发起了登记。
数据流:进去的是 Backend 自身;它取出一次性接收器并等待后台任务发送请求行;出来的是类似 POST /.../enroll HTTP/1.1 的字符串。
调用关系:多个测试在发 enable 后调用它。它和 complete_enrollment 配合:先确认请求到了,再决定什么时候让后台回复。
BlockingRemoteControlBackend::complete_enrollment882–888 ↗
fn complete_enrollment(&mut self) -> Result<()>
作用:告诉阻塞式假后台:现在可以返回登记成功响应了。测试用它精确控制启用流程结束的时间点。
数据流:进去的是 Backend 自身;它取出一次性发送器,发送一个空信号;后台任务收到信号后写回登记成功 JSON;如果接收端已经没了,就返回错误。
调用关系:remote_control_enable_returns_connecting_status、disable_waits_for_in_flight_durable_enable 等测试在确认等待行为后调用它。
BlockingRemoteControlBackend::websocket_url890–892 ↗
fn websocket_url(&self) -> &str
作用:返回这个假后台对应的 websocket 地址。测试用它去状态库里查同一条远程控制登记记录。
数据流:进去的是 Backend 自身;它读取内部保存的 websocket_url 字符串引用;出来的是这个 URL 的只读文本。
调用关系:持久偏好相关测试会在启动 Backend 后调用它,再把地址交给 remote_control_preference 做数据库检查。
PairingRemoteControlBackend::start901–975 ↗
async fn start(codex_home: &std::path::Path) -> Result<Self>
作用:启动一个能模拟“登记、配对、查询配对状态”的假远程后台。它让配对测试不需要真的连接云端服务。
数据流:进去的是临时 CODEX_HOME;它创建监听器和通道;后台先处理 enroll 并返回 server_id、environment_id 和 token;然后处理配对开始请求并返回配对码;最后处理两次配对状态查询,分别检查请求体是 pairing_code 和 manual_pairing_code,再返回 claimed = true。
调用关系:remote_control_pairing_start_returns_pairing_artifacts 和 pairing_start_works_after_ephemeral_enable 调用它。它内部多次使用 read_http_request 和 respond_with_json,像剧本一样安排后台响应。
调用图:调用 3 个内部函数(configured_remote_control_listener, read_http_request, respond_with_json);被 2 处调用(pairing_start_works_after_ephemeral_enable, remote_control_pairing_start_returns_pairing_artifacts);外部调用 5 个(anyhow!, assert_eq!, channel, json!, spawn)。
PairingRemoteControlBackend::wait_for_enroll_request977–982 ↗
async fn wait_for_enroll_request(&mut self) -> Result<String>
作用:等待配对假后台收到登记请求。测试用它确认配对流程开始前,服务器已经完成远程控制登记。
数据流:进去的是 Backend 自身;它取出一次性接收器并等待后台任务发送登记请求行;出来的是请求行字符串。
调用关系:配对相关测试在启用或配对开始后调用它,用来检查 enroll 请求确实发生。
PairingRemoteControlBackend::drop986–988 ↗
fn drop(&mut self)
作用:在配对假后台对象销毁时停止后台任务。这样测试结束后不会留下一个永远等待的异步任务。
数据流:进去的是 Backend 内部保存的任务句柄;它调用 abort 取消任务;没有普通返回值,只影响后台任务生命周期。
调用关系:由 Rust 自动调用。PairingRemoteControlBackend::start 启动的任务里有 pending 等待,所以必须在 drop 时主动停掉。
调用图:外部调用 1 个(abort)。
BlockingRemoteControlBackend::drop992–994 ↗
fn drop(&mut self)
作用:在阻塞式假后台对象销毁时取消后台任务。它防止测试结束后还有任务挂着。
数据流:进去的是后台任务句柄;它调用 abort 结束任务;没有返回值。
调用关系:由 Rust 自动调用。BlockingRemoteControlBackend::start 启动的假服务会保持等待,所以这里负责清理。
调用图:外部调用 1 个(abort)。
ClientManagementRemoteControlBackend::drop998–1000 ↗
fn drop(&mut self)
作用:在客户端管理假后台对象销毁时取消后台任务。这样测试资源会被干净回收。
数据流:进去的是后台任务句柄;它调用 abort 停止任务;没有返回值。
调用关系:由 Rust 自动调用。它清理 ClientManagementRemoteControlBackend::start 里 spawn 出来的任务。
调用图:外部调用 1 个(abort)。
configured_remote_control_listener1009–1025 ↗
async fn configured_remote_control_listener(codex_home: &std::path::Path) -> Result<TcpListener>
作用:创建一个本机假远程控制后台监听器,并把测试服务器配置成会访问它。它还写入假的 ChatGPT 登录凭据,让服务器认为自己已登录。
数据流:进去的是临时 CODEX_HOME 路径;它绑定 127.0.0.1 的随机端口,生成后台基础 URL;把 mock responses 配置和认证文件写进临时目录;出来的是 TcpListener,供假后台读取请求。
调用关系:多个假 Backend 和 listen_off 测试都会调用它。它是所有“假装有远端后台”的测试的共同准备步骤。
调用图:调用 1 个内部函数(new);被 7 处调用(start, start, start, listen_off_exits_without_persisted_remote_control_enable, listen_off_honors_persisted_remote_control_enable, listen_off_ignores_persisted_enable_when_disabled_by_requirements, remote_control_disable_returns_disabled_status);外部调用 4 个(bind, write_chatgpt_auth, write_mock_responses_config_toml_with_chatgpt_base_url, format!)。
read_enroll_request1027–1030 ↗
async fn read_enroll_request(listener: &TcpListener) -> Result<(String, BufReader<TcpStream>)>
作用:读取一个 HTTP 请求,并只保留登记请求需要的两样东西:请求行和连接读写器。它是读取 enroll 请求的简化包装。
数据流:进去的是 TcpListener;它调用 read_http_request 接受连接并解析 HTTP;出来的是请求行字符串和 BufReader<TcpStream>,后者还能继续拿到底层连接用来回响应。
调用关系:BlockingRemoteControlBackend::start 使用它处理登记请求。它把通用 HTTP 解析结果裁剪成登记场景需要的形式。
调用图:调用 1 个内部函数(read_http_request);被 1 处调用(start)。
read_http_request1032–1063 ↗
async fn read_http_request(listener: &TcpListener) -> Result<HttpRequest>
作用:从本机监听器里接收一个简单 HTTP 请求,并解析出请求行、请求体和连接。它是这些假后台看懂应用服务器请求的基础工具。
数据流:进去的是 TcpListener;它 accept 一个 TCP 连接,逐行读取请求行和头,找到 content-length,再按长度读取 body;出来的是 HttpRequest,包含 request_line、body 和 reader。
调用关系:三个假 Backend 都会直接或间接用它。它不做完整 HTTP 服务器,只解析测试需要的最小内容。
调用图:被 3 处调用(start, start, read_enroll_request);外部调用 5 个(new, from_utf8, new, accept, vec!)。
respond_with_json1065–1078 ↗
async fn respond_with_json(stream: TcpStream, body: serde_json::Value) -> Result<()>
作用:往 TCP 连接里写一个简单的 HTTP 200 JSON 响应。假后台用它把登记、配对、列表等结果返回给应用服务器。
数据流:进去的是 TCP 流和 JSON 值;它把 JSON 转成字符串,计算长度,拼出 HTTP 响应头和响应体,然后写入连接;出来是成功或写入错误。
调用关系:各个 Backend 的 start 后台任务在收到请求后调用它。它是测试假后台“回话”的通用出口。
调用图:被 3 处调用(start, start, start);外部调用 3 个(write_all, to_string, format!)。
respond_with_status1080–1091 ↗
async fn respond_with_status(mut stream: TcpStream, status: &str, body: &str) -> Result<()>
作用:往 TCP 连接里写一个指定状态码和文本内容的 HTTP 响应。这里主要用来模拟撤销客户端成功后的 204 No Content。
数据流:进去的是 TCP 流、状态字符串和响应体文本;它计算 body 长度,拼出 HTTP 响应并写入连接;出来是成功或写入错误。
调用关系:ClientManagementRemoteControlBackend::start 在处理 DELETE 撤销请求后调用它。respond_with_json 用于 JSON 返回,它用于非 JSON 或无内容状态。