Codex 系统手册

App-server 集成套件 — 插件、marketplace、MCP 和工具/执行器集成

stage-23.1.4.324 个文件

这一阶段像一套总验收,专门检查 app-server 的“扩展和工具”入口会不会坏。插件相关测试负责看安装、读取、分享、卸载、缓存和权限;marketplace 测试看插件来源能不能添加、删除、升级;MCP,也就是外接工具服务器,测试工具、资源、追问确认和状态列表;hooks、skills 和执行器测试确保能力只在该出现的地方出现;命令、搜索、图片、sleep 等测试则检查具体工具能跑、能记录、能安全收尾。

本阶段的文件24

插件目录和生命周期

这些套件覆盖发现插件和市场、读取插件详情、安装和移除插件、分享插件,以及升级或删除由市场支持的内容。

app-server/tests/suite/v2/plugin_list.rs源码 ↗
test测试运行期间,尤其是应用服务器初始化、请求处理和后台插件同步时

插件功能牵涉很多地方:本地文件、用户配置、ChatGPT 登录信息、远程接口、缓存目录、下载的插件包等。任何一个环节弄错,用户看到的插件列表就可能漏掉、重复、状态不对,甚至不该联网时也去联网。这个测试文件会临时造出假的用户目录、假的项目目录和假的远程服务器,然后向测试版应用服务器发送 plugin/list 或 plugin/installed 请求。它检查返回结果里有哪些市场、哪些插件、是否已安装、是否启用、共享信息是否正确,也检查坏 JSON、相对路径、管理员禁用、远程缓存刷新、插件包下载和清理等边角情况。文件后半部分是一批小工具:写配置、造假插件目录、搭建假的 HTTP 响应、等待异步任务完成。整体作用就是把“插件列表这台机器”的各种齿轮都转一遍,防止以后改代码时悄悄弄坏。

函数细节72
write_plugins_enabled_config49–56 ↗
fn write_plugins_enabled_config(codex_home: &std::path::Path) -> std::io::Result<()>

作用:给临时的 Codex 用户目录写一个最简单的配置文件,只打开插件功能。测试用它来快速准备“插件已启用”的环境。

数据流:输入一个用户目录路径 → 在里面写入 config.toml,内容只有 features.plugins = true → 返回写文件是否成功,并改变磁盘上的配置文件。

调用关系:多个本地插件列表测试会先调用它准备环境;它只做写文件这一步,把真正的服务器启动和请求交给各个测试函数。

调用图:被 7 处调用(plugin_list_accepts_legacy_string_default_prompt, plugin_list_accepts_omitted_cwds, plugin_list_keeps_valid_marketplaces_when_another_marketplace_fails_to_load, plugin_list_returns_plugin_interface_with_absolute_asset_paths, plugin_list_returns_share_context_for_shared_local_plugin, plugin_list_skips_invalid_marketplace_file_and_reports_error, plugin_list_uses_alternate_discoverable_manifest_and_keeps_undiscoverable_plugins);外部调用 2 个(join, write)。

write_plugins_enabled_config_with_base_url58–72 ↗
fn write_plugins_enabled_config_with_base_url(
    codex_home: &std::path::Path,
    base_url: &str,
) -> std::io::Result<()>

作用:写一个既开启插件、又指定 ChatGPT 后端地址的配置文件。这样测试可以把服务器请求导向假的 wiremock 服务,而不是访问真实网络。

数据流:输入用户目录和一个 base_url → 拼出 config.toml 内容并写入磁盘 → 后续应用服务器读取这个配置后会访问指定的假后端。

调用关系:涉及远程设置、远程插件目录和共享插件的测试会调用它;它为这些测试打好配置底座。

调用图:被 8 处调用(plugin_list_does_not_query_openai_curated_remote_collection_by_default, plugin_list_fetches_shared_with_me_kind, plugin_list_fetches_workspace_directory_kind_without_remote_plugin_flag, plugin_list_includes_openai_curated_remote_collection_when_requested, plugin_list_propagates_explicit_openai_curated_remote_collection_errors, plugin_list_returns_empty_when_workspace_codex_plugins_disabled, plugin_list_reuses_cached_workspace_codex_plugins_setting, plugin_list_skips_explicit_openai_curated_remote_collection_for_api_auth);外部调用 3 个(join, format!, write)。

plugin_list_skips_invalid_marketplace_file_and_reports_error75–130 ↗
async fn plugin_list_skips_invalid_marketplace_file_and_reports_error() -> Result<()>

作用:检查插件列表遇到坏掉的 marketplace.json 时,不会把坏市场混进结果里,同时会把错误告诉调用方。

数据流:先造一个项目和一个内容不是 JSON 的市场文件 → 启动测试服务器并发送 plugin/list → 得到响应后确认坏文件被跳过,错误列表里记录了这个文件路径和错误信息。

调用关系:这是测试框架直接运行的用例;它调用 write_plugins_enabled_config 准备配置,再通过 TestAppServer 发请求并读取响应。

调用图:调用 3 个内部函数(new_with_env, write_plugins_enabled_config, try_from);外部调用 9 个(new, Integer, to_response, assert!, assert_eq!, create_dir_all, write, timeout, vec!)。

plugin_installed_includes_installed_plugins_and_explicit_install_suggestions133–183 ↗
async fn plugin_installed_includes_installed_plugins_and_explicit_install_suggestions() -> Result<()>

作用:检查“已安装插件”接口不仅返回真正安装的插件,也能返回调用方明确建议安装的插件。

数据流:先写一个 OpenAI curated 市场和一个已安装的 linear 插件,再把 computer-use 当作安装建议传入 → 请求 plugin/installed → 返回结果应包含已安装的 linear 和未安装的 computer-use。

调用关系:测试运行器调用它;它使用 write_openai_curated_marketplace 和 write_installed_plugin 造数据,然后由服务器的 plugin/installed 流程生成结果。

调用图:调用 3 个内部函数(new, write_installed_plugin, write_openai_curated_marketplace);外部调用 7 个(new, Integer, to_response, assert_eq!, write, timeout, vec!)。

plugin_installed_prefers_remote_curated_conflicts_when_remote_plugin_enabled186–285 ↗
async fn plugin_installed_prefers_remote_curated_conflicts_when_remote_plugin_enabled() -> Result<()>

作用:检查本地 curated 插件和远程 curated 插件名字冲突时,如果远程插件功能打开,远程版本应优先显示。

数据流:准备本地 linear、calendar,并让假后端返回远程 linear 和 remote-only → 请求已安装插件 → 结果中 linear 出现在远程市场,本地市场只剩不冲突的 calendar。

调用关系:它把远程接口交给 mount_remote_installed_plugins 等辅助函数模拟;用例验证服务器合并本地和远程插件时的取舍规则。

调用图:调用 8 个内部函数(new, new, empty_remote_installed_plugins_body, mount_empty_user_installed_plugins, mount_remote_installed_plugins, remote_installed_plugin_body, write_installed_plugin, write_openai_curated_marketplace);外部调用 12 个(start, new, Integer, to_response, write_chatgpt_auth, assert_eq!, format!, from_str, json!, to_string (+2 more))。

plugin_installed_ignores_local_cache_without_catalog288–321 ↗
async fn plugin_installed_ignores_local_cache_without_catalog() -> Result<()>

作用:检查只有缓存目录、没有市场目录说明时,系统不会把缓存里的东西误认为可展示插件。

数据流:写入一个插件缓存和启用配置,但不写 marketplace.json → 请求 plugin/installed → 返回空市场且没有加载错误。

调用关系:测试运行器调用它;它用 write_installed_plugin 造缓存,重点验证服务器不会只凭缓存目录就展示插件。

调用图:调用 2 个内部函数(new, write_installed_plugin);外部调用 6 个(new, Integer, to_response, assert_eq!, write, timeout)。

plugin_list_rejects_relative_cwds324–347 ↗
async fn plugin_list_rejects_relative_cwds() -> Result<()>

作用:检查 plugin/list 不接受相对路径的工作目录。这样可以避免服务器在不明确的位置查文件。

数据流:发送一个原始请求,cwds 里放 relative-root → 服务器校验请求失败 → 返回 JSON-RPC 的 Invalid request 错误。

调用关系:这个用例直接用 send_raw_request 绕过正常类型包装,专门测试请求参数校验层。

调用图:调用 1 个内部函数(new);外部调用 6 个(new, Integer, assert!, assert_eq!, json!, timeout)。

plugin_list_keeps_valid_marketplaces_when_another_marketplace_fails_to_load350–469 ↗
async fn plugin_list_keeps_valid_marketplaces_when_another_marketplace_fails_to_load() -> Result<()>

作用:检查多个市场一起加载时,一个坏市场不会拖垮另一个好市场。

数据流:准备一个合法市场和一个坏 JSON 市场 → 请求 plugin/list → 返回里保留合法市场和插件,同时错误列表记录坏市场。

调用关系:它调用 write_plugins_enabled_config 准备插件开关;服务器负责遍历多个 cwd,测试确认失败是局部失败而不是全局失败。

调用图:调用 3 个内部函数(new_with_env, write_plugins_enabled_config, try_from);外部调用 9 个(new, Integer, to_response, assert!, assert_eq!, create_dir_all, write, timeout, vec!)。

plugin_list_returns_empty_when_workspace_codex_plugins_disabled472–553 ↗
async fn plugin_list_returns_empty_when_workspace_codex_plugins_disabled() -> Result<()>

作用:检查团队/账号设置禁止工作区插件时,本地工作区里的插件市场不会被展示。

数据流:准备一个工作区市场,并让假后端账号设置返回 enable_plugins=false → 请求 plugin/list → 返回完全空的插件列表和错误列表。

调用关系:它通过 wiremock 模拟账号设置接口;TestAppServer 使用未托管配置启动,以便真实走远程设置检查流程。

调用图:调用 3 个内部函数(new, new_without_managed_config_with_env, write_plugins_enabled_config_with_base_url);外部调用 16 个(given, start, new, new, Integer, to_response, write_chatgpt_auth, assert_eq!, format!, create_dir_all (+6 more))。

plugin_list_reuses_cached_workspace_codex_plugins_setting556–641 ↗
async fn plugin_list_reuses_cached_workspace_codex_plugins_setting() -> Result<()>

作用:检查工作区插件开关的远程设置会被缓存,不会每次列插件都重复请求服务器。

数据流:假后端第一次返回 enable_plugins=true → 连续发两次 plugin/list → 两次都有本地市场,但最后确认设置接口只被请求了一次。

调用关系:它调用 wait_for_workspace_settings_request_count 等待并核对请求次数,验证服务器内部的设置缓存机制。

调用图:调用 4 个内部函数(new, new_without_managed_config_with_env, wait_for_workspace_settings_request_count, write_plugins_enabled_config_with_base_url);外部调用 16 个(given, start, new, new, Integer, to_response, write_chatgpt_auth, assert_eq!, format!, create_dir_all (+6 more))。

plugin_list_uses_alternate_discoverable_manifest_and_keeps_undiscoverable_plugins644–786 ↗
async fn plugin_list_uses_alternate_discoverable_manifest_and_keeps_undiscoverable_plugins() -> Result<()>

作用:检查系统能识别另一套插件发现文件位置,同时即使某个插件没有清单文件也不会被直接丢掉。

数据流:在 .claude-plugin 路径写市场文件和一个插件清单,另一个插件目录不存在 → 请求列表 → 返回两个插件,存在的插件带界面信息,不存在的插件界面为空。

调用关系:它使用 ALTERNATE_* 常量准备文件;服务器的插件发现流程会同时支持旧/新位置,测试确保兼容性。

调用图:调用 3 个内部函数(new_with_env, write_plugins_enabled_config, try_from);外部调用 9 个(new, Integer, to_response, assert!, assert_eq!, create_dir_all, write, timeout, vec!)。

plugin_list_accepts_omitted_cwds789–833 ↗
async fn plugin_list_accepts_omitted_cwds() -> Result<()>

作用:检查请求里不传 cwds 时也能正常处理。也就是说调用方不指定工作目录不会导致请求失败。

数据流:在用户 HOME 下准备一个市场文件 → 发送 cwds=None 的 plugin/list → 响应能成功解析成 PluginListResponse。

调用关系:它调用 write_plugins_enabled_config 并用 new_with_env 设置 HOME;主要验证默认搜索位置。

调用图:调用 2 个内部函数(new_with_env, write_plugins_enabled_config);外部调用 6 个(new, Integer, to_response, create_dir_all, write, timeout)。

plugin_list_returns_share_context_for_shared_local_plugin836–906 ↗
async fn plugin_list_returns_share_context_for_shared_local_plugin() -> Result<()>

作用:检查本地插件如果已经和远程共享记录绑定,列表里会带上共享上下文信息。

数据流:准备本地插件清单和本地路径到远程插件 ID 的映射文件 → 请求 plugin/list → 返回的插件包含 remote_plugin_id、版本等共享上下文字段。

调用关系:它调用 write_plugin_share_local_path_mapping 写映射;服务器读到映射后把共享信息附加到插件摘要里。

调用图:调用 4 个内部函数(new, write_plugin_share_local_path_mapping, write_plugins_enabled_config, try_from);外部调用 8 个(new, Integer, to_response, assert_eq!, create_dir_all, write, timeout, vec!)。

plugin_list_includes_install_and_enabled_state_from_config909–1041 ↗
async fn plugin_list_includes_install_and_enabled_state_from_config() -> Result<()>

作用:检查插件列表会正确标出插件是否已安装、是否启用,以及市场界面信息。

数据流:准备三个插件:一个已安装启用、一个已安装禁用、一个未安装 → 请求列表 → 返回结果逐个带上 installed 和 enabled 状态。

调用关系:它用 write_installed_plugin 造缓存,用 config.toml 造启用状态;服务器把市场、缓存和配置合并成最终列表。

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

plugin_list_uses_home_config_for_enabled_state1044–1145 ↗
async fn plugin_list_uses_home_config_for_enabled_state() -> Result<()>

作用:检查插件启用状态以用户主目录配置为准,不被工作区配置轻易覆盖。

数据流:HOME 配置里启用 shared-plugin,工作区配置里禁用它,并把工作区设为可信 → 请求多个 cwd 的列表 → 返回插件仍然是启用状态。

调用关系:它调用 set_project_trust_level 设置项目信任;测试服务器加载配置后,验证 home 配置的优先级。

调用图:调用 3 个内部函数(new_with_env, write_installed_plugin, set_project_trust_level);外部调用 8 个(new, Integer, to_response, assert_eq!, create_dir_all, write, timeout, vec!)。

plugin_list_returns_plugin_interface_with_absolute_asset_paths1148–1279 ↗
async fn plugin_list_returns_plugin_interface_with_absolute_asset_paths() -> Result<()>

作用:检查插件界面信息里的图片等资源路径会被转成绝对路径,方便前端直接使用。

数据流:准备带 displayName、描述、网站、图标、logo、截图等字段的插件清单 → 请求列表 → 返回 interface 中资源路径都变成插件目录下的绝对路径。

调用关系:它搭建本地市场和插件清单;服务器解析插件 manifest 时负责把相对资源路径补全。

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

plugin_list_accepts_legacy_string_default_prompt1282–1346 ↗
async fn plugin_list_accepts_legacy_string_default_prompt() -> Result<()>

作用:检查老格式 defaultPrompt 是字符串时仍然能接受。这样旧插件不会因为字段格式升级而坏掉。

数据流:写一个 defaultPrompt 为单个字符串的插件清单 → 请求列表 → 返回时被转换成只含一个字符串的列表。

调用关系:它复用本地插件测试流程;重点验证服务器的清单解析兼容旧格式。

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

plugin_list_returns_installed_git_source_interface_from_cache1349–1464 ↗
async fn plugin_list_returns_installed_git_source_interface_from_cache() -> Result<()>

作用:检查 Git 来源插件即使远程仓库不可用,只要本地已安装缓存存在,也能从缓存读出界面信息。

数据流:市场里写一个 git-subdir 插件,远程路径指向不存在的目录,但缓存里有 plugin.json → 请求列表 → 返回已安装、已启用,并带缓存中的界面资源绝对路径。

调用关系:测试服务器处理 Git 插件时会查缓存;这个用例确认它不会因为远程缺失就丢掉已安装插件的展示信息。

调用图:调用 1 个内部函数(new);外部调用 11 个(new, Integer, to_response, assert_eq!, format!, canonicalize, create_dir_all, write, timeout, from_directory_path (+1 more))。

app_server_startup_sync_downloads_remote_installed_plugin_bundles1467–1532 ↗
async fn app_server_startup_sync_downloads_remote_installed_plugin_bundles() -> Result<()>

作用:检查应用服务器启动时的后台任务会下载远程已安装插件包。

数据流:假后端返回一个已安装远程插件和压缩包下载地址 → 启动带插件启动任务的服务器 → 等待缓存目录出现 plugin.json、.app.json 和技能文件。

调用关系:它调用 mount_remote_plugin_bundle 和 remote_plugin_bundle_tar_gz_bytes 准备下载内容;服务器初始化后后台同步任务完成落盘。

调用图:调用 10 个内部函数(new, new_with_env_and_plugin_startup_tasks, empty_remote_installed_plugins_body, mount_empty_user_installed_plugins, mount_remote_installed_plugins, mount_remote_plugin_bundle, remote_installed_plugin_body_with_app_manifest, remote_plugin_bundle_tar_gz_bytes, wait_for_path_exists, write_remote_plugin_catalog_config);外部调用 10 个(start, new, write_chatgpt_auth, assert!, assert_eq!, format!, from_str, json!, read_to_string, timeout)。

plugin_list_sync_upgrades_and_removes_remote_installed_plugin_bundles1535–1638 ↗
async fn plugin_list_sync_upgrades_and_removes_remote_installed_plugin_bundles() -> Result<()>

作用:检查列插件时会触发远程插件包同步:升级新版本,并清理已经不该存在的旧缓存。

数据流:先放旧版本 linear 和 stale 缓存,再让假后端返回 linear 新版本 → 请求 plugin/list → 新版本被下载,旧版本和 stale 被删除。

调用关系:它结合远程目录接口、已安装接口和下载包接口;测试 plugin/list 触发的同步副作用。

调用图:调用 13 个内部函数(new, new_with_env, empty_remote_installed_plugins_body, mount_empty_user_installed_plugins, mount_remote_installed_plugins, mount_remote_plugin_bundle, mount_remote_plugin_list, remote_installed_plugin_body_with_app_manifest, remote_plugin_bundle_tar_gz_bytes, wait_for_path_exists (+3 more));外部调用 12 个(start, new, Integer, to_response, write_chatgpt_auth, assert!, assert_eq!, format!, from_str, json! (+2 more))。

plugin_list_includes_remote_marketplaces_when_remote_plugin_enabled1641–1870 ↗
async fn plugin_list_includes_remote_marketplaces_when_remote_plugin_enabled() -> Result<()>

作用:检查远程插件功能打开时,plugin/list 会把远程 OpenAI curated 市场加入结果。

数据流:配置 remote_plugin=true,假后端返回远程目录和已安装状态 → 请求列表 → 返回 openai-curated-remote 市场,插件带远程 ID、安装启用状态、界面、关键词,并写入远程目录缓存。

调用关系:它直接用 wiremock 写多个 GET 接口;验证服务器从远程 API 取目录、合并安装状态、再缓存。

调用图:调用 3 个内部函数(new, new, write_remote_plugin_catalog_config);外部调用 18 个(given, start, new, new, Integer, to_response, write_chatgpt_auth, assert!, assert_eq!, format! (+8 more))。

plugin_list_uses_cached_global_remote_catalog_and_refreshes_it1873–1968 ↗
async fn plugin_list_uses_cached_global_remote_catalog_and_refreshes_it() -> Result<()>

作用:检查远程全局目录会先用缓存返回旧数据,同时在后台刷新成新数据。

数据流:第一次请求让远程返回 linear 并写缓存;重置假服务器后改成 notion → 第二次请求仍先返回缓存里的 linear,随后缓存文件刷新成 notion 的远程 ID。

调用关系:它调用 wait_for_cached_remote_catalog_plugin_ids 验证缓存变化;测试服务器的“先快返回、再后台刷新”策略。

调用图:调用 10 个内部函数(new, new, empty_remote_installed_plugins_body, mount_empty_user_installed_plugins, mount_remote_installed_plugins, mount_remote_plugin_list, remote_plugin_list_body, wait_for_cached_remote_catalog_plugin_ids, wait_for_remote_plugin_request_count, write_remote_plugin_catalog_config);外部调用 8 个(start, new, Integer, to_response, write_chatgpt_auth, assert_eq!, format!, timeout)。

plugin_list_includes_openai_curated_remote_collection_when_requested1971–2073 ↗
async fn plugin_list_includes_openai_curated_remote_collection_when_requested() -> Result<()>

作用:检查调用方明确请求 vertical 集合时,会去远程拉取对应的 OpenAI curated 集合。

数据流:传 marketplace_kinds=Vertical,并让假后端响应该 collection 查询 → 请求列表 → 返回 openai-curated-remote 市场和其中的 linear 插件。

调用关系:它用 mount_openai_curated_remote_collection_plugin_list 安装特定 query 的接口;验证显式市场类型会变成远程 collection 参数。

调用图:调用 7 个内部函数(new, new, empty_remote_installed_plugins_body, mount_empty_user_installed_plugins, mount_openai_curated_remote_collection_plugin_list, mount_remote_installed_plugins, write_plugins_enabled_config_with_base_url);外部调用 10 个(start, new, Integer, to_response, write_chatgpt_auth, assert!, assert_eq!, format!, timeout, vec!)。

plugin_list_propagates_explicit_openai_curated_remote_collection_errors2076–2129 ↗
async fn plugin_list_propagates_explicit_openai_curated_remote_collection_errors() -> Result<()>

作用:检查显式请求远程 collection 时,如果远程服务报错,错误会返回给调用方而不是静默吞掉。

数据流:假后端对 vertical collection 返回 500 → 请求 plugin/list → 服务器返回 JSON-RPC 内部错误,消息里说明是列 OpenAI curated 远程目录失败。

调用关系:它设置失败的 wiremock 响应;测试服务器对显式请求采用“失败就报错”的策略。

调用图:调用 6 个内部函数(new, new, empty_remote_installed_plugins_body, mount_empty_user_installed_plugins, mount_remote_installed_plugins, write_plugins_enabled_config_with_base_url);外部调用 15 个(given, start, new, new, Integer, write_chatgpt_auth, assert!, assert_eq!, format!, timeout (+5 more))。

plugin_list_skips_explicit_openai_curated_remote_collection_for_api_auth2132–2166 ↗
async fn plugin_list_skips_explicit_openai_curated_remote_collection_for_api_auth() -> Result<()>

作用:检查使用 API key 登录时,即使请求 vertical 远程集合,也不会去访问 ChatGPT 远程插件接口。

数据流:用 API key 登录,发送 marketplace_kinds=Vertical → 返回空市场 → 最后确认远程 /ps/plugins/list 请求次数为 0。

调用关系:它调用 login_with_api_key 设置认证方式;服务器根据认证类型跳过 ChatGPT 专用远程插件。

调用图:调用 4 个内部函数(new, wait_for_remote_plugin_request_count, write_plugins_enabled_config_with_base_url, default);外部调用 9 个(start, new, Integer, to_response, assert!, login_with_api_key, format!, timeout, vec!)。

plugin_list_includes_api_curated_marketplace_for_api_auth_when_remote_plugin_enabled2169–2221 ↗
async fn plugin_list_includes_api_curated_marketplace_for_api_auth_when_remote_plugin_enabled() -> Result<()>

作用:检查 API key 用户在 remote_plugin 打开时看到的是 API curated 本地市场,而不是 ChatGPT 远程市场。

数据流:配置远程插件,写 api_marketplace.json,并用 API key 登录 → 请求列表 → 返回 openai-api-curated 市场且没有远程目录请求。

调用关系:它调用 write_openai_api_curated_marketplace 准备 API 用户专属市场;服务器按认证类型选择市场来源。

调用图:调用 5 个内部函数(new, wait_for_remote_plugin_request_count, write_openai_api_curated_marketplace, write_remote_plugin_catalog_config, default);外部调用 9 个(start, new, Integer, to_response, assert!, assert_eq!, login_with_api_key, format!, timeout)。

plugin_list_does_not_query_openai_curated_remote_collection_by_default2224–2274 ↗
async fn plugin_list_does_not_query_openai_curated_remote_collection_by_default() -> Result<()>

作用:检查默认 plugin/list 不会主动请求 vertical collection。只有调用方明确要,才查这个特殊集合。

数据流:配置 ChatGPT 登录和 base_url,但请求里不传 marketplace_kinds → 返回结果不含 openai-curated-remote,并确认没有 collection=vertical 请求。

调用关系:它验证默认行为;服务器不会因为有登录信息就额外请求特殊集合。

调用图:调用 3 个内部函数(new, new, write_plugins_enabled_config_with_base_url);外部调用 8 个(start, new, Integer, to_response, write_chatgpt_auth, assert!, format!, timeout)。

plugin_list_vertical_kind_noops_when_remote_plugin_enabled2277–2327 ↗
async fn plugin_list_vertical_kind_noops_when_remote_plugin_enabled() -> Result<()>

作用:检查 remote_plugin 已经打开时,单独请求 Vertical 不会再触发旧的 vertical collection 路径。

数据流:配置 remote_plugin=true 并请求 marketplace_kinds=Vertical → 返回中没有 openai-curated-remote,且没有 collection=vertical 请求。

调用关系:它与显式 collection 测试形成对照,验证不同功能开关组合下的分支。

调用图:调用 3 个内部函数(new, new, write_remote_plugin_catalog_config);外部调用 9 个(start, new, Integer, to_response, write_chatgpt_auth, assert!, format!, timeout, vec!)。

plugin_list_does_not_append_global_remote_when_marketplace_kinds_are_explicit2330–2372 ↗
async fn plugin_list_does_not_append_global_remote_when_marketplace_kinds_are_explicit() -> Result<()>

作用:检查调用方显式指定只要本地市场时,服务器不会自动附加全局远程市场。

数据流:配置 remote_plugin=true,但请求 marketplace_kinds=Local → 返回不含远程市场 → 确认远程 list 接口没有被调用。

调用关系:它调用 wait_for_remote_plugin_request_count 检查联网次数;验证显式过滤条件优先。

调用图:调用 4 个内部函数(new, new, wait_for_remote_plugin_request_count, write_remote_plugin_catalog_config);外部调用 9 个(start, new, Integer, to_response, write_chatgpt_auth, assert!, format!, timeout, vec!)。

plugin_installed_includes_remote_shared_with_me_plugins2375–2474 ↗
async fn plugin_installed_includes_remote_shared_with_me_plugins() -> Result<()>

作用:检查 plugin/installed 在插件共享功能打开时,会列出别人共享给我的远程插件。

数据流:假后端返回 workspace 作用域的 PRIVATE 和 UNLISTED 已安装插件 → 请求已安装插件 → 返回 workspace-shared-with-me 市场,插件都标为已安装并带启用状态。

调用关系:它用 workspace_remote_plugin_page_body 造远程返回;服务器读取 GLOBAL 和 WORKSPACE 已安装接口后按共享规则分类。

调用图:调用 7 个内部函数(new, new, mount_empty_user_installed_plugins, mount_remote_installed_plugins, remote_installed_plugin_body, wait_for_remote_installed_scope_request, workspace_remote_plugin_page_body);外部调用 11 个(start, new, Integer, to_response, write_chatgpt_auth, assert_eq!, format!, from_str, to_string, write (+1 more))。

plugin_installed_includes_workspace_directory_without_plugin_sharing2477–2561 ↗
async fn plugin_installed_includes_workspace_directory_without_plugin_sharing() -> Result<()>

作用:检查插件共享关闭时,已安装的工作区公开插件仍会作为 workspace directory 展示。

数据流:假后端返回一个 LISTED 工作区插件和一个 PRIVATE 插件 → 请求 plugin/installed → 结果只包含 workspace-linear,归到 workspace-directory 市场。

调用关系:它验证 plugin_sharing=false 的分支;服务器会过滤掉共享私有插件,但保留工作区目录插件。

调用图:调用 7 个内部函数(new, new, empty_remote_installed_plugins_body, mount_empty_user_installed_plugins, mount_remote_installed_plugins, wait_for_remote_installed_scope_request, workspace_remote_plugin_page_body);外部调用 11 个(start, new, Integer, to_response, write_chatgpt_auth, assert_eq!, format!, from_str, to_string, write (+1 more))。

plugin_installed_includes_created_by_me_when_remote_plugins_enabled2564–2652 ↗
async fn plugin_installed_includes_created_by_me_when_remote_plugins_enabled() -> Result<()>

作用:检查远程插件功能打开时,用户自己创建的远程插件会出现在“Created by me”市场,并触发包下载。

数据流:假后端 USER 作用域返回 private-linear 和下载地址 → 请求已安装插件 → 返回 created-by-me-remote 市场,同时缓存目录出现下载后的 plugin.json。

调用关系:它调用 mount_remote_plugin_bundle 造插件包;服务器从 USER 已安装接口拿到插件并启动同步。

调用图:调用 9 个内部函数(new, new_with_env, empty_remote_installed_plugins_body, mount_remote_installed_plugins, mount_remote_plugin_bundle, remote_plugin_bundle_tar_gz_bytes, user_remote_plugin_page_body, wait_for_path_exists, wait_for_remote_installed_scope_request);外部调用 12 个(start, new, Integer, to_response, write_chatgpt_auth, assert_eq!, format!, from_str, json!, to_string (+2 more))。

plugin_installed_starts_remote_installed_bundle_sync2655–2731 ↗
async fn plugin_installed_starts_remote_installed_bundle_sync() -> Result<()>

作用:检查调用 plugin/installed 也会启动远程已安装插件包同步,不只是 plugin/list 或启动时同步。

数据流:假后端返回全局已安装 linear 和压缩包地址 → 请求 plugin/installed → 返回插件状态后,等待本地缓存出现对应 plugin.json。

调用关系:它使用远程已安装接口和 bundle 下载辅助函数;验证 plugin/installed 流程会把后台同步任务排起来。

调用图:调用 10 个内部函数(new, new_with_env, empty_remote_installed_plugins_body, mount_empty_user_installed_plugins, mount_remote_installed_plugins, mount_remote_plugin_bundle, remote_installed_plugin_body, remote_plugin_bundle_tar_gz_bytes, wait_for_path_exists, wait_for_remote_installed_scope_request);外部调用 9 个(start, new, Integer, to_response, write_chatgpt_auth, assert_eq!, format!, write, timeout)。

plugin_list_fetches_workspace_directory_kind_without_remote_plugin_flag2734–2819 ↗
async fn plugin_list_fetches_workspace_directory_kind_without_remote_plugin_flag() -> Result<()>

作用:检查即使 remote_plugin 开关没开,显式请求 WorkspaceDirectory 也可以拉工作区目录插件。

数据流:配置只开启 plugins,假后端返回 WORKSPACE 目录和已安装状态 → 请求 marketplace_kinds=WorkspaceDirectory → 返回 workspace-directory 市场,并确认没有 GLOBAL 请求。

调用关系:它调用 mount_remote_plugin_list 和 mount_remote_installed_plugins;验证显式类型能单独走工作区远程接口。

调用图:调用 7 个内部函数(new, new, mount_empty_user_installed_plugins, mount_remote_installed_plugins, mount_remote_plugin_list, workspace_remote_plugin_page_body, write_plugins_enabled_config_with_base_url);外部调用 10 个(start, new, Integer, to_response, write_chatgpt_auth, assert!, assert_eq!, format!, timeout, vec!)。

plugin_list_fetches_user_plugins_in_created_by_me_remote_marketplace2822–2955 ↗
async fn plugin_list_fetches_user_plugins_in_created_by_me_remote_marketplace() -> Result<()>

作用:检查请求 CreatedByMeRemote 时,会分页拉取用户自己创建的远程插件。

数据流:假后端 USER list 第一页返回一个插件和下一页 token,第二页返回另一个插件,并返回 USER installed 状态 → 请求列表 → 返回两个 created-by-me 插件,第一个已安装启用。

调用关系:它直接用 wiremock 设置带 pageToken 的两页响应;服务器负责跟随分页直到结束。

调用图:调用 5 个内部函数(new, new, empty_remote_installed_plugins_body, mount_remote_installed_plugins, user_remote_plugin_page_body);外部调用 19 个(given, start, new, new, Integer, to_response, write_chatgpt_auth, assert!, assert_eq!, format! (+9 more))。

plugin_list_fetches_shared_with_me_kind2958–3152 ↗
async fn plugin_list_fetches_shared_with_me_kind() -> Result<()>

作用:检查请求 SharedWithMe 时,会把共享给我的私有插件和未列出插件分成合适的市场,并附上共享上下文。

数据流:假后端返回 shared 接口和 workspace installed 接口的多种插件 → 请求 SharedWithMe → 返回 private 和 unlisted 两个共享市场,插件带创建者、分享链接、远程版本等信息。

调用关系:它调用 mount_shared_workspace_plugins 和已安装接口辅助函数;服务器把“共享列表”和“已安装列表”合并。

调用图:调用 10 个内部函数(new, new, empty_remote_installed_plugins_body, mount_empty_user_installed_plugins, mount_remote_installed_plugins, mount_shared_workspace_plugins, wait_for_remote_installed_scope_request, wait_for_remote_plugin_request_count, workspace_remote_plugin_page_body, write_plugins_enabled_config_with_base_url);外部调用 11 个(start, new, Integer, to_response, write_chatgpt_auth, assert_eq!, format!, from_str, to_string, timeout (+1 more))。

plugin_list_omits_shared_with_me_kind_when_plugin_sharing_disabled3155–3211 ↗
async fn plugin_list_omits_shared_with_me_kind_when_plugin_sharing_disabled() -> Result<()>

作用:检查插件共享功能关闭时,即使请求 SharedWithMe,也不会访问共享接口或返回共享市场。

数据流:配置 plugin_sharing=false → 请求 marketplace_kinds=SharedWithMe → 返回空结果,并确认 /ps/plugins/workspace/shared 请求次数为 0。

调用关系:它通过 wait_for_remote_plugin_request_count 确认没有联网;验证功能开关能挡住整条流程。

调用图:调用 3 个内部函数(new, new, wait_for_remote_plugin_request_count);外部调用 10 个(start, new, Integer, to_response, write_chatgpt_auth, assert_eq!, format!, write, timeout, vec!)。

plugin_list_omits_created_by_me_when_remote_plugins_disabled3214–3266 ↗
async fn plugin_list_omits_created_by_me_when_remote_plugins_disabled() -> Result<()>

作用:检查远程插件功能关闭时,请求 CreatedByMeRemote 不会生效。

数据流:配置 remote_plugin=false、plugin_sharing=true → 请求 CreatedByMeRemote → 返回空结果,并确认没有 /ps/plugins/list 请求。

调用关系:它验证 created-by-me 依赖 remote_plugin 开关;服务器在开关关闭时提前返回。

调用图:调用 3 个内部函数(new, new, wait_for_remote_plugin_request_count);外部调用 10 个(start, new, Integer, to_response, write_chatgpt_auth, assert_eq!, format!, write, timeout, vec!)。

plugin_list_marks_remote_plugin_disabled_by_admin3269–3401 ↗
async fn plugin_list_marks_remote_plugin_disabled_by_admin() -> Result<()>

作用:检查远程插件如果被管理员禁用,列表里会标出 DisabledByAdmin,而不是只看用户是否启用。

数据流:假后端目录和已安装接口都返回 status=DISABLED_BY_ADMIN,enabled=true → 请求列表 → 插件仍显示已安装启用,但 availability 标为管理员禁用。

调用关系:它手写远程 API 响应;服务器转换远程状态为协议里的 PluginAvailability。

调用图:调用 3 个内部函数(new, new, write_remote_plugin_catalog_config);外部调用 14 个(given, start, new, new, Integer, to_response, write_chatgpt_auth, assert_eq!, format!, timeout (+4 more))。

plugin_list_does_not_fetch_remote_marketplaces_when_plugins_disabled3404–3449 ↗
async fn plugin_list_does_not_fetch_remote_marketplaces_when_plugins_disabled() -> Result<()>

作用:检查总插件开关关闭时,服务器不会再去拉远程插件市场。

数据流:配置 features.plugins=false 且 remote_plugin=true → 请求列表 → 返回空市场,并确认远程 list 接口请求次数为 0。

调用关系:它验证最外层插件开关优先;服务器应该在进入远程流程前就停止。

调用图:调用 3 个内部函数(new, new, wait_for_remote_plugin_request_count);外部调用 9 个(start, new, Integer, to_response, write_chatgpt_auth, assert!, format!, write, timeout)。

wait_for_workspace_settings_request_count3536–3542 ↗
async fn wait_for_workspace_settings_request_count(
    server: &MockServer,
    expected_count: usize,
) -> Result<()>

作用:等待账号设置接口被请求指定次数,用来验证设置缓存有没有生效。

数据流:输入假服务器和期望次数 → 检查 /accounts/account-123/settings 的 GET 请求数 → 达到期望后返回。

调用关系:它被 workspace 设置缓存测试调用,并复用 wait_for_remote_plugin_request_count 的通用轮询逻辑。

调用图:调用 1 个内部函数(wait_for_remote_plugin_request_count);被 1 处调用(plugin_list_reuses_cached_workspace_codex_plugins_setting)。

wait_for_remote_plugin_request_count3544–3573 ↗
async fn wait_for_remote_plugin_request_count(
    server: &MockServer,
    path_suffix: &str,
    expected_count: usize,
) -> Result<()>

作用:反复查看假服务器收到的 GET 请求数量,直到某个路径的请求次数等于预期。它是很多异步测试里的“等一等并核对”。

数据流:输入 MockServer、路径后缀和期望次数 → 循环读取请求记录并计数 → 次数相等返回,超过或超时则报错。

调用关系:许多测试用它确认该联网或不该联网;wait_for_featured_plugin_request_count 和 wait_for_workspace_settings_request_count 也基于它。

调用图:被 10 处调用(plugin_list_does_not_append_global_remote_when_marketplace_kinds_are_explicit, plugin_list_does_not_fetch_remote_marketplaces_when_plugins_disabled, plugin_list_fetches_shared_with_me_kind, plugin_list_includes_api_curated_marketplace_for_api_auth_when_remote_plugin_enabled, plugin_list_omits_created_by_me_when_remote_plugins_disabled, plugin_list_omits_shared_with_me_kind_when_plugin_sharing_disabled, plugin_list_skips_explicit_openai_curated_remote_collection_for_api_auth, plugin_list_uses_cached_global_remote_catalog_and_refreshes_it, wait_for_featured_plugin_request_count, wait_for_workspace_settings_request_count);外部调用 5 个(from_millis, received_requests, bail!, sleep, timeout)。

wait_for_remote_installed_scope_request3575–3596 ↗
async fn wait_for_remote_installed_scope_request(server: &MockServer, scope: &str) -> Result<()>

作用:等待假服务器收到某个 scope 的远程已安装插件请求。scope 可以理解成范围,比如 GLOBAL、WORKSPACE、USER。

数据流:输入 MockServer 和 scope 字符串 → 循环查看 /ps/plugins/installed 请求里有没有对应 query 参数 → 找到就返回,超时则失败。

调用关系:远程已安装、共享插件、created-by-me 等测试用它确认服务器确实查了对应范围。

调用图:被 5 处调用(plugin_installed_includes_created_by_me_when_remote_plugins_enabled, plugin_installed_includes_remote_shared_with_me_plugins, plugin_installed_includes_workspace_directory_without_plugin_sharing, plugin_installed_starts_remote_installed_bundle_sync, plugin_list_fetches_shared_with_me_kind);外部调用 5 个(from_millis, received_requests, bail!, sleep, timeout)。

wait_for_cached_remote_catalog_plugin_ids3598–3619 ↗
async fn wait_for_cached_remote_catalog_plugin_ids(
    codex_home: &std::path::Path,
    expected_plugin_ids: &[&str],
) -> Result<()>

作用:等待远程插件目录缓存文件里出现期望的插件 ID 列表。

数据流:输入用户目录和期望 ID → 排序期望值,循环读取缓存里的实际 ID → 一致后返回。

调用关系:它被远程目录缓存刷新测试调用,并把读缓存文件的具体动作交给 cached_remote_catalog_plugin_ids。

调用图:调用 1 个内部函数(cached_remote_catalog_plugin_ids);被 1 处调用(plugin_list_uses_cached_global_remote_catalog_and_refreshes_it);外部调用 3 个(from_millis, sleep, timeout)。

cached_remote_catalog_plugin_ids3621–3642 ↗
fn cached_remote_catalog_plugin_ids(codex_home: &std::path::Path) -> Result<Vec<String>>

作用:从本地远程插件目录缓存文件中读出所有插件 ID,方便测试比较缓存内容。

数据流:输入 Codex 用户目录 → 找 cache/remote_plugin_catalog,逐个读 JSON 文件 → 收集 plugins[].id,排序后返回。

调用关系:它只被 wait_for_cached_remote_catalog_plugin_ids 调用,是缓存验证的小工具。

调用图:被 1 处调用(wait_for_cached_remote_catalog_plugin_ids);外部调用 5 个(join, new, from_slice, read, read_dir)。

wait_for_path_exists3644–3655 ↗
async fn wait_for_path_exists(path: &std::path::Path) -> Result<()>

作用:等待某个文件或目录真的出现在磁盘上。它用于检查后台下载或同步任务是否完成。

数据流:输入路径 → 循环判断 path.exists() → 一旦存在就返回,超时则失败。

调用关系:远程插件包下载、升级、created-by-me 同步等测试都会用它等缓存文件落盘。

调用图:被 4 处调用(app_server_startup_sync_downloads_remote_installed_plugin_bundles, plugin_installed_includes_created_by_me_when_remote_plugins_enabled, plugin_installed_starts_remote_installed_bundle_sync, plugin_list_sync_upgrades_and_removes_remote_installed_plugin_bundles);外部调用 4 个(from_millis, exists, sleep, timeout)。

wait_for_path_missing3657–3668 ↗
async fn wait_for_path_missing(path: &std::path::Path) -> Result<()>

作用:等待某个文件或目录从磁盘上消失。它用于检查旧缓存是否被清理。

数据流:输入路径 → 循环判断路径是否不存在 → 不存在后返回,超时则失败。

调用关系:插件包升级清理测试用它确认旧版本目录和 stale 目录已经被删除。

调用图:被 1 处调用(plugin_list_sync_upgrades_and_removes_remote_installed_plugin_bundles);外部调用 4 个(from_millis, exists, sleep, timeout)。

mount_remote_plugin_list3670–3680 ↗
async fn mount_remote_plugin_list(server: &MockServer, scope: &str, body: &str)

作用:在假服务器上注册一个远程插件目录接口响应。

数据流:输入 MockServer、scope 和响应 body → 安装一个匹配 GET /backend-api/ps/plugins/list 的 mock → 后续服务器请求这个接口时会拿到指定 JSON。

调用关系:远程目录、工作区目录、缓存刷新等测试调用它;它负责搭好假后端的一条路由。

调用图:被 3 处调用(plugin_list_fetches_workspace_directory_kind_without_remote_plugin_flag, plugin_list_sync_upgrades_and_removes_remote_installed_plugin_bundles, plugin_list_uses_cached_global_remote_catalog_and_refreshes_it);外部调用 6 个(given, new, header, method, path, query_param)。

remote_plugin_list_body3682–3717 ↗
fn remote_plugin_list_body(
    remote_plugin_id: &str,
    plugin_name: &str,
    display_name: &str,
    short_description: &str,
) -> String

作用:生成一段标准的远程插件目录 JSON 文本,减少测试里手写重复 JSON。

数据流:输入远程插件 ID、插件名、显示名和短描述 → 拼成包含一个插件的 list 响应字符串 → 返回给 mount_remote_plugin_list 使用。

调用关系:远程目录缓存刷新测试调用它来快速生成旧目录和新目录两种返回。

调用图:被 1 处调用(plugin_list_uses_cached_global_remote_catalog_and_refreshes_it);外部调用 1 个(format!)。

mount_openai_curated_remote_collection_plugin_list3719–3730 ↗
async fn mount_openai_curated_remote_collection_plugin_list(server: &MockServer, body: &str)

作用:在假服务器上注册 OpenAI curated vertical collection 的目录响应。

数据流:输入 MockServer 和 body → 安装匹配 scope=GLOBAL、collection=vertical 的插件列表接口 → 请求命中时返回给定内容。

调用关系:显式请求 OpenAI curated remote collection 的测试调用它;它模拟特殊 collection 查询。

调用图:被 1 处调用(plugin_list_includes_openai_curated_remote_collection_when_requested);外部调用 6 个(given, new, header, method, path, query_param)。

mount_shared_workspace_plugins3732–3741 ↗
async fn mount_shared_workspace_plugins(server: &MockServer, body: &str)

作用:在假服务器上注册“共享给我的工作区插件”接口响应。

数据流:输入 MockServer 和 body → 安装 GET /backend-api/ps/plugins/workspace/shared 的 mock → 服务器查询共享插件时得到这段 JSON。

调用关系:SharedWithMe 测试调用它;服务器随后把这个共享列表和已安装列表合并。

调用图:被 1 处调用(plugin_list_fetches_shared_with_me_kind);外部调用 6 个(given, new, header, method, path, query_param)。

mount_remote_installed_plugins3743–3752 ↗
async fn mount_remote_installed_plugins(server: &MockServer, scope: &str, body: &str)

作用:在假服务器上注册远程已安装插件接口响应。

数据流:输入 MockServer、scope 和 body → 安装匹配 GET /backend-api/ps/plugins/installed?scope=... 的 mock → 后续请求返回指定 JSON。

调用关系:大量远程插件测试调用它;mount_empty_user_installed_plugins 也复用它生成空 USER 响应。

调用图:被 14 处调用(app_server_startup_sync_downloads_remote_installed_plugin_bundles, mount_empty_user_installed_plugins, plugin_installed_includes_created_by_me_when_remote_plugins_enabled, plugin_installed_includes_remote_shared_with_me_plugins, plugin_installed_includes_workspace_directory_without_plugin_sharing, plugin_installed_prefers_remote_curated_conflicts_when_remote_plugin_enabled, plugin_installed_starts_remote_installed_bundle_sync, plugin_list_fetches_shared_with_me_kind, plugin_list_fetches_user_plugins_in_created_by_me_remote_marketplace, plugin_list_fetches_workspace_directory_kind_without_remote_plugin_flag (+4 more));外部调用 6 个(given, new, header, method, path, query_param)。

mount_empty_user_installed_plugins3754–3756 ↗
async fn mount_empty_user_installed_plugins(server: &MockServer)

作用:快速注册一个 USER 范围为空的已安装插件响应。

数据流:输入 MockServer → 取 empty_remote_installed_plugins_body 生成空列表 → 调用 mount_remote_installed_plugins 注册 USER mock。

调用关系:不关心用户自建插件的测试会调用它,避免服务器查 USER scope 时没有 mock。

调用图:调用 2 个内部函数(empty_remote_installed_plugins_body, mount_remote_installed_plugins);被 11 处调用(app_server_startup_sync_downloads_remote_installed_plugin_bundles, plugin_installed_includes_remote_shared_with_me_plugins, plugin_installed_includes_workspace_directory_without_plugin_sharing, plugin_installed_prefers_remote_curated_conflicts_when_remote_plugin_enabled, plugin_installed_starts_remote_installed_bundle_sync, plugin_list_fetches_shared_with_me_kind, plugin_list_fetches_workspace_directory_kind_without_remote_plugin_flag, plugin_list_includes_openai_curated_remote_collection_when_requested, plugin_list_propagates_explicit_openai_curated_remote_collection_errors, plugin_list_sync_upgrades_and_removes_remote_installed_plugin_bundles (+1 more))。

empty_remote_installed_plugins_body3758–3766 ↗
fn empty_remote_installed_plugins_body() -> &'static str

作用:返回一段表示“没有已安装插件”的远程接口 JSON。

数据流:不需要输入 → 返回固定字符串,里面 plugins 是空数组、next_page_token 是 null → 调用方拿它当假后端响应。

调用关系:它被多个 mount 和测试函数复用,用来填充 GLOBAL、WORKSPACE 或 USER 的空结果。

调用图:被 12 处调用(app_server_startup_sync_downloads_remote_installed_plugin_bundles, mount_empty_user_installed_plugins, plugin_installed_includes_created_by_me_when_remote_plugins_enabled, plugin_installed_includes_workspace_directory_without_plugin_sharing, plugin_installed_prefers_remote_curated_conflicts_when_remote_plugin_enabled, plugin_installed_starts_remote_installed_bundle_sync, plugin_list_fetches_shared_with_me_kind, plugin_list_fetches_user_plugins_in_created_by_me_remote_marketplace, plugin_list_includes_openai_curated_remote_collection_when_requested, plugin_list_propagates_explicit_openai_curated_remote_collection_errors (+2 more))。

workspace_remote_plugin_page_body3768–3822 ↗
fn workspace_remote_plugin_page_body(
    remote_plugin_id: &str,
    plugin_name: &str,
    display_name: &str,
    discoverability: &str,
    enabled: Option<bool>,
) -> String

作用:生成一段 WORKSPACE 范围的远程插件分页 JSON,可选择带 enabled 字段。

数据流:输入远程 ID、插件名、显示名、可发现性和可选启用状态 → 拼出包含创建者、分享链接、发布信息的 JSON 字符串 → 返回给测试或 mock 注册函数。

调用关系:共享插件、工作区目录和已安装插件测试大量使用它;user_remote_plugin_page_body 也基于它改 scope。

调用图:被 5 处调用(plugin_installed_includes_remote_shared_with_me_plugins, plugin_installed_includes_workspace_directory_without_plugin_sharing, plugin_list_fetches_shared_with_me_kind, plugin_list_fetches_workspace_directory_kind_without_remote_plugin_flag, user_remote_plugin_page_body);外部调用 1 个(format!)。

user_remote_plugin_page_body3824–3839 ↗
fn user_remote_plugin_page_body(
    remote_plugin_id: &str,
    plugin_name: &str,
    display_name: &str,
    discoverability: &str,
    enabled: Option<bool>,
) -> String

作用:生成 USER 范围的远程插件分页 JSON,用于模拟用户自己创建的插件。

数据流:输入和 workspace_remote_plugin_page_body 相同的信息 → 先生成 WORKSPACE JSON,再把 scope 改成 USER → 返回结果字符串。

调用关系:created-by-me 相关测试调用它;它复用 workspace_remote_plugin_page_body,避免重复维护两套 JSON。

调用图:调用 1 个内部函数(workspace_remote_plugin_page_body);被 2 处调用(plugin_installed_includes_created_by_me_when_remote_plugins_enabled, plugin_list_fetches_user_plugins_in_created_by_me_remote_marketplace)。

remote_installed_plugin_body3841–3852 ↗
fn remote_installed_plugin_body(
    bundle_download_url: &str,
    release_version: &str,
    enabled: bool,
) -> String

作用:生成一个常见的远程已安装插件响应,不带 app manifest。

数据流:输入插件包下载地址、版本号和启用状态 → 调用更通用的 optional app manifest 版本 → 返回包含 linear 插件的 JSON 字符串。

调用关系:远程已安装、冲突优先级和包同步测试调用它;复杂版本交给 remote_installed_plugin_body_with_optional_app_manifest。

调用图:调用 1 个内部函数(remote_installed_plugin_body_with_optional_app_manifest);被 3 处调用(plugin_installed_includes_remote_shared_with_me_plugins, plugin_installed_prefers_remote_curated_conflicts_when_remote_plugin_enabled, plugin_installed_starts_remote_installed_bundle_sync)。

remote_installed_plugin_body_with_app_manifest3854–3866 ↗
fn remote_installed_plugin_body_with_app_manifest(
    bundle_download_url: &str,
    release_version: &str,
    enabled: bool,
    app_manifest: serde_json::Value,
) -> String

作用:生成带 app manifest 的远程已安装插件响应,用来测试插件包下载时额外应用清单也会落盘。

数据流:输入下载地址、版本、启用状态和 app_manifest JSON → 调用 optional 版本把 manifest 插进 release → 返回完整响应字符串。

调用关系:启动同步和升级同步测试调用它;最终拼字符串的工作由 remote_installed_plugin_body_with_optional_app_manifest 做。

调用图:调用 1 个内部函数(remote_installed_plugin_body_with_optional_app_manifest);被 2 处调用(app_server_startup_sync_downloads_remote_installed_plugin_bundles, plugin_list_sync_upgrades_and_removes_remote_installed_plugin_bundles)。

remote_installed_plugin_body_with_optional_app_manifest3868–3906 ↗
fn remote_installed_plugin_body_with_optional_app_manifest(
    bundle_download_url: &str,
    release_version: &str,
    enabled: bool,
    app_manifest: Option<serde_json::Value>,
) -> String

作用:生成远程已安装插件响应的核心函数,可以选择是否包含 app_manifest。

数据流:输入下载地址、版本、启用状态和可选 app manifest → 拼出一段包含 linear 插件、release、enabled 等字段的 JSON → 返回字符串。

调用关系:remote_installed_plugin_body 和 remote_installed_plugin_body_with_app_manifest 都调用它,是远程已安装响应的统一模板。

调用图:被 2 处调用(remote_installed_plugin_body, remote_installed_plugin_body_with_app_manifest);外部调用 1 个(format!)。

mount_remote_plugin_bundle3908–3924 ↗
async fn mount_remote_plugin_bundle(
    server: &MockServer,
    plugin_name: &str,
    body: Vec<u8>,
) -> String

作用:在假服务器上注册一个插件压缩包下载地址。

数据流:输入 MockServer、插件名和压缩包字节 → 安装 GET /bundles/{plugin}.tar.gz 的 mock → 返回完整下载 URL 给远程插件响应使用。

调用关系:远程插件包下载和同步测试调用它;服务器随后会访问这个 URL 下载插件包。

调用图:被 4 处调用(app_server_startup_sync_downloads_remote_installed_plugin_bundles, plugin_installed_includes_created_by_me_when_remote_plugins_enabled, plugin_installed_starts_remote_installed_bundle_sync, plugin_list_sync_upgrades_and_removes_remote_installed_plugin_bundles);外部调用 5 个(given, new, format!, method, path)。

remote_plugin_bundle_tar_gz_bytes3926–3950 ↗
fn remote_plugin_bundle_tar_gz_bytes(plugin_name: &str) -> Result<Vec<u8>>

作用:现场生成一个假的插件压缩包,里面有 plugin.json 和一个技能文件。

数据流:输入插件名 → 用 tar 打包、gzip 压缩,写入 .codex-plugin/plugin.json 和 skills/plan-work/SKILL.md → 返回压缩后的字节数组。

调用关系:需要测试下载解压的用例会先调用它,再把结果交给 mount_remote_plugin_bundle。

调用图:被 4 处调用(app_server_startup_sync_downloads_remote_installed_plugin_bundles, plugin_installed_includes_created_by_me_when_remote_plugins_enabled, plugin_installed_starts_remote_installed_bundle_sync, plugin_list_sync_upgrades_and_removes_remote_installed_plugin_bundles);外部调用 6 个(new, new, default, format!, new, new_gnu)。

write_installed_plugin3952–3958 ↗
fn write_installed_plugin(
    codex_home: &TempDir,
    marketplace_name: &str,
    plugin_name: &str,
) -> Result<()>

作用:快速写一个本地已安装插件缓存,版本固定为 local。

数据流:输入临时用户目录、市场名和插件名 → 调用 write_installed_plugin_with_version → 在 plugins/cache 下生成对应 plugin.json。

调用关系:本地安装状态相关测试调用它;具体目录结构由 write_installed_plugin_with_version 处理。

调用图:调用 1 个内部函数(write_installed_plugin_with_version);被 5 处调用(plugin_installed_ignores_local_cache_without_catalog, plugin_installed_includes_installed_plugins_and_explicit_install_suggestions, plugin_installed_prefers_remote_curated_conflicts_when_remote_plugin_enabled, plugin_list_includes_install_and_enabled_state_from_config, plugin_list_uses_home_config_for_enabled_state)。

write_installed_plugin_with_version3960–3979 ↗
fn write_installed_plugin_with_version(
    codex_home: &TempDir,
    marketplace_name: &str,
    plugin_name: &str,
    plugin_version: &str,
) -> Result<()>

作用:按指定版本写一个本地已安装插件缓存目录。

数据流:输入用户目录、市场名、插件名和版本 → 创建 plugins/cache/{market}/{plugin}/{version}/.codex-plugin → 写入最小 plugin.json。

调用关系:write_installed_plugin 和远程升级清理测试调用它;服务器之后通过这个缓存判断插件已安装。

调用图:被 2 处调用(plugin_list_sync_upgrades_and_removes_remote_installed_plugin_bundles, write_installed_plugin);外部调用 4 个(path, format!, create_dir_all, write)。

write_plugin_sync_config3981–4002 ↗
fn write_plugin_sync_config(codex_home: &std::path::Path, base_url: &str) -> std::io::Result<()>

作用:写一份用于插件同步/精选插件测试的配置,包含 base_url、插件开关和几个本地插件启用状态。

数据流:输入用户目录和 base_url → 写 config.toml,启用 plugins 并设置 linear、gmail、calendar 状态 → 后续测试服务器按此配置运行。

调用关系:精选插件 ID 相关测试调用它,配合假后端 /plugins/featured 使用。

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

write_remote_plugin_catalog_config4004–4020 ↗
fn write_remote_plugin_catalog_config(
    codex_home: &std::path::Path,
    base_url: &str,
) -> std::io::Result<()>

作用:写一份开启远程插件目录的配置。

数据流:输入用户目录和 base_url → 写 config.toml,设置 chatgpt_base_url、plugins=true、remote_plugin=true → 服务器会尝试拉远程插件目录。

调用关系:远程市场、缓存刷新、包同步、管理员禁用等测试都会调用它作为基本配置。

调用图:被 8 处调用(app_server_startup_sync_downloads_remote_installed_plugin_bundles, plugin_list_does_not_append_global_remote_when_marketplace_kinds_are_explicit, plugin_list_includes_api_curated_marketplace_for_api_auth_when_remote_plugin_enabled, plugin_list_includes_remote_marketplaces_when_remote_plugin_enabled, plugin_list_marks_remote_plugin_disabled_by_admin, plugin_list_sync_upgrades_and_removes_remote_installed_plugin_bundles, plugin_list_uses_cached_global_remote_catalog_and_refreshes_it, plugin_list_vertical_kind_noops_when_remote_plugin_enabled);外部调用 3 个(join, format!, write)。

write_openai_curated_marketplace4022–4033 ↗
fn write_openai_curated_marketplace(
    codex_home: &std::path::Path,
    plugin_names: &[&str],
) -> std::io::Result<()>

作用:写一个名为 openai-curated 的本地 curated 市场。

数据流:输入用户目录和插件名列表 → 调用 write_curated_marketplace,使用 marketplace.json 和默认显示名 → 在临时目录下生成市场和插件清单。

调用关系:本地 curated、安装建议、精选插件等测试调用它;底层通用写法交给 write_curated_marketplace。

调用图:调用 1 个内部函数(write_curated_marketplace);被 4 处调用(plugin_installed_includes_installed_plugins_and_explicit_install_suggestions, plugin_installed_prefers_remote_curated_conflicts_when_remote_plugin_enabled, plugin_list_fetches_featured_plugin_ids_without_chatgpt_auth, plugin_list_uses_warmed_featured_plugin_ids_cache_on_first_request)。

write_openai_api_curated_marketplace4035–4046 ↗
fn write_openai_api_curated_marketplace(
    codex_home: &std::path::Path,
    plugin_names: &[&str],
) -> std::io::Result<()>

作用:写一个 API key 用户使用的 openai-api-curated 市场。

数据流:输入用户目录和插件名列表 → 调用 write_curated_marketplace,使用 api_marketplace.json,并设置显示名 OpenAI Curated → 生成本地市场文件。

调用关系:API auth curated 市场测试调用它;它和 write_openai_curated_marketplace 共享同一个通用生成器。

调用图:调用 1 个内部函数(write_curated_marketplace);被 1 处调用(plugin_list_includes_api_curated_marketplace_for_api_auth_when_remote_plugin_enabled)。

write_curated_marketplace4048–4109 ↗
fn write_curated_marketplace(
    codex_home: &std::path::Path,
    manifest_name: &str,
    marketplace_name: &str,
    display_name: Option<&str>,
    plugin_names: &[&str],
) -> std::io::Result<()>

作用:生成一整套假的 curated 插件市场,包括市场文件、插件目录、插件清单和固定 SHA 文件。

数据流:输入用户目录、市场文件名、市场名、可选显示名和插件名列表 → 创建 .tmp/plugins 下的 git 结构、marketplace 文件和每个插件的 plugin.json → 写入 .tmp/plugins.sha。

调用关系:write_openai_curated_marketplace 和 write_openai_api_curated_marketplace 都调用它,是本地 curated 市场测试数据的核心制造器。

调用图:被 2 处调用(write_openai_api_curated_marketplace, write_openai_curated_marketplace);外部调用 4 个(join, format!, create_dir_all, write)。

write_plugin_share_local_path_mapping4111–4130 ↗
fn write_plugin_share_local_path_mapping(
    codex_home: &std::path::Path,
    remote_plugin_id: &str,
    plugin_path: &AbsolutePathBuf,
) -> std::io::Result<()>

作用:写本地插件路径和远程插件 ID 的映射文件,用来模拟“这个本地插件已经被共享过”。

数据流:输入用户目录、远程插件 ID 和本地插件绝对路径 → 生成 JSON,写到 .tmp/plugin-share-local-paths-v1.json → 服务器读到后能给插件补共享上下文。

调用关系:共享本地插件列表测试调用它;它为服务器的分享上下文查找提供假数据。

调用图:被 1 处调用(plugin_list_returns_share_context_for_shared_local_plugin);外部调用 8 个(join, format!, new, json!, to_string_pretty, to_value, create_dir_all, write)。

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

可以把这个文件看成插件详情页的“验收清单”。测试会临时搭一个假的用户目录、假的远程 ChatGPT 后端、假的应用连接器服务,然后让 TestAppServer 像真实运行一样接收 JSON-RPC 请求(JSON-RPC 是一种用 JSON 发请求和收响应的协议)。它覆盖很多容易出错的情况:没有指定读取来源、同时指定两个来源、远程插件不存在、插件功能被关闭、本地清单缺失、共享插件信息不完整、API Key 登录时隐藏应用等。下面的辅助函数负责快速写配置、写插件市场文件、造本地插件目录和启动假服务,让每个测试只关注一个具体行为。

函数细节30
plugin_read_rejects_missing_read_source57–83 ↗
async fn plugin_read_rejects_missing_read_source() -> Result<()>

作用:检查读取插件时,如果既没有给本地市场文件路径,也没有给远程市场名,服务器会拒绝请求。这样可以防止系统不知道该去哪里找插件。

数据流:测试先创建临时用户目录并启动测试服务器,然后发送一个缺少读取来源的插件读取请求。服务器返回错误后,测试确认错误码是“无效请求”,并且错误信息明确说必须二选一提供来源。

调用关系:这是由异步测试运行器直接执行的用例。它主要通过 TestAppServer 发请求、用 timeout 防止测试卡死,再读取服务器返回的错误。

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

plugin_read_rejects_multiple_read_sources86–114 ↗
async fn plugin_read_rejects_multiple_read_sources() -> Result<()>

作用:检查读取插件时,如果同时给了本地市场文件路径和远程市场名,服务器也会拒绝。这样能避免同一个插件名到底按本地还是远程解释产生歧义。

数据流:测试准备临时目录,把一个本地 marketplace 路径和一个远程 marketplace 名一起放进请求里。服务器处理后返回错误,测试确认错误码和提示都说明“只能提供一个来源”。

调用关系:测试运行器调用它。它借助 AbsolutePathBuf 转成绝对路径,用 TestAppServer 发 JSON-RPC 请求,然后验证错误响应。

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

plugin_read_returns_remote_mcp_servers_when_uninstalled117–290 ↗
async fn plugin_read_returns_remote_mcp_servers_when_uninstalled() -> Result<()>

作用:检查远程插件还没安装时,读取详情仍能返回可用的 MCP 服务器和关联应用。MCP 服务器可以理解成插件背后可调用的小工具服务。

数据流:测试写入启用远程插件的配置和 ChatGPT 登录信息,启动假的远程后端,并准备插件详情、已安装列表、应用目录三类响应。然后读取远程插件,最后确认返回的插件名、远程 ID、默认提示、MCP 服务器和应用信息都符合预期。

调用关系:测试运行器调用它。它把远程接口交给 wiremock 假服务器模拟,把真实请求交给 TestAppServer,再用 to_response 把通用 JSON-RPC 响应转成插件读取结果。

调用图:调用 2 个内部函数(new, new);外部调用 16 个(given, start, new, new, Integer, to_response, write_chatgpt_auth, assert_eq!, format!, json! (+6 more))。

plugin_read_returns_share_context_for_shared_remote_plugin293–443 ↗
async fn plugin_read_returns_share_context_for_shared_remote_plugin() -> Result<()>

作用:检查别人共享给当前用户的远程插件,会带回共享上下文。共享上下文就是谁创建、谁能看、分享链接是什么这类信息。

数据流:测试写远程插件配置和登录信息,假后端返回一个工作区共享插件详情和空安装列表。随后分别用两个共享市场名读取同一插件,确认返回被规范成统一市场名,并且共享人、权限成员、分享链接、远程版本都被保留下来。

调用关系:测试运行器调用它。它使用 write_remote_plugin_catalog_config 写配置,wiremock 模拟远程插件目录,TestAppServer 负责走完整读取流程。

调用图:调用 3 个内部函数(new, new, write_remote_plugin_catalog_config);外部调用 14 个(given, start, new, new, Integer, to_response, write_chatgpt_auth, assert_eq!, format!, timeout (+4 more))。

plugin_read_includes_share_url_for_admin_disabled_remote_plugin446–671 ↗
async fn plugin_read_includes_share_url_for_admin_disabled_remote_plugin() -> Result<()>

作用:检查远程插件即使被管理员禁用,读取详情时仍会返回分享链接和完整展示信息。这样用户能知道插件为何不可用,同时还能看到它的来源和说明。

数据流:测试搭好远程服务,让插件详情标记为管理员禁用,并让已安装列表显示插件已安装但关闭、某个技能也关闭。读取后,测试确认可用性是“管理员禁用”、分享链接存在、技能状态为关闭、模板应用和默认提示都正确。

调用关系:测试运行器调用它。它依赖远程配置辅助函数和 wiremock 假接口,最后通过 TestAppServer 收到 PluginReadResponse 并逐项核对。

调用图:调用 3 个内部函数(new, new, write_remote_plugin_catalog_config);外部调用 14 个(given, start, new, new, Integer, to_response, write_chatgpt_auth, assert_eq!, format!, timeout (+4 more))。

plugin_skill_read_reads_remote_skill_contents_when_remote_plugin_enabled674–735 ↗
async fn plugin_skill_read_reads_remote_skill_contents_when_remote_plugin_enabled() -> Result<()>

作用:检查读取远程插件里的某个技能时,可以拿到技能 Markdown 内容。Markdown 是一种常见的纯文本排版格式,常用来写说明文档。

数据流:测试配置远程插件功能和登录信息,假后端在技能详情接口返回 skill_md_contents。测试发送技能读取请求,服务器返回后,确认 contents 字段就是那段 Markdown 文本。

调用关系:测试运行器调用它。它不读整个插件详情,而是通过 TestAppServer 的技能读取请求直连远程技能接口,验证专门的 plugin skill read 路径。

调用图:调用 3 个内部函数(new, new, write_remote_plugin_catalog_config);外部调用 13 个(given, start, new, new, Integer, to_response, write_chatgpt_auth, assert_eq!, format!, timeout (+3 more))。

plugin_read_maps_missing_remote_plugin_to_invalid_request738–786 ↗
async fn plugin_read_maps_missing_remote_plugin_to_invalid_request() -> Result<()>

作用:检查远程插件不存在时,服务器会把远程 404 转成客户端能理解的“无效请求”。这样调用方不会看到晦涩的底层网络错误。

数据流:测试让假远程后端对某个插件详情请求返回 404。服务器收到读取请求后转成 JSON-RPC 错误,测试确认错误码是无效请求,并且消息里说明读取远程插件详情失败。

调用关系:测试运行器调用它。它用 write_remote_plugin_catalog_config 准备远程目录配置,用 wiremock 制造 404,再让 TestAppServer 走实际错误映射。

调用图:调用 3 个内部函数(new, new, write_remote_plugin_catalog_config);外部调用 13 个(given, start, new, new, Integer, write_chatgpt_auth, assert!, assert_eq!, format!, timeout (+3 more))。

plugin_read_rejects_remote_marketplace_when_plugins_are_disabled789–838 ↗
async fn plugin_read_rejects_remote_marketplace_when_plugins_are_disabled() -> Result<()>

作用:检查总的插件功能被关闭时,即使远程插件开关看似打开,也不能读取远程插件。这样避免配置互相打架时绕过总开关。

数据流:测试写入 plugins=false 的配置和登录信息,然后发送远程插件读取请求。服务器没有去访问远程服务,而是直接返回错误,测试确认错误提示说远程插件读取没有启用。

调用关系:测试运行器调用它。它只需要启动 TestAppServer 并验证配置门禁,不依赖具体插件详情。

调用图:调用 2 个内部函数(new, new);外部调用 9 个(start, new, Integer, write_chatgpt_auth, assert!, assert_eq!, format!, write, timeout)。

plugin_read_rejects_invalid_remote_plugin_name841–869 ↗
async fn plugin_read_rejects_invalid_remote_plugin_name() -> Result<()>

作用:检查远程插件 ID 里如果出现路径穿越这类危险字符,会被拒绝。比如 linear/../../oops 这种名字可能试图绕到不该访问的位置。

数据流:测试写入远程插件配置,然后发送带斜杠和相对路径片段的插件名。服务器返回无效请求,测试确认错误信息说明远程插件 ID 不合法,并列出只允许的字符。

调用关系:测试运行器调用它。它使用配置辅助函数准备环境,再通过 TestAppServer 验证输入校验逻辑。

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

plugin_read_returns_canonical_openai_curated_marketplace_name872–927 ↗
async fn plugin_read_returns_canonical_openai_curated_marketplace_name() -> Result<()>

作用:检查本地读取 OpenAI 官方精选市场时,返回的市场名保持规范写法 openai-curated。这样插件 ID 和配置项能稳定对应。

数据流:测试创建一个本地插件市场、插件清单和已安装缓存,并在配置里启用该插件。读取后,测试确认返回的 marketplace_name、marketplace_path、插件 ID 和插件名都按规范生成。

调用关系:测试运行器调用它。它会调用 write_plugin_marketplace 和 write_installed_plugin 搭本地环境,然后交给 TestAppServer 读取。

调用图:调用 4 个内部函数(new, write_installed_plugin, write_plugin_marketplace, try_from);外部调用 7 个(new, Integer, to_response, assert_eq!, create_dir_all, write, timeout)。

plugin_read_returns_share_context_for_shared_local_plugin930–1070 ↗
async fn plugin_read_returns_share_context_for_shared_local_plugin() -> Result<()>

作用:检查本地插件如果对应一个远程共享插件,也能在详情里显示共享上下文。也就是说,本地文件和远程分享记录可以被关联起来。

数据流:测试创建本地插件市场和插件文件,再写入“远程插件 ID 到本地路径”的映射文件。假远程后端返回共享详情。读取本地插件后,测试确认本地版本、远程版本、分享链接、创建者和权限成员都正确。

调用关系:测试运行器调用它。它把本地准备工作交给 write_plugin_marketplace 和 write_plugin_share_local_path_mapping,把远程详情交给 wiremock 模拟。

调用图:调用 6 个内部函数(new, new, write_plugin_marketplace, write_plugin_share_local_path_mapping, write_remote_plugin_catalog_config, try_from);外部调用 16 个(given, start, new, new, Integer, to_response, write_chatgpt_auth, assert_eq!, format!, json! (+6 more))。

plugin_read_keeps_remote_version_when_share_principals_are_missing1073–1175 ↗
async fn plugin_read_keeps_remote_version_when_share_principals_are_missing() -> Result<()>

作用:检查远程共享详情里的成员列表缺失时,系统仍然保留远程版本号。这样部分共享信息坏了或没返回时,不会连版本信息也丢掉。

数据流:测试搭出一个本地共享插件映射,远程返回 share_principals 为 null 的详情。读取后,测试确认 remote_version 还在,但分享链接、创建者、可发现性和成员列表这些依赖完整共享信息的字段为空。

调用关系:测试运行器调用它。它和共享本地插件测试类似,但故意让远程响应缺少成员信息,用来检查降级行为。

调用图:调用 6 个内部函数(new, new, write_plugin_marketplace, write_plugin_share_local_path_mapping, write_remote_plugin_catalog_config, try_from);外部调用 16 个(given, start, new, new, Integer, to_response, write_chatgpt_auth, assert_eq!, format!, json! (+6 more))。

plugin_read_falls_back_to_local_share_context_without_remote_auth1178–1228 ↗
async fn plugin_read_falls_back_to_local_share_context_without_remote_auth() -> Result<()>

作用:检查没有远程登录信息时,本地共享映射仍能生成最基本的共享上下文。这样用户至少知道这个本地插件对应哪个远程共享插件。

数据流:测试只启用插件功能,不写 ChatGPT 登录凭据;创建本地插件和共享映射。读取后,服务器无法补充远程详情,但仍返回 remote_plugin_id,其他远程字段保持为空。

调用关系:测试运行器调用它。它调用 write_plugins_enabled_config、write_plugin_marketplace、write_plugin_source 和 write_plugin_share_local_path_mapping 搭本地场景。

调用图:调用 6 个内部函数(new, write_plugin_marketplace, write_plugin_share_local_path_mapping, write_plugin_source, write_plugins_enabled_config, try_from);外部调用 5 个(new, Integer, to_response, assert_eq!, timeout)。

plugin_read_fails_on_malformed_share_mapping1231–1277 ↗
async fn plugin_read_fails_on_malformed_share_mapping() -> Result<()>

作用:检查共享映射文件如果不是合法 JSON,读取插件会明确失败。JSON 是一种常见的数据格式,写坏了就无法安全解析。

数据流:测试创建本地插件环境,然后把共享映射文件写成“not valid json”。读取插件时服务器尝试加载映射失败,测试确认返回内部错误,并且错误信息指出加载共享路径映射失败。

调用关系:测试运行器调用它。它用本地辅助函数搭插件,再故意手写坏文件,验证错误不会被悄悄吞掉。

调用图:调用 5 个内部函数(new, write_plugin_marketplace, write_plugin_source, write_plugins_enabled_config, try_from);外部调用 7 个(new, Integer, assert!, assert_eq!, create_dir_all, write, timeout)。

plugin_read_returns_plugin_details_with_bundle_contents1280–1556 ↗
async fn plugin_read_returns_plugin_details_with_bundle_contents() -> Result<()>

作用:检查本地插件详情能把一个插件包里的主要内容都读出来,包括清单、技能、钩子、应用和 MCP 服务器。钩子可以理解成在特定时机自动执行的小动作。

数据流:测试手工创建完整插件目录:plugin.json、技能文件、应用文件、MCP 配置和 hooks.json,并写入用户配置来禁用某个技能和某个钩子。读取后,测试确认描述、安装状态、展示信息、关键词、过滤后的技能、钩子列表、应用和 MCP 服务器都正确。

调用关系:测试运行器调用它。它是本文件最完整的本地插件读取验收测试,使用 write_installed_plugin 标记已安装,然后让 TestAppServer 汇总多个文件来源。

调用图:调用 3 个内部函数(new, write_installed_plugin, try_from);外部调用 8 个(new, Integer, to_response, assert!, assert_eq!, create_dir_all, write, timeout)。

plugin_read_returns_app_metadata_category1559–1660 ↗
async fn plugin_read_returns_app_metadata_category() -> Result<()>

作用:检查插件关联的应用能从连接器目录里带出分类信息。连接器可以理解成 ChatGPT 能连接的外部应用入口。

数据流:测试启动一个假的应用目录服务,返回 alpha 和 beta 两个应用,其中 alpha 带 Productivity 分类。插件本地 .app.json 引用这两个应用。读取插件后,测试确认 alpha 有分类,beta 没分类。

调用关系:测试运行器调用它。它通过 start_apps_server 启动假的 Axum HTTP 服务,用 write_connectors_config 告诉服务器去哪里查连接器。

调用图:调用 7 个内部函数(new, new, start_apps_server, write_connectors_config, write_plugin_marketplace, write_plugin_source, try_from);外部调用 7 个(new, Integer, to_response, write_chatgpt_auth, assert_eq!, timeout, vec!)。

plugin_read_hides_apps_for_api_key_auth1663–1748 ↗
async fn plugin_read_hides_apps_for_api_key_auth() -> Result<()>

作用:检查用户用 API Key 登录时,插件详情不会显示应用连接器,但仍显示 MCP 服务器。API Key 是一把接口钥匙,不等同于完整 ChatGPT 账号登录。

数据流:测试启动假的应用目录服务并写入 API Key 风格的 auth.json,同时清掉环境里的访问令牌。插件同时声明应用和 MCP 服务器。读取后,测试确认 apps 是空的,但 mcp_servers 仍包含 alpha。

调用关系:测试运行器调用它。它使用 TestAppServer::new_with_env 控制环境变量,验证认证方式会影响应用展示。

调用图:调用 6 个内部函数(new_with_env, start_apps_server, write_connectors_config, write_plugin_marketplace, write_plugin_source, try_from);外部调用 8 个(new, Integer, to_response, assert!, assert_eq!, write, timeout, vec!)。

plugin_read_accepts_legacy_string_default_prompt1751–1814 ↗
async fn plugin_read_accepts_legacy_string_default_prompt() -> Result<()>

作用:检查旧格式的 defaultPrompt 字符串仍被接受。这样老插件不用立刻改成新格式,也能正常显示默认提示。

数据流:测试创建本地插件,plugin.json 里的 interface.defaultPrompt 是一个字符串而不是字符串数组。读取后,服务器把它转换成只有一项的列表,测试确认结果正确。

调用关系:测试运行器调用它。它使用 write_plugins_enabled_config 启用插件,再让 TestAppServer 验证兼容旧清单格式。

调用图:调用 3 个内部函数(new, write_plugins_enabled_config, try_from);外部调用 7 个(new, Integer, to_response, assert_eq!, create_dir_all, write, timeout)。

plugin_read_describes_uninstalled_git_source_without_cloning1817–1884 ↗
async fn plugin_read_describes_uninstalled_git_source_without_cloning() -> Result<()>

作用:检查跨仓库 Git 插件未安装时,读取详情只给简短说明,不会偷偷克隆代码。克隆就是把远程仓库下载到本地,测试确保未安装时不会做这件重活。

数据流:测试写一个指向不存在本地目录的 git-subdir 插件来源,然后读取插件。服务器返回说明文字,提示安装后才能看更多详情,并说明源地址和子路径;测试还确认没有创建临时克隆目录。

调用关系:测试运行器调用它。它直接手写 marketplace.json,使用 TestAppServer 验证未安装跨仓库插件的轻量读取路径。

调用图:调用 3 个内部函数(new, write_plugins_enabled_config, try_from);外部调用 10 个(new, Integer, to_response, assert!, assert_eq!, format!, create_dir_all, write, timeout, from_directory_path)。

plugin_read_returns_invalid_request_when_plugin_is_missing1887–1935 ↗
async fn plugin_read_returns_invalid_request_when_plugin_is_missing() -> Result<()>

作用:检查请求的插件名不在市场文件里时,会返回清楚的无效请求。这样用户不会误以为是服务器崩了或网络坏了。

数据流:测试创建只包含 demo-plugin 的本地市场,却请求 missing-plugin。服务器查找失败并返回错误,测试确认错误码和错误信息都指向插件不存在。

调用关系:测试运行器调用它。它使用 write_plugins_enabled_config 开启插件功能,然后通过 TestAppServer 触发本地市场查找。

调用图:调用 3 个内部函数(new, write_plugins_enabled_config, try_from);外部调用 7 个(new, Integer, assert!, assert_eq!, create_dir_all, write, timeout)。

plugin_read_returns_invalid_request_when_plugin_manifest_is_missing1938–1984 ↗
async fn plugin_read_returns_invalid_request_when_plugin_manifest_is_missing() -> Result<()>

作用:检查插件目录存在但 plugin.json 清单缺失时,读取会失败并提示清单缺失或无效。plugin.json 是插件的身份证,没有它就无法可靠描述插件。

数据流:测试创建 marketplace.json 和插件目录,但不创建 .codex-plugin/plugin.json。读取该插件后,服务器返回无效请求,测试确认消息包含 missing or invalid plugin.json。

调用关系:测试运行器调用它。它手工准备残缺插件目录,用 TestAppServer 验证清单校验。

调用图:调用 3 个内部函数(new, write_plugins_enabled_config, try_from);外部调用 7 个(new, Integer, assert!, assert_eq!, create_dir_all, write, timeout)。

write_installed_plugin1986–2003 ↗
fn write_installed_plugin(
    codex_home: &TempDir,
    marketplace_name: &str,
    plugin_name: &str,
) -> Result<()>

作用:在临时用户目录里伪造一个“插件已安装”的缓存记录。测试用它让服务器认为某个插件已经安装过。

数据流:输入用户临时目录、市场名和插件名。函数创建 plugins/cache/<marketplace>/<plugin>/local/.codex-plugin 目录,并写入一个最小 plugin.json;输出是成功或失败结果,同时磁盘上多出安装缓存文件。

调用关系:它被读取官方精选市场名测试和完整本地插件详情测试调用,是这些测试搭建“已安装状态”的小工具。

调用图:被 2 处调用(plugin_read_returns_canonical_openai_curated_marketplace_name, plugin_read_returns_plugin_details_with_bundle_contents);外部调用 4 个(path, format!, create_dir_all, write)。

write_plugins_enabled_config2005–2013 ↗
fn write_plugins_enabled_config(codex_home: &TempDir) -> Result<()>

作用:写一个最简单的配置文件,只打开插件功能。很多本地插件测试不关心其他设置,就用它快速准备环境。

数据流:输入用户临时目录。函数在里面写 config.toml,内容只有 features.plugins = true;输出写文件结果,之后 TestAppServer 启动时会读到插件已启用。

调用关系:它被多个本地读取和错误场景测试调用,负责统一准备最低限度的配置。

调用图:被 6 处调用(plugin_read_accepts_legacy_string_default_prompt, plugin_read_describes_uninstalled_git_source_without_cloning, plugin_read_fails_on_malformed_share_mapping, plugin_read_falls_back_to_local_share_context_without_remote_auth, plugin_read_returns_invalid_request_when_plugin_is_missing, plugin_read_returns_invalid_request_when_plugin_manifest_is_missing);外部调用 2 个(path, write)。

start_apps_server2020–2042 ↗
async fn start_apps_server(connectors: Vec<AppInfo>) -> Result<(String, JoinHandle<()>)>

作用:启动一个假的应用连接器目录服务,供测试里的服务器查询应用信息。这样测试不用访问真实网络。

数据流:输入一组 AppInfo 应用信息。函数把它们包成共享状态,绑定本机随机端口,注册两个 HTTP 路径,然后后台启动 Axum 服务;输出服务地址和可中止的后台任务句柄。

调用关系:它被应用分类测试和 API Key 隐藏应用测试调用。真实请求到来时,它会把处理交给 list_directory_connectors。

调用图:被 2 处调用(plugin_read_hides_apps_for_api_key_auth, plugin_read_returns_app_metadata_category);外部调用 9 个(new, new, new, bind, get, serve, format!, json!, spawn)。

list_directory_connectors2044–2073 ↗
async fn list_directory_connectors(
    State(state): State<Arc<AppsServerState>>,
    headers: HeaderMap,
    uri: Uri,
) -> Result<impl axum::response::IntoResponse, StatusCode>

作用:处理假的应用目录服务请求,并检查请求头和查询参数是否像真实客户端一样正确。它保证测试不仅看结果,也看认证信息有没有带对。

数据流:输入共享的应用响应状态、HTTP 请求头和 URI。函数检查 Authorization 是否是 Bearer chatgpt-token、账号 ID 是否正确、查询里是否有 external_logos=true;通过就返回 JSON 应用列表,否则返回 401 或 400。

调用关系:它由 start_apps_server 注册到 Axum 路由上,当 TestAppServer 查询连接器目录时被 HTTP 服务自动调用。

调用图:外部调用 3 个(get, query, Json)。

write_connectors_config2075–2090 ↗
fn write_connectors_config(codex_home: &std::path::Path, base_url: &str) -> std::io::Result<()>

作用:写一份启用插件和连接器功能的配置,并把 ChatGPT 后端地址指向测试里的假服务。这样插件读取时会去本地假服务器查应用,而不是访问真实服务。

数据流:输入用户目录和 base_url。函数写 config.toml,包含 chatgpt_base_url、凭据存储方式,以及 plugins/connectors 开关;输出写文件结果。

调用关系:它被应用分类测试和 API Key 隐藏应用测试调用,通常和 start_apps_server 搭配使用。

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

write_remote_plugin_catalog_config2092–2108 ↗
fn write_remote_plugin_catalog_config(
    codex_home: &std::path::Path,
    base_url: &str,
) -> std::io::Result<()>

作用:写一份启用远程插件读取的配置,并指定远程后端地址。测试用它把远程插件请求导向 wiremock 假服务器。

数据流:输入用户目录和 base_url。函数写 config.toml,设置 chatgpt_base_url,并打开 plugins 和 remote_plugin;输出写文件结果。

调用关系:它被多个远程插件和共享插件测试调用,是这些测试进入远程读取路径的共同准备步骤。

调用图:被 7 处调用(plugin_read_includes_share_url_for_admin_disabled_remote_plugin, plugin_read_keeps_remote_version_when_share_principals_are_missing, plugin_read_maps_missing_remote_plugin_to_invalid_request, plugin_read_rejects_invalid_remote_plugin_name, plugin_read_returns_share_context_for_shared_local_plugin, plugin_read_returns_share_context_for_shared_remote_plugin, plugin_skill_read_reads_remote_skill_contents_when_remote_plugin_enabled);外部调用 3 个(join, format!, write)。

write_plugin_marketplace2110–2135 ↗
fn write_plugin_marketplace(
    repo_root: &std::path::Path,
    marketplace_name: &str,
    plugin_name: &str,
    source_path: &str,
) -> std::io::Result<()>

作用:创建一个最小的本地插件市场文件。插件市场文件就像目录,告诉系统有哪些插件、名字是什么、源码在本地哪里。

数据流:输入仓库根目录、市场名、插件名和插件源码路径。函数创建 .git 和 .agents/plugins 目录,再写 marketplace.json;输出写文件结果,之后读取请求可以按这个市场找到插件。

调用关系:它被多种本地插件测试调用,常和 write_plugin_source 或手工创建插件目录一起使用。

调用图:被 7 处调用(plugin_read_fails_on_malformed_share_mapping, plugin_read_falls_back_to_local_share_context_without_remote_auth, plugin_read_hides_apps_for_api_key_auth, plugin_read_keeps_remote_version_when_share_principals_are_missing, plugin_read_returns_app_metadata_category, plugin_read_returns_canonical_openai_curated_marketplace_name, plugin_read_returns_share_context_for_shared_local_plugin);外部调用 4 个(join, format!, create_dir_all, write)。

write_plugin_source2137–2158 ↗
fn write_plugin_source(
    repo_root: &std::path::Path,
    plugin_name: &str,
    app_ids: &[&str],
) -> Result<()>

作用:创建一个最小可读取的本地插件源码目录,并可选写入关联应用。这样测试不用每次手写重复的 plugin.json 和 .app.json。

数据流:输入仓库根目录、插件名和应用 ID 列表。函数创建插件目录和 .codex-plugin/plugin.json,再把应用 ID 组装成 .app.json;输出成功或失败结果,同时磁盘上出现插件文件。

调用关系:它被共享降级、坏映射、连接器分类和 API Key 场景调用,用来快速搭一个本地插件。

调用图:被 4 处调用(plugin_read_fails_on_malformed_share_mapping, plugin_read_falls_back_to_local_share_context_without_remote_auth, plugin_read_hides_apps_for_api_key_auth, plugin_read_returns_app_metadata_category);外部调用 6 个(join, format!, json!, to_vec_pretty, create_dir_all, write)。

write_plugin_share_local_path_mapping2160–2179 ↗
fn write_plugin_share_local_path_mapping(
    codex_home: &std::path::Path,
    remote_plugin_id: &str,
    plugin_path: &AbsolutePathBuf,
) -> std::io::Result<()>

作用:写入“远程共享插件 ID 对应哪个本地插件路径”的映射文件。这个文件让系统能把本地插件和远程分享记录连起来。

数据流:输入用户目录、远程插件 ID 和本地插件绝对路径。函数把路径序列化成 JSON,写到 .tmp/plugin-share-local-paths-v1.json;输出写文件结果,之后读取本地插件时可以找到对应的共享上下文。

调用关系:它被三个本地共享插件测试调用,用来模拟插件已经和远程分享记录建立过关联。

调用图:被 3 处调用(plugin_read_falls_back_to_local_share_context_without_remote_auth, plugin_read_keeps_remote_version_when_share_principals_are_missing, plugin_read_returns_share_context_for_shared_local_plugin);外部调用 8 个(join, format!, json!, new, to_string_pretty, to_value, create_dir_all, write)。

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

这个测试像是在临时搭了一个假的插件市场:先创建一个临时的 Codex 主目录,再在里面放出 marketplace 的目录结构、市场说明文件和一个示例插件文件。然后它启动测试用的应用服务器,发送“添加 marketplace”的请求,来源写成相对路径 ./marketplace。测试接着等服务器回一条 JSON-RPC 响应。JSON-RPC 可以理解成一种用 JSON 包起来的“请求-回答”通信格式。拿到回答后,测试检查三件关键事:服务器认出的市场名字是不是 debug;安装根目录是不是刚才那个本地目录的真实绝对路径;这次是不是第一次添加,而不是已经添加过。最后它还读回插件里的 marker.txt,确认本地文件内容没有丢。这个文件重要在于,它验证了“从本地目录添加插件市场”这条用户路径真的能走通。

函数细节1
marketplace_add_local_directory_source17–62 ↗
async fn marketplace_add_local_directory_source() -> Result<()>

作用:这是一个异步测试函数,用来模拟用户把本机的 ./marketplace 文件夹添加为插件市场。它的目的不是实现功能,而是证明服务器在这种场景下会返回正确结果,并且能保留本地插件文件。

数据流:开始时,它创建一个临时目录作为假的 Codex 工作环境,并在里面写入 marketplace.json、plugin.json 和 marker.txt。然后它启动 TestAppServer,初始化服务器,再发送 MarketplaceAddParams,其中 source 是 ./marketplace。服务器返回 JSONRPCResponse 后,它用 to_response 把通用响应拆成 MarketplaceAddResponse。最后它把返回的市场名、安装路径、是否已添加、以及磁盘上的 marker.txt 内容逐一和预期值比较;如果任何一步不对,测试就失败。

调用关系:这个函数由 Rust 的 tokio 测试框架在跑测试时调用,tokio 是异步运行环境,适合等待服务器这类异步操作。它先用 TestAppServer::new 搭好测试服务器,再通过服务器的初始化、发送请求和读取响应这些辅助方法完成一次完整交互。拿到响应后,它把解析工作交给 to_response,并用 assert_eq!、assert! 这些断言宏检查结果是否符合预期。

调用图:调用 2 个内部函数(new, from_absolute_path);外部调用 8 个(new, Integer, to_response, assert!, assert_eq!, create_dir_all, write, timeout)。

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

插件安装会碰到很多容易出错的地方:用户可能传错路径,远程服务可能不给下载地址,管理员可能禁用了插件,插件还可能声明需要连接某些 ChatGPT 应用或 MCP 服务。这个测试文件用临时目录、假服务器和假接口,把这些情况一一模拟出来。测试会启动一个测试版 app server,通过 JSON-RPC(一种用 JSON 发请求和收响应的通信格式)发送 plugin/install 请求,然后检查返回结果、错误码、磁盘上写出的文件、远程接口被调用的次数,以及分析事件是否上报。文件后半部分提供很多“小道具”:写配置文件、造插件市场文件、打包远程插件、搭建假的 connectors 应用目录服务、统计 OAuth 探测请求等。没有这些测试,插件安装流程很容易在安全校验、远程下载顺序、授权提示或缓存写入上悄悄坏掉。

函数细节62
plugin_install_rejects_relative_marketplace_paths73–97 ↗
async fn plugin_install_rejects_relative_marketplace_paths() -> Result<()>

作用:测试安装插件时不能传相对路径的 marketplace 文件。这样可以避免服务器在不明确的位置乱找文件。

数据流:进去的是一个临时主目录和一个带相对 marketplacePath 的原始请求 → 测试服务器初始化后收到请求 → 出来的是 JSON-RPC 错误,错误码是无效请求,并且消息里说明请求不合法。

调用关系:由测试运行器直接执行;它只启动 TestAppServer、发送原始 plugin/install 请求,再等待错误响应,不依赖本文件的辅助挂载函数。

调用图:调用 1 个内部函数(new);外部调用 6 个(new, Integer, assert!, assert_eq!, json!, timeout)。

plugin_install_rejects_missing_install_source100–126 ↗
async fn plugin_install_rejects_missing_install_source() -> Result<()>

作用:测试安装请求必须说明插件从哪里来。既不给本地市场路径,也不给远程市场名时,应该直接拒绝。

数据流:进去的是一个没有 marketplace_path、也没有 remote_marketplace_name 的 PluginInstallParams → 服务器检查安装来源数量 → 出来的是无效请求错误,提示必须二选一。

调用关系:由测试运行器调用;它走 TestAppServer 的正式 send_plugin_install_request 通道,验证参数校验发生在安装动作之前。

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

plugin_install_rejects_multiple_install_sources129–157 ↗
async fn plugin_install_rejects_multiple_install_sources() -> Result<()>

作用:测试安装请求不能同时声明本地市场和远程市场。这样避免同一个插件名到底按哪边安装产生歧义。

数据流:进去的是同时带 marketplace_path 和 remote_marketplace_name 的安装参数 → 服务器做来源数量检查 → 出来的是无效请求错误,提示只能提供其中一个。

调用关系:由测试运行器执行;它构造一个绝对路径后发给 TestAppServer,用来补齐“来源太多”的边界情况。

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

plugin_install_rejects_remote_marketplace_when_plugins_are_disabled160–192 ↗
async fn plugin_install_rejects_remote_marketplace_when_plugins_are_disabled() -> Result<()>

作用:测试当配置里关闭插件功能时,远程插件安装会被拒绝。这样管理员或配置开关才真正有效。

数据流:进去的是写有 plugins = false 的配置文件和一个远程插件安装请求 → 服务器读取配置并发现功能关闭 → 出来的是无效请求错误,说明远程插件安装未启用。

调用关系:由测试运行器调用;它先写 config.toml,再启动 TestAppServer,检查配置开关能挡住后续远程安装流程。

调用图:调用 1 个内部函数(new);外部调用 6 个(new, Integer, assert!, assert_eq!, write, timeout)。

plugin_install_writes_remote_plugin_to_cloud_and_cache195–289 ↗
async fn plugin_install_writes_remote_plugin_to_cloud_and_cache() -> Result<()>

作用:测试远程插件安装成功时,会先下载并写入本地缓存,再通知云端安装。它保证用户真正能在本机拿到插件文件,同时云端状态也同步。

数据流:进去的是假远程后端、假插件详情、假压缩包和临时主目录 → 测试发送远程安装请求,服务器下载 bundle、解包、写 plugin.json 和 .app.json,再调用云端安装接口 → 出来的是成功响应、本地缓存文件、正确的请求次数,并确认旧式按插件 ID 的缓存目录没有被误建。

调用关系:由测试运行器执行;它串起 configure_remote_plugin_test、mount_remote_plugin_bundle、mount_remote_plugin_detail_with_app_manifest、mount_remote_plugin_install_after_cache_write 和 send_remote_plugin_install_request,专门验证“先落盘再通知云端”的顺序。

调用图:调用 9 个内部函数(new_with_env, configure_remote_plugin_test, mount_empty_remote_installed_plugins, mount_remote_plugin_bundle, mount_remote_plugin_detail_with_app_manifest, mount_remote_plugin_install_after_cache_write, remote_plugin_bundle_tar_gz_bytes_with_contents, send_remote_plugin_install_request, wait_for_remote_plugin_request_count);外部调用 11 个(start, new, Integer, to_response, assert!, assert_eq!, format!, json!, from_str, read_to_string (+1 more))。

plugin_install_uses_remote_apps_needing_auth_response292–357 ↗
async fn plugin_install_uses_remote_apps_needing_auth_response() -> Result<()>

作用:测试远程安装接口如果直接返回哪些应用需要授权,客户端就应该采用这个结果。这样不必再额外查应用目录。

数据流:进去的是带 app_manifest 的远程插件详情、远程安装接口返回的 app_ids_needing_auth 和插件压缩包 → 服务器安装后把 alpha 转成 AppSummary → 出来的是带 alpha 授权提示的 PluginInstallResponse,并确认没有请求 connectors 目录列表。

调用关系:由测试运行器调用;它使用 configure_remote_plugin_with_apps_test 和 mount_remote_plugin_install_with_apps_needing_auth,验证远程后端结果优先于本地目录查询。

调用图:调用 9 个内部函数(new_with_env, configure_remote_plugin_with_apps_test, mount_empty_remote_installed_plugins, mount_remote_plugin_bundle, mount_remote_plugin_detail_with_app_manifest, mount_remote_plugin_install_with_apps_needing_auth, remote_plugin_bundle_tar_gz_bytes, send_remote_plugin_install_request, wait_for_remote_plugin_request_count);外部调用 7 个(start, new, Integer, to_response, assert_eq!, json!, timeout)。

plugin_install_rejects_missing_remote_bundle_url360–403 ↗
async fn plugin_install_rejects_missing_remote_bundle_url() -> Result<()>

作用:测试远程插件详情没有下载地址时不能继续安装。没有安装包地址,就像只给商品名不给取货地址。

数据流:进去的是没有 bundle_download_url 的远程插件详情 → 服务器查询详情后发现无法下载 → 出来的是内部错误,云端 install 接口没有被调用,本地缓存也没有生成。

调用关系:由测试运行器调用;它通过 mount_remote_plugin_detail 搭出缺字段响应,再用 wait_for_remote_plugin_request_count 确认安装接口没有被误触发。

调用图:调用 6 个内部函数(new, configure_remote_plugin_test, mount_empty_remote_installed_plugins, mount_remote_plugin_detail, send_remote_plugin_install_request, wait_for_remote_plugin_request_count);外部调用 7 个(start, new, Integer, assert!, assert_eq!, format!, timeout)。

plugin_install_rejects_plain_http_remote_bundle_url406–444 ↗
async fn plugin_install_rejects_plain_http_remote_bundle_url() -> Result<()>

作用:测试普通 HTTP 下载地址默认会被拒绝。这样避免插件包在网络中被轻易篡改或窃听。

数据流:进去的是一个 http:// 开头的 bundle 地址 → 服务器检查 URL 协议并拒绝 → 出来的是内部错误,远程安装接口未调用,本地没有缓存目录。

调用关系:由测试运行器执行;它和远程 bundle 挂载配合,验证除测试放行开关外,下载必须走安全协议。

调用图:调用 6 个内部函数(new, configure_remote_plugin_test, mount_empty_remote_installed_plugins, mount_remote_plugin_detail, send_remote_plugin_install_request, wait_for_remote_plugin_request_count);外部调用 7 个(start, new, Integer, assert!, assert_eq!, format!, timeout)。

plugin_install_rejects_invalid_remote_release_version447–486 ↗
async fn plugin_install_rejects_invalid_remote_release_version() -> Result<()>

作用:测试远程插件版本号不能包含 ../ 这类危险路径片段。这样防止解包或缓存时写到不该写的目录。

数据流:进去的是 release version 为 ../1.2.3 的远程详情 → 服务器做路径安全检查 → 出来的是内部错误,下载和云端安装都不会继续。

调用关系:由测试运行器调用;它用 mount_remote_plugin_detail 构造坏版本号,再用 wait_for_remote_plugin_request_count 确认后续请求没有发生。

调用图:调用 6 个内部函数(new, configure_remote_plugin_test, mount_empty_remote_installed_plugins, mount_remote_plugin_detail, send_remote_plugin_install_request, wait_for_remote_plugin_request_count);外部调用 7 个(start, new, Integer, assert!, assert_eq!, format!, timeout)。

plugin_install_rejects_invalid_remote_plugin_name489–512 ↗
async fn plugin_install_rejects_invalid_remote_plugin_name() -> Result<()>

作用:测试远程插件 ID 不能带路径穿越内容。它防止用户把插件名伪装成文件路径来影响本地文件系统。

数据流:进去的是 plugin_name 为 linear/../../oops 的远程安装请求 → 服务器在发网络请求前检查 ID 格式 → 出来的是无效请求错误,提示远程插件 ID 不合法。

调用关系:由测试运行器执行;它只写远程插件目录配置,不挂真实后端,说明这个错误应在本地参数校验阶段就被发现。

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

plugin_install_rejects_remote_plugin_disabled_by_admin_before_download515–572 ↗
async fn plugin_install_rejects_remote_plugin_disabled_by_admin_before_download() -> Result<()>

作用:测试远程插件如果被管理员禁用,服务器会在下载前停止。这样不会绕过组织策略偷偷安装。

数据流:进去的是状态为 DisabledByAdmin 的远程插件详情和一个可用 bundle 地址 → 服务器读到禁用状态后立刻报错 → 出来的是无效请求错误,bundle 下载次数和云端 install 次数都为 0,本地无缓存。

调用关系:由测试运行器调用;它使用 mount_remote_plugin_detail_with_status 模拟管理员禁用,并用计数函数确认拒绝发生在下载之前。

调用图:调用 8 个内部函数(new_with_env, configure_remote_plugin_test, mount_empty_remote_installed_plugins, mount_remote_plugin_bundle, mount_remote_plugin_detail_with_status, remote_plugin_bundle_tar_gz_bytes, send_remote_plugin_install_request, wait_for_remote_plugin_request_count);外部调用 7 个(start, new, Integer, assert!, assert_eq!, format!, timeout)。

plugin_install_rejects_when_workspace_codex_plugins_disabled575–639 ↗
async fn plugin_install_rejects_when_workspace_codex_plugins_disabled() -> Result<()>

作用:测试工作区级别禁用 Codex 插件时,本地插件也不能安装。它确认云端账号设置能管住本机行为。

数据流:进去的是启用插件的本地配置、ChatGPT 登录信息、一个本地插件市场,以及后端返回 enable_plugins=false 的账号设置 → 服务器安装前查询设置 → 出来的是无效请求错误,提示该工作区禁用了 Codex 插件。

调用关系:由测试运行器调用;它用 wiremock 挂出账号 settings 接口,再通过 write_plugin_marketplace 和 write_plugin_source 准备本地插件。

调用图:调用 6 个内部函数(new, new, write_plugin_marketplace, write_plugin_source, write_plugins_enabled_config_with_base_url, try_from);外部调用 13 个(given, start, new, new, Integer, write_chatgpt_auth, assert!, assert_eq!, format!, timeout (+3 more))。

plugin_install_returns_invalid_request_for_missing_marketplace_file642–667 ↗
async fn plugin_install_returns_invalid_request_for_missing_marketplace_file() -> Result<()>

作用:测试本地 marketplace 文件不存在时会返回清楚的无效请求错误。这样用户能知道是文件路径问题,而不是安装器坏了。

数据流:进去的是一个指向 missing-marketplace.json 的绝对路径 → 服务器检查文件存在性 → 出来的是错误码 -32600,消息包含 marketplace file 和 does not exist。

调用关系:由测试运行器执行;它通过 TestAppServer 走正常安装入口,只覆盖文件缺失这个早期失败点。

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

plugin_install_returns_invalid_request_for_not_available_plugin670–705 ↗
async fn plugin_install_returns_invalid_request_for_not_available_plugin() -> Result<()>

作用:测试市场文件里标记为 NOT_AVAILABLE 的插件不能安装。这样市场发布方可以下架或隐藏插件。

数据流:进去的是一个本地 marketplace,其中插件安装策略为 NOT_AVAILABLE → 服务器读取市场和插件源后检查策略 → 出来的是无效请求错误,说明该插件不可安装。

调用关系:由测试运行器调用;它用 write_plugin_marketplace 写出特殊策略,再用 write_plugin_source 准备最小插件目录。

调用图:调用 4 个内部函数(new, write_plugin_marketplace, write_plugin_source, try_from);外部调用 5 个(new, Integer, assert!, assert_eq!, timeout)。

plugin_install_returns_invalid_request_for_disallowed_product_plugin708–755 ↗
async fn plugin_install_returns_invalid_request_for_disallowed_product_plugin() -> Result<()>

作用:测试插件只允许某个产品使用时,其他产品来源不能安装。这里模拟 atlas 会话来源安装只给 CHATGPT 的插件。

数据流:进去的是 marketplace 中 policy.products 只包含 CHATGPT、以及用 --session-source atlas 启动的服务器 → 服务器比对当前产品来源 → 出来的是无效请求错误,提示不可安装。

调用关系:由测试运行器执行;它手写 marketplace JSON,再用 TestAppServer::new_with_args 改变会话来源,验证产品限制生效。

调用图:调用 3 个内部函数(new_with_args, write_plugin_source, try_from);外部调用 7 个(new, Integer, assert!, assert_eq!, create_dir_all, write, timeout)。

plugin_install_tracks_analytics_event758–821 ↗
async fn plugin_install_tracks_analytics_event() -> Result<()>

作用:测试本地插件安装成功后会上报一条分析事件。分析事件就是给后台统计用的记录,比如“谁装了什么插件”。

数据流:进去的是假分析服务器、登录信息、本地插件市场和插件源 → 安装成功后服务器向分析接口 POST 事件 → 出来的是读取到的事件 JSON,并断言插件名、市场名、技能数量、连接器列表等字段正确。

调用关系:由测试运行器调用;它使用 write_analytics_config 指向假服务,最后通过 wait_for_plugin_analytics_payload 等到上报内容。

调用图:调用 7 个内部函数(new, new, wait_for_plugin_analytics_payload, write_analytics_config, write_plugin_marketplace, write_plugin_source, try_from);外部调用 7 个(new, Integer, start_analytics_events_server, to_response, write_chatgpt_auth, assert_eq!, timeout)。

plugin_install_tracks_remote_plugin_analytics_event824–874 ↗
async fn plugin_install_tracks_remote_plugin_analytics_event() -> Result<()>

作用:测试远程插件安装成功后也会上报分析事件。它确保远程插件和本地插件在统计系统里都能被看到。

数据流:进去的是远程插件详情、bundle、云端安装接口和分析接口 mock → 服务器下载并安装后发送分析事件 → 出来的是事件 JSON,字段显示远程插件 ID、linear 名称、有技能、无 MCP server 和连接器。

调用关系:由测试运行器执行;它把 configure_remote_plugin_test、mount_backend_analytics_events、mount_remote_plugin_install 等辅助函数串起来,验证完整远程成功路径。

调用图:调用 10 个内部函数(new_with_env, configure_remote_plugin_test, mount_backend_analytics_events, mount_empty_remote_installed_plugins, mount_remote_plugin_bundle, mount_remote_plugin_detail, mount_remote_plugin_install, remote_plugin_bundle_tar_gz_bytes, send_remote_plugin_install_request, wait_for_plugin_analytics_payload);外部调用 6 个(start, new, Integer, to_response, assert_eq!, timeout)。

plugin_install_errors_when_remote_bundle_download_fails877–928 ↗
async fn plugin_install_errors_when_remote_bundle_download_fails() -> Result<()>

作用:测试远程插件包下载失败时,安装会停止并报错。这样不会把半成品插件写进系统。

数据流:进去的是返回 503 的 bundle 下载接口和正常插件详情 → 服务器尝试下载,收到失败状态 → 出来的是内部错误,云端 install 没有调用,本地缓存目录不存在。

调用关系:由测试运行器调用;它用 mount_remote_plugin_bundle 设置失败响应,再用 wait_for_remote_plugin_request_count 验证只发生了下载请求。

调用图:调用 8 个内部函数(new_with_env, configure_remote_plugin_test, mount_empty_remote_installed_plugins, mount_remote_plugin_bundle, mount_remote_plugin_detail, mount_remote_plugin_install, send_remote_plugin_install_request, wait_for_remote_plugin_request_count);外部调用 7 个(start, new, Integer, assert!, assert_eq!, format!, timeout)。

plugin_install_returns_apps_needing_auth931–1028 ↗
async fn plugin_install_returns_apps_needing_auth() -> Result<()>

作用:测试本地插件声明需要某些应用时,安装响应会告诉用户哪些应用还要授权。这样界面可以引导用户去连接缺少权限的应用。

数据流:进去的是假应用目录,其中 alpha、beta 不可访问,另有 MCP 工具表明 beta 已由工具暴露;插件声明需要 alpha 和 beta → 服务器安装后查询目录并过滤 → 出来的是只包含 alpha 的 apps_needing_auth,授权策略为 OnInstall。

调用关系:由测试运行器调用;它通过 start_apps_server 启动假 connectors 服务,用 connector_tool 模拟 beta 工具,再检查目录请求计数增加。

调用图:调用 7 个内部函数(new, new, start_apps_server, write_connectors_config, write_plugin_marketplace, write_plugin_source, try_from);外部调用 8 个(new, Integer, to_response, write_chatgpt_auth, assert!, assert_eq!, timeout, vec!)。

plugin_install_skips_mcp_oauth_for_chatgpt_dual_surface_plugin1031–1099 ↗
async fn plugin_install_skips_mcp_oauth_for_chatgpt_dual_surface_plugin() -> Result<()>

作用:测试当同一个插件既有 ChatGPT 应用入口又有 MCP 配置时,如果 ChatGPT 应用已经覆盖它,就不启动 MCP OAuth。OAuth 是一种让用户授权第三方服务访问账号的登录流程。

数据流:进去的是包含 sample-mcp connector 的应用目录、一个 OAuth mock 服务、插件 app 声明和 .mcp.json → 安装时服务器识别这是双入口插件并跳过 MCP OAuth 探测 → 出来的是 OnInstall 响应,OAuth 发现请求次数为 0。

调用关系:由测试运行器执行;它依赖 start_apps_server、write_plugin_mcp_config 和 oauth_discovery_request_count,验证不会重复走授权发现。

调用图:调用 8 个内部函数(new, new, start_apps_server, write_connectors_config, write_plugin_marketplace, write_plugin_mcp_config, write_plugin_source, try_from);外部调用 9 个(start, new, new, Integer, to_response, write_chatgpt_auth, assert_eq!, timeout, vec!)。

plugin_install_starts_mcp_oauth_when_only_plugin_apps_are_disallowed1102–1160 ↗
async fn plugin_install_starts_mcp_oauth_when_only_plugin_apps_are_disallowed() -> Result<()>

作用:测试当插件声明的应用不被允许或不在目录里时,仍会启动 MCP OAuth。这样用户还有机会通过 MCP 服务完成授权。

数据流:进去的是空应用目录、一个插件声明的 app id、以及 HTTP 类型 MCP 配置 → 安装后没有 apps_needing_auth,但服务器去探测 OAuth 元数据 → 出来的是成功响应,并且 OAuth 发现请求次数大于 0。

调用关系:由测试运行器调用;它用空 start_apps_server 模拟没有可用 ChatGPT 应用,转而检查 oauth_discovery_request_count。

调用图:调用 8 个内部函数(new, new, start_apps_server, write_connectors_config, write_plugin_marketplace, write_plugin_mcp_config, write_plugin_source, try_from);外部调用 9 个(start, new, new, Integer, to_response, write_chatgpt_auth, assert!, assert_eq!, timeout)。

plugin_install_starts_mcp_oauth_for_api_key_dual_surface_plugin1163–1215 ↗
async fn plugin_install_starts_mcp_oauth_for_api_key_dual_surface_plugin() -> Result<()>

作用:测试用 API key 登录时,即使插件是双入口,也会启动 MCP OAuth。因为这时没有 ChatGPT 账号应用目录可替代授权。

数据流:进去的是启用 plugins 和 connectors 的配置、OPENAI_API_KEY 环境变量、本地插件和 MCP 配置 → 安装后服务器探测 OAuth 服务 → 出来的是 OnInstall 响应,OAuth 发现请求数大于 0。

调用关系:由测试运行器执行;它用 TestAppServer::new_with_env 注入 API key,并用 write_plugin_mcp_config 准备 HTTP MCP 服务。

调用图:调用 5 个内部函数(new_with_env, write_plugin_marketplace, write_plugin_mcp_config, write_plugin_source, try_from);外部调用 8 个(start, new, Integer, to_response, assert!, assert_eq!, write, timeout)。

plugin_install_starts_remote_mcp_oauth_for_install_response_only_app1218–1263 ↗
async fn plugin_install_starts_remote_mcp_oauth_for_install_response_only_app() -> Result<()>

作用:测试远程插件安装响应里说某个应用需要授权,但 bundle 里没有同名 app 时,会启动远程 MCP OAuth。这样远程插件仍能触发必要的授权流程。

数据流:进去的是带 .mcp.json 的远程插件 bundle、远程安装接口返回 alpha 需要授权 → 服务器安装、返回 alpha 的 AppSummary,并探测 OAuth → 出来的是 OnUse 响应和大于 0 的 OAuth 发现请求数。

调用关系:由测试运行器调用;它使用 remote_plugin_bundle_tar_gz_bytes_with_mcp_config 造包,并借 mount_remote_plugin_install_with_apps_needing_auth 提供后端授权列表。

调用图:调用 8 个内部函数(new_with_env, configure_remote_plugin_with_apps_test, mount_empty_remote_installed_plugins, mount_remote_plugin_bundle, mount_remote_plugin_detail, mount_remote_plugin_install_with_apps_needing_auth, remote_plugin_bundle_tar_gz_bytes_with_mcp_config, send_remote_plugin_install_request);外部调用 7 个(start, new, Integer, to_response, assert!, assert_eq!, timeout)。

plugin_install_skips_remote_mcp_oauth_for_bundled_same_name_app1266–1315 ↗
async fn plugin_install_skips_remote_mcp_oauth_for_bundled_same_name_app() -> Result<()>

作用:测试远程 bundle 里已经带了与 MCP 同名的 app 时,不再启动 MCP OAuth。这样避免同一授权入口被重复触发。

数据流:进去的是同时带 .app.json 和 .mcp.json 的远程 bundle,安装接口返回 alpha 需要授权 → 服务器安装并返回 alpha 授权提示 → 出来的是成功响应,但 OAuth 发现请求数为 0。

调用关系:由测试运行器执行;它通过 remote_plugin_bundle_tar_gz_bytes_with_app_and_mcp_config 制造“同包同名”场景,最后用 oauth_discovery_request_count 验证跳过。

调用图:调用 8 个内部函数(new_with_env, configure_remote_plugin_with_apps_test, mount_empty_remote_installed_plugins, mount_remote_plugin_bundle, mount_remote_plugin_detail, mount_remote_plugin_install_with_apps_needing_auth, remote_plugin_bundle_tar_gz_bytes_with_app_and_mcp_config, send_remote_plugin_install_request);外部调用 6 个(start, new, Integer, to_response, assert_eq!, timeout)。

plugin_install_filters_disallowed_apps_needing_auth1318–1406 ↗
async fn plugin_install_filters_disallowed_apps_needing_auth() -> Result<()>

作用:测试安装响应里的待授权应用会过滤掉不允许的应用。用户只会看到真正能去授权的应用。

数据流:进去的是应用目录里只有 alpha、插件声明 alpha 和一个不允许的 asdk_app id、并把 auth_policy 设为 ON_USE → 先预热应用目录缓存,再安装插件 → 出来的是只包含 alpha 的 apps_needing_auth,并确认安装时没有再次请求目录。

调用关系:由测试运行器调用;它先调用 warm_app_directory_cache,再执行安装,用 AppsServerControl 的计数证明缓存被复用。

调用图:调用 8 个内部函数(new, new, start_apps_server, warm_app_directory_cache, write_connectors_config, write_plugin_marketplace, write_plugin_source, try_from);外部调用 8 个(new, new, Integer, to_response, write_chatgpt_auth, assert_eq!, timeout, vec!)。

plugin_install_makes_bundled_mcp_servers_available_to_followup_requests1409–1479 ↗
async fn plugin_install_makes_bundled_mcp_servers_available_to_followup_requests() -> Result<()>

作用:测试插件自带的 MCP server 安装后能被后续请求识别,但不会直接写进全局 config.toml。这样插件里的服务是随插件可用,不污染用户配置。

数据流:进去的是一个本地插件,里面带 .mcp.json,声明 sample-mcp 命令为 echo → 安装后读取 config.toml,确认没有写入 mcp_servers 配置 → 再发起 mcpServer/oauth/login 请求,服务器识别 sample-mcp 并返回该类型不支持 OAuth 的错误。

调用关系:由测试运行器执行;它用 write_plugin_marketplace 和 write_plugin_source 准备插件,再通过后续 raw request 验证安装结果已进入运行态。

调用图:调用 4 个内部函数(new, write_plugin_marketplace, write_plugin_source, try_from);外部调用 9 个(new, Integer, to_response, assert!, assert_eq!, json!, read_to_string, write, timeout)。

AppsServerControl::directory_request_count1493–1495 ↗
fn directory_request_count(&self) -> usize

作用:读取假应用目录服务器收到过多少次目录请求。测试用它判断服务器有没有多查或少查后端。

数据流:进去的是 AppsServerControl 里保存的原子计数器(一种可以安全跨任务累加的数字)→ 函数按顺序一致模式读取它 → 出来的是当前请求次数,不改动任何状态。

调用关系:被 warm_app_directory_cache 和多个测试间接使用;它是观察 start_apps_server 行为的小窗口。

warm_app_directory_cache1498–1524 ↗
async fn warm_app_directory_cache(
    mcp: &mut TestAppServer,
    server_control: &AppsServerControl,
    expected_app_name: &str,
) -> Result<usize>

作用:提前请求一次应用列表,把应用目录缓存热起来。这样后续测试可以验证安装时是否复用了缓存,而不是又去请求后端。

数据流:进去的是 TestAppServer、请求计数控制器和期望出现的应用名 → 它发送 apps/list 且强制刷新,读取响应并确认应用存在,再读取请求计数 → 出来的是刷新后的目录请求次数。

调用关系:被 plugin_install_filters_disallowed_apps_needing_auth 调用;它把活交给 TestAppServer 的 apps/list 请求,再通过 AppsServerControl::directory_request_count 做计数检查。

调用图:调用 2 个内部函数(read_stream_until_response_message, send_apps_list_request);被 1 处调用(plugin_install_filters_disallowed_apps_needing_auth);外部调用 6 个(default, Integer, directory_request_count, to_response, assert!, timeout)。

PluginInstallMcpServer::get_info1532–1534 ↗
fn get_info(&self) -> ServerInfo

作用:告诉客户端这个假 MCP 服务器支持工具列表。MCP 可以理解成让模型调用外部工具的一套协议。

数据流:进去的是这个假服务器实例 → 它构造带 tools 能力的 ServerInfo → 出来的是服务器能力说明,没有读取或修改工具列表。

调用关系:由 rmcp 框架在客户端连接假 MCP 服务时调用;它配合 PluginInstallMcpServer::list_tools,让测试里的假服务看起来像真实 MCP 服务。

调用图:外部调用 2 个(builder, new)。

PluginInstallMcpServer::list_tools1536–1554 ↗
fn list_tools(
        &self,
        _request: Option<rmcp::model::PaginatedRequestParams>,
        _context: rmcp::service::RequestContext<rmcp::service::RoleServer>,
    ) -> impl std::future::Futu

作用:返回这个假 MCP 服务器当前提供的工具。测试用它模拟某个 connector 已经通过 MCP 工具暴露出来。

数据流:进去的是可选分页请求和请求上下文,但函数不使用它们 → 它加锁读取 tools 列表并复制一份 → 出来的是 ListToolsResult,里面有工具列表、没有下一页。

调用关系:由 rmcp 框架在客户端请求工具列表时调用;工具数据来自 start_apps_server 创建时传入的 Vec<Tool>。

start_apps_server1557–1601 ↗
async fn start_apps_server(
    connectors: Vec<AppInfo>,
    tools: Vec<Tool>,
) -> Result<(String, JoinHandle<()>, AppsServerControl)>

作用:启动一个假的 ChatGPT 应用目录和 MCP 服务。测试不需要连真实后端,就能模拟 connectors 列表和工具列表。

数据流:进去的是一组 AppInfo 和一组 Tool → 它创建共享状态、绑定本地随机端口、配置两个目录接口和一个 MCP HTTP 服务,然后后台启动 axum 服务器 → 出来的是服务 URL、后台任务句柄和请求计数控制器。

调用关系:被多个插件安装测试调用;它把目录请求交给 list_directory_connectors,把 MCP 能力交给 PluginInstallMcpServer。

调用图:被 4 处调用(plugin_install_filters_disallowed_apps_needing_auth, plugin_install_returns_apps_needing_auth, plugin_install_skips_mcp_oauth_for_chatgpt_dual_surface_plugin, plugin_install_starts_mcp_oauth_when_only_plugin_apps_are_disallowed);外部调用 13 个(new, new, default, new, new, default, new, bind, get, serve (+3 more))。

list_directory_connectors1603–1634 ↗
async fn list_directory_connectors(
    State(state): State<Arc<AppsServerState>>,
    headers: HeaderMap,
    uri: Uri,
) -> Result<impl axum::response::IntoResponse, StatusCode>

作用:处理假应用目录的 HTTP 请求。它检查请求是否带了正确登录信息和参数,然后返回预设应用列表。

数据流:进去的是共享状态、HTTP 头和 URI → 它先把目录请求计数加一,再检查 Authorization、chatgpt-account-id 和 external_logos=true → 出来要么是未授权/坏请求状态码,要么是 JSON 应用列表。

调用关系:由 start_apps_server 挂到 /connectors/directory/list 和 /connectors/directory/list_workspace;测试服务器查询 connectors 时会打到这里。

调用图:外部调用 3 个(get, query, Json)。

connector_tool1636–1655 ↗
fn connector_tool(connector_id: &str, connector_name: &str) -> Result<Tool>

作用:造一个假 MCP 工具,并在工具元数据里写上它属于哪个 connector。测试用它判断某个应用是否已通过工具可用。

数据流:进去的是 connector_id 和 connector_name → 它创建一个空参数 schema、设置工具名和只读标记,再把 connector_id/name 放进 meta → 出来的是 Tool 对象。

调用关系:被 plugin_install_returns_apps_needing_auth 用来准备 start_apps_server 的工具列表,帮助测试过滤已由 MCP 暴露的 connector。

调用图:外部调用 9 个(new, Borrowed, Owned, new, new, format!, json!, new, from_value)。

write_connectors_config1657–1670 ↗
fn write_connectors_config(codex_home: &std::path::Path, base_url: &str) -> std::io::Result<()>

作用:写一份启用 connectors 的测试配置。它让 app server 知道假 ChatGPT 后端地址在哪里。

数据流:进去的是 codex_home 路径和 base_url → 它把 chatgpt_base_url、OAuth 凭据存储方式和 features.connectors=true 写入 config.toml → 出来是文件写入结果。

调用关系:被多项需要应用目录的测试调用;写好配置后,TestAppServer 启动时会读到这些设置。

调用图:被 4 处调用(plugin_install_filters_disallowed_apps_needing_auth, plugin_install_returns_apps_needing_auth, plugin_install_skips_mcp_oauth_for_chatgpt_dual_surface_plugin, plugin_install_starts_mcp_oauth_when_only_plugin_apps_are_disallowed);外部调用 3 个(join, format!, write)。

write_plugins_enabled_config_with_base_url1672–1686 ↗
fn write_plugins_enabled_config_with_base_url(
    codex_home: &std::path::Path,
    base_url: &str,
) -> std::io::Result<()>

作用:写一份启用插件功能并指定后端地址的测试配置。它用于测试本地插件安装时还会去查工作区设置。

数据流:进去的是 codex_home 和 base_url → 它生成 config.toml,包含 chatgpt_base_url 和 features.plugins=true → 出来是写文件结果。

调用关系:被 plugin_install_rejects_when_workspace_codex_plugins_disabled 调用,配合假 settings 接口验证工作区禁用逻辑。

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

write_analytics_config1688–1693 ↗
fn write_analytics_config(codex_home: &std::path::Path, base_url: &str) -> std::io::Result<()>

作用:写一份只指定 ChatGPT 后端地址的配置,让分析事件发到假服务器。它用于验证统计上报。

数据流:进去的是 codex_home 和分析服务 base_url → 它写入 chatgpt_base_url 到 config.toml → 出来是写文件结果。

调用关系:被 plugin_install_tracks_analytics_event 调用;随后 wait_for_plugin_analytics_payload 会从同一个假服务器读取上报内容。

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

mount_backend_analytics_events1695–1701 ↗
async fn mount_backend_analytics_events(server: &MockServer)

作用:在假后端上挂一个分析事件接收接口。它让服务器 POST 统计事件时能得到成功响应。

数据流:进去的是 MockServer → 它注册 POST /backend-api/codex/analytics-events/events 的匹配规则,并返回 200 与 {status: ok} → 出来没有普通返回值,只改变 mock 服务器规则。

调用关系:被 plugin_install_tracks_remote_plugin_analytics_event 调用;和 wait_for_plugin_analytics_payload 一起验证远程安装的统计上报。

调用图:被 1 处调用(plugin_install_tracks_remote_plugin_analytics_event);外部调用 4 个(given, new, method, path)。

wait_for_plugin_analytics_payload1703–1724 ↗
async fn wait_for_plugin_analytics_payload(server: &MockServer) -> Result<serde_json::Value>

作用:等待并取出插件安装的分析事件请求体。因为上报可能是异步的,所以它会循环等一会儿。

数据流:进去的是 MockServer → 它反复查看已收到请求,寻找路径以 /codex/analytics-events/events 结尾的 POST → 出来的是解析后的 JSON;如果一直等不到或 JSON 坏了,就返回错误。

调用关系:被本地和远程分析事件测试调用;它不发请求,只观察 mount_backend_analytics_events 或 analytics test server 收到的请求。

调用图:被 2 处调用(plugin_install_tracks_analytics_event, plugin_install_tracks_remote_plugin_analytics_event);外部调用 5 个(from_millis, received_requests, from_slice, sleep, timeout)。

oauth_discovery_request_count1726–1734 ↗
async fn oauth_discovery_request_count(server: &MockServer) -> usize

作用:统计假 OAuth 服务收到过多少次 OAuth 发现请求。OAuth 发现请求通常用来读取授权服务器的元数据。

数据流:进去的是 MockServer → 它读取所有已记录请求,筛选 URL 路径包含 oauth-authorization-server 的请求 → 出来的是数量。

调用关系:被多项 MCP OAuth 相关测试调用;它帮助判断安装流程到底有没有启动 OAuth 探测。

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

write_remote_plugin_catalog_config1736–1752 ↗
fn write_remote_plugin_catalog_config(
    codex_home: &std::path::Path,
    base_url: &str,
) -> std::io::Result<()>

作用:写一份启用远程插件功能的配置。它告诉测试服务器远程插件目录接口在哪个假后端。

数据流:进去的是 codex_home 和 base_url → 它写 config.toml,包含 chatgpt_base_url、plugins=true、remote_plugin=true → 出来是写文件结果。

调用关系:被 configure_remote_plugin_test 调用,也被 invalid remote plugin name 测试直接调用;它是远程插件测试的基础配置。

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

configure_remote_plugin_test1754–1764 ↗
fn configure_remote_plugin_test(codex_home: &std::path::Path, server: &MockServer) -> Result<()>

作用:一次性准备远程插件测试所需的配置和登录凭据。这样每个远程插件测试不用重复写同样样板代码。

数据流:进去的是 codex_home 和 MockServer → 它把后端地址写进远程插件配置,再写入固定的 ChatGPT token、账号 ID 和用户 ID → 出来是成功或错误。

调用关系:被多条远程插件测试调用;内部先调用 write_remote_plugin_catalog_config,再调用测试支持库的 write_chatgpt_auth。

调用图:调用 2 个内部函数(new, write_remote_plugin_catalog_config);被 7 处调用(plugin_install_errors_when_remote_bundle_download_fails, plugin_install_rejects_invalid_remote_release_version, plugin_install_rejects_missing_remote_bundle_url, plugin_install_rejects_plain_http_remote_bundle_url, plugin_install_rejects_remote_plugin_disabled_by_admin_before_download, plugin_install_tracks_remote_plugin_analytics_event, plugin_install_writes_remote_plugin_to_cloud_and_cache);外部调用 2 个(write_chatgpt_auth, format!)。

configure_remote_plugin_with_apps_test1766–1792 ↗
fn configure_remote_plugin_with_apps_test(
    codex_home: &std::path::Path,
    server: &MockServer,
) -> Result<()>

作用:准备既启用远程插件、又启用 connectors 的测试环境。它用于那些需要判断应用授权的远程插件场景。

数据流:进去的是 codex_home 和 MockServer → 它写 config.toml,打开 plugins、remote_plugin、connectors,再写入固定 ChatGPT 登录信息 → 出来是配置和凭据写入结果。

调用关系:被远程 apps_needing_auth 和远程 MCP OAuth 测试调用;它相当于 configure_remote_plugin_test 的“带应用目录版”。

调用图:调用 1 个内部函数(new);被 3 处调用(plugin_install_skips_remote_mcp_oauth_for_bundled_same_name_app, plugin_install_starts_remote_mcp_oauth_for_install_response_only_app, plugin_install_uses_remote_apps_needing_auth_response);外部调用 4 个(join, write_chatgpt_auth, format!, write)。

mount_remote_plugin_bundle1794–1809 ↗
async fn mount_remote_plugin_bundle(
    server: &MockServer,
    status_code: u16,
    body: Vec<u8>,
) -> String

作用:在假服务器上挂一个远程插件压缩包下载地址。测试可以控制这个下载是成功还是失败。

数据流:进去的是 MockServer、HTTP 状态码和压缩包字节 → 它注册 GET /bundles/linear.tar.gz,返回 gzip 内容类型和指定 body → 出来的是这个 bundle 的 URL 字符串。

调用关系:被多个远程插件测试调用;生成的 URL 会放进 mount_remote_plugin_detail 返回的插件详情里。

调用图:被 7 处调用(plugin_install_errors_when_remote_bundle_download_fails, plugin_install_rejects_remote_plugin_disabled_by_admin_before_download, plugin_install_skips_remote_mcp_oauth_for_bundled_same_name_app, plugin_install_starts_remote_mcp_oauth_for_install_response_only_app, plugin_install_tracks_remote_plugin_analytics_event, plugin_install_uses_remote_apps_needing_auth_response, plugin_install_writes_remote_plugin_to_cloud_and_cache);外部调用 5 个(given, new, format!, method, path)。

mount_remote_plugin_detail1811–1825 ↗
async fn mount_remote_plugin_detail(
    server: &MockServer,
    remote_plugin_id: &str,
    release_version: &str,
    bundle_download_url: Option<&str>,
)

作用:挂一个默认可安装的远程插件详情接口。它是最常用的远程插件详情 mock。

数据流:进去的是服务器、插件 ID、版本号和可选下载 URL → 它把状态固定为 Available,并转交给 mount_remote_plugin_detail_with_status → 出来是 mock 规则被注册。

调用关系:被多条远程插件测试调用;它是 mount_remote_plugin_detail_with_status_and_app_manifest 的简化入口。

调用图:调用 1 个内部函数(mount_remote_plugin_detail_with_status);被 7 处调用(plugin_install_errors_when_remote_bundle_download_fails, plugin_install_rejects_invalid_remote_release_version, plugin_install_rejects_missing_remote_bundle_url, plugin_install_rejects_plain_http_remote_bundle_url, plugin_install_skips_remote_mcp_oauth_for_bundled_same_name_app, plugin_install_starts_remote_mcp_oauth_for_install_response_only_app, plugin_install_tracks_remote_plugin_analytics_event)。

mount_remote_plugin_detail_with_app_manifest1827–1843 ↗
async fn mount_remote_plugin_detail_with_app_manifest(
    server: &MockServer,
    remote_plugin_id: &str,
    release_version: &str,
    bundle_download_url: Option<&str>,
    app_manifest: serde_js

作用:挂一个带 app_manifest 的远程插件详情接口。app_manifest 描述插件里包含哪些应用。

数据流:进去的是服务器、插件 ID、版本、下载 URL 和 app_manifest JSON → 它把状态设为 Available,并把 manifest 包进详情响应 → 出来是 mock 规则被注册。

调用关系:被需要验证应用清单写入或授权提示的远程测试调用;内部交给 mount_remote_plugin_detail_with_status_and_app_manifest。

调用图:调用 1 个内部函数(mount_remote_plugin_detail_with_status_and_app_manifest);被 2 处调用(plugin_install_uses_remote_apps_needing_auth_response, plugin_install_writes_remote_plugin_to_cloud_and_cache)。

mount_remote_plugin_detail_with_status1845–1861 ↗
async fn mount_remote_plugin_detail_with_status(
    server: &MockServer,
    remote_plugin_id: &str,
    release_version: &str,
    bundle_download_url: Option<&str>,
    status: PluginAvailability,

作用:挂一个可指定可用状态的远程插件详情接口。它用于模拟插件被管理员禁用等状态。

数据流:进去的是服务器、插件 ID、版本、下载 URL 和 PluginAvailability → 它不提供 app_manifest,只把状态传给底层详情挂载函数 → 出来是 mock 规则。

调用关系:被 mount_remote_plugin_detail 和管理员禁用测试调用;它封装了“状态不同、结构相同”的详情响应。

调用图:调用 1 个内部函数(mount_remote_plugin_detail_with_status_and_app_manifest);被 2 处调用(mount_remote_plugin_detail, plugin_install_rejects_remote_plugin_disabled_by_admin_before_download)。

mount_remote_plugin_detail_with_status_and_app_manifest1863–1912 ↗
async fn mount_remote_plugin_detail_with_status_and_app_manifest(
    server: &MockServer,
    remote_plugin_id: &str,
    release_version: &str,
    bundle_download_url: Option<&str>,
    status: Plu

作用:真正生成远程插件详情响应的底层函数。它可以同时控制状态、版本、下载地址和应用清单。

数据流:进去的是 mock 服务器、插件 ID、版本、可选 bundle URL、可用状态和可选 app_manifest → 它拼出插件详情 JSON,并注册带鉴权头和 includeDownloadUrls=true 的 GET 匹配规则 → 出来是可被测试服务器访问的详情接口。

调用关系:被多个更简单的 mount_remote_plugin_detail* 包装函数调用;它是远程插件详情 mock 的核心。

调用图:被 2 处调用(mount_remote_plugin_detail_with_app_manifest, mount_remote_plugin_detail_with_status);外部调用 7 个(given, new, format!, header, method, path, query_param)。

mount_empty_remote_installed_plugins1914–1931 ↗
async fn mount_empty_remote_installed_plugins(server: &MockServer)

作用:挂一个“当前没有已安装远程插件”的接口。它让安装流程从干净状态开始。

数据流:进去的是 MockServer → 它注册 GET /backend-api/ps/plugins/installed,要求 scope=GLOBAL 和正确鉴权头,返回空 plugins 列表 → 出来是 mock 规则被加入服务器。

调用关系:被多条远程安装测试调用;安装流程查询已安装列表时会命中这个假接口。

调用图:被 10 处调用(plugin_install_errors_when_remote_bundle_download_fails, plugin_install_rejects_invalid_remote_release_version, plugin_install_rejects_missing_remote_bundle_url, plugin_install_rejects_plain_http_remote_bundle_url, plugin_install_rejects_remote_plugin_disabled_by_admin_before_download, plugin_install_skips_remote_mcp_oauth_for_bundled_same_name_app, plugin_install_starts_remote_mcp_oauth_for_install_response_only_app, plugin_install_tracks_remote_plugin_analytics_event, plugin_install_uses_remote_apps_needing_auth_response, plugin_install_writes_remote_plugin_to_cloud_and_cache);外部调用 6 个(given, new, header, method, path, query_param)。

mount_remote_plugin_install1933–1946 ↗
async fn mount_remote_plugin_install(server: &MockServer, remote_plugin_id: &str)

作用:挂一个远程插件安装成功接口。它模拟云端接受安装请求并返回 enabled=true。

数据流:进去的是 MockServer 和远程插件 ID → 它注册 POST /backend-api/ps/plugins/{id}/install,检查鉴权头,返回插件 ID 和 enabled=true → 出来是 mock 规则。

调用关系:被远程安装成功和下载失败测试调用;下载失败测试用它确认失败时这个接口没有真正被调用。

调用图:被 2 处调用(plugin_install_errors_when_remote_bundle_download_fails, plugin_install_tracks_remote_plugin_analytics_event);外部调用 6 个(given, new, format!, header, method, path)。

mount_remote_plugin_install_with_apps_needing_auth1948–1967 ↗
async fn mount_remote_plugin_install_with_apps_needing_auth(
    server: &MockServer,
    remote_plugin_id: &str,
    app_ids_needing_auth: &[&str],
)

作用:挂一个安装成功并返回待授权应用列表的远程接口。它模拟后端直接告诉客户端哪些 app 还需要用户授权。

数据流:进去的是服务器、插件 ID 和 app id 列表 → 它注册带 includeAppsNeedingAuth=true 的 POST install 接口,返回 enabled=true 与 app_ids_needing_auth → 出来是 mock 规则。

调用关系:被远程 apps_needing_auth 和远程 MCP OAuth 测试调用;安装流程会把这些 app id 转成用户能看的 AppSummary。

调用图:被 3 处调用(plugin_install_skips_remote_mcp_oauth_for_bundled_same_name_app, plugin_install_starts_remote_mcp_oauth_for_install_response_only_app, plugin_install_uses_remote_apps_needing_auth_response);外部调用 8 个(given, new, format!, json!, header, method, path, query_param)。

CacheManifestExists::matches1975–1977 ↗
fn matches(&self, _request: &Request) -> bool

作用:自定义一个 mock 匹配条件:只有本地缓存里的插件 manifest 文件已经存在时,远程安装接口才算匹配。它用来验证安装顺序。

数据流:进去的是一次 HTTP 请求,但函数不看请求内容 → 它检查保存的 manifest_path 是否是文件 → 出来是 true 或 false,不修改任何东西。

调用关系:被 mount_remote_plugin_install_after_cache_write 注册到 wiremock;plugin_install_writes_remote_plugin_to_cloud_and_cache 用它证明先写缓存、再调用云端安装。

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

mount_remote_plugin_install_after_cache_write1980–1998 ↗
async fn mount_remote_plugin_install_after_cache_write(
    server: &MockServer,
    remote_plugin_id: &str,
    manifest_path: std::path::PathBuf,
)

作用:挂一个只有在缓存 manifest 已写好后才会成功匹配的远程安装接口。它专门检查远程安装的时序。

数据流:进去的是服务器、插件 ID 和本地 manifest 路径 → 它注册 POST install mock,并附加 CacheManifestExists 条件 → 出来是 mock 规则,只有文件存在时才响应成功。

调用关系:被 plugin_install_writes_remote_plugin_to_cloud_and_cache 调用;它把顺序验证交给 CacheManifestExists::matches。

调用图:被 1 处调用(plugin_install_writes_remote_plugin_to_cloud_and_cache);外部调用 6 个(given, new, format!, header, method, path)。

send_remote_plugin_install_request2000–2010 ↗
async fn send_remote_plugin_install_request(
    mcp: &mut TestAppServer,
    remote_plugin_id: &str,
) -> Result<i64>

作用:发送一条远程插件安装请求的便捷函数。它故意传一个会被忽略的远程市场名,重点测试插件 ID 路径。

数据流:进去的是 TestAppServer 和远程插件 ID → 它构造 PluginInstallParams,marketplace_path 为空,remote_marketplace_name 有占位值,plugin_name 是远程 ID → 出来的是请求 ID。

调用关系:被多条远程插件测试调用;它把请求发送细节集中起来,后续由测试读取成功响应或错误响应。

调用图:调用 1 个内部函数(send_plugin_install_request);被 10 处调用(plugin_install_errors_when_remote_bundle_download_fails, plugin_install_rejects_invalid_remote_release_version, plugin_install_rejects_missing_remote_bundle_url, plugin_install_rejects_plain_http_remote_bundle_url, plugin_install_rejects_remote_plugin_disabled_by_admin_before_download, plugin_install_skips_remote_mcp_oauth_for_bundled_same_name_app, plugin_install_starts_remote_mcp_oauth_for_install_response_only_app, plugin_install_tracks_remote_plugin_analytics_event, plugin_install_uses_remote_apps_needing_auth_response, plugin_install_writes_remote_plugin_to_cloud_and_cache)。

wait_for_remote_plugin_request_count2012–2042 ↗
async fn wait_for_remote_plugin_request_count(
    server: &MockServer,
    method_name: &str,
    path_suffix: &str,
    expected_count: usize,
) -> Result<()>

作用:等待假远程服务器收到指定数量的某类请求。它用于确认某个接口被调用了、没被调用,或没有被多调用。

数据流:进去的是 MockServer、HTTP 方法名、路径后缀和期望次数 → 它循环读取收到的请求并计数,数量达到就成功,超过就报错,超时也报错 → 出来是成功或错误。

调用关系:被多条远程插件测试调用;它是验证下载、云端安装和目录查询次数的通用观察工具。

调用图:被 7 处调用(plugin_install_errors_when_remote_bundle_download_fails, plugin_install_rejects_invalid_remote_release_version, plugin_install_rejects_missing_remote_bundle_url, plugin_install_rejects_plain_http_remote_bundle_url, plugin_install_rejects_remote_plugin_disabled_by_admin_before_download, plugin_install_uses_remote_apps_needing_auth_response, plugin_install_writes_remote_plugin_to_cloud_and_cache);外部调用 5 个(from_millis, received_requests, bail!, sleep, timeout)。

write_plugin_marketplace2044–2089 ↗
fn write_plugin_marketplace(
    repo_root: &std::path::Path,
    marketplace_name: &str,
    plugin_name: &str,
    source_path: &str,
    install_policy: Option<&str>,
    auth_policy: Option<&str>,

作用:写一个测试用的本地插件市场文件。市场文件就像插件清单,告诉服务器有哪些插件、叫什么、源码在哪里、有什么策略。

数据流:进去的是仓库根目录、市场名、插件名、源码路径,以及可选安装/认证策略 → 它创建 .git 和 .agents/plugins 目录,拼出 marketplace.json → 出来是文件写入结果。

调用关系:被大量本地插件安装测试调用;它通常和 write_plugin_source 搭配,一个写清单,一个写插件本体。

调用图:被 9 处调用(plugin_install_filters_disallowed_apps_needing_auth, plugin_install_makes_bundled_mcp_servers_available_to_followup_requests, plugin_install_rejects_when_workspace_codex_plugins_disabled, plugin_install_returns_apps_needing_auth, plugin_install_returns_invalid_request_for_not_available_plugin, plugin_install_skips_mcp_oauth_for_chatgpt_dual_surface_plugin, plugin_install_starts_mcp_oauth_for_api_key_dual_surface_plugin, plugin_install_starts_mcp_oauth_when_only_plugin_apps_are_disallowed, plugin_install_tracks_analytics_event);外部调用 5 个(join, new, format!, create_dir_all, write)。

write_plugin_source2091–2112 ↗
fn write_plugin_source(
    repo_root: &std::path::Path,
    plugin_name: &str,
    app_ids: &[&str],
) -> Result<()>

作用:写一个最小可用的测试插件目录。它包含插件 manifest 和应用清单。

数据流:进去的是仓库根目录、插件名和 app id 列表 → 它创建插件目录和 .codex-plugin/plugin.json,再把 app id 们写进 .app.json → 出来是成功或错误。

调用关系:被多个本地插件测试调用;write_plugin_marketplace 会指向这里生成的插件源码目录。

调用图:被 10 处调用(plugin_install_filters_disallowed_apps_needing_auth, plugin_install_makes_bundled_mcp_servers_available_to_followup_requests, plugin_install_rejects_when_workspace_codex_plugins_disabled, plugin_install_returns_apps_needing_auth, plugin_install_returns_invalid_request_for_disallowed_product_plugin, plugin_install_returns_invalid_request_for_not_available_plugin, plugin_install_skips_mcp_oauth_for_chatgpt_dual_surface_plugin, plugin_install_starts_mcp_oauth_for_api_key_dual_surface_plugin, plugin_install_starts_mcp_oauth_when_only_plugin_apps_are_disallowed, plugin_install_tracks_analytics_event);外部调用 6 个(join, format!, json!, to_vec_pretty, create_dir_all, write)。

write_plugin_mcp_config2114–2133 ↗
fn write_plugin_mcp_config(
    repo_root: &std::path::Path,
    plugin_name: &str,
    mcp_base_url: &str,
) -> Result<()>

作用:给测试插件写一份 .mcp.json,声明一个 HTTP 类型的 sample-mcp 服务。它用于触发或跳过 MCP OAuth 的测试。

数据流:进去的是仓库根目录、插件名和 MCP 服务 base URL → 它把 sample-mcp 的 type=http 和 url 写到插件目录下的 .mcp.json → 出来是写入结果。

调用关系:被 MCP OAuth 相关测试调用;安装流程读取这个文件后,可能会去 oauth_discovery_request_count 观察的假 OAuth 服务探测。

调用图:被 3 处调用(plugin_install_skips_mcp_oauth_for_chatgpt_dual_surface_plugin, plugin_install_starts_mcp_oauth_for_api_key_dual_surface_plugin, plugin_install_starts_mcp_oauth_when_only_plugin_apps_are_disallowed);外部调用 3 个(join, format!, write)。

remote_plugin_bundle_tar_gz_bytes2135–2138 ↗
fn remote_plugin_bundle_tar_gz_bytes(plugin_name: &str) -> Result<Vec<u8>>

作用:生成一个最简单的远程插件 tar.gz 压缩包。tar.gz 可以理解成先把多个文件打成包,再用 gzip 压缩。

数据流:进去的是插件名 → 它生成只含 name 的 plugin.json 内容,再交给 remote_plugin_bundle_tar_gz_bytes_with_contents → 出来是压缩包字节。

调用关系:被多个远程插件测试调用;它是生成默认远程 bundle 的快捷入口。

调用图:调用 1 个内部函数(remote_plugin_bundle_tar_gz_bytes_with_contents);被 3 处调用(plugin_install_rejects_remote_plugin_disabled_by_admin_before_download, plugin_install_tracks_remote_plugin_analytics_event, plugin_install_uses_remote_apps_needing_auth_response);外部调用 1 个(format!)。

remote_plugin_bundle_tar_gz_bytes_with_mcp_config2140–2160 ↗
fn remote_plugin_bundle_tar_gz_bytes_with_mcp_config(
    plugin_name: &str,
    mcp_base_url: &str,
) -> Result<Vec<u8>>

作用:生成一个带 .mcp.json 的远程插件压缩包。它用来测试远程插件安装后是否会触发 MCP OAuth。

数据流:进去的是插件名和 MCP base URL → 它拼出 plugin manifest 和 sample-mcp HTTP 配置,再交给 remote_plugin_bundle_tar_gz_bytes_with_entries 打包 → 出来是 bundle 字节。

调用关系:被 plugin_install_starts_remote_mcp_oauth_for_install_response_only_app 调用;生成的字节再由 mount_remote_plugin_bundle 提供下载。

调用图:调用 1 个内部函数(remote_plugin_bundle_tar_gz_bytes_with_entries);被 1 处调用(plugin_install_starts_remote_mcp_oauth_for_install_response_only_app);外部调用 1 个(format!)。

remote_plugin_bundle_tar_gz_bytes_with_app_and_mcp_config2162–2183 ↗
fn remote_plugin_bundle_tar_gz_bytes_with_app_and_mcp_config(
    plugin_name: &str,
    app_manifest: &str,
    mcp_base_url: &str,
) -> Result<Vec<u8>>

作用:生成同时带应用清单和 MCP 配置的远程插件压缩包。它用于测试同名 app 会让 MCP OAuth 被跳过。

数据流:进去的是插件名、app_manifest 字符串和 MCP base URL → 它构造 plugin manifest 与 sample-mcp 配置,并把 app_manifest 一起打包 → 出来是 tar.gz 字节。

调用关系:被 plugin_install_skips_remote_mcp_oauth_for_bundled_same_name_app 调用;它最终交给 remote_plugin_bundle_tar_gz_bytes_with_entries 完成压缩。

调用图:调用 1 个内部函数(remote_plugin_bundle_tar_gz_bytes_with_entries);被 1 处调用(plugin_install_skips_remote_mcp_oauth_for_bundled_same_name_app);外部调用 1 个(format!)。

remote_plugin_bundle_tar_gz_bytes_with_contents2185–2194 ↗
fn remote_plugin_bundle_tar_gz_bytes_with_contents(
    plugin_manifest: &str,
    app_manifest: Option<&str>,
) -> Result<Vec<u8>>

作用:生成一个可自定义 plugin.json 和可选 .app.json 的远程插件压缩包。它比最简单版本更灵活。

数据流:进去的是 plugin_manifest 字符串和可选 app_manifest → 它不加 MCP 配置,直接交给 remote_plugin_bundle_tar_gz_bytes_with_entries → 出来是压缩包字节。

调用关系:被 plugin_install_writes_remote_plugin_to_cloud_and_cache 和 remote_plugin_bundle_tar_gz_bytes 调用;是远程 bundle 生成的中间层。

调用图:调用 1 个内部函数(remote_plugin_bundle_tar_gz_bytes_with_entries);被 2 处调用(plugin_install_writes_remote_plugin_to_cloud_and_cache, remote_plugin_bundle_tar_gz_bytes)。

remote_plugin_bundle_tar_gz_bytes_with_entries2196–2230 ↗
fn remote_plugin_bundle_tar_gz_bytes_with_entries(
    plugin_manifest: &str,
    app_manifest: Option<&str>,
    mcp_config: Option<&str>,
) -> Result<Vec<u8>>

作用:真正把远程插件文件打成 tar.gz 的底层函数。它负责把 manifest、技能文件、可选 app 清单和可选 MCP 配置写进压缩包。

数据流:进去的是 plugin_manifest、可选 app_manifest、可选 mcp_config → 它创建 gzip 编码器和 tar 打包器,依次写入 .codex-plugin/plugin.json、skills/plan-work/SKILL.md,以及可选文件 → 出来是完整压缩包的字节数组。

调用关系:被所有 remote_plugin_bundle_tar_gz_bytes* 包装函数调用;mount_remote_plugin_bundle 会把它产出的字节作为远程下载内容。

调用图:被 3 处调用(remote_plugin_bundle_tar_gz_bytes_with_app_and_mcp_config, remote_plugin_bundle_tar_gz_bytes_with_contents, remote_plugin_bundle_tar_gz_bytes_with_mcp_config);外部调用 6 个(new, new, default, new, new_gnu, vec!)。

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

插件分享牵涉本地文件、登录凭据、远端接口、权限设置和市场列表,任何一环错了,用户可能会上传失败、看到错误的共享插件,甚至把权限发错人。这个测试文件就像一套验收清单:先造一个临时的 Codex 主目录和测试插件,再用 wiremock(一种假的 HTTP 服务器)假装 ChatGPT 后端,规定“收到什么请求就回什么结果”。然后它启动测试版 app server,通过 JSON-RPC(一种用 JSON 发命令、收结果的通信格式)发送 plugin/share/save、list、checkout、updateTargets、delete 等请求,最后检查返回值、远端请求内容、本地文件是否都正确。文件后半部分是很多小工具,用来写配置、生成测试插件包、准备假的远端插件 JSON,以及写本地路径映射。

函数细节27
plugin_share_save_uploads_local_plugin53–191 ↗
async fn plugin_share_save_uploads_local_plugin() -> Result<()>

作用:测试把一个本地插件分享出去时,系统是否真的会先上传插件包,再创建远端分享记录。它还顺便确认坏掉的本地路径映射文件不会把流程搞崩。

数据流:进去的是临时目录里的测试插件、假的登录信息、假的后端响应 → 测试启动 app server,发送 plugin/share/save,请求服务端压缩并上传插件,再发送 plugin/share/list 查看结果 → 出来的是一个包含远端插件 ID 和分享链接的成功响应,并且列表里能看到这个远端插件和对应的本地路径。

调用关系:这个测试会调用 write_test_plugin 准备插件,调用 write_remote_plugin_config 和 write_chatgpt_auth 准备配置和登录,调用 write_corrupt_plugin_share_local_path_mapping 放入坏映射文件,然后把上传、创建、列表接口都交给 wiremock 假服务器模拟。它验证的是保存分享到列出分享这一整条主流程。

调用图:调用 6 个内部函数(new, new, write_corrupt_plugin_share_local_path_mapping, write_remote_plugin_config, write_test_plugin, try_from);外部调用 16 个(given, start, new, new, Integer, to_response, write_chatgpt_auth, assert_eq!, format!, json! (+6 more))。

plugin_share_save_forwards_access_policy194–289 ↗
async fn plugin_share_save_forwards_access_policy() -> Result<()>

作用:测试创建插件分享时,客户端传入的可见性和分享对象是否会被正确转发给后端。这里重点看权限信息有没有被改错或漏掉。

数据流:进去的是本地插件路径、discoverability 为 UNLISTED、一个用户分享目标 → app server 先申请上传地址、上传插件包,再创建远端插件分享 → 发给后端的 JSON 里应包含用户目标,并自动补上当前 workspace 的 reader 权限,最后返回远端插件 ID 和分享链接。

调用关系:它复用 write_test_plugin 和 write_remote_plugin_config 搭环境,再用 wiremock 的 body_json 精确检查请求体。它不像只看返回值的测试,而是专门盯住 app server 发给后端的权限字段。

调用图:调用 5 个内部函数(new, new, write_remote_plugin_config, write_test_plugin, try_from);外部调用 15 个(given, start, new, new, Integer, to_response, write_chatgpt_auth, assert_eq!, format!, json! (+5 more))。

plugin_share_save_rejects_listed_discoverability292–331 ↗
async fn plugin_share_save_rejects_listed_discoverability() -> Result<()>

作用:测试系统会拒绝把插件保存成 LISTED,也就是公开上架式可发现状态。这个分享接口只允许更受控的 UNLISTED 或 PRIVATE。

数据流:进去的是一个带 discoverability=LISTED 的 plugin/share/save 请求 → app server 在本地校验参数,没有继续走上传或远端创建 → 出来的是 JSON-RPC 错误,错误码是 -32600,消息说明 LISTED 不支持。

调用关系:它仍然用 write_test_plugin、write_remote_plugin_config 和登录 fixture 建好基本环境,但重点不是远端交互,而是确认请求刚进 app server 就被拦下。

调用图:调用 4 个内部函数(new, new, write_remote_plugin_config, write_test_plugin);外部调用 8 个(start, new, Integer, write_chatgpt_auth, assert_eq!, format!, json!, timeout)。

plugin_share_save_rejects_when_plugin_sharing_disabled334–389 ↗
async fn plugin_share_save_rejects_when_plugin_sharing_disabled() -> Result<()>

作用:测试配置里关闭插件分享时,保存分享请求会被拒绝。这样可以防止功能开关关掉后,系统还偷偷访问远端服务。

数据流:进去的是一个配置文件,其中 plugin_sharing=false,再加一个本地插件路径 → app server 初始化后收到 plugin/share/save → 出来的是“plugin sharing is disabled”的错误,并且假后端没有收到任何请求。

调用关系:它直接写 config.toml,而不是用 write_remote_plugin_config,因为要特意关闭功能开关。它调用 write_test_plugin 做插件样本,并通过 wiremock 的收到请求记录确认没有发生网络调用。

调用图:调用 3 个内部函数(new, new, write_test_plugin);外部调用 10 个(start, new, Integer, write_chatgpt_auth, assert!, assert_eq!, format!, json!, write, timeout)。

plugin_share_rejects_workspace_targets_from_client392–467 ↗
async fn plugin_share_rejects_workspace_targets_from_client() -> Result<()>

作用:测试客户端不能直接把 workspace 作为 shareTargets 传进来。workspace 链接访问要用 UNLISTED 表示,不能伪装成普通目标。

数据流:进去的是两次带 workspace principal 的请求:一次 plugin/share/save,一次 plugin/share/updateTargets → app server 都在参数校验阶段拒绝 → 出来的是相同的 JSON-RPC 错误,提示不要在 shareTargets 里放 workspace。

调用关系:它先用 write_test_plugin、write_remote_plugin_config 和登录信息搭起正常环境,然后连续测试创建分享和更新目标两个入口,确保两条路都不会放过非法 workspace 目标。

调用图:调用 4 个内部函数(new, new, write_remote_plugin_config, write_test_plugin);外部调用 8 个(start, new, Integer, write_chatgpt_auth, assert_eq!, format!, json!, timeout)。

plugin_share_save_rejects_access_policy_for_existing_plugin470–517 ↗
async fn plugin_share_save_rejects_access_policy_for_existing_plugin() -> Result<()>

作用:测试更新已有远端插件内容时,不能顺手改分享可见性和分享对象。这样能把“更新插件包”和“改权限”两件事分清楚。

数据流:进去的是 plugin/share/save 请求,里面既有 remotePluginId,又有 discoverability 和 shareTargets → app server 识别为已有插件的保存请求,并发现混入了创建时才允许的权限字段 → 出来的是错误,提示要改分享设置应使用 plugin/share/updateTargets。

调用关系:它调用 write_test_plugin 准备本地插件,但不需要 mock 上传接口,因为请求会在本地校验阶段被拒绝。它补充了 save 接口在“新建”和“更新”两种模式下的边界规则。

调用图:调用 4 个内部函数(new, new, write_remote_plugin_config, write_test_plugin);外部调用 8 个(start, new, Integer, write_chatgpt_auth, assert_eq!, format!, json!, timeout)。

plugin_share_list_returns_created_workspace_plugins520–595 ↗
async fn plugin_share_list_returns_created_workspace_plugins() -> Result<()>

作用:测试列出插件分享时,系统能把当前 workspace 创建的远端插件整理成客户端能看懂的列表。它检查插件摘要、权限上下文和安装状态是否合并正确。

数据流:进去的是两个假后端接口的结果:一个返回已创建的 workspace 插件,一个返回已安装的 workspace 插件 → app server 收到 plugin/share/list 后拉取这两份数据并合并 → 出来的是 PluginShareListResponse,里面有插件名称、远端 ID、分享上下文、接口描述和 installed=true。

调用关系:它用 remote_plugin_json、installed_remote_plugin_json、empty_pagination_json 这些辅助数据喂给 wiremock。这个测试覆盖的是列表聚合流程,也就是后端多份信息变成前端一条列表项的过程。

调用图:调用 3 个内部函数(new, new, write_remote_plugin_config);外部调用 15 个(given, start, new, new, Integer, to_response, write_chatgpt_auth, assert_eq!, format!, json! (+5 more))。

plugin_share_checkout_adds_personal_marketplace_entry598–763 ↗
async fn plugin_share_checkout_adds_personal_marketplace_entry() -> Result<()>

作用:测试把别人分享的远端插件 checkout 到本地时,系统会下载插件包,并把它加入个人 marketplace(可以理解成本地插件目录清单)。它还确认重复 checkout 不会覆盖用户自己的本地改动。

数据流:进去的是远端插件 ID、假的插件详情、一个 gzip 压缩的插件包,以及临时 HOME 目录 → app server 下载包,解压到 HOME/plugins/demo-plugin,写入 .agents/plugins/marketplace.json,并记录远端 ID 到本地路径的映射 → 出来的是 checkout 响应,包含本地路径、marketplace 路径和远端版本;再次 checkout 后,本地额外文件仍被保留。

调用关系:它调用 remote_plugin_bundle_tar_gz_bytes 造插件包,mount_remote_plugin_bundle 挂下载接口,mount_remote_plugin_detail_with_bundle 挂详情接口,mount_empty_remote_installed_plugins 挂安装列表接口。之后还发 plugin/list 请求,确认 checkout 结果能被普通插件列表看到。

调用图:调用 8 个内部函数(new, new_with_env, mount_empty_remote_installed_plugins, mount_remote_plugin_bundle, mount_remote_plugin_detail_with_bundle, remote_plugin_bundle_tar_gz_bytes, write_remote_plugin_config, try_from);外部调用 14 个(start, new, Integer, to_response, write_chatgpt_auth, assert!, assert_eq!, format!, json!, from_str (+4 more))。

plugin_share_checkout_rejects_non_share_remote_plugin766–827 ↗
async fn plugin_share_checkout_rejects_non_share_remote_plugin() -> Result<()>

作用:测试 checkout 只允许 workspace 分享插件,不允许拿全局插件走这个分享下载流程。这样能避免把普通远端市场插件误当成分享插件处理。

数据流:进去的是一个 scope=GLOBAL 的远端插件详情和 checkout 请求 → app server 拉到详情后发现它不是 workspace 分享插件 → 出来的是错误响应,并且本地 HOME/plugins/global-plugin 没有被创建。

调用关系:它使用 mount_remote_plugin_detail_with_bundle 和 mount_empty_remote_installed_plugins 模拟一个全局插件。和成功 checkout 测试相反,它验证的是类型不匹配时必须中止,不写本地文件。

调用图:调用 5 个内部函数(new, new_with_env, mount_empty_remote_installed_plugins, mount_remote_plugin_detail_with_bundle, write_remote_plugin_config);外部调用 9 个(start, new, Integer, write_chatgpt_auth, assert!, assert_eq!, format!, json!, timeout)。

plugin_share_checkout_cleans_up_path_when_marketplace_update_fails830–924 ↗
async fn plugin_share_checkout_cleans_up_path_when_marketplace_update_fails() -> Result<()>

作用:测试 checkout 过程中如果更新 marketplace 失败,已经解压出来的插件目录和路径映射会被清理掉。这样不会留下半成品,避免下次运行时误判。

数据流:进去的是一个已存在且冲突的 marketplace.json,以及正常的远端插件包和详情 → app server 先下载并准备写入,但发现 marketplace 已有同名插件且来源不同 → 出来的是错误响应,同时刚创建的插件目录不存在,远端 ID 到本地路径的映射文件也不存在。

调用关系:它复用 mount_remote_plugin_bundle、mount_remote_plugin_detail_with_bundle、mount_empty_remote_installed_plugins 和 remote_plugin_bundle_tar_gz_bytes 准备远端数据。这个测试专门覆盖失败后的回滚行为,就像安装失败要把拆开的零件收拾干净。

调用图:调用 7 个内部函数(new, new_with_env, mount_empty_remote_installed_plugins, mount_remote_plugin_bundle, mount_remote_plugin_detail_with_bundle, remote_plugin_bundle_tar_gz_bytes, write_remote_plugin_config);外部调用 12 个(start, new, Integer, write_chatgpt_auth, assert!, assert_eq!, format!, json!, to_string_pretty, create_dir_all (+2 more))。

plugin_share_update_targets_updates_share_targets927–1039 ↗
async fn plugin_share_update_targets_updates_share_targets() -> Result<()>

作用:测试更新插件分享对象时,app server 会把目标和可见性正确发给后端,并把后端返回的完整成员列表交回客户端。

数据流:进去的是 remotePluginId、discoverability=UNLISTED、一个用户 editor 目标 → app server 组装 PUT /shares 请求,并自动带上 workspace reader 目标 → 后端返回 owner、editor、workspace reader 三个 principal,函数最终检查响应里这些人和角色都正确。

调用关系:它通过 wiremock 精确匹配 PUT 请求体,确认 updateTargets 不只是返回对了,还真的按协议向后端发了正确内容。它和 save_forwards_access_policy 一起覆盖创建和更新两种权限发送场景。

调用图:调用 3 个内部函数(new, new, write_remote_plugin_config);外部调用 15 个(given, start, new, new, Integer, to_response, write_chatgpt_auth, assert_eq!, format!, json! (+5 more))。

plugin_share_update_targets_rejects_when_plugin_sharing_disabled1042–1090 ↗
async fn plugin_share_update_targets_rejects_when_plugin_sharing_disabled() -> Result<()>

作用:测试插件分享功能关闭时,更新分享对象也会被拒绝。这样功能开关对所有分享入口都一致生效。

数据流:进去的是 plugin_sharing=false 的配置和一个 plugin/share/updateTargets 请求 → app server 初始化后进行功能开关检查 → 出来的是错误码 -32600,消息为“plugin sharing is disabled”。

调用关系:它直接写 config.toml 来关闭分享功能,并发送 updateTargets 请求。它和保存分享的禁用测试互相补齐,确保不是只有某一个接口检查了开关。

调用图:调用 2 个内部函数(new, new);外部调用 9 个(start, new, Integer, write_chatgpt_auth, assert_eq!, format!, json!, write, timeout)。

plugin_share_delete_removes_created_workspace_plugin1093–1196 ↗
async fn plugin_share_delete_removes_created_workspace_plugin() -> Result<()>

作用:测试删除一个已创建的 workspace 分享插件时,系统会调用远端删除接口,并清掉本地记录的路径映射。随后再列表时不应该再显示本地路径。

数据流:进去的是一个远端插件 ID 和预先写好的本地路径映射 → app server 收到 plugin/share/delete 后向后端 DELETE 对应插件,并更新本地映射文件 → 出来的是空的成功响应;再调用 plugin/share/list 时,同一个远端插件的 local_plugin_path 变成 None。

调用关系:它先用 write_plugin_share_local_path_mapping 造出本地映射,再用 wiremock 模拟 DELETE 成功。删除后又挂载列表相关 mock,复查 delete 对后续 list 的影响。

调用图:调用 5 个内部函数(new, new, write_plugin_share_local_path_mapping, write_remote_plugin_config, try_from);外部调用 15 个(given, start, new, new, Integer, to_response, write_chatgpt_auth, assert_eq!, format!, json! (+5 more))。

write_remote_plugin_config1198–1211 ↗
fn write_remote_plugin_config(codex_home: &Path, base_url: &str) -> std::io::Result<()>

作用:给临时 Codex 主目录写一个最小配置文件,打开插件和远端插件功能,并指定假的 ChatGPT 后端地址。测试需要它来让 app server 去访问 wiremock,而不是真实网络。

数据流:进去的是 codex_home 路径和 base_url 字符串 → 它拼出 config.toml 的内容,写入 chatgpt_base_url 和 features 配置 → 出来的是一个写好的配置文件,失败时返回文件写入错误。

调用关系:很多测试在启动 TestAppServer 前都会调用它。它不参与业务判断,只负责把测试环境的“后台地址”和功能开关准备好。

调用图:被 11 处调用(plugin_share_checkout_adds_personal_marketplace_entry, plugin_share_checkout_cleans_up_path_when_marketplace_update_fails, plugin_share_checkout_rejects_non_share_remote_plugin, plugin_share_delete_removes_created_workspace_plugin, plugin_share_list_returns_created_workspace_plugins, plugin_share_rejects_workspace_targets_from_client, plugin_share_save_forwards_access_policy, plugin_share_save_rejects_access_policy_for_existing_plugin, plugin_share_save_rejects_listed_discoverability, plugin_share_save_uploads_local_plugin (+1 more));外部调用 3 个(join, format!, write)。

mount_remote_plugin_bundle1213–1230 ↗
async fn mount_remote_plugin_bundle(
    server: &MockServer,
    plugin_name: &str,
    body: Vec<u8>,
) -> String

作用:在假服务器上挂一个插件包下载地址。checkout 测试用它来假装远端真的提供了 .tar.gz 插件包。

数据流:进去的是 MockServer、插件名和插件包字节内容 → 它注册一个 GET /bundles/插件名.tar.gz 的假接口,返回 application/gzip 内容 → 出来的是完整下载 URL,供插件详情接口引用。

调用关系:它被成功 checkout 和失败回滚 checkout 测试调用,通常配合 remote_plugin_bundle_tar_gz_bytes 生成包,再配合 mount_remote_plugin_detail_with_bundle 把这个 URL 放进远端插件详情里。

调用图:被 2 处调用(plugin_share_checkout_adds_personal_marketplace_entry, plugin_share_checkout_cleans_up_path_when_marketplace_update_fails);外部调用 5 个(given, new, format!, method, path)。

mount_remote_plugin_detail_with_bundle1232–1274 ↗
async fn mount_remote_plugin_detail_with_bundle(
    server: &MockServer,
    remote_plugin_id: &str,
    plugin_name: &str,
    bundle_url: &str,
    scope: &str,
)

作用:在假服务器上挂一个远端插件详情接口,并在详情里带上插件包下载链接。checkout 流程需要先读这个详情,才知道插件叫什么、是什么范围、包从哪里下。

数据流:进去的是服务器、远端插件 ID、插件名、bundle_url 和 scope → 它注册 GET /ps/plugins/{id}?includeDownloadUrls=true,并要求带授权头 → 出来的是一份固定结构的插件详情 JSON,包含版本、描述、接口能力、分享信息和下载地址。

调用关系:checkout 相关测试都会用它。成功场景传 WORKSPACE,拒绝场景传 GLOBAL,用同一个假接口测试 app server 对 scope 的不同处理。

调用图:被 3 处调用(plugin_share_checkout_adds_personal_marketplace_entry, plugin_share_checkout_cleans_up_path_when_marketplace_update_fails, plugin_share_checkout_rejects_non_share_remote_plugin);外部调用 8 个(given, new, format!, json!, header, method, path, query_param)。

mount_empty_remote_installed_plugins1276–1290 ↗
async fn mount_empty_remote_installed_plugins(server: &MockServer, scope: &str)

作用:在假服务器上挂一个“当前没有已安装远端插件”的接口。checkout 时系统会查安装列表,这个函数让查询结果保持简单。

数据流:进去的是服务器和 scope → 它注册 GET /ps/plugins/installed?scope=指定范围,并要求授权头 → 出来的是 plugins 为空、分页结束的 JSON 响应。

调用关系:它被三个 checkout 测试调用,用来满足 app server checkout 过程中会进行的安装状态查询。它把测试重点留给下载、写 marketplace 和错误处理。

调用图:被 3 处调用(plugin_share_checkout_adds_personal_marketplace_entry, plugin_share_checkout_cleans_up_path_when_marketplace_update_fails, plugin_share_checkout_rejects_non_share_remote_plugin);外部调用 7 个(given, new, json!, header, method, path, query_param)。

remote_plugin_json1292–1326 ↗
fn remote_plugin_json(plugin_id: &str) -> serde_json::Value

作用:生成一份标准的远端 workspace 插件 JSON。列表测试用它模拟后端返回的“我创建的分享插件”。

数据流:进去的是 plugin_id → 它把这个 ID 填进一份固定 JSON,里面有名称、scope、分享链接、成员、安装策略、认证策略和 release 信息 → 出来的是 serde_json::Value,可直接作为假后端响应体的一部分。

调用关系:它被 installed_remote_plugin_json 复用,也被列表和删除后列表测试间接使用。它是测试数据模板,保证多个测试里的远端插件长得一致。

调用图:被 1 处调用(installed_remote_plugin_json);外部调用 1 个(json!)。

installed_remote_plugin_json1328–1336 ↗
fn installed_remote_plugin_json(plugin_id: &str) -> serde_json::Value

作用:在标准远端插件 JSON 基础上,加上“已安装且启用”的字段。列表测试用它模拟后端的 installed 接口返回。

数据流:进去的是 plugin_id → 它先调用 remote_plugin_json 生成基础插件对象,再插入 enabled=true 和 disabled_skill_names=[] → 出来的是带安装状态的 JSON。

调用关系:它依赖 remote_plugin_json,避免重复写一大段插件数据。plugin_share_list_returns_created_workspace_plugins 和删除后的列表复查会用它来让 app server 合并安装状态。

调用图:调用 1 个内部函数(remote_plugin_json);外部调用 2 个(json!, unreachable!)。

empty_pagination_json1338–1342 ↗
fn empty_pagination_json() -> serde_json::Value

作用:生成一个表示“没有下一页”的分页 JSON。测试远端列表接口时用它补齐后端响应格式。

数据流:进去不需要参数 → 它创建 {"next_page_token": null} → 出来的是 serde_json::Value,说明这批数据已经取完。

调用关系:它通常和 remote_plugin_json、installed_remote_plugin_json 一起放进 wiremock 响应。它让测试不用关心翻页,只测试当前页的数据合并。

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

expected_plugin_interface1344–1364 ↗
fn expected_plugin_interface() -> PluginInterface

作用:生成测试期望中的插件界面描述。它把后端 release.interface 里的展示名、短描述和能力列表转换成客户端协议里的结构。

数据流:进去不需要参数 → 它构造 PluginInterface,填入 Demo Plugin、A demo plugin、Read/Write 等字段,其余可选字段为空 → 出来的是用于 assert_eq 比较的期望值。

调用关系:列表相关测试会拿它和 app server 返回的 PluginSummary.interface 比较。它是“正确答案”的一部分,帮助看清字段映射有没有错。

调用图:外部调用 2 个(new, vec!)。

expected_share_context1366–1389 ↗
fn expected_share_context(plugin_id: &str) -> PluginShareContext

作用:生成测试期望中的插件分享上下文。这里面包含远端插件 ID、版本、可见性、分享链接和分享成员。

数据流:进去的是 plugin_id → 它构造 PluginShareContext,把 ID、版本 0.1.0、PRIVATE、固定分享 URL、owner 和 reader 两个成员填进去 → 出来的是用于断言的期望结构。

调用关系:保存后列表和普通列表测试会用它检查 share_context。它对应 remote_plugin_json 里的分享字段,是远端 JSON 转成本地协议对象后的标准答案。

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

write_test_plugin1391–1402 ↗
fn write_test_plugin(root: &Path, plugin_name: &str) -> std::io::Result<PathBuf>

作用:在临时目录里写出一个最小可用的本地插件。测试上传分享时需要一个真实存在的插件目录,而不是空路径。

数据流:进去的是根目录和插件名 → 它创建 插件名/.codex-plugin/plugin.json 和 skills/example/SKILL.md 两个文件 → 出来的是插件根目录路径。

调用关系:多个 save 相关测试调用它准备输入文件。它把实际写文件的动作交给 write_file,这样目录不存在时也能自动创建。

调用图:调用 1 个内部函数(write_file);被 6 处调用(plugin_share_rejects_workspace_targets_from_client, plugin_share_save_forwards_access_policy, plugin_share_save_rejects_access_policy_for_existing_plugin, plugin_share_save_rejects_listed_discoverability, plugin_share_save_rejects_when_plugin_sharing_disabled, plugin_share_save_uploads_local_plugin);外部调用 2 个(join, format!)。

remote_plugin_bundle_tar_gz_bytes1404–1428 ↗
fn remote_plugin_bundle_tar_gz_bytes(plugin_name: &str) -> Result<Vec<u8>>

作用:把一个测试插件打成 tar.gz 压缩包的字节数据。checkout 测试用它模拟远端下载下来的插件包。

数据流:进去的是插件名 → 它创建 gzip 编码器和 tar 打包器,把 plugin.json 和 SKILL.md 放进包里,并设置文件权限 → 出来的是 Vec<u8>,也就是压缩包的原始字节。

调用关系:它被 checkout 成功测试和回滚测试调用,生成的数据再交给 mount_remote_plugin_bundle 作为假下载接口的响应体。

调用图:被 2 处调用(plugin_share_checkout_adds_personal_marketplace_entry, plugin_share_checkout_cleans_up_path_when_marketplace_update_fails);外部调用 6 个(new, new, default, format!, new, new_gnu)。

write_corrupt_plugin_share_local_path_mapping1430–1435 ↗
fn write_corrupt_plugin_share_local_path_mapping(codex_home: &Path) -> std::io::Result<()>

作用:故意写一个坏掉的本地路径映射文件。测试用它确认系统遇到旧文件损坏时,不会在分享保存流程里崩溃。

数据流:进去的是 codex_home 路径 → 它往 .tmp/plugin-share-local-paths-v1.json 写入字符串 not-json → 出来的是一个格式错误的 JSON 文件。

调用关系:它只被 plugin_share_save_uploads_local_plugin 调用。这个坏文件像一个“脏现场”,用来测试 app server 的容错能力。

调用图:调用 1 个内部函数(write_file);被 1 处调用(plugin_share_save_uploads_local_plugin);外部调用 1 个(join)。

write_plugin_share_local_path_mapping1437–1455 ↗
fn write_plugin_share_local_path_mapping(
    codex_home: &Path,
    remote_plugin_id: &str,
    plugin_path: &AbsolutePathBuf,
) -> std::io::Result<()>

作用:写一份远端插件 ID 到本地插件路径的映射文件。删除分享测试用它模拟系统之前记住过某个远端插件对应的本地目录。

数据流:进去的是 codex_home、remote_plugin_id 和本地插件路径 → 它把路径序列化成 JSON,放到 localPluginPathsByRemotePluginId 下面,再写入 .tmp/plugin-share-local-paths-v1.json → 出来的是一份格式正确的映射文件。

调用关系:它被 plugin_share_delete_removes_created_workspace_plugin 调用,删除接口成功后,测试会再列表确认这个映射已不再影响结果。它把底层写文件交给 write_file。

调用图:调用 1 个内部函数(write_file);被 1 处调用(plugin_share_delete_removes_created_workspace_plugin);外部调用 6 个(join, format!, json!, new, to_string_pretty, to_value)。

write_file1457–1466 ↗
fn write_file(path: &Path, contents: &str) -> std::io::Result<()>

作用:一个测试里的通用写文件小工具:先确保父目录存在,再写入内容。这样其他辅助函数不用反复处理建目录的问题。

数据流:进去的是目标文件路径和字符串内容 → 它先检查路径有没有父目录,没有就返回错误;有的话创建父目录,再写文件 → 出来的是写入成功或具体的 I/O 错误。

调用关系:write_test_plugin、write_corrupt_plugin_share_local_path_mapping、write_plugin_share_local_path_mapping 都调用它。它是这个测试文件最底层的文件写入积木。

调用图:被 3 处调用(write_corrupt_plugin_share_local_path_mapping, write_plugin_share_local_path_mapping, write_test_plugin);外部调用 5 个(parent, other, format!, create_dir_all, write)。

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

插件卸载看起来只是“删掉一个东西”,但实际很容易出岔子:本地缓存可能没删干净,配置文件可能还留着旧开关,远程插件可能需要先问云端详情再卸载,插件编号如果带空格或路径符号还可能变成安全问题。这个测试文件就像一套验收清单,用临时目录假装用户的 Codex 主目录,用测试服务器假装云端接口,再启动一个测试版 app server,通过 JSON-RPC(用 JSON 传请求和响应的一种通信格式)发出卸载请求。它检查成功时返回空响应,失败时返回正确错误;还检查是否写了统计事件、是否带了登录凭证、是否按插件的作用范围删除正确缓存。辅助函数负责造假插件、写配置、挂载假接口、等待假服务器收到指定请求。

函数细节15
plugin_uninstall_removes_plugin_cache_and_config_entry32–80 ↗
async fn plugin_uninstall_removes_plugin_cache_and_config_entry() -> Result<()>

作用:这个测试确认卸载一个本地插件时,程序不只是说“成功”,还真的把插件缓存目录删掉,并从配置文件里移除这个插件的配置项。它还确认重复卸载同一个插件不会报错。

数据流:进去的是一个临时的 Codex 主目录,里面先被写入一个假的已安装插件和一段启用插件的配置。测试启动 TestAppServer,发出 plugin_id 为“sample-plugin@debug”的卸载请求,读取 JSON-RPC 响应并转成卸载响应。出来的结果是响应为空的成功对象;同时磁盘上的插件缓存目录不存在了,config.toml 里也不再包含这个插件的配置块。随后它再发一次同样请求,确认已经删过的插件再卸载也能安全成功。

调用关系:这是本地插件卸载的基础场景测试。它先用 write_installed_plugin 搭好假插件,再让 TestAppServer 执行真正的卸载流程,最后用 to_response 把服务器回信变成可断言的结果。

调用图:调用 2 个内部函数(new, write_installed_plugin);外部调用 8 个(new, Integer, to_response, assert!, assert_eq!, read_to_string, write, timeout)。

plugin_uninstall_tracks_analytics_event83–153 ↗
async fn plugin_uninstall_tracks_analytics_event() -> Result<()>

作用:这个测试确认卸载插件成功后,程序会发一条统计事件。这样产品侧能知道用户卸载了哪个插件,以及这个插件的一些基本信息。

数据流:进去的是一个临时主目录、一个假的统计事件服务器、一个假的本地插件、配置文件里的统计服务器地址,以及一份假的 ChatGPT 登录凭证。测试启动 app server 后发出卸载请求,先确认卸载成功。然后它不断查看假统计服务器收到的请求,直到找到发往“/codex/analytics-events/events”的 POST 请求。出来的结果是请求体被解析成 JSON,并且内容必须精确等于预期:事件类型是插件已卸载,参数里有插件编号、插件名、市场名、是否有技能、MCP 服务器数量、连接器列表和客户端名。

调用关系:这是卸载流程和统计上报流程的联动测试。它用 write_installed_plugin 造插件,用 start_analytics_events_server 接收统计请求,用 write_chatgpt_auth 准备身份信息,最后通过服务器收到的请求验证卸载流程确实触发了上报。

调用图:调用 3 个内部函数(new, new, write_installed_plugin);外部调用 12 个(from_millis, new, Integer, start_analytics_events_server, to_response, write_chatgpt_auth, assert_eq!, format!, from_slice, write (+2 more))。

plugin_uninstall_rejects_remote_plugin_when_plugins_are_disabled156–186 ↗
async fn plugin_uninstall_rejects_remote_plugin_when_plugins_are_disabled() -> Result<()>

作用:这个测试确认当配置里关闭插件功能时,远程插件不能被卸载。这样可以避免用户或客户端绕过功能开关,偷偷触发远程插件逻辑。

数据流:进去的是一个临时主目录,其中 config.toml 明确写着 plugins = false。测试启动 app server 后,发送一个看起来像远程插件的编号“plugins~Plugin_sample”。出来的不是成功响应,而是 JSON-RPC 错误;错误码是 -32600,错误消息说明远程插件卸载没有启用。

调用关系:这是功能开关的保护测试。它不需要搭建远程服务器,因为重点是确认请求在本地配置检查阶段就被拦下。

调用图:调用 1 个内部函数(new);外部调用 6 个(new, Integer, assert!, assert_eq!, write, timeout)。

plugin_uninstall_writes_remote_plugin_to_cloud_when_remote_plugin_enabled189–259 ↗
async fn plugin_uninstall_writes_remote_plugin_to_cloud_when_remote_plugin_enabled() -> Result<()>

作用:这个测试确认远程插件卸载时,程序会真的调用云端卸载接口,并在成功后清掉本地缓存。它覆盖的是“远程插件功能已开启”的正常路径。

数据流:进去的是临时主目录、假的后端服务器、远程插件配置、假的登录凭证、远程插件详情接口,以及一个云端卸载接口的模拟响应。测试还预先创建了新版缓存目录和旧版遗留缓存目录。发送卸载请求后,程序先获取远程插件详情,再带着授权头调用云端卸载接口。出来的结果是卸载响应成功,假服务器收到一次对应 POST 请求,新版和旧版远程插件缓存目录都被删除。

调用关系:这是远程卸载的主流程测试。它用 write_remote_plugin_catalog_config 指向假后端,用 mount_remote_plugin_detail 提供插件详情,用 wait_for_remote_plugin_request_count 确认云端卸载请求确实发生。

调用图:调用 5 个内部函数(new, new, mount_remote_plugin_detail, wait_for_remote_plugin_request_count, write_remote_plugin_catalog_config);外部调用 16 个(given, start, new, new, Integer, to_response, write_chatgpt_auth, assert!, assert_eq!, format! (+6 more))。

plugin_uninstall_uses_detail_scope_for_cache_namespace262–331 ↗
async fn plugin_uninstall_uses_detail_scope_for_cache_namespace() -> Result<()>

作用:这个测试确认删除远程插件缓存时,程序会看云端详情里的 scope,也就是插件属于全局还是工作区。这样不会误删另一个范围里的同名插件缓存。

数据流:进去的是临时主目录、假的后端服务器、登录凭证,以及一个 scope 为 WORKSPACE 的远程插件详情。测试同时创建工作区缓存目录和全局缓存目录。发送卸载请求后,程序获取详情,知道这个插件是工作区插件,于是调用云端卸载接口,并只删除 workspace-directory 下的缓存。出来的结果是工作区缓存不存在了,全局缓存仍然存在。

调用关系:这是远程插件缓存命名空间的边界测试。它复用 mount_remote_plugin_detail 来声明插件范围,再用 wait_for_remote_plugin_request_count 确认卸载请求发出,最后用文件是否存在来检查没有误删。

调用图:调用 5 个内部函数(new, new, mount_remote_plugin_detail, wait_for_remote_plugin_request_count, write_remote_plugin_catalog_config);外部调用 16 个(given, start, new, new, Integer, to_response, write_chatgpt_auth, assert!, assert_eq!, format! (+6 more))。

plugin_uninstall_accepts_workspace_remote_plugin_id_shape334–404 ↗
async fn plugin_uninstall_accepts_workspace_remote_plugin_id_shape() -> Result<()>

作用:这个测试确认程序接受另一种工作区远程插件编号格式,比如带下划线和长哈希的编号。它防止合法的工作区插件因为编号长得不一样而卸载失败。

数据流:进去的是临时主目录、假的后端服务器、远程插件配置、登录凭证,以及一个插件名为“skill-improver”、scope 为 WORKSPACE 的详情接口。测试创建对应的工作区缓存目录,然后发送这个工作区远程插件编号的卸载请求。出来的结果是卸载成功,假后端收到一次 POST 卸载请求,对应缓存目录被删除。

调用关系:这是远程插件编号格式的兼容性测试。它直接调用 mount_remote_plugin_detail_with_name,因为需要指定不同插件名;随后通过 wait_for_remote_plugin_request_count 验证网络调用次数。

调用图:调用 5 个内部函数(new, new, mount_remote_plugin_detail_with_name, wait_for_remote_plugin_request_count, write_remote_plugin_catalog_config);外部调用 16 个(given, start, new, new, Integer, to_response, write_chatgpt_auth, assert!, assert_eq!, format! (+6 more))。

plugin_uninstall_rejects_before_post_when_remote_detail_fetch_fails407–460 ↗
async fn plugin_uninstall_rejects_before_post_when_remote_detail_fetch_fails() -> Result<()>

作用:这个测试确认如果获取远程插件详情失败,程序会在调用云端卸载接口之前停下来。这样可以避免在不知道插件范围和版本的情况下乱删缓存或乱发卸载请求。

数据流:进去的是临时主目录、假的后端地址、登录凭证,以及一个预先创建的旧版远程插件缓存目录。测试没有挂载插件详情接口,所以详情请求会失败。发送卸载请求后,出来的是错误响应,错误码是 -32600,消息里提到远程插件目录请求失败。假服务器收到一次 GET 详情请求,收到零次 POST 卸载请求,本地遗留缓存仍然存在。

调用关系:这是远程卸载的失败保护测试。它故意不调用 mount_remote_plugin_detail,让详情获取失败;再用 wait_for_remote_plugin_request_count 分别确认“查详情发生了”和“卸载没有发生”。

调用图:调用 4 个内部函数(new, new, wait_for_remote_plugin_request_count, write_remote_plugin_catalog_config);外部调用 9 个(start, new, Integer, write_chatgpt_auth, assert!, assert_eq!, format!, create_dir_all, timeout)。

plugin_uninstall_rejects_remote_plugin_id_with_spaces_before_network_call463–495 ↗
async fn plugin_uninstall_rejects_remote_plugin_id_with_spaces_before_network_call() -> Result<()>

作用:这个测试确认带空格的远程插件编号会被立刻拒绝,不会拿去拼网络地址。这样可以减少奇怪输入造成的接口误调或安全风险。

数据流:进去的是临时主目录和假的远程插件配置,插件编号是“sample plugin”。测试启动 app server 后发出卸载请求。出来的是错误响应,错误码是 -32600,消息包含“invalid remote plugin id”。同时假服务器没有收到针对这个非法路径的 POST 卸载请求。

调用关系:这是输入校验测试。它用 write_remote_plugin_catalog_config 打开远程插件功能,但不配置任何详情或卸载 mock,因为期望请求在本地校验阶段就停止;wait_for_remote_plugin_request_count 用来证明没有网络副作用。

调用图:调用 3 个内部函数(new, wait_for_remote_plugin_request_count, write_remote_plugin_catalog_config);外部调用 7 个(start, new, Integer, assert!, assert_eq!, format!, timeout)。

plugin_uninstall_rejects_invalid_remote_plugin_id_before_network_call498–530 ↗
async fn plugin_uninstall_rejects_invalid_remote_plugin_id_before_network_call() -> Result<()>

作用:这个测试确认像“linear/../../oops”这种带路径跳转含义的插件编号会被拒绝。它防止插件编号被当成路径片段后访问到不该访问的地址。

数据流:进去的是临时主目录、假的后端服务器配置,以及一个包含“../”的恶意样式插件编号。测试发送卸载请求后,出来的是错误响应,错误码为 -32600,错误消息说明远程插件编号非法。假服务器没有收到用这个编号拼出来的 POST 卸载请求。

调用关系:这是安全输入校验测试。它和带空格编号的测试类似,重点是证明非法编号不会进入网络请求阶段;wait_for_remote_plugin_request_count 负责检查请求次数为零。

调用图:调用 3 个内部函数(new, wait_for_remote_plugin_request_count, write_remote_plugin_catalog_config);外部调用 7 个(start, new, Integer, assert!, assert_eq!, format!, timeout)。

plugin_uninstall_rejects_empty_remote_plugin_id533–558 ↗
async fn plugin_uninstall_rejects_empty_remote_plugin_id() -> Result<()>

作用:这个测试确认空字符串不能作为远程插件编号。没有这个检查,程序可能会拼出错误的云端地址,或者给用户返回不清楚的失败。

数据流:进去的是临时主目录、假的后端服务器配置,以及一个空的 plugin_id。测试启动 app server 并发送卸载请求。出来的是错误响应,错误码是 -32600,错误消息包含“invalid remote plugin id”。

调用关系:这是最基本的输入校验测试之一。它只需要打开远程插件配置并发送空编号,验证 app server 自己能在早期返回清楚的错误。

调用图:调用 2 个内部函数(new, write_remote_plugin_catalog_config);外部调用 8 个(start, new, new, Integer, assert!, assert_eq!, format!, timeout)。

write_installed_plugin560–577 ↗
fn write_installed_plugin(
    codex_home: &TempDir,
    marketplace_name: &str,
    plugin_name: &str,
) -> Result<()>

作用:这个辅助函数在临时目录里造出一个“看起来已经安装好”的本地插件。测试用它来快速搭场景,不用真的跑安装流程。

数据流:进去的是临时 Codex 主目录、市场名和插件名。函数把这些拼成“plugins/cache/市场名/插件名/local/.codex-plugin”目录,创建目录,并写入一个简单的 plugin.json,里面只有插件名。出来的结果是磁盘上多了一份假的插件缓存;如果创建或写文件失败,就把错误交给调用者。

调用关系:它被本地卸载测试和统计事件测试调用,用来准备卸载前的插件状态。真正的卸载动作不在这里发生,它只是布置测试现场。

调用图:被 2 处调用(plugin_uninstall_removes_plugin_cache_and_config_entry, plugin_uninstall_tracks_analytics_event);外部调用 4 个(path, format!, create_dir_all, write)。

write_remote_plugin_catalog_config579–595 ↗
fn write_remote_plugin_catalog_config(
    codex_home: &std::path::Path,
    base_url: &str,
) -> std::io::Result<()>

作用:这个辅助函数给临时 Codex 主目录写一份配置,让程序把远程插件接口指向测试里的假服务器,并打开插件和远程插件功能。

数据流:进去的是 Codex 主目录路径和一个 base_url。函数把 base_url 写进 config.toml 的 chatgpt_base_url,同时写入 features.plugins = true 和 features.remote_plugin = true。出来的结果是测试服务器启动后会把远程插件相关请求发到假后端,而不是发到真实网络。

调用关系:大多数远程插件卸载测试都会先调用它。它是远程测试的“换路牌”步骤,把 app server 的云端流量引到 wiremock 假服务器。

调用图:被 7 处调用(plugin_uninstall_accepts_workspace_remote_plugin_id_shape, plugin_uninstall_rejects_before_post_when_remote_detail_fetch_fails, plugin_uninstall_rejects_empty_remote_plugin_id, plugin_uninstall_rejects_invalid_remote_plugin_id_before_network_call, plugin_uninstall_rejects_remote_plugin_id_with_spaces_before_network_call, plugin_uninstall_uses_detail_scope_for_cache_namespace, plugin_uninstall_writes_remote_plugin_to_cloud_when_remote_plugin_enabled);外部调用 3 个(join, format!, write)。

mount_remote_plugin_detail597–611 ↗
async fn mount_remote_plugin_detail(
    server: &MockServer,
    remote_plugin_id: &str,
    release_version: &str,
    scope: &str,
)

作用:这个辅助函数给假后端挂上一个远程插件详情接口,默认插件名是“linear”。测试用它来模拟云端告诉程序:这个插件叫什么、版本是多少、属于哪个范围。

数据流:进去的是假服务器、远程插件编号、发布版本和 scope。函数不自己拼完整响应,而是把默认插件名“linear”和这些参数转交给 mount_remote_plugin_detail_with_name。出来的效果是假服务器具备一个 GET 插件详情接口。

调用关系:它是 mount_remote_plugin_detail_with_name 的简化包装。普通远程卸载测试不关心插件名变化时会用它;需要自定义插件名的工作区编号测试则直接调用更底层的函数。

调用图:调用 1 个内部函数(mount_remote_plugin_detail_with_name);被 2 处调用(plugin_uninstall_uses_detail_scope_for_cache_namespace, plugin_uninstall_writes_remote_plugin_to_cloud_when_remote_plugin_enabled)。

mount_remote_plugin_detail_with_name613–653 ↗
async fn mount_remote_plugin_detail_with_name(
    server: &MockServer,
    remote_plugin_id: &str,
    plugin_name: &str,
    release_version: &str,
    scope: &str,
)

作用:这个辅助函数在假后端上注册一个“获取远程插件详情”的接口,并能自定义插件名。它让测试不用访问真实云端,也能得到一份像真的一样的插件详情。

数据流:进去的是假服务器、远程插件编号、插件名、版本号和 scope。函数先根据 scope 判断要不要在响应里加 discoverability 字段,然后拼出一段 JSON,里面包括插件编号、名称、范围、安装策略、认证策略和 release 信息。最后它要求请求必须是 GET、路径匹配、并带正确 authorization 和 chatgpt-account-id 头,匹配后返回 200 和这段详情 JSON。出来的效果是后续 app server 查询插件详情时会拿到这份模拟数据。

调用关系:它被 mount_remote_plugin_detail 间接调用,也被需要特殊插件名的测试直接调用。它负责把“云端详情服务”这块零件装到 wiremock 假服务器上。

调用图:被 2 处调用(mount_remote_plugin_detail, plugin_uninstall_accepts_workspace_remote_plugin_id_shape);外部调用 6 个(given, new, format!, header, method, path)。

wait_for_remote_plugin_request_count655–688 ↗
async fn wait_for_remote_plugin_request_count(
    server: &MockServer,
    method_name: &str,
    path_suffix: &str,
    expected_count: usize,
) -> Result<()>

作用:这个辅助函数等待假服务器收到指定数量的远程插件请求。它避免测试因为异步请求还没来得及到达,就过早检查而误判失败。

数据流:进去的是假服务器、HTTP 方法名、路径后缀和期望次数。函数在最多 DEFAULT_TIMEOUT 的时间内反复读取假服务器记录的请求,筛选出方法相同且路径以指定后缀结尾的请求并计数。如果数量等于期望值,就成功返回;如果超过期望值,立刻报错;如果还不够,就短暂睡眠后继续等。出来的是成功的空结果,或者说明请求次数不对的错误。

调用关系:多个远程插件测试都用它来验证“该发的请求发了几次”和“不该发的请求一次也没发”。它位于测试断言阶段,专门检查 app server 与假后端之间的网络副作用。

调用图:被 6 处调用(plugin_uninstall_accepts_workspace_remote_plugin_id_shape, plugin_uninstall_rejects_before_post_when_remote_detail_fetch_fails, plugin_uninstall_rejects_invalid_remote_plugin_id_before_network_call, plugin_uninstall_rejects_remote_plugin_id_with_spaces_before_network_call, plugin_uninstall_uses_detail_scope_for_cache_namespace, plugin_uninstall_writes_remote_plugin_to_cloud_when_remote_plugin_enabled);外部调用 5 个(from_millis, received_requests, bail!, sleep, timeout)。

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

这个测试文件像是在模拟一个用户对应用服务器说:“请把名叫 debug 的 marketplace 移除。”它先搭一个临时的 Codex 主目录,里面放一份 marketplace 配置,再假装磁盘上已经安装了对应文件。然后启动测试用服务器,通过 JSON-RPC(一种用 JSON 格式发请求、收响应的通信方式)发送 marketplace/remove 请求。成功场景里,它检查响应里返回的名字和被删除目录是否正确,还检查配置文件里不再有 debug,磁盘目录也真的没了。失败场景里,它不提前准备任何 marketplace,直接请求删除 debug,确认服务器会拒绝,并返回“没有配置或安装”的明确错误。这个文件重要在于:删除操作很危险,既要删干净,又不能误删,还要对不存在的目标说清楚原因。

函数细节5
configured_marketplace_update20–29 ↗
fn configured_marketplace_update() -> MarketplaceConfigUpdate<'static>

作用:这个小函数准备一份假的 marketplace 配置信息,供测试使用。它相当于给测试造一个“用户曾经配置过这个 marketplace”的记录。

数据流:进去不需要任何输入 → 它直接组装出一份固定的 MarketplaceConfigUpdate,包括更新时间、来源类型、Git 地址和分支名 → 出来的是这份配置数据,后面的测试会把它写进临时配置文件。

调用关系:它被 marketplace_remove_deletes_config_and_installed_root 使用,用来先把名叫 debug 的 marketplace 记录到配置里。这样删除测试才有一个真实可删的目标。

调用图:被 1 处调用(marketplace_remove_deletes_config_and_installed_root)。

write_installed_marketplace31–36 ↗
fn write_installed_marketplace(codex_home: &std::path::Path, marketplace_name: &str) -> Result<()>

作用:这个函数在临时目录里伪造一个已经安装好的 marketplace。它不是联网安装,而是手动造出服务器认为“已安装”的目录和标记文件。

数据流:进去的是 Codex 主目录路径和 marketplace 名字 → 它先用 marketplace_install_root 找到 marketplace 应该安装到哪里,再创建 .agents/plugins 目录,并写入一个 marketplace.json 文件 → 出来是成功或失败结果,同时磁盘上多了一个假的已安装 marketplace 目录。

调用关系:它被 marketplace_remove_deletes_config_and_installed_root 调用,负责布置测试现场。它内部把路径计算交给 marketplace_install_root,把建目录和写文件交给系统文件操作 create_dir_all 和 write。

调用图:调用 1 个内部函数(marketplace_install_root);被 1 处调用(marketplace_remove_deletes_config_and_installed_root);外部调用 2 个(create_dir_all, write)。

canonicalize_path_with_existing_parent38–47 ↗
fn canonicalize_path_with_existing_parent(path: &std::path::Path) -> Result<std::path::PathBuf>

作用:这个函数把一个路径整理成更标准、可比较的样子,但允许路径最后一段本身已经不存在。它主要是为了测试删除后的目录路径时,不被相对路径、符号链接这类差异干扰。

数据流:进去的是一个路径 → 它先取出父目录和最后的文件名,要求这两样都必须存在;然后只把父目录转成标准绝对路径,再把原来的文件名接回去 → 出来的是一个更稳定的路径,用来和另一个路径做相等比较。

调用关系:它在这个测试文件里服务于删除成功的检查:因为目录可能已经被删了,不能直接对整个路径做普通标准化,所以它只标准化仍然存在的父目录。它会用到路径对象的 parent 和 file_name 这类基础方法。

调用图:外部调用 2 个(file_name, parent)。

marketplace_remove_deletes_config_and_installed_root50–88 ↗
async fn marketplace_remove_deletes_config_and_installed_root() -> Result<()>

作用:这是成功删除的完整测试。它确认删除 marketplace 时,服务器会同时清理配置文件和本地安装目录,并在响应里告诉调用方删掉的是哪个目录。

数据流:开始时它创建一个临时 Codex 主目录 → 写入 debug 的 marketplace 配置,再伪造 debug 的安装目录 → 启动 TestAppServer,并完成初始化 → 发送 marketplace/remove 请求 → 读取对应请求 ID 的成功响应,并转成 MarketplaceRemoveResponse → 最后检查响应里的名字和目录正确、config.toml 里不再有 debug、安装目录也不存在了。

调用关系:它是这个文件的主测试之一,由测试运行器通过 tokio::test 自动执行。它先调用 configured_marketplace_update 和 write_installed_marketplace 搭好现场,再通过 TestAppServer 走真实的服务器请求流程,最后用 to_response 解析响应,并用断言确认删除行为真的发生。

调用图:调用 4 个内部函数(new, configured_marketplace_update, write_installed_marketplace, marketplace_install_root);外部调用 8 个(new, Integer, to_response, assert!, assert_eq!, record_user_marketplace, read_to_string, timeout)。

marketplace_remove_rejects_unknown_marketplace91–115 ↗
async fn marketplace_remove_rejects_unknown_marketplace() -> Result<()>

作用:这是失败情况的测试。它确认当用户要求删除一个既没配置也没安装的 marketplace 时,服务器不会假装成功,而是返回明确错误。

数据流:开始时它只创建一个空的临时 Codex 主目录 → 启动并初始化测试服务器 → 发送删除 debug 的请求 → 等待读取同一个请求 ID 对应的错误响应 → 最后检查错误码是 -32600,错误消息说明 debug 没有配置或安装。

调用关系:它也是由 tokio::test 自动执行的测试。它不调用前面的造配置、造目录函数,故意保持环境为空;然后通过 TestAppServer 发请求,并用 timeout 防止测试一直卡住,最后用断言检查服务器拒绝请求的方式是否符合预期。

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

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

这个测试文件像是在给“插件市场升级”功能做一次真实演练。marketplace 可以理解成一包插件清单,用户配置里会记着它从哪里来、上次更新到哪个 Git 版本。这里先用临时文件夹搭出几个小型 Git 仓库,里面放 marketplace.json 和 marker.txt;marker.txt 就像一个版本标签,用来证明服务器最后拿到的是旧内容还是新内容。然后测试把这些仓库登记进临时的 codex_home 配置,启动 TestAppServer,通过 JSON-RPC(用 JSON 格式发请求和收回复的一种通信方式)发送升级请求。它检查四类重要情况:升级所有 Git marketplace、只升级指定 marketplace、已经最新时不返回安装目录、以及未知或本地 marketplace 会被拒绝。文件里还有一些辅助函数,负责造 Git 仓库、写配置、算预期安装路径、发送请求,让每个测试只关注“用户会看到什么结果”。

函数细节15
run_git26–37 ↗
fn run_git(cwd: &Path, args: &[&str]) -> Result<String>

作用:这个函数是在指定目录里运行一条 Git 命令,比如初始化仓库、提交文件、查看当前版本号。测试需要大量模拟真实 Git 仓库,所以它把“调用 git 并检查是否成功”这件事封装起来。

数据流:进去的是一个工作目录和一组 Git 参数 → 它在这个目录里启动 git 命令,拿到标准输出和错误输出 → 如果命令成功,就返回去掉首尾空白的输出文字;如果失败,就带着命令、目录和错误信息直接报错。

调用关系:它是底层小工具,被 init_marketplace_repo 和 commit_marketplace_marker 调用。上层测试不直接关心怎么跑 Git,只通过这些更具体的函数来准备仓库状态。

调用图:被 2 处调用(commit_marketplace_marker, init_marketplace_repo);外部调用 3 个(from_utf8_lossy, bail!, new)。

write_marketplace_files39–47 ↗
fn write_marketplace_files(root: &Path, marketplace_name: &str, marker: &str) -> Result<()>

作用:这个函数给一个临时仓库写入 marketplace 需要的最小文件。它让测试仓库看起来像一个真的插件市场来源。

数据流:进去的是仓库根目录、marketplace 名字和一个 marker 标记文字 → 它创建 .agents/plugins 目录,写入 marketplace.json,并写入 marker.txt → 出来时磁盘上多了一套可被识别的 marketplace 文件。

调用关系:它只被 init_marketplace_repo 使用。init_marketplace_repo 先靠它写好文件,再把这些文件提交到 Git,形成测试用的初始版本。

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

init_marketplace_repo49–57 ↗
fn init_marketplace_repo(root: &Path, marketplace_name: &str, marker: &str) -> Result<String>

作用:这个函数把一个空临时目录变成一个可用的 Git marketplace 仓库,并返回第一次提交的版本号。测试用它来制造“用户当前配置里记录的旧版本”。

数据流:进去的是目录、marketplace 名字和初始 marker 内容 → 它运行 git init,设置提交用的用户名和邮箱,写入 marketplace 文件,添加并提交所有文件 → 返回这个初始提交的 Git 版本号,也就是 HEAD。

调用关系:它被三个升级测试调用,用来准备每个测试的源仓库。它内部把具体工作交给 run_git 和 write_marketplace_files。

调用图:调用 2 个内部函数(run_git, write_marketplace_files);被 3 处调用(marketplace_upgrade_all_configured_git_marketplaces, marketplace_upgrade_named_marketplace_only, marketplace_upgrade_returns_empty_roots_when_already_up_to_date)。

commit_marketplace_marker59–64 ↗
fn commit_marketplace_marker(root: &Path, marker: &str) -> Result<String>

作用:这个函数在已有 Git 仓库里改一下 marker.txt 并提交,用来制造“远端已经有新版本”的场景。测试之后会检查服务器是否真的拿到了这个新 marker。

数据流:进去的是仓库目录和新的 marker 内容 → 它覆盖 marker.txt,把这个文件加入 Git 暂存区并提交 → 返回新提交的版本号。

调用关系:它被多个测试用在 init_marketplace_repo 之后,模拟 marketplace 源头发生了更新。它内部继续使用 run_git 来执行 Git 命令。

调用图:调用 1 个内部函数(run_git);被 3 处调用(marketplace_upgrade_all_configured_git_marketplaces, marketplace_upgrade_named_marketplace_only, marketplace_upgrade_returns_empty_roots_when_already_up_to_date);外部调用 2 个(join, write)。

configured_git_marketplace_update66–79 ↗
fn configured_git_marketplace_update(
    source: &'a str,
    last_revision: Option<&'a str>,
    ref_name: Option<&'a str>,
) -> MarketplaceConfigUpdate<'a>

作用:这个函数生成一份“这个 marketplace 来自 Git”的配置记录。测试用它来告诉服务器:这个 marketplace 应该按 Git 仓库来升级。

数据流:进去的是来源地址、可选的上次版本号、可选的分支名或提交名 → 它把这些值填进 MarketplaceConfigUpdate 结构里,并固定 source_type 为 git → 返回这份配置对象,不直接写磁盘。

调用关系:它被 record_git_marketplace 调用。record_git_marketplace 会再把这份配置真正写进用户配置文件。

调用图:被 1 处调用(record_git_marketplace)。

configured_local_marketplace_update81–90 ↗
fn configured_local_marketplace_update(source: &str) -> MarketplaceConfigUpdate<'_>

作用:这个函数生成一份“这个 marketplace 来自本地目录”的配置记录。它主要用来测试:升级接口不应该把本地 marketplace 当成 Git marketplace 来升级。

数据流:进去的是本地来源路径 → 它填出 MarketplaceConfigUpdate,并把 source_type 设为 local、版本号和 ref_name 留空 → 返回这份配置对象。

调用关系:它只在 marketplace_upgrade_rejects_unknown_or_non_git_marketplace 里使用,用来准备一个应该被升级请求拒绝的本地 marketplace。

调用图:被 1 处调用(marketplace_upgrade_rejects_unknown_or_non_git_marketplace)。

record_git_marketplace92–106 ↗
fn record_git_marketplace(
    codex_home: &Path,
    marketplace_name: &str,
    source: &Path,
    last_revision: &str,
    ref_name: Option<&str>,
) -> Result<()>

作用:这个函数把一个 Git marketplace 写进临时用户配置里。没有这一步,测试服务器启动后就不知道有哪些 marketplace 可以升级。

数据流:进去的是 codex_home 目录、marketplace 名字、Git 源目录、上次版本号和可选 ref_name → 它把源目录转成字符串,生成 Git 类型配置,再调用配置写入函数 → 出来时用户配置文件里记录了这个 marketplace。

调用关系:它被三个正常升级测试调用,是测试准备阶段的一部分。它把配置内容的生成交给 configured_git_marketplace_update,把真正写配置交给外部的 record_user_marketplace。

调用图:调用 1 个内部函数(configured_git_marketplace_update);被 3 处调用(marketplace_upgrade_all_configured_git_marketplaces, marketplace_upgrade_named_marketplace_only, marketplace_upgrade_returns_empty_roots_when_already_up_to_date);外部调用 2 个(display, record_user_marketplace)。

disable_plugin_startup_tasks108–116 ↗
fn disable_plugin_startup_tasks(codex_home: &Path) -> Result<()>

作用:这个函数关掉测试服务器启动时的插件相关后台任务。这样测试只观察“手动发送升级请求”的结果,避免启动过程自己做事干扰判断。

数据流:进去的是 codex_home 目录 → 它读取 config.toml,在末尾追加 features.plugins = false 这一段 → 出来时配置文件被改写,插件启动任务被禁用。

调用关系:它在几个升级测试启动 TestAppServer 之前调用。这样后面的 send_marketplace_upgrade 才是触发升级的唯一动作。

调用图:被 3 处调用(marketplace_upgrade_all_configured_git_marketplaces, marketplace_upgrade_named_marketplace_only, marketplace_upgrade_returns_empty_roots_when_already_up_to_date);外部调用 4 个(join, format!, read_to_string, write)。

marketplace_install_root118–120 ↗
fn marketplace_install_root(codex_home: &Path) -> std::path::PathBuf

作用:这个函数算出 marketplace 安装目录的总根路径。测试用它判断某个 marketplace 是否被安装到了预期位置。

数据流:进去的是 codex_home 目录 → 它拼上固定的 .tmp/marketplaces 子目录 → 返回安装根目录路径。

调用关系:它主要被 expected_installed_root 使用,也在一个测试里直接用来确认未被指定的 marketplace 没有被安装。

调用图:被 1 处调用(expected_installed_root);外部调用 1 个(join)。

expected_installed_root122–127 ↗
fn expected_installed_root(codex_home: &Path, marketplace_name: &str) -> Result<AbsolutePathBuf>

作用:这个函数算出某个具体 marketplace 升级后应该落在哪个绝对路径下。测试响应里会返回安装路径,所以需要一个可靠的预期值来对比。

数据流:进去的是 codex_home 和 marketplace 名字 → 它先把 codex_home 转成真实绝对路径,再拼出 .tmp/marketplaces/名字 → 返回 AbsolutePathBuf,也就是确认过的绝对路径;如果路径不能变成绝对路径就报错。

调用关系:它被“升级全部”和“只升级指定项”两个测试调用。它内部先用 marketplace_install_root 算总目录,再加上具体 marketplace 名字。

调用图:调用 2 个内部函数(marketplace_install_root, try_from);被 2 处调用(marketplace_upgrade_all_configured_git_marketplaces, marketplace_upgrade_named_marketplace_only);外部调用 1 个(canonicalize)。

send_marketplace_upgrade129–145 ↗
async fn send_marketplace_upgrade(
    mcp: &mut TestAppServer,
    marketplace_name: Option<&str>,
) -> Result<MarketplaceUpgradeResponse>

作用:这个异步函数向测试服务器发送 marketplace 升级请求,并等服务器返回成功结果。它把一串通信细节包装成“发一个升级请求,拿一个升级响应”。

数据流:进去的是 TestAppServer 和可选 marketplace 名字 → 它发送 MarketplaceUpgradeParams,拿到请求编号,然后在超时时间内等待对应编号的 JSON-RPC 成功响应 → 最后把通用响应转换成 MarketplaceUpgradeResponse 返回。

调用关系:它被三个成功路径测试调用。测试本身只关心返回内容是否正确,发送请求、等响应、转换类型这些活儿都交给它。

调用图:调用 2 个内部函数(read_stream_until_response_message, send_marketplace_upgrade_request);被 3 处调用(marketplace_upgrade_all_configured_git_marketplaces, marketplace_upgrade_named_marketplace_only, marketplace_upgrade_returns_empty_roots_when_already_up_to_date);外部调用 3 个(Integer, to_response, timeout)。

marketplace_upgrade_all_configured_git_marketplaces148–199 ↗
async fn marketplace_upgrade_all_configured_git_marketplaces() -> Result<()>

作用:这个测试确认:当请求里不指定 marketplace 名字时,服务器会升级所有已经配置好的 Git marketplace。它还确认升级后文件内容和配置里的版本号都变成新的。

数据流:开始时它创建临时 codex_home 和两个临时 Git 源仓库 → 先提交旧版本,再提交新版本,把两个 marketplace 按旧版本登记到配置里,并指定可更新到的新 revision → 启动测试服务器后发送“升级全部”请求 → 最后检查响应列出了 debug 和 tools、返回了两个安装目录、没有错误,磁盘上的 marker.txt 是新内容,config.toml 也记录了新版本号。

调用关系:这是完整成功路径的主测试。它调用 init_marketplace_repo、commit_marketplace_marker、record_git_marketplace、disable_plugin_startup_tasks、expected_installed_root 和 send_marketplace_upgrade,把仓库准备、配置准备、请求发送和结果验证串起来。

调用图:调用 7 个内部函数(new, commit_marketplace_marker, disable_plugin_startup_tasks, expected_installed_root, init_marketplace_repo, record_git_marketplace, send_marketplace_upgrade);外部调用 5 个(new, assert!, assert_eq!, read_to_string, timeout)。

marketplace_upgrade_named_marketplace_only202–250 ↗
async fn marketplace_upgrade_named_marketplace_only() -> Result<()>

作用:这个测试确认:如果请求里指定了某一个 marketplace,服务器只升级这个名字对应的 marketplace,不会顺手升级其他已配置项。

数据流:开始时它创建 debug 和 tools 两个 Git marketplace,并都制造新提交 → 把两个都登记进配置,然后启动测试服务器 → 发送只升级 tools 的请求 → 最后检查响应只选中了 tools、只返回 tools 的安装目录、tools 的 marker 是新内容,并且 debug 的安装目录根本不存在。

调用关系:它测试选择范围是否正确。准备阶段复用 init_marketplace_repo、commit_marketplace_marker、record_git_marketplace 和 disable_plugin_startup_tasks,请求阶段复用 send_marketplace_upgrade。

调用图:调用 7 个内部函数(new, commit_marketplace_marker, disable_plugin_startup_tasks, expected_installed_root, init_marketplace_repo, record_git_marketplace, send_marketplace_upgrade);外部调用 4 个(new, assert!, assert_eq!, timeout)。

marketplace_upgrade_returns_empty_roots_when_already_up_to_date253–283 ↗
async fn marketplace_upgrade_returns_empty_roots_when_already_up_to_date() -> Result<()>

作用:这个测试确认:同一个 Git marketplace 第一次升级成功后,再升级一次时,如果没有新版本,响应里的 upgraded_roots 应该是空的。也就是说服务器不会假装做了更新。

数据流:开始时它创建一个 debug Git 仓库,先有旧版本,再提交新版本,并把旧版本写进配置 → 启动服务器后第一次发送升级请求,让它更新到最新 → 接着第二次发送同样请求 → 最后检查第二次响应仍然选中了 debug、没有错误,但 upgraded_roots 是空数组。

调用关系:它验证升级操作的幂等性,也就是重复执行不会产生多余结果。它复用仓库准备和请求发送的辅助函数,重点观察第二次 send_marketplace_upgrade 的返回。

调用图:调用 6 个内部函数(new, commit_marketplace_marker, disable_plugin_startup_tasks, init_marketplace_repo, record_git_marketplace, send_marketplace_upgrade);外部调用 4 个(new, assert!, assert_eq!, timeout)。

marketplace_upgrade_rejects_unknown_or_non_git_marketplace286–318 ↗
async fn marketplace_upgrade_rejects_unknown_or_non_git_marketplace() -> Result<()>

作用:这个测试确认:服务器会拒绝升级不存在的 marketplace,或者拒绝把本地类型 marketplace 当成 Git marketplace 升级。这样可以避免对错误来源执行 Git 操作。

数据流:开始时它创建临时 codex_home,并登记一个 source_type 为 local 的 marketplace → 启动测试服务器后,分别发送升级 missing 和 local-only 的请求 → 对每个请求,它不等成功响应,而是等待错误响应 → 最后检查错误码是 -32600,错误信息明确说明这个 marketplace 没有被配置成 Git marketplace。

调用关系:这是失败路径测试。它用 configured_local_marketplace_update 准备本地配置,用 record_user_marketplace 写入配置,然后直接通过 TestAppServer 发送请求并读取错误响应,不使用 send_marketplace_upgrade,因为这里预期结果不是成功响应。

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

应用、钩子和技能发现

这些测试专注于列出应用服务器公开的高级扩展能力,包括来自本地、插件和 MCP 支持来源的应用、钩子和技能。

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

应用列表不是简单读一个文件:它要同时看远端应用目录、用户能访问的工具、插件缓存、本地配置和登录方式。这个测试文件就像一个小型排练场,先用临时目录伪造用户环境,再启动一个假的 HTTP 服务器来冒充 ChatGPT 后端,还启动一个假的 MCP(Model Context Protocol,一种让应用暴露工具给模型用的协议)服务来返回工具列表。测试会向真正的 TestAppServer 发送 JSON-RPC(一种用 JSON 包装请求和响应的通信格式)请求,然后检查返回的应用、更新通知、分页游标和错误。它特别关注边界情况:功能关闭时必须返回空、API key 登录不能拿连接器、强制刷新失败不能弄丢旧缓存、目录和工具加载先后不同也不能发错通知。

函数细节29
list_apps_returns_empty_when_connectors_disabled64–90 ↗
async fn list_apps_returns_empty_when_connectors_disabled() -> Result<()>

作用:测试当连接器功能没有打开时,请求应用列表应该得到空列表。这样可以防止用户明明关了功能,界面却还看到远端应用。

数据流:它创建一个临时的用户目录,启动测试版 app-server,完成初始化后发送 app/list 请求。服务器读取默认配置后发现连接器不可用,于是返回没有数据、也没有下一页游标的响应。测试把响应解析出来并确认列表为空。

调用关系:这是最基础的关开关测试。它不启动假的远端应用服务器,只通过 TestAppServer 走完整请求链路,最后用 to_response 把 JSON-RPC 响应还原成 AppsListResponse。

调用图:调用 1 个内部函数(new);外部调用 5 个(new, Integer, to_response, assert!, timeout)。

list_apps_returns_empty_with_api_key_auth93–155 ↗
async fn list_apps_returns_empty_with_api_key_auth() -> Result<()>

作用:测试用户用 API key 登录时,即使配置了连接器服务器,也不能返回 ChatGPT 连接器应用。因为这类应用依赖 ChatGPT 账号身份,不是普通 API key 能访问的。

数据流:它先搭一个假的应用服务器,里面放一个 beta 应用和对应工具;再写入连接器配置和 API key 认证文件。app-server 收到应用列表请求后会看到认证模式不合适,于是结果仍然是空列表。最后测试关闭假的服务器。

调用关系:这个测试会调用 start_apps_server_with_delays 搭远端假服务,用 write_connectors_config 写配置,用 save_auth 写 API key 登录状态,再通过 TestAppServer 验证真实逻辑不会去暴露这些应用。

调用图:调用 4 个内部函数(new, start_apps_server_with_delays, write_connectors_config, default);外部调用 7 个(new, Integer, to_response, assert!, save_auth, timeout, vec!)。

list_apps_returns_empty_when_workspace_codex_plugins_disabled158–217 ↗
async fn list_apps_returns_empty_when_workspace_codex_plugins_disabled() -> Result<()>

作用:测试团队或工作区设置里禁用了 Codex 插件时,应用列表也应该为空。它确认远端账号级别的开关会压过本地配置。

数据流:它启动一个假的服务器,并让工作区设置接口返回 enable_plugins=false。随后写入连接器配置和 ChatGPT 登录信息,启动 app-server,请求应用列表。服务器检查工作区设置后拒绝展示这些应用,输出空数据。

调用关系:这个测试使用 start_apps_server_with_workspace_plugins_enabled 专门控制工作区开关。它和普通连接器测试的区别是,app-server 会去请求 workspace_settings_response 这类账号设置接口。

调用图:调用 4 个内部函数(new, new_without_managed_config, start_apps_server_with_workspace_plugins_enabled, write_connectors_config);外部调用 7 个(new, Integer, to_response, write_chatgpt_auth, assert!, timeout, vec!)。

list_apps_includes_plugin_apps_for_chatgpt_auth220–261 ↗
async fn list_apps_includes_plugin_apps_for_chatgpt_auth() -> Result<()>

作用:测试 ChatGPT 登录用户能看到本地插件声明出来的应用。也就是说,应用列表不只来自远端目录,还可以来自插件缓存里的 .app.json。

数据流:它启动一个空的远端服务器,写入同时开启 connectors 和 plugins 的配置,再在临时目录里写一个插件应用夹具文件。app-server 请求应用列表时会扫描插件缓存,把 app_id 为 connector_sample 的应用合进结果里。

调用关系:这个测试依赖 write_connectors_and_plugins_config 打开两个功能,并用 write_plugin_app_fixture 造出插件元数据。它验证插件来源的数据能进入同一个 app/list 响应。

调用图:调用 5 个内部函数(new, new, start_apps_server_with_delays, write_connectors_and_plugins_config, write_plugin_app_fixture);外部调用 7 个(new, new, Integer, to_response, write_chatgpt_auth, assert!, timeout)。

list_apps_uses_thread_feature_flag_when_thread_id_is_provided264–364 ↗
async fn list_apps_uses_thread_feature_flag_when_thread_id_is_provided() -> Result<()>

作用:测试如果请求里带了 thread_id,应用列表会按这个会话线程的功能开关来判断,而不是只看最新的全局配置。这样正在进行的对话不会被后续配置改动突然影响。

数据流:它先在连接器开启时启动线程,拿到 thread.id。然后把 config.toml 改成 connectors=false。普通应用列表请求返回空;但带上之前 thread_id 的请求仍能看到 beta 应用,因为这个线程保留了创建时的功能状态。

调用关系:这个测试先走 thread/start,再走两次 app/list。它把同一个服务器状态放在“全局请求”和“线程请求”之间对比,确认线程上下文会参与应用列表判断。

调用图:调用 4 个内部函数(new, new, start_apps_server_with_delays, write_connectors_config);外部调用 10 个(new, Integer, default, to_response, write_chatgpt_auth, assert!, format!, write, timeout, vec!)。

list_apps_keeps_apps_with_app_only_tools_accessible367–431 ↗
async fn list_apps_keeps_apps_with_app_only_tools_accessible() -> Result<()>

作用:测试工具即使标记成只在 app 界面可见,也仍然能让对应应用显示为可访问。避免因为工具不该在别处出现,就误判用户不能用这个应用。

数据流:它创建 beta 应用和一个带 ui.visibility=["app"] 元数据的工具。app-server 拉取工具后识别出这个工具仍属于 beta 应用,于是返回一个 beta 应用,并把 is_accessible 标成 true。

调用关系:这个测试用 connector_tool 先造基础工具,再手动加上 app-only 元数据。它通过 start_apps_server_with_delays 把目录数据和工具数据一起喂给 app-server。

调用图:调用 5 个内部函数(new, new, connector_tool, start_apps_server_with_delays, write_connectors_config);外部调用 9 个(new, Integer, to_response, write_chatgpt_auth, assert!, assert_eq!, json!, timeout, vec!)。

list_apps_reports_is_enabled_from_config434–507 ↗
async fn list_apps_reports_is_enabled_from_config() -> Result<()>

作用:测试应用是否启用要听本地配置的。哪怕远端说 beta 应用默认启用,本地 config.toml 里写了 enabled=false,返回结果也必须体现为未启用。

数据流:它启动假服务器提供 beta 应用和工具,然后写 config.toml,里面开启 connectors,但在 [apps.beta] 下关闭这个应用。请求列表后,响应仍包含 beta,但 is_enabled 字段变成 false。

调用关系:这个测试直接写配置文件,不走 write_connectors_config,因为它需要额外插入 [apps.beta]。它检查远端目录结果和本地用户设置合并后的最终状态。

调用图:调用 3 个内部函数(new, new, start_apps_server_with_delays);外部调用 10 个(new, Integer, to_response, write_chatgpt_auth, assert!, assert_eq!, format!, write, timeout, vec!)。

list_apps_emits_updates_and_returns_after_both_lists_load510–680 ↗
async fn list_apps_emits_updates_and_returns_after_both_lists_load() -> Result<()>

作用:测试应用列表加载过程中会发更新通知,并且最终响应要等“可访问工具列表”和“目录应用列表”都加载完再返回。它保护前端不会拿到半截结果当最终结果。

数据流:它准备 alpha、beta 两个目录应用,其中 beta 有对应工具;目录接口故意慢 300 毫秒,工具接口立即返回。app-server 先发一次只包含可访问 beta 的更新,再等目录回来后发合并后的更新,最后 app/list 响应也返回完整合并结果。

调用关系:这个测试多次调用 read_app_list_updated_notification 读取服务器主动推送的 app/list/updated。它验证通知顺序、合并规则、品牌和元数据保留,以及最终 JSON-RPC 响应一致。

调用图:调用 5 个内部函数(new, new, read_app_list_updated_notification, start_apps_server_with_delays, write_connectors_config);外部调用 10 个(from_millis, from, new, Integer, to_response, write_chatgpt_auth, assert!, assert_eq!, timeout, vec!)。

list_apps_waits_for_accessible_data_before_emitting_directory_updates683–805 ↗
async fn list_apps_waits_for_accessible_data_before_emitting_directory_updates() -> Result<()>

作用:测试如果目录列表先回来、可访问工具列表后回来,服务器不能先推送一份只有目录应用的误导性更新。它要等知道哪些应用可访问后再发完整结果。

数据流:它让目录接口立即返回,工具接口延迟 300 毫秒。请求应用列表后,测试循环读取更新;如果最终合并结果还没到,任何中间更新都必须只包含可访问应用,不能是纯目录应用。最后响应必须等于完整的 beta 加 alpha。

调用关系:这个测试围绕 read_app_list_updated_notification 做顺序检查。它和上一个测试互补:一个目录慢,一个工具慢,用来覆盖两种竞态情况。

调用图:调用 5 个内部函数(new, new, read_app_list_updated_notification, start_apps_server_with_delays, write_connectors_config);外部调用 9 个(from_millis, new, Integer, to_response, write_chatgpt_auth, assert!, assert_eq!, timeout, vec!)。

list_apps_does_not_emit_empty_interim_updates808–895 ↗
async fn list_apps_does_not_emit_empty_interim_updates() -> Result<()>

作用:测试加载过程中不能发空的临时更新。否则前端可能会闪一下“没有应用”,造成误导。

数据流:它准备一个 alpha 目录应用,但让目录接口延迟返回,而且没有可访问工具。请求发出后,测试在 150 毫秒内尝试读更新,并期待超时;等目录真正回来后,才应该收到包含 alpha 的更新和最终响应。

调用关系:这个测试用 timeout 包住 read_app_list_updated_notification,故意确认“没有消息”也是正确行为。它检查通知系统不会把空缓存当作有效更新发出去。

调用图:调用 5 个内部函数(new, new, read_app_list_updated_notification, start_apps_server_with_delays, write_connectors_config);外部调用 10 个(from_millis, new, new, Integer, to_response, write_chatgpt_auth, assert!, assert_eq!, timeout, vec!)。

list_apps_paginates_results898–1038 ↗
async fn list_apps_paginates_results() -> Result<()>

作用:测试应用列表支持分页,也就是一次只要一部分结果时,会返回下一页游标。这样应用很多时前端不用一次加载全部。

数据流:它准备 beta 可访问应用和 alpha 目录应用,请求第一页时 limit=1,只拿到 beta 和一个 next_cursor。等完整列表更新后,再带着这个 cursor 请求第二页,拿到 alpha,且没有更多页。

调用关系:这个测试先用 app/list 得到游标,再监听更新确保后台缓存完整,然后再次 app/list。它验证分页是在合并后的稳定列表上进行的,而不是随便按某个单独来源分页。

调用图:调用 5 个内部函数(new, new, read_app_list_updated_notification, start_apps_server_with_delays, write_connectors_config);外部调用 9 个(from_millis, new, Integer, to_response, write_chatgpt_auth, assert!, assert_eq!, timeout, vec!)。

list_apps_force_refetch_preserves_previous_cache_on_failure1041–1142 ↗
async fn list_apps_force_refetch_preserves_previous_cache_on_failure() -> Result<()>

作用:测试强制刷新失败时,旧缓存不能被清空。这样网络或认证临时出错时,用户还能看到上一次成功加载的应用。

数据流:它先用有效 ChatGPT token 成功加载 beta,并保存初始结果。然后把认证文件改成无效 token,再用 force_refetch=true 请求刷新,预期收到错误。之后普通请求不强制刷新时,仍返回之前的 beta 缓存。

调用关系:这个测试通过两次写入认证文件制造“先成功、后失败”。它检查刷新路径的错误处理不会污染正常缓存路径。

调用图:调用 4 个内部函数(new, new, start_apps_server_with_delays, write_connectors_config);外部调用 8 个(new, Integer, to_response, write_chatgpt_auth, assert!, assert_eq!, timeout, vec!)。

list_apps_force_refetch_patches_updates_from_cached_snapshots1145–1383 ↗
async fn list_apps_force_refetch_patches_updates_from_cached_snapshots() -> Result<()>

作用:测试强制刷新时,如果已有缓存,服务器会先用旧快照补一份合理更新,再等新数据完成后替换成最终结果。这样刷新期间界面不会突然丢失全部应用。

数据流:它先加载 alpha v1 和 beta v1,其中 beta 可访问。然后通过控制器把假服务器改成只剩 alpha v2、没有工具。强制刷新后,先收到旧缓存更新;短时间内不会收到只有不可访问数据的半成品;最终收到只含 alpha v2 的结果。

调用关系:这是最复杂的缓存刷新测试,使用 start_apps_server_with_delays_and_control 拿到 AppsServerControl,运行中修改远端返回的数据和工具列表。它多次读取 app/list/updated 来验证刷新阶段的每一步。

调用图:调用 5 个内部函数(new, new, read_app_list_updated_notification, start_apps_server_with_delays_and_control, write_connectors_config);外部调用 10 个(from_millis, new, new, Integer, to_response, write_chatgpt_auth, assert!, assert_eq!, timeout, vec!)。

read_app_list_updated_notification1385–1398 ↗
async fn read_app_list_updated_notification(
    mcp: &mut TestAppServer,
) -> Result<AppListUpdatedNotification>

作用:这是测试用的小帮手,专门从 app-server 的输出流里等一条 app/list/updated 通知,并把它解析成好用的结构。

数据流:它接收一个可变的 TestAppServer,等待指定名字的通知消息。拿到原始通知后转换成 ServerNotification;如果不是 AppListUpdated 类型就报错,否则返回里面的 AppListUpdatedNotification 数据。

调用关系:多个需要检查“服务器主动推送更新”的测试都会调用它。它把读流、超时、类型检查这些重复步骤包起来,让测试主体只关注更新内容。

调用图:调用 1 个内部函数(read_stream_until_notification_message);被 5 处调用(list_apps_does_not_emit_empty_interim_updates, list_apps_emits_updates_and_returns_after_both_lists_load, list_apps_force_refetch_patches_updates_from_cached_snapshots, list_apps_paginates_results, list_apps_waits_for_accessible_data_before_emitting_directory_updates);外部调用 2 个(bail!, timeout)。

AppListMcpServer::new1416–1418 ↗
fn new(tools: Arc<StdMutex<Vec<Tool>>>, tools_delay: Duration) -> Self

作用:创建一个假的 MCP 工具服务器实例。测试用它来模拟远端服务会告诉 app-server:哪些连接器有可用工具。

数据流:它接收一份共享的工具列表和一个人为延迟时间,把两者保存进 AppListMcpServer。返回的新对象之后会被 HTTP MCP 服务调用。

调用关系:start_apps_server_with_delays_and_control_inner 在搭假服务器时会用它。每个 MCP 会话需要服务对象时,工厂函数就创建一个新的 AppListMcpServer。

AppsServerControl::set_connectors1428–1434 ↗
fn set_connectors(&self, connectors: Vec<AppInfo>)

作用:让测试在服务器已经跑起来之后,动态改掉假的目录应用列表。它用来模拟远端目录数据发生变化。

数据流:它接收新的 AppInfo 列表,拿到共享响应数据上的互斥锁(一把锁,防止两个任务同时改同一份数据),把 JSON 响应替换成 {apps: 新列表, next_token: null}。

调用关系:list_apps_force_refetch_patches_updates_from_cached_snapshots 会调用它,把远端从 alpha v1/beta v1 改成 alpha v2,从而测试强制刷新后的变化。

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

AppsServerControl::set_tools1436–1442 ↗
fn set_tools(&self, tools: Vec<Tool>)

作用:让测试在运行中改掉假的 MCP 工具列表。它用来模拟某个应用的可访问工具消失或变化。

数据流:它接收新的 Tool 列表,锁住共享工具容器,然后用新列表替换旧列表。之后 AppListMcpServer::list_tools 再被调用时,就会返回新工具。

调用关系:它和 set_connectors 配合使用,特别是在强制刷新测试里,让目录数据和可访问工具数据同时发生变化。

AppListMcpServer::get_info1446–1448 ↗
fn get_info(&self) -> ServerInfo

作用:告诉 MCP 客户端这个假服务器有什么能力。这里声明它支持列出工具。

数据流:它不读取外部输入,直接构造一个 ServerInfo,里面的能力标记开启 tools。返回值会作为 MCP 握手信息。

调用关系:MCP 服务初始化或握手时会调用它。app-server 通过这份信息知道可以继续请求 list_tools。

调用图:外部调用 2 个(builder, new)。

AppListMcpServer::list_tools1450–1472 ↗
fn list_tools(
        &self,
        _request: Option<rmcp::model::PaginatedRequestParams>,
        _context: rmcp::service::RequestContext<rmcp::service::RoleServer>,
    ) -> impl std::future::Futu

作用:返回当前假的工具列表。测试用它控制哪些应用被认为“用户可访问”。

数据流:它接收分页请求和上下文,但测试里不使用这些参数。函数可以先睡一段指定时间来制造慢响应,然后复制共享工具列表,包装成 ListToolsResult 返回,没有下一页。

调用关系:app-server 拉取可访问连接器工具时会触发它。多个测试通过 tools_delay 制造工具先到或后到,来检查 app-server 合并数据时是否稳健。

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

start_apps_server_with_delays1475–1485 ↗
async fn start_apps_server_with_delays(
    connectors: Vec<AppInfo>,
    tools: Vec<Tool>,
    directory_delay: Duration,
    tools_delay: Duration,
) -> Result<(String, JoinHandle<()>)>

作用:快速启动一个假的应用服务器,并允许分别设置目录接口和工具接口的延迟。大多数测试用它来搭标准假后端。

数据流:它接收连接器列表、工具列表和两个延迟时间,转交给带控制器的启动函数。拿到服务器地址、后台任务句柄和控制器后,只返回地址和句柄。

调用关系:许多应用列表测试都调用它。它是简化版入口,内部依赖 start_apps_server_with_delays_and_control,但隐藏掉运行中修改数据的控制器。

调用图:调用 1 个内部函数(start_apps_server_with_delays_and_control);被 10 处调用(list_apps_does_not_emit_empty_interim_updates, list_apps_emits_updates_and_returns_after_both_lists_load, list_apps_force_refetch_preserves_previous_cache_on_failure, list_apps_includes_plugin_apps_for_chatgpt_auth, list_apps_keeps_apps_with_app_only_tools_accessible, list_apps_paginates_results, list_apps_reports_is_enabled_from_config, list_apps_returns_empty_with_api_key_auth, list_apps_uses_thread_feature_flag_when_thread_id_is_provided, list_apps_waits_for_accessible_data_before_emitting_directory_updates)。

start_apps_server_with_workspace_plugins_enabled1487–1502 ↗
async fn start_apps_server_with_workspace_plugins_enabled(
    connectors: Vec<AppInfo>,
    tools: Vec<Tool>,
    workspace_plugins_enabled: bool,
) -> Result<(String, JoinHandle<()>)>

作用:启动一个假的应用服务器,并指定工作区设置里插件功能是开还是关。它专门服务工作区开关相关测试。

数据流:它接收连接器、工具和 workspace_plugins_enabled 布尔值,把目录和工具延迟都设为 0,然后调用底层启动函数。返回服务器地址和后台任务句柄。

调用关系:list_apps_returns_empty_when_workspace_codex_plugins_disabled 会调用它。它让 workspace_settings_response 返回可控的 enable_plugins 值。

调用图:调用 1 个内部函数(start_apps_server_with_delays_and_control_inner);被 1 处调用(list_apps_returns_empty_when_workspace_codex_plugins_disabled)。

start_apps_server_with_delays_and_control1504–1518 ↗
async fn start_apps_server_with_delays_and_control(
    connectors: Vec<AppInfo>,
    tools: Vec<Tool>,
    directory_delay: Duration,
    tools_delay: Duration,
) -> Result<(String, JoinHandle<()>, A

作用:启动假的应用服务器,并把运行中可修改数据的控制器也返回给测试。适合测试刷新、缓存、数据变化这些场景。

数据流:它接收初始连接器、工具和延迟,默认把工作区插件开关设为 true,然后调用更底层的启动函数。输出服务器地址、后台任务句柄和 AppsServerControl。

调用关系:start_apps_server_with_delays 会间接用它;强制刷新补丁测试会直接用它,因为那类测试需要中途改服务器返回内容。

调用图:调用 1 个内部函数(start_apps_server_with_delays_and_control_inner);被 2 处调用(list_apps_force_refetch_patches_updates_from_cached_snapshots, start_apps_server_with_delays)。

start_apps_server_with_delays_and_control_inner1520–1574 ↗
async fn start_apps_server_with_delays_and_control_inner(
    connectors: Vec<AppInfo>,
    tools: Vec<Tool>,
    directory_delay: Duration,
    tools_delay: Duration,
    workspace_plugins_enabled: b

作用:这是搭建假后端的核心函数。它真的绑定本地端口,注册 HTTP 路由,挂上 MCP 服务,然后把服务器跑在后台。

数据流:它接收初始应用列表、工具列表、延迟和工作区开关。函数把应用响应和工具列表放进可共享、可加锁的容器,建立 AppsServerState,绑定 127.0.0.1 的随机端口,配置目录接口、工作区设置接口和 MCP 服务,最后启动 axum HTTP 服务器并返回访问地址、任务句柄、控制器。

调用关系:所有假服务器启动函数最终都会到这里。它把 list_directory_connectors、workspace_settings_response 和 AppListMcpServer 组装成一个完整测试后端。

调用图:被 2 处调用(start_apps_server_with_delays_and_control, start_apps_server_with_workspace_plugins_enabled);外部调用 12 个(new, default, new, new, default, new, bind, get, serve, format! (+2 more))。

workspace_settings_response1576–1598 ↗
async fn workspace_settings_response(
    State(state): State<Arc<AppsServerState>>,
    headers: HeaderMap,
) -> Result<impl axum::response::IntoResponse, StatusCode>

作用:这是假的账号设置接口,返回工作区是否允许插件。它还检查请求有没有带正确的认证头。

数据流:它读取共享状态和 HTTP 请求头,检查 Authorization 是否等于预期 Bearer token,以及 chatgpt-account-id 是否匹配。认证失败返回 401;成功则返回 JSON,里面的 beta_settings.enable_plugins 来自测试预设。

调用关系:app-server 查询账号工作区设置时会访问这个路由。工作区插件关闭测试依赖它给出 false。

调用图:外部调用 3 个(get, Json, json!)。

list_directory_connectors1600–1633 ↗
async fn list_directory_connectors(
    State(state): State<Arc<AppsServerState>>,
    headers: HeaderMap,
    uri: Uri,
) -> Result<impl axum::response::IntoResponse, StatusCode>

作用:这是假的连接器目录接口,模拟 ChatGPT 后端返回应用目录列表。它同时检查认证和查询参数,确保 app-server 发出的请求格式正确。

数据流:它先按需要等待 directory_delay,模拟慢网络。然后检查 Bearer token、账号 ID,以及 URL 查询里是否有 external_logos=true。认证不对返回 401,参数缺失返回 400;都正确时,复制当前共享响应 JSON 并返回。

调用关系:app-server 拉远端应用目录时会访问这个路由。多个测试通过延迟和 AppsServerControl 改变它返回的内容,来验证合并、通知和刷新行为。

调用图:外部调用 4 个(get, query, Json, sleep)。

connector_tool1635–1654 ↗
fn connector_tool(connector_id: &str, connector_name: &str) -> Result<Tool>

作用:创建一个测试用的 MCP 工具,并在工具元数据里标明它属于哪个连接器应用。这样 app-server 能把“工具可用”翻译成“应用可访问”。

数据流:它接收 connector_id 和 connector_name,先构造一个简单的 JSON schema(描述工具输入格式的 JSON 结构),再创建 Tool,标记为只读,并写入 connector_id、connector_name 元数据。成功时返回这个工具。

调用关系:很多测试需要工具列表,但调用图只标出部分直接调用。这个函数生成的工具会被 AppListMcpServer::list_tools 返回,进而影响应用的 is_accessible 和显示名称。

调用图:被 1 处调用(list_apps_keeps_apps_with_app_only_tools_accessible);外部调用 9 个(new, Borrowed, Owned, new, new, format!, json!, new, from_value)。

write_connectors_config1656–1670 ↗
fn write_connectors_config(codex_home: &std::path::Path, base_url: &str) -> std::io::Result<()>

作用:往临时用户目录写一份最常用的测试配置:指定假的 ChatGPT 地址,并打开 connectors 功能。

数据流:它接收 codex_home 路径和 base_url,拼出 config.toml 文件路径,然后写入 chatgpt_base_url、凭据存储方式和 [features] connectors=true。返回文件写入结果。

调用关系:大多数连接器测试都会先调用它,再启动 TestAppServer。它让 app-server 知道该去哪个假服务器拉目录数据。

调用图:被 10 处调用(list_apps_does_not_emit_empty_interim_updates, list_apps_emits_updates_and_returns_after_both_lists_load, list_apps_force_refetch_patches_updates_from_cached_snapshots, list_apps_force_refetch_preserves_previous_cache_on_failure, list_apps_keeps_apps_with_app_only_tools_accessible, list_apps_paginates_results, list_apps_returns_empty_when_workspace_codex_plugins_disabled, list_apps_returns_empty_with_api_key_auth, list_apps_uses_thread_feature_flag_when_thread_id_is_provided, list_apps_waits_for_accessible_data_before_emitting_directory_updates);外部调用 3 个(join, format!, write)。

write_connectors_and_plugins_config1672–1690 ↗
fn write_connectors_and_plugins_config(codex_home: &Path, base_url: &str) -> std::io::Result<()>

作用:写一份同时开启连接器和插件的测试配置。它用于验证插件里的应用也能进入应用列表。

数据流:它接收临时用户目录和假服务器地址,写入 config.toml。内容包含 chatgpt_base_url、凭据存储方式、connectors=true、plugins=true,以及启用名为 sample@test 的插件。

调用关系:list_apps_includes_plugin_apps_for_chatgpt_auth 会调用它,并配合 write_plugin_app_fixture 写出的插件缓存一起测试插件应用来源。

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

write_plugin_app_fixture1692–1712 ↗
fn write_plugin_app_fixture(codex_home: &Path, plugin_name: &str, app_id: &str) -> Result<()>

作用:在临时目录里造一个假的插件缓存,让测试模拟“某个插件声明了一个应用”。

数据流:它接收用户目录、插件名和应用 ID,创建 plugins/cache/test/<plugin>/local/.codex-plugin 目录,写入 plugin.json,再写入 .app.json,里面声明这个插件对应的 app_id。完成后返回成功或文件错误。

调用关系:插件应用列表测试会在写完插件配置后调用它。app-server 扫描插件缓存时会读到这些文件,并把声明的应用加入 app/list 结果。

调用图:被 1 处调用(list_apps_includes_plugin_apps_for_chatgpt_auth);外部调用 6 个(join, format!, json!, to_vec_pretty, create_dir_all, write)。

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

这个测试文件像一份验收清单,确认服务器在面对用户配置、插件配置、项目配置和 Git worktree 这种特殊目录时,都能列出正确的 hook。它会临时造出假的用户目录、项目目录和插件目录,写入配置文件,然后启动测试版服务器,通过 JSON-RPC(一种用 JSON 发请求、收响应的通信格式)发起 hooks/list、config 写入、thread start 和 turn start 请求。测试重点不只是“能不能列出来”,还包括:坏的插件配置要给警告;不同工作目录要按各自配置判断 hooks 是否开启;同一个 hook 被用户信任后会变成 Trusted;hook 内容改了以后会变成 Modified;把 hook 禁用后,正在运行的会话也不该再执行它。没有这些测试,hook 这种能运行命令的功能很容易出现安全状态显示错误、配置刷新不生效、或者插件 hook 漏加载的问题。

函数细节13
command_hook_hash43–67 ↗
fn command_hook_hash(
    event_name: &'static str,
    matcher: Option<&str>,
    command: &str,
    timeout_sec: u64,
    status_message: Option<&str>,
) -> String

作用:这个辅助函数用来算出一个命令型 hook 的“身份指纹”。测试用它来和服务器返回的 current_hash 对比,确认服务器判断 hook 是否被改过时用的是同一套规则。

数据流:进去的是事件名、匹配条件、命令、超时时间和状态提示文字 → 函数把这些拼成一个标准化的 hook 配置对象,再转成 TOML 值,最后调用配置库算版本哈希 → 出来的是一段字符串,代表这个 hook 当前内容的指纹;它不改文件,也不改服务器状态。

调用关系:它不直接和测试服务器通信,而是在多个断言里当“标准答案”使用。各个 hooks/list 测试拿服务器返回的 current_hash 和它算出的值比较,确保列表接口给出的信任依据是稳定、可预测的。

调用图:外部调用 4 个(try_from, version_for_toml, unreachable!, vec!)。

write_user_hook_config69–85 ↗
fn write_user_hook_config(codex_home: &std::path::Path) -> Result<()>

作用:这个辅助函数在临时的 Codex 用户目录里写一份用户级 hook 配置。测试用它快速准备“用户自己配置了一个命令 hook”的场景。

数据流:进去的是一个 codex_home 路径 → 函数在这个目录下写入 config.toml,里面声明 PreToolUse 事件、Bash 匹配条件、要运行的 python 命令、超时时间和状态提示 → 出来是成功或失败结果;磁盘上多了这份用户配置文件。

调用关系:它被 hooks_list_shows_discovered_hook 和 config_batch_write_toggles_user_hook 调用。前者用它检查 hook 能否被列出,后者用它检查同一个 hook 能否通过配置写入接口打开和关闭。

调用图:被 2 处调用(config_batch_write_toggles_user_hook, hooks_list_shows_discovered_hook);外部调用 2 个(join, write)。

write_plugin_hook_config87–107 ↗
fn write_plugin_hook_config(codex_home: &std::path::Path, hooks_json: &str) -> Result<()>

作用:这个辅助函数搭建一个假的插件目录,并写入插件 hook 配置。测试用它模拟“插件带来了 hook”的情况。

数据流:进去的是 codex_home 路径和一段 hooks_json 文本 → 函数创建插件缓存目录、插件说明文件 plugin.json、插件的 hooks/hooks.json,并在用户 config.toml 里打开 plugins 和 hooks 功能且启用这个插件 → 出来是成功或失败结果;磁盘上形成一个可被服务器发现的插件安装结构。

调用关系:它被插件相关的三个测试调用:一个检查正常插件 hook 能列出来,一个检查坏 JSON 会产生警告,一个检查 hooks/list 会预热插件能力,方便后续 thread start 使用插件 MCP 服务器信息。

调用图:被 3 处调用(hooks_list_shows_discovered_plugin_hook, hooks_list_shows_plugin_hook_load_warnings, hooks_list_warms_plugin_capabilities_for_thread_start);外部调用 3 个(join, create_dir_all, write)。

write_project_hook_config109–130 ↗
fn write_project_hook_config(dot_codex_folder: &std::path::Path, command: &str) -> Result<()>

作用:这个辅助函数给某个项目的 .codex 目录写一份项目级 hook 配置。它主要服务于 Git worktree 场景的测试。

数据流:进去的是 .codex 文件夹路径和要执行的命令字符串 → 函数创建目录,并写入 config.toml,里面打开 hooks 功能,配置一个 PreToolUse 的命令 hook → 出来是成功或失败结果;对应项目目录下多了一份项目 hook 配置。

调用关系:它被 hooks_list_uses_root_repo_hooks_for_linked_worktrees 调用两次,分别给主仓库和 linked worktree 写不同 hook,从而验证服务器最终应该选择主仓库的配置,而不是误用 worktree 自己的配置。

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

hooks_list_shows_discovered_hook133–187 ↗
async fn hooks_list_shows_discovered_hook() -> Result<()>

作用:这个测试确认:用户配置文件里的 hook 会被 hooks/list 接口发现,并且返回的每个字段都正确。它检查的不只是“有一个 hook”,还检查来源、命令、超时、显示顺序、信任状态等细节。

数据流:开始时创建临时用户目录和临时工作目录,并写入用户 hook 配置 → 启动测试服务器,发送 hooks/list 请求 → 读取 JSON-RPC 响应并转成 HooksListResponse → 最后把返回内容和预期的 HooksListEntry 精确比较;测试过程中会读写临时文件,但不影响真实用户环境。

调用关系:它调用 write_user_hook_config 准备配置,再通过 TestAppServer 模拟客户端请求服务器。服务器返回后,它用 command_hook_hash 生成预期哈希,作为断言的一部分。

调用图:调用 3 个内部函数(new, write_user_hook_config, from_absolute_path);外部调用 7 个(new, Integer, to_response, assert_eq!, canonicalize, timeout, vec!)。

hooks_list_shows_discovered_plugin_hook190–265 ↗
async fn hooks_list_shows_discovered_plugin_hook() -> Result<()>

作用:这个测试确认:插件提供的 hook 也会出现在 hooks/list 结果里,并被标记成插件来源。它保证插件 hook 不会被误认为用户 hook 或项目 hook。

数据流:开始时创建临时目录,写入一个启用状态的假插件和它的 hooks.json → 启动测试服务器并发送 hooks/list 请求 → 把响应解析成 HooksListResponse → 对比返回的 hook 元数据,包括 plugin_id、source_path、命令、超时、状态提示和未信任状态。

调用关系:它依赖 write_plugin_hook_config 搭建插件环境,通过 TestAppServer 调服务器接口。断言时同样借助 command_hook_hash 算出插件 hook 的预期内容指纹。

调用图:调用 3 个内部函数(new, write_plugin_hook_config, from_absolute_path);外部调用 7 个(new, Integer, to_response, assert_eq!, canonicalize, timeout, vec!)。

hooks_list_warms_plugin_capabilities_for_thread_start268–343 ↗
async fn hooks_list_warms_plugin_capabilities_for_thread_start() -> Result<()>

作用:这个测试确认:调用 hooks/list 不只是列 hook,还会顺便让服务器预先知道插件的能力信息。这样后面启动线程时,即使插件的 MCP 配置文件被删了,服务器仍然能按之前发现的信息启动插件相关服务。

数据流:先创建带 hook 的假插件,再额外写入插件的 .mcp.json,里面声明一个 plugin-server → 启动服务器并调用 hooks/list → 删除 .mcp.json → 再发 thread start 请求 → 最后等待一条通知,确认名为 plugin-server 的 MCP 服务器开始启动。

调用关系:它先用 write_plugin_hook_config 准备插件 hook,再手动补充 MCP 配置。hooks/list 在这里位于 thread start 之前,作用像“提前扫一遍仓库”,后续 thread start 依赖这次扫描留下的插件能力缓存。

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

hooks_list_shows_plugin_hook_load_warnings346–375 ↗
async fn hooks_list_shows_plugin_hook_load_warnings() -> Result<()>

作用:这个测试确认:插件的 hook 配置如果写坏了,服务器不会崩溃,而是返回一条可读的警告。这样界面可以告诉用户问题在哪里,而不是静默失败。

数据流:进去的场景是一份内容为“{ not-json”的坏 hooks.json → 测试启动服务器并请求 hooks/list → 响应中应该没有 hook,但 warnings 里应该有一条解析失败的提示 → 测试断言 hook 列表为空、警告数量为 1、警告文字包含指定内容。

调用关系:它通过 write_plugin_hook_config 故意制造坏插件配置,然后让 TestAppServer 走真实的列表接口。这个测试覆盖的是异常路径,和正常插件 hook 列表测试形成互补。

调用图:调用 2 个内部函数(new, write_plugin_hook_config);外部调用 7 个(new, Integer, to_response, assert!, assert_eq!, timeout, vec!)。

hooks_list_uses_each_cwds_effective_feature_enablement378–469 ↗
async fn hooks_list_uses_each_cwds_effective_feature_enablement() -> Result<()>

作用:这个测试确认:一次 hooks/list 请求里如果带了多个工作目录,服务器会分别按每个目录的有效配置判断 hooks 是否开启。也就是说,不会因为一个目录禁用了 hooks,就把另一个项目里开启的 hooks 也漏掉。

数据流:先在用户配置里把 hooks 关掉,再创建一个项目目录,在项目的 .codex/config.toml 里把 hooks 打开并配置项目 hook,同时把项目标记为可信 → 启动服务器后一次请求两个 cwd → 返回结果中,第一个 cwd 没有 hook,第二个 cwd 有项目 hook → 测试精确比较两份结果。

调用关系:它直接写用户配置和项目配置,并调用 set_project_trust_level 设置信任等级。随后通过 TestAppServer 发送 hooks/list,请服务器对每个 cwd 独立计算配置效果。

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

hooks_list_uses_root_repo_hooks_for_linked_worktrees472–551 ↗
async fn hooks_list_uses_root_repo_hooks_for_linked_worktrees() -> Result<()>

作用:这个测试检查 Git linked worktree 的特殊情况:worktree 目录应该使用主仓库根目录的 hook 配置。Git linked worktree 可以理解成同一个仓库的另一个工作副本。

数据流:测试先手工造出一个主仓库目录、一个 worktree 目录,以及指向主仓库 .git/worktrees 的 .git 文件 → 主仓库和 worktree 各写一份不同命令的项目 hook → 把主仓库设为可信 → 请求两个目录的 hooks/list → 断言两边看到的都是主仓库 hook,key 和 source_path 也相同 → 再写入 trusted_hash 信任这个 hook,重新列 worktree,确认信任状态变成 Trusted。

调用关系:它调用 write_project_hook_config 准备两套相互冲突的 hook,再用 set_project_trust_level 和配置写入接口模拟用户信任操作。这个测试把 hooks/list 和 config_batch_write 连起来,确认 linked worktree 下的 hook 身份不会漂移。

调用图:调用 4 个内部函数(new, write_project_hook_config, set_project_trust_level, from_absolute_path);外部调用 9 个(new, Integer, to_response, assert_eq!, format!, create_dir_all, write, timeout, vec!)。

config_batch_write_toggles_user_hook554–650 ↗
async fn config_batch_write_toggles_user_hook() -> Result<()>

作用:这个测试确认:用户可以通过配置批量写入接口打开或关闭某个 hook,而且 hooks/list 能马上反映这个开关状态。它保障界面上的“启用/禁用 hook”按钮真的生效。

数据流:先写入用户 hook 配置并启动服务器 → 第一次 hooks/list 看到 hook 是 enabled=true → 发送 config_batch_write,把 hooks.state 里这个 hook 的 enabled 改成 false → 再次 hooks/list,确认同一个 key 的 hook 变成 disabled → 再写回 enabled=true → 最后确认它重新启用。

调用关系:它调用 write_user_hook_config 准备 hook,然后交替使用 hooks/list 和 config_batch_write 两类请求。这个测试关注列表接口和配置写入接口之间的联动。

调用图:调用 2 个内部函数(new, write_user_hook_config);外部调用 6 个(new, Integer, to_response, assert_eq!, timeout, vec!)。

config_batch_write_updates_hook_trust_for_loaded_session653–901 ↗
async fn config_batch_write_updates_hook_trust_for_loaded_session() -> Result<()>

作用:这个测试确认:在一个已经启动的会话里,用户后来信任 hook,或修改了 hook 内容,运行时都会马上按新状态执行或停止执行。它重点保障 hook 的安全状态不会只在启动时读一次就固定不变。

数据流:测试先准备一个假的模型响应服务器,以及一个 Python hook 脚本;脚本被执行时会把收到的输入写进日志文件 → 启动 app server,列出 hook,确认它未信任 → 开始线程并跑第一轮对话,未信任所以日志文件不存在 → 通过 config_batch_write 写入 trusted_hash 信任它 → 再列 hook,状态变 Trusted → 跑第二轮对话,hook 被执行,日志增加一行 → 接着改 hook 配置,让内容哈希变化 → 再列 hook,状态变 Modified → 跑第三轮对话,hook 不再执行,日志行数保持不变。

调用关系:它串起了更多系统部件:mock 响应服务器提供模型输出,TestAppServer 处理线程和回合请求,config_batch_write 改 hook 信任状态,hooks/list 验证状态变化,turn_start 验证真实执行效果。Windows 下会跳过,因为测试运行命令脚本的行为依赖类 Unix 环境。

调用图:调用 1 个内部函数(new);外部调用 13 个(default, new, Integer, create_mock_responses_server_sequence_unchecked, to_response, assert!, assert_eq!, assert_ne!, format!, skip_if_windows! (+3 more))。

config_batch_write_disables_hook_for_loaded_session904–1094 ↗
async fn config_batch_write_disables_hook_for_loaded_session() -> Result<()>

作用:这个测试确认:即使会话已经启动,用户把 hook 禁用后,后续回合也不会再执行它。它防止“列表里显示禁用了,但后台还在跑脚本”的危险情况。

数据流:测试先搭建假的模型响应服务器和一个会写日志的 Python hook → 启动服务器并列出 hook,确认它启用 → 写入 trusted_hash,让 hook 可信 → 启动线程并跑第一轮对话,hook 执行,日志有一行 → 通过 config_batch_write 把这个 hook 的 enabled 改成 false → 跑第二轮对话 → 最后检查日志仍然只有一行,说明禁用后没有再次执行。

调用关系:它和 config_batch_write_updates_hook_trust_for_loaded_session 很像,但关注点从“信任状态变化”换成“启用开关变化”。它通过 hooks/list 找到 hook 的 key 和 hash,再用 config_batch_write 改状态,最后用 turn_start 的实际运行结果验证配置刷新真的影响了已加载会话。

调用图:调用 1 个内部函数(new);外部调用 11 个(default, new, Integer, create_mock_responses_server_sequence_unchecked, to_response, assert_eq!, format!, skip_if_windows!, write, timeout (+1 more))。

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

这里的“技能”可以理解成放在特定文件夹里的说明书文件,服务器会读这些文件,再告诉客户端当前有哪些可用能力。这个测试文件会临时搭建假的用户目录、项目目录和假的网络服务器,然后通过应用服务器的 JSON-RPC(一种用 JSON 传请求和响应的通信格式)接口发请求,检查返回结果。它覆盖了很多容易出岔子的情况:插件被禁用时不能露出插件技能;远程插件安装信息刷新后,要能从本地缓存读到技能;环境变量关掉执行环境时,不要扫描当前项目里的技能;相对路径也要能接受;多目录请求要保持原顺序;普通请求会用缓存,强制刷新才重新扫描;运行中新增额外技能目录后,要发出“技能已变化”的通知。整体像是在给“技能目录雷达”做验收,确保它既找得到该找的,也不会把不该出现的东西报出来。

函数细节14
write_skill38–44 ↗
fn write_skill(root: &TempDir, name: &str) -> Result<()>

作用:在一个临时用户目录里造出一个假的技能文件,供测试使用。这样测试不用依赖开发者电脑上真实存在的文件。

数据流:输入是一个临时目录和技能名字 → 它在这个目录下创建 skills/技能名 文件夹,并写入一份 SKILL.md,里面带有名字和描述 → 输出是成功或失败;成功后,磁盘上多了一个可被服务器扫描到的技能。

调用关系:多个测试在启动服务器前调用它,先把测试场景布置好。后面的技能列表请求会间接读到它写出的文件,用来验证服务器是否能发现或更新这些技能。

调用图:被 3 处调用(skills_changed_notification_is_emitted_after_skill_change, skills_list_excludes_plugin_skills_when_workspace_codex_plugins_disabled, skills_list_skips_cwd_roots_when_environment_disabled);外部调用 4 个(path, format!, create_dir_all, write)。

expect_skills_changed_notification46–61 ↗
async fn expect_skills_changed_notification(
    mcp: &mut TestAppServer,
    timeout_duration: Duration,
) -> Result<()>

作用:等待并检查服务器确实发出了“技能变了”的通知。它用于确认服务器不只是内部更新了,还主动告诉了客户端。

数据流:输入是正在运行的测试服务器连接和等待时长 → 它在限定时间内读取通知消息,要求消息名是 skills/changed,并把通知参数解析成固定格式 → 输出是成功或失败;如果没有通知、通知格式不对,测试就失败。

调用关系:它被运行时额外技能目录的测试反复使用。测试先发送修改技能根目录的请求,然后把等待通知这一步交给它,确保每次修改都会触发客户端可见的提示。

调用图:调用 1 个内部函数(read_stream_until_notification_message);被 1 处调用(skills_extra_roots_set_updates_process_runtime_roots);外部调用 3 个(assert_eq!, from_value, timeout)。

write_plugins_enabled_config_with_base_url63–77 ↗
fn write_plugins_enabled_config_with_base_url(
    codex_home: &std::path::Path,
    base_url: &str,
) -> std::io::Result<()>

作用:写一份测试用配置文件,打开插件功能,并指定假的 ChatGPT 后端地址。这样测试可以让服务器去访问本地 mock 服务器,而不是访问真实网络。

数据流:输入是用户目录路径和后端基础网址 → 它把 config.toml 写到用户目录里,内容包括 chatgpt_base_url 和 plugins = true → 输出是文件写入结果;成功后,服务器启动时会按这份配置启用插件。

调用关系:插件禁用场景的测试会先调用它准备配置。之后测试服务器读取这份配置,再去 mock 后端查询账号设置,从而验证插件技能是否应该被隐藏。

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

write_remote_plugins_enabled_config_with_base_url79–94 ↗
fn write_remote_plugins_enabled_config_with_base_url(
    codex_home: &std::path::Path,
    base_url: &str,
) -> std::io::Result<()>

作用:写一份同时打开普通插件和远程插件的测试配置。它让远程插件相关测试能走完整的配置路径。

数据流:输入是用户目录路径和假的后端基础网址 → 它写出 config.toml,里面启用 plugins 和 remote_plugin → 输出是写文件结果;成功后,服务器会认为远程插件功能可用,并向指定地址请求插件信息。

调用关系:远程插件缓存测试会用它搭好开关。之后服务器启动、列插件、刷新安装状态,都依赖这份配置指向 mock 网络服务。

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

write_plugin_with_skill96–135 ↗
fn write_plugin_with_skill(
    repo_root: &std::path::Path,
    plugin_name: &str,
    skill_name: &str,
) -> Result<()>

作用:在一个临时项目里造出一个本地插件,并让这个插件自带一个技能。它用来模拟真实仓库里放了插件的情况。

数据流:输入是项目根目录、插件名和技能名 → 它创建 .git、插件市场文件、插件描述文件,以及插件自己的 skills/技能名/SKILL.md → 输出是成功或失败;成功后,这个项目看起来就像安装了一个带技能的本地插件。

调用关系:插件被工作区设置禁用的测试会调用它。测试随后请求技能列表,检查这个插件技能是否因为服务端设置而被排除,同时普通用户技能仍然保留。

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

write_cached_remote_plugin_with_skill137–155 ↗
fn write_cached_remote_plugin_with_skill(
    codex_home: &std::path::Path,
) -> Result<std::path::PathBuf>

作用:在用户目录里伪造一个已经缓存好的远程插件技能。它用于测试“远程插件安装后,服务器能不能从本地缓存读出技能”。

数据流:输入是用户目录路径 → 它创建 plugins/cache 下的远程插件目录、插件描述文件和 triage-issues 的 SKILL.md → 输出是这个技能文件的路径;成功后,磁盘上有一份远程插件缓存。

调用关系:远程已安装插件技能测试先调用它准备缓存。后续服务器刷新插件安装信息后,技能列表应把这份缓存里的技能作为已启用插件技能返回。

调用图:被 1 处调用(skills_list_loads_remote_installed_plugin_skills_from_cache);外部调用 3 个(join, create_dir_all, write)。

skills_list_loads_remote_installed_plugin_skills_from_cache158–337 ↗
async fn skills_list_loads_remote_installed_plugin_skills_from_cache() -> Result<()>

作用:测试远程插件被确认已安装后,技能列表能从本地缓存里显示该插件的技能。它还确认在安装状态刷新前,这个技能不会过早出现。

数据流:它先创建临时用户目录和工作目录,写入远程插件缓存和配置,再用 mock 服务器假装后端返回插件目录和安装列表 → 它启动测试服务器,先请求技能列表确认缓存里的远程插件技能暂时不出现;然后请求插件列表触发安装状态刷新;最后反复请求技能列表,直到看到 linear:triage-issues → 结果是确认返回的技能路径、启用状态和错误列表都符合预期。

调用关系:这是一个端到端式测试:它使用 write_cached_remote_plugin_with_skill 和 write_remote_plugins_enabled_config_with_base_url 布置本地文件,用 wiremock 假装远程接口,再通过 TestAppServer 发送插件列表和技能列表请求,验证整个远程插件刷新链路。

调用图:调用 4 个内部函数(new, new, write_cached_remote_plugin_with_skill, write_remote_plugins_enabled_config_with_base_url);外部调用 19 个(from_millis, given, start, new, new, Integer, to_response, write_chatgpt_auth, assert!, assert_eq! (+9 more))。

skills_list_excludes_plugin_skills_when_workspace_codex_plugins_disabled340–402 ↗
async fn skills_list_excludes_plugin_skills_when_workspace_codex_plugins_disabled() -> Result<()>

作用:测试当账号或工作区设置关闭 Codex 插件时,插件里的技能不会出现在列表里。它同时保证普通技能不受影响。

数据流:它创建用户目录、项目目录和 mock 后端,写入一个普通 home-skill 和一个插件技能 plugin-skill,再让后端账号设置返回 enable_plugins=false → 启动服务器后请求技能列表 → 输出检查结果:home-skill 仍然存在,而 demo-plugin:plugin-skill 不存在。

调用关系:这个测试把 write_skill、write_plugin_with_skill 和 write_plugins_enabled_config_with_base_url 串起来造场景,再通过 mock 后端提供禁用插件的设置,最后用技能列表接口验证服务器的过滤规则。

调用图:调用 5 个内部函数(new, new_without_managed_config, write_plugin_with_skill, write_plugins_enabled_config_with_base_url, write_skill);外部调用 15 个(given, start, new, new, Integer, to_response, write_chatgpt_auth, assert!, assert_eq!, format! (+5 more))。

skills_list_skips_cwd_roots_when_environment_disabled405–452 ↗
async fn skills_list_skips_cwd_roots_when_environment_disabled() -> Result<()>

作用:测试当执行环境被环境变量关闭时,服务器不要扫描当前工作目录里的技能。这样可以避免在不该访问项目环境时误读项目文件。

数据流:它创建用户目录技能 home-skill,又在工作目录的 .codex/skills 里放 repo-skill,然后用环境变量把执行服务器地址设成 none → 启动服务器并请求该工作目录的技能列表 → 返回结果应包含 home-skill,但不包含 repo-skill,并且没有错误。

调用关系:这个测试用 write_skill 准备用户级技能,然后直接写入项目级技能。TestAppServer 按指定环境变量启动后,技能列表请求验证“环境关闭”这个开关是否影响工作目录扫描。

调用图:调用 2 个内部函数(new_with_env, write_skill);外部调用 9 个(new, Integer, to_response, assert!, assert_eq!, create_dir_all, write, timeout, vec!)。

skills_list_accepts_relative_cwds455–480 ↗
async fn skills_list_accepts_relative_cwds() -> Result<()>

作用:测试技能列表接口能接受相对路径形式的当前工作目录。也就是说,客户端不一定非得传绝对路径。

数据流:它在临时用户目录下创建 relative-cwd 文件夹,然后启动服务器并把 relative-cwd 这个相对路径作为 cwds 参数发出去 → 服务器返回技能列表 → 测试确认返回里的 cwd 仍是请求传入的相对路径,并且没有错误。

调用关系:这是路径输入兼容性的测试。它通过 TestAppServer 直接发技能列表请求,重点检查服务器没有强行拒绝或改坏相对路径。

调用图:调用 1 个内部函数(new);外部调用 8 个(new, Integer, to_response, assert_eq!, create_dir_all, from, timeout, vec!)。

skills_list_preserves_requested_cwd_order483–517 ↗
async fn skills_list_preserves_requested_cwd_order() -> Result<()>

作用:测试一次请求多个工作目录时,返回结果的顺序和请求里的顺序一致。客户端通常会按顺序对应结果,顺序乱了就容易显示错。

数据流:它创建两个临时工作目录,按 first、second 的顺序放进请求 → 服务器返回每个目录对应的技能列表条目 → 测试取出返回的 cwd 顺序,确认仍然是 first、second。

调用关系:这个测试只关注接口契约中的顺序问题。TestAppServer 发出一个包含两个目录的技能列表请求,服务器处理后,断言检查返回数组没有被排序或打乱。

调用图:调用 1 个内部函数(new);外部调用 6 个(new, Integer, to_response, assert_eq!, timeout, vec!)。

skills_list_uses_cached_result_until_force_reload520–595 ↗
async fn skills_list_uses_cached_result_until_force_reload() -> Result<()>

作用:测试技能列表的缓存行为:普通请求会继续用旧结果,只有 force_reload 为 true 时才重新扫描磁盘。这样能防止频繁读文件,但也要保证强制刷新有效。

数据流:它先在没有本地技能时请求一次,把空结果放进缓存 → 然后在工作目录里新增 late-extra-skill → 第二次不强制刷新,请求结果仍看不到新技能;第三次带 force_reload=true,请求结果就能看到新技能 → 输出是对三次返回内容的断言。

调用关系:这是缓存规则测试。它通过连续三次调用技能列表接口,配合中途写入文件,验证服务器什么时候复用旧结果、什么时候重新扫描。

调用图:调用 1 个内部函数(new);外部调用 9 个(new, Integer, to_response, assert!, assert_eq!, create_dir_all, write, timeout, vec!)。

skills_extra_roots_set_updates_process_runtime_roots598–739 ↗
async fn skills_extra_roots_set_updates_process_runtime_roots() -> Result<()>

作用:测试运行过程中设置“额外技能根目录”会立刻影响技能列表,并且这种设置只在当前服务器进程里生效。这里的额外根目录就是临时告诉服务器:再去这个目录找技能。

数据流:它创建一个额外 skills 目录并放入 runtime-skill → 启动服务器后发送 extra_roots 设置请求,等待 skills/changed 通知,再请求技能列表确认 runtime-skill 出现 → 然后把额外根目录改成不存在的目录,确认技能消失;再清空额外根目录,确认仍然消失;最后重启服务器,确认之前的运行时设置没有被保存下来 → 输出是一连串响应和通知都符合预期。

调用关系:这个测试围绕 SkillsExtraRootsSet 接口展开。它每次修改运行时根目录后,都调用 expect_skills_changed_notification 检查通知,再调用技能列表接口检查实际可见技能,最后用重启证明设置不会持久化。

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

skills_changed_notification_is_emitted_after_skill_change742–849 ↗
async fn skills_changed_notification_is_emitted_after_skill_change() -> Result<()>

作用:测试技能文件内容变动后,服务器会发出“技能已变化”通知,并且后续技能列表能读到新描述。它验证文件监听机制真的在工作。

数据流:它启动一个假的响应服务器,写好配置和初始技能 demo,然后启动应用服务器并请求技能列表,确认描述是 demo description → 接着启动一个线程会话,让服务器进入会监听技能变化的状态 → 它改写 SKILL.md,把描述改成 updated,然后等待 skills/changed 通知 → 最后再次请求技能列表,确认描述已经变成 updated。

调用关系:这个测试使用 write_skill 和配置辅助函数准备环境,用 create_mock_responses_server_repeating_assistant 提供假的模型响应服务,再通过线程启动和技能列表请求触发完整运行流程,最后验证文件变化通知和重新读取结果。

调用图:调用 2 个内部函数(new_with_env, write_skill);外部调用 11 个(new, Integer, create_mock_responses_server_repeating_assistant, to_response, write_mock_responses_config_toml_with_chatgpt_base_url, assert!, assert_eq!, from_value, write, timeout (+1 more))。

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

这份测试像是在模拟一次完整对话:先开一个假的模型服务器,让它假装会返回“Done”;再临时造一个 Codex 配置目录,里面放一个本地技能;同时再造一个插件目录,插件里也有一个同名技能,但内容不同。接着测试启动真实的 app server,发起一个线程,并明确告诉它选中的能力根目录是这个插件。然后用户输入“Use $demo-plugin:deploy”。测试最后检查发给模型的请求:开发者消息里应该提到这个技能名;用户消息里应该只出现插件技能的正文标记,而不能出现本地技能的标记。简单说,它验证“指定插件优先、同名本地技能不能抢戏”这个规则。

函数细节1
selected_executor_root_exposes_plugin_skill24–148 ↗
async fn selected_executor_root_exposes_plugin_skill() -> Result<()>

作用:这是一条端到端测试,也就是尽量像真实使用一样,从启动服务器、创建线程、发送用户输入,到检查模型收到的内容。它要证明选中的插件能力根目录会暴露插件技能,并且同名本地技能不会被错误混入。

数据流:进去的是测试临时创建的一套环境:一个假的模型服务器、一个临时 Codex home、本地技能文件、插件技能文件,以及一段用户输入。函数先把这些文件和配置写到磁盘,再启动 TestAppServer,发送 thread/start 创建会话,随后发送 turn/start 让用户请求使用技能。最后它从假的模型服务器取出实际收到的请求,检查里面包含插件技能名和插件技能正文标记,同时不包含本地技能正文标记。出来的结果是测试通过或失败;如果任何一步拿错技能、超时、响应格式不对,测试就会报错。

调用关系:这个函数由 Tokio 测试框架在跑测试时自动调用。它先用 responses::start_mock_server、responses::sse 和 responses::mount_sse_once 搭好一个假的模型响应源,再用 TestAppServer::new 启动被测服务器;过程中用 Default::default 补齐请求参数,用 RequestId::Integer 等待指定请求的回应,并用 to_response 把通用 JSON-RPC 响应转成 thread/start 的具体结果。它把整个流程串起来,最后通过断言检查服务器真正发给模型的内容是否符合预期。

调用图:调用 4 个内部函数(new, mount_sse_once, sse, start_mock_server);外部调用 11 个(default, new, Integer, to_response, assert!, assert_eq!, format!, create_dir_all, write, timeout (+1 more))。

MCP 服务器、工具和资源

这些套件端到端测试 MCP 集成,从服务器状态和按执行器范围公开,到直接工具调用、引导请求转发和资源访问。

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

可以把这里的测试想成一次模拟演练:先搭一个临时的应用服务器,再造一个带插件的“执行器环境”。MCP 可以理解成一套让模型调用外部工具的接口;这里的 MCP 工具通过标准输入输出,也就是 stdio,像命令行程序一样和服务器说话。测试先创建一个插件,里面声明了一个 echo 工具,并让它只能从某个执行器环境拿到特定环境变量。然后启动一个选中了这个插件的线程,检查模型确实能看到并调用这个工具,而且工具输出里带着正确环境变量。接着测试修改全局 MCP 配置并重新加载,确认新加的全局工具也能用。最后再开一个没选插件的新线程,确认它看不到前面那个插件工具。这个文件重要的地方在于:它防止“某个线程专属的工具泄漏到别的线程”,也防止配置刷新时把工具边界搞坏。

函数细节3
selected_executor_plugin_exposes_its_stdio_mcp_only_to_that_thread34–219 ↗
async fn selected_executor_plugin_exposes_its_stdio_mcp_only_to_that_thread() -> Result<()>

作用:这是主测试。它验证一个被选中的执行器插件会把自己的 stdio MCP 工具暴露给对应线程,但不会暴露给没有选它的线程;同时也验证重新加载 MCP 配置后,全局新工具仍然可用。

数据流:进去的是一套临时测试资源:临时配置目录、临时插件目录、假的模型响应服务器,以及测试用的 app server。函数先写配置文件和插件文件,再启动服务器和线程;然后让假的模型请求调用插件里的 echo 工具,读取返回结果并检查里面有没有预期文字和环境变量;之后改配置并触发重新加载,再直接调用新加的 MCP 工具;最后分别查询选中线程和未选中线程能看到的 MCP 服务器名字。出来的结果不是业务数据,而是一组断言:如果工具可见性、调用结果或配置刷新不符合预期,测试就失败。

调用关系:它是这个文件的核心入口,由测试框架自动运行。它自己负责搭测试场景,并把开线程的细活交给 start_thread,把查询某个线程能看到哪些 MCP 服务器的细活交给 mcp_server_names。它还会使用测试支撑代码创建 TestAppServer、启动假的响应服务器、挂载模拟 SSE 响应;SSE 是一种服务器持续推送消息的格式,这里用来假装模型一步步返回“我要调用工具”和“完成了”。

调用图:调用 4 个内部函数(new, start_thread, mount_sse_sequence, start_mock_server);外部调用 16 个(new, default, new, Integer, to_response, write_mock_responses_config_toml, assert!, assert_eq!, format!, json! (+6 more))。

mcp_server_names221–244 ↗
async fn mcp_server_names(
    app_server: &mut TestAppServer,
    thread_id: String,
) -> Result<Vec<String>>

作用:这个小帮手用来问 app server:某个线程当前能看到哪些 MCP 服务器。它把完整的状态响应简化成一串服务器名字,方便测试做判断。

数据流:进去的是一个正在运行的 TestAppServer 和一个 thread_id,也就是线程编号。函数向服务器发送“列出 MCP 服务器状态”的请求,并指定只看这个线程;然后等待对应请求编号的响应,把响应转换成 ListMcpServerStatusResponse;最后从每个服务器状态里取出 name 字段,返回 Vec<String>,也就是名字列表。它不会改配置,只是读取当前服务器看到的状态。

调用关系:它服务于主测试 selected_executor_plugin_exposes_its_stdio_mcp_only_to_that_thread。主测试在需要确认“选中线程看得到插件 MCP、未选中线程看不到插件 MCP”时调用它。它把底层的请求发送、等待响应、JSON 转类型这些步骤包起来,让主测试只关心名字列表。

调用图:调用 2 个内部函数(read_stream_until_response_message, send_list_mcp_server_status_request);外部调用 3 个(Integer, to_response, timeout)。

start_thread246–264 ↗
async fn start_thread(
    app_server: &mut TestAppServer,
    selected_capability_roots: Option<Vec<SelectedCapabilityRoot>>,
) -> Result<String>

作用:这个小帮手负责在测试服务器里新建一个对话线程。调用者可以选择传入能力根,也就是告诉服务器这个线程选中了哪些插件或环境资源。

数据流:进去的是一个 TestAppServer,以及可选的 selected_capability_roots。函数构造 ThreadStartParams,把模型名设成 mock-model,并带上这些被选中的能力根;然后发送“启动线程”请求,等待同一个请求编号的响应;接着把响应转换成 ThreadStartResponse,取出新线程的 id 返回。它的效果是在测试服务器内部多出一个新线程。

调用关系:它被主测试 selected_executor_plugin_exposes_its_stdio_mcp_only_to_that_thread 调用两次:第一次创建选中了执行器插件的线程,第二次创建没有选插件的普通线程。这样主测试就能对比两个线程看到的 MCP 工具是否被正确隔离。它自己不判断业务规则,只负责把“开线程并拿到编号”这件事做稳。

调用图:调用 2 个内部函数(read_stream_until_response_message, send_thread_start_request);被 1 处调用(selected_executor_plugin_exposes_its_stdio_mcp_only_to_that_thread);外部调用 4 个(default, Integer, to_response, timeout)。

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

这个测试文件像是在给“查看外接工具服务器状态”这个功能做体检。它会临时搭一个假的应用服务器,再临时搭一个或多个假的 MCP 服务器,让应用去连接它们,然后检查返回结果是不是符合预期。这里重点验证几件事:工具名和服务器名要按原样显示,不能被偷偷改掉;某个线程如果属于一个项目,就要能读到这个项目自己的 .codex 配置;当只想看“工具和认证信息”时,不应该去调用很慢的资源清单接口,避免用户等太久;即使两个服务器名字经过“清洗”后可能撞名,也要分别保留各自工具。文件里还有两个小型假 MCP 服务器:一个正常返回工具列表,一个故意把资源列表返回得很慢,用来检查主程序有没有做不必要的慢操作。

函数细节12
mcp_server_status_list_returns_raw_server_and_tool_names46–115 ↗
async fn mcp_server_status_list_returns_raw_server_and_tool_names() -> Result<()>

作用:这个测试确认应用列出的 MCP 服务器名和工具名是原始名字,没有被改写或美化。这样用户在配置里写什么、远端服务器声明什么,界面或接口里看到的就是什么。

数据流:进去的是一个临时配置目录、一个假的模型响应服务器、一个假的 MCP 服务器地址,以及工具名“look-up.raw”。测试把 MCP 服务器写进 config.toml,启动 TestAppServer,发送“列出 MCP 服务器状态”的请求,再把返回消息转成结构化结果。出来的是一组断言:只有一个服务器,名字是“some-server”,工具键和工具对象里的名字都是“look-up.raw”,服务器标题是“Lookup Server”;最后关闭临时 MCP 服务。

调用关系:它是整个状态查询功能的基础验收测试。它会调用 start_mcp_server 搭建一个会返回工具信息的假 MCP 服务,再通过 TestAppServer 的请求接口走完整链路,最后用 to_response 把应用服务器的响应变成可检查的数据。

调用图:调用 2 个内部函数(new, start_mcp_server);外部调用 12 个(new, new, new, Integer, create_mock_responses_server_sequence_unchecked, to_response, write_mock_responses_config_toml, assert_eq!, format!, read_to_string (+2 more))。

mcp_server_status_list_uses_thread_project_local_config118–207 ↗
async fn mcp_server_status_list_uses_thread_project_local_config() -> Result<()>

作用:这个测试确认:如果请求指定了某个线程,应用会使用这个线程所在项目的本地配置。也就是说,同一个应用里,不同项目可以有自己的 MCP 服务器设置。

数据流:进去的是临时的全局配置目录、临时工作区、项目信任状态、以及项目 .codex/config.toml 里的 MCP 服务器地址。测试先启动应用,再创建一个 cwd 指向该工作区的线程;随后分别发两次状态查询,一次不带 thread_id,一次带 thread_id。结果是:不带线程时看不到项目本地 MCP;带线程时能看到 project-server 和它的 project_lookup 工具。

调用关系:它先调用 start_mcp_server 准备项目专用的假 MCP 服务,又调用 set_project_trust_level 把工作区标记为可信项目。这个测试模拟真实用户在某个项目里开线程后再查询 MCP 状态的流程,重点验证状态查询会跟随线程上下文读配置。

调用图:调用 3 个内部函数(new, start_mcp_server, set_project_trust_level);外部调用 13 个(new, default, new, new, Integer, create_mock_responses_server_sequence_unchecked, to_response, write_mock_responses_config_toml, assert_eq!, format! (+3 more))。

McpStatusServer::get_info215–219 ↗
fn get_info(&self) -> ServerInfo

作用:这个函数让假的 MCP 服务器告诉客户端:我支持工具功能,并且我的服务器信息叫 lookup-server,标题是 Lookup Server。它用于测试应用能不能把远端服务器的基本信息读回来。

数据流:进去的是这个假服务器自身,不需要额外参数。它组装一个 ServerInfo,里面写明服务器能力包含 tools,也就是“可列出工具”,并附带实现名、版本和标题。出来的是这份服务器介绍信息,不改动外部状态。

调用关系:当应用服务器连接 start_mcp_server 创建的假 MCP 服务并询问服务器信息时,rmcp 框架会调用它。它和 McpStatusServer::list_tools 配合,一个负责说“我是谁、我会什么”,一个负责说“我有哪些工具”。

调用图:外部调用 3 个(new, builder, new)。

McpStatusServer::list_tools221–244 ↗
async fn list_tools(
        &self,
        _request: Option<rmcp::model::PaginatedRequestParams>,
        _context: rmcp::service::RequestContext<rmcp::service::RoleServer>,
    ) -> Result<ListTools

作用:这个函数让假的 MCP 服务器返回一个工具列表。测试用它来检查应用是否能正确读取工具名、工具说明和只读标记。

数据流:进去的是可选分页参数、请求上下文,以及服务器里保存的 tool_name。函数先构造一个很简单的输入格式说明:这是一个对象,不允许额外字段;再用 tool_name 创建一个工具,说明文字是“Look up test data.”,并标记为只读。出来的是包含一个工具、没有下一页的 ListToolsResult。

调用关系:当测试里的应用服务器向假 MCP 服务请求工具清单时,rmcp 框架会调用它。它服务于多个测试中的 start_mcp_server,帮助这些测试拿到稳定、可预测的工具结果。

调用图:外部调用 8 个(new, Borrowed, Owned, new, json!, new, from_value, vec!)。

SlowInventoryServer::get_info253–260 ↗
fn get_info(&self) -> ServerInfo

作用:这个函数让另一个假的 MCP 服务器声明:我既支持工具,也支持资源。它的作用是制造一种“看起来有更多清单可查”的服务器,方便测试应用是否会避开慢接口。

数据流:进去的是 SlowInventoryServer 自身,不需要别的输入。它创建一份 ServerInfo,能力里打开 tools 和 resources,也就是支持工具列表和资源列表。出来的是这份能力声明。

调用关系:它属于 start_slow_inventory_mcp_server 创建的慢服务器。后续应用如果看到它支持资源,理论上可能会去查资源;相关测试正是用它来确认在 ToolsAndAuthOnly 模式下不会去碰那些慢资源接口。

调用图:外部调用 2 个(builder, new)。

SlowInventoryServer::list_tools262–285 ↗
async fn list_tools(
        &self,
        _request: Option<PaginatedRequestParams>,
        _context: RequestContext<rmcp::service::RoleServer>,
    ) -> Result<ListToolsResult, rmcp::ErrorData>

作用:这个函数给慢服务器返回工具列表,但它本身不慢。这样测试可以验证:即使服务器的资源接口很慢,应用仍然可以快速拿到工具信息。

数据流:进去的是可选分页参数、请求上下文,以及服务器保存的 tool_name。函数构造简单输入格式,创建一个只读工具,并把它放进结果列表。出来的是包含一个工具、没有下一页的 ListToolsResult。

调用关系:它和 SlowInventoryServer::list_resources、SlowInventoryServer::list_resource_templates 形成对比:工具列表可以马上返回,资源相关列表会故意等待。mcp_server_status_list_tools_and_auth_only_skips_slow_inventory_calls 依靠这种差异判断应用有没有误调用慢接口。

调用图:外部调用 8 个(new, Borrowed, Owned, new, json!, new, from_value, vec!)。

SlowInventoryServer::list_resources287–298 ↗
async fn list_resources(
        &self,
        _request: Option<PaginatedRequestParams>,
        _context: RequestContext<rmcp::service::RoleServer>,
    ) -> Result<ListResourcesResult, rmcp::ErrorD

作用:这个函数故意把“列出资源”做得很慢,用来当测试陷阱。只要应用在不该查资源时查了它,测试就会因为超时或变慢而暴露问题。

数据流:进去的是可选分页参数和请求上下文,函数并不使用它们。它先等待 2 秒,然后返回一个空的资源列表、没有下一页。出来的是 ListResourcesResult,同时没有修改其他状态。

调用关系:它只在慢 MCP 服务器被询问资源清单时才会被调用。mcp_server_status_list_tools_and_auth_only_skips_slow_inventory_calls 设置了 500 毫秒超时,如果应用误走到这个函数,测试很可能赶不上时间,从而证明实现有问题。

调用图:外部调用 3 个(from_secs, new, sleep)。

SlowInventoryServer::list_resource_templates300–311 ↗
async fn list_resource_templates(
        &self,
        _request: Option<PaginatedRequestParams>,
        _context: RequestContext<rmcp::service::RoleServer>,
    ) -> Result<ListResourceTemplatesRes

作用:这个函数故意把“列出资源模板”做得很慢。资源模板可以理解成“资源地址的模板清单”,这里用慢返回来检查应用是否避免无关查询。

数据流:进去的是可选分页参数和请求上下文,函数不使用这些内容。它等待 2 秒后,返回空的资源模板列表、没有下一页。出来的是 ListResourceTemplatesResult,不改动外部状态。

调用关系:它和 SlowInventoryServer::list_resources 一起扮演慢接口。相关测试请求的是 ToolsAndAuthOnly,也就是只要工具和认证信息,所以正常流程不应该把活儿交给这个函数。

调用图:外部调用 3 个(from_secs, new, sleep)。

mcp_server_status_list_tools_and_auth_only_skips_slow_inventory_calls315–372 ↗
async fn mcp_server_status_list_tools_and_auth_only_skips_slow_inventory_calls() -> Result<()>

作用:这个测试确认当请求只需要“工具和认证信息”时,应用不会去查询慢吞吞的资源和资源模板。这样能保证状态列表在精简模式下足够快。

数据流:进去的是一个慢库存 MCP 服务器、临时配置目录,以及 detail 设置为 ToolsAndAuthOnly 的状态查询请求。测试把慢服务器写入配置,启动应用,然后用 500 毫秒的短超时等待响应。出来的结果应该包含服务器 some-server 和工具 lookup,但 resources 和 resource_templates 都是空列表;如果应用去调用慢资源接口,就会等 2 秒,测试会失败。

调用关系:它调用 start_slow_inventory_mcp_server 搭出带慢接口的假服务。这个测试专门验证状态查询流程里的分支选择:ToolsAndAuthOnly 模式只该触发 SlowInventoryServer::list_tools,不该触发 SlowInventoryServer::list_resources 或 SlowInventoryServer::list_resource_templates。

调用图:调用 2 个内部函数(new, start_slow_inventory_mcp_server);外部调用 13 个(new, from_millis, new, new, Integer, create_mock_responses_server_sequence_unchecked, to_response, write_mock_responses_config_toml, assert_eq!, format! (+3 more))。

mcp_server_status_list_keeps_tools_for_sanitized_name_collisions375–451 ↗
async fn mcp_server_status_list_keeps_tools_for_sanitized_name_collisions() -> Result<()>

作用:这个测试确认两个 MCP 服务器名字即使很相似,也不会因为内部名字清洗而丢掉其中一个工具。比如 some-server 和 some_server 都应该各算各的。

数据流:进去的是两个假 MCP 服务器地址,一个配置名是 some-server,另一个是 some_server,各自返回不同工具名。测试把两段配置写入同一个 config.toml,启动应用,发送状态查询请求。出来的结果应该有两个状态项:some-server 对应 dash_lookup,some_server 对应 underscore_lookup,二者都保留。

调用关系:它两次调用 start_mcp_server 创建两个独立假服务。这个测试覆盖的是容易被忽略的边界情况:如果应用内部把横杠、下划线等字符处理成相近形式,也不能把两个真实配置项混在一起。

调用图:调用 2 个内部函数(new, start_mcp_server);外部调用 12 个(new, new, new, Integer, create_mock_responses_server_sequence_unchecked, to_response, write_mock_responses_config_toml, assert_eq!, format!, read_to_string (+2 more))。

start_mcp_server453–473 ↗
async fn start_mcp_server(tool_name: &str) -> Result<(String, JoinHandle<()>)>

作用:这个辅助函数启动一个临时的、正常响应的假 MCP HTTP 服务器。测试用它来模拟真实外接工具服务器,而不需要依赖网络上的真实服务。

数据流:进去的是一个工具名字符串。函数先在本机 127.0.0.1 上绑定一个随机空闲端口,把工具名放进可共享的 Arc(线程安全引用计数指针,方便多个异步任务共用同一份数据),创建 McpStatusServer 服务,再用 axum 启动 HTTP 路由 /mcp。出来的是服务器 URL 和一个 JoinHandle(异步任务句柄,用来之后停止或等待这个服务器任务)。

调用关系:多个测试会先调用它搭建假 MCP 服务,然后把返回的 URL 写进配置文件。它把真正响应请求的工作交给 McpStatusServer::get_info 和 McpStatusServer::list_tools;测试结束时会 abort 这个句柄,清理临时服务器。

调用图:被 3 处调用(mcp_server_status_list_keeps_tools_for_sanitized_name_collisions, mcp_server_status_list_returns_raw_server_and_tool_names, mcp_server_status_list_uses_thread_project_local_config);外部调用 9 个(new, default, new, default, new, bind, serve, format!, spawn)。

start_slow_inventory_mcp_server475–495 ↗
async fn start_slow_inventory_mcp_server(tool_name: &str) -> Result<(String, JoinHandle<()>)>

作用:这个辅助函数启动一个带慢资源接口的假 MCP HTTP 服务器。它专门用于测试应用有没有在精简查询时误碰耗时接口。

数据流:进去的是一个工具名字符串。函数绑定本机随机端口,创建 SlowInventoryServer,并把它挂到 /mcp 路由上异步运行。出来的是可访问的 HTTP 地址和 JoinHandle,测试可以用地址写配置,也可以用句柄在结束时关闭服务。

调用关系:它只被 mcp_server_status_list_tools_and_auth_only_skips_slow_inventory_calls 使用。它把请求处理交给 SlowInventoryServer 的几个方法:工具列表马上返回,资源和资源模板会故意睡 2 秒,从而帮助测试判断应用查询路径是否正确。

调用图:被 1 处调用(mcp_server_status_list_tools_and_auth_only_skips_slow_inventory_calls);外部调用 9 个(new, default, new, default, new, bind, serve, format!, spawn)。

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

这个测试文件搭了一个假的 MCP 工具服务器,再启动一个测试版 app-server,模拟真实用户和模型调用工具的过程。它验证几件很关键的事:正常工具调用能返回文本、结构化数据和附加信息;线程不存在时会报清楚的错;工具需要用户确认或打开登录网址时,app-server 会把请求转交给客户端,再把客户端回答送回工具;工具返回特别大的内容时,通知里会被截断,避免消息太大把系统撑爆。文件里的 ToolAppsMcpServer 就像一个小道具服务器,只提供一个 echo_tool,根据输入返回不同结果,用来逼出各种边界情况。

函数细节10
mcp_server_tool_call_returns_tool_result73–159 ↗
async fn mcp_server_tool_call_returns_tool_result() -> Result<()>

作用:这个测试确认:当 app-server 被要求调用一个 MCP 工具时,它能真的连到 MCP 服务器,拿到工具结果,并把结果原样包装成协议响应返回。

数据流:测试先启动假的模型响应服务器和假的 MCP 工具服务器,再写入临时配置,让 app-server 知道 MCP 服务器地址。然后它创建一个线程,发送一次工具调用,输入 message 为“hello from app”。最后读取返回值,检查文本内容、结构化内容、是否出错、附加 meta 信息都符合预期。

调用关系:它是正向主流程测试,会调用 start_mcp_server 搭建临时 MCP 服务,也用 TestAppServer 向 app-server 发请求。工具端实际响应由 ToolAppsMcpServer::call_tool 生成。

调用图:调用 3 个内部函数(new, start_mcp_server, start_mock_server);外部调用 12 个(new, default, new, Integer, to_response, write_mock_responses_config_toml, assert_eq!, format!, json!, read_to_string (+2 more))。

mcp_server_tool_call_returns_error_for_unknown_thread162–188 ↗
async fn mcp_server_tool_call_returns_error_for_unknown_thread() -> Result<()>

作用:这个测试确认:如果客户端拿一个不存在的线程 ID 去调用 MCP 工具,app-server 不会糊里糊涂继续做,而是返回“线程不存在”的错误。

数据流:测试启动一个没有任何已创建线程的 TestAppServer,然后发送工具调用请求,里面放一个假的 thread_id。app-server 查不到这个线程后返回 JSON-RPC 错误,测试再检查错误文字里确实包含 thread not found。

调用关系:它不需要真的 MCP 工具服务器,因为错误应该在 app-server 找线程这一步就发生。它补上了异常路径,防止系统把无效请求传给后面的工具调用流程。

调用图:调用 1 个内部函数(new);外部调用 5 个(new, Integer, assert!, json!, timeout)。

mcp_server_tool_call_round_trips_elicitation191–298 ↗
async fn mcp_server_tool_call_round_trips_elicitation() -> Result<()>

作用:这个测试确认:MCP 工具如果临时需要用户填写或确认信息,app-server 能把这个“询问用户”的请求转发给客户端,并把客户端的回答再送回 MCP 工具。

数据流:测试配置并启动 app-server 和假 MCP 服务器,创建一个线程,然后调用工具并传入特殊 message:confirm。工具端看到这个值后发起表单式确认请求。测试读取 app-server 转发出来的请求,确认里面的问题、线程、服务器名和表单结构都对;接着模拟客户端回答“接受,并 confirmed=true”。最后工具调用完成,返回文本 accepted。

调用关系:它依赖 start_mcp_server 启动工具服务,工具里的 ToolAppsMcpServer::call_tool 负责触发 elicitation。这里的 elicitation 指“工具向用户追问信息”,像收银员付款前问你要不要确认一样。

调用图:调用 3 个内部函数(new, start_mcp_server, start_mock_server);外部调用 18 个(new, new, default, builder, Boolean, new, Integer, to_response, write_mock_responses_config_toml, assert_eq! (+8 more))。

mcp_server_tool_call_forwards_url_elicitation301–401 ↗
async fn mcp_server_tool_call_forwards_url_elicitation() -> Result<()>

作用:这个测试确认:MCP 工具如果要求用户打开一个网址完成操作,比如登录授权,app-server 会把这个网址请求正确转给客户端。

数据流:测试启动临时 app-server 和 MCP 服务器,创建线程后调用工具,传入特殊 message:auth。工具端据此发出 URL 类型的用户请求,里面有提示语、登录网址和 elicitation_id。测试检查 app-server 收到并转发的内容完全正确,然后模拟客户端接受,最终工具调用返回 accepted。

调用关系:它和表单确认测试类似,但覆盖的是 URL 询问场景。ToolAppsMcpServer::call_tool 触发网址请求,app-server 在中间充当传话筒,把工具的登录要求交给客户端。

调用图:调用 3 个内部函数(new, start_mcp_server, start_mock_server);外部调用 14 个(new, default, new, Integer, to_response, write_mock_responses_config_toml, assert_eq!, format!, json!, panic! (+4 more))。

mcp_tool_call_completion_notification_contains_truncated_large_result404–534 ↗
async fn mcp_tool_call_completion_notification_contains_truncated_large_result() -> Result<()>

作用:这个测试确认:当 MCP 工具返回非常大的结果时,app-server 发给客户端的完成通知会截断内容,避免一条通知过大。

数据流:测试先准备假的模型响应,让模型发起一次 MCP 工具调用,参数 message 为 large。假 MCP 工具收到后返回巨大的文本和结构化内容。测试启动一轮对话,等待指定工具调用完成通知,然后检查通知里的结果已经被压成文本提示,包含 truncated 字样,并且序列化后的整体大小低于限制范围。

调用关系:它会调用 start_mcp_server 启动工具服务器,并调用 wait_for_mcp_tool_call_completed 等待目标工具调用完成。这个测试覆盖的是模型驱动的工具调用流程,而不是客户端直接发工具调用请求。

调用图:调用 3 个内部函数(new, start_mcp_server, wait_for_mcp_tool_call_completed);外部调用 17 个(new, default, new, Integer, create_mock_responses_server_sequence, to_response, write_mock_responses_config_toml, assert!, assert_eq!, format! (+7 more))。

ToolAppsMcpServer::get_info540–542 ↗
fn get_info(&self) -> ServerInfo

作用:这个函数告诉 MCP 框架:这个假服务器支持工具功能。没有这个声明,客户端可能不知道可以向它列工具或调用工具。

数据流:它不读取外部输入,只创建一个 ServerInfo,里面标明服务器能力启用了 tools。输出就是这份能力说明。

调用关系:它是 ToolAppsMcpServer 按 MCP 服务接口提供的基础信息。start_mcp_server 启动服务后,MCP 框架在握手或初始化时会用到它。

调用图:外部调用 2 个(builder, new)。

ToolAppsMcpServer::list_tools544–572 ↗
async fn list_tools(
        &self,
        _request: Option<rmcp::model::PaginatedRequestParams>,
        _context: RequestContext<RoleServer>,
    ) -> Result<ListToolsResult, rmcp::ErrorData>

作用:这个函数列出假 MCP 服务器提供的工具。这里它只公布一个工具:echo_tool,并说明这个工具接收一个字符串 message。

数据流:它收到列工具请求和请求上下文,但这里基本不用。函数先构造输入格式说明,也就是 message 必须是字符串;再创建工具定义,写上名字、描述和只读标记;最后返回工具列表。

调用关系:它是 MCP 客户端发现工具时会走的入口。测试里的 app-server 连接 MCP 服务器后,会通过这类信息知道 TEST_TOOL_NAME 这个工具存在,以及该怎么传参数。

调用图:外部调用 7 个(new, Borrowed, new, json!, new, from_value, vec!)。

ToolAppsMcpServer::call_tool574–665 ↗
async fn call_tool(
        &self,
        request: CallToolRequestParams,
        context: RequestContext<RoleServer>,
    ) -> Result<CallToolResult, rmcp::ErrorData>

作用:这个函数是假 MCP 工具真正干活的地方。它根据传进来的 message 返回普通回声、超大结果、表单确认流程,或者网址确认流程。

数据流:它读取工具名、arguments 里的 message,以及上下文 meta 里的 threadId。普通情况会返回“echo: 消息”、结构化 echoed/threadId 和 meta。message 是 large 时返回很大的内容;是 confirm 时向客户端发起表单确认并根据回答返回 accepted/declined/cancelled;是 auth 时发起打开网址的确认流程,再返回对应结果。

调用关系:各个测试通过 app-server 间接触发它。它把不同输入变成不同场景,帮助测试 app-server 的工具调用、用户追问转发和大结果截断这些能力。

调用图:外部调用 9 个(new, builder, new, Boolean, assert_eq!, json!, structured, success, vec!)。

start_mcp_server668–683 ↗
async fn start_mcp_server() -> Result<(String, JoinHandle<()>)>

作用:这个辅助函数在本机临时启动一个假的 MCP HTTP 服务器,供测试连接。它让每个测试都能拥有一个干净、可控的外部工具服务。

数据流:它先绑定 127.0.0.1 的随机空闲端口,创建 MCP HTTP 服务,并把 ToolAppsMcpServer 接进去;然后用 axum 启动 HTTP 路由 /mcp;最后返回服务器地址和后台任务句柄,方便测试结束时停止它。

调用关系:多个测试都会先调用它准备 MCP 服务,包括正常工具调用、表单确认、网址确认和大结果截断测试。它把底层网络启动细节藏起来,让测试主体只关心行为。

调用图:被 4 处调用(mcp_server_tool_call_forwards_url_elicitation, mcp_server_tool_call_returns_tool_result, mcp_server_tool_call_round_trips_elicitation, mcp_tool_call_completion_notification_contains_truncated_large_result);外部调用 9 个(new, default, new, default, new, bind, serve, format!, spawn)。

wait_for_mcp_tool_call_completed685–703 ↗
async fn wait_for_mcp_tool_call_completed(
    mcp: &mut TestAppServer,
    call_id: &str,
) -> Result<ItemCompletedNotification>

作用:这个辅助函数一直等待,直到 app-server 流里出现指定 MCP 工具调用的完成通知。它避免测试被其他通知打断。

数据流:它反复从 TestAppServer 读取 item/completed 通知,每次拿到 params 后解析成 ItemCompletedNotification。如果里面的 ThreadItem 是 MCP 工具调用,并且 id 等于目标 call_id,就返回这个完成通知;否则继续等。

调用关系:它被大结果截断测试调用,用来精确抓住那一次工具调用完成事件。它不负责发起工具调用,只负责在通知流里筛出测试关心的那一条。

调用图:调用 1 个内部函数(read_stream_until_notification_message);被 1 处调用(mcp_tool_call_completion_notification_contains_truncated_large_result);外部调用 3 个(matches!, from_value, timeout)。

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

这个测试文件解决的是一个很实际的问题:外部应用工具有时不能直接执行动作,必须先问用户一句“你同意吗?”。这里的 MCP 可以理解成“应用工具和 Codex 之间说话的一套协议”。文件里搭了一个假的日历应用服务器,声明自己有一个确认工具;当这个工具被调用时,它会发起一次 elicitation,也就是“向用户索要确认信息”。测试再启动真正的 app server,把假模型、假应用、假登录信息都接起来,像真人使用一样发起对话。关键检查点是:服务端是否把确认请求推给客户端;客户端回复“接受,并且 confirmed=true”后,工具是否得到这个结果;最后模型是否收到工具输出 accepted。没有这类测试,工具确认弹窗可能看似能发出,但回复无法回传,或者顺序错乱,最终导致应用动作卡住或误执行。

函数细节7
mcp_server_elicitation_round_trip76–300 ↗
async fn mcp_server_elicitation_round_trip() -> Result<()>

作用:这是主测试,模拟一次从用户发消息、模型要求调用日历工具、工具反过来请求用户确认、用户同意、最后工具返回成功的完整来回。它的价值是验证多方协作不是只在单个零件里正确,而是整条路都能走通。

数据流:进去的是一组临时搭好的环境:假模型服务器、假应用服务器、临时配置目录和假登录凭据。它先让模型返回一段预设的流式响应,再启动 app server,创建会话和对话轮次;当工具触发确认请求时,它读取服务端发来的请求,检查请求内容是不是“Allow this request?”和一个 confirmed 布尔字段;随后它把“接受,confirmed=true”发回去。出来的结果是:测试确认先收到“请求已解决”的通知,再收到“对话轮次完成”的通知,并且假模型第三次请求里包含工具输出 accepted。

调用关系:它是这个文件的总导演。它会调用 start_apps_server 搭一个假的应用服务器,调用 write_config_toml 写好测试配置,还借助测试工具启动假模型响应。后面 ElicitationAppsMcpServer::list_tools 和 ElicitationAppsMcpServer::call_tool 会在这个流程中被 app server 间接触发,用来提供工具列表和执行工具确认。测试结束时,它会中止假的应用服务器任务,避免后台任务继续跑。

调用图:调用 6 个内部函数(new, new, start_apps_server, write_config_toml, mount_sse_sequence, start_mock_server);外部调用 18 个(new, default, builder, Boolean, new, Integer, to_response, write_chatgpt_auth, assert!, assert_eq! (+8 more))。

ElicitationAppsMcpServer::get_info312–315 ↗
fn get_info(&self) -> ServerInfo

作用:这个函数告诉对方:这个假的 MCP 服务器支持什么能力。这里重点是声明“我支持工具”,这样 Codex 才会去问它有哪些工具可用。

数据流:进去的是这个假的服务器实例本身,不需要额外输入。它创建一份服务器信息,打开 tools 能力,并标明协议版本。出来的是 ServerInfo,也就是一张“自我介绍卡片”,告诉客户端可以按指定协议来列工具、调工具。

调用关系:它属于假的日历 MCP 服务器的基础握手环节。start_apps_server 把 ElicitationAppsMcpServer 挂到 HTTP 服务上后,Codex 连接这个 MCP 服务时会用到这类信息;之后才会继续进入 list_tools 和 call_tool 这类具体能力。

调用图:外部调用 2 个(builder, new)。

ElicitationAppsMcpServer::list_tools317–347 ↗
async fn list_tools(
        &self,
        _request: Option<rmcp::model::PaginatedRequestParams>,
        _context: RequestContext<RoleServer>,
    ) -> Result<ListToolsResult, rmcp::ErrorData>

作用:这个函数给 Codex 列出假的日历应用提供的工具。它声明有一个“确认日历动作”的工具,并带上连接器 id 和名字,让系统知道这个工具属于 Calendar 应用。

数据流:进去的是列工具请求和请求上下文,但这个测试里没有真正使用它们。函数先做一个很简单的输入格式说明:工具参数必须是对象,并且不能有额外字段;然后创建一个工具,名字是 calendar_confirm_action,描述是确认日历动作,还标注为只读;最后把 connector_id=calendar、connector_name=Calendar 塞到元数据里。出来的是一个只包含这个工具的工具列表。

调用关系:它在 Codex 发现应用能力时被调用。mcp_server_elicitation_round_trip 先发一轮 warmup,就是为了让连接器和工具信息被加载出来;这个函数提供的工具名称和元数据,后面让模型能够用 mcp__codex_apps__calendar 这个命名空间触发对应工具。

调用图:外部调用 8 个(new, Borrowed, new, new, json!, new, from_value, vec!)。

ElicitationAppsMcpServer::call_tool349–384 ↗
async fn call_tool(
        &self,
        _request: CallToolRequestParams,
        context: RequestContext<RoleServer>,
    ) -> Result<CallToolResult, rmcp::ErrorData>

作用:这个函数模拟真正执行日历工具时的行为:先别急着做事,而是向用户发起确认。用户同意就返回 accepted,不同意或取消就返回对应文字。

数据流:进去的是工具调用请求和一个上下文;请求本身在测试里不看,重点用上下文里的 peer,也就是和客户端通信的另一端。函数先造一个表单 schema,要求用户返回 confirmed 这个布尔值;然后发送 elicitation 请求,消息是“Allow this request?”。等回复回来后,如果 action 是 Accept,它会检查内容确实是 confirmed=true,并输出一段文本 accepted;如果是 Decline 或 Cancel,则分别输出 declined 或 cancelled。出来的是 MCP 工具调用结果,里面装着一条文本内容。

调用关系:它是本测试最核心的假工具执行点。mcp_server_elicitation_round_trip 让模型发起工具调用后,MCP 服务会进入这里;这里又把确认问题交回给 Codex 客户端。客户端在主测试里回复后,这个函数继续执行并把 accepted 交回,最终被测试检查是否写进模型的 function_call_output。

调用图:外部调用 6 个(new, builder, Boolean, assert_eq!, success, vec!)。

start_apps_server387–416 ↗
async fn start_apps_server() -> Result<(String, JoinHandle<()>)>

作用:这个函数启动一个临时的假应用服务器。它让测试不用依赖真实的线上应用目录或真实 MCP 服务,也能完整模拟 Calendar 应用。

数据流:进去不需要外部参数。函数先准备服务器状态,包括预期的 Bearer 登录令牌和账号 id;然后在本机随机端口绑定一个 TCP 监听器;再创建 MCP HTTP 服务,把 ElicitationAppsMcpServer 作为实际处理工具请求的服务器;最后用路由挂上两个应用目录接口和一个 MCP 接口,并在后台任务里启动 HTTP 服务。出来的是服务器地址字符串和一个 JoinHandle,JoinHandle 可以理解成“后台任务的遥控器”,测试结束时可用它停止服务器。

调用关系:它被 mcp_server_elicitation_round_trip 在测试开头调用。启动后,write_config_toml 会把它的地址写进配置,让 app server 把这里当成 ChatGPT apps 服务。它内部的目录接口会走 list_directory_connectors,MCP 接口会把请求交给 ElicitationAppsMcpServer 的 get_info、list_tools 和 call_tool。

调用图:被 1 处调用(mcp_server_elicitation_round_trip);外部调用 10 个(new, default, new, default, new, bind, get, serve, format!, spawn)。

list_directory_connectors418–458 ↗
async fn list_directory_connectors(
    State(state): State<Arc<AppsServerState>>,
    headers: HeaderMap,
    uri: Uri,
) -> Result<Json<serde_json::Value>, StatusCode>

作用:这个函数模拟“应用目录列表”接口,告诉 Codex 当前有一个 Calendar 连接器可用。它还会检查请求有没有带正确登录信息,避免测试绕过认证流程。

数据流:进去的是共享状态、HTTP 请求头和请求地址。它从请求头里检查 Authorization 是否等于 Bearer chatgpt-token,也检查 chatgpt-account-id 是否等于 account-123;还检查网址查询参数里是否有 external_logos=true。如果登录信息不对,就返回 401 未授权;如果缺少查询参数,就返回 400 请求错误;都正确时,出来的是一段 JSON,里面列出一个 id 为 calendar、名字为 Calendar、已启用的应用。

调用关系:它被 start_apps_server 挂到 /connectors/directory/list 和 /connectors/directory/list_workspace 两个路由上。mcp_server_elicitation_round_trip 启动 app server 并写入认证信息后,系统加载应用目录时会访问这里;返回的 calendar 信息让后续工具命名空间和连接器元数据能对上。

调用图:外部调用 4 个(get, query, Json, json!)。

write_config_toml460–489 ↗
fn write_config_toml(
    codex_home: &std::path::Path,
    responses_server_uri: &str,
    apps_server_url: &str,
) -> std::io::Result<()>

作用:这个函数给临时测试目录写一份配置文件,让被测 app server 知道该连哪个假模型服务、哪个假应用服务,以及开启 apps 功能。没有这份配置,测试里的各个假服务就接不起来。

数据流:进去的是临时的 codex_home 路径、假模型服务器地址和假应用服务器地址。它把这些值拼进 config.toml 文本里,同时设置 mock-model、只读沙箱、untrusted 审批策略、文件形式的 OAuth 凭据存储,并开启 features.apps。出来的是写文件的结果;成功时,临时目录下多了一个 config.toml。

调用关系:它被 mcp_server_elicitation_round_trip 在启动 TestAppServer 前调用。TestAppServer 后续读取这份配置,才会把模型请求发到 responses mock server,把应用和 MCP 请求发到 start_apps_server 启动的假应用服务器。

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

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

这里搭了一个很小的假世界:一个假的 Apps MCP 服务器、一个假的模型响应服务器,再启动被测的 app server。MCP 可以理解成一种“工具和资源接口协议”,让程序像向图书馆借书一样,按 URI 去列出和读取资源。测试会检查几件事:普通资源读取能返回文本和二进制内容;没有线程时也能读公共资源;线程 ID 写错时要报“找不到线程”;只有在没有本地执行器的编排器场景里,技能资源才会被发现并注入给模型;重复读取同一个引用资源会走缓存;重新加载 MCP 配置后会重新发现技能。文件底部的 ResourceAppsMcpServer 是测试用的假 MCP 服务器,它会故意分页、故意在后续页报错,并记录被调用次数,方便测试确认系统行为不是碰巧成功。

函数细节14
mcp_resource_read_returns_resource_contents87–128 ↗
async fn mcp_resource_read_returns_resource_contents() -> Result<()>

作用:测试“带着线程读取 MCP 资源”这条路是否正常。它确认 app server 能从 Apps MCP 服务器拿到一个资源,并把文本内容和二进制内容都按协议返回。

数据流:进去的是一个临时启动的假模型服务器、假 Apps MCP 服务器,以及一个新建线程。测试先启动线程,拿到线程 ID;再发出读取 test://codex/resource 的请求;最后把返回结果转成结构化响应,和预先写好的期望内容比较。结束时它会关闭假 Apps MCP 服务器。

调用关系:这是最基础的正向测试。它先借助 start_resource_apps_mcp_server 搭资源服务器,再用 start_resource_test_app_server 启动被测服务,最后用 expected_resource_read_response 提供标准答案来校验整条读取链路。

调用图:调用 3 个内部函数(start_resource_apps_mcp_server, start_resource_test_app_server, start_mock_server);外部调用 5 个(default, Integer, to_response, assert_eq!, timeout)。

orchestrator_skill_can_read_referenced_resource_without_an_executor131–364 ↗
async fn orchestrator_skill_can_read_referenced_resource_without_an_executor() -> Result<()>

作用:测试“没有本地执行器时,编排器技能仍然能被发现和读取”。这里的技能像一张说明书,主说明书里又引用了另一份资料,测试确认模型可以通过技能工具读到那份引用资料。

数据流:进去的是假 Apps MCP 服务器、假模型响应流,以及一个明确没有执行环境的线程。测试让模型依次触发 skills.list 和两次 skills.read;系统先从 MCP 资源列表中发现合法技能,再把技能说明注入给模型,然后读取技能引用的资源。出来的是多次模型请求记录、函数调用输出、调用次数快照;测试还会触发 MCP 重新加载,再确认刷新后会重新列资源和重读主技能,但引用资源仍可复用缓存。

调用关系:这是本文件最完整的流程测试。它调用 start_resource_apps_mcp_serverstart_resource_test_app_server 搭环境,假模型响应由 mount_sse_sequence 提供;过程中会间接打到 ResourceAppsMcpServer::list_resourcesResourceAppsMcpServer::read_resource,最后用 ResourceAppsMcpCalls::snapshot 确认列表、主技能读取、引用读取的次数符合预期。

调用图:调用 4 个内部函数(start_resource_apps_mcp_server, start_resource_test_app_server, mount_sse_sequence, start_mock_server);外部调用 9 个(default, new, Integer, to_response, assert!, assert_eq!, format!, timeout, vec!)。

local_executor_does_not_expose_orchestrator_skills367–437 ↗
async fn local_executor_does_not_expose_orchestrator_skills() -> Result<()>

作用:测试“有本地执行器时,不应该暴露编排器技能”。这是一个安全边界检查,避免把只该给编排器用的技能混进本地执行场景。

数据流:进去的是普通线程、假 Apps MCP 服务器和一次假模型响应。测试让用户请求使用某个编排器技能;系统正常完成一轮对话;然后测试检查发给模型的请求里没有 skills.list、没有 skills.read,开发者消息和用户消息里也没有技能名或技能正文标记。最后关闭假服务器。

调用关系:它复用 start_resource_apps_mcp_serverstart_resource_test_app_server 搭环境,但和上一个测试相反,它要证明技能发现这条路没有被打开。假模型响应由 mount_sse_once 提供,只需要一次模型请求就能检查结果。

调用图:调用 5 个内部函数(start_resource_apps_mcp_server, start_resource_test_app_server, mount_sse_once, sse, start_mock_server);外部调用 6 个(default, Integer, to_response, assert!, timeout, vec!)。

mcp_resource_read_returns_resource_contents_without_thread440–490 ↗
async fn mcp_resource_read_returns_resource_contents_without_thread() -> Result<()>

作用:测试“不提供线程 ID 也能读取 MCP 资源”。这保证某些全局资源或不依赖会话的资源,不必先创建对话线程也能被读取。

数据流:进去的是一个只配置 Apps 功能和认证信息的临时配置目录。测试启动假 Apps MCP 服务器,写入配置和登录凭据,启动 app server 并初始化;随后发送一个 thread_id: None 的资源读取请求;出来的是和标准答案一样的文本加二进制资源内容。最后关闭假服务器。

调用关系:它直接使用 start_resource_apps_mcp_server 搭资源端,但没有用通用的 start_resource_test_app_server,因为它要专门构造“没有线程”的配置场景。校验时同样依赖 expected_resource_read_response 作为标准答案。

调用图:调用 3 个内部函数(new, new, start_resource_apps_mcp_server);外部调用 7 个(new, Integer, write_chatgpt_auth, assert_eq!, format!, write, timeout)。

mcp_resource_read_returns_error_for_unknown_thread493–553 ↗
async fn mcp_resource_read_returns_error_for_unknown_thread() -> Result<()>

作用:测试“给了不存在的线程 ID 时必须报错”。这能防止系统悄悄忽略错误线程,或者把请求错误地算到别的会话里。

数据流:进去的是一个临时配置目录和一个假的 UUID 线程 ID。测试用进程内方式启动 app server,也就是不额外拉起子进程;然后发送 McpResourceRead 请求,里面带着不存在的线程 ID;出来的应该是错误响应,错误消息里必须包含“thread not found”。最后它会关闭这个进程内客户端。

调用关系:这是负向测试,不需要假 Apps MCP 服务器。它通过 in_process::start 直接启动被测服务,避开子进程清理带来的测试干扰,然后检查请求接口在错误输入下是否守住边界。

调用图:调用 5 个内部函数(start, default, without_managed_config_for_tests, default_for_tests, new);外部调用 8 个(new, new, new, bail!, Integer, default, assert!, default)。

start_resource_test_app_server555–599 ↗
async fn start_resource_test_app_server(
    apps_server_url: &str,
    responses_server_uri: &str,
) -> Result<(TempDir, TestAppServer)>

作用:搭一个完整的测试版 app server。它负责写临时配置、写假登录信息,并把服务初始化好,让其他测试可以直接发请求。

数据流:进去的是 Apps MCP 服务器地址和假模型服务器地址。函数创建临时目录,在里面写 config.toml,开启 apps 和技能说明,把模型提供方指向假响应服务器;再写入 ChatGPT 认证文件;最后启动 TestAppServer 并等待初始化完成。出来的是临时目录和已经可用的测试服务器对象。

调用关系:多个正向和隔离测试都会先调用它,省得每个测试重复写配置。它依赖外部的 write_chatgpt_auth 写凭据,并用超时保护初始化过程,避免测试卡死。

调用图:调用 2 个内部函数(new, new);被 3 处调用(local_executor_does_not_expose_orchestrator_skills, mcp_resource_read_returns_resource_contents, orchestrator_skill_can_read_referenced_resource_without_an_executor);外部调用 5 个(new, write_chatgpt_auth, format!, write, timeout)。

start_resource_apps_mcp_server601–624 ↗
async fn start_resource_apps_mcp_server() -> Result<(String, Arc<ResourceAppsMcpCalls>, JoinHandle<()>)>

作用:启动一个测试专用的 Apps MCP HTTP 服务器。它像一个小型假资源仓库,供 app server 去列资源、读资源。

数据流:进去不需要业务输入。函数先在本机随机端口监听,创建共享的调用计数器,再把 ResourceAppsMcpServer 包成可通过 HTTP 访问的 MCP 服务,挂到 /api/codex/ps/mcp 路径上;出来的是服务器 URL、调用计数器,以及后台任务句柄。测试用句柄在结束时停止服务器。

调用关系:大多数测试都用它来提供可控的 MCP 资源端。真正处理 MCP 请求的是 ResourceAppsMcpServer::get_infoResourceAppsMcpServer::list_resourcesResourceAppsMcpServer::read_resource

调用图:被 4 处调用(local_executor_does_not_expose_orchestrator_skills, mcp_resource_read_returns_resource_contents, mcp_resource_read_returns_resource_contents_without_thread, orchestrator_skill_can_read_referenced_resource_without_an_executor);外部调用 11 个(clone, new, default, new, default, new, bind, default, serve, format! (+1 more))。

expected_resource_read_response626–643 ↗
fn expected_resource_read_response() -> McpResourceReadResponse

作用:生成普通资源读取的标准答案。测试用它来判断实际返回是不是一字不差地包含预期的文本资源和二进制资源。

数据流:进去没有参数。函数把文件顶部定义的测试 URI、文本、二进制字符串和 MIME 类型组装成 McpResourceReadResponse;出来的是一个包含两个内容项的响应对象。它不修改任何外部状态。

调用关系:资源读取相关测试会拿它和真实响应做比较。这样期望值集中放在一个地方,避免多个测试各写一份而互相不一致。

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

ResourceAppsMcpCalls::snapshot653–659 ↗
fn snapshot(&self) -> ResourceAppsMcpCallCounts

作用:读取假 MCP 服务器到目前为止被调用了多少次。它让测试能确认系统有没有缓存、有没有刷新、有没有多读。

数据流:进去的是保存计数的 ResourceAppsMcpCalls 对象。函数从三个原子计数器里读数;原子计数器可以理解成多线程下也能安全累加的数字。出来的是普通的 ResourceAppsMcpCallCounts 快照,不会把计数清零。

调用关系:复杂技能测试会在关键步骤后调用它,确认 list_resources、主技能读取、引用读取的实际次数。计数本身是在 ResourceAppsMcpServer::list_resourcesResourceAppsMcpServer::read_resource 里增加的。

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

ResourceAppsMcpServer::get_info675–678 ↗
fn get_info(&self) -> ServerInfo

作用:告诉客户端这个假 MCP 服务器支持什么能力。这里主要声明它支持“资源”能力,并使用指定的 MCP 协议版本。

数据流:进去的是服务器自身。函数构造一个 ServerInfo,里面写明启用了资源功能和协议版本;出来的是这份能力说明。它不读取资源,也不改计数。

调用关系:当 app server 连接这个假 MCP 服务器、做握手或初始化时,会用到这份信息。没有它,客户端可能不知道这个服务器可以列资源、读资源。

调用图:外部调用 2 个(builder, new)。

ResourceAppsMcpServer::list_resources680–726 ↗
async fn list_resources(
        &self,
        request: Option<PaginatedRequestParams>,
        _context: RequestContext<RoleServer>,
    ) -> Result<ListResourcesResult, rmcp::ErrorData>

作用:模拟 MCP 的“列出资源”接口。它故意做成分页,并在后面一页报错,用来测试 app server 发现技能时如何处理分页和警告。

数据流:进去的是可选分页参数,里面可能带 cursor,也就是“从哪一页继续”的标记。函数先把列资源次数加一;如果没有 cursor,就返回一个应被忽略的普通资源和下一页标记;如果 cursor 是 skills-page,就返回真正的技能资源,并指向一个会失败的下一页;如果 cursor 是 failing-page,就返回模拟内部错误;其他 cursor 则返回参数错误。出来的是资源列表、下一页标记,或错误。

调用关系:技能发现流程会调用它。它返回的资源由 skill_resource 组装,调用次数会被 ResourceAppsMcpCalls::snapshot 检查;复杂测试正是靠它制造“先发现技能,后续分页失败但给出警告”的场景。

调用图:外部调用 3 个(internal_error, invalid_params, vec!)。

ResourceAppsMcpServer::read_resource728–777 ↗
async fn read_resource(
        &self,
        request: ReadResourceRequestParams,
        _context: RequestContext<RoleServer>,
    ) -> Result<ReadResourceResult, rmcp::ErrorData>

作用:模拟 MCP 的“读取资源”接口。它根据 URI 返回主技能说明、技能引用资料,或者普通测试资源。

数据流:进去的是一个读取请求,核心是资源 URI。函数先看 URI:如果是主技能说明,就增加主技能读取次数并返回技能正文;如果是引用资料,就增加引用读取次数并返回参考文档;如果是普通测试资源,就返回一个文本内容和一个二进制内容;如果都不是,就返回“资源不存在”错误。出来的是资源内容列表或错误。

调用关系:普通资源读取测试和技能读取测试都会间接打到它。它产生的调用次数会被 ResourceAppsMcpCalls::snapshot 用来判断缓存和刷新行为是否正确。

调用图:外部调用 4 个(new, format!, resource_not_found, vec!)。

skill_resource780–795 ↗
fn skill_resource(
    uri: &str,
    name: &str,
    description: &str,
    mime_type: &str,
    plugin_name: &str,
    skill_name: &str,
) -> Resource

作用:把一组普通字符串包装成一个 MCP 资源条目。它让测试能方便地造出“看起来像技能资源”的列表项。

数据流:进去的是资源 URI、显示名、描述、MIME 类型、插件名和技能名。函数先创建原始资源对象,填上描述和 MIME 类型,再调用 skill_resource_meta 加上插件和技能的元数据;出来的是完整的 Resource 对象。

调用关系ResourceAppsMcpServer::list_resources 用它生成分页返回的资源。它把元数据细节交给 skill_resource_meta,这样资源本体和附加标签分开组装。

调用图:调用 1 个内部函数(skill_resource_meta);外部调用 2 个(new, new)。

skill_resource_meta797–802 ↗
fn skill_resource_meta(plugin_name: &str, skill_name: &str) -> Meta

作用:生成技能资源的附加信息。这里的附加信息包括插件名和技能名,供上层判断这个资源是不是某个插件里的技能。

数据流:进去的是插件名和技能名两个字符串。函数把它们放进一个 JSON 风格的键值表,再包装成 MCP 的 Meta 类型;出来的是可挂到资源上的元数据对象。它不访问外部服务,也不改状态。

调用关系:它只被 skill_resource 调用,是构造测试资源的小零件。没有这块元数据,技能发现逻辑就很难知道资源属于哪个插件、对应哪个技能。

调用图:被 1 处调用(skill_resource);外部调用 3 个(json!, Meta, from_iter)。

命令和 shell 执行

这些测试验证应用服务器的命令执行界面,包括通用命令 RPC、线程 shell 命令,以及专门的 zsh-fork 回合启动路径。

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

这个文件测试的是 command/exec,也就是客户端让服务器在本机环境里跑一条命令的能力。这个能力很敏感:如果处理不好,可能出现命令杀不掉、输出丢失、权限绕过、不同客户端互相影响等问题。测试会先搭一个临时的配置目录和假的后端服务,再通过测试服务器或 WebSocket(网页/程序和服务器之间的一条长连接)发送 JSON-RPC 请求(用 JSON 格式发“调用某个方法”的消息)。它覆盖普通一次性命令、流式输出、标准输入写入、伪终端 TTY(像真实终端一样交互)、窗口大小调整、权限配置、环境变量合并、输出截断、错误参数校验,以及连接断开后进程是否被清理。后半部分是辅助函数,负责等输出、解码 base64 输出片段、改测试配置、检查系统进程是否还活着。

函数细节30
command_exec_without_streams_can_be_terminated41–88 ↗
async fn command_exec_without_streams_can_be_terminated() -> Result<()>

作用:测试一个不走流式输入输出的长时间命令,也能通过 processId 被提前终止。这样可以防止后台命令一直卡着,占用机器资源。

数据流:它先创建临时配置和测试服务器 → 发起一个 sleep 30 命令,并给它一个进程编号 → 再发送终止请求 → 最后确认终止请求成功、原命令返回非 0 退出码,并且没有 stdout/stderr 输出。

调用关系:这是 command/exec 终止能力的验收测试。它依赖测试服务器启动和配置函数,最后用 to_response 把 JSON 结果转成命令响应来检查。

调用图:调用 2 个内部函数(new, create_config_toml);外部调用 9 个(new, new, Integer, create_mock_responses_server_sequence_unchecked, to_response, assert_eq!, assert_ne!, timeout, vec!)。

command_exec_without_process_id_keeps_buffered_compatibility91–135 ↗
async fn command_exec_without_process_id_keeps_buffered_compatibility() -> Result<()>

作用:测试老用法:不提供 processId 时,命令仍然可以一次性执行并把 stdout/stderr 缓存在最终响应里返回。这样旧客户端不会因为新加的流式机制而坏掉。

数据流:它启动测试服务器 → 发一个不带 processId 的 shell 命令,命令分别写标准输出和标准错误 → 等最终响应 → 确认退出码是 0,stdout 是 legacy-out,stderr 是 legacy-err

调用关系:这是兼容性测试。它不使用本文件里的流式读取辅助函数,只走普通请求和最终响应转换。

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

command_exec_env_overrides_merge_with_server_environment_and_support_unset138–194 ↗
async fn command_exec_env_overrides_merge_with_server_environment_and_support_unset() -> Result<()>

作用:测试命令执行时的环境变量规则:请求里的变量可以覆盖服务器已有变量,也可以新增变量,还可以把某个变量取消掉。环境变量就像给程序的一组临时说明纸条,合并错了会让命令行为不对。

数据流:它用带环境变量的方式启动测试服务器 → 请求里传入覆盖、新增、删除三种环境变量设置 → 命令打印这些变量和 CODEX_HOME → 最后确认打印结果符合预期。

调用关系:它通过 new_with_env 准备服务器原始环境,再用 command/exec 参数里的 env 覆盖它,最后读取普通命令响应来验证。

调用图:调用 2 个内部函数(new_with_env, create_config_toml);外部调用 9 个(from, new, new, Integer, create_mock_responses_server_sequence_unchecked, to_response, assert_eq!, timeout, vec!)。

command_exec_accepts_permission_profile197–241 ↗
async fn command_exec_accepts_permission_profile() -> Result<()>

作用:测试 command/exec 能接受 permissionProfile(权限配置档,一组预先定义好的访问规则)。这保证客户端可以用命名好的权限方案,而不是每次手写底层沙箱规则。

数据流:它启动测试服务器 → 发一个带只读权限配置档的简单打印命令 → 读取最终响应 → 确认命令成功并输出 profile

调用关系:这是权限配置入口的基础测试。它只确认参数被接受且命令正常跑完,不深入检查权限细节。

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

command_exec_permission_profile_starts_selected_network_proxy244–292 ↗
async fn command_exec_permission_profile_starts_selected_network_proxy() -> Result<()>

作用:测试当选中的权限配置档允许网络代理时,命令环境里确实会启动并标记网络代理。网络代理可以理解成“上网要走的门卫”。

数据流:它先把一个名为 networked 的权限配置写进配置文件 → 启动服务器 → 用这个配置档执行命令 → 命令打印 CODEX_NETWORK_PROXY_ACTIVE → 最后确认值是 1

调用关系:它调用 insert_networked_permission_profile_config 修改测试配置,再通过 command/exec 验证服务器是否按所选权限档启动代理。

调用图:调用 3 个内部函数(new, insert_networked_permission_profile_config, create_config_toml);外部调用 8 个(new, new, Integer, create_mock_responses_server_sequence_unchecked, to_response, assert_eq!, timeout, vec!)。

command_exec_permission_profile_does_not_reuse_default_network_proxy295–340 ↗
async fn command_exec_permission_profile_does_not_reuse_default_network_proxy() -> Result<()>

作用:测试如果默认权限配置启用了网络代理,另一个明确选择的只读配置档不会偷偷复用这个代理。这样能避免“默认有网”误传给本该没网的命令。

数据流:它把默认权限设成 networked,同时请求里选择只读权限档 → 命令检查网络代理标记 → 最后确认输出是 unset,表示没有代理。

调用关系:它和上一个网络代理测试互为补充:一个确认选中网络配置会启用代理,另一个确认没选中时不会误启用。配置写入工作交给 insert_networked_permission_profile_config

调用图:调用 3 个内部函数(new, insert_networked_permission_profile_config, create_config_toml);外部调用 8 个(new, new, Integer, create_mock_responses_server_sequence_unchecked, to_response, assert_eq!, timeout, vec!)。

command_exec_permission_profile_project_roots_use_command_cwd344–402 ↗
async fn command_exec_permission_profile_project_roots_use_command_cwd() -> Result<()>

作用:这个 Unix 平台测试确认权限配置里的“工作区根目录”会按命令自己的 cwd(当前工作目录)来算,而不是按服务器启动目录来算。否则命令可能获得不该有的目录写权限。

数据流:它创建一个子目录作为命令 cwd → 写入一段权限配置:根目录只读、工作区可写 → 命令尝试在子目录写文件,并尝试在父目录写文件 → 最后确认子目录文件写成了,父目录文件没有出现。

调用关系:它调用 insert_command_exec_config 插入专门的权限配置。这个测试只在 Unix 上编译运行,因为它依赖对应平台的文件权限/沙箱行为。

调用图:调用 3 个内部函数(new, insert_command_exec_config, create_config_toml);外部调用 10 个(new, new, Integer, create_mock_responses_server_sequence_unchecked, to_response, assert!, assert_eq!, create_dir, timeout, vec!)。

command_exec_returns_error_when_local_environment_is_disabled405–441 ↗
async fn command_exec_returns_error_when_local_environment_is_disabled() -> Result<()>

作用:测试本地执行环境被关闭时,command/exec 会返回明确错误,而不是假装执行或给出含糊失败。这样用户能知道问题是“本地环境没配置”。

数据流:它用环境变量把执行服务器地址设成 none → 启动测试服务器 → 发一个简单命令 → 等错误响应 → 确认错误消息是 local environment is not configured

调用关系:它使用 new_with_env 模拟被禁用的本地执行环境,随后检查 command/exec 的错误路径。

调用图:调用 2 个内部函数(new_with_env, create_config_toml);外部调用 7 个(new, new, Integer, create_mock_responses_server_sequence_unchecked, assert_eq!, timeout, vec!)。

command_exec_rejects_sandbox_policy_with_permission_profile444–479 ↗
async fn command_exec_rejects_sandbox_policy_with_permission_profile() -> Result<()>

作用:测试不能同时传 sandboxPolicy(沙箱策略,限制命令能访问什么)和 permissionProfile(权限配置档)。两套权限说法同时出现会互相打架,所以必须拒绝。

数据流:它发起一个同时带 sandboxPolicy 和 permissionProfile 的命令请求 → 不期待命令真正运行 → 读取错误响应 → 确认错误文字说明这两个参数不能组合。

调用关系:这是参数校验测试。它不关心命令输出,只确认服务器在执行前就拦下矛盾配置。

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

command_exec_rejects_disable_timeout_with_timeout_ms482–517 ↗
async fn command_exec_rejects_disable_timeout_with_timeout_ms() -> Result<()>

作用:测试不能同时说“禁用超时”和“设置超时时间”。这就像同时告诉司机“别限时”和“1 秒内到”,服务器必须直接判为无效。

数据流:它发一个带 processId 的命令请求,其中 disable_timeout 为 true,同时 timeout_ms 又有值 → 等错误响应 → 确认错误消息指出这两个参数不能同时设置。

调用关系:这是 command/exec 的参数冲突校验之一,确保错误在命令启动前返回。

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

command_exec_rejects_disable_output_cap_with_output_bytes_cap520–555 ↗
async fn command_exec_rejects_disable_output_cap_with_output_bytes_cap() -> Result<()>

作用:测试不能同时说“禁用输出上限”和“设置输出字节上限”。这两个要求互相矛盾,必须被拒绝。

数据流:它发一个命令请求,同时设置 output_bytes_capdisable_output_cap → 等错误响应 → 确认错误文字说明这两个参数不能同时设置。

调用关系:这是输出限制参数的校验测试,和超时冲突测试结构类似。

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

command_exec_rejects_negative_timeout_ms558–593 ↗
async fn command_exec_rejects_negative_timeout_ms() -> Result<()>

作用:测试超时时间不能是负数。负数时间没有实际意义,服务器应该给出清楚错误。

数据流:它发一个 timeout_ms: -1 的命令请求 → 等错误响应 → 确认错误消息包含具体的非法值 -1

调用关系:这是 command/exec 参数范围校验的一部分,保证坏请求不会进入真正执行阶段。

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

command_exec_without_process_id_rejects_streaming596–631 ↗
async fn command_exec_without_process_id_rejects_streaming() -> Result<()>

作用:测试如果想要流式输出或 TTY,就必须提供客户端指定的 processId。没有这个编号,服务器不知道后续写入、终止、调整大小该对应哪个进程。

数据流:它发一个不带 processId、但打开 stream_stdout_stderr 的命令请求 → 等错误响应 → 确认错误说明 TTY 或流式模式需要 processId。

调用关系:这是流式命令的基本约束测试。后面那些交互式测试都提供 processId,正是为了满足这里验证的规则。

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

command_exec_non_streaming_respects_output_cap634–678 ↗
async fn command_exec_non_streaming_respects_output_cap() -> Result<()>

作用:测试非流式命令会遵守输出长度上限,stdout 和 stderr 都会被截断到指定字节数。这样可以防止命令输出太多撑爆响应。

数据流:它发一个同时输出 abcdefuvwxyz 的命令,并把输出上限设为 5 → 等最终响应 → 确认 stdout 只剩 abcde,stderr 只剩 uvwxy

调用关系:这是缓冲输出模式的容量限制测试。它和后面的流式输出上限测试一起覆盖两种输出方式。

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

command_exec_streaming_does_not_buffer_output681–742 ↗
async fn command_exec_streaming_does_not_buffer_output() -> Result<()>

作用:测试流式输出模式下,输出会通过通知一段段发给客户端,而最终命令响应里不会再重复缓存这些输出。这样避免同一份输出既发流又塞响应,浪费内存和带宽。

数据流:它启动一个会先打印很多字再睡眠的命令 → 用 collect_command_exec_output_until 收集 stdout,直到发现输出达到上限 → 再终止命令 → 最后确认最终响应没有 stdout/stderr 内容,且退出码表示被终止。

调用关系:它使用本文件的核心流式收集辅助函数 collect_command_exec_output_until,验证 command/exec/outputDelta 通知和最终响应的分工。

调用图:调用 3 个内部函数(new, collect_command_exec_output_until, create_config_toml);外部调用 10 个(new, new, Integer, Mcp, create_mock_responses_server_sequence_unchecked, to_response, assert_eq!, assert_ne!, timeout, vec!)。

command_exec_pipe_streams_output_and_accepts_write745–818 ↗
async fn command_exec_pipe_streams_output_and_accepts_write() -> Result<()>

作用:测试普通管道模式下,命令可以一边把 stdout/stderr 流出来,一边接收客户端写入的标准输入。简单说,就是确认“你说一句,程序回一句”的交互能工作。

数据流:它启动一个先输出开场文字、再等待输入的 shell 命令 → 等 stdout 和 stderr 都出现开场内容 → 发送 base64 编码的 hello\n 到 stdin,并关闭输入 → 再等命令把这行内容回显到 stdout 和 stderr → 最后确认命令成功结束。

调用关系:它调用 wait_for_command_exec_outputs_contains 等待两路输出齐全,再通过写入请求测试 command/exec/write。

调用图:调用 3 个内部函数(new, wait_for_command_exec_outputs_contains, create_config_toml);外部调用 8 个(new, new, Integer, create_mock_responses_server_sequence_unchecked, to_response, assert_eq!, timeout, vec!)。

command_exec_tty_implies_streaming_and_reports_pty_output821–889 ↗
async fn command_exec_tty_implies_streaming_and_reports_pty_output() -> Result<()>

作用:测试打开 TTY(伪终端,让程序以为自己连着真实终端)时,即使没显式打开流式输出,也会按流式方式报告输出。很多交互式程序只有在 TTY 里才表现正常。

数据流:它启动一个检查自己是否在终端里的命令 → 等输出里出现 tty → 向 stdin 写入 world\n → 等回显 echo:world → 最后确认最终响应成功但不缓存输出。

调用关系:它用 wait_for_command_exec_output_contains 等待 TTY 输出,并通过 command/exec/write 测试交互输入。

调用图:调用 3 个内部函数(new, wait_for_command_exec_output_contains, create_config_toml);外部调用 8 个(new, new, Integer, create_mock_responses_server_sequence_unchecked, to_response, assert_eq!, timeout, vec!)。

command_exec_tty_supports_initial_size_and_resize892–977 ↗
async fn command_exec_tty_supports_initial_size_and_resize() -> Result<()>

作用:测试 TTY 支持初始窗口大小,也支持运行中调整窗口大小。对于全屏终端程序来说,行数和列数很重要。

数据流:它以 31 行、101 列启动 TTY 命令 → 等命令打印初始大小 → 发送 resize 请求改成 45 行、132 列 → 写入一行让命令继续 → 等命令打印调整后的大小 → 最后确认命令正常结束。

调用关系:它结合了 wait_for_command_exec_output_contains、command/exec/resize 和 command/exec/write,验证 TTY 生命周期里的尺寸变化。

调用图:调用 3 个内部函数(new, wait_for_command_exec_output_contains, create_config_toml);外部调用 8 个(new, new, Integer, create_mock_responses_server_sequence_unchecked, to_response, assert_eq!, timeout, vec!)。

command_exec_process_ids_are_connection_scoped_and_disconnect_terminates_process980–1062 ↗
async fn command_exec_process_ids_are_connection_scoped_and_disconnect_terminates_process() -> Result<()>

作用:测试 processId 只在同一个 WebSocket 连接里有效,不同客户端即使用同名 processId 也不能互相控制;并且客户端断开后,它启动的进程会被自动终止。

数据流:它启动真实 WebSocket 服务器 → 建两个客户端连接并初始化 → 第一个连接启动一个带特殊 marker 的长进程 → 确认进程存在 → 第二个连接尝试终止同名 processId,预期失败 → 关闭第一个连接 → 等系统进程消失 → 最后杀掉测试服务器进程。

调用关系:这是最接近真实联网场景的测试。它使用 WebSocket 辅助函数、collect_command_exec_output_until 收集输出、wait_for_process_marker 检查系统进程是否存在。

调用图:调用 10 个内部函数(collect_command_exec_output_until, read_initialize_response, wait_for_process_marker, assert_no_message, connect_websocket, create_config_toml, read_jsonrpc_message, send_initialize_request, send_request, spawn_websocket_server);外部调用 9 个(from_millis, new, new, Integer, Websocket, create_mock_responses_server_sequence_unchecked, assert_eq!, format!, json!)。

read_command_exec_delta1064–1071 ↗
async fn read_command_exec_delta(
    mcp: &mut TestAppServer,
) -> Result<CommandExecOutputDeltaNotification>

作用:从测试用的 TestAppServer 里读取一条 command/exec 输出片段通知,并把它变成强类型对象。强类型对象就是字段明确的 Rust 数据,后面检查起来更安全。

数据流:输入是测试服务器连接 → 它一直读到方法名为 command/exec/outputDelta 的通知 → 调用 decode_delta_notification 解析通知参数 → 返回一个输出片段对象。

调用关系:它被 collect_command_exec_output_until 调用,负责 MCP/TestAppServer 这条通道上的输出读取;真正的 JSON 解析交给 decode_delta_notification

调用图:调用 2 个内部函数(read_stream_until_notification_message, decode_delta_notification);被 1 处调用(collect_command_exec_output_until)。

wait_for_command_exec_output_contains1073–1094 ↗
async fn wait_for_command_exec_output_contains(
    mcp: &mut TestAppServer,
    process_id: &str,
    stream: CommandExecOutputStream,
    expected: &str,
) -> Result<()>

作用:等待某个进程的某一路输出包含指定文字。它让测试不用关心输出被拆成多少小块,只要最终出现目标内容就行。

数据流:输入是测试服务器、processId、stdout 或 stderr、期望文字 → 它包装一个停止条件 → 调用 collect_command_exec_output_until 持续收集输出 → 目标文字出现后返回成功。

调用关系:TTY 相关测试会调用它来等关键输出。它是 collect_command_exec_output_until 的更易用版本,专门处理单一路输出。

调用图:调用 1 个内部函数(collect_command_exec_output_until);被 2 处调用(command_exec_tty_implies_streaming_and_reports_pty_output, command_exec_tty_supports_initial_size_and_resize);外部调用 2 个(Mcp, format!)。

wait_for_command_exec_outputs_contains1096–1112 ↗
async fn wait_for_command_exec_outputs_contains(
    mcp: &mut TestAppServer,
    process_id: &str,
    stdout_expected: &str,
    stderr_expected: &str,
) -> Result<()>

作用:等待同一个进程的 stdout 和 stderr 都包含指定文字。适合测试同时检查两条输出管道。

数据流:输入是测试服务器、processId、期望的 stdout 文字和 stderr 文字 → 它持续收集两路输出 → 当两边都包含目标文字时返回成功。

调用关系:管道交互测试 command_exec_pipe_streams_output_and_accepts_write 使用它来确认命令启动输出和回显输出都到了。底层仍交给 collect_command_exec_output_until

调用图:调用 1 个内部函数(collect_command_exec_output_until);被 1 处调用(command_exec_pipe_streams_output_and_accepts_write);外部调用 2 个(Mcp, format!)。

collect_command_exec_output_until1125–1167 ↗
async fn collect_command_exec_output_until(
    mut reader: CommandExecDeltaReader<'_>,
    process_id: &str,
    waiting_for: impl Into<String>,
    mut should_stop: impl FnMut(
        &CollectedCom

作用:这是本文件读取流式命令输出的核心工具。它会不断收集 stdout/stderr 片段,直到调用者给的条件说“够了”。

数据流:输入是一个输出读取来源、processId、等待说明文字和停止判断函数 → 它在超时时间内循环读取输出通知 → 校验通知属于目标 processId → 解码 base64 文本并追加到 stdout 或 stderr 缓冲里 → 一旦停止条件满足,就返回已收集的输出;超时则报出已经收集到的内容方便排查。

调用关系:多个流式测试和等待辅助函数都调用它。它根据来源不同,把读取工作分给 read_command_exec_deltaread_command_exec_delta_ws,再统一做解码、累积和停止判断。

调用图:调用 2 个内部函数(read_command_exec_delta, read_command_exec_delta_ws);被 4 处调用(command_exec_process_ids_are_connection_scoped_and_disconnect_terminates_process, command_exec_streaming_does_not_buffer_output, wait_for_command_exec_output_contains, wait_for_command_exec_outputs_contains);外部调用 6 个(now, into, from_utf8, default, assert_eq!, timeout)。

read_command_exec_delta_ws1169–1181 ↗
async fn read_command_exec_delta_ws(
    stream: &mut super::connection_handling_websocket::WsClient,
) -> Result<CommandExecOutputDeltaNotification>

作用:从 WebSocket 客户端连接里读取 command/exec 输出片段通知。它会跳过不是输出通知的消息。

数据流:输入是 WebSocket 客户端 → 它循环读取 JSON-RPC 消息 → 只接受方法名为 command/exec/outputDelta 的通知 → 调用 decode_delta_notification 解析后返回。

调用关系:它被 collect_command_exec_output_until 用在真实 WebSocket 测试路径上,和 read_command_exec_delta 分别服务两种读取来源。

调用图:调用 2 个内部函数(decode_delta_notification, read_jsonrpc_message);被 1 处调用(collect_command_exec_output_until)。

decode_delta_notification1183–1190 ↗
fn decode_delta_notification(
    notification: JSONRPCNotification,
) -> Result<CommandExecOutputDeltaNotification>

作用:把一条 JSON-RPC 通知里的 params 字段解析成命令输出片段对象。没有它,测试只能手动翻 JSON 字段,容易写错。

数据流:输入是一条 JSON-RPC 通知 → 它先确认通知里有 params → 再用 JSON 反序列化把 params 转成 CommandExecOutputDeltaNotification → 返回解析后的结构;如果缺字段或格式不对就报错。

调用关系:它被 read_command_exec_deltaread_command_exec_delta_ws 共用,是两条读取路径的统一解码步骤。

调用图:被 2 处调用(read_command_exec_delta, read_command_exec_delta_ws);外部调用 1 个(from_value)。

insert_networked_permission_profile_config1192–1215 ↗
fn insert_networked_permission_profile_config(
    codex_home: &Path,
    default_permissions: Option<&str>,
) -> Result<()>

作用:往测试配置里插入一个允许网络代理的权限配置档。它让网络代理相关测试不用手写大段配置。

数据流:输入是 codex_home 路径和可选默认权限名 → 它拼出一段 TOML 配置文本,包括启用 network_proxy 功能、定义 networked 文件系统和网络权限 → 调用 insert_command_exec_config 写入配置文件。

调用关系:网络代理两个测试都会调用它。它只负责生成网络权限配置,真正把文本插入文件的工作交给 insert_command_exec_config

调用图:调用 1 个内部函数(insert_command_exec_config);被 2 处调用(command_exec_permission_profile_does_not_reuse_default_network_proxy, command_exec_permission_profile_starts_selected_network_proxy);外部调用 1 个(format!)。

insert_command_exec_config1217–1227 ↗
fn insert_command_exec_config(codex_home: &Path, inserted_config: &str) -> Result<()>

作用:把一段额外配置插进测试用的 config.toml 里。它像在现成说明书中间夹一页测试专用说明。

数据流:输入是 codex_home 路径和要插入的配置字符串 → 它读取 config.toml → 找到 mock provider 配置表之前的位置 → 把新配置插进去 → 写回文件。

调用关系:权限根目录测试直接调用它,网络权限配置函数也调用它。它是本文件修改测试配置的底层小工具。

调用图:被 2 处调用(command_exec_permission_profile_project_roots_use_command_cwd, insert_networked_permission_profile_config);外部调用 4 个(join, format!, read_to_string, write)。

read_initialize_response1229–1241 ↗
async fn read_initialize_response(
    stream: &mut super::connection_handling_websocket::WsClient,
    request_id: i64,
) -> Result<()>

作用:等待某个 WebSocket 初始化请求对应的响应回来。初始化是连接开始时的握手,没完成就不能可靠发送后续请求。

数据流:输入是 WebSocket 连接和请求 id → 它循环读取 JSON-RPC 消息 → 找到 id 匹配的 Response 后返回成功 → 其他消息会被忽略。

调用关系:真实 WebSocket 连接隔离测试会用它分别等待两个客户端初始化完成。它依赖通用的 read_jsonrpc_message 读取消息。

调用图:调用 1 个内部函数(read_jsonrpc_message);被 1 处调用(command_exec_process_ids_are_connection_scoped_and_disconnect_terminates_process);外部调用 1 个(Integer)。

wait_for_process_marker1243–1255 ↗
async fn wait_for_process_marker(marker: &str, should_exist: bool) -> Result<()>

作用:等待系统进程列表里出现或消失某个特殊标记。测试用这个办法确认命令进程真的还活着,或真的已经退出。

数据流:输入是 marker 字符串和期望是否存在 → 它最多等 5 秒,反复调用 process_with_marker_exists 查进程列表 → 状态符合期望就返回 → 超时就报错说明 marker 没按预期出现或退出。

调用关系:连接隔离和断开清理测试调用它。它把“查一次”的工作交给 process_with_marker_exists,自己负责等待、重试和超时报错。

调用图:调用 1 个内部函数(process_with_marker_exists);被 1 处调用(command_exec_process_ids_are_connection_scoped_and_disconnect_terminates_process);外部调用 5 个(from_millis, from_secs, now, bail!, sleep)。

process_with_marker_exists1257–1264 ↗
fn process_with_marker_exists(marker: &str) -> Result<bool>

作用:检查当前系统进程命令行里是否包含某个 marker。marker 是测试放进命令参数里的独特字符串,用来识别那条测试进程。

数据流:输入是 marker 字符串 → 它运行 ps -axo command 获取进程命令行列表 → 把输出转成文本 → 看每一行是否包含 marker → 返回 true 或 false。

调用关系:它只被 wait_for_process_marker 调用,是进程存在性检查的底层实现。

调用图:被 1 处调用(wait_for_process_marker);外部调用 2 个(from_utf8, new)。

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

这份测试文件像一个质检员,模拟用户和 App Server 对话,然后在对话线程里插入本地 shell 命令。shell 可以理解成电脑里的命令行,比如 macOS/Linux 的 sh,Windows 的 cmd 或 PowerShell。测试会临时建一个干净的配置目录,启动假的模型服务器,再用 JSON-RPC(一种用 JSON 发请求和收回复的通信格式)跟测试服务器说话。它重点确认三件事:用户手动执行的命令会发出“开始、输出、完成”这些实时通知;这些命令不会被塞进之后返回的线程历史里,避免历史记录被本地执行细节污染;如果本地执行环境被关掉,请求会明确报错。它还测试一个细节:如果已经有一个正在进行的回合,用户 shell 命令应该挂到这个活跃回合上,而不是另起炉灶。

函数细节10
thread_shell_command_history_responses_exclude_persisted_command_executions47–181 ↗
async fn thread_shell_command_history_responses_exclude_persisted_command_executions() -> Result<()>

作用:这个测试确认:用户在一个线程里手动运行 shell 命令后,实时通知里能看到命令执行过程,但之后读取线程历史、列出回合、分叉线程时,都不应该把这些命令执行记录返回出来。这样可以避免本地命令输出污染对话历史。

数据流:进去的是一个临时的测试环境、一个假的模型服务器地址,以及一条会输出固定文字的 shell 命令。它先启动测试服务器,创建线程,发送 shell 命令请求;随后读取命令开始通知、输出增量通知和完成通知,并检查来源、状态、输出和退出码都正确。最后它分别调用线程读取、回合列表和线程分叉接口,拿到返回的回合项目后,确认里面没有 CommandExecution 这种命令执行项。

调用关系:这是本文件最完整的一条端到端测试。它会用 create_config_toml 准备配置,用 current_shell_output_command 生成适合当前系统的命令,用 wait_for_command_execution_started、wait_for_command_execution_output_delta、wait_for_command_execution_completed 等待实时事件,最后把结果交给 assert_no_command_executions 检查历史响应是否干净。

调用图:调用 7 个内部函数(new, assert_no_command_executions, create_config_toml, current_shell_output_command, wait_for_command_execution_completed, wait_for_command_execution_output_delta, wait_for_command_execution_started);外部调用 11 个(default, default, new, Integer, default, create_mock_responses_server_sequence, assert_eq!, create_dir, timeout, unreachable! (+1 more))。

thread_shell_command_returns_error_when_local_environment_is_disabled184–224 ↗
async fn thread_shell_command_returns_error_when_local_environment_is_disabled() -> Result<()>

作用:这个测试确认:如果本地执行环境被明确关闭,用户再要求运行 shell 命令时,服务器不会偷偷执行,而是返回清楚的错误。它保护用户环境,避免配置禁用后仍然发生本地命令执行。

数据流:进去的是一个临时配置目录和一个特殊环境变量,这个环境变量把执行服务器地址设成 none,表示本地环境不可用。测试启动服务器并创建线程后,发送一条 pwd 命令请求;服务器没有返回正常结果,而是返回错误消息。测试最后检查错误文字必须是“local environment is not configured”。

调用关系:这个测试用 create_config_toml 写基础配置,再用 TestAppServer::new_with_env 带着额外环境变量启动服务。它不像其他测试那样等待命令开始或完成通知,因为这里正确行为就是命令根本不进入执行流程,而是在请求阶段直接报错。

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

thread_shell_command_uses_existing_active_turn227–377 ↗
async fn thread_shell_command_uses_existing_active_turn() -> Result<()>

作用:这个测试确认:当一个对话回合已经在进行中时,用户手动运行的 shell 命令会归到这个现有回合里。这样前端看到的时间线才连贯,不会把同一段互动拆成奇怪的两个回合。

数据流:进去的是一个临时工作目录、一个会模拟模型要求执行 Python 命令的假响应,以及一条用户手动 shell 命令。测试先启动线程和一个正在进行的回合,等到模型发起的命令执行项出现并请求批准;这时再发送用户 shell 命令。它检查用户命令的开始和完成通知都带着同一个 turn id,也就是同一个回合编号,并确认输出正确。之后它拒绝模型命令的批准请求,让回合结束,再读取线程历史,确认历史里仍然没有命令执行项。

调用关系:这个测试把“模型发起命令”和“用户手动命令”放在同一个活跃回合里比较。它用 create_config_toml 搭环境,用 current_shell_output_command 生成跨系统命令,用 wait_for_command_execution_started 观察模型命令,用 wait_for_command_execution_started_by_source 专门等用户来源的命令,再用 wait_for_command_execution_completed 和 assert_no_command_executions 收尾验证。

调用图:调用 7 个内部函数(new, assert_no_command_executions, create_config_toml, current_shell_output_command, wait_for_command_execution_completed, wait_for_command_execution_started, wait_for_command_execution_started_by_source);外部调用 14 个(default, default, new, Integer, default, create_mock_responses_server_sequence, assert_eq!, panic!, from_value, to_value (+4 more))。

assert_no_command_executions379–386 ↗
fn assert_no_command_executions(items: &[ThreadItem], context: &str)

作用:这个小工具函数检查一批线程项目里有没有命令执行记录。有人会用它来保证接口返回的历史内容没有夹带本地命令执行细节。

数据流:进去的是一组 ThreadItem,也就是线程里的项目列表,以及一个说明当前检查场景的文字。它逐个看这些项目,只要发现 CommandExecution 这种命令执行项就让测试失败;如果一个都没有,就什么也不返回,表示通过。失败时会把 context 放进报错信息里,方便知道是哪个接口返回错了。

调用关系:它被两个端到端测试调用:一个检查 thread/read、thread/turns/list、thread/fork 的结果,另一个检查活跃回合结束后 thread/read 的结果。它不负责拿数据,只负责当最后一道门卫,判断返回内容是否干净。

调用图:被 2 处调用(thread_shell_command_history_responses_exclude_persisted_command_executions, thread_shell_command_uses_existing_active_turn);外部调用 1 个(assert!)。

current_shell_output_command388–404 ↗
fn current_shell_output_command(text: &str) -> Result<(String, String)>

作用:这个函数根据当前操作系统默认 shell,生成一条“打印指定文字”的命令,以及这条命令应该产生的输出。这样同一个测试可以在 Windows、macOS、Linux 上都跑通。

数据流:进去的是想打印的文字。它先查看默认用户 shell 的名字:如果是 PowerShell,就生成 Write-Output 命令并使用 Windows 风格换行;如果是 cmd,就生成 echo 命令;其他情况通常是类 Unix shell,它会先安全引用文字,再生成 printf 命令。出来的是一对字符串:要执行的命令,以及预期看到的输出。

调用关系:它被两个需要真实执行用户 shell 命令的测试调用。上层测试不需要关心当前机器到底用哪种命令行,只要拿到这里返回的命令和预期输出,再去发送请求和核对通知即可。

调用图:调用 1 个内部函数(default_user_shell);被 2 处调用(thread_shell_command_history_responses_exclude_persisted_command_executions, thread_shell_command_uses_existing_active_turn);外部调用 2 个(format!, try_quote)。

wait_for_command_execution_started406–426 ↗
async fn wait_for_command_execution_started(
    mcp: &mut TestAppServer,
    expected_id: Option<&str>,
) -> Result<ItemStartedNotification>

作用:这个异步辅助函数一直等到服务器发来“命令执行开始”的通知。它可以按指定命令编号过滤,也可以接受遇到的第一个命令执行开始事件。

数据流:进去的是正在通信的测试服务器连接,以及可选的 expected_id。它循环读取 item/started 通知,把 JSON 参数转成 ItemStartedNotification;如果通知里的项目不是命令执行,就跳过;如果是命令执行,并且编号符合要求,就把这个开始通知返回给调用者。

调用关系:多个测试用它来等待命令真正进入执行流程。wait_for_command_execution_started_by_source 也把它当底层工具,先拿到命令开始事件,再按来源继续筛选。

调用图:调用 1 个内部函数(read_stream_until_notification_message);被 3 处调用(thread_shell_command_history_responses_exclude_persisted_command_executions, thread_shell_command_uses_existing_active_turn, wait_for_command_execution_started_by_source);外部调用 1 个(from_value)。

wait_for_command_execution_started_by_source428–441 ↗
async fn wait_for_command_execution_started_by_source(
    mcp: &mut TestAppServer,
    expected_source: CommandExecutionSource,
) -> Result<ItemStartedNotification>

作用:这个异步辅助函数等待某个特定来源的命令开始,比如用户手动发起的命令,或智能体发起的命令。来源可以理解成“这条命令是谁要求跑的”。

数据流:进去的是测试服务器连接和期望的 CommandExecutionSource。它反复调用 wait_for_command_execution_started 拿命令开始通知,然后查看通知里命令的 source 字段;如果来源匹配,就返回这个通知;不匹配就继续等下一条。

调用关系:它只在活跃回合测试里使用。那个场景里同时可能有智能体命令和用户 shell 命令,所以单看“命令开始”不够,还要用这个函数等到来源是 UserShell 的那一条。

调用图:调用 1 个内部函数(wait_for_command_execution_started);被 1 处调用(thread_shell_command_uses_existing_active_turn)。

wait_for_command_execution_completed443–463 ↗
async fn wait_for_command_execution_completed(
    mcp: &mut TestAppServer,
    expected_id: Option<&str>,
) -> Result<ItemCompletedNotification>

作用:这个异步辅助函数一直等到服务器发来“命令执行完成”的通知。它让测试可以确认命令最后真的结束了,并检查最终状态、完整输出和退出码。

数据流:进去的是测试服务器连接,以及可选的 expected_id。它循环读取 item/completed 通知,把参数转成 ItemCompletedNotification;如果完成的项目不是命令执行,就跳过;如果是命令执行并且编号符合要求,就把完成通知返回。

调用关系:它被两个主要测试调用,用在命令开始和输出之后。上层测试靠它拿到最终的 CommandExecution 项,再检查状态是不是 Completed、输出是不是预期内容。

调用图:调用 1 个内部函数(read_stream_until_notification_message);被 2 处调用(thread_shell_command_history_responses_exclude_persisted_command_executions, thread_shell_command_uses_existing_active_turn);外部调用 1 个(from_value)。

wait_for_command_execution_output_delta465–482 ↗
async fn wait_for_command_execution_output_delta(
    mcp: &mut TestAppServer,
    item_id: &str,
) -> Result<CommandExecutionOutputDeltaNotification>

作用:这个异步辅助函数等待某条命令的实时输出片段。输出片段可以理解成命令运行时一点点吐出来的文字,而不是等结束后一次性给出。

数据流:进去的是测试服务器连接和目标 item_id,也就是命令执行项的编号。它循环读取 item/commandExecution/outputDelta 通知,把 JSON 参数转成 CommandExecutionOutputDeltaNotification;如果这段输出属于目标命令,就返回它,否则继续等。

调用关系:它在“历史响应不包含命令执行记录”的测试里使用。那个测试不仅检查最终输出,还要确认运行过程中确实发过实时输出通知,所以会在完成通知之前调用它。

调用图:调用 1 个内部函数(read_stream_until_notification_message);被 1 处调用(thread_shell_command_history_responses_exclude_persisted_command_executions);外部调用 1 个(from_value)。

create_config_toml484–524 ↗
fn create_config_toml(
    codex_home: &Path,
    server_uri: &str,
    approval_policy: &str,
    feature_flags: &BTreeMap<Feature, bool>,
) -> std::io::Result<()>

作用:这个函数为测试写出一份 config.toml 配置文件。没有这份配置,测试服务器就不知道用哪个假模型服务、审批策略是什么、哪些功能开关打开或关闭。

数据流:进去的是 Codex 主目录路径、假服务器地址、审批策略字符串,以及一组功能开关。它把功能开关转换成 TOML 配置里的 key = true/false 行,再拼出完整配置文本,最后写到 codex_home/config.toml。出来的是文件写入结果;成功时磁盘上多了一份测试配置。

调用关系:三个测试启动服务器前都会调用它。它属于测试准备阶段的铺路函数:先把配置文件放好,后面的 TestAppServer 启动、线程创建、命令执行请求才会按测试设计连接到 mock provider,而不是碰真实外部服务。

调用图:被 3 处调用(thread_shell_command_history_responses_exclude_persisted_command_executions, thread_shell_command_returns_error_when_local_environment_is_disabled, thread_shell_command_uses_existing_active_turn);外部调用 3 个(join, format!, write)。

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

这份测试像是在搭一个临时小剧场:先创建临时的 Codex 主目录和工作目录,再把真实的 app-server 程序和测试用 zsh 复制到一个假“安装包”里,然后写一份 config.toml 打开 ShellZshFork 功能。测试不会真的访问 OpenAI,而是起一个假的 responses 服务器,按剧本返回“模型想执行某条命令”。接着它用 JSON-RPC(用 JSON 格式发请求和收回应的一套通信方式)启动线程和回合,观察 app-server 发出的 item/started、item/completed、turn/completed 等消息。几个测试分别确认:命令确实由打包 zsh 启动;用户拒绝审批时命令不运行;用户取消审批时整个回合中断;zsh 能拦到由父 shell 继续启动的子命令,并且子命令被拒绝后父命令也被标成拒绝。文件里还有一些搭测试环境的小工具,比如找测试 zsh、复制程序并保留权限、生成配置文件。

函数细节12
turn_start_shell_zsh_fork_executes_command_v250–176 ↗
async fn turn_start_shell_zsh_fork_executes_command_v2() -> Result<()>

作用:这个测试确认:打开 zsh-fork 功能后,app-server 执行 shell 命令时,真的会走打包进测试安装包里的 zsh,而不是随便用系统里的 shell。它还故意让命令一直等着,方便测试中途打断正在运行的命令。

数据流:进去的是一个临时目录、找到的测试 zsh、假模型服务器返回的“请执行 /bin/sh -c 等待文件出现”指令。它写好配置,启动测试 app-server,创建线程和回合,然后不断读服务端消息,直到看到命令开始执行。出来的是一组断言:命令 id 对、状态是执行中、命令字符串里包含打包 zsh 路径、/bin/sh -c、sleep 循环和正确工作目录;最后它发送中断,让正在跑的回合停下来。

调用关系:这是测试运行器直接调用的异步测试。它先靠 find_test_zsh_path 找测试 zsh,再用 create_config_toml 写配置,用 create_zsh_test_mcp_process 启动带测试环境的 app-server;中间由测试支持库伪造模型响应和读写 JSON-RPC 消息,最后把中断交给 TestAppServer 的辅助方法完成。

调用图:调用 4 个内部函数(create_config_toml, create_zsh_test_mcp_process, find_test_zsh_path, sse);外部调用 16 个(from, default, new, Integer, create_mock_responses_server_sequence_unchecked, create_shell_command_sse_response, assert!, assert_eq!, eprintln!, format! (+6 more))。

turn_start_shell_zsh_fork_exec_approval_decline_v2179–312 ↗
async fn turn_start_shell_zsh_fork_exec_approval_decline_v2() -> Result<()>

作用:这个测试确认:当命令需要用户批准,而用户选择“拒绝”时,zsh-fork 命令不会被执行,并且结果会清楚标成 Declined(已拒绝)。这能防止危险命令在用户不允许时偷偷运行。

数据流:进去的是假模型服务器给出的 python3 -c print(42) 命令、配置里的 untrusted 审批策略,以及临时的 Codex 主目录和工作目录。测试启动服务后发起一次回合,等 app-server 向客户端请求命令执行审批,再回一个 Decline。出来的是完成通知:命令 id 匹配、状态是 Declined、没有退出码、也没有命令输出,随后还能看到整个回合完成。

调用关系:测试运行器触发它后,它和其他测试一样先调用 find_test_zsh_path、create_config_toml、create_zsh_test_mcp_process 搭环境。真正的审批往返由 app-server 发出 ServerRequest,测试用 mcp.send_response 扮演用户回绝,然后继续等 item/completed 和 turn/completed 来验证结果。

调用图:调用 3 个内部函数(create_config_toml, create_zsh_test_mcp_process, find_test_zsh_path);外部调用 16 个(from, default, new, Integer, create_mock_responses_server_sequence, assert!, assert_eq!, eprintln!, panic!, from_value (+6 more))。

turn_start_shell_zsh_fork_exec_approval_cancel_v2315–443 ↗
async fn turn_start_shell_zsh_fork_exec_approval_cancel_v2() -> Result<()>

作用:这个测试确认:命令审批弹出来后,如果用户选择“取消”,命令项会被当作拒绝处理,同时整个对话回合会被中断。它检查的是“取消”和普通拒绝不同的连带效果。

数据流:进去的是一条假模型要求执行的 python 命令、untrusted 审批配置、临时目录和测试 zsh。测试启动线程与回合,等到 app-server 发送命令审批请求后,回传 Cancel。出来的是两个关键变化:命令完成通知里的状态是 Declined;回合完成通知里的状态是 Interrupted,表示这轮对话被用户取消打断了。

调用关系:它由测试框架运行,前半段复用 find_test_zsh_path、create_config_toml、create_zsh_test_mcp_process 搭建同样的 zsh-fork 测试环境。中段它扮演客户端收到审批请求并选择取消,后段通过读取通知确认 app-server 把取消一路传播到了命令项和回合状态。

调用图:调用 3 个内部函数(create_config_toml, create_zsh_test_mcp_process, find_test_zsh_path);外部调用 15 个(from, default, new, Integer, create_mock_responses_server_sequence, assert_eq!, eprintln!, panic!, from_value, to_value (+5 more))。

turn_start_shell_zsh_fork_subcommand_decline_marks_parent_declined_v2446–740 ↗
async fn turn_start_shell_zsh_fork_subcommand_decline_marks_parent_declined_v2() -> Result<()>

作用:这个测试确认:父 shell 命令里连续执行多个子命令时,zsh-fork 能拦住每个真正要执行的子命令;如果其中一个子命令被用户取消,父命令也应该被标成拒绝或回合被安全结束。它主要保护“复合命令里藏危险操作”的场景。

数据流:进去的是两个临时文件、一条类似“rm 第一个文件 && rm 第二个文件”的 shell 命令、开启 zsh-fork 的配置,以及支持 EXEC_WRAPPER 拦截的测试 zsh。测试先确认这个 zsh 能用 EXEC_WRAPPER(一种通过环境变量拦截要执行程序的机制)拦截执行动作,然后启动回合。它循环读取审批请求:父 shell 审批放行,第一个 rm 子命令放行,第二个 rm 子命令取消。出来的是一组验证:两个子命令审批都有独立 approval_id,分别指向两个文件;父命令最后被标成 Declined,或者某些 zsh/沙箱环境下回合直接完成或中断但仍处于可接受状态。

调用关系:这是本文件最复杂的测试。它调用 find_test_zsh_path 找 zsh,再用 supports_exec_wrapper_intercept 筛掉不支持拦截的 zsh,用 command_packaged_zsh_path 判断父命令是否通过打包 zsh 发起;环境仍由 create_config_toml 和 create_zsh_test_mcp_process 准备。测试过程不断响应 app-server 发来的审批请求,把不同决定喂回去,再根据通知判断拒绝是否从子命令传回父命令。

调用图:调用 6 个内部函数(command_packaged_zsh_path, create_config_toml, create_zsh_test_mcp_process, find_test_zsh_path, supports_exec_wrapper_intercept, sse);外部调用 22 个(from, default, new, new, Integer, create_mock_responses_server_sequence_unchecked, assert!, assert_eq!, assert_ne!, eprintln! (+12 more))。

create_zsh_test_mcp_process742–755 ↗
async fn create_zsh_test_mcp_process(
    codex_home: &Path,
    zdotdir: &Path,
    zsh_path: &Path,
) -> Result<TestAppServer>

作用:这个辅助函数专门启动一个用于 zsh-fork 测试的 app-server 进程。它会把测试用 zsh 放进假安装包,并设置 ZDOTDIR,让 zsh 的启动文件目录指向测试工作区,避免用户本机配置干扰测试。

数据流:进去的是 Codex 主目录、zsh 启动配置目录 zdotdir、以及测试 zsh 的路径。它先调用 create_test_package_app_server 准备一个假的 app-server 安装包,再把 zdotdir 转成字符串,作为环境变量 ZDOTDIR 传给 TestAppServer。出来的是一个已经启动、可以收发测试请求的 TestAppServer。

调用关系:四个主测试都会调用它来得到同样的受控服务进程。它自己把“准备安装包”的活交给 create_test_package_app_server,再把“真正拉起进程和设置环境变量”的活交给测试支持库的 new_with_program_and_env。

调用图:调用 2 个内部函数(new_with_program_and_env, create_test_package_app_server);被 4 处调用(turn_start_shell_zsh_fork_exec_approval_cancel_v2, turn_start_shell_zsh_fork_exec_approval_decline_v2, turn_start_shell_zsh_fork_executes_command_v2, turn_start_shell_zsh_fork_subcommand_decline_marks_parent_declined_v2);外部调用 2 个(as_str, to_string_lossy)。

create_test_package_app_server757–775 ↗
fn create_test_package_app_server(codex_home: &Path, zsh_path: &Path) -> Result<PathBuf>

作用:这个辅助函数搭一个假的 Codex 安装包目录,把 app-server 可执行文件和测试 zsh 复制进去。这样测试能验证 app-server 使用“随包带的 zsh”,而不是依赖开发机器上的随机路径。

数据流:进去的是 Codex 主目录和真实测试 zsh 的路径。它计算 test-package/bin、codex-resources/zsh/bin/zsh 等路径,创建目录,写一个空的 codex-package.json,然后复制 codex-app-server 和 zsh,并保留原文件权限。出来的是假安装包里 codex-app-server 的路径。

调用关系:create_zsh_test_mcp_process 会先调用它准备磁盘上的测试安装包。它内部用 packaged_zsh_path 统一计算 zsh 应放的位置,用 copy_with_permissions 完成复制和权限保留;如果计算出的 zsh 路径没有父目录,就直接报错,避免后面用坏路径启动。

调用图:调用 2 个内部函数(copy_with_permissions, packaged_zsh_path);被 1 处调用(create_zsh_test_mcp_process);外部调用 5 个(join, bail!, cargo_bin, create_dir_all, write)。

packaged_zsh_path777–784 ↗
fn packaged_zsh_path(codex_home: &Path) -> PathBuf

作用:这个小函数统一给出测试安装包里 zsh 应该放在哪里。统一路径很重要,否则复制 zsh 和检查命令字符串时可能各用各的路径,测试就会变得不可信。

数据流:进去的是 Codex 主目录路径。它在这个目录后面依次拼上 test-package、codex-resources、zsh、bin、zsh。出来的是一个 PathBuf,也就是 Rust 里可拥有的文件路径对象。

调用关系:create_test_package_app_server 用它决定把 zsh 复制到哪里;command_packaged_zsh_path 也用它得到同一个基础路径,再做规范化。它是多个辅助函数之间共享的“路径标准答案”。

调用图:被 2 处调用(command_packaged_zsh_path, create_test_package_app_server);外部调用 1 个(join)。

command_packaged_zsh_path786–789 ↗
fn command_packaged_zsh_path(codex_home: &Path) -> PathBuf

作用:这个函数给出命令日志里更可能出现的打包 zsh 路径。它会尽量把路径规范化,避免符号链接、相对路径等细节让字符串比较失败。

数据流:进去的是 Codex 主目录。它先通过 packaged_zsh_path 算出测试包里的 zsh 路径,再尝试 canonicalize(把路径解析成系统认可的真实绝对路径);如果解析失败,就退回原路径。出来的是用于断言命令字符串的 zsh 路径。

调用关系:子命令拦截测试用它判断某个审批请求是不是父 shell 命令发出的。它依赖 packaged_zsh_path 保持路径规则一致,同时把文件系统里的实际路径差异尽量抹平。

调用图:调用 1 个内部函数(packaged_zsh_path);被 1 处调用(turn_start_shell_zsh_fork_subcommand_decline_marks_parent_declined_v2);外部调用 1 个(canonicalize)。

copy_with_permissions791–794 ↗
fn copy_with_permissions(source: &Path, destination: &Path) -> std::io::Result<()>

作用:这个辅助函数复制文件时顺手保留原来的权限,比如“可执行”权限。没有它,复制出来的 app-server 或 zsh 可能只是普通文件,测试启动时就会失败。

数据流:进去的是源文件路径和目标文件路径。它先复制文件内容,再读取源文件权限,并把同样的权限设置到目标文件上。出来的是成功或失败的 I/O 结果;成功时磁盘上多了一个权限正确的目标文件。

调用关系:create_test_package_app_server 调用它两次:一次复制 codex-app-server,一次复制 zsh。它不参与测试协议本身,只负责把测试舞台上的“道具”复制好并确保能运行。

调用图:被 1 处调用(create_test_package_app_server);外部调用 3 个(copy, metadata, set_permissions)。

create_config_toml796–841 ↗
fn create_config_toml(
    codex_home: &Path,
    server_uri: &str,
    approval_policy: &str,
    feature_flags: &BTreeMap<Feature, bool>,
) -> std::io::Result<()>

作用:这个辅助函数为每个测试写一份 config.toml,告诉 app-server 使用假的模型服务器、指定审批策略,并打开或关闭需要的功能开关。没有这份配置,测试就无法稳定复现 zsh-fork 场景。

数据流:进去的是 Codex 主目录、假服务器地址、审批策略字符串,以及一组功能开关。它先默认关掉 RemoteModels,再合并传入的开关;然后把功能枚举转换成配置文件里的键名,拼成 TOML 文本(一种常见配置文件格式);最后写到 codex_home/config.toml。出来的是写文件的成功或失败结果,同时磁盘上生成了完整测试配置。

调用关系:四个主测试在启动 app-server 前都会调用它。它把测试想要的行为变成 app-server 启动时能读懂的配置,后续 create_zsh_test_mcp_process 启动的进程就会按这些设置连接假 responses 服务器并启用 zsh-fork。

调用图:被 4 处调用(turn_start_shell_zsh_fork_exec_approval_cancel_v2, turn_start_shell_zsh_fork_exec_approval_decline_v2, turn_start_shell_zsh_fork_executes_command_v2, turn_start_shell_zsh_fork_subcommand_decline_marks_parent_declined_v2);外部调用 4 个(from, join, format!, write)。

find_test_zsh_path843–861 ↗
fn find_test_zsh_path() -> Result<Option<std::path::PathBuf>>

作用:这个函数负责找到测试专用的 zsh 可执行文件。它先找仓库里的 DotSlash 文件;DotSlash 可以理解成一个“小指针文件”,第一次用时会下载或定位真正的大文件。

数据流:进去不需要业务参数,它从 cargo 工具那里拿仓库根目录,然后定位到 codex-rs/app-server/tests/suite/zsh。如果这个 DotSlash 文件不存在,就打印跳过原因并返回 None;如果存在,就尝试 fetch_dotslash_file 取到真正的 zsh 路径。出来的是 Some(path) 表示找到了可执行 zsh,或者 None 表示本次测试应跳过。

调用关系:四个主测试一开始都会调用它,因为没有测试 zsh 就没法验证 zsh-fork。它把“仓库里测试资源在哪里、DotSlash 怎么取文件”的细节藏起来,让测试主体只关心能不能拿到 zsh。

调用图:被 4 处调用(turn_start_shell_zsh_fork_exec_approval_cancel_v2, turn_start_shell_zsh_fork_exec_approval_decline_v2, turn_start_shell_zsh_fork_executes_command_v2, turn_start_shell_zsh_fork_subcommand_decline_marks_parent_declined_v2);外部调用 3 个(repo_root, fetch_dotslash_file, eprintln!)。

supports_exec_wrapper_intercept863–873 ↗
fn supports_exec_wrapper_intercept(zsh_path: &Path) -> bool

作用:这个函数检查某个 zsh 是否支持 EXEC_WRAPPER 拦截。只有支持这种拦截,子命令审批测试才能判断 zsh 是否真的拦到了 rm 这样的子命令。

数据流:进去的是 zsh 可执行文件路径。它启动这个 zsh,让它执行 /usr/bin/true,同时设置 EXEC_WRAPPER=/usr/bin/false;如果拦截生效,原本会成功的 true 会被 false 替代,进程状态就会失败。出来的是布尔值:true 表示这个 zsh 支持拦截,false 表示不支持或启动失败。

调用关系:只有 turn_start_shell_zsh_fork_subcommand_decline_marks_parent_declined_v2 会调用它。这个测试在正式开跑前用它做能力检查:不支持就跳过,避免把“环境不支持”误判成“功能坏了”。

调用图:被 1 处调用(turn_start_shell_zsh_fork_subcommand_decline_marks_parent_declined_v2);外部调用 1 个(new)。

扩展支持的工具执行

这些套件覆盖通过回合公开的内置或扩展支持工具行为,包括图像生成、sleep、网页搜索和模糊文件搜索会话。

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

这个测试文件像一套“彩排剧本”:它不连真正的图片服务,而是启动一个假的 HTTP 服务器,让 app-server 以为自己在和真实后端说话。测试会先写一份临时配置,打开 imagegenext 功能,再用测试客户端启动线程和一轮对话。然后假的模型会要求调用 image_gen 工具,测试检查 app-server 是否把图片生成请求发到正确地址,是否把返回的 base64 图片保存成文件,是否把图片和保存路径提示正确交回模型。它还覆盖失败情况、图片编辑情况,以及“只在代码模式里暴露工具”的情况。这里的 JSON-RPC 可以理解成一种“按编号寄信和收回信”的通信格式;SSE 是服务器持续推送消息的一种方式,像一条不断吐出事件的流水线。

函数细节13
standalone_image_generation_returns_saved_path_hint_to_model53–148 ↗
async fn standalone_image_generation_returns_saved_path_hint_to_model() -> Result<()>

作用:测试普通图片生成成功时,系统不仅能拿到图片,还会把图片保存到本地,并把保存路径作为提示交给模型。这样模型后续能知道图片文件在哪里,而不是只看到一串图片数据。

数据流:进去的是一个假的图片后端、一个假的模型事件序列、临时配置和登录凭据 → 测试启动 app-server,发起“生成图片”的对话,等待图片生成完成 → 出来的是一个完成状态的 ImageGeneration 项目;测试还读取保存路径里的文件,确认内容是预期的图片数据,并检查第二次模型请求里包含图片输入和路径提示,同时没有旧版的 developer 提示。

调用关系:这是一个完整端到端测试。它先让 mount_image_response 准备图片接口,再用 create_config_toml 写配置,用 start_image_generation_turn 发起对话,用 wait_for_image_generation_completed 等图片完成,最后检查 responses::mount_sse_sequence 记录到的模型请求是否正确。

调用图:调用 8 个内部函数(new, new_with_env, create_config_toml, mount_image_response, start_image_generation_turn, wait_for_image_generation_completed, mount_sse_sequence, start_mock_server);外部调用 7 个(new, write_chatgpt_auth, assert!, assert_eq!, panic!, timeout, vec!)。

standalone_image_generation_failure_emits_terminal_item151–226 ↗
async fn standalone_image_generation_failure_emits_terminal_item() -> Result<()>

作用:测试图片生成接口失败时,系统会明确产出一个“失败”的图片项目,而不是卡住、假装成功,或让前端不知道发生了什么。

数据流:进去的是一个会返回 500 错误的假图片接口、一个假的模型调用流程、临时配置和认证信息 → 测试启动服务器并请求生成图片 → 出来的是一个状态为 failed 的 ImageGeneration 项目,里面没有图片结果和保存路径;随后测试还确认系统把“图片生成失败”的说明作为工具输出发回了模型。

调用关系:这个测试和成功路径很像,但它不用 mount_image_response,而是直接用 wiremock 配出失败响应。它仍然通过 create_config_toml、start_image_generation_turn 和 wait_for_image_generation_completed 串起完整流程,用来保证错误路径也会正常收尾。

调用图:调用 7 个内部函数(new, new_with_env, create_config_toml, start_image_generation_turn, wait_for_image_generation_completed, mount_sse_sequence, start_mock_server);外部调用 10 个(given, new, new, write_chatgpt_auth, assert!, assert_eq!, timeout, vec!, method, path)。

standalone_image_edit_uses_attached_model_visible_image229–255 ↗
async fn standalone_image_edit_uses_attached_model_visible_image() -> Result<()>

作用:测试图片编辑时,如果用户附上了本地图片文件,系统会把这张图片转成模型能看到的 data URL,再发给图片编辑接口。

数据流:进去的是一个闭包,这个闭包会在临时目录写入一张很小的 PNG,并构造“编辑这张图”的用户输入和工具参数 → run_image_edit_test 跑完整编辑流程并抓取发给假图片编辑接口的请求 → 出来的是请求 JSON;测试确认 prompt 没变,并确认 images[0].image_url 是那张本地 PNG 转成的 base64 data URL。

调用关系:这个测试把大部分共同流程交给 run_image_edit_test。它只负责准备“本地附件图片”这种场景,并在流程结束后检查图片编辑请求里的图片来源是否正确。

调用图:调用 1 个内部函数(run_image_edit_test);外部调用 1 个(assert_eq!)。

standalone_image_edit_uses_recent_pathless_image258–283 ↗
async fn standalone_image_edit_uses_recent_pathless_image() -> Result<()>

作用:测试图片编辑时,即使图片不是本地路径,而是最近对话里出现过的网络图片 URL,系统也能把它当作参考图发给编辑接口。

数据流:进去的是一个网络图片地址和一组用户输入,其中图片没有本地文件路径 → run_image_edit_test 启动假后端、发起对话、等待图片编辑完成,并返回实际发出的编辑请求 → 出来的是请求 JSON;测试确认 prompt 正确,并确认 images[0].image_url 保留为原来的网络地址。

调用关系:它复用 run_image_edit_test 这条公共测试流水线。和本地图片测试不同,它验证的是“最近图片、没有本地路径”的输入分支,确保图片编辑不只依赖文件系统。

调用图:调用 1 个内部函数(run_image_edit_test);外部调用 1 个(assert_eq!)。

standalone_image_generation_is_exposed_in_code_mode_only286–326 ↗
async fn standalone_image_generation_is_exposed_in_code_mode_only() -> Result<()>

作用:测试当图片生成扩展被设置为“只在代码模式使用”时,工具仍然会被暴露给模型,只是暴露形式适合代码工具调用。

数据流:进去的是一个设置了 code_mode_only 的临时配置、假的模型响应和认证信息 → 测试启动 app-server,发起一次普通对话,并等到回合完成 → 出来的是假模型服务收到的一次请求;测试检查请求体里确实包含 image_gen__imagegen 这个工具名。

调用关系:这个测试重点不在真正生成图片,所以只用 mount_sse_once 返回普通完成消息。它依靠 create_config_toml 写出 code_mode_only 配置,再用 start_image_generation_turn 触发一次请求,最后检查工具是否出现在发给模型的请求里。

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

standalone_image_generation_is_callable_from_code_mode_only330–402 ↗
async fn standalone_image_generation_is_callable_from_code_mode_only() -> Result<()>

作用:测试在“只允许代码模式调用”的配置下,模型写代码调用图片生成工具时,系统真的会执行图片生成,并把结果按代码工具输出的格式返回。

数据流:进去的是假的图片生成接口、假的模型事件序列,其中模型发出一段调用 tools.image_gen__imagegen 的代码 → 测试启动 app-server,发起对话并等回合完成 → 出来的是两次模型请求记录;测试确认第一次请求暴露了工具,第二次请求里包含生成的图片输入、保存路径提示,并且输出项数量符合预期。

调用关系:这个测试把 mount_image_response、create_config_toml 和 start_image_generation_turn 串起来,模拟代码模式工具调用的完整闭环。它和普通直连模式测试互补,保证同一个图片能力在代码执行路径里也能工作。

调用图:调用 7 个内部函数(new, new_with_env, create_config_toml, mount_image_response, start_image_generation_turn, mount_sse_sequence, start_mock_server);外部调用 6 个(new, write_chatgpt_auth, assert!, assert_eq!, timeout, vec!)。

start_image_generation_turn404–413 ↗
async fn start_image_generation_turn(mcp: &mut TestAppServer) -> Result<()>

作用:这是一个小帮手,用固定的用户文字“Generate an image”开始一轮对话。测试里多次需要触发图片生成,用它可以少写重复代码。

数据流:进去的是一个已经启动的 TestAppServer 测试客户端 → 它包装出一条文本用户输入,并交给 start_turn 去创建线程和启动回合 → 出来没有额外数据;如果中途通信失败,就把错误返回给调用者。

调用关系:它被多个图片生成相关测试调用。自己不直接和服务器细聊,而是把真正的线程创建、回合启动工作交给 start_turn。

调用图:调用 1 个内部函数(start_turn);被 4 处调用(standalone_image_generation_failure_emits_terminal_item, standalone_image_generation_is_callable_from_code_mode_only, standalone_image_generation_is_exposed_in_code_mode_only, standalone_image_generation_returns_saved_path_hint_to_model);外部调用 1 个(vec!)。

run_image_edit_test415–477 ↗
async fn run_image_edit_test(
    input: impl FnOnce(&Path) -> Result<(serde_json::Value, Vec<V2UserInput>)>,
) -> Result<serde_json::Value>

作用:这是图片编辑测试的公共流水线。不同测试只要告诉它“要传什么图片和参数”,它就负责搭好假后端、启动 app-server、跑完整流程,并把实际发出的编辑请求拿回来。

数据流:进去的是一个闭包,闭包会根据临时目录生成工具参数和用户输入 → 函数启动假服务器,挂上图片编辑接口,写配置和认证,启动 app-server,发起对话,等待图片生成项目完成,再从假服务器收集收到的 HTTP 请求 → 出来的是发往 /api/codex/images/edits 的 JSON 请求体。

调用关系:它被两个图片编辑测试复用:一个测试本地附件图片,一个测试最近出现的网络图片。内部会调用 mount_image_edit_response 准备假编辑接口,调用 start_turn 发起对话,调用 wait_for_image_generation_completed 等待结果。

调用图:调用 8 个内部函数(new, new_with_env, create_config_toml, mount_image_edit_response, start_turn, wait_for_image_generation_completed, mount_sse_sequence, start_mock_server);被 2 处调用(standalone_image_edit_uses_attached_model_visible_image, standalone_image_edit_uses_recent_pathless_image);外部调用 5 个(new, write_chatgpt_auth, assert_eq!, timeout, vec!)。

start_turn479–506 ↗
async fn start_turn(mcp: &mut TestAppServer, input: Vec<V2UserInput>) -> Result<()>

作用:这个函数负责用测试客户端正式开始一次对话回合。可以把它理解成先开一个聊天窗口,再把用户消息发进去。

数据流:进去的是 TestAppServer 和一组用户输入 → 它先发送 thread/start 请求创建线程,等待对应编号的回复,再发送 turn/start 请求启动一轮对话,并等待启动成功的回复 → 出来没有业务结果,只表示这一轮已经成功开始;如果请求或解析失败,就返回错误。

调用关系:它是多个测试辅助函数的底层步骤。start_image_generation_turn 用它发固定文本,run_image_edit_test 用它发带图片的输入;它直接调用测试客户端的 send_thread_start_request、send_turn_start_request 和 read_stream_until_response_message。

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

wait_for_image_generation_completed508–524 ↗
async fn wait_for_image_generation_completed(
    mcp: &mut TestAppServer,
) -> Result<ItemCompletedNotification>

作用:这个函数一直读服务器推送的完成通知,直到看到“图片生成项目完成”为止。它避免测试误把普通消息完成当成图片生成完成。

数据流:进去的是 TestAppServer 测试客户端 → 它循环读取 item/completed 通知,把通知参数解析成 ItemCompletedNotification,并检查里面的项目类型 → 出来的是第一个 ImageGeneration 完成通知;如果读流或解析失败,就返回错误。

调用关系:它被成功、失败和图片编辑测试用来等待关键节点。它不触发任何新操作,只负责在 app-server 的通知流里筛出测试真正关心的图片生成结果。

调用图:调用 1 个内部函数(read_stream_until_notification_message);被 3 处调用(run_image_edit_test, standalone_image_generation_failure_emits_terminal_item, standalone_image_generation_returns_saved_path_hint_to_model);外部调用 2 个(matches!, from_value)。

mount_image_response526–536 ↗
async fn mount_image_response(server: &MockServer)

作用:这个函数在假 HTTP 服务器上注册一个“图片生成成功”的接口。真实系统访问 /api/codex/images/generations 时,会收到固定的假图片结果。

数据流:进去的是一个 MockServer 假服务器 → 它设置规则:收到 POST /api/codex/images/generations,就返回 200 和包含 b64_json 的 JSON,并要求这个接口被调用一次 → 出来没有返回值,但假服务器多了这个可用的模拟接口。

调用关系:它被图片生成成功测试和代码模式可调用测试使用。这样测试不用依赖真实图片服务,也能稳定检查 app-server 发请求和处理结果的行为。

调用图:被 2 处调用(standalone_image_generation_is_callable_from_code_mode_only, standalone_image_generation_returns_saved_path_hint_to_model);外部调用 5 个(given, new, json!, method, path)。

mount_image_edit_response538–548 ↗
async fn mount_image_edit_response(server: &MockServer)

作用:这个函数在假 HTTP 服务器上注册一个“图片编辑成功”的接口。它让图片编辑测试可以专心检查请求内容,而不用真的生成图片。

数据流:进去的是一个 MockServer 假服务器 → 它设置规则:收到 POST /api/codex/images/edits,就返回 200 和固定的 base64 图片结果,并要求调用一次 → 出来没有返回值,但假服务器开始能响应图片编辑请求。

调用关系:它只被 run_image_edit_test 调用,是图片编辑公共测试流程的一部分。run_image_edit_test 后面会再从假服务器取出收到的请求,检查图片和 prompt 是否传对。

调用图:被 1 处调用(run_image_edit_test);外部调用 5 个(given, new, json!, method, path)。

create_config_toml550–584 ↗
fn create_config_toml(
    codex_home: &Path,
    server_uri: &str,
    mode: ImagegenTestMode,
) -> std::io::Result<()>

作用:这个函数给每个测试写一份临时 config.toml 配置文件,打开图片生成扩展,并把模型服务地址指向假服务器。

数据流:进去的是临时的 codex_home 目录、假服务器地址,以及测试模式 Direct 或 CodeModeOnly → 它根据模式决定是否写入 code_mode_only = true,然后拼出完整 TOML 配置文本并写到 config.toml → 出来是文件写入结果;成功后 app-server 启动时会读取这份配置。

调用关系:几乎所有测试都会先调用它,因为没有这份配置,app-server 就不知道要使用假模型提供方、假 base_url、响应式 API,以及是否启用 imagegenext 功能。

调用图:被 5 处调用(run_image_edit_test, standalone_image_generation_failure_emits_terminal_item, standalone_image_generation_is_callable_from_code_mode_only, standalone_image_generation_is_exposed_in_code_mode_only, standalone_image_generation_returns_saved_path_hint_to_model);外部调用 3 个(join, format!, write)。

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

这个测试模拟了一次完整对话:用户让系统“睡一小会儿”,假服务器返回一个 sleep 函数调用,然后真实的测试版应用服务器要把这个调用变成线程里的一个项目,并通知外界它已经开始、之后又完成。这里的 JSON-RPC 可以理解成一种“用 JSON 格式来发请求和收消息的通信规矩”。测试会先搭一个假的模型服务,再写一份临时配置,让应用服务器连接这个假服务,并打开 sleep_tool 功能。接着它创建线程、启动一轮对话,持续读取服务器发出的消息,直到看到 item/started 和 item/completed。最后它核对:通知里的项目确实是 Sleep,编号和睡眠时长没错,所属线程和轮次没错,而且完成时间不早于开始时间。没有这个测试,sleep 工具可能已经执行了,但界面或客户端却不知道它何时开始、何时结束。

函数细节2
sleep_emits_started_and_completed_items25–150 ↗
async fn sleep_emits_started_and_completed_items() -> Result<()>

作用:这是主测试函数,用来验证一次 sleep 工具调用会产生正确的“开始”和“完成”通知。有人改 sleep 工具、通知协议或对话流程时,这个测试能及时发现它们有没有被改坏。

数据流:进去的是测试里写死的 sleep 调用编号和时长,以及一个临时创建的假模型服务器地址。它先启动假服务器,安排假服务器按顺序吐出“创建响应、调用 sleep、完成响应、助手回复完成”等事件;再写配置文件,启动测试应用服务器;然后发起线程和对话,读取 JSON-RPC 消息流,从里面筛出 Sleep 项目的 item/started 和 item/completed。出来的结果不是业务数据,而是断言:如果通知内容、线程编号、轮次编号、时间顺序都对,测试通过;任何一步不符合预期,测试失败。

调用关系:它是这个文件的核心入口,由测试框架 tokio 在跑测试时自动执行。它会调用 responses::start_mock_server 和 responses::mount_sse_sequence 准备假的上游模型服务,调用 create_config_toml 写临时配置,借助 TestAppServer 启动并操作应用服务器,还用 to_response 把通用 JSON-RPC 响应转成更具体的线程和轮次响应。整个流程像一次彩排:假模型负责出题,测试服务器负责演出,这个函数负责坐在观众席检查每个关键动作有没有按剧本发生。

调用图:调用 4 个内部函数(new, create_config_toml, mount_sse_sequence, start_mock_server);外部调用 10 个(default, new, Integer, to_response, assert!, assert_eq!, matches!, from_value, timeout, vec!)。

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

作用:这个小工具函数给测试写一份临时配置文件,让应用服务器知道该连哪个假的模型服务,并且打开 sleep_tool 功能。没有它,测试服务器可能会读不到模型配置,或者根本不会启用 sleep 工具。

数据流:进去的是一个临时目录路径 codex_home,以及假模型服务器的地址 server_uri。它把这些信息拼成 config.toml 的内容,里面包括模型名、审批策略、沙箱模式、模型提供方地址、重试次数,以及 features.sleep_tool = true。出来的是一次文件写入结果:成功时临时目录下多了 config.toml;失败时返回文件系统错误。

调用关系:它只被 sleep_emits_started_and_completed_items 调用,发生在测试应用服务器启动之前。主测试先准备假服务器地址,再把地址交给这个函数写进配置;之后 TestAppServer 才根据这份配置启动。也就是说,它像是测试开场前贴在后台墙上的“演出说明书”,告诉服务器这场测试该用假模型、别重试,并且允许使用 sleep 工具。

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

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

这个测试文件像一次完整彩排:先搭一个假的外部服务器,假装它既是模型接口,又是网页搜索接口;再启动 app-server,让它以为自己正在和真实服务通信。测试会让用户发一句“Search the web”,然后模拟模型要求调用 web.run 搜索工具。接着它检查几件关键事:系统有没有把“托管网页搜索”换成项目自己的“独立网页搜索”;发给搜索接口的请求内容对不对;搜索结果有没有作为函数调用结果送回模型;客户端有没有收到“网页搜索开始”和“网页搜索完成”的通知。最后它还重启服务器,再读回线程记录,确认这次网页搜索被正确保存。没有这个测试,网页搜索可能表面能用,但请求格式、通知、持久化其中任一环坏了都不容易发现。

函数细节7
standalone_web_search_round_trips_output44–221 ↗
async fn standalone_web_search_round_trips_output() -> Result<()>

作用:这是主测试,完整验证一次独立网页搜索从发起、执行、回传结果到保存历史的全过程。有人改网页搜索、模型工具调用或线程记录时,这个测试能及时发现是否把流程弄坏了。

数据流:进去的是测试代码自己准备的一套假环境:临时配置目录、假的模型服务器、假的搜索接口和一条用户输入。它启动测试版 app-server,创建线程,发送一轮对话,等待网页搜索开始和完成通知,然后检查外部请求、搜索请求体、模型收到的函数输出,以及重启后读回来的历史记录。出来的是测试通过或失败;过程中它会写入临时 config.toml 和认证文件,也会让假服务器记录收到的请求。

调用关系:它是这个文件的总导演。它先调用 create_config_toml 写配置,调用 mount_search_response 准备假的搜索返回,借助 mount_sse_sequence 准备假的模型事件流,再用 wait_for_web_search_started 和 wait_for_web_search_completed 等待服务器发出的通知,最后用 search_request_body 把假搜索接口收到的请求捞出来核对。has_hosted_web_search 也在这里被用来确认旧式托管搜索没有混进请求里。

调用图:调用 9 个内部函数(new, new_with_env, create_config_toml, mount_search_response, search_request_body, wait_for_web_search_completed, wait_for_web_search_started, mount_sse_sequence, start_mock_server);外部调用 9 个(default, new, Integer, default, write_chatgpt_auth, assert!, assert_eq!, timeout, vec!)。

wait_for_web_search_started223–237 ↗
async fn wait_for_web_search_started(mcp: &mut TestAppServer) -> Result<ItemStartedNotification>

作用:这个函数专门等“网页搜索开始”的通知。因为服务器会发很多通知,它会一直读,直到看到真正属于网页搜索的那一条。

数据流:进去的是一个正在运行的 TestAppServer,也就是测试里控制 app-server 的对象。它不断读取名为 item/started 的通知,把通知里的 JSON 参数转成 ItemStartedNotification,然后检查里面的 item 是否是 WebSearch。找到后返回这条开始通知;如果读消息或解析失败,就返回错误。

调用关系:standalone_web_search_round_trips_output 在发起对话后调用它,用来确认服务器已经把网页搜索当成一个线程项目启动了。它自己把具体读消息的活交给 read_stream_until_notification_message,再用 from_value 把原始 JSON 变成测试能比较的结构。

调用图:调用 1 个内部函数(read_stream_until_notification_message);被 1 处调用(standalone_web_search_round_trips_output);外部调用 2 个(matches!, from_value)。

wait_for_web_search_completed239–255 ↗
async fn wait_for_web_search_completed(
    mcp: &mut TestAppServer,
) -> Result<ItemCompletedNotification>

作用:这个函数专门等“网页搜索完成”的通知。它确保测试拿到的是网页搜索完成后的最终内容,而不是别的普通项目完成事件。

数据流:进去的是同一个 TestAppServer。它循环读取 item/completed 通知,把每条通知的参数解析成 ItemCompletedNotification,并判断完成的项目是不是 WebSearch。等到目标通知后返回它;如果没有参数、解析失败或读取失败,就把错误传出去。

调用关系:standalone_web_search_round_trips_output 在确认搜索开始后调用它,用来拿到搜索完成时的最终 query 和 action。它依赖 read_stream_until_notification_message 取通知,依赖 from_value 把 JSON 通知变成强类型数据,最后把结果交回主测试做断言。

调用图:调用 1 个内部函数(read_stream_until_notification_message);被 1 处调用(standalone_web_search_round_trips_output);外部调用 2 个(matches!, from_value)。

mount_search_response257–267 ↗
async fn mount_search_response(server: &MockServer)

作用:这个函数给假的服务器装上一个搜索接口。只要测试中的 app-server 向 /api/codex/alpha/search 发 POST 请求,它就返回固定的“Search result”。

数据流:进去的是 MockServer,也就是测试用的假 HTTP 服务器。它注册一条规则:匹配 POST 方法和 /api/codex/alpha/search 路径,返回状态码 200 和一段 JSON,里面有 encrypted_output 和 output。出来没有业务结果,但假服务器被改好了,之后能接住并记录一次搜索请求。

调用关系:standalone_web_search_round_trips_output 在启动 app-server 前调用它,先把搜索接口准备好。它使用 wiremock 的 given、method、path 和 ResponseTemplate 这些测试工具,把“收到什么请求就回什么响应”的规则挂到服务器上。

调用图:被 1 处调用(standalone_web_search_round_trips_output);外部调用 5 个(given, new, json!, method, path)。

search_request_body279–289 ↗
async fn search_request_body(server: &MockServer) -> Result<Value>

作用:这个函数从假的服务器记录里找出真正发给搜索接口的那一次请求,并取出它的 JSON 内容。主测试用它来核对 app-server 发出去的搜索请求是否正确。

数据流:进去的是 MockServer。它读取这个假服务器收到过的所有请求,找到路径为 /api/codex/alpha/search 的那一条,然后把请求体解析成 JSON 值返回。找不到请求、读取失败或请求体不是 JSON 时,就返回带说明的错误。

调用关系:standalone_web_search_round_trips_output 在搜索完成后调用它,检查 model、commands、allowed_callers 和 input 等字段。它把底层的 received_requests 结果整理成主测试最关心的一份请求体。

调用图:被 1 处调用(standalone_web_search_round_trips_output);外部调用 1 个(received_requests)。

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

作用:这个函数在临时目录里写一份测试专用配置文件。配置会打开 standalone_web_search,也会把模型服务地址指向假的服务器,避免测试真的访问外网。

数据流:进去的是 codex_home 路径和假服务器地址 server_uri。它拼出 config.toml 的完整路径,写入一段 TOML 配置文本,里面设置模型名、审批策略、沙箱模式、ChatGPT 基础地址、自定义模型提供方,以及独立网页搜索开关。出来是写文件成功或失败的结果;成功后临时目录里会多出这份配置文件。

调用关系:standalone_web_search_round_trips_output 在启动 TestAppServer 前调用它,因为服务器启动时会读取配置。它只负责落盘配置,不启动服务;后面的 new_with_env 会基于这份配置把 app-server 拉起来。

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

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

模糊文件搜索,就是用户只输入几个不完整的字母,也能找到匹配的文件,比如输入“alp”找到“alpha.txt”。这个测试文件会临时搭一个干净的服务器环境,再临时创建一些文件,然后像真实客户端一样给服务器发请求,看返回结果是否正确。它既测一次性搜索,也测“搜索会话”:用户一边输入,服务器一边推送更新。文件里有一些辅助函数负责写测试配置、启动服务器、等待指定通知、确认不会再收到通知。重要的是,它不只看“能不能搜到”,还检查排序、匹配位置、大小写不敏感、取消令牌能被接受、会话停止后不再乱发消息、两个会话互不串线等细节。这些测试能防止搜索体验在改代码时悄悄变坏。

函数细节18
create_config_toml32–45 ↗
fn create_config_toml(codex_home: &Path) -> std::io::Result<()>

作用:给临时的 Codex 主目录写一份最小配置文件,让测试服务器能用固定、安全的方式启动。没有这份配置,后面的测试可能会受真实用户环境影响,结果就不稳定。

数据流:输入是一个临时目录路径 → 它在里面拼出 config.toml 文件名,并写入模型名、审批策略、沙箱模式和关闭 shell_snapshot 的配置 → 输出是写文件成功或失败的结果,同时磁盘上多了一份测试专用配置。

调用关系:它是很多测试的准备步骤。initialized_mcp 会先调用它再启动服务器;两个一次性搜索测试也直接调用它,保证 TestAppServer 启动时读到的是这份可控配置。

调用图:被 3 处调用(initialized_mcp, test_fuzzy_file_search_accepts_cancellation_token, test_fuzzy_file_search_sorts_and_includes_indices);外部调用 2 个(join, write)。

initialized_mcp47–52 ↗
async fn initialized_mcp(codex_home: &TempDir) -> Result<TestAppServer>

作用:一键准备好已经初始化完成的测试服务器。测试用它可以少写重复代码,直接拿到一个能收发请求的服务器连接。

数据流:输入是临时 Codex 主目录 → 它先写配置,再创建 TestAppServer,并等待 initialize 初始化响应,等待时间有上限 → 输出是初始化好的 TestAppServer;如果启动或初始化超时,就返回错误。

调用关系:大多数会话类测试都会先用它搭好服务器。它把具体启动细节交给 create_config_toml、TestAppServer::new 和 initialize,自己负责把这些步骤串起来。

调用图:调用 2 个内部函数(new, create_config_toml);被 10 处调用(test_fuzzy_file_search_query_cleared_sends_blank_snapshot, test_fuzzy_file_search_session_multiple_query_updates_work, test_fuzzy_file_search_session_no_updates_after_complete_until_query_edited, test_fuzzy_file_search_session_stops_sending_updates_after_stop, test_fuzzy_file_search_session_streams_updates, test_fuzzy_file_search_session_update_after_stop_fails, test_fuzzy_file_search_session_update_before_start_errors, test_fuzzy_file_search_session_update_is_case_insensitive, test_fuzzy_file_search_session_update_works_without_waiting_for_start_response, test_fuzzy_file_search_two_sessions_are_independent);外部调用 2 个(path, timeout)。

wait_for_session_updated54–99 ↗
async fn wait_for_session_updated(
    mcp: &mut TestAppServer,
    session_id: &str,
    query: &str,
    file_expectation: FileExpectation,
) -> Result<FuzzyFileSearchSessionUpdatedNotification>

作用:等待服务器发来某个搜索会话的“结果更新”通知,并确认这个通知属于指定会话、指定查询词,文件列表是否符合预期。它避免测试误把别的通知当成目标结果。

数据流:输入是服务器连接、会话 id、查询词,以及期望文件列表是任意、空、还是非空 → 它在规定时间内不断读通知,只接受方法名正确、参数能解析、会话和查询词匹配、文件数量符合要求的那一条 → 输出解析后的更新通知;如果等不到,就带着当前缓存的通知方法报错。

调用关系:许多会话测试在发出 update 请求后都会调用它来确认服务器真的推送了新结果。它底层把等待工作交给 TestAppServer 的 read_stream_until_matching_notification,并用 timeout 防止测试无限卡住。

调用图:调用 1 个内部函数(read_stream_until_matching_notification);被 8 处调用(test_fuzzy_file_search_query_cleared_sends_blank_snapshot, test_fuzzy_file_search_session_multiple_query_updates_work, test_fuzzy_file_search_session_no_updates_after_complete_until_query_edited, test_fuzzy_file_search_session_stops_sending_updates_after_stop, test_fuzzy_file_search_session_streams_updates, test_fuzzy_file_search_session_update_is_case_insensitive, test_fuzzy_file_search_session_update_works_without_waiting_for_start_response, test_fuzzy_file_search_two_sessions_are_independent);外部调用 3 个(bail!, format!, timeout)。

wait_for_session_completed101–140 ↗
async fn wait_for_session_completed(
    mcp: &mut TestAppServer,
    session_id: &str,
) -> Result<FuzzyFileSearchSessionCompletedNotification>

作用:等待某个搜索会话发出“本轮搜索完成”的通知。它用来确认服务器不只是发了中间结果,还把这一轮搜索收尾了。

数据流:输入是服务器连接和会话 id → 它在超时时间内筛选通知,只接受 fuzzyFileSearch/sessionCompleted 且 session_id 对得上的消息 → 输出解析后的完成通知;如果没有等到,就报超时错误并附带已缓存通知信息。

调用关系:需要验证搜索生命周期的测试会调用它,比如确认一次搜索更新后会结束、下一次修改查询词后又能重新完成。它和 wait_for_session_updated 搭配使用,一个看更新,一个看完成。

调用图:调用 1 个内部函数(read_stream_until_matching_notification);被 3 处调用(test_fuzzy_file_search_session_multiple_query_updates_work, test_fuzzy_file_search_session_no_updates_after_complete_until_query_edited, test_fuzzy_file_search_session_streams_updates);外部调用 3 个(bail!, format!, timeout)。

assert_update_request_fails_for_missing_session142–161 ↗
async fn assert_update_request_fails_for_missing_session(
    mcp: &mut TestAppServer,
    session_id: &str,
    query: &str,
) -> Result<()>

作用:确认给不存在的搜索会话发送更新请求时,服务器会明确报错。这样可以防止服务器悄悄忽略错误,或把请求错发到别的地方。

数据流:输入是服务器连接、一个不存在或已停止的会话 id、查询词 → 它发送会话更新请求,然后等待对应请求 id 的错误响应 → 输出为空表示断言通过;过程中会检查错误码是 -32600,错误消息也必须说明找不到该会话。

调用关系:会话未开始就更新、会话停止后再更新这两个测试会用它。它把发请求交给 send_fuzzy_file_search_session_update_request,把读错误响应交给 read_stream_until_error_message。

调用图:调用 2 个内部函数(read_stream_until_error_message, send_fuzzy_file_search_session_update_request);被 2 处调用(test_fuzzy_file_search_session_update_after_stop_fails, test_fuzzy_file_search_session_update_before_start_errors);外部调用 3 个(Integer, assert_eq!, timeout)。

assert_no_session_updates_for163–215 ↗
async fn assert_no_session_updates_for(
    mcp: &mut TestAppServer,
    session_id: &str,
    grace_period: std::time::Duration,
    duration: std::time::Duration,
) -> Result<()>

作用:确认某个会话在一段时间内不会再收到搜索更新。它主要用来检查停止会话或搜索完成后的“别再发了”这个约定。

数据流:输入是服务器连接、会话 id、一个宽限期和一个正式观察时长 → 它先在宽限期内把可能已经在路上的旧通知读掉,然后在观察期继续监听更新通知 → 如果观察期内没有该会话的新更新就成功;如果收到同一 session_id 的更新,就报错。

调用关系:停止会话后不应继续推送、完成后查询未改不应重复推送,这些测试会调用它。它依赖 TestAppServer 读取指定方法名的通知,并用 timeout 控制等待边界。

调用图:调用 1 个内部函数(read_stream_until_notification_message);被 2 处调用(test_fuzzy_file_search_session_no_updates_after_complete_until_query_edited, test_fuzzy_file_search_session_stops_sending_updates_after_stop);外部调用 3 个(bail!, now, timeout)。

test_fuzzy_file_search_sorts_and_includes_indices218–295 ↗
async fn test_fuzzy_file_search_sorts_and_includes_indices() -> Result<()>

作用:测试一次性模糊搜索的核心结果:返回哪些文件、顺序是否正确、每个匹配字符的位置是否给出来。它保证前端能高亮匹配字符,也能按合理顺序展示结果。

数据流:它创建临时配置和一批专门设计的文件名 → 启动服务器后发送查询“abe”的搜索请求 → 读取 JSON-RPC 响应,并把 files 列表、分数、路径、匹配位置 indices 与预期 JSON 完整比较。

调用关系:这是一次性搜索路径的验收测试。它直接使用 create_config_toml 和 TestAppServer,而不是会话辅助函数,因为它只关心普通请求的一次响应。

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

test_fuzzy_file_search_accepts_cancellation_token298–344 ↗
async fn test_fuzzy_file_search_accepts_cancellation_token() -> Result<()>

作用:测试模糊搜索请求可以带 cancellation token,也就是“取消令牌”:客户端用来标记或取消某个请求的字符串。这里重点确认带上这个字段不会让搜索失败。

数据流:它创建一个 alpha.txt 文件并启动服务器 → 先发一个搜索请求拿到请求 id,再用这个 id 字符串作为 cancellation token 发第二个请求 → 读取第二个响应,确认结果里正好有 alpha.txt。

调用关系:这个测试直接走一次性搜索接口。它调用 create_config_toml 准备环境,然后通过 TestAppServer 发两次 fuzzy file search 请求并读取第二次响应。

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

test_fuzzy_file_search_session_streams_updates347–372 ↗
async fn test_fuzzy_file_search_session_streams_updates() -> Result<()>

作用:测试搜索会话能按预期推送结果:开始会话、更新查询词后,服务器会发结果更新,随后发完成通知。

数据流:它创建 alpha.txt,启动已初始化服务器 → 开始一个会话并把查询词更新为“alp” → 等待非空更新,检查返回文件的 root 和 path,再等待完成通知,最后停止会话。

调用关系:这是会话流式搜索的基础测试。它通过 initialized_mcp 启动环境,用 wait_for_session_updated 和 wait_for_session_completed 验证服务器推送的两类通知。

调用图:调用 3 个内部函数(initialized_mcp, wait_for_session_completed, wait_for_session_updated);外部调用 4 个(new, assert_eq!, write, vec!)。

test_fuzzy_file_search_session_update_is_case_insensitive375–396 ↗
async fn test_fuzzy_file_search_session_update_is_case_insensitive() -> Result<()>

作用:测试搜索会话里的查询不区分大小写。用户输入“ALP”时,也应该能找到小写文件名 alpha.txt。

数据流:它创建 alpha.txt 并启动服务器 → 开始会话后发送大写查询“ALP” → 等待非空更新,并确认结果就是 alpha.txt。

调用关系:它复用 initialized_mcp 和 wait_for_session_updated,只专注验证大小写处理。这个测试保护的是用户输入体验,避免搜索变成大小写严格匹配。

调用图:调用 2 个内部函数(initialized_mcp, wait_for_session_updated);外部调用 4 个(new, assert_eq!, write, vec!)。

test_fuzzy_file_search_session_no_updates_after_complete_until_query_edited399–423 ↗
async fn test_fuzzy_file_search_session_no_updates_after_complete_until_query_edited() -> Result<()>

作用:测试一轮搜索完成后,如果用户没有改查询词,服务器不会反复推送同样的更新;但查询词一变,又应该重新搜索并推送。

数据流:它创建文件并启动会话 → 查询“alp”,等待更新和完成 → 观察一小段时间确认没有同会话的新更新 → 再把查询改成“alpha”,确认又收到新的非空更新。

调用关系:它把 wait_for_session_updated、wait_for_session_completed 和 assert_no_session_updates_for 串起来,验证“完成后安静、修改后重新工作”的状态规则。

调用图:调用 4 个内部函数(assert_no_session_updates_for, initialized_mcp, wait_for_session_completed, wait_for_session_updated);外部调用 3 个(new, write, vec!)。

test_fuzzy_file_search_session_update_before_start_errors426–432 ↗
async fn test_fuzzy_file_search_session_update_before_start_errors() -> Result<()>

作用:测试还没开始会话就发送更新请求时,服务器必须报错。这样可以防止客户端调用顺序错了却看不出来。

数据流:它启动已初始化服务器,但不创建任何搜索会话 → 对名为 missing 的会话发送更新请求 → 通过辅助断言确认服务器返回“会话找不到”的错误。

调用关系:它主要调用 initialized_mcp 准备服务器,再调用 assert_update_request_fails_for_missing_session 验证错误路径。

调用图:调用 2 个内部函数(assert_update_request_fails_for_missing_session, initialized_mcp);外部调用 1 个(new)。

test_fuzzy_file_search_session_update_works_without_waiting_for_start_response435–470 ↗
async fn test_fuzzy_file_search_session_update_works_without_waiting_for_start_response() -> Result<()>

作用:测试客户端发出“开始会话”请求后,即使还没等到开始响应,马上发“更新查询”也能正常工作。这符合真实界面里快速输入的情况。

数据流:它创建 alpha.txt 并启动服务器 → 连续发送 start 请求和 update 请求,不先等 start 完成 → 分别等待两个请求的响应,然后等待搜索更新,确认找到了 alpha.txt。

调用关系:它验证服务器能处理紧挨着来的请求。它用 initialized_mcp 建环境,用 TestAppServer 直接发 start/update 请求,再用 wait_for_session_updated 看最终结果。

调用图:调用 2 个内部函数(initialized_mcp, wait_for_session_updated);外部调用 6 个(new, Integer, assert_eq!, write, timeout, vec!)。

test_fuzzy_file_search_session_multiple_query_updates_work473–504 ↗
async fn test_fuzzy_file_search_session_multiple_query_updates_work() -> Result<()>

作用:测试同一个会话可以连续改查询词,每次都得到对应的新结果。用户在搜索框里不断输入、删除时就依赖这个行为。

数据流:它创建 alpha.txt 和 alphabet.txt,启动会话 → 查询“alp”并确认有结果且 root 正确,再等待完成 → 查询“zzzz”,确认收到的是这个新查询的更新且文件列表为空,再等待完成。

调用关系:它复用 initialized_mcp、wait_for_session_updated 和 wait_for_session_completed,覆盖同一 session_id 下多轮搜索的状态切换。

调用图:调用 3 个内部函数(initialized_mcp, wait_for_session_completed, wait_for_session_updated);外部调用 4 个(new, assert_eq!, write, vec!)。

test_fuzzy_file_search_session_update_after_stop_fails507–522 ↗
async fn test_fuzzy_file_search_session_update_after_stop_fails() -> Result<()>

作用:测试会话停止后,再更新这个会话会失败。停止就像把窗口关掉,后面再往这个窗口发内容不应该被接受。

数据流:它创建文件并启动服务器 → 开始一个会话,随后停止它 → 再对同一个 session_id 发送查询更新,并确认服务器返回“会话找不到”的错误。

调用关系:它用 initialized_mcp 建环境,用 stop_fuzzy_file_search_session 改变会话状态,然后把错误验证交给 assert_update_request_fails_for_missing_session。

调用图:调用 2 个内部函数(assert_update_request_fails_for_missing_session, initialized_mcp);外部调用 3 个(new, write, vec!)。

test_fuzzy_file_search_session_stops_sending_updates_after_stop525–548 ↗
async fn test_fuzzy_file_search_session_stops_sending_updates_after_stop() -> Result<()>

作用:测试会话停止后,服务器不会继续给这个会话推送搜索结果。它防止后台任务停不干净,导致用户已经关闭搜索后还收到旧消息。

数据流:它创建 512 个文件,让搜索可能产生较多更新 → 启动会话并查询“file-”,先确认至少收到一次非空更新 → 停止会话后观察一段时间,确认不会再收到该会话的更新。

调用关系:它用 wait_for_session_updated 确认搜索确实开始过,再用 assert_no_session_updates_for 检查停止后的安静状态。这个测试专门覆盖后台推送的收尾行为。

调用图:调用 3 个内部函数(assert_no_session_updates_for, initialized_mcp, wait_for_session_updated);外部调用 4 个(new, format!, write, vec!)。

test_fuzzy_file_search_two_sessions_are_independent551–587 ↗
async fn test_fuzzy_file_search_two_sessions_are_independent() -> Result<()>

作用:测试两个搜索会话互不影响。一个会话搜 alpha,另一个会话搜 beta,结果不能串到对方那里。

数据流:它创建两个不同临时目录,一个放 alpha.txt,一个放 beta.txt → 启动两个不同 session_id 的会话 → 分别更新查询“alp”和“bet”,并确认每个会话只返回自己目录里的文件。

调用关系:它通过 initialized_mcp 启动同一个服务器里的两个会话,用 wait_for_session_updated 分别等待对应 session_id 的通知,验证会话隔离。

调用图:调用 2 个内部函数(initialized_mcp, wait_for_session_updated);外部调用 4 个(new, assert_eq!, write, vec!)。

test_fuzzy_file_search_query_cleared_sends_blank_snapshot590–611 ↗
async fn test_fuzzy_file_search_query_cleared_sends_blank_snapshot() -> Result<()>

作用:测试用户清空搜索框时,服务器会推送一个空结果快照。这样界面可以立刻把旧搜索结果清掉,而不是继续显示过期内容。

数据流:它创建 alpha.txt 并启动会话 → 先查询“alp”确认能收到非空结果 → 然后把查询词更新为空字符串 → 等待一个文件列表为空的更新,并断言结果确实为空。

调用关系:它复用 initialized_mcp 和 wait_for_session_updated,覆盖“查询词从有内容变为空”的特殊场景,确保前端清空搜索时有明确反馈。

调用图:调用 2 个内部函数(initialized_mcp, wait_for_session_updated);外部调用 4 个(new, assert_eq!, write, vec!)。