Codex 系统手册

Daemon、传输和测试客户端支持测试

stage-23.1.213 个文件

这一阶段是幕后质检,不是正式对用户干活。它给后台服务、通信通道和测试客户端做体检:pid、版本、更新测试防止服务状态认错、该重启不重启;socket 和消息测试确认本机连接安全、握手收发正常、慢客户端不拖全局;远程控制用假服务器查配对、撤销和重试。测试客户端能启动服务、发消息、模拟审批,再配合假接口和埋点工具,核对插件安装、启用、卸载记录是否完整。

本阶段的文件13

守护进程后端和更新器测试

这些测试固定守护进程端的 PID 处理,以及决定后端启动和重启行为的托管安装/更新身份判定。

app-server-daemon/src/backend/pid_tests.rs源码 ↗
testtest

这个文件不是真正运行后台服务的代码,而是给它做“安全演练”。pid 文件可以理解成门口的小牌子,写着后台服务的进程编号;锁文件像一把临时占位锁,表示“有人正在启动,先别乱动”。这些测试会造出各种容易出错的现场:空 pid 文件但锁还在、空 pid 文件但锁没了、停止时别人还占着锁、旧记录被新记录替换、启动命令参数是否正确、错误日志只截取最近几行。它用临时目录制造干净环境,用异步测试模拟等待和文件变化,确保 PidBackend 在真实机器上遇到残留文件、并发启动、远程控制开关等情况时,不会误判服务状态,也不会删掉别人刚写好的新记录。

函数细节9
locked_empty_pid_file_is_treated_as_active_reservation18–43 ↗
async fn locked_empty_pid_file_is_treated_as_active_reservation()

作用:这个测试确认:如果 pid 文件是空的,但对应的锁文件还被占着,就应该认为服务正在启动,而不是坏掉了。这样可以避免另一个启动流程还没写完 pid 时,被系统误删或误判。

数据流:它先创建临时目录和一个空的 pid 文件,再创建 PidBackend,并手动打开、锁住对应的锁文件。然后它读取 pid 文件状态,期望得到 PidFileState::Starting。最后还检查 pid 文件仍然存在,说明代码没有把这个“正在启动的占位”清掉。

调用关系:这个测试由测试框架运行。它先用 PidBackend::new 搭出被测对象,再用 try_lock_file 模拟另一个启动者正在占位,最后调用 backend.read_pid_file_state 来验证 pid 状态判断逻辑。

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

unlocked_empty_pid_file_is_treated_as_stale_reservation46–63 ↗
async fn unlocked_empty_pid_file_is_treated_as_stale_reservation()

作用:这个测试确认:如果 pid 文件是空的,而且没有任何锁保护它,就应该把它当成过期残留。这样系统不会被一个没人负责的空文件卡住。

数据流:它创建一个空 pid 文件,但不去锁住锁文件。然后让 PidBackend 读取状态。结果应该是 PidFileState::Missing,并且原来的空 pid 文件会被删除,表示这块旧占位已经清理干净。

调用关系:这个测试由测试框架运行。它通过 PidBackend::new 创建后端,然后调用 backend.read_pid_file_state,检查后端在没有活锁的情况下会自己清理无效 pid 文件。

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

stop_waits_for_live_reservation_to_resolve66–95 ↗
async fn stop_waits_for_live_reservation_to_resolve()

作用:这个测试确认:停止后台服务时,如果发现有一个正在启动的占位锁,stop 不会立刻粗暴处理,而是会等这件事结束。这样可以避免“服务刚启动到一半就被停止逻辑误伤”。

数据流:它先创建空 pid 文件并锁住锁文件,模拟服务正在启动。接着启动一个异步小任务,等 50 毫秒后释放锁并删除 pid 文件。主测试调用 backend.stop,期望它会耐心等到锁释放、文件消失后正常结束。最后等待清理任务完成。

调用关系:这个测试由异步测试运行器执行。它用 PidBackend::new 建立对象,用 try_lock_file 制造活跃占位,用 tokio::spawn 安排稍后的清理动作,然后验证 backend.stop 在这种过渡状态下会等待,而不是直接失败或误删。

调用图:调用 1 个内部函数(new);外部调用 8 个(from_millis, new, assert!, new, remove_file, write, spawn, sleep)。

start_retries_stale_empty_pid_file_under_its_own_lock98–115 ↗
async fn start_retries_stale_empty_pid_file_under_its_own_lock()

作用:这个测试确认:启动时遇到一个没人锁住的空 pid 文件,系统会把它当成旧垃圾处理,并继续尝试启动。这里故意给了一个不存在的 codex 程序路径,所以最后应该失败在“启动程序”这一步,而不是卡在旧 pid 文件上。

数据流:它先写入一个空 pid 文件,再创建 PidBackend,但把可执行文件路径设成 missing-codex。然后调用 backend.start。预期结果是报错,并且错误信息以“failed to spawn detached app-server process using”开头,说明它已经走到了真正启动进程的阶段。

调用关系:这个测试由测试框架运行。它通过 PidBackend::new 搭建场景,再调用 backend.start,间接验证启动流程会先处理陈旧空 pid 文件,然后才去尝试拉起后台服务。

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

stale_record_cleanup_preserves_replacement_record118–148 ↗
async fn stale_record_cleanup_preserves_replacement_record()

作用:这个测试确认:清理旧 pid 记录时,如果文件里已经被别人写入了新记录,清理逻辑不能把新记录删掉。它保护的是并发场景下的新服务,不被旧服务清理动作误伤。

数据流:它准备一条旧记录 stale 和一条新记录 replacement,然后把新记录序列化成 JSON 写进 pid 文件。接着调用 backend.refresh_after_stale_record,并告诉它旧记录是什么。函数返回的状态应该是 PidFileState::Running(replacement),表示它识别到文件已经换成新服务记录,并保留下来。

调用关系:这个测试由测试框架运行。它用 PidBackend::new 建立对象,用 serde_json::to_vec 把新 pid 记录写成文件内容,然后调用 backend.refresh_after_stale_record,验证旧记录清理流程会重新读取现场,而不是盲目删除文件。

调用图:调用 1 个内部函数(new);外部调用 4 个(new, assert_eq!, to_vec, write)。

update_loop_uses_hidden_app_server_subcommand151–163 ↗
fn update_loop_uses_hidden_app_server_subcommand()

作用:这个测试检查“pid 更新循环”启动时使用的命令参数是否正确。这个循环是隐藏的内部子命令,参数错了就无法启动正确的维护进程。

数据流:它手工构造一个 PidBackend,并把 command_kind 设成 PidCommandKind::UpdateLoop。然后调用 backend.command_args,期望得到 app-server daemon pid-update-loop 这一组参数。它不改文件,也不启动真实进程,只检查要传给命令行的字面参数。

调用关系:这个测试由普通测试框架运行。它不通过 PidBackend::new,而是直接构造结构体,专门验证 command_args 在 UpdateLoop 模式下会选择隐藏的 app-server 子命令。

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

app_server_remote_control_uses_runtime_flag166–177 ↗
fn app_server_remote_control_uses_runtime_flag()

作用:这个测试确认:启用远程控制时,启动 app-server 的命令行参数里会明确带上 --remote-control。远程控制就是允许外部通过指定通道控制服务,参数缺了就不会按预期开放这个能力。

数据流:它用 PidBackend::new 创建一个远程控制开启的后端。然后读取 backend.command_args,期望得到 app-server、--remote-control、--listen、unix:// 这组参数。结果只验证参数,不实际打开网络或启动服务。

调用关系:这个测试由测试框架运行。它通过 PidBackend::new 传入 remote_control_enabled 为 true,然后检查 command_args 如何把这个运行时开关翻译成启动 app-server 的命令行参数。

调用图:调用 1 个内部函数(new);外部调用 1 个(assert_eq!)。

app_server_disabled_remote_control_uses_compatible_args_and_runtime_env180–195 ↗
fn app_server_disabled_remote_control_uses_compatible_args_and_runtime_env()

作用:这个测试确认:关闭远程控制时,命令行参数仍保持兼容,同时会用环境变量告诉子进程“远程控制被关了”。环境变量可以理解成启动程序时顺手塞给它的一张小纸条。

数据流:它用 PidBackend::new 创建一个远程控制关闭的后端。然后检查 command_args 只包含 app-server、--listen、unix://,不包含 --remote-control。接着检查 command_env 返回 REMOTE_CONTROL_DISABLED_ENV_VAR=1,表示子进程会收到禁用远程控制的提示。

调用关系:这个测试由测试框架运行。它用 PidBackend::new 传入 remote_control_enabled 为 false,然后同时验证 command_args 和 command_env,确保旧参数形式和新的禁用标记能一起工作。

调用图:调用 1 个内部函数(new);外部调用 1 个(assert_eq!)。

read_stderr_log_tail_returns_recent_complete_lines198–216 ↗
async fn read_stderr_log_tail_returns_recent_complete_lines()

作用:这个测试确认:读取后台服务的标准错误日志时,只返回最近的完整行,而不是把很早的大段内容或半截行乱塞出来。标准错误日志就是程序报错时常写入的文字记录。

数据流:它先根据 pid 文件路径算出对应的错误日志文件路径,再写入一段很长的旧内容,加上 recent error 和 usage 两行近期内容。然后调用 read_stderr_log_tail。期望结果包含日志路径,并且内容只剩 recent error\nusage,说明函数会从尾部取最近有用的完整日志。

调用关系:这个测试由异步测试运行器执行。它用 stderr_log_file_for_pid_file 找到日志文件名,用 tokio::fs::write 写入测试内容,然后调用 read_stderr_log_tail,验证日志截尾功能能在启动失败等场景里给用户看最近的错误线索。

调用图:外部调用 5 个(new, assert_eq!, format!, stderr_log_file_for_pid_file, write)。

app-server-daemon/src/managed_install_tests.rs源码 ↗
testtest run

这个文件不参与正式运行,而是在开发者跑测试时工作。它像质检员一样,专门检查两个关键行为:第一,命令行工具输出类似“codex 1.2.3”时,代码能不能准确取出版本号“1.2.3”;如果输出格式不完整,比如只有“codex”,就必须报错,不能假装成功。第二,它检查可执行文件的“身份”是否真的来自文件内容。这里的身份可以理解成给文件内容做出的指纹:内容一样,指纹就一样;内容变了,指纹也应该变。这样系统之后判断某个程序是不是同一个版本时,才不会只看名字或路径而误判。文件里用断言(测试里的检查语句,条件不满足就让测试失败)来表达这些期望。

函数细节3
parses_codex_cli_version_output7–12 ↗
fn parses_codex_cli_version_output()

作用:这个测试确认正常的 Codex 命令行版本输出能被正确解析。也就是说,看到“codex 1.2.3”这类文字时,程序应该拿到真正的版本号“1.2.3”。

数据流:进去的是一段模拟的命令输出字符串“codex 1.2.3\n” → 测试把它交给版本解析函数,并要求解析结果存在 → 出来后用断言检查结果必须等于“1.2.3”,如果不是,测试就失败。

调用关系:这是管理安装相关解析逻辑的一个正向样例测试。它最后通过 assert_eq! 这个测试断言来核对结果,帮助保证上层安装流程以后读取 Codex 版本时不会拿错值。

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

rejects_malformed_codex_cli_version_output15–17 ↗
fn rejects_malformed_codex_cli_version_output()

作用:这个测试确认格式坏掉的版本输出会被拒绝。它防止程序在没有版本号的情况下还继续往下走,避免后面做出错误安装判断。

数据流:进去的是一段不完整的字符串“codex\n” → 测试把它交给版本解析函数 → 期望得到的是错误结果,而不是一个假版本号;最后用断言检查“确实出错”。

调用关系:这是版本解析逻辑的反向样例测试。它通过 assert! 检查错误是否发生,和正常解析测试配套,说明这个解析函数既要会认对的,也要会拒绝错的。

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

executable_identity_uses_binary_contents20–27 ↗
fn executable_identity_uses_binary_contents()

作用:这个测试确认可执行文件的身份是按二进制内容算出来的。简单说,同样的内容应该被认作同一个文件,不同内容应该被认作不同文件。

数据流:进去的是三段模拟文件内容:两段是“old”,一段是“new” → 测试分别调用 executable_identity_from_bytes 生成它们的身份指纹 → 出来后检查两个“old”的身份相同,同时检查“old”和“new”的身份不同。

调用关系:这是文件身份判断逻辑的核心安全网。它把具体的指纹计算交给 executable_identity_from_bytes,然后用 assert_eq! 和 assert_ne! 分别确认“相同内容相同身份”和“不同内容不同身份”,为后续安装时判断二进制是否变化提供保障。

调用图:外部调用 3 个(assert_eq!, assert_ne!, executable_identity_from_bytes)。

app-server-daemon/src/update_loop_tests.rs源码 ↗
testtest run

这个测试文件盯住一个很关键的判断:新旧两个更新器程序是不是同一个。这里的“身份”可以理解成给程序内容算出来的指纹,内容一样指纹就一样,内容变了指纹就不同。测试分两种情况:如果新旧更新器内容相同,就不需要强制重新执行更新器,只按版本号变化来决定是否重启;如果内容不同,就算版本号看起来可能没变,也要强制使用更谨慎的策略,重新执行被管理的二进制文件。它像是在给更新系统装两道安全门:没变化时别折腾用户,有变化时一定别漏掉关键替换。文件里用 assert_eq!(断言相等,也就是测试“实际结果”和“预期结果”是否一样)来确认这些规则没有被破坏。

函数细节2
unchanged_updater_uses_version_based_restart9–17 ↗
fn unchanged_updater_uses_version_based_restart()

作用:这个测试确认:如果旧更新器和新更新器的内容完全一样,系统不应该强制刷新更新器。它只需要按版本号是否变化来决定要不要重启。

数据流:输入是两份相同的字节内容 same,先把它们变成两个“可执行文件身份”指纹,再交给更新策略判断函数。函数应该返回“只有版本变化才重启”和“无需刷新更新器”,最后测试用 assert_eq! 检查实际结果是否正好等于这个预期。

调用关系:这个函数由 Rust 的测试运行器在跑测试时自动调用。它主要是在验证 update_modes_for_identities 的正常、不变场景,并把判断结果交给 assert_eq! 做对比;如果结果不同,测试就会失败,提醒开发者更新逻辑被改坏了。

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

changed_updater_forces_refresh_even_when_version_may_match20–31 ↗
fn changed_updater_forces_refresh_even_when_version_may_match()

作用:这个测试确认:如果旧更新器和新更新器的内容不同,系统必须采取更强硬的刷新策略。即使版本号可能一样,也不能假装没事。

数据流:输入是两份不同的字节内容 oldnew,它们被转换成两个不同的可执行文件身份指纹。测试把这两个身份交给更新策略判断函数,期望得到“总是重启”和“如果被管理的二进制变了就重新执行”的结果,然后用 assert_eq! 验证实际输出是否符合预期。

调用关系:这个函数同样由测试运行器自动执行。它覆盖的是更危险的变化场景:更新器本身变了。它通过调用 update_modes_for_identities 来检查主逻辑,再用 assert_eq! 把实际行为和安全预期对上,避免以后有人把强制刷新规则误删或放松。

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

传输行为测试

这些文件从本地套接字机制到路由规则和远程控制 API,测试服务器和传输层。

app-server-transport/src/transport/unix_socket_tests.rs源码 ↗
testtest

可以把 Unix socket 理解成本机上的一根“私密电话线”,客户端和应用服务器不用走网络端口,而是在本机文件路径上连起来。这个测试文件就是反复确认这根电话线好不好用。它先检查配置字符串,比如 unix:// 或自定义路径,能不能被正确理解成 Unix socket 传输方式。然后它真的启动一个控制 socket 接收器,让客户端连上去,并升级成 WebSocket(在一条连接上持续双向收发消息的协议),确认 JSON-RPC 消息能被转成内部事件,ping 能得到 pong,断开后会发出关闭事件并删除 socket 文件。它还测试启动锁,确保两个服务器不会同时抢同一个控制通道。最后,在 Unix 系统上,它确认 socket 文件权限是 0600,也就是只有当前用户能读写,避免别人偷连。

函数细节12
listen_unix_socket_parses_as_unix_socket_transport26–33 ↗
fn listen_unix_socket_parses_as_unix_socket_transport()

作用:这个测试确认最简单的监听地址 unix:// 会被识别成 Unix socket 模式,并使用默认的控制 socket 路径。没有这个保证,用户写默认配置时服务器可能根本不知道该监听哪里。

数据流:输入是一段字符串 unix://,测试把它交给传输地址解析函数。解析结果会和“Unix socket 加默认路径”的预期值比较。输出不是业务结果,而是测试通过或失败;如果不一致,测试会立刻报错。

调用关系:它由测试运行器自动调用,主要盯住 AppServerTransport::from_listen_url 的默认解析行为。它用 default_control_socket_path 算出预期路径,再用断言确认解析器没有走偏。

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

listen_unix_socket_accepts_absolute_custom_path36–43 ↗
fn listen_unix_socket_accepts_absolute_custom_path()

作用:这个测试确认用户可以给 Unix socket 指定一个绝对路径,比如 /tmp/codex.sock。这样服务器不只能用默认位置,也能按用户或测试环境指定的位置启动。

数据流:输入是字符串 unix:///tmp/codex.sock。测试先把 /tmp/codex.sock 转成项目使用的绝对路径类型,再和解析函数返回的结果比较。结果一致就通过,不一致就说明自定义绝对路径解析有问题。

调用关系:它由测试运行器调用,用来覆盖 AppServerTransport::from_listen_url 的自定义路径分支。它把路径转换这件小事交给 absolute_path,自己只关心最终解析结果是否正确。

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

listen_unix_socket_accepts_relative_custom_path46–54 ↗
fn listen_unix_socket_accepts_relative_custom_path()

作用:这个测试确认用户写相对路径,比如 unix://codex.sock,也能被接受并转换成当前目录下的绝对路径。这样命令行使用起来更方便,不必总写完整路径。

数据流:输入是字符串 unix://codex.sock。测试让解析器处理它,同时自己用“相对于当前目录”的规则算出预期路径。最后比较两边是否完全一样;一样就说明相对路径处理正确。

调用关系:它由测试运行器调用,检查监听地址解析器对相对路径的支持。它不启动真实 socket,只专注于配置字符串到内部传输配置的转换。

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

control_socket_acceptor_upgrades_and_forwards_websocket_text_messages_and_pings57–142 ↗
async fn control_socket_acceptor_upgrades_and_forwards_websocket_text_messages_and_pings()

作用:这个测试完整模拟一次客户端连接服务器控制 socket 的过程,确认连接能升级成 WebSocket,文本消息能转成内部事件,ping 会收到 pong,关闭连接也会被服务器知道。它像一次端到端彩排,能发现单独测函数发现不了的问题。

数据流:一开始它创建临时目录和 socket 路径,再启动控制 socket 接收器,并拿到一个接收内部事件的通道。接着客户端连到这个 socket,完成 WebSocket 握手。之后客户端发送一条 JSON-RPC 通知,测试从事件通道里收到对应的 IncomingMessage。再发送 ping,客户端收到相同内容的 pong。最后客户端关闭连接,测试收到 ConnectionClosed,然后取消服务端任务,并检查 socket 文件已清理。

调用关系:它由异步测试运行器调用,是这个文件里最像真实运行的一条测试。它会用 test_socket_path 准备临时路径,用 start_control_socket_acceptor 启动被测服务,用 connect_to_socket 模拟客户端连接,结束时调用 assert_socket_path_removed 确认现场收拾干净。

调用图:调用 3 个内部函数(assert_socket_path_removed, connect_to_socket, test_socket_path);外部调用 14 个(from_static, new, from_secs, Ping, Text, Notification, assert!, assert_eq!, panic!, to_string (+4 more))。

app_server_startup_lock_serializes_waiters145–164 ↗
async fn app_server_startup_lock_serializes_waiters()

作用:这个测试确认启动锁会让后来者排队,而不是两个应用服务器同时启动并争抢同一个控制 socket。可以把它想成门口只有一把钥匙,拿到钥匙的人没还之前,别人只能等。

数据流:它先在临时目录里算出锁文件路径,然后第一个任务成功拿到锁。第二个任务尝试拿同一把锁,测试用很短的超时时间确认它还拿不到。随后第一个锁被释放,第二个任务才顺利完成。测试结果证明锁确实把并发启动串起来了。

调用关系:它由异步测试运行器调用,直接验证 acquire_app_server_startup_lock 的等待行为。路径准备交给 test_startup_lock_path,并发等待通过 tokio::spawn 启动第二个异步任务来模拟。

调用图:调用 1 个内部函数(test_startup_lock_path);外部调用 4 个(assert!, acquire_app_server_startup_lock, new, spawn)。

control_socket_file_is_private_after_bind168–191 ↗
async fn control_socket_file_is_private_after_bind()

作用:这个测试只在 Unix 系统上运行,确认控制 socket 文件创建后权限是私密的。也就是说,只有当前用户能访问,避免本机其他用户随便连进控制通道。

数据流:它创建临时 socket 路径,启动控制 socket 接收器,然后读取这个 socket 文件的系统元数据。测试把权限位取出来,只看最后三组权限,确认等于 0600。最后取消接收器任务并等待它退出。

调用关系:它由异步测试运行器在 Unix 平台调用,重点检查 start_control_socket_acceptor 绑定文件后的安全设置。它使用 test_socket_path 生成路径,用文件元数据读取系统实际权限。

调用图:调用 1 个内部函数(test_socket_path);外部调用 5 个(new, assert_eq!, start_control_socket_acceptor, new, metadata)。

absolute_path193–195 ↗
fn absolute_path(path: &str) -> AbsolutePathBuf

作用:这个小工具把字符串形式的绝对路径变成项目内部统一使用的绝对路径类型。测试里用它来写出清楚的预期值。

数据流:输入是一段路径字符串,比如 /tmp/codex.sock。函数调用路径类型的校验和转换方法,确认它真的是绝对路径。输出是 AbsolutePathBuf;如果路径不合格,测试会直接失败。

调用关系:它主要服务于监听地址解析测试,帮测试构造“正确答案”。它把具体的路径校验交给 AbsolutePathBuf::from_absolute_path

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

default_control_socket_path197–200 ↗
fn default_control_socket_path() -> AbsolutePathBuf

作用:这个小工具算出应用服务器默认应该使用的控制 socket 路径。测试用它来判断 unix:// 这种没有写明路径的配置是否走了正确默认值。

数据流:它先读取 Codex 的家目录,也就是项目默认存放本地状态文件的位置。然后把这个目录交给控制 socket 路径生成函数,得到默认 socket 文件路径。输出是一个绝对路径;如果家目录或路径生成失败,测试会报错。

调用关系:它被默认监听地址解析测试使用。它把“Codex 家目录在哪里”交给 find_codex_home,把“控制 socket 文件名和目录怎么拼”交给 app_server_control_socket_path

调用图:调用 1 个内部函数(find_codex_home);外部调用 1 个(app_server_control_socket_path)。

test_socket_path202–209 ↗
fn test_socket_path(temp_dir: &Path) -> AbsolutePathBuf

作用:这个小工具在临时目录里拼出测试专用的控制 socket 路径。这样测试不会污染用户真实环境,也不会和别的测试互相撞车。

数据流:输入是一个临时目录路径。函数在里面追加 app-server-control/app-server-control.sock 这两段名字,再把结果转换成项目认可的绝对路径。输出是这个测试 socket 路径。

调用关系:它被真实连接测试和权限测试调用,用来给 start_control_socket_acceptor 提供安全、隔离的文件位置。它内部只做路径拼接和绝对路径转换。

调用图:调用 1 个内部函数(from_absolute_path);被 2 处调用(control_socket_acceptor_upgrades_and_forwards_websocket_text_messages_and_pings, control_socket_file_is_private_after_bind);外部调用 1 个(join)。

test_startup_lock_path211–218 ↗
fn test_startup_lock_path(temp_dir: &Path) -> AbsolutePathBuf

作用:这个小工具在临时目录里拼出测试专用的启动锁文件路径。它让启动锁测试可以安全地模拟抢锁,不影响真实应用的锁文件。

数据流:输入是一个临时目录路径。函数在里面追加 app-server-control/app-server-startup.lock,再转换成绝对路径类型。输出是锁文件路径,供测试拿锁使用。

调用关系:它被 app_server_startup_lock_serializes_waiters 调用。该测试拿它生成的路径去调用 acquire_app_server_startup_lock,从而验证同一把锁会让第二个任务等待。

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

connect_to_socket220–222 ↗
async fn connect_to_socket(socket_path: &Path) -> IoResult<UnixStream>

作用:这个异步小工具让测试客户端连接到指定的 Unix socket。它把底层连接动作包起来,让主测试读起来更像“客户端连上服务器”。

数据流:输入是 socket 文件路径。函数调用 Unix stream 的连接方法,等待连接建立。输出要么是一条可读写的本地连接流,要么是系统 I/O 错误。

调用关系:它被完整 WebSocket 流程测试调用。连接成功后,测试把这条流交给 WebSocket 客户端握手函数,继续验证消息收发。

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

assert_socket_path_removed230–233 ↗
fn assert_socket_path_removed(_socket_path: &Path)

作用:这个辅助检查测试结束后 socket 文件有没有被清掉。在 Unix 上它会确认路径不存在;在 Windows 相关实现里,因为没有同样的 socket 文件节点,这个检查会被弱化为不做实际断言。

数据流:输入是 socket 路径。Unix 版本会查看这个路径是否还存在,并断言它已经不存在;如果还在,测试失败。它没有返回业务数据,只通过断言表达结果。

调用关系:它被完整 WebSocket 流程测试在关闭接收器之后调用。它验证 start_control_socket_acceptor 的清理行为,确保测试留下的临时 socket 文件不会继续占着位置。

调用图:被 1 处调用(control_socket_acceptor_upgrades_and_forwards_websocket_text_messages_and_pings);外部调用 1 个(assert!)。

app-server/src/transport_tests.rs源码 ↗
testtest run

这个文件测试的是“出站消息运输”这条路,也就是服务器准备把通知或请求发给客户端时,中间那层路由该怎么做。可以把它想成快递分拣站:有些客户贴了“不要某类广告”的标签,就不能投递;有些包裹是实验功能,只有声明自己能收的客户才收得到;有些旧客户端不认识新字段,服务器要先把这些字段拿掉,免得对方看不懂。文件还测试了两个很重要的排队场景:群发消息时,如果某个连接的发送队列满了,不能让它卡住所有连接,而是把慢连接踢掉;但如果是点对点发送到标准输入输出这类连接,队列满时应该等一等,而不是粗暴断开。这里大量使用异步通道(像一根临时传话管道)和超时检查,确保行为真的符合预期。

函数细节11
absolute_path13–15 ↗
fn absolute_path(path: &str) -> AbsolutePathBuf

作用:把字符串形式的路径变成项目里使用的“绝对路径”对象。测试里需要构造像 /tmp 这样的路径参数,所以用它来避免每个测试重复写转换代码。

数据流:进去的是一个路径字符串 → 它调用 from_absolute_path 检查并转换这个字符串,要求它必须真的是绝对路径 → 出来的是 AbsolutePathBuf,也就是已经确认过的绝对路径对象;如果传入的不是绝对路径,测试会直接失败。

调用关系:它是测试里的小帮手。两个命令审批相关测试会调用它来准备当前目录和允许读取的目录,随后这些路径会被放进要发送给客户端的请求消息里。

调用图:调用 1 个内部函数(from_absolute_path);被 2 处调用(command_execution_request_approval_keeps_additional_permissions_with_capability, command_execution_request_approval_strips_additional_permissions_without_capability)。

thread_realtime_started_notification17–23 ↗
fn thread_realtime_started_notification() -> ServerNotification

作用:构造一条“实时会话已开始”的服务器通知。这个通知属于实验性能力,所以测试用它来验证:没有声明支持实验功能的客户端收不到,有声明支持的客户端能收到。

数据流:它不接收外部输入 → 在函数内部填好 thread_id、realtime_session_id 和版本号 → 返回一条 ServerNotification::ThreadRealtimeStarted 通知,供后面的路由测试发送。

调用关系:它被两个实验通知测试共用。测试先用它造出同一类消息,再交给 route_outgoing_envelope,看路由层会根据连接能力选择丢掉还是投递。

调用图:被 2 处调用(experimental_notifications_are_dropped_without_capability, experimental_notifications_are_preserved_with_capability);外部调用 1 个(ThreadRealtimeStarted)。

to_connection_notification_respects_opt_out_filters26–66 ↗
async fn to_connection_notification_respects_opt_out_filters()

作用:确认客户端如果明确选择不接收某类通知,服务器就真的不会把这类通知发过去。这里测试的是 configWarning 配置警告通知。

数据流:进去的是测试自己搭出来的一条连接:连接已初始化、支持实验功能,但它的“不想接收列表”里有 configWarning → 测试把一条配置警告通知交给消息路由 → 结果是发送通道里收不到任何消息,说明通知被正确丢弃了。

调用关系:这是对 route_outgoing_envelope 的行为检查。它先创建 OutboundConnectionState 作为假连接,再把 OutgoingEnvelope::ToConnection 交给路由函数,最后从写出通道确认消息没有被投递。

调用图:调用 1 个内部函数(new);外部调用 9 个(new, new, new, from, new, ConfigWarning, AppServerNotification, assert!, channel)。

to_connection_notifications_are_dropped_for_opted_out_clients69–106 ↗
async fn to_connection_notifications_are_dropped_for_opted_out_clients()

作用:再次验证“退订通知”规则:客户端退订了某个通知方法,服务器就不能发这类通知。它和前一个测试覆盖同一类安全网,确保行为稳定。

数据流:测试创建一个连接,并把 configWarning 放进这个连接的退订列表 → 路由层收到一条发给该连接的配置警告通知 → 输出通道为空,表示这条消息没有到达客户端。

调用关系:它围绕 route_outgoing_envelope 做断言。测试构造连接状态和通知消息,路由函数负责实际判断是否投递,测试最后检查通道来判断结果。

调用图:调用 1 个内部函数(new);外部调用 9 个(new, new, new, from, new, ConfigWarning, AppServerNotification, assert!, channel)。

to_connection_notifications_are_preserved_for_non_opted_out_clients109–152 ↗
async fn to_connection_notifications_are_preserved_for_non_opted_out_clients()

作用:确认没有退订的客户端能正常收到通知。这样可以防止过滤规则太激进,把本该发送的普通通知也误删了。

数据流:测试创建一个没有退订任何通知的连接 → 把一条 configWarning 通知交给路由层 → 通道里收到这条消息,并且消息里的摘要仍然是 task_started,说明内容没有被改坏。

调用关系:它是退订过滤测试的反面案例。前两个测试证明该丢的会丢,这个测试证明不该丢的会送达,三者一起约束 route_outgoing_envelope 的过滤逻辑。

调用图:调用 1 个内部函数(new);外部调用 9 个(new, new, new, new, new, ConfigWarning, AppServerNotification, assert!, channel)。

experimental_notifications_are_dropped_without_capability155–185 ↗
async fn experimental_notifications_are_dropped_without_capability()

作用:确认实验性通知不会发给不支持实验功能的客户端。这样可以避免旧客户端收到自己不认识的消息后出错。

数据流:测试创建一个连接,并把“支持实验功能”的标记设为 false → 用 thread_realtime_started_notification 生成一条实验性通知并交给路由层 → 输出通道没有消息,表示路由层把它挡下来了。

调用关系:它调用 thread_realtime_started_notification 准备测试消息,然后把消息交给 route_outgoing_envelope。这个测试关注的是连接能力标记如何影响通知投递。

调用图:调用 2 个内部函数(new, thread_realtime_started_notification);外部调用 8 个(new, new, new, new, new, AppServerNotification, assert!, channel)。

experimental_notifications_are_preserved_with_capability188–222 ↗
async fn experimental_notifications_are_preserved_with_capability()

作用:确认声明支持实验功能的客户端可以收到实验性通知。这样新客户端能使用新功能,同时不影响旧客户端。

数据流:测试创建一个连接,并把“支持实验功能”的标记设为 true → 生成一条实时会话开始通知并交给路由层 → 通道里收到 ThreadRealtimeStarted 通知,说明路由层允许投递。

调用关系:它和 experimental_notifications_are_dropped_without_capability 是一组正反测试。两者都使用 thread_realtime_started_notification,区别只在连接是否具备实验能力。

调用图:调用 2 个内部函数(new, thread_realtime_started_notification);外部调用 8 个(new, new, new, new, new, AppServerNotification, assert!, channel)。

command_execution_request_approval_strips_additional_permissions_without_capability225–287 ↗
async fn command_execution_request_approval_strips_additional_permissions_without_capability()

作用:确认旧客户端收到命令执行审批请求时,不会看到它不支持的 additionalPermissions 字段。这个字段表示额外权限请求,如果旧客户端不认识,可能会解析失败或显示异常。

数据流:测试创建一个不支持实验能力的连接 → 构造一条命令执行审批请求,里面带有额外文件读取权限,例如允许读 /tmp/allowed → 路由层发送前会按客户端能力改写消息 → 测试把发出的消息转成 JSON,确认 additionalPermissions 已经不存在。

调用关系:它使用 absolute_path 构造请求里的路径,再把完整请求交给 route_outgoing_envelope。路由函数在投递前负责兼容性处理,测试通过序列化后的 JSON 检查字段是否被移除。

调用图:调用 2 个内部函数(new, absolute_path);外部调用 11 个(new, new, new, new, new, Integer, Request, assert_eq!, channel, to_value (+1 more))。

command_execution_request_approval_keeps_additional_permissions_with_capability290–362 ↗
async fn command_execution_request_approval_keeps_additional_permissions_with_capability()

作用:确认新客户端如果支持相关能力,就能收到命令审批请求里的额外权限信息。这样客户端才能向用户展示“程序还想多读哪些文件”等细节。

数据流:测试创建一个支持实验能力的连接 → 构造一条带 additionalPermissions 的命令执行审批请求,其中包含允许读取的路径 → 路由层把消息送出 → 测试把消息转成 JSON,确认额外权限字段仍在,而且路径内容正确。

调用关系:它是上一条测试的反面案例。两者都通过 absolute_path 准备路径,并都调用 route_outgoing_envelope;区别是客户端能力不同,所以路由层一个会删字段,一个会保留字段。

调用图:调用 2 个内部函数(new, absolute_path);外部调用 11 个(new, new, new, new, new, Integer, Request, assert_eq!, channel, to_value (+1 more))。

broadcast_does_not_block_on_slow_connection365–449 ↗
async fn broadcast_does_not_block_on_slow_connection()

作用:确认群发消息时,一个慢连接不会卡住所有连接。现实里这很重要:如果一个客户端不读消息,服务器不能因为它就让其他客户端也收不到通知。

数据流:测试创建两个连接:一个快连接,一个慢连接;慢连接的发送队列先被塞满 → 路由层收到一条广播通知 → 它能在很短时间内返回,快连接收到新通知,慢连接被从连接表移除并触发断开信号,而慢连接原来队列里的旧消息还保留着。

调用关系:这个测试直接考验 route_outgoing_envelope 处理 Broadcast 的策略。它用超时来防止测试假通过:如果路由函数被慢连接卡住,测试就会失败。

调用图:调用 2 个内部函数(new, new);外部调用 12 个(new, new, new, from_millis, new, new, new, ConfigWarning, AppServerNotification, assert! (+2 more))。

to_connection_stdio_waits_instead_of_disconnecting_when_writer_queue_is_full452–524 ↗
async fn to_connection_stdio_waits_instead_of_disconnecting_when_writer_queue_is_full()

作用:确认点对点发送时,如果发送队列暂时满了,路由层会等待队列腾出位置,而不是立刻断开连接。这个行为适合标准输入输出这类连接,因为短暂排队不代表客户端坏了。

数据流:测试先创建一个容量为 1 的发送队列,并放入第一条消息让队列变满 → 再启动一个异步任务发送第二条消息 → 测试读走第一条消息后,路由任务顺利完成,第二条消息进入队列 → 最后确认第一条和第二条消息都按顺序存在。

调用关系:它测试的是 route_outgoing_envelope 处理 ToConnection 时的等待行为。和广播测试不同,这里不是踢掉慢连接,而是等接收端清空一点空间后继续投递。

调用图:调用 2 个内部函数(new, new);外部调用 12 个(new, new, from_millis, new, new, new, ConfigWarning, AppServerNotification, assert!, channel (+2 more))。

app-server-transport/src/transport/remote_control/tests/clients_tests.rs源码 ↗
testtest execution

远程控制功能需要向后台查询哪些设备连过来,也需要把某个设备撤销掉。这个文件就是给这些行为做安全网的测试。它不会真的连线上服务,而是在本机临时开一个小服务器,像收快递一样检查请求地址、请求头和请求内容,再回给代码不同的结果。测试覆盖了几个容易出错的点:远程控制当前是关闭状态时,仍然可以列出或撤销客户端;访问令牌过期时,会重新读一次新的登录信息再试;但如果仍然未授权,就不会无限重试;遇到禁止访问、返回坏 JSON 时,错误信息里要保留服务器地址、状态码、请求编号和响应正文,方便排查问题。

函数细节8
client_management_handle13–37 ↗
fn client_management_handle(
    remote_control_url: String,
    auth_manager: Arc<AuthManager>,
) -> RemoteControlHandle

作用:这个辅助函数造出一个可用于测试的 RemoteControlHandle,也就是远程控制功能的操作入口。它故意把远程控制状态设成“关闭”,用来验证客户端管理操作不依赖连接是否开启。

数据流:进去的是假的远程控制服务地址和认证管理器 → 它创建几个测试用的状态通道、锁和注册状态,并把这些零件装进 RemoteControlHandle → 出来的是一个可以调用 list_clients 和 revoke_client 的测试句柄,不会真的持久化远程控制状态。

调用关系:它被 remote_control_handle_lists_clients_while_disabled 和 remote_control_handle_revokes_client_while_disabled 调用。那两个测试先搭好假服务器,再用这个函数做出句柄,最后通过句柄触发真正的列表或撤销请求。

调用图:调用 1 个内部函数(new);被 2 处调用(remote_control_handle_lists_clients_while_disabled, remote_control_handle_revokes_client_while_disabled);外部调用 3 个(new, new, channel)。

empty_client_list39–44 ↗
fn empty_client_list() -> serde_json::Value

作用:这个辅助函数生成一份“客户端列表为空”的 JSON 响应。它让测试不用反复手写同一段空列表数据。

数据流:它不接收输入 → 直接拼出包含 items 空数组和 cursor 为 null 的 JSON 值 → 返回给测试里的假服务器,当作后台响应正文。

调用关系:它被 list_remote_control_clients_recovers_auth_after_unauthorized 使用。那个测试在第二次请求成功后,用这份空列表确认鉴权恢复流程走通了。

调用图:被 1 处调用(list_remote_control_clients_recovers_auth_after_unauthorized);外部调用 1 个(json!)。

remote_control_handle_lists_clients_while_disabled47–116 ↗
async fn remote_control_handle_lists_clients_while_disabled()

作用:这个测试确认:即使远程控制连接本身处于关闭状态,代码仍然能通过句柄查询客户端列表。这样用户在未开启连接时也能看到并管理已登记设备。

数据流:测试先启动本地假服务器并准备好预期请求 → 调用 client_management_handle 做出关闭状态的远程控制句柄 → 通过 handle.list_clients 传入环境 ID、分页游标、数量限制和排序 → 假服务器检查 URL 编码、鉴权头和账号头,再返回一条客户端记录 → 测试最后确认代码把后台 JSON 转成了 RemoteControlClientsListResponse,并把时间转成时间戳。

调用关系:这是一个完整流程测试。它自己搭服务器,用 client_management_handle 组装测试入口,然后间接验证客户端列表功能会发出正确的 HTTP GET 请求并正确解析响应。

调用图:调用 1 个内部函数(client_management_handle);外部调用 4 个(bind, assert_eq!, json!, spawn)。

remote_control_handle_revokes_client_while_disabled119–144 ↗
async fn remote_control_handle_revokes_client_while_disabled()

作用:这个测试确认:远程控制连接关闭时,仍然可以撤销某个远程控制客户端。它防止代码误把“连接关闭”和“不能管理设备”混为一谈。

数据流:测试先启动本地假服务器 → 用 client_management_handle 做出关闭状态的句柄 → 调用 handle.revoke_client,传入环境 ID 和客户端 ID → 假服务器检查 DELETE 请求路径里的特殊字符被正确编码,并返回 204 No Content → 测试确认函数返回一个空的成功响应。

调用关系:它和列表测试类似,也是通过句柄入口触发真实撤销逻辑。它重点检查撤销动作的 HTTP DELETE 地址是否正确,以及关闭状态不会挡住这次管理操作。

调用图:调用 1 个内部函数(client_management_handle);外部调用 3 个(bind, assert_eq!, spawn)。

list_remote_control_clients_recovers_auth_after_unauthorized147–222 ↗
async fn list_remote_control_clients_recovers_auth_after_unauthorized()

作用:这个测试确认:查询客户端列表时,如果第一次用旧令牌被后台拒绝,代码会重新读取最新登录信息,然后再试一次。这样用户换了新登录信息后,不需要手动重启流程。

数据流:测试先让假服务器准备接收两次请求:第一次要求看到 stale-token 并返回 401 Unauthorized,第二次要求看到 fresh-token 并返回空列表 → 它在临时目录里先保存旧认证,再创建 AuthManager,然后覆盖保存新认证 → 调用 list_remote_control_clients → 函数第一次失败后重新拿认证并第二次请求 → 最终返回空的 RemoteControlClientsListResponse。

调用关系:它直接测试 list_remote_control_clients,而不是通过句柄。empty_client_list 提供第二次成功时的响应正文,AuthManager::shared 用来模拟真实程序读取认证文件的方式。

调用图:调用 4 个内部函数(list_remote_control_clients, empty_client_list, default, shared);外部调用 5 个(default, bind, new, assert_eq!, spawn)。

list_remote_control_clients_retries_unauthorized_only_once225–300 ↗
async fn list_remote_control_clients_retries_unauthorized_only_once()

作用:这个测试确认:遇到 401 Unauthorized 时最多只重试一次。这样如果新令牌也不行,代码不会像坏掉的门铃一样一直请求服务器。

数据流:测试启动假服务器,安排两次请求都返回 401:第一次检查旧令牌,第二次检查新令牌 → 临时认证文件先写旧令牌,AuthManager 建好后再写新令牌 → 调用 list_remote_control_clients → 函数尝试一次恢复鉴权后仍失败 → 返回 PermissionDenied 错误;假服务器还确认没有第三次请求进来。

调用关系:它直接围绕 list_remote_control_clients 的重试规则做验证。测试里的超时等待用来证明流程真的停住了,没有继续把活儿交给网络请求。

调用图:调用 3 个内部函数(list_remote_control_clients, default, shared);外部调用 6 个(default, bind, new, assert!, assert_eq!, spawn)。

revoke_remote_control_client_does_not_retry_forbidden303–338 ↗
async fn revoke_remote_control_client_does_not_retry_forbidden()

作用:这个测试确认:撤销客户端时,如果后台返回 403 Forbidden,也就是“你没有权限”,代码不会重试。因为这不是令牌过期,而是权限不足,再试通常也没用。

数据流:测试先启动假服务器 → 服务器收到撤销请求后返回 403 Forbidden,并带上 x-request-id、cf-ray 这类排查用响应头和 forbidden 正文 → 调用 revoke_remote_control_client → 函数返回 PermissionDenied 错误 → 测试检查错误文字里包含请求地址、状态码、请求编号、cf-ray 和响应正文。

调用关系:它直接测试 revoke_remote_control_client 的错误处理。假服务器只接受一次请求,用这种方式证明 403 不会进入“刷新认证再重试”的流程。

调用图:调用 1 个内部函数(revoke_remote_control_client);外部调用 3 个(bind, assert_eq!, spawn)。

list_remote_control_clients_preserves_decode_error_context341–371 ↗
async fn list_remote_control_clients_preserves_decode_error_context()

作用:这个测试确认:后台虽然返回 200 OK,但响应内容不是合法 JSON 时,错误信息不能只说“解析失败”,还要带上现场信息。这样排查问题时能看到是哪次请求、什么状态、原始正文是什么。

数据流:测试启动假服务器 → 服务器收到列表请求后返回状态 200 OK,但正文只有一个不完整的 { → 调用 list_remote_control_clients → 函数尝试把正文解析成客户端列表时失败 → 返回错误;测试检查错误文字包含响应来源地址、HTTP 200 OK、原始 body 和 decode error。

调用关系:它直接验证 list_remote_control_clients 的解析失败路径。网络请求本身成功,但后续把 JSON 变成程序数据时出错,这个测试确保错误上下文没有在传递过程中丢失。

调用图:调用 1 个内部函数(list_remote_control_clients);外部调用 4 个(default, bind, assert!, spawn)。

app-server-transport/src/transport/remote_control/tests/pairing_tests.rs源码 ↗
testtest

远程控制配对就像把一台机器和一个手机遥控器“认亲”。这件事如果做错,用户可能拿不到配对码,或者旧账号、旧服务器的结果被误用。这个测试文件不连真实后台,而是在本机开一个临时 TCP 监听器(一种本地网络入口),假装后台服务器。测试会先收下程序发来的 HTTP 请求,检查地址、请求体和授权头,再返回成功、失败、坏 JSON、401 未授权等不同结果。它重点验证几件事:配对开始前会先刷新快过期的服务器令牌;查询配对状态时能接受普通配对码和手动配对码;后台错误会变成用户能理解的错误;账号变化、旧 enrollment(已登记的远程控制服务器信息)等边界情况不会污染当前状态。整体上,它像给配对流程装了一套“情景演练器”。

函数细节20
remote_control_enrollment8–25 ↗
fn remote_control_enrollment(
    remote_control_url: &str,
    remote_control_token: &str,
) -> RemoteControlEnrollment

作用:造出一个测试用的远程控制 enrollment,也就是“这台服务器已经登记到某个远程控制后台”的假资料。其他测试用它来避免每次都手写一大坨固定字段。

数据流:进去的是远程控制后台地址和服务器令牌 → 函数把地址规范化,并填入固定的账号、环境、服务器名、服务器 ID、过期时间 → 出来一个完整的 RemoteControlEnrollment 测试对象,后续可以直接拿去发配对或状态查询请求。

调用关系:这是测试里的小工厂函数。pairing_error、pairing_response_error、pairing_status_error,以及几个配对状态测试都会先找它要一份 enrollment;它内部只借用时间解析函数把固定的未来时间戳变成时间对象。

调用图:被 6 处调用(pairing_error, pairing_response_error, pairing_status_error, remote_control_pairing_status_accepts_manual_pairing_code, remote_control_pairing_status_returns_claimed, remote_control_pairing_status_returns_pending);外部调用 1 个(from_unix_timestamp)。

pairing_error27–52 ↗
async fn pairing_error(status: &'static str, body: &'static str) -> (String, String)

作用:搭一个会让“开始配对”失败的假后台,并把程序最终报出的错误文字拿回来。它用来验证错误信息里有没有保留后台状态码、请求 ID、Cloudflare ray 等排查线索。

数据流:进去的是假后台要返回的 HTTP 状态和响应正文 → 函数开一个本地监听端口,启动一个异步任务等待配对请求并返回指定错误 → 再用测试 enrollment 发起配对,拿到失败结果 → 出来错误字符串和本次应访问的配对地址。

调用关系:它是两个错误上下文测试的公共铺垫。start_remote_control_pairing_preserves_backend_error_context 和 start_remote_control_pairing_preserves_decode_error_context 调它;它自己会调用 remote_control_enrollment,并用 bind、spawn 建立假服务器。

调用图:调用 1 个内部函数(remote_control_enrollment);被 2 处调用(start_remote_control_pairing_preserves_backend_error_context, start_remote_control_pairing_preserves_decode_error_context);外部调用 2 个(bind, spawn)。

pairing_response_error54–70 ↗
async fn pairing_response_error(body: serde_json::Value) -> String

作用:模拟“开始配对时后台返回了 JSON,但这个 JSON 内容不合格”的场景。测试借它确认程序不会把坏数据当成好配对结果。

数据流:进去的是一段 JSON 响应 → 函数开本地假服务器,收到配对请求后原样返回这段 JSON → 客户端尝试解析并校验,预期失败 → 出来最终的错误文字。

调用关系:它服务于配对响应校验类测试,尤其是过期时间解析失败的测试。它会先用 remote_control_enrollment 造客户端资料,再用本地监听器和异步任务模拟后台。

调用图:调用 1 个内部函数(remote_control_enrollment);被 1 处调用(start_remote_control_pairing_preserves_expiry_parse_error_context);外部调用 2 个(bind, spawn)。

pairing_status_error72–100 ↗
async fn pairing_status_error(status: &'static str, body: &'static str) -> (io::Error, String)

作用:搭一个会让“查询配对状态”失败的假后台。它帮助测试确认不同后台错误会被翻译成合适的本地错误类型,并且坏响应的上下文不会丢。

数据流:进去的是状态码和响应正文 → 函数启动本地假后台,收到状态查询请求后返回这些内容和排查用响应头 → 客户端查询配对状态并预期失败 → 出来 io::Error 错误对象和应该访问的状态查询地址。

调用关系:它被 remote_control_pairing_status_maps_user_actionable_backend_errors 和 remote_control_pairing_status_preserves_decode_error_context 使用。内部会调用 remote_control_enrollment,并用 bind、spawn 准备一次性假服务。

调用图:调用 1 个内部函数(remote_control_enrollment);被 2 处调用(remote_control_pairing_status_maps_user_actionable_backend_errors, remote_control_pairing_status_preserves_decode_error_context);外部调用 2 个(bind, spawn)。

remote_control_handle_starts_pairing_before_websocket_connects103–190 ↗
async fn remote_control_handle_starts_pairing_before_websocket_connects()

作用:验证即使远程控制 WebSocket(长期网络连接)还没连上,也能先用当前 enrollment 开始配对。这样用户点“配对”时不会被连接状态卡住。

数据流:测试先开假后台,并把当前 enrollment 的过期时间改成快过期 → 程序开始配对时先发刷新令牌请求,再用新令牌发配对请求 → 假后台返回配对码和手动配对码 → 测试确认得到的响应字段完全正确。

调用关系:这是 Tokio 异步测试运行器直接执行的测试。它自己搭假服务器,检查刷新请求和配对请求,然后驱动 remote_handle.start_pairing 走完整流程。

调用图:外部调用 6 个(now_utc, bind, assert_eq!, json!, seconds, spawn)。

remote_control_pairing_status_returns_pending193–226 ↗
async fn remote_control_pairing_status_returns_pending()

作用:验证配对状态为“还没人认领”时,程序能正确返回 claimed=false。也就是配对码还在等用户输入或确认。

数据流:测试开假后台 → 客户端带服务器令牌发送 pairing_code 查询 → 假后台返回 {"claimed": false} → 函数确认程序解析出的结果确实是未认领。

调用关系:这是测试运行器执行的独立用例。它调用 remote_control_enrollment 创建测试 enrollment,并通过本地假服务器检查请求路径、授权头和请求体。

调用图:调用 1 个内部函数(remote_control_enrollment);外部调用 5 个(bind, assert!, assert_eq!, json!, spawn)。

remote_control_pairing_status_accepts_manual_pairing_code229–258 ↗
async fn remote_control_pairing_status_accepts_manual_pairing_code()

作用:验证查询配对状态时可以使用手动配对码。手动配对码就是给人看的短码,比如 ABCD-EFGH,不是内部长配对码。

数据流:测试准备一个只带 manual_pairing_code 的状态查询 → 假后台收到请求后检查请求体里确实是手动码 → 返回未认领 → 测试确认程序接受这种输入并正常返回。

调用关系:这是配对状态输入格式的测试。它使用 remote_control_enrollment 造 enrollment,再由假服务器观察客户端到底发了什么。

调用图:调用 1 个内部函数(remote_control_enrollment);外部调用 5 个(bind, assert!, assert_eq!, json!, spawn)。

remote_control_pairing_status_returns_claimed261–285 ↗
async fn remote_control_pairing_status_returns_claimed()

作用:验证当后台说配对码已经被认领时,程序会把 claimed=true 原样告诉上层。这样界面或调用方就知道配对已经成功完成。

数据流:测试开一个假后台 → 客户端发送配对状态查询 → 假后台返回 {"claimed": true} → 测试检查响应里的 claimed 是 true。

调用关系:这是测试运行器直接调用的成功路径用例。它通过 remote_control_enrollment 创建请求来源,并用本地假服务器返回已认领状态。

调用图:调用 1 个内部函数(remote_control_enrollment);外部调用 5 个(bind, assert!, assert_eq!, json!, spawn)。

remote_control_handle_refreshes_after_pairing_status_auth_failure288–348 ↗
async fn remote_control_handle_refreshes_after_pairing_status_auth_failure()

作用:验证查询配对状态时如果服务器令牌失效,程序会自动刷新令牌后再试一次。这样用户不用因为一次 401 未授权就手动重登或重配。

数据流:假后台先收到一次带旧令牌的状态查询,并返回 401 → 程序随后发刷新请求拿到新服务器令牌 → 再用新令牌查询状态 → 假后台返回已认领 → 测试确认最终结果成功。

调用关系:这是 remote_handle.pairing_status 的恢复能力测试。测试运行器启动它;它自己组织三次假后台交互,检查程序有没有按“失败、刷新、重试”的顺序走。

调用图:外部调用 5 个(bind, assert!, assert_eq!, json!, spawn)。

remote_control_pairing_status_maps_user_actionable_backend_errors351–360 ↗
async fn remote_control_pairing_status_maps_user_actionable_backend_errors()

作用:验证查询配对状态时,常见后台错误会变成有意义的本地错误类别。比如 403 是没权限,404/410 更像输入或配对信息已经不可用。

数据流:测试依次喂入 403、404、410 三种假后台响应 → 每次通过 pairing_status_error 触发一次失败查询 → 拿到 io::Error 后检查它的错误种类是否符合预期。

调用关系:它是 pairing_status_error 的调用者,用循环覆盖多个状态码。它不关心请求细节,只关心最终错误分类是否方便上层处理和提示用户。

调用图:调用 1 个内部函数(pairing_status_error);外部调用 1 个(assert_eq!)。

remote_control_pairing_status_preserves_decode_error_context363–374 ↗
async fn remote_control_pairing_status_preserves_decode_error_context()

作用:验证配对状态响应如果不是合法 JSON,错误信息会保留足够上下文。这样排查时能看到访问了哪个地址、状态码、请求 ID、响应体等。

数据流:测试让假后台返回 200 OK 但正文是坏 JSON“{” → 程序解析失败 → 测试把错误转成文字,检查里面包含 URL、HTTP 状态、request-id、cf-ray、body 和 decode error。

调用关系:它调用 pairing_status_error 来搭失败场景。这个测试站在排障角度,确认底层解析失败不会只留下一个模糊的“解析错了”。

调用图:调用 1 个内部函数(pairing_status_error);外部调用 1 个(assert!)。

remote_control_handle_refreshes_after_pairing_auth_failure377–459 ↗
async fn remote_control_handle_refreshes_after_pairing_auth_failure()

作用:验证开始配对时如果服务器令牌失效,程序会刷新令牌并重新发起配对。这样旧令牌不会直接导致用户配对失败。

数据流:假后台先收到带旧服务器令牌的配对请求并返回 401 → 程序用用户访问令牌发刷新请求 → 假后台给新服务器令牌 → 程序再次配对并成功拿到配对码 → 测试确认最终响应正确。

调用关系:这是 remote_handle.start_pairing 的认证恢复测试。它由测试运行器触发,自己搭建三步服务器对话,重点检查授权头从旧令牌变成刷新后的新令牌。

调用图:外部调用 5 个(bind, default, assert_eq!, json!, spawn)。

remote_control_handle_recovers_auth_before_refreshing_pairing462–584 ↗
async fn remote_control_handle_recovers_auth_before_refreshing_pairing()

作用:验证如果刷新服务器令牌时发现用户访问令牌也旧了,程序会先重新加载认证信息,再继续刷新和配对。它防止“本地文件已有新 token,但内存还拿旧 token”导致失败。

数据流:测试先把旧访问令牌写入临时认证文件并创建 AuthManager → 再把文件改成新访问令牌 → 假后台先拒绝旧令牌刷新请求,再接受新令牌刷新请求,并返回新服务器令牌 → 程序用新服务器令牌配对成功 → 输出正确配对响应。

调用关系:这是更复杂的恢复链路测试。它会用 AuthManager::shared 建认证管理器,用临时目录模拟真实认证文件变化,再驱动 start_pairing 验证“旧认证失败、重新加载、刷新、配对”的完整顺序。

调用图:调用 2 个内部函数(default, shared);外部调用 8 个(now_utc, bind, new, default, assert_eq!, json!, seconds, spawn)。

start_remote_control_pairing_preserves_backend_error_context587–597 ↗
async fn start_remote_control_pairing_preserves_backend_error_context()

作用:验证开始配对时后台直接报错,最终错误文字会完整保留后台给的排查信息。没有这些信息,线上问题会很难定位。

数据流:测试让 pairing_error 模拟 503 Service Unavailable 和正文“pairing unavailable” → 拿到错误字符串 → 和预期文本逐字比较,确认 URL、HTTP 状态、request-id、cf-ray、body 都在。

调用关系:它是 pairing_error 的一个使用者,专门覆盖“后台返回非成功状态码”的场景。它不自己搭服务器,而是把搭环境的活交给 helper。

调用图:调用 1 个内部函数(pairing_error);外部调用 1 个(assert_eq!)。

start_remote_control_pairing_preserves_decode_error_context600–609 ↗
async fn start_remote_control_pairing_preserves_decode_error_context()

作用:验证开始配对时后台返回了成功状态但内容无法解析,错误里仍然有完整上下文。这样能区分“后台挂了”和“后台返回格式坏了”。

数据流:测试调用 pairing_error,让假后台返回 200 OK 但正文是坏 JSON → 程序解析配对响应失败 → 测试检查错误文字里包含配对 URL、状态码、request-id、cf-ray、body 和解析错误说明。

调用关系:它复用 pairing_error 来制造坏响应。这个测试补上了成功状态码但内容错误的边界情况。

调用图:调用 1 个内部函数(pairing_error);外部调用 1 个(assert!)。

start_remote_control_pairing_rejects_mismatched_backend_enrollment612–624 ↗
async fn start_remote_control_pairing_rejects_mismatched_backend_enrollment()

作用:验证后台返回的配对结果如果属于别的服务器或别的环境,程序会拒绝。这样不会把 A 服务器的配对码误当成 B 服务器的。

数据流:测试构造一个 server_id 和 environment_id 都不匹配的配对响应 → 触发配对解析失败 → 检查错误文字明确写出期望值和实际值。

调用关系:这是配对响应安全校验测试。它通过 pairing_response_error 的同类思路验证 enrollment 身份必须匹配,避免跨服务器、跨环境混淆。

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

start_remote_control_pairing_preserves_expiry_parse_error_context627–643 ↗
async fn start_remote_control_pairing_preserves_expiry_parse_error_context()

作用:验证配对响应里的过期时间 expires_at 如果格式坏了,错误信息会说明是哪段响应导致的。过期时间关系到配对码有效期,不能随便忽略。

数据流:测试把 expires_at 设成“not-a-timestamp” → pairing_response_error 返回这个响应并触发解析失败 → 测试确认错误里有 HTTP 200、缺失的请求头标记、原始 expires_at 字段和时间解析错误。

调用关系:它调用 pairing_response_error 来制造坏 JSON 内容。重点不是请求是否发对,而是坏时间字段的错误上下文是否足够清楚。

调用图:调用 1 个内部函数(pairing_response_error);外部调用 2 个(assert!, json!)。

remote_control_handle_disable_keeps_current_enrollment646–659 ↗
async fn remote_control_handle_disable_keeps_current_enrollment()

作用:验证把远程控制设为禁用时,不会把当前选中的 enrollment 清掉。这样用户暂时关掉功能后,再打开还能记得原来要连哪台远程控制服务器。

数据流:测试创建一个带当前 enrollment 的 remote_handle → 把期望状态改成 Disabled → 读取 current_enrollment → 确认它仍然存在。

调用关系:这是状态切换的小测试。它不需要假网络服务器,只检查 desired_state_tx 发送禁用状态后,remote_handle 内部保存的 enrollment 是否被保留。

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

remote_control_handle_reenrolls_after_stale_pairing_enrollment662–787 ↗
async fn remote_control_handle_reenrolls_after_stale_pairing_enrollment()

作用:验证如果旧 enrollment 已经过时,配对请求被后台判定找不到,程序会重新登记服务器并再次配对。这样保存过的旧登记不会让用户卡死。

数据流:测试先把旧 enrollment 写进临时状态库 → 开始配对时假后台对旧服务器令牌返回 404 → 程序转去 enroll 重新登记,拿到新服务器 ID、环境 ID 和新令牌 → 再用新令牌配对成功 → 最后确认状态库里保存的是刷新后的 enrollment,并且启用偏好还在。

调用关系:这是 start_pairing 的旧登记恢复测试。它同时用到本地状态数据库和假后台,串起“旧配对失败、重新 enroll、再配对、写回持久状态”这一整条链。

调用图:外部调用 6 个(bind, new, default, assert_eq!, json!, spawn)。

remote_control_handle_discards_pairing_response_after_auth_change790–854 ↗
async fn remote_control_handle_discards_pairing_response_after_auth_change()

作用:验证配对请求发出去后,如果本地账号已经切换,旧请求返回的配对结果会被丢弃。这样不会把上一个账号的配对码显示给新账号。

数据流:测试先保存 account_id 的认证并开始配对 → 在假后台还没响应时,把认证文件改成 next_account_id 并让 AuthManager 重新加载 → 再让旧配对请求返回成功 JSON → 程序发现认证已变,拒绝这个过时结果 → 测试确认错误是“等待 enrollment 完成前不可用”。

调用关系:这是并发和账号切换安全测试。它用 spawn 让配对请求挂起,同时手动改认证状态;等响应回来后,检查 remote_handle.start_pairing 没有接受这份已经过期的结果。

调用图:调用 2 个内部函数(default, shared);外部调用 6 个(bind, new, default, assert_eq!, json!, spawn)。

分析捕获辅助工具

这些辅助模块和专项测试定义插件分析捕获的生成、读取和验证方式。

app-server-test-client/src/plugin_analytics_capture.rs源码 ↗
testtest validation

这个文件主要服务于测试客户端:它从一个捕获文件里读出一行行 JSON 记录,JSON 可以理解成一种常见的结构化文本格式,然后只挑出某个远程插件的事件。没有它,测试只能看到表面结果,没法确认后台有没有正确上报“插件安装”等关键行为。它先安全地读文件:如果文件还不存在,就当作暂时没有事件,而不是直接报错;如果文件存在但内容坏了,就给出带文件名和行号的错误,方便查问题。接着,它会检查事件里是否真的有指定插件的编号、名字、市场名称,以及一些必须存在的字段。整体上,它像一个质检员:先从日志堆里筛出目标插件的单据,再逐项核对单据内容是不是完整、是不是写对了。

函数细节4
read_events_for_remote_plugin9–43 ↗
fn read_events_for_remote_plugin(
    path: &Path,
    remote_plugin_id: &str,
) -> Result<Vec<Value>>

作用:读取埋点捕获文件,并从里面找出属于某个远程插件的事件。测试等待插件事件时会用它来确认目标插件的记录是否已经出现。

数据流:进去的是一个文件路径和一个远程插件编号。它先尝试把文件读成文本;如果文件不存在,就返回空列表,表示还没有捕获到事件。然后它逐行解析 JSON,取出每行里的 events 数组,只保留 event_params.plugin_id 等于目标插件编号的事件,最后把这些匹配事件作为列表返回;如果读文件或解析 JSON 出错,会返回带上下文的错误。

调用关系:它会被 wait_for_remote_plugin_event 调用,通常是在测试不断等待某个插件事件出现的时候。它自己把底层活儿交给系统读文件函数 read_to_string 和 JSON 解析函数 from_str,然后把筛好的事件交回给上层继续判断。

调用图:被 1 处调用(wait_for_remote_plugin_event);外部调用 3 个(new, read_to_string, from_str)。

validate_mutation_events51–69 ↗
fn validate_mutation_events(
    events: Vec<Value>,
    expected: PluginEventIdentity<'_>,
) -> Result<Vec<Value>>

作用:检查一组事件里是否刚好有一个“插件已安装”事件,并确认这个事件写的是期望中的插件。它用于防止测试误把多报、漏报或报错插件的情况当成成功。

数据流:进去的是一组 JSON 事件,以及期望的插件身份信息,包括插件编号、插件名和市场名。它先筛出 event_type 等于 codex_plugin_installed 的事件;如果不是刚好一个,就直接报错。找到唯一事件后,它把事件交给 validate_event 做更细的字段核对,成功后返回只包含这个合格事件的新列表。

调用关系:它位于事件读取之后、测试断言之前。上层拿到 read_events_for_remote_plugin 返回的事件后,可以调用它做最终质检;它内部再调用 validate_event 来检查事件内容,遇到数量不对时用 bail! 立即中止并给出错误。

调用图:调用 1 个内部函数(validate_event);外部调用 2 个(bail!, vec!)。

validate_event71–90 ↗
fn validate_event(event: &Value, expected: &PluginEventIdentity<'_>) -> Result<()>

作用:逐项检查单个插件安装事件的内容是否正确、是否完整。它确保事件里的插件编号、名字、市场名对得上,并且一些关键字段没有缺失。

数据流:进去的是一个事件 JSON 和期望的插件身份信息。它先进入事件的 event_params 部分,分别检查 plugin_id、plugin_name、marketplace_name 是否等于期望值;然后检查 has_skills、mcp_server_count、connector_ids、product_client_id 这些字段是否存在且不是空值。全部通过就返回成功;任何一项不对都会返回错误。

调用关系:它只服务于 validate_mutation_events,是更细粒度的质检步骤。字符串字段的比较交给 require_string 来做,非字符串但必须存在的字段则由它自己检查;发现缺失或空值时用 bail! 报错。

调用图:调用 1 个内部函数(require_string);被 1 处调用(validate_mutation_events);外部调用 1 个(bail!)。

require_string92–98 ↗
fn require_string(params: &Value, field: &str, expected: &str) -> Result<()>

作用:检查某个 JSON 字段是不是指定的字符串值。它把常见的“字段不对”判断集中起来,避免每个字段都写一遍重复代码。

数据流:进去的是事件参数 JSON、字段名和期望字符串。它按字段名取出实际值,并尝试把它当成字符串;如果实际字符串和期望值一致,就返回成功。如果字段不存在、不是字符串,或者值不一样,就返回一条说明期望值和实际值的错误。

调用关系:它是 validate_event 的小帮手,专门负责 plugin_id、plugin_name、marketplace_name 这类文本字段的核对。validate_event 每检查一个字符串字段就调用它一次,它发现问题时会用 bail! 直接生成错误返回给上层。

调用图:被 1 处调用(validate_event);外部调用 2 个(get, bail!)。

app-server-test-client/src/plugin_analytics_capture_tests.rs源码 ↗
testtest run

这个文件是一组自动测试,专门盯住插件分析事件的读取和校验。这里的“分析事件”可以理解成系统写下的操作记录,比如某个插件被安装了。测试先造出假的事件数据,有的属于目标插件,有的是干扰项,然后写进临时文件,再调用真正的读取函数,只拿目标插件的事件。接着它用校验函数确认事件内容完整,比如插件编号、插件名、市场名,以及能力信息是否都在。它还故意制造两种坏情况:同一个安装事件出现两次、关键能力字段缺失,确保系统会报错而不是默默放过。辅助函数负责造标准事件、造期望的插件身份,以及生成不会和别的测试撞名的临时文件路径。

函数细节6
reads_and_validates_remote_plugin_mutation_events14–40 ↗
fn reads_and_validates_remote_plugin_mutation_events()

作用:这个测试确认:系统能从捕获文件里读出目标远程插件的变更事件,并且能通过完整性校验。它还顺便确认无关插件的事件会被过滤掉。

数据流:进去的是测试自己造出来的一份临时事件文件,里面一行是无关插件事件,另一行是目标插件安装事件。函数把这些 JSON 文本写到临时文件里,再调用 read_events_for_remote_plugin 读取目标插件的事件,接着调用 validate_mutation_events 做校验。出来的结果应该只剩目标插件那一条安装事件;最后它删除临时文件,避免留下测试垃圾。

调用关系:它是这组测试里最接近真实流程的一条:先用 unique_capture_path 找临时文件名,用 mutation_event 造目标事件,用 expected_identity 给校验函数提供“正确插件身份”。然后把核心工作交给 read_events_for_remote_plugin 和 validate_mutation_events,最后用断言确认结果正确。

调用图:调用 3 个内部函数(expected_identity, mutation_event, unique_capture_path);外部调用 6 个(assert_eq!, remove_file, write, json!, read_events_for_remote_plugin, validate_mutation_events)。

rejects_duplicate_mutation_events43–49 ↗
fn rejects_duplicate_mutation_events()

作用:这个测试确认:如果同一种插件变更事件重复出现,校验会失败。这样可以防止系统把一次安装误看成多次安装。

数据流:进去的是同一个安装事件的两份拷贝,以及期望的插件身份信息。函数把这两个重复事件交给 validate_mutation_events。出来的应该不是成功结果,而是一个错误;测试再检查错误文字里提到了“found 2”,说明它确实发现了两个重复项。

调用关系:它用 mutation_event 造重复数据,用 expected_identity 提供校验基准,然后专门考 validate_mutation_events 的防重能力。这个测试不碰文件,只直接验证校验环节。

调用图:调用 2 个内部函数(expected_identity, mutation_event);外部调用 3 个(assert!, validate_mutation_events, vec!)。

rejects_missing_capability_metadata52–59 ↗
fn rejects_missing_capability_metadata()

作用:这个测试确认:插件事件如果缺少能力信息,校验会失败。能力信息指插件有没有技能、服务器数量、连接器等用来描述插件能做什么的数据。

数据流:进去的是一条本来完整的安装事件。函数先把 event_params 里的 has_skills 改成空值,故意模拟“关键字段缺失”。然后它把这条坏事件交给 validate_mutation_events。出来的应该是错误,并且错误信息里要提到 has_skills,说明系统指出了缺哪一项。

调用关系:它先借 mutation_event 生成标准事件,再手动破坏其中一个关键字段,最后交给 validate_mutation_events。它和重复事件测试一样,重点不是读文件,而是确认校验规则够严格。

调用图:调用 2 个内部函数(expected_identity, mutation_event);外部调用 3 个(assert!, validate_mutation_events, vec!)。

mutation_event61–74 ↗
fn mutation_event(event_type: &str) -> Value

作用:这个辅助函数用来快速造一条标准的插件事件。测试不用每次手写一大段 JSON,直接传入事件类型就能得到结构完整的事件。

数据流:进去的是 event_type,也就是事件名字,比如“插件已安装”。函数把这个名字和固定的插件编号、插件名、市场名、能力信息等拼成一个 JSON 值。出来的是一条可以直接拿去写文件或交给校验函数的事件数据。

调用关系:它被三个测试共同使用,是测试数据的“模板工厂”。reads_and_validates_remote_plugin_mutation_events 用它造正常安装事件,另外两个测试先用它造正常事件,再复制或改坏,来检查校验函数是否能发现问题。

调用图:被 3 处调用(reads_and_validates_remote_plugin_mutation_events, rejects_duplicate_mutation_events, rejects_missing_capability_metadata);外部调用 1 个(json!)。

expected_identity76–82 ↗
fn expected_identity() -> PluginEventIdentity<'static>

作用:这个辅助函数给出测试里那只目标插件的标准身份信息。校验时会拿事件里的插件编号、名称、市场名和这里的标准答案对照。

数据流:它不需要外部输入,直接返回一个 PluginEventIdentity。这个返回值包含固定的插件编号、插件名和市场名。它不改动任何外部状态,只提供校验用的参照物。

调用关系:它被三个测试传给 validate_mutation_events。可以把它理解成考卷的答案卡:事件数据是考生答案,校验函数拿它来判断事件是不是属于正确的插件、信息是不是一致。

调用图:被 3 处调用(reads_and_validates_remote_plugin_mutation_events, rejects_duplicate_mutation_events, rejects_missing_capability_metadata)。

unique_capture_path84–93 ↗
fn unique_capture_path(name: &str) -> PathBuf

作用:这个辅助函数生成一个临时文件路径,用来存放测试里的事件捕获内容。它避免多个测试或多次运行时使用同一个文件名而互相干扰。

数据流:进去的是一个简短名字,比如 valid。函数读取当前时间的纳秒数,再加上当前进程编号,并放到系统临时目录下面,拼成一个看起来很独特的 jsonl 文件名。出来的是一个 PathBuf,也就是 Rust 里表示文件路径的对象。

调用关系:它只被 reads_and_validates_remote_plugin_mutation_events 使用。那个测试需要真的写一个捕获文件,所以先找它要一个安全的临时路径,再把事件内容写进去,测试结束后再删除。

调用图:被 1 处调用(reads_and_validates_remote_plugin_mutation_events);外部调用 3 个(now, format!, temp_dir)。

app-server-test-client/src/loopback_responses_server.rs源码 ↗
test测试启动时创建,测试请求期间响应,测试结束时关闭

这个文件解决的是测试时“不能依赖真实网络服务”的问题。它会在 127.0.0.1 上临时开一个 TCP 端口,像一个很小的 HTTP 服务器一样等请求。外部代码启动它后,可以拿到 base_url,也就是这个假服务器的网址。测试客户端把请求发到这里,如果请求是 POST 到 /responses,它就返回一段固定的事件流,模拟 Responses API 创建并完成响应;如果不是这个地址,就返回 404。这里没有完整实现一个大而全的 Web 服务器,只做测试需要的最小功能。它还带了自动收尾:对象被丢弃时会通知后台线程停下来,并等待线程退出,避免测试结束后还留着一个“幽灵服务器”占端口或跑后台。

函数细节7
LoopbackResponsesServer::start22–54 ↗
fn start() -> Result<Self>

作用:启动这个本机假服务器,并返回一个可以查询地址、还能在结束时自动关闭的服务器对象。测试流程会用它来替代真正的外部 Responses API。

数据流:进去时不需要传参数;它会绑定到本机 127.0.0.1 的一个随机空闲端口,把监听器设成非阻塞模式,再开一个后台线程反复等待连接。出来的是 LoopbackResponsesServer,里面保存了服务器网址、关闭开关,以及后台线程的句柄。

调用关系:它由 run 调用,通常是在测试或测试客户端启动流程里先把假服务器架起来。启动后,后台线程每收到一个连接,就把连接交给 handle_model_connection 处理;如果暂时没有连接,就短暂睡一下,避免一直空转占 CPU。

调用图:被 1 处调用(run);外部调用 6 个(clone, new, new, bind, format!, spawn)。

LoopbackResponsesServer::base_url56–58 ↗
fn base_url(&self) -> &str

作用:把这个假服务器的网址告诉调用方。测试客户端需要这个地址,才能把原本要发给真实服务的请求改发到本机假服务。

数据流:进去的是已经启动好的 LoopbackResponsesServer;它只读取里面保存的 base_url 字符串引用,不修改任何状态。出来的是类似 http://127.0.0.1:某端口 的地址文本。

调用关系:它通常在 start 之后使用。start 负责把服务器真正开起来,base_url 则像把“门牌号”递给测试客户端,让后续请求能找到这个本机服务器。

LoopbackResponsesServer::drop62–67 ↗
fn drop(&mut self)

作用:在服务器对象不用时自动关掉后台线程。这样测试结束后不会留下还在运行的监听线程。

数据流:进去的是即将被销毁的 LoopbackResponsesServer;它先把 shutdown 这个原子布尔值(一种线程间安全共享的开关)设为 true,然后取出后台线程句柄并等待线程结束。出来没有返回值,但副作用是服务器循环会停,线程会被回收。

调用关系:它不是手动调用的普通函数,而是 Rust 在对象生命周期结束时自动触发的清理动作。start 创建后台线程,drop 负责给这个线程发“该下班了”的信号,并等它收工。

handle_model_connection70–95 ↗
fn handle_model_connection(mut stream: TcpStream) -> io::Result<()>

作用:处理一条客户端连进来的 TCP 连接,读出 HTTP 请求,并决定返回模拟成功响应还是 404。它是这个假服务器真正“接待请求”的地方。

数据流:进去的是一个已经建立好的 TCP 连接;它先把连接设为阻塞读取,并设置最多等 2 秒的读超时,然后调用 read_http_request 把请求读完整。接着它看第一行是不是 POST,并且路径里有没有 /responses:匹配就写回一段固定的事件流;不匹配就写回 JSON 格式的 not found。出来是 io::Result,表示这次连接处理成功或失败。

调用关系:它由 start 启动的后台线程在 accept 到连接后调用。它自己不直接拼底层 HTTP 写入细节,而是把读取交给 read_http_request,把响应发送交给 write_http_response,像前台接待员先看请求,再让专门的人读单据、写回执。

调用图:调用 2 个内部函数(read_http_request, write_http_response);外部调用 4 个(from_secs, set_nonblocking, set_read_timeout, concat!)。

read_http_request97–119 ↗
fn read_http_request(stream: &mut TcpStream) -> io::Result<Vec<u8>>

作用:从 TCP 连接里读出一个完整的 HTTP 请求。它会先读请求头,再根据 Content-Length 决定还要不要继续读请求体。

数据流:进去的是可读取的 TCP 流;它一块一块读取字节,直到发现 HTTP 头结束标记,也就是空行 \r\n\r\n。然后它调用 parse_content_length 看请求体有多长,如果有正文,就继续读取到够长或连接关闭。出来的是包含请求头和请求体的原始字节数组。

调用关系:它被 handle_model_connection 调用,负责把网络上的零散字节整理成一份完整请求。读到头部后,它把解析 Content-Length 的小任务交给 parse_content_length。

调用图:调用 1 个内部函数(parse_content_length);被 1 处调用(handle_model_connection);外部调用 2 个(read, new)。

parse_content_length121–131 ↗
fn parse_content_length(headers: &[u8]) -> usize

作用:从 HTTP 请求头里找出 Content-Length,也就是请求正文有多少字节。没有这个值时,它就认为正文长度是 0。

数据流:进去的是请求头的原始字节;它先用宽容的方式转成文字,再逐行查找形如 Content-Length: 123 的行,并把后面的数字转成 usize。出来的是正文长度数字;如果没找到或解析失败,就返回 0。

调用关系:它被 read_http_request 调用。read_http_request 需要知道正文长度,才知道后面还要从连接里读多少数据;parse_content_length 就像帮它读包裹面单上的“重量/长度”信息。

调用图:被 1 处调用(read_http_request);外部调用 1 个(from_utf8_lossy)。

write_http_response133–145 ↗
fn write_http_response(
    stream: &mut TcpStream,
    status: &str,
    content_type: &str,
    body: &str,
) -> io::Result<()>

作用:把状态码、内容类型和正文包装成一个简单的 HTTP 响应并写回客户端。调用方不用自己手写响应头格式。

数据流:进去的是 TCP 连接、HTTP 状态文字、Content-Type 内容类型和响应正文;它计算正文长度,写出 HTTP/1.1 响应头、空行和正文,然后 flush,把缓冲区里的数据真正送出去。出来是 io::Result,表示写入是否成功。

调用关系:它被 handle_model_connection 调用。handle_model_connection 负责决定该回 200 还是 404,而 write_http_response 负责把这个决定变成客户端能读懂的 HTTP 数据。

调用图:被 1 处调用(handle_model_connection);外部调用 2 个(flush, write!)。

测试客户端冒烟工作流

此序列先引入可复用的测试客户端框架,然后将其用于非破坏性和破坏性插件分析冒烟场景。

app-server-test-client/src/lib.rs源码 ↗
entrypointstartup, request handling, teardown

可以把这个文件理解成一台“验收用遥控器”。Codex app-server 是后台服务,平时通过 JSON-RPC(一种用 JSON 表达请求和响应的通信格式)和客户端说话;这个测试客户端就负责扮演客户端,按真实流程发请求、读通知、自动回答服务器抛来的审批问题。它既能通过标准输入输出启动一个私有 app-server,也能连到 WebSocket(一条持续打开的网络连接)服务器。上层的 run 负责解析命令行,决定要做哪种测试;with_client 负责准备追踪信息和连接;CodexClient 负责真正收发协议消息。文件还包含一些安全和验证逻辑,比如动态工具只能在支持实验接口的命令里使用、审批测试会统计是否真的收到了预期次数、长时间命令测试会确认“暂停等待用户确认”不会被超时误杀。

函数细节70
run315–517 ↗
async fn run() -> Result<()>

作用:这是整个测试客户端的总入口。它读取命令行参数,然后按用户选择的子命令去启动服务器、发消息、测试登录、列模型或跑专项冒烟测试。

数据流:进去的是命令行参数和环境变量 → 它先解析动态工具参数,再根据子命令检查哪些参数能不能用,解析连接方式 → 出来的是对应测试流程的执行结果;如果参数冲突或流程失败,就返回错误。

调用关系:它站在最外层,负责把用户输入分派给 serve、send_message_v2_with_policies、with_client 包装的各类流程,以及插件测试模块。它会先用 resolve_endpoint 或 resolve_shared_websocket_url 决定连哪里。

调用图:调用 26 个内部函数(ensure_dynamic_tools_unused, get_account_rate_limits, live_elicitation_timeout_pause, model_list, no_trigger_cmd_approval, parse_dynamic_tools_arg, from_flag, run, run_cleanup, run (+15 more));外部调用 2 个(parse, bail!)。

resolve_endpoint529–540 ↗
fn resolve_endpoint(codex_bin: Option<PathBuf>, url: Option<String>) -> Result<Endpoint>

作用:这个函数决定客户端应该怎么连接 app-server:是自己启动一个 codex 进程,还是连接已有的 WebSocket 地址。

数据流:进去的是可选的 codex 可执行文件路径和可选 URL → 它检查两者不能同时出现,并选择一种连接方式 → 出来是 Endpoint,表示“启动进程”或“连网络地址”。

调用关系:run 在多数子命令开始前调用它。后面 CodexClient::connect 会根据这个 Endpoint 真的建立连接。

调用图:被 1 处调用(run);外部调用 3 个(ConnectWs, SpawnCodex, bail!)。

resolve_shared_websocket_url542–554 ↗
fn resolve_shared_websocket_url(
    codex_bin: Option<PathBuf>,
    url: Option<String>,
    command: &str,
) -> Result<String>

作用:这个函数专门给必须连接共享 WebSocket 服务器的命令用。它防止用户传 --codex-bin,因为那会启动私有服务器,达不到修改已有线程状态的目的。

数据流:进去的是可选 codex 路径、可选 URL 和命令名 → 它拒绝 codex 路径,缺 URL 时用默认地址 → 出来是最终 WebSocket URL。

调用关系:run 在 thread-increment-elicitation 和 thread-decrement-elicitation 这类命令中调用它,然后交给对应函数直接连接服务器。

调用图:被 1 处调用(run);外部调用 1 个(bail!)。

BackgroundAppServer::spawn557–587 ↗
fn spawn(codex_bin: &Path, config_overrides: &[String]) -> Result<Self>

作用:它在后台临时启动一个 WebSocket 版 app-server,主要给现场测试用。这样测试不必要求用户提前手动开好服务器。

数据流:进去的是 codex 程序路径和配置覆盖项 → 它先找一个空闲本地端口,拼出监听 URL,再启动 codex app-server --listen → 出来是 BackgroundAppServer,里面保存子进程和 URL。

调用关系:live_elicitation_timeout_pause 在需要临时服务器时调用它。它创建的进程会在 BackgroundAppServer::drop 里自动清理。

调用图:被 1 处调用(live_elicitation_timeout_pause);外部调用 8 个(from, parent, inherit, null, bind, new, format!, var_os)。

BackgroundAppServer::drop591–599 ↗
fn drop(&mut self)

作用:这是后台服务器对象被丢弃时的清理动作。它确保临时启动的 app-server 不会留在系统里变成“僵尸进程”。

数据流:进去的是对象里保存的子进程 → 它先看进程是否已退出;没退出就杀掉并等待回收 → 结果是后台进程被清理。

调用关系:Rust 会在 BackgroundAppServer 生命周期结束时自动调用它,尤其是 live_elicitation_timeout_pause 测试结束或出错时。

调用图:外部调用 4 个(kill, try_wait, wait, println!)。

serve602–647 ↗
fn serve(codex_bin: &Path, config_overrides: &[String], listen: &str, kill: bool) -> Result<()>

作用:这个函数把 app-server 作为一个长期后台服务启动起来,并把日志写到临时目录。它适合先开服务,再用其他命令连接测试。

数据流:进去的是 codex 路径、配置覆盖项、监听地址和是否清理端口 → 它准备日志目录,必要时杀掉占用端口的旧进程,再用 nohup 启动服务 → 出来是打印给用户的监听地址、进程号和日志路径。

调用关系:run 的 serve 子命令会调用它。若用户要求清端口,它会先交给 kill_listeners_on_same_port。

调用图:调用 1 个内部函数(kill_listeners_on_same_port);被 1 处调用(run);外部调用 8 个(new, from, from, null, new, format!, create_dir_all, println!)。

kill_listeners_on_same_port649–708 ↗
fn kill_listeners_on_same_port(listen: &str) -> Result<()>

作用:它清理已经占用同一个监听端口的进程,避免新 app-server 启动时因为端口被占而失败。

数据流:进去的是监听 URL → 它解析端口,用 lsof 找监听进程,先发普通 kill,等一会儿后还没退出就强杀 → 出来是端口尽量被释放。

调用关系:serve 在用户传了 kill 选项时调用它。它依赖系统命令 lsof 和 kill,所以更像测试工具里的运维辅助。

调用图:被 1 处调用(serve);外部调用 7 个(from_millis, from_utf8_lossy, parse, new, format!, println!, sleep)。

shell_quote710–712 ↗
fn shell_quote(input: &str) -> String

作用:它把一段文本包成适合 shell 命令行使用的安全引号形式。这样路径或参数里有空格、单引号时也不容易把命令拼坏。

数据流:进去的是原始字符串 → 它替换里面的单引号并整体加单引号 → 出来是可放进 shell 命令里的字符串。

调用关系:serve 和 live_elicitation_timeout_pause 在拼 shell 命令时会用它,避免用户路径影响命令结构。

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

send_message722–741 ↗
async fn send_message(
    endpoint: &Endpoint,
    config_overrides: &[String],
    user_message: String,
) -> Result<()>

作用:它实现旧命令 send-message,但实际走的是新版线程/轮次接口。用户给一句话,它帮忙发给 app-server 并打印流式结果。

数据流:进去的是连接目标、配置覆盖项和用户消息 → 它构造一组默认策略,不启用实验接口和动态工具 → 出来是一次完整发送消息流程的结果。

调用关系:run 的 SendMessage 分支调用它。它把真正工作交给 send_message_v2_with_policies。

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

send_message_v2743–758 ↗
async fn send_message_v2(
    codex_bin: &Path,
    config_overrides: &[String],
    user_message: String,
    dynamic_tools: &Option<Vec<DynamicToolSpec>>,
) -> Result<()>

作用:这是给代码内部直接调用的 V2 发送消息入口。它固定通过启动 codex 进程来连接,并启用实验接口。

数据流:进去的是 codex 路径、配置覆盖、消息和动态工具 → 它把路径包装成 SpawnCodex 连接目标 → 出来是 send_message_v2_endpoint 执行后的结果。

调用关系:它是 send_message_v2_endpoint 的便捷包装;调用图里它也可能被其他测试模块使用。

调用图:调用 1 个内部函数(send_message_v2_endpoint);外部调用 2 个(to_path_buf, SpawnCodex)。

send_message_v2_endpoint760–784 ↗
async fn send_message_v2_endpoint(
    endpoint: &Endpoint,
    config_overrides: &[String],
    user_message: String,
    experimental_api: bool,
    dynamic_tools: &Option<Vec<DynamicToolSpec>>,
) -

作用:它是 V2 发送消息的通用入口,支持指定连接方式,也能选择是否启用实验接口。

数据流:进去的是连接目标、配置、消息、实验接口开关和动态工具 → 它先确认动态工具只能配合实验接口使用 → 出来是带合适策略的一次消息发送流程。

调用关系:run 的 SendMessageV2 分支和 send_message_v2 会调用它;实际发送动作继续交给 send_message_v2_with_policies。

调用图:调用 1 个内部函数(send_message_v2_with_policies);被 2 处调用(run, send_message_v2);外部调用 1 个(bail!)。

trigger_zsh_fork_multi_cmd_approval786–890 ↗
async fn trigger_zsh_fork_multi_cmd_approval(
    endpoint: &Endpoint,
    config_overrides: &[String],
    user_message: Option<String>,
    min_approvals: usize,
    abort_on: Option<usize>,
    dyn

作用:它专门测试一个复杂命令是否会触发多次命令审批,并检查审批数量和最终状态是否符合预期。

数据流:进去的是连接信息、提示词、期望最少审批次数、可选的第几次审批取消、动态工具 → 它启动线程和轮次,自动接受或取消审批,收集审批和命令状态 → 出来是通过验证或报错说明哪里不对。

调用关系:run 的 trigger-zsh-fork-multi-cmd-approval 分支调用它。它用 with_client 建连接,再依赖 CodexClient 的初始化、thread_start、turn_start 和 stream_turn。

调用图:调用 1 个内部函数(with_client);被 1 处调用(run);外部调用 1 个(bail!)。

resume_message_v2892–927 ↗
async fn resume_message_v2(
    endpoint: &Endpoint,
    config_overrides: &[String],
    thread_id: String,
    user_message: String,
    dynamic_tools: &Option<Vec<DynamicToolSpec>>,
) -> Result<()>

作用:它恢复一个已有 V2 线程,然后继续往这个线程里发一条新消息。

数据流:进去的是连接目标、配置、线程 ID、用户消息和动态工具参数 → 它拒绝动态工具,连接服务器,恢复线程,启动新轮次并读取结果 → 出来是这次续聊的执行结果。

调用关系:run 的 ResumeMessageV2 分支调用它。它通过 with_client 包住 CodexClient 的 initialize、thread_resume、turn_start 和 stream_turn。

调用图:调用 2 个内部函数(ensure_dynamic_tools_unused, with_client);被 1 处调用(run)。

thread_resume_follow929–948 ↗
async fn thread_resume_follow(
    endpoint: &Endpoint,
    config_overrides: &[String],
    thread_id: String,
) -> Result<()>

作用:它恢复一个已有线程后不退出,而是一直打印服务器发来的通知。适合观察线程后续发生了什么。

数据流:进去的是连接目标、配置和线程 ID → 它初始化客户端,恢复线程,然后进入无限读取通知的循环 → 出来通常没有正常结果,直到用户终止进程或连接出错。

调用关系:run 的 ThreadResume 分支调用它。它把持续监听交给 CodexClient::stream_notifications_forever。

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

watch950–959 ↗
async fn watch(endpoint: &Endpoint, config_overrides: &[String]) -> Result<()>

作用:它只初始化连接,然后持续打印所有收到的服务器消息。像一个协议监听器,方便调试 app-server 在说什么。

数据流:进去的是连接目标和配置 → 它建立客户端、完成初始化握手,然后不断读取通知 → 出来通常靠用户手动中断。

调用关系:run 的 Watch 分支调用它。它使用 with_client,并最终进入 CodexClient::stream_notifications_forever。

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

trigger_cmd_approval961–985 ↗
async fn trigger_cmd_approval(
    endpoint: &Endpoint,
    config_overrides: &[String],
    user_message: Option<String>,
    dynamic_tools: &Option<Vec<DynamicToolSpec>>,
) -> Result<()>

作用:它发送一个会要求执行命令的提示,并故意配置成需要人工审批。用于确认 app-server 的命令审批流程能触发。

数据流:进去的是连接信息、配置、可选提示词和动态工具 → 它准备默认或用户指定的命令提示,设置“按请求审批”和只读沙箱 → 出来是一次会自动处理审批的消息流程结果。

调用关系:run 的 TriggerCmdApproval 分支调用它。它复用 send_message_v2_with_policies 完成通用发送流程。

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

trigger_patch_approval987–1011 ↗
async fn trigger_patch_approval(
    endpoint: &Endpoint,
    config_overrides: &[String],
    user_message: Option<String>,
    dynamic_tools: &Option<Vec<DynamicToolSpec>>,
) -> Result<()>

作用:它发送一个会修改文件的提示,并配置成需要审批。用于测试文件变更审批是不是正常出现和被处理。

数据流:进去的是连接信息、配置、可选提示词和动态工具 → 它构造创建文件的提示,设置审批和只读沙箱策略 → 出来是执行和审批后的结果。

调用关系:run 的 TriggerPatchApproval 分支调用它。它把具体协议流程交给 send_message_v2_with_policies。

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

no_trigger_cmd_approval1013–1032 ↗
async fn no_trigger_cmd_approval(
    endpoint: &Endpoint,
    config_overrides: &[String],
    dynamic_tools: &Option<Vec<DynamicToolSpec>>,
) -> Result<()>

作用:它发送一个执行命令的提示,但不强制开启审批策略。用于验证某些场景下不应弹出命令审批。

数据流:进去的是连接信息、配置和动态工具 → 它用固定提示词并不设置审批/沙箱策略 → 出来是发送消息和流式读取结果。

调用关系:run 的 NoTriggerCmdApproval 分支调用它。它仍然走 send_message_v2_with_policies,只是策略为空。

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

send_message_v2_with_policies1034–1075 ↗
async fn send_message_v2_with_policies(
    endpoint: &Endpoint,
    config_overrides: &[String],
    user_message: String,
    policies: SendMessagePolicies<'_>,
) -> Result<()>

作用:这是发 V2 消息的核心模板。它把“初始化、开线程、开轮次、流式读取结果”这套固定步骤串起来,同时允许传入审批和沙箱策略。

数据流:进去的是连接目标、配置、用户消息和策略集合 → 它连接客户端,初始化,创建线程,带着消息和策略启动 turn(一轮对话处理),然后持续读到这轮结束 → 出来是成功结束或错误。

调用关系:send_message、send_message_v2_endpoint、trigger_cmd_approval、trigger_patch_approval、no_trigger_cmd_approval 都复用它。它通过 with_client 获得 CodexClient。

调用图:调用 1 个内部函数(with_client);被 5 处调用(no_trigger_cmd_approval, send_message, send_message_v2_endpoint, trigger_cmd_approval, trigger_patch_approval)。

send_follow_up_v21077–1125 ↗
async fn send_follow_up_v2(
    endpoint: &Endpoint,
    config_overrides: &[String],
    first_message: String,
    follow_up_message: String,
    dynamic_tools: &Option<Vec<DynamicToolSpec>>,
) -> R

作用:它在同一个线程里连续发送两轮消息,用来测试追问或上下文延续是否正常。

数据流:进去的是连接信息、配置、第一条消息、追问消息和动态工具 → 它创建线程,先跑第一轮并读完,再在同一线程跑第二轮并读完 → 出来是两轮都完成的结果。

调用关系:run 的 SendFollowUpV2 分支调用它。它通过 with_client 使用 CodexClient 的 thread_start、turn_start 和 stream_turn。

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

test_login1127–1177 ↗
async fn test_login(
    endpoint: &Endpoint,
    config_overrides: &[String],
    device_code: bool,
) -> Result<()>

作用:它测试 ChatGPT 账号登录流程,可以走浏览器回调或设备码方式。它会等到服务器通知登录完成。

数据流:进去的是连接目标、配置和是否使用设备码 → 它初始化客户端,发起登录,打印用户需要打开的网址或输入的验证码,然后等待完成通知 → 出来是登录成功或失败错误。

调用关系:run 的 TestLogin 分支调用它。它依赖 CodexClient 的 login_account_chatgpt、login_account_chatgpt_device_code 和 wait_for_account_login_completion。

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

get_account_rate_limits1179–1195 ↗
async fn get_account_rate_limits(endpoint: &Endpoint, config_overrides: &[String]) -> Result<()>

作用:它向 app-server 查询当前账号的用量或速率限制信息。用于确认账号状态接口能正常返回。

数据流:进去的是连接目标和配置 → 它初始化客户端,发送读取限额请求 → 出来是打印出的限额响应或错误。

调用关系:run 的 GetAccountRateLimits 分支调用它。它通过 with_client 调用 CodexClient::get_account_rate_limits。

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

model_list1197–1208 ↗
async fn model_list(endpoint: &Endpoint, config_overrides: &[String]) -> Result<()>

作用:它向 app-server 要可用模型列表。用户可以用它确认当前配置和账号能看到哪些模型。

数据流:进去的是连接目标和配置 → 它初始化客户端,发送 model/list 请求 → 出来是打印出的模型列表响应。

调用关系:run 的 ModelList 分支调用它。它通过 with_client 调用 CodexClient::model_list。

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

thread_list1210–1233 ↗
async fn thread_list(endpoint: &Endpoint, config_overrides: &[String], limit: u32) -> Result<()>

作用:它列出服务器里保存的历史线程。limit 参数控制最多返回多少条。

数据流:进去的是连接目标、配置和数量上限 → 它初始化客户端,组装 thread/list 参数并发送 → 出来是打印出的线程列表。

调用关系:run 的 ThreadList 分支调用它。它把协议请求交给 CodexClient::thread_list。

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

with_client1235–1255 ↗
async fn with_client(
    command_name: &'static str,
    endpoint: &Endpoint,
    config_overrides: &[String],
    f: impl FnOnce(&mut CodexClient) -> Result<T>,
) -> Result<T>

作用:这是很多命令共用的外壳。它负责初始化追踪、建立客户端连接、运行具体测试代码,并最后打印追踪链接。

数据流:进去的是命令名、连接目标、配置覆盖项和一段要对客户端执行的函数 → 它准备 tracing(追踪,一种把请求链路记录下来方便排查问题的机制),创建 CodexClient 并执行传入逻辑 → 出来是那段逻辑的返回值。

调用关系:大多数高级命令都通过它运行。它调用 TestClientTracing::initialize、CodexClient::connect,并在结束时调用 print_trace_summary。

调用图:调用 2 个内部函数(initialize, print_trace_summary);被 10 处调用(get_account_rate_limits, model_list, resume_message_v2, send_follow_up_v2, send_message_v2_with_policies, test_login, thread_list, thread_resume_follow, trigger_zsh_fork_multi_cmd_approval, watch);外部调用 1 个(info_span!)。

thread_increment_elicitation1257–1269 ↗
fn thread_increment_elicitation(url: &str, thread_id: String) -> Result<()>

作用:它给某个线程的“外部等待/暂停计数”加一。这个计数用于告诉服务器:现在有额外的人机交互在进行,某些超时不要立刻生效。

数据流:进去的是 WebSocket URL 和线程 ID → 它连接服务器,初始化,然后发送 thread/increment_elicitation 请求 → 出来是打印出的响应。

调用关系:run 的 thread-increment-elicitation 分支调用它。它直接使用 CodexClient::connect 和 CodexClient::thread_increment_elicitation。

调用图:调用 1 个内部函数(connect);被 1 处调用(run);外部调用 2 个(ConnectWs, println!)。

thread_decrement_elicitation1271–1283 ↗
fn thread_decrement_elicitation(url: &str, thread_id: String) -> Result<()>

作用:它给某个线程的“外部等待/暂停计数”减一。通常用于交互结束后的清理,避免线程一直处于暂停状态。

数据流:进去的是 WebSocket URL 和线程 ID → 它连接服务器,初始化,然后发送 thread/decrement_elicitation 请求 → 出来是打印出的响应。

调用关系:run 的 thread-decrement-elicitation 分支调用它。live_elicitation_timeout_pause 结束时也会用同类客户端方法做清理。

调用图:调用 1 个内部函数(connect);被 1 处调用(run);外部调用 2 个(ConnectWs, println!)。

live_elicitation_timeout_pause1285–1440 ↗
fn live_elicitation_timeout_pause(
    codex_bin: Option<PathBuf>,
    url: Option<String>,
    config_overrides: &[String],
    model: String,
    workspace: PathBuf,
    script: Option<PathBuf>,

作用:这是一个现场验证用的专项测试:确认外部 elicitation 暂停期间,长时间执行的辅助脚本不会被 10 秒统一超时误杀。

数据流:进去的是可选 codex 路径或 URL、配置、模型、工作目录、脚本路径和等待秒数 → 它准备服务器和脚本,启动线程,让模型执行一个会睡眠的脚本,边读输出边检查状态 → 出来是验证通过的总结,或说明超时、提前完成、输出缺失等问题。

调用关系:run 的 LiveElicitationTimeoutPause 分支调用它。它可能调用 BackgroundAppServer::spawn 临时启动服务,并大量使用 CodexClient 的连接、thread_start、turn_start、stream_turn 和 thread_decrement_elicitation。

调用图:调用 2 个内部函数(spawn, connect);被 1 处调用(run);外部调用 11 个(default, now, canonicalize, ConnectWs, bail!, cfg!, eprintln!, format!, println!, current_exe (+1 more))。

ensure_dynamic_tools_unused1442–1452 ↗
fn ensure_dynamic_tools_unused(
    dynamic_tools: &Option<Vec<DynamicToolSpec>>,
    command: &str,
) -> Result<()>

作用:它检查某个命令是否错误地带了动态工具参数。动态工具只支持特定 V2 thread/start 流程,其他命令带上会让测试含义不清。

数据流:进去的是动态工具选项和命令名 → 如果有动态工具就报错,否则什么也不改 → 出来是通过检查或错误提示。

调用关系:run 在很多不支持动态工具的分支里调用它,resume_message_v2 也会额外调用。

调用图:被 2 处调用(resume_message_v2, run);外部调用 1 个(bail!)。

parse_dynamic_tools_arg1454–1475 ↗
fn parse_dynamic_tools_arg(dynamic_tools: &Option<String>) -> Result<Option<Vec<DynamicToolSpec>>>

作用:它把命令行里的动态工具 JSON 解析成程序能用的工具规格。支持直接传 JSON,也支持用 @文件名 从文件读取。

数据流:进去的是可选字符串参数 → 它读取原始 JSON,接受单个对象或数组,并调用 normalize_dynamic_tool_specs 做规范化 → 出来是可选的 DynamicToolSpec 列表。

调用关系:run 一开始就调用它。后续只有支持动态工具的 V2 线程启动流程会把结果传给服务器。

调用图:调用 1 个内部函数(normalize_dynamic_tool_specs);被 1 处调用(run);外部调用 5 个(new, bail!, read_to_string, from_str, vec!)。

item_started_before_helper_done_is_unexpected1512–1522 ↗
fn item_started_before_helper_done_is_unexpected(
    item: &ThreadItem,
    command_item_started: bool,
    helper_done_seen: bool,
) -> bool

作用:它判断在辅助脚本还没完成时,新开始的线程项目是不是异常。这个判断用于长时间命令暂停测试。

数据流:进去的是一个线程项目、命令项目是否已开始、辅助脚本是否已完成 → 如果命令已开始但脚本未完成,且新项目不是用户消息,就认为异常 → 出来是布尔值。

调用关系:CodexClient::stream_turn 在收到 ItemStarted 通知时调用它,用来收集 live_elicitation_timeout_pause 的验证证据。

调用图:被 1 处调用(stream_turn);外部调用 1 个(matches!)。

CodexClient::connect1525–1530 ↗
fn connect(endpoint: &Endpoint, config_overrides: &[String]) -> Result<Self>

作用:它根据连接目标创建一个 CodexClient。目标可以是启动一个本地 codex app-server,也可以是连已有 WebSocket。

数据流:进去的是 Endpoint 和配置覆盖项 → 它按 Endpoint 分支调用 spawn_stdio 或 connect_websocket → 出来是已连接并准备收发消息的 CodexClient。

调用关系:with_client、thread_increment_elicitation、thread_decrement_elicitation、live_elicitation_timeout_pause 等流程会间接或直接用它。

调用图:被 3 处调用(live_elicitation_timeout_pause, thread_decrement_elicitation, thread_increment_elicitation);外部调用 2 个(connect_websocket, spawn_stdio)。

CodexClient::spawn_stdio1532–1534 ↗
fn spawn_stdio(codex_bin: &Path, config_overrides: &[String]) -> Result<Self>

作用:它通过标准输入输出启动 app-server。标准输入输出就是父进程和子进程之间一行一行传文字的管道。

数据流:进去的是 codex 路径和配置覆盖项 → 它不加额外环境变量,转交给 spawn_stdio_with_env → 出来是走 stdio 通道的 CodexClient。

调用关系:CodexClient::connect 在 Endpoint::SpawnCodex 时使用它;一些插件测试清理流程也可能直接使用。

调用图:被 1 处调用(run_cleanup);外部调用 1 个(spawn_stdio_with_env)。

CodexClient::spawn_stdio_with_env1536–1594 ↗
fn spawn_stdio_with_env(
        codex_bin: &Path,
        config_overrides: &[String],
        environment: &[(OsString, OsString)],
    ) -> Result<Self>

作用:它是真正启动 stdio app-server 的函数,并允许额外设置环境变量。它把子进程的 stdin/stdout 接起来,作为协议通道。

数据流:进去的是 codex 路径、配置覆盖项和环境变量 → 它构造命令,传入 app-server,拿到子进程输入输出管道,并初始化各种统计字段 → 出来是 CodexClient。

调用关系:spawn_stdio 会调用它,插件相关测试也可能用它创建带特殊环境的客户端。CodexClient::drop 之后会负责关闭这个子进程。

调用图:被 2 处调用(spawn_client, run);外部调用 11 个(new, from, display, parent, inherit, piped, new, new, new, new (+1 more))。

CodexClient::connect_websocket1596–1633 ↗
fn connect_websocket(url: &str) -> Result<Self>

作用:它连接一个 WebSocket app-server,并带有重试。这样刚启动服务器时,客户端不用因为服务器慢半秒就立刻失败。

数据流:进去的是 WebSocket URL → 它解析 URL,在最多约 10 秒内反复尝试连接,成功后初始化客户端状态 → 出来是走 WebSocket 通道的 CodexClient。

调用关系:CodexClient::connect 在 Endpoint::ConnectWs 时调用它。所有连已有服务的命令都依赖这一步。

调用图:外部调用 10 个(new, from_millis, from_secs, now, new, parse, new, new, sleep, connect)。

CodexClient::note_helper_output1635–1643 ↗
fn note_helper_output(&mut self, output: &str)

作用:它记录命令输出,并识别辅助脚本是否已经打印完成标记。主要服务于长时间 elicitation 暂停测试。

数据流:进去的是一段命令输出 → 它追加到累计输出里,如果看到 [elicitation-hold] done 就把完成标记设为 true → 出来是客户端内部状态被更新。

调用关系:CodexClient::stream_turn 在收到命令输出增量或完整输出时调用它。live_elicitation_timeout_pause 后面会检查这些状态。

调用图:被 1 处调用(stream_turn)。

CodexClient::initialize1645–1647 ↗
fn initialize(&mut self) -> Result<InitializeResponse>

作用:它完成 app-server 初始化握手,并默认开启实验接口能力。多数测试命令都先做这一步。

数据流:进去的是客户端自身 → 它调用 initialize_with_experimental_api,并传 true → 出来是服务器的初始化响应。

调用关系:很多 with_client 包裹的命令会先调用它。真正组装请求的工作在 initialize_with_experimental_api。

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

CodexClient::initialize_with_experimental_api1649–1685 ↗
fn initialize_with_experimental_api(
        &mut self,
        experimental_api: bool,
    ) -> Result<InitializeResponse>

作用:它向服务器发送 initialize 请求,告诉服务器这个测试客户端是谁、支持哪些能力。随后再发 initialized 通知,完成握手。

数据流:进去的是是否启用实验 API 的布尔值 → 它生成请求 ID,组装客户端信息和能力,发送请求并等待响应,再发送 initialized 通知 → 出来是 InitializeResponse。

调用关系:initialize 调用它;send_message_v2_with_policies 会直接用它控制实验接口开关。它依赖 send_request 和 write_jsonrpc_message。

调用图:调用 3 个内部函数(request_id, send_request, write_jsonrpc_message);被 1 处调用(initialize);外部调用 2 个(Notification, env!)。

CodexClient::thread_start1687–1695 ↗
fn thread_start(&mut self, params: ThreadStartParams) -> Result<ThreadStartResponse>

作用:它请求服务器创建一个新线程。线程可以理解成一段会话的容器,后续多轮消息都挂在里面。

数据流:进去的是 ThreadStartParams → 它生成请求 ID,包装成 thread/start 请求并发送 → 出来是 ThreadStartResponse,包含新线程信息。

调用关系:发送消息、追问、审批测试和插件轮次测试都会用它。底层统一交给 send_request。

调用图:调用 2 个内部函数(request_id, send_request);被 1 处调用(run_plugin_turn)。

CodexClient::thread_resume1697–1705 ↗
fn thread_resume(&mut self, params: ThreadResumeParams) -> Result<ThreadResumeResponse>

作用:它请求服务器恢复一个已有线程。这样客户端可以继续接着历史会话工作。

数据流:进去的是 ThreadResumeParams,主要包含线程 ID → 它生成请求 ID,发送 thread/resume → 出来是恢复后的线程响应。

调用关系:resume_message_v2 和 thread_resume_follow 会用它,底层仍通过 send_request 等响应。

调用图:调用 2 个内部函数(request_id, send_request)。

CodexClient::turn_start1707–1715 ↗
fn turn_start(&mut self, params: TurnStartParams) -> Result<TurnStartResponse>

作用:它在某个线程里启动一轮新的处理。一次 turn 可以理解成用户发了一条消息,服务器开始思考和执行工具直到结束。

数据流:进去的是 TurnStartParams,包含线程 ID、用户输入、审批策略等 → 它生成请求 ID 并发送 turn/start → 出来是 TurnStartResponse,包含这轮的 ID。

调用关系:多数发消息和专项测试都会调用它,然后马上用 stream_turn 监听这轮的通知直到完成。

调用图:调用 2 个内部函数(request_id, send_request);被 1 处调用(run_plugin_turn)。

CodexClient::login_account_chatgpt1717–1727 ↗
fn login_account_chatgpt(&mut self) -> Result<LoginAccountResponse>

作用:它发起普通 ChatGPT 登录流程,通常会返回一个需要在浏览器打开的授权地址。

数据流:进去的是客户端自身 → 它生成请求 ID,发送 account/login/start,参数选择 ChatGPT 浏览器登录 → 出来是 LoginAccountResponse。

调用关系:test_login 在非设备码模式下调用它,然后等待 wait_for_account_login_completion。

调用图:调用 2 个内部函数(request_id, send_request)。

CodexClient::login_account_chatgpt_device_code1729–1737 ↗
fn login_account_chatgpt_device_code(&mut self) -> Result<LoginAccountResponse>

作用:它发起设备码登录流程。用户会得到一个网址和短码,在别的页面输入后完成登录。

数据流:进去的是客户端自身 → 它生成请求 ID,发送 account/login/start,参数选择设备码 → 出来是 LoginAccountResponse。

调用关系:test_login 在 device_code 为 true 时调用它,然后同样等待登录完成通知。

调用图:调用 2 个内部函数(request_id, send_request)。

CodexClient::get_account_rate_limits1739–1747 ↗
fn get_account_rate_limits(&mut self) -> Result<GetAccountRateLimitsResponse>

作用:它向服务器读取当前账号的限额信息,比如使用量或速率限制。

数据流:进去的是客户端自身 → 它生成请求 ID,发送 account/rateLimits/read → 出来是 GetAccountRateLimitsResponse。

调用关系:get_account_rate_limits 命令包装函数调用它;底层由 send_request 负责收发。

调用图:调用 2 个内部函数(request_id, send_request)。

CodexClient::model_list1749–1757 ↗
fn model_list(&mut self, params: ModelListParams) -> Result<ModelListResponse>

作用:它请求服务器返回可用模型列表。

数据流:进去的是 ModelListParams → 它生成请求 ID,发送 model/list → 出来是 ModelListResponse。

调用关系:model_list 命令包装函数调用它;底层通过 send_request 等待对应响应。

调用图:调用 2 个内部函数(request_id, send_request)。

CodexClient::thread_list1759–1767 ↗
fn thread_list(&mut self, params: ThreadListParams) -> Result<ThreadListResponse>

作用:它请求服务器列出历史线程。

数据流:进去的是 ThreadListParams,包含数量、过滤等选项 → 它生成请求 ID,发送 thread/list → 出来是 ThreadListResponse。

调用关系:thread_list 命令包装函数调用它。它和其他 RPC 方法一样复用 send_request。

调用图:调用 2 个内部函数(request_id, send_request)。

CodexClient::thread_increment_elicitation1769–1780 ↗
fn thread_increment_elicitation(
        &mut self,
        params: ThreadIncrementElicitationParams,
    ) -> Result<ThreadIncrementElicitationResponse>

作用:它给线程的 elicitation 暂停计数加一。这里的 elicitation 指服务器等待外部用户交互或确认的状态。

数据流:进去的是 ThreadIncrementElicitationParams → 它生成请求 ID,发送 thread/increment_elicitation → 出来是服务器确认响应。

调用关系:thread_increment_elicitation 顶层函数调用它;live elicitation 测试的辅助脚本也可能通过客户端命令触发相关流程。

调用图:调用 2 个内部函数(request_id, send_request)。

CodexClient::thread_decrement_elicitation1782–1793 ↗
fn thread_decrement_elicitation(
        &mut self,
        params: ThreadDecrementElicitationParams,
    ) -> Result<ThreadDecrementElicitationResponse>

作用:它给线程的 elicitation 暂停计数减一,用来结束或清理暂停状态。

数据流:进去的是 ThreadDecrementElicitationParams → 它生成请求 ID,发送 thread/decrement_elicitation → 出来是服务器确认响应。

调用关系:thread_decrement_elicitation 顶层函数和 live_elicitation_timeout_pause 的清理阶段会用它。

调用图:调用 2 个内部函数(request_id, send_request)。

CodexClient::wait_for_account_login_completion1795–1821 ↗
fn wait_for_account_login_completion(
        &mut self,
        expected_login_id: &str,
    ) -> Result<AccountLoginCompletedNotification>

作用:它一直等服务器发来指定登录流程的完成通知。这样测试不会只停在“登录已开始”,而能确认最终成败。

数据流:进去的是期望的 login_id → 它循环读取通知,忽略不相关通知和别的登录 ID,只返回匹配的完成通知 → 出来是 AccountLoginCompletedNotification。

调用关系:test_login 发起登录后调用它。它依赖 next_notification 读取消息,并把 JSON-RPC 通知转换成服务器通知类型。

调用图:调用 1 个内部函数(next_notification);外部调用 2 个(try_from, println!)。

CodexClient::stream_turn1823–1917 ↗
fn stream_turn(&mut self, thread_id: &str, turn_id: &str) -> Result<()>

作用:它持续读取某一轮 turn 的服务器通知,打印模型输出、命令输出、工具进度,并记录命令状态,直到这轮完成。

数据流:进去的是线程 ID 和 turn ID → 它循环读取通知,按类型打印或更新内部状态;遇到目标 turn/completed 就停止 → 出来是内部记录了最后状态、命令状态和输出,并返回成功或错误。

调用关系:发送消息、审批测试、长时间暂停测试和插件轮次测试都会在 turn_start 后调用它。它会调用 next_notification、note_helper_output 和 item_started_before_helper_done_is_unexpected。

调用图:调用 3 个内部函数(next_notification, note_helper_output, item_started_before_helper_done_is_unexpected);被 1 处调用(run_plugin_turn);外部调用 5 个(try_from, matches!, print!, println!, stdout)。

CodexClient::stream_notifications_forever1919–1923 ↗
fn stream_notifications_forever(&mut self) -> Result<()>

作用:它无限读取服务器通知,用于 watch 或跟随线程时观察实时事件。

数据流:进去的是客户端自身 → 它不停调用 next_notification,读到什么主要靠底层打印 → 出来通常不会主动返回,除非读取失败。

调用关系:watch 和 thread_resume_follow 调用它,让程序进入持续监听状态。

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

CodexClient::send_request1925–1946 ↗
fn send_request(
        &mut self,
        request: ClientRequest,
        request_id: RequestId,
        method: &str,
    ) -> Result<T>

作用:它是发送 JSON-RPC 请求的统一入口。它还给每个请求套上 tracing span,方便排查请求链路。

数据流:进去的是具体客户端请求、请求 ID 和方法名 → 它写出请求,然后等待同 ID 的响应并反序列化成指定类型 → 出来是对应响应对象。

调用关系:initialize、thread_start、turn_start、登录、模型列表、线程列表等几乎所有 RPC 方法都调用它。内部依赖 write_request 和 wait_for_response。

调用图:被 16 处调用(get_account_rate_limits, initialize_with_experimental_api, login_account_chatgpt, login_account_chatgpt_device_code, model_list, thread_decrement_elicitation, thread_increment_elicitation, thread_list, thread_resume, thread_start (+6 more));外部调用 1 个(info_span!)。

CodexClient::write_request1948–1957 ↗
fn write_request(&mut self, request: &ClientRequest) -> Result<()>

作用:它把内部的 ClientRequest 转成标准 JSON-RPC 请求文本,并写到服务器。发送前还加上当前追踪上下文。

数据流:进去的是 ClientRequest → 它转成 JSON-RPC 结构,填入 trace 字段,打印漂亮格式,再写出紧凑 JSON → 出来是数据发到 stdio 或 WebSocket。

调用关系:send_request 调用它。最后实际写通道的是 write_payload。

调用图:调用 2 个内部函数(write_payload, print_multiline_with_prefix);外部调用 5 个(current_span_w3c_trace_context, from_value, to_string, to_string_pretty, to_value)。

CodexClient::wait_for_response1959–1986 ↗
fn wait_for_response(&mut self, request_id: RequestId, method: &str) -> Result<T>

作用:它等待某个请求 ID 对应的响应。等待期间如果服务器先发通知或反向请求,它也会妥善处理。

数据流:进去的是请求 ID 和方法名 → 它循环读 JSON-RPC 消息;匹配响应就解析返回,错误就报错,通知先存起来,服务器请求则立即处理 → 出来是指定类型的响应。

调用关系:send_request 写完请求后调用它。它会调用 read_jsonrpc_message 和 handle_server_request。

调用图:调用 2 个内部函数(handle_server_request, read_jsonrpc_message);外部调用 3 个(push_back, bail!, from_value)。

CodexClient::next_notification1988–2007 ↗
fn next_notification(&mut self) -> Result<JSONRPCNotification>

作用:它取下一条服务器通知。如果之前等待响应时攒下了通知,就先用缓存;否则继续从连接里读。

数据流:进去的是客户端自身 → 它先检查 pending_notifications 队列,没有就读新消息;遇到服务器反向请求会先处理 → 出来是一条 JSONRPCNotification。

调用关系:stream_turn、stream_notifications_forever 和 wait_for_account_login_completion 都靠它获取通知。

调用图:调用 2 个内部函数(handle_server_request, read_jsonrpc_message);被 3 处调用(stream_notifications_forever, stream_turn, wait_for_account_login_completion);外部调用 1 个(pop_front)。

CodexClient::read_jsonrpc_message2009–2025 ↗
fn read_jsonrpc_message(&mut self) -> Result<JSONRPCMessage>

作用:它从底层通道读一条原始文本,解析成 JSON-RPC 消息,并打印出来便于调试。

数据流:进去的是客户端自身 → 它调用 read_payload 取文本,跳过空行,解析 JSON,打印格式化内容,再转成 JSONRPCMessage → 出来是一条结构化消息。

调用关系:wait_for_response 和 next_notification 都调用它;真正读 stdio/WebSocket 的动作在 read_payload。

调用图:调用 2 个内部函数(read_payload, print_multiline_with_prefix);被 2 处调用(next_notification, wait_for_response);外部调用 3 个(from_str, from_value, to_string_pretty)。

CodexClient::request_id2027–2029 ↗
fn request_id(&self) -> RequestId

作用:它生成一个新的请求 ID。请求 ID 像快递单号,用来把发出的请求和收到的响应配对。

数据流:进去不需要外部数据 → 它生成一个 UUID(全局唯一风格的随机字符串)并包装成 RequestId → 出来是新的请求 ID。

调用关系:所有 CodexClient 的 RPC 方法在发送前都会调用它。wait_for_response 后面用这个 ID 找对应响应。

调用图:被 16 处调用(get_account_rate_limits, initialize_with_experimental_api, login_account_chatgpt, login_account_chatgpt_device_code, model_list, thread_decrement_elicitation, thread_increment_elicitation, thread_list, thread_resume, thread_start (+6 more));外部调用 2 个(new_v4, String)。

CodexClient::handle_server_request2031–2048 ↗
fn handle_server_request(&mut self, request: JSONRPCRequest) -> Result<()>

作用:它处理服务器反过来问客户端的问题,比如“这个命令能不能执行?”或“这个文件改动能不能批准?”。

数据流:进去的是 JSON-RPC 请求 → 它转换成 ServerRequest,按类型分发到命令审批或文件变更审批处理器 → 出来是审批响应已发回,或遇到不支持请求时报错。

调用关系:wait_for_response 和 next_notification 在读到服务器请求时调用它。它会交给 handle_command_execution_request_approval 或 approve_file_change_request。

调用图:调用 2 个内部函数(approve_file_change_request, handle_command_execution_request_approval);被 2 处调用(next_notification, wait_for_response);外部调用 2 个(try_from, bail!)。

CodexClient::handle_command_execution_request_approval2050–2124 ↗
fn handle_command_execution_request_approval(
        &mut self,
        request_id: RequestId,
        params: CommandExecutionRequestApprovalParams,
    ) -> Result<()>

作用:它自动回答服务器的命令执行审批请求。测试客户端可以总是同意,也可以在指定第几次审批时取消。

数据流:进去的是请求 ID 和审批参数 → 它打印命令、原因、工作目录等信息,更新审批计数,根据 command_approval_behavior 决定接受或取消,再发回响应 → 出来是服务器收到审批决定,客户端也记录了统计信息。

调用关系:handle_server_request 在遇到 CommandExecutionRequestApproval 时调用它。它通过 send_server_request_response 回复服务器。

调用图:调用 1 个内部函数(send_server_request_response);被 1 处调用(handle_server_request);外部调用 1 个(println!)。

CodexClient::approve_file_change_request2126–2156 ↗
fn approve_file_change_request(
        &mut self,
        request_id: RequestId,
        params: FileChangeRequestApprovalParams,
    ) -> Result<()>

作用:它自动批准服务器提出的文件变更请求。测试场景下这样可以让流程继续走完,而不用真人点按钮。

数据流:进去的是请求 ID 和文件变更审批参数 → 它打印线程、轮次、项目、原因和授权根目录,然后构造 Accept 响应 → 出来是批准结果发回服务器。

调用关系:handle_server_request 在遇到 FileChangeRequestApproval 时调用它。回复动作交给 send_server_request_response。

调用图:调用 1 个内部函数(send_server_request_response);被 1 处调用(handle_server_request);外部调用 1 个(println!)。

CodexClient::send_server_request_response2158–2167 ↗
fn send_server_request_response(&mut self, request_id: RequestId, response: &T) -> Result<()>

作用:它把客户端对服务器反向请求的回答包装成 JSON-RPC response 发回去。

数据流:进去的是服务器请求 ID 和可序列化的响应内容 → 它转成 JSON 值,包装成 JSONRPCResponse,再写出 → 出来是响应消息发到服务器。

调用关系:命令审批和文件变更审批处理器都调用它。底层写消息由 write_jsonrpc_message 完成。

调用图:调用 1 个内部函数(write_jsonrpc_message);被 2 处调用(approve_file_change_request, handle_command_execution_request_approval);外部调用 2 个(Response, to_value)。

CodexClient::write_jsonrpc_message2169–2174 ↗
fn write_jsonrpc_message(&mut self, message: JSONRPCMessage) -> Result<()>

作用:它发送一条已经构造好的 JSON-RPC 消息,比如 initialized 通知或审批响应。

数据流:进去的是 JSONRPCMessage → 它序列化成紧凑文本,同时打印漂亮格式 → 出来是消息通过 write_payload 发到服务器。

调用关系:initialize_with_experimental_api 和 send_server_request_response 调用它。

调用图:调用 2 个内部函数(write_payload, print_multiline_with_prefix);被 2 处调用(initialize_with_experimental_api, send_server_request_response);外部调用 2 个(to_string, to_string_pretty)。

CodexClient::write_payload2176–2195 ↗
fn write_payload(&mut self, payload: &str) -> Result<()>

作用:它是最底层的“写出去”函数,负责把一段 JSON 文本写到 stdio 或 WebSocket。

数据流:进去的是 payload 字符串 → 如果是 stdio,就写一行并 flush;如果是 WebSocket,就发文本帧 → 出来是服务器收到文本,或返回写入错误。

调用关系:write_request 和 write_jsonrpc_message 都最终调用它。它隐藏了两种传输方式的差异。

调用图:被 2 处调用(write_jsonrpc_message, write_request);外部调用 3 个(bail!, Text, writeln!)。

CodexClient::read_payload2197–2223 ↗
fn read_payload(&mut self) -> Result<String>

作用:它是最底层的“读进来”函数,负责从 stdio 或 WebSocket 取一段服务器发来的文本。

数据流:进去的是客户端自身 → stdio 模式读一行,WebSocket 模式读文本帧并跳过 ping/pong 等控制帧 → 出来是一段 JSON 文本。

调用关系:read_jsonrpc_message 调用它,然后再解析成结构化 JSON-RPC 消息。

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

print_multiline_with_prefix2226–2230 ↗
fn print_multiline_with_prefix(prefix: &str, payload: &str)

作用:它把多行文本逐行打印,并在每行前加同一个前缀。这样日志里可以清楚区分发出的消息和收到的消息。

数据流:进去的是前缀和多行内容 → 它按行遍历并打印 → 出来是带前缀的控制台日志。

调用关系:write_request、write_jsonrpc_message 和 read_jsonrpc_message 都用它打印协议内容。

调用图:被 3 处调用(read_jsonrpc_message, write_jsonrpc_message, write_request);外部调用 1 个(println!)。

TestClientTracing::initialize2238–2269 ↗
async fn initialize(config_overrides: &[String]) -> Result<Self>

作用:它根据配置初始化追踪系统。追踪可以理解成给一次命令执行画一张路线图,方便在 Datadog 等系统里查问题。

数据流:进去的是配置覆盖项 → 它解析覆盖项,加载 Codex 配置,尝试创建 OpenTelemetry provider(OpenTelemetry 是通用追踪标准),有追踪器就接入 tracing subscriber → 出来是 TestClientTracing,标记追踪是否可用。

调用关系:with_client 在执行每个命令前调用它。TraceSummary 后面会根据追踪是否开启生成可打印的链接。

调用图:调用 1 个内部函数(build_provider);被 1 处调用(with_client);外部调用 3 个(load_with_cli_overrides, env!, registry)。

TraceSummary::capture2279–2287 ↗
fn capture(traces_enabled: bool) -> Self

作用:它抓取当前命令的追踪摘要。如果追踪开启并能拿到 trace id,就生成一个可访问的短链接。

数据流:进去的是 traces_enabled 标志 → 如果没开启就返回 Disabled;开启时读取当前 span 的 W3C trace context,并尝试提取 URL → 出来是 Enabled 或 Disabled。

调用关系:with_client 在命令 span 里调用它。它会使用 trace_url_from_context 解析 traceparent。

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

trace_url_from_context2290–2301 ↗
fn trace_url_from_context(trace: &W3cTraceContext) -> Option<String>

作用:它从 W3C trace context 里提取 trace id,并拼出 Datadog 跟踪链接。W3C trace context 是跨服务传递追踪编号的一种标准格式。

数据流:进去的是 W3cTraceContext → 它读取 traceparent 字符串,按横线拆分,确认 trace id 长度正确 → 出来是类似 go/trace/... 的链接,或 None。

调用关系:TraceSummary::capture 调用它。print_trace_summary 后面会把结果展示给用户。

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

print_trace_summary2303–2309 ↗
fn print_trace_summary(trace_summary: &TraceSummary)

作用:它在命令结束时打印追踪信息。开启追踪时给链接,没开启时提示去配置里打开。

数据流:进去的是 TraceSummary → 它打印标题,然后按 Enabled 或 Disabled 输出链接或提示语 → 出来是控制台上的追踪摘要。

调用关系:with_client 在具体命令执行完后调用它。

调用图:被 1 处调用(with_client);外部调用 1 个(println!)。

CodexClient::drop2312–2340 ↗
fn drop(&mut self)

作用:这是 CodexClient 被销毁时的清理动作,主要用于 stdio 模式。它尽量让子 app-server 优雅退出,超时后再强制杀掉。

数据流:进去的是客户端里保存的传输状态 → 如果不是 stdio 就不做事;如果是子进程,就关闭 stdin,等待一小段时间,仍未退出就 kill 并 wait → 出来是子进程被回收。

调用关系:Rust 在 CodexClient 离开作用域时自动调用它。with_client、live_elicitation_timeout_pause 等流程结束后会触发这个清理。

调用图:外部调用 3 个(now, println!, sleep)。

app-server-test-client/src/plugin_analytics_smoke.rs源码 ↗
test测试运行期间,从启动测试进程到校验完捕获的分析事件为止

这个文件像一个自动质检员。它先准备一个临时的事件记录文件和临时配置文件,再启动 Codex 客户端,并打开分析、插件、远程插件等开关。接着它询问当前装了哪些插件,确认目标插件真的已安装、已启用、可用。然后它故意把插件配置写成禁用,再写回启用,用来触发“插件被禁用”和“插件被启用”事件。最后它开一个临时对话,在输入里点名这个插件,等插件真正被用到,再去捕获文件里读出一行行 JSON 事件,确认必须有三类事件:禁用、启用、使用。这里还会检查事件里的插件 ID、插件名、市场名、模型名等信息是否正确。整体上,它不是产品功能本身,而是防止“插件能用但统计没记上”这种隐蔽问题的测试工具。

函数细节23
run40–95 ↗
fn run(
    codex_bin: &Path,
    config_overrides: &[String],
    plugin_id: &str,
    capture_file: Option<PathBuf>,
) -> Result<()>

作用:这是整个插件分析冒烟测试的总入口。它把临时文件、假响应服务器、Codex 子进程、插件开关、一次插件使用和最终事件校验串起来。

数据流:进去的是 Codex 可执行文件路径、配置覆盖项、要测试的插件 ID,以及可选的事件捕获文件路径。它先准备捕获文件和临时配置,启动本地回环响应服务器,再带着环境变量启动 Codex;之后它查询插件、改写启用状态、跑一次插件对话、读取并校验事件。出来的是成功或错误;成功时会打印通过校验的事件和捕获文件位置,也会创建并清理临时配置文件。

调用关系:它是这个文件的指挥官。上层测试命令会调用它;它会依次把活交给 TemporaryConfigFile::create、LoopbackResponsesServer::start、CodexClient::spawn_stdio_with_env、plugin_installed、expected_plugin、write_plugin_enabled、wait_for_plugin_usage、wait_for_plugin_events 和 validate_plugin_events。

调用图:调用 12 个内部函数(spawn_stdio_with_env, start, create, expected_plugin, plugin_installed, prepare_capture_file, smoke_config_overrides, validate_plugin_events, wait_for_plugin_events, wait_for_plugin_usage (+2 more));被 1 处调用(run);外部调用 2 个(println!, vec!)。

run_plugin_turn97–124 ↗
fn run_plugin_turn(client: &mut CodexClient, expected: &ExpectedPlugin) -> Result<String>

作用:这个函数专门制造一次“用户在对话里使用插件”的动作。它开一个临时对话线程,输入里提到目标插件,然后等这一轮对话跑完。

数据流:进去的是已经连上的 CodexClient 和期望的插件信息。它创建线程,发起一轮包含插件 mention(点名插件)的输入,持续读取流式结果,最后检查状态必须是完成。出来的是这一轮对话的 turn_id;如果对话失败或没完成,就直接报错。

调用关系:它被 wait_for_plugin_usage 反复调用。wait_for_plugin_usage 需要通过它一次次尝试使用插件,直到捕获文件里真的出现“插件被使用”的分析事件。

调用图:调用 3 个内部函数(stream_turn, thread_start, turn_start);被 1 处调用(wait_for_plugin_usage);外部调用 4 个(default, new, bail!, vec!)。

wait_for_plugin_usage126–157 ↗
fn wait_for_plugin_usage(
    client: &mut CodexClient,
    capture_path: &Path,
    expected: &ExpectedPlugin,
) -> Result<()>

作用:这个函数负责等远程插件真正可用。因为插件包可能还没准备好,所以它会多试几轮对话,而不是只试一次就失败。

数据流:进去的是 CodexClient、事件捕获文件路径和期望插件信息。它循环调用 run_plugin_turn 发起插件使用,再用 wait_for_turn_analytics 等这一轮的分析事件写入文件;如果看到了对应插件的 codex_plugin_used 事件,就成功返回。超时还没看到,就报错。

调用关系:它由 run 调用,处在“确认插件真的能被使用”这一步。它向下依赖 run_plugin_turn 来制造使用动作,依赖 wait_for_turn_analytics 来确认这一轮事件已经落盘。

调用图:调用 2 个内部函数(run_plugin_turn, wait_for_turn_analytics);被 1 处调用(run);外部调用 4 个(now, bail!, println!, sleep)。

plugin_installed166–179 ↗
fn plugin_installed(client: &mut CodexClient) -> Result<PluginInstalledResponse>

作用:这个函数向 Codex 服务询问当前安装了哪些插件。它的作用是拿到插件清单,后面才能判断目标插件是否存在、是否可用。

数据流:进去的是 CodexClient。它生成一个请求 ID,发送 plugin/installed 请求,不指定工作目录和安装建议。出来的是 PluginInstalledResponse,也就是包含市场和插件列表的响应。

调用关系:它由 run 在启动并初始化客户端后调用。拿到的结果会交给 expected_plugin,进一步筛出本次测试要验证的那个插件。

调用图:调用 2 个内部函数(request_id, send_request);被 1 处调用(run)。

expected_plugin181–221 ↗
fn expected_plugin(response: &PluginInstalledResponse, plugin_id: &str) -> Result<ExpectedPlugin>

作用:这个函数从插件清单里找出测试指定的插件,并检查它是不是符合测试前提。比如必须刚好找到一个、已经安装、已经启用、状态可用。

数据流:进去的是 plugin_installed 返回的插件清单和目标插件 ID。它在所有市场的插件列表里查找这个 ID,检查安装、启用、可用、远程插件 ID 等条件。出来的是简化后的 ExpectedPlugin,包含插件 ID、插件名和市场名;任何条件不满足都会报错。

调用关系:它由 run 调用,是测试开始前的把关人。它产出的 ExpectedPlugin 会被 write_plugin_enabled、wait_for_plugin_usage、validate_plugin_events 等后续步骤拿来对照。

调用图:被 1 处调用(run);外部调用 1 个(bail!)。

write_plugin_enabled223–255 ↗
fn write_plugin_enabled(
    client: &mut CodexClient,
    config_path: &Path,
    plugin_id: &str,
    enabled: bool,
) -> Result<()>

作用:这个函数通过 Codex 配置接口,把某个插件写成启用或禁用。测试用它来故意触发“插件被禁用”和“插件被启用”的分析事件。

数据流:进去的是 CodexClient、配置文件路径、插件 ID 和 enabled 布尔值。它发送 config/value/write 请求,把 plugins.<插件ID>.enabled 写进指定临时配置文件,并要求直接替换旧值。出来的是成功或错误;如果写入状态不是 Ok,就认为被覆盖或失败并报错。

调用关系:它由 run 调用两次:先写 false,再写 true。这样后面的 wait_for_plugin_events 才有机会在捕获文件里看到禁用和启用事件。

调用图:调用 2 个内部函数(request_id, send_request);被 1 处调用(run);外部调用 5 个(display, bail!, format!, json!, println!)。

smoke_config_overrides257–279 ↗
fn smoke_config_overrides(responses_base_url: &str) -> Result<Vec<String>>

作用:这个函数生成本次冒烟测试必须用的一组配置覆盖项。它会打开分析和插件功能,并把模型请求指向本地假服务器,避免测试依赖真实外部服务。

数据流:进去的是本地回环响应服务器的基础 URL。它把这个 URL 拼成模型提供商的 base_url,并生成一串形如 key=value 的配置字符串,包括模型名、提供商 ID、协议类型、重试次数等。出来的是配置字符串列表。

调用关系:它由 run 调用。run 会把它生成的配置追加到外部传入的配置覆盖项里,然后交给 CodexClient::spawn_stdio_with_env 启动子进程。

调用图:被 1 处调用(run);外部调用 3 个(format!, to_string, vec!)。

quoted281–283 ↗
fn quoted(value: &str) -> Result<String>

作用:这个小函数把普通字符串转成 JSON 字符串写法,方便安全地塞进配置覆盖项里。比如会自动加引号并处理特殊字符。

数据流:进去的是一个字符串切片。它用 serde_json 序列化成带引号、可安全写入配置表达式的字符串。出来的是序列化后的字符串;如果序列化失败就返回错误。

调用关系:它是 smoke_config_overrides 的小帮手。配置里需要写模型名、提供商名、协议名等字符串时,会用它保证格式正确。

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

prepare_capture_file285–304 ↗
fn prepare_capture_file(path: &Path) -> Result<()>

作用:这个函数在测试开始前整理事件捕获文件。它确认父目录存在,并删除旧文件,避免上一次测试留下的事件污染这一次结果。

数据流:进去的是捕获文件路径。它先检查路径有没有父目录、父目录是不是存在;然后尝试删除同名旧文件,文件不存在也算正常。出来的是成功或错误;它可能会改动磁盘上的捕获文件状态。

调用关系:它由 run 在启动 Codex 前调用。后续 wait_until_capture_is_ready、read_capture_events 等函数都会围绕这个捕获文件工作。

调用图:被 2 处调用(run, run);外部调用 3 个(parent, bail!, remove_file)。

wait_until_capture_is_ready306–325 ↗
fn wait_until_capture_is_ready(path: &Path) -> Result<()>

作用:这个函数等待分析捕获文件被 Codex 创建出来。它像在门口等信号灯:文件出现了,说明捕获机制已经准备好。

数据流:进去的是捕获文件路径。它不断查看这个路径是否已有文件,每隔很短时间重试一次,直到文件出现或超时。出来的是成功或错误;如果超时,它会提示可能需要使用支持调试捕获的 Codex 二进制文件。

调用关系:它由 run 在启动 Codex 子进程后、初始化客户端前调用。这样可以尽早确认分析事件捕获通道是通的,避免后面跑完才发现根本没记录。

调用图:被 2 处调用(run, run);外部调用 4 个(now, bail!, metadata, sleep)。

wait_for_plugin_events327–349 ↗
fn wait_for_plugin_events(path: &Path, plugin_id: &str) -> Result<Vec<Value>>

作用:这个函数等待插件相关的三种关键分析事件都出现。三种事件是:插件被禁用、插件被启用、插件被使用。

数据流:进去的是捕获文件路径和插件 ID。它反复调用 read_plugin_events 读取该插件的事件,再对照 required_event_types 看每种至少有没有一条。出来的是该插件的事件列表;如果一直等不到完整事件,就报超时错误并说明已经看到了哪些事件类型。

调用关系:它由 run 在确认插件已经实际使用后调用。它负责把“文件里终于写齐了没有”这件事判断清楚,之后事件会交给 validate_plugin_events 做更严格的内容校验。

调用图:调用 2 个内部函数(read_plugin_events, required_event_types);被 1 处调用(run);外部调用 3 个(now, bail!, sleep)。

wait_for_turn_analytics351–369 ↗
fn wait_for_turn_analytics(path: &Path, turn_id: &str) -> Result<Vec<Value>>

作用:这个函数等待某一轮对话的分析事件写入捕获文件。它用 turn_id 当凭证,确认这一轮对话的事件已经落盘。

数据流:进去的是捕获文件路径和 turn_id。它反复读取所有捕获事件,寻找 event_type 为 codex_turn_event 且参数里 turn_id 匹配的事件。找到就返回当前读到的所有事件;超时找不到就报错。

调用关系:它被 wait_for_plugin_usage 调用。因为一次对话完成事件排在插件使用之后,所以看到这轮对话事件,就相当于有了一个“前面的插件使用事件应该也写完了”的检查点。

调用图:调用 1 个内部函数(read_capture_events);被 1 处调用(wait_for_plugin_usage);外部调用 3 个(now, bail!, sleep)。

read_plugin_events371–376 ↗
fn read_plugin_events(path: &Path, plugin_id: &str) -> Result<Vec<Value>>

作用:这个函数从捕获文件里只挑出属于某个插件的事件。它把一大堆分析事件过滤成测试关心的那一小部分。

数据流:进去的是捕获文件路径和插件 ID。它先调用 read_capture_events 读出所有事件,再筛选 event_params.plugin_id 等于目标插件 ID 的事件。出来的是过滤后的 JSON 事件列表。

调用关系:它被 wait_for_plugin_events 调用。wait_for_plugin_events 只关心目标插件,所以先让它完成过滤,再判断三种必需事件有没有出现。

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

read_capture_events378–404 ↗
fn read_capture_events(path: &Path) -> Result<Vec<Value>>

作用:这个函数读取分析捕获文件,并把里面的 JSON 行拆成单个事件。捕获文件是一行一个 JSON 包,每个包里可能有多个 events。

数据流:进去的是捕获文件路径。它读取整个文件;如果文件还不存在,就返回空列表。然后逐行解析 JSON,跳过空行,从每行的 events 数组里取出事件并合并。出来的是所有捕获到的事件列表;如果文件读不了、JSON 解析失败或缺少 events 字段,就报错。

调用关系:它是读取捕获文件的底层工具,被 read_plugin_events 和 wait_for_turn_analytics 使用。上层函数不需要关心文件是一行行 JSON,只拿到整理好的事件列表。

调用图:被 2 处调用(read_plugin_events, wait_for_turn_analytics);外部调用 3 个(new, read_to_string, from_str)。

validate_plugin_events406–427 ↗
fn validate_plugin_events(events: Vec<Value>, expected: &ExpectedPlugin) -> Result<Vec<Value>>

作用:这个函数对已经收集到的插件事件做最终验收。它不只是看事件有没有,还要求每种必需事件刚好一条,并且内容对得上。

数据流:进去的是插件事件列表和期望插件信息。它遍历 required_event_types,找出每种事件的匹配项;数量必须正好一个。然后用 validate_identity 检查插件身份信息;如果是 codex_plugin_used,还用 validate_used_metadata 检查使用事件的额外信息。出来的是通过校验的事件列表;任何不符合都会报错。

调用关系:它由 run 在 wait_for_plugin_events 之后调用,是测试的最后一道验收关。它会把细节检查交给 validate_identity 和 validate_used_metadata。

调用图:调用 3 个内部函数(required_event_types, validate_identity, validate_used_metadata);被 1 处调用(run);外部调用 2 个(new, bail!)。

required_event_types429–435 ↗
fn required_event_types() -> [&'static str; 3]

作用:这个函数集中列出本测试必须看到的三种事件类型。把清单放在一个地方,可以避免等待逻辑和校验逻辑各写一份导致不一致。

数据流:它不需要输入。它直接返回三个固定字符串:codex_plugin_disabled、codex_plugin_enabled、codex_plugin_used。它不改动任何外部状态。

调用关系:它被 wait_for_plugin_events 和 validate_plugin_events 使用。前者用它判断是否等齐,后者用它逐项做严格校验。

调用图:被 2 处调用(validate_plugin_events, wait_for_plugin_events)。

event_count437–442 ↗
fn event_count(events: &[Value], event_type: &str) -> usize

作用:这个函数数一数某类事件出现了多少次。它就是一把小计数尺,用来判断某个事件类型是否已经出现。

数据流:进去的是事件列表和事件类型字符串。它遍历事件,检查 event_type 字段是否等于目标类型,并累计数量。出来的是匹配数量;不改动事件本身。

调用关系:它本身不启动任何流程,只服务于需要统计事件数量的等待判断。它和 required_event_types 配合,可以回答“每种必需事件至少有一条了吗”。

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

validate_identity444–449 ↗
fn validate_identity(event: &Value, expected: &ExpectedPlugin) -> Result<()>

作用:这个函数检查某条事件里写的插件身份是否正确。它确认插件 ID、插件名、市场名都和测试预期一致。

数据流:进去的是一条 JSON 事件和 ExpectedPlugin。它取出 event_params,然后分别检查 plugin_id、plugin_name、marketplace_name 三个字段。出来的是成功或错误;字段缺失或值不对都会报错。

调用关系:它被 validate_plugin_events 调用。每一种必需事件都要先通过这个身份检查,确保不是别的插件的事件混了进来。

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

validate_used_metadata451–467 ↗
fn validate_used_metadata(event: &Value) -> Result<()>

作用:这个函数专门检查“插件被使用”事件里的附加信息。因为使用事件比启用、禁用事件更复杂,必须带上线程、轮次、模型、连接器等关键字段。

数据流:进去的是一条 JSON 事件。它查看 event_params,要求 has_skills、mcp_server_count、connector_ids、mcp_server_names、thread_id、turn_id、model_slug 等字段都存在且不是 null,并要求 model_slug 等于测试用的假模型名。出来的是成功或错误。

调用关系:它只被 validate_plugin_events 在检查 codex_plugin_used 事件时调用。它把使用事件的细节校验从主流程里拆出来,让最终验收更清楚。

调用图:调用 1 个内部函数(require_string);被 1 处调用(validate_plugin_events);外部调用 1 个(bail!)。

require_string469–475 ↗
fn require_string(params: &Value, field: &str, expected: &str) -> Result<()>

作用:这个小函数检查 JSON 参数里的某个字段是不是指定字符串。它把重复的“取字段、转字符串、比较值”封装成一处。

数据流:进去的是 JSON 参数对象、字段名和期望字符串。它取出该字段并尝试当作字符串读取,然后和期望值比较。出来的是成功或错误;字段不存在、不是字符串或值不同都会报错。

调用关系:它被 validate_identity 和 validate_used_metadata 调用。上层校验函数用它来检查插件身份字段和模型名字段。

调用图:被 2 处调用(validate_identity, validate_used_metadata);外部调用 2 个(get, bail!)。

TemporaryConfigFile::create482–490 ↗
fn create() -> Result<Self>

作用:这个函数创建一个临时配置文件,供测试安全地写插件启用状态。这样不会碰到用户真实配置。

数据流:它不需要外部输入。它在系统临时目录里按当前进程 ID 拼出一个文件名,写入一个空文件。出来的是 TemporaryConfigFile 对象,里面记着这个临时文件路径;如果写文件失败就报错。

调用关系:它由 run 在测试开始时调用。run 后面会通过 TemporaryConfigFile::path 把这个路径传给配置写入请求;对象销毁时 TemporaryConfigFile::drop 会清理文件。

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

TemporaryConfigFile::path492–494 ↗
fn path(&self) -> &Path

作用:这个函数返回临时配置文件的路径。外部步骤需要知道这个路径,才能把插件启用状态写到正确的临时文件里。

数据流:进去的是 TemporaryConfigFile 自身。它只借出内部保存的 Path 引用,不复制文件,也不改动文件。出来的是配置文件路径引用。

调用关系:它被 run 用来拿到临时配置路径,并传给 write_plugin_enabled 和子进程环境变量设置。它是 TemporaryConfigFile 对外暴露路径的安全小窗口。

TemporaryConfigFile::drop498–500 ↗
fn drop(&mut self)

作用:这个函数在 TemporaryConfigFile 对象用完时自动删除临时配置文件。它像退房时自动清理房间,避免测试垃圾留在临时目录。

数据流:进去的是即将被销毁的 TemporaryConfigFile。它尝试删除内部保存路径上的文件,并忽略删除失败。出来没有显式结果;它的主要效果是清理磁盘文件。

调用关系:它由 Rust 的 Drop 机制自动调用,不需要 run 手动调用。run 结束后临时配置对象离开作用域,这个清理动作就会发生。

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

app-server-test-client/src/plugin_analytics_mutation_smoke.rs源码 ↗
test测试命令运行时;失败后也会参与清理和恢复提示

这个文件像一个带安全绳的验收脚本。它先要求用户明确确认,因为接下来会在当前账号上安装和卸载插件。然后它启动 Codex 服务客户端,打开插件功能和 analytics(分析事件记录),把事件写到一个捕获文件里。测试会先读取远程插件,确认它一开始没安装、可安装;接着安装它,等后台真的显示“已安装”,再等到安装事件写进文件;然后卸载它,等后台回到“未安装”,最后检查捕获到的事件内容是否和预期一致。无论中途成功还是失败,它都会尽量把插件恢复成未安装状态。如果恢复不了,它会打印一条用户可以复制执行的清理命令,避免把测试账号弄脏。

函数细节16
run36–114 ↗
fn run(
    codex_bin: &Path,
    config_overrides: &[String],
    remote_plugin_id: &str,
    confirmation: AccountMutationConfirmation,
    capture_file: Option<PathBuf>,
) -> Result<()>

作用:这是完整的插件 analytics 变更冒烟测试入口。它会安装再卸载一个远程插件,并确认相关分析事件被记录,同时尽力把账号恢复到测试前的未安装状态。

数据流:进去的是 Codex 可执行文件路径、配置覆盖项、远程插件 ID、用户是否确认会改账号,以及可选的事件捕获文件路径。它先检查确认,准备捕获文件,启动客户端,读取并校验插件初始状态,然后执行安装/卸载测试,最后做清理并根据结果打印成功、干净失败、脏状态或未知状态。出来的是成功或错误;同时它可能改动远程账号上的插件安装状态,但会尽量恢复。

调用关系:它是这个文件的主流程,由外层命令分发调用。它把安全检查交给 require_confirmation,把客户端启动交给 spawn_client,把插件读取交给 read_remote_plugin,把初始条件检查交给 validate_initial_plugin,把核心安装卸载流程交给 run_mutation_sequence,把收尾清理交给 restore_uninstalled_state;如果清理失败,还会调用 print_dirty_recovery 或 print_recovery_command 告诉人怎么补救。

调用图:调用 10 个内部函数(print_dirty_recovery, print_recovery_command, read_remote_plugin, require_confirmation, restore_uninstalled_state, run_mutation_sequence, spawn_client, validate_initial_plugin, prepare_capture_file, wait_until_capture_is_ready);被 1 处调用(run);外部调用 2 个(eprintln!, println!)。

run_cleanup116–154 ↗
fn run_cleanup(
    codex_bin: &Path,
    config_overrides: &[String],
    remote_plugin_id: &str,
    confirmation: AccountMutationConfirmation,
) -> Result<()>

作用:这是专门用来清理远程插件安装状态的命令入口。它不做 analytics 验证,只负责确认指定插件最终是未安装状态。

数据流:进去的是 Codex 路径、配置覆盖项、远程插件 ID,以及用户确认。它先要求确认,然后额外关闭 analytics、打开插件和远程插件功能,启动客户端并初始化,再尝试恢复未安装状态。出来的是成功或错误;成功表示插件已经卸载,失败时会说明是本地清理失败、仍然脏,还是无法确认状态。

调用关系:它通常在测试失败或人工恢复时使用。它会调用 require_confirmation 防止误操作,然后通过 CodexClient::spawn_stdio 启动客户端,再把真正的卸载和确认工作交给 restore_uninstalled_state;如果发现插件还没卸掉,会调用 print_dirty_recovery 给出恢复命令。

调用图:调用 4 个内部函数(spawn_stdio, print_dirty_recovery, require_confirmation, restore_uninstalled_state);被 1 处调用(run);外部调用 2 个(eprintln!, println!)。

AccountMutationConfirmation::from_flag163–169 ↗
fn from_flag(confirm_account_mutation: bool) -> Self

作用:这个函数把命令行里的布尔开关变成更明确的确认状态。简单说,就是把“用户有没有打确认参数”翻译成代码里好判断的枚举值。

数据流:进去的是一个 true 或 false。true 会变成 Confirmed,表示用户明确允许改账号;false 会变成 Missing,表示缺少确认。出来的是 AccountMutationConfirmation,不会改动外部状态。

调用关系:它在外层命令解析之后被使用,把用户输入整理成 run 和 run_cleanup 能理解的安全标记。后续 require_confirmation 会根据这个标记决定是否允许继续。

调用图:被 1 处调用(run)。

require_confirmation172–179 ↗
fn require_confirmation(confirmation: AccountMutationConfirmation) -> Result<()>

作用:这个函数是安全闸门。因为测试会真的安装和卸载账号里的插件,所以没有明确确认就直接拒绝运行。

数据流:进去的是确认状态。它检查是否为 Missing;如果缺少确认,就返回一个错误,提示用户加上 --confirm-account-mutation;如果已经确认,就什么也不改并返回成功。

调用关系:run 和 run_cleanup 一开始都会调用它。它位于所有危险操作之前,确保后面的 spawn_client、安装、卸载等动作不会在用户没确认时发生。

调用图:被 2 处调用(run, run_cleanup);外部调用 2 个(bail!, matches!)。

ExpectedInstalledState::is_installed188–190 ↗
fn is_installed(self) -> bool

作用:这个小函数把“期望状态”翻译成真假值。Installed 变成 true,Uninstalled 变成 false,方便和读取到的实际 installed 字段比较。

数据流:进去的是 ExpectedInstalledState。它判断枚举是哪一种,然后出来一个布尔值:是否应该是已安装。它不读取外部信息,也不改状态。

调用关系:wait_for_installed_state 会用它来判断当前插件状态是否已经达到目标。它是轮询等待逻辑里的一个小翻译器。

调用图:被 1 处调用(wait_for_installed_state);外部调用 1 个(matches!)。

spawn_client193–209 ↗
fn spawn_client(
    codex_bin: &Path,
    config_overrides: &[String],
    capture_path: &Path,
) -> Result<CodexClient>

作用:这个函数按测试需要启动 Codex 客户端。它会打开 analytics、插件、远程插件功能,并告诉程序把 analytics 事件写到指定文件。

数据流:进去的是 Codex 可执行文件路径、原始配置覆盖项和事件捕获文件路径。它复制配置,追加测试必须的配置项,再设置一个环境变量指向捕获文件,最后启动一个通过标准输入输出通信的 CodexClient。出来的是可用客户端,或启动失败的错误。

调用关系:run 在准备好捕获文件后调用它。它把真正的进程启动交给 CodexClient::spawn_stdio_with_env,后续 run 会用这个客户端初始化服务、读取插件、发送安装和卸载请求。

调用图:调用 1 个内部函数(spawn_stdio_with_env);被 1 处调用(run);外部调用 1 个(vec!)。

read_remote_plugin222–257 ↗
fn read_remote_plugin(
    client: &mut CodexClient,
    remote_plugin_id: &str,
) -> Result<RemotePluginExpectation>

作用:这个函数向服务端询问某个远程插件当前是什么样。它会拿到本地插件 ID、远程插件 ID、名字、市场名、是否已安装、是否可安装等信息。

数据流:进去的是客户端和远程插件 ID。它生成请求 ID,发送 plugin/read 请求,并要求从指定的远程市场线索里找这个插件;收到响应后,它检查返回的 remote_plugin_id 必须和请求的一样。出来的是 RemotePluginExpectation 这份插件摘要;如果服务端没返回远程 ID 或返回错了,就报错。

调用关系:run 用它读取测试开始前的插件信息,wait_for_installed_state 用它反复查询状态,restore_uninstalled_state 用它判断清理前后是否还安装着。它依赖客户端的 request_id 和 send_request 来和服务端对话。

调用图:调用 2 个内部函数(request_id, send_request);被 3 处调用(restore_uninstalled_state, run, wait_for_installed_state);外部调用 1 个(bail!)。

validate_initial_plugin259–275 ↗
fn validate_initial_plugin(plugin: &RemotePluginExpectation, remote_plugin_id: &str) -> Result<()>

作用:这个函数检查测试能不能安全开始。它要求目标插件一开始必须没安装,而且确实可用、可安装。

数据流:进去的是刚读到的插件信息和远程插件 ID。它依次检查 installed、availability 和 install_policy;如果插件已经安装、不可用或不能安装,就返回错误。出来的是成功或失败,不会改动任何状态。

调用关系:run 在真正安装插件前调用它。它像起跑前的裁判,避免测试从错误状态开始,否则后面无法判断是测试造成的变化,还是账号原本就那样。

调用图:被 1 处调用(run);外部调用 1 个(bail!)。

run_mutation_sequence282–338 ↗
fn run_mutation_sequence(
    client: &mut CodexClient,
    capture_path: &Path,
    expected: &RemotePluginExpectation,
) -> MutationSequenceResult

作用:这是核心测试步骤:安装插件、确认安装事件、卸载插件、确认卸载后事件符合预期。它把“账号状态变化”和“analytics 事件变化”连起来验证。

数据流:进去的是客户端、事件捕获文件路径,以及预期的插件身份信息。它先安装插件,等待服务端显示已安装,再等待捕获文件里出现安装事件;随后卸载插件,等待服务端显示未安装;最后读取该插件的事件并校验事件内容。出来的是校验后的事件列表或错误,并额外标记卸载 RPC 是否报过错。

调用关系:run 会调用它来执行真正的安装/卸载冒烟流程。它内部会用 install_remote_plugin、uninstall_remote_plugin、wait_for_installed_state、wait_for_remote_plugin_event,以及从 analytics 捕获模块来的 read_events_for_remote_plugin 和 validate_mutation_events。它完成后,run 仍会调用 restore_uninstalled_state 做保险清理。

调用图:被 1 处调用(run)。

install_remote_plugin340–355 ↗
fn install_remote_plugin(client: &mut CodexClient, plugin: &RemotePluginExpectation) -> Result<()>

作用:这个函数向服务端发送“安装远程插件”的请求。它只负责发起安装动作,不负责等待安装是否最终生效。

数据流:进去的是客户端和插件信息。它生成请求 ID,组装 plugin/install 请求,里面带上市场名和远程插件名,然后发送给服务端。出来的是成功或错误;成功表示请求被服务端接受,但状态确认要靠后续轮询。

调用关系:它是 run_mutation_sequence 的第一步。安装请求发出后,流程会继续用 wait_for_installed_state 反复读取插件状态,确认后台真的变成已安装。

调用图:调用 2 个内部函数(request_id, send_request)。

uninstall_remote_plugin357–370 ↗
fn uninstall_remote_plugin(client: &mut CodexClient, remote_plugin_id: &str) -> Result<()>

作用:这个函数向服务端发送“卸载远程插件”的请求。它用于测试流程里的卸载,也用于失败后的清理。

数据流:进去的是客户端和远程插件 ID。它生成请求 ID,组装 plugin/uninstall 请求,把插件 ID 放进去,然后发送给服务端。出来的是成功或错误;它本身不保证状态已经立刻变成未安装。

调用关系:restore_uninstalled_state 会调用它来清理账号状态,run_mutation_sequence 也用它执行测试中的卸载步骤。卸载请求之后,流程通常会交给 wait_for_installed_state 去确认服务端最终状态。

调用图:调用 2 个内部函数(request_id, send_request);被 1 处调用(restore_uninstalled_state)。

wait_for_installed_state372–392 ↗
fn wait_for_installed_state(
    client: &mut CodexClient,
    remote_plugin_id: &str,
    expected_state: ExpectedInstalledState,
) -> Result<RemotePluginExpectation>

作用:这个函数会等到插件变成指定状态,比如“已安装”或“未安装”。它不是只查一次,而是在超时时间内反复查,适合处理后台状态需要一点时间才更新的情况。

数据流:进去的是客户端、远程插件 ID 和期望状态。它设定截止时间,然后循环调用 read_remote_plugin;如果实际 installed 字段和期望一致,就返回最新插件信息;如果一直不一致,到点后就返回超时错误。循环之间会短暂睡眠,避免不停轰炸服务端。

调用关系:run_mutation_sequence 用它确认安装和卸载是否真的完成,restore_uninstalled_state 用它确认清理是否成功。它调用 ExpectedInstalledState::is_installed 把期望状态转成可比较的真假值。

调用图:调用 2 个内部函数(is_installed, read_remote_plugin);被 1 处调用(restore_uninstalled_state);外部调用 3 个(now, bail!, sleep)。

restore_uninstalled_state401–433 ↗
fn restore_uninstalled_state(
    client: &mut CodexClient,
    remote_plugin_id: &str,
) -> RestorationStatus

作用:这个函数负责把指定远程插件恢复成“未安装”。它是测试后的安全网,尽量避免冒烟测试把真实账号留下已安装插件。

数据流:进去的是客户端和远程插件 ID。它先读取当前状态;如果本来就未安装,直接返回 Clean;如果已安装,就发送卸载请求,再等待状态变成未安装。出来的是 RestorationStatus:干净、后端已卸但本地清理报错、仍然脏、或无法确认。

调用关系:run 在测试结束后一定会调用它,run_cleanup 也把它当作核心清理动作。它依赖 read_remote_plugin 判断当前状态,依赖 uninstall_remote_plugin 发起卸载,依赖 wait_for_installed_state 做最终确认,并把不同失败情况分类给上层打印不同提示。

调用图:调用 3 个内部函数(read_remote_plugin, uninstall_remote_plugin, wait_for_installed_state);被 2 处调用(run, run_cleanup);外部调用 4 个(anyhow!, Dirty, LocalCleanupFailure, Unknown)。

wait_for_remote_plugin_event435–451 ↗
fn wait_for_remote_plugin_event(
    path: &Path,
    remote_plugin_id: &str,
    event_type: &str,
) -> Result<()>

作用:这个函数等待某个插件的某类 analytics 事件出现在捕获文件里。比如安装后,它会等 codex_plugin_installed 事件真正写出来。

数据流:进去的是捕获文件路径、远程插件 ID 和事件类型。它在超时时间内反复读取该插件相关事件,检查其中是否有指定 event_type;找到了就成功返回,等不到就报超时错误。它只读文件,不改账号状态。

调用关系:run_mutation_sequence 在安装插件之后用它确认 analytics 记录链路已经工作。它把读文件的工作交给 read_events_for_remote_plugin,并通过短暂 sleep 做轮询等待。

调用图:调用 1 个内部函数(read_events_for_remote_plugin);外部调用 3 个(now, bail!, sleep)。

print_dirty_recovery453–463 ↗
fn print_dirty_recovery(
    codex_bin: &Path,
    config_overrides: &[String],
    remote_plugin_id: &str,
    err: &anyhow::Error,
)

作用:这个函数在清理失败、插件看起来仍然已安装时,打印醒目的失败说明和恢复办法。它的重点是告诉人“账号可能还脏着”。

数据流:进去的是 Codex 路径、配置覆盖项、远程插件 ID 和错误详情。它先把 FAIL-DIRTY 信息打印到错误输出,然后调用 print_recovery_command 拼出可复制执行的清理命令。它不返回数据,也不执行清理命令。

调用关系:run 和 run_cleanup 在 restore_uninstalled_state 返回 Dirty 时会调用它。它自己再调用 print_recovery_command,把技术错误转成用户下一步能做的操作。

调用图:调用 1 个内部函数(print_recovery_command);被 2 处调用(run, run_cleanup);外部调用 1 个(eprintln!)。

print_recovery_command465–483 ↗
fn print_recovery_command(codex_bin: &Path, config_overrides: &[String], remote_plugin_id: &str)

作用:这个函数生成一条手动恢复命令,让用户可以复制后再次尝试卸载远程插件。它主要用于测试没能确认清理成功的场景。

数据流:进去的是 Codex 可执行文件路径、配置覆盖项和远程插件 ID。它先找当前测试客户端程序路径,找不到就用默认名字;然后把 Codex 路径、配置项和 plugin-remote-uninstall 命令拼起来,并对参数做 shell 引号保护,最后打印到错误输出。出来的是屏幕上的恢复命令,不会直接执行。

调用关系:print_dirty_recovery 会调用它;run 在状态未知时也会直接调用它。它位于失败处理的最后一环,作用是把复杂的恢复步骤整理成一条人能复制的命令。

调用图:被 2 处调用(print_dirty_recovery, run);外部调用 3 个(eprintln!, format!, current_exe)。