Codex 系统手册

Exec-server、沙箱和远程传输测试工具

stage-23.449 个文件

这一阶段不是正式干活的主流程,而是一整套“试车场”和测试工具,专门检查 exec-server、远程连接、文件读写、进程执行和沙箱安全有没有坏。公共工具先负责启动假服务器、真服务器、Wine 里的 Windows 服务器,帮测试连上、发消息、收尾。另一批测试检查 WebSocket、初始化、健康检查、HTTP、Noise 加密和中继传输。文件系统测试盯住路径、链接、权限和大文件读取。沙箱测试则分别在 Linux、macOS、Windows 上确认命令被关进安全围栏,输入输出桥和清理工作也不掉链子。

本阶段的文件49

exec-server 测试框架基础

这些文件建立可复用的 exec-server 测试框架,并验证服务器的基本传输、初始化、健康状态和核心处理器行为。

exec-server/tests/common/mod.rs源码 ↗
testtest startup and subprocess launch

测试 exec-server 时,光调用普通函数不够,还要真的启动子进程,模拟外部命令、沙箱程序和服务器进程。这个文件就在测试程序一启动时先装好一套“分身规则”:看到特定参数,就不要继续跑普通测试,而是改去执行对应的小工具。比如参数是 exec-server,就启动真正的 exec-server 并监听指定地址;参数是延迟输出测试,就先让父进程退出,再让子进程稍后打印内容,用来检查系统会不会过早丢掉输出。它还提供当前测试二进制和 Linux 沙箱二进制的位置,方便其他测试启动正确的 helper。可以把它理解成测试里的“后台演员调度室”:正式测试是舞台,很多隐藏演员都由这里按暗号上场。

函数细节7
current_test_binary_helper_paths40–51 ↗
fn current_test_binary_helper_paths() -> anyhow::Result<(PathBuf, Option<PathBuf>)>

作用:告诉测试代码:当前这个测试程序在哪里,以及在 Linux 上应该用哪个程序当沙箱 helper。有人要启动子进程做集成测试时,会用它拿到正确路径。

数据流:进去不需要业务参数,只读取操作系统告诉它的当前可执行文件路径,并查看当前是不是 Linux。然后它根据测试分发器里记录的别名路径,决定 Linux 沙箱程序路径;如果没有单独路径,就退回使用当前测试程序自己。出来的是一对路径:当前测试二进制路径,以及可选的 Linux 沙箱二进制路径。

调用关系:它被更高层的测试使用,比如检查进程退出后输出是否还会被保留,以及测试远程环境如何路由加密的 exec-server 调用。它本身不启动任何东西,只把后续测试要用的“工具地址”准备好。

调用图:被 2 处调用(assert_exec_process_retains_output_after_exit_until_streams_close, remote_environment_routes_encrypted_exec_server_rpc);外部调用 2 个(cfg!, current_exe)。

maybe_run_delayed_output_after_exit_from_test_binary53–70 ↗
fn maybe_run_delayed_output_after_exit_from_test_binary()

作用:检查当前测试程序是不是被当成“延迟输出测试小程序”启动了。如果是,它就立刻切换身份,去跑父进程或子进程的那段特殊逻辑。

数据流:进去没有显式参数,它直接读取命令行参数。它跳过程序名,查看第一个真正参数是不是约定好的父进程暗号或子进程暗号;如果是,就继续解析释放文件路径,再调用对应的运行函数;如果不是,就什么也不做,让普通测试继续跑。

调用关系:它在测试二进制启动早期被调用,是测试分发机制的一部分。它把参数解析交给 next_release_path_arg,把真正的父进程行为交给 run_delayed_output_after_exit_parent,把真正的子进程等待和打印交给 run_delayed_output_after_exit_child。

调用图:调用 3 个内部函数(next_release_path_arg, run_delayed_output_after_exit_child, run_delayed_output_after_exit_parent);外部调用 1 个(args)。

next_release_path_arg72–82 ↗
fn next_release_path_arg(mut args: impl Iterator<Item = String>) -> PathBuf

作用:从命令行里取出那个“释放信号文件”的路径,并确保参数数量刚刚好。这样测试小程序不会因为少参数或多参数而悄悄跑错。

数据流:进去是一串还没读完的命令行参数。它取下一个参数当作路径;如果没有,就打印错误并退出进程;如果还有多余参数,也打印错误并退出进程。正常情况下,出来的是一个 PathBuf,也就是 Rust 里表示文件路径的对象。

调用关系:它只服务于 maybe_run_delayed_output_after_exit_from_test_binary。父进程模式和子进程模式都需要同一个释放路径,所以先由这个函数统一把路径读干净,再交给后面的运行函数。

调用图:被 1 处调用(maybe_run_delayed_output_after_exit_from_test_binary);外部调用 4 个(next, from, eprintln!, exit)。

run_delayed_output_after_exit_parent84–104 ↗
fn run_delayed_output_after_exit_parent(release_path: &Path)

作用:扮演延迟输出测试里的“父进程”:它启动一个子进程后马上退出。这个设计用来模拟一种麻烦情况:父进程没了,但它留下的子进程之后还会往标准输出写东西。

数据流:进去是释放信号文件的路径。它先找到当前测试二进制文件的位置,然后用这个同一个程序再启动一个子进程,并传入子进程暗号和释放路径;子进程的标准输入被关掉。启动成功后,父进程直接以成功状态退出;启动失败则打印错误并以失败状态退出。

调用关系:它由 maybe_run_delayed_output_after_exit_from_test_binary 在看到父进程暗号时调用。它不自己打印延迟输出,而是把后续工作交给新启动的 run_delayed_output_after_exit_child 对应进程。

调用图:被 1 处调用(maybe_run_delayed_output_after_exit_from_test_binary);外部调用 5 个(null, new, current_exe, eprintln!, exit)。

run_delayed_output_after_exit_child106–127 ↗
fn run_delayed_output_after_exit_child(release_path: &Path)

作用:扮演延迟输出测试里的“子进程”:它等到某个文件出现后,才往标准输出写一行文字。这样可以测试 exec-server 是否会等所有输出流真正关闭,而不是父进程一退出就草草收工。

数据流:进去是释放信号文件的路径。它最多循环 1000 次,每次检查这个路径是否已经存在;还不存在就睡 10 毫秒再试。文件一出现,它就向标准输出写入“late output after exit”,刷新输出并成功退出;如果写入、刷新失败,或一直等不到文件,它会打印错误并失败退出。

调用关系:它由 maybe_run_delayed_output_after_exit_from_test_binary 在看到子进程暗号时调用。通常它是 run_delayed_output_after_exit_parent 启动出来的后代进程,用来制造“父进程退出后才来的输出”这个测试场景。

调用图:被 1 处调用(maybe_run_delayed_output_after_exit_from_test_binary);外部调用 7 个(from_millis, exists, eprintln!, stdout, exit, sleep, writeln!)。

maybe_run_exec_server_from_test_binary129–192 ↗
fn maybe_run_exec_server_from_test_binary(guard: Option<&TestBinaryDispatchGuard>)

作用:检查当前测试程序是不是被要求直接当 exec-server 运行。如果是,它就解析监听地址,搭好运行环境,然后启动真正的 exec-server。

数据流:进去是可选的测试分发守卫,它里面可能记录了一些 helper 程序的临时路径。函数读取命令行参数,只有第一个参数是 exec-server 时才继续;然后要求参数格式必须是 --listen 加一个监听 URL,不能多也不能少。接着它找到当前可执行文件,计算运行时需要的 helper 路径,创建 Tokio runtime(Tokio 是 Rust 常用的异步任务运行器),最后调用 codex_exec_server::run_main。运行成功就以 0 退出,失败就打印错误并以 1 退出。

调用关系:它在测试二进制启动早期被调用,和普通测试抢先判断当前进程该不该变身成服务器。它会调用 linux_sandbox_exe 决定 Linux 沙箱路径,再把监听地址和运行路径交给 codex_exec_server::run_main,真正的服务器主逻辑在那里执行。

调用图:调用 2 个内部函数(new, linux_sandbox_exe);外部调用 6 个(run_main, args, current_exe, eprintln!, exit, new_multi_thread)。

linux_sandbox_exe194–210 ↗
fn linux_sandbox_exe(
    guard: Option<&TestBinaryDispatchGuard>,
    current_exe: &std::path::Path,
) -> Option<PathBuf>

作用:决定在当前平台上,exec-server 测试应该使用哪个 Linux 沙箱可执行文件。非 Linux 平台不需要这个东西,所以会返回空。

数据流:进去是可选的测试分发守卫,以及当前测试二进制路径。在 Linux 上,它优先使用守卫里记录的 codex_linux_sandbox_exe 路径;如果没有,就用当前测试二进制自己当沙箱入口。在非 Linux 上,它忽略这些输入,直接返回 None,表示没有沙箱程序路径。

调用关系:它被 maybe_run_exec_server_from_test_binary 调用,是启动测试版 exec-server 前准备运行路径的一小步。它不启动沙箱,只负责告诉 ExecServerRuntimePaths 应该把哪个文件当作沙箱 helper。

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

exec-server/tests/common/exec_server.rs源码 ↗
testtest setup, request handling, teardown

这个文件把测试 exec-server 时最麻烦的杂活包成一个工具。它先找到测试用的 codex 可执行文件,创建一个临时的 CODEX_HOME 目录,启动子进程,并让服务器监听一个本机 WebSocket 地址。WebSocket 可以理解成一根双向电话线,测试和服务器都能往里发消息。服务器启动后会把实际监听地址打印到标准输出,这里会读出来,然后反复尝试连接,直到服务器真的准备好。核心结构 ExecServerHarness 像一个测试遥控器:可以发 JSON-RPC 请求和通知,JSON-RPC 是一种“带方法名和参数的 JSON 消息格式”;也可以发送故意错误的原始文本或二进制内容,用来测服务器抗不抗造;还可以等待服务器回事件、断线重连、主动关闭。它还有一个重要保险:测试对象被丢弃时会尝试杀掉子进程,避免测试失败后留下孤儿服务器。

函数细节18
ExecServerHarness::drop41–43 ↗
fn drop(&mut self)

作用:这是测试工具被销毁时的兜底清理。它会尽量杀掉正在跑的 exec-server 子进程,防止测试结束后后台还残留一个服务器。

数据流:进去的是 ExecServerHarness 里保存的子进程句柄 → 它调用系统提供的 start_kill 发出终止信号,并且忽略清理时的错误 → 出来没有返回值,但外部进程会被要求停止。

调用关系:它不由测试手动调用,而是在 ExecServerHarness 生命周期结束时自动发生。它把最后的清理工作交给子进程句柄的 start_kill,和 shutdown 的主动关闭形成双保险。

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

test_codex_helper_paths51–57 ↗
fn test_codex_helper_paths() -> anyhow::Result<TestCodexHelperPaths>

作用:这个函数找到测试要启动的 codex 辅助程序路径。测试不能凭空启动服务器,必须先知道可执行文件在哪里。

数据流:进去没有参数 → 它询问外层测试工具 current_test_binary_helper_paths,拿到 codex 主程序路径和可选的 Linux 沙箱程序路径 → 出来是 TestCodexHelperPaths,里面装着这些路径。

调用关系:exec_server_with_env 启动服务器前会先用它找程序位置;create_file_system_context 这类测试辅助也会用它准备文件系统相关环境。

调用图:被 2 处调用(exec_server_with_env, create_file_system_context);外部调用 1 个(current_test_binary_helper_paths)。

exec_server59–61 ↗
async fn exec_server() -> anyhow::Result<ExecServerHarness>

作用:这是最常用的默认启动入口:用标准环境启动一个 exec-server 测试实例。多数测试只要调用它,就能拿到一个可发消息、可收事件的测试遥控器。

数据流:进去没有参数 → 它传入一个空的环境变量列表,交给 exec_server_with_env 做真正的启动工作 → 出来是一个已经连上 WebSocket 的 ExecServerHarness。

调用关系:大量 exec-server 测试会直接调用它,比如进程上下文、文件读取、句柄容量限制、流读取等测试。它自己不做细节,只是把活儿转给 exec_server_with_env。

调用图:调用 1 个内部函数(exec_server_with_env);被 24 处调用(create_process_context, completed_streams_release_handle_capacity, file_reads_reject_fifo_without_waiting_for_a_writer, file_reads_reject_named_pipes, open_enforces_the_per_connection_limit_and_close_releases_capacity, open_rejects_handle_ids_longer_than_32_bytes, read_block_supports_non_sequential_offsets_and_lengths, stream_keeps_reading_the_open_file_after_path_replacement, stream_rejects_platform_sandbox, stream_stops_after_an_exact_block_boundary (+14 more))。

exec_server_with_env63–91 ↗
async fn exec_server_with_env(env: I) -> anyhow::Result<ExecServerHarness>

作用:这个函数用指定环境变量启动 exec-server。它适合那些需要改环境来验证特殊行为的测试。

数据流:进去是一组额外环境变量 → 它先找 helper 程序路径,创建临时 CODEX_HOME,配置子进程参数、标准输入输出和环境变量,启动进程;然后从标准输出读出 WebSocket 地址,再等服务器可连接 → 出来是 ExecServerHarness,里面保存临时目录、子进程、地址、WebSocket 连接和下一个请求编号。

调用关系:exec_server 是它的简化包装;需要特殊环境的测试,比如 sandboxed_file_system_helper_finds_bwrap_on_preserved_path,会直接调用它。它把读地址交给 read_listen_url_from_stdout,把连接等待交给 connect_websocket_when_ready。

调用图:调用 3 个内部函数(connect_websocket_when_ready, read_listen_url_from_stdout, test_codex_helper_paths);被 2 处调用(exec_server, sandboxed_file_system_helper_finds_bwrap_on_preserved_path);外部调用 5 个(inherit, null, piped, new, new)。

ExecServerHarness::websocket_url94–96 ↗
fn websocket_url(&self) -> &str

作用:这个函数把当前 exec-server 的 WebSocket 地址拿出来。测试如果要检查地址,或者自己建立额外连接,就会用到它。

数据流:进去的是 harness 自身 → 它只读取里面保存的 websocket_url 字符串引用 → 出来是这个地址,不改动任何状态。

调用关系:它是 ExecServerHarness 暴露给测试的小窗口,不参与启动或发消息流程,只负责让外部看见服务器监听在哪里。

ExecServerHarness::disconnect_websocket98–101 ↗
async fn disconnect_websocket(&mut self) -> anyhow::Result<()>

作用:这个函数主动断开当前 WebSocket 连接。它常用来测试服务器遇到客户端断线时会不会表现正常。

数据流:进去的是当前 harness 和里面的 WebSocket 连接 → 它发送关闭帧,让这根双向通信线正常收口 → 出来是成功或失败结果,连接状态被改为关闭。

调用关系:它把真正的关闭动作交给 WebSocket 库的 close。之后如果测试还想继续通信,可以调用 reconnect_websocket 重新连上。

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

ExecServerHarness::reconnect_websocket103–107 ↗
async fn reconnect_websocket(&mut self) -> anyhow::Result<()>

作用:这个函数用原来的地址重新连到 exec-server。它让测试可以模拟“客户端断了又回来”的场景。

数据流:进去的是 harness 中保存的 websocket_url → 它调用 connect_websocket_when_ready,拿到新的 WebSocket 连接 → 出来是成功或失败结果,并把 harness 里的旧连接替换成新连接。

调用关系:它依赖 connect_websocket_when_ready 处理“服务器还没马上接受连接”的等待和重试。通常会在 disconnect_websocket 之后使用。

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

ExecServerHarness::send_request109–124 ↗
async fn send_request(
        &mut self,
        method: &str,
        params: serde_json::Value,
    ) -> anyhow::Result<RequestId>

作用:这个函数向 exec-server 发送一条 JSON-RPC 请求。请求有编号,所以测试之后可以把服务器回复和这次请求对应起来。

数据流:进去是方法名和 JSON 参数 → 它用当前 next_request_id 生成 RequestId,再把编号加一,组装成 JSONRPCRequest,并交给 send_message 发送 → 出来是这次请求的编号,同时 WebSocket 上已经发出一条请求消息。

调用关系:initialize_exec_server 这类测试初始化流程会用它发正式请求。它不直接碰编码和网络发送,而是把消息交给 send_message 统一处理。

调用图:调用 1 个内部函数(send_message);被 1 处调用(initialize_exec_server);外部调用 2 个(Request, Integer)。

ExecServerHarness::send_notification126–136 ↗
async fn send_notification(
        &mut self,
        method: &str,
        params: serde_json::Value,
    ) -> anyhow::Result<()>

作用:这个函数向 exec-server 发送一条 JSON-RPC 通知。通知没有请求编号,意思是“告诉你一件事”,通常不要求服务器按编号回复。

数据流:进去是方法名和 JSON 参数 → 它组装成 JSONRPCNotification,再交给 send_message 编码并发送 → 出来是成功或失败结果,不生成请求编号。

调用关系:initialize_exec_server 会用它发送初始化过程中的通知。它和 send_request 共享 send_message,区别是它发的是无编号消息。

调用图:调用 1 个内部函数(send_message);被 1 处调用(initialize_exec_server);外部调用 1 个(Notification)。

ExecServerHarness::send_raw_text138–143 ↗
async fn send_raw_text(&mut self, text: &str) -> anyhow::Result<()>

作用:这个函数直接往 WebSocket 发一段原始文本,不帮你包装成 JSON-RPC。它常用于故意发送坏消息,测试服务器是否能正确报错而不是崩掉。

数据流:进去是一段字符串 → 它把字符串包成 WebSocket 的文本帧并发送 → 出来是成功或失败结果,服务器会收到这段原始文本。

调用关系:它绕过 send_message,不做 JSON 编码。测试需要模拟异常输入时会直接用它。

调用图:外部调用 2 个(send, Text)。

ExecServerHarness::send_raw_binary145–148 ↗
async fn send_raw_binary(&mut self, bytes: Vec<u8>) -> anyhow::Result<()>

作用:这个函数直接往 WebSocket 发一段原始二进制数据。它用来测试服务器面对非文本或特殊字节内容时的反应。

数据流:进去是一组字节 → 它把这些字节包成 WebSocket 的二进制帧并发送 → 出来是成功或失败结果,服务器会收到这段二进制内容。

调用关系:它也绕过 JSON-RPC 包装,和 send_raw_text 一样服务于“坏输入”或边界情况测试。

调用图:外部调用 2 个(send, Binary)。

ExecServerHarness::next_event150–152 ↗
async fn next_event(&mut self) -> anyhow::Result<JSONRPCMessage>

作用:这个函数等待服务器发来的下一条 JSON-RPC 消息。它给测试一个简单办法:拿到服务器接下来发生的事件或回复。

数据流:进去的是 harness 当前的 WebSocket 连接 → 它使用默认的 EVENT_TIMEOUT 超时时间调用 next_event_with_timeout → 出来是一条解析好的 JSONRPCMessage,或者超时、断线、解析失败等错误。

调用关系:collect_response_body_deltas 会用它连续收集响应内容片段。它是 next_event_with_timeout 的便捷包装。

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

ExecServerHarness::wait_for_event154–175 ↗
async fn wait_for_event(
        &mut self,
        mut predicate: F,
    ) -> anyhow::Result<JSONRPCMessage>

作用:这个函数一直收服务器消息,直到某条消息符合测试给出的条件。它适合服务器会发很多事件、测试只关心其中一种的情况。

数据流:进去是一个判断函数 predicate,以及当前 WebSocket 连接 → 它在总超时时间内一条条调用 next_event_with_timeout 取消息,每取到一条就让 predicate 判断 → 出来是第一条匹配的消息;如果一直没有匹配,就返回超时错误。

调用关系:wait_for_response 和 wait_for_error_response 这类辅助会用它等待指定回复。它把单次收消息交给 next_event_with_timeout,自己负责循环和筛选。

调用图:调用 1 个内部函数(next_event_with_timeout);被 2 处调用(wait_for_error_response, wait_for_response);外部调用 2 个(now, anyhow!)。

ExecServerHarness::shutdown177–183 ↗
async fn shutdown(&mut self) -> anyhow::Result<()>

作用:这个函数主动关闭测试启动的 exec-server,并等待它真的退出。比起只发杀进程信号,它还会确认收尾完成。

数据流:进去的是 harness 里的子进程句柄 → 它先调用 start_kill 要求进程停止,再用 timeout 包住 child.wait 等待退出 → 出来是成功或错误;成功时子进程已经结束,超时则报告关闭失败。

调用关系:测试如果想在中途明确结束服务器,会调用它。它和 Drop 的自动清理不同:shutdown 会等待并报告结果,Drop 只是兜底尝试杀掉。

调用图:外部调用 3 个(start_kill, wait, timeout)。

ExecServerHarness::send_message185–189 ↗
async fn send_message(&mut self, message: JSONRPCMessage) -> anyhow::Result<()>

作用:这是发送 JSON-RPC 消息的内部统一出口。它把 Rust 里的消息对象变成 JSON 文本,再通过 WebSocket 发出去。

数据流:进去是一条 JSONRPCMessage → 它用 serde_json 转成字符串,再包成 WebSocket 文本帧发送 → 出来是成功或失败结果,网络上已经发出一段 JSON 文本。

调用关系:send_request 和 send_notification 都把最终发送工作交给它。这样两种消息共享同一套编码和发送方式,减少重复。

调用图:被 2 处调用(send_notification, send_request);外部调用 3 个(send, to_string, Text)。

ExecServerHarness::next_event_with_timeout191–213 ↗
async fn next_event_with_timeout(
        &mut self,
        timeout_duration: Duration,
    ) -> anyhow::Result<JSONRPCMessage>

作用:这个函数在指定时间内等下一条服务器消息,并把它解析成 JSON-RPC 消息。它是接收事件的底层工具。

数据流:进去是一个超时时长和当前 WebSocket 连接 → 它等待下一帧;如果是文本,就按 JSON 字符串解析;如果是二进制,就按 JSON 字节解析;如果是关闭帧就报连接关闭;如果是 ping/pong 这类心跳帧就忽略继续等 → 出来是一条 JSONRPCMessage,或超时、断线、解析错误。

调用关系:next_event 用它实现默认超时等待,wait_for_event 用它反复取消息并筛选。它集中处理 WebSocket 帧到 JSON-RPC 消息的转换。

调用图:被 2 处调用(next_event, wait_for_event);外部调用 5 个(next, anyhow!, from_slice, from_str, timeout)。

connect_websocket_when_ready216–239 ↗
async fn connect_websocket_when_ready(
    websocket_url: &str,
) -> anyhow::Result<(
    tokio_tungstenite::WebSocketStream<tokio_tungstenite::MaybeTlsStream<tokio::net::TcpStream>>,
    tokio_tungst

作用:这个函数负责连接 exec-server 的 WebSocket,并处理“服务器刚启动还没准备好”的短暂空窗。它会在一段时间内反复重试连接。

数据流:进去是 WebSocket 地址字符串 → 它调用 connect_async 尝试连接;如果只是连接被拒绝,并且还没超过总超时,就睡一小会儿再试;如果连上就返回连接和握手响应;如果是别的错误或超时后仍失败,就返回错误 → 出来是可用的 WebSocket 连接或失败原因。

调用关系:exec_server_with_env 启动服务器后用它建立第一次连接;reconnect_websocket 用它重新连接。它把“等服务器准备好”的细节从测试代码里藏起来。

调用图:被 2 处调用(reconnect_websocket, exec_server_with_env);外部调用 4 个(now, matches!, sleep, connect_async)。

read_listen_url_from_stdout241–266 ↗
async fn read_listen_url_from_stdout(child: &mut Child) -> anyhow::Result<String>

作用:这个函数从 exec-server 的标准输出里读出它实际监听的 WebSocket 地址。因为测试让服务器使用端口 0,意思是让系统随机挑一个空闲端口,所以必须读回真实地址。

数据流:进去是子进程句柄 → 它取走子进程的 stdout,用按行读取器一行行读,在超时前寻找以 ws:// 开头的行 → 出来是监听地址字符串;如果 stdout 没抓到、提前关闭、或等太久都没地址,就返回错误。

调用关系:exec_server_with_env 启动子进程后马上调用它拿地址,然后再把地址交给 connect_websocket_when_ready 去连接。它是从“进程已启动”走到“客户端知道去哪连”的关键桥梁。

调用图:被 1 处调用(exec_server_with_env);外部调用 4 个(new, now, anyhow!, timeout)。

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

执行服务器可以用不同方式和外界说话,比如 WebSocket(一种常见的网络长连接)或 stdio(标准输入输出,也就是从进程的输入读消息、往输出写消息)。这个测试文件就是防止这些“接线口”坏掉。它先检查 parse_listen_url 能不能把用户传入的监听地址正确翻译成内部类型:默认 WebSocket、stdio、stdio://、合法的 ws://IP:PORT 都应该通过;hostname 或不支持的 http:// 则应该给出清楚错误。文件里最重要的异步测试会用 tokio 的 duplex 做一对假的输入输出管道,像在内存里拉了两根线,把测试客户端和服务器接起来。测试客户端发送 JSON-RPC(一种用 JSON 包装请求和响应的通信格式)初始化请求,确认服务器返回 session_id,然后再发送 initialized 通知并断开,确保 stdio 通道能干净结束。

函数细节9
parse_listen_url_accepts_default_websocket_url27–37 ↗
fn parse_listen_url_accepts_default_websocket_url()

作用:这个测试确认服务器内置的默认监听地址能被正确理解成 WebSocket 地址。它保证“什么都不特别配置”时,默认入口不会一启动就解析失败。

数据流:输入是 DEFAULT_LISTEN_URL 这个默认字符串 → 测试把它交给 parse_listen_url 解析 → 结果应该变成 WebSocket,并且地址是 127.0.0.1:0;如果不是,就用断言让测试失败。

调用关系:它直接检查 parse_listen_url 这个解析函数的默认场景。这个测试不再把工作交给别的本文件函数,只负责守住默认配置这条路。

调用图:外部调用 2 个(assert_eq!, parse_listen_url)。

parse_listen_url_accepts_stdio40–43 ↗
fn parse_listen_url_accepts_stdio()

作用:这个测试确认用户写 --listen stdio 时,服务器会选择标准输入输出通信。这样命令行或父进程用管道启动服务器时,就能用最简单的写法。

数据流:输入是字符串 stdio → parse_listen_url 把它解析成内部的 Stdio 选项 → 测试比较结果,确认没有被误认成网络地址或报错。

调用关系:它覆盖 parse_listen_url 的 stdio 简写形式。它和后面的 stdio:// 测试一起保证同一种通信方式的两种写法都可用。

调用图:外部调用 2 个(assert_eq!, parse_listen_url)。

parse_listen_url_accepts_stdio_url46–49 ↗
fn parse_listen_url_accepts_stdio_url()

作用:这个测试确认 stdio:// 这种像网址一样的写法也能表示标准输入输出通信。它让配置写法更宽容,减少用户因为格式小差异踩坑。

数据流:输入是字符串 stdio:// → parse_listen_url 解析它 → 输出应该仍然是 Stdio;测试用断言确认这一点。

调用关系:它同样围绕 parse_listen_url,只是检查 stdio 的 URL 形式。它补上了 parse_listen_url_accepts_stdio 没覆盖的写法。

调用图:外部调用 2 个(assert_eq!, parse_listen_url)。

stdio_listen_transport_serves_initialize52–112 ↗
async fn stdio_listen_transport_serves_initialize()

作用:这个异步测试确认 stdio 通道不只是能被解析出来,还真的能跑通一次初始化通信。也就是说,客户端发 initialize 请求后,服务器要能回一个有效会话号,并在客户端断开后正常收尾。

数据流:一开始它把 stdio 字符串解析成 Stdio,然后用内存里的双向管道模拟客户端和服务器之间的输入输出 → 启动 run_stdio_connection_with_io 作为服务器任务,并构造 JSON-RPC initialize 请求写进去 → 从客户端读回服务器响应,解析成 InitializeResponse,检查请求 id 对得上、session_id 不是空的 → 再发送 initialized 通知,关闭客户端写入端和读取端 → 最后等待服务器任务结束,确认它没有超时、没有崩溃、没有返回错误。

调用关系:这是本文件里最接近真实使用的一条端到端测试。它调用 test_runtime_paths 准备服务器运行时需要的路径,用 write_jsonrpc_line 把请求和通知写成一行一条的 JSON-RPC 消息,然后驱动 run_stdio_connection_with_io 验证 stdio 传输层真的能服务初始化流程。

调用图:调用 2 个内部函数(test_runtime_paths, write_jsonrpc_line);外部调用 16 个(new, from_secs, Notification, Request, Integer, assert!, assert_eq!, panic!, from_str, from_value (+6 more))。

parse_listen_url_accepts_websocket_url115–126 ↗
fn parse_listen_url_accepts_websocket_url()

作用:这个测试确认合法的 WebSocket 监听地址会被接受。它保护的是网络模式下最基本的配置入口。

数据流:输入是 ws://127.0.0.1:1234 → parse_listen_url 解析这个字符串 → 输出应该是 WebSocket,并带着 127.0.0.1:1234 这个 socket 地址;测试用比较确认结果准确。

调用关系:它直接测试 parse_listen_url 的 WebSocket 正常路径。和默认 WebSocket 测试相比,它检查的是用户显式写出的地址。

调用图:外部调用 2 个(assert_eq!, parse_listen_url)。

parse_listen_url_rejects_invalid_websocket_url129–136 ↗
fn parse_listen_url_rejects_invalid_websocket_url()

作用:这个测试确认 ws://localhost:1234 会被拒绝,因为这里要求写明确的 IP 地址,而不是 localhost 这种主机名。这样可以避免绑定地址含糊不清,减少运行时意外。

数据流:输入是 ws://localhost:1234 → parse_listen_url 尝试解析但应该失败 → 测试拿到错误信息,并确认它清楚说明期望格式是 ws://IP:PORT。

调用关系:它检查 parse_listen_url 的错误路径。它和接受合法 WebSocket 的测试配对,说明这个解析器不是随便放行,而是有明确格式要求。

调用图:外部调用 2 个(assert_eq!, parse_listen_url)。

parse_listen_url_rejects_unsupported_url139–146 ↗
fn parse_listen_url_rejects_unsupported_url()

作用:这个测试确认不支持的协议,比如 http://,会被明确拒绝。它防止用户以为服务器能用普通 HTTP 方式监听,结果后面才出现更难懂的问题。

数据流:输入是 http://127.0.0.1:1234 → parse_listen_url 判断协议不在允许范围内 → 返回错误;测试检查错误文字是否说明只支持 ws://IP:PORT 或 stdio。

调用关系:它守住 parse_listen_url 的边界条件。它和其他解析测试一起,把“哪些写法能用、哪些写法不能用”固定下来。

调用图:外部调用 2 个(assert_eq!, parse_listen_url)。

write_jsonrpc_line148–158 ↗
async fn write_jsonrpc_line(writer: &mut tokio::io::DuplexStream, message: &JSONRPCMessage)

作用:这个小工具函数把一条 JSON-RPC 消息写进模拟连接里,并在末尾加换行。服务器按“一行一条消息”来读,所以这个换行很关键。

数据流:输入是一条 JSONRPCMessage 和一个可写的内存流 → 函数先把消息序列化成 JSON 字节 → 写入流里,再写入一个换行符 → 输出没有单独返回值,但会改变流里的内容,让对面能读到完整消息。

调用关系:它被 stdio_listen_transport_serves_initialize 调用两次:一次写 initialize 请求,一次写 initialized 通知。它把测试里的“发消息”动作包装起来,让主测试不用重复关心 JSON 编码和换行细节。

调用图:被 1 处调用(stdio_listen_transport_serves_initialize);外部调用 2 个(write_all, to_vec)。

test_runtime_paths160–166 ↗
fn test_runtime_paths() -> ExecServerRuntimePaths

作用:这个小工具函数给测试里的服务器准备运行时路径。服务器启动时需要知道当前可执行文件等位置,所以测试也要给它一份看起来真实的路径配置。

数据流:它读取当前正在运行的测试可执行文件路径 → 用这个路径创建 ExecServerRuntimePaths,并把 Linux sandbox 可执行文件位置设为空 → 返回可用的运行时路径对象;如果路径读取或创建失败,测试会直接失败。

调用关系:它被 stdio_listen_transport_serves_initialize 在启动 stdio 服务器任务前调用。它负责补齐服务器运行所需的环境信息,让主测试能专心验证通信流程。

调用图:调用 1 个内部函数(new);被 1 处调用(stdio_listen_transport_serves_initialize);外部调用 1 个(current_exe)。

exec-server/tests/initialize.rs源码 ↗
testtest runtime

这个测试只在 Unix 系统上运行。它先启动一个测试用的 exec-server,然后像真实客户端一样发出一个 JSON-RPC 请求。JSON-RPC 可以简单理解成“用 JSON 格式写信,请服务器做一件事,再等它回信”。这里请求的方法名是 initialize,意思是告诉服务器“我要开始和你通信了”。测试接着等服务器回消息,确认收到的是响应,不是别的事件;再确认响应的编号和刚才发出去的请求编号一致,避免把别人的回信当成自己的。最后,它把返回内容解析成 InitializeResponse,并检查里面的 session_id 是合法的 UUID,也就是一个标准格式的唯一会话号。测试结束前还会关闭服务器,避免后台进程残留。

函数细节1
exec_server_accepts_initialize14–36 ↗
async fn exec_server_accepts_initialize() -> anyhow::Result<()>

作用:这个测试函数检查 exec-server 的初始化流程是否真的能跑通。有人改了初始化协议、返回格式或测试服务器启动方式后,它能第一时间发现“客户端刚连上就失败”的问题。

数据流:进去的是一个测试环境:函数先通过 exec_server 启动服务器;然后把 InitializeParams 转成 JSON,发送 initialize 请求;服务器返回一条消息后,函数检查它是不是 JSON-RPC 响应、响应编号是不是刚才那次请求的编号;接着把响应内容转成 InitializeResponse,并用 UUID 解析器检查 session_id 格式正确;最后关闭服务器。出来的结果是 Ok,表示初始化成功;如果中间任何一步不对,测试就会报错或 panic。

调用关系:它是测试框架 Tokio 在运行测试时调用的异步测试。它把启动服务器的工作交给 common 里的 exec_server,把参数和结果的 JSON 转换交给 serde_json,把会话编号格式检查交给 Uuid::parse_str,并用 assert_eq! 确认请求和响应对得上。整个流程模拟了一个真实客户端第一次连接服务器时会做的事情。

调用图:调用 1 个内部函数(exec_server);外部调用 5 个(parse_str, assert_eq!, panic!, from_value, to_value)。

exec-server/tests/health.rs源码 ↗
testtest run

这个测试文件像是给 exec-server 做一次开机体检。exec-server 可以理解成一个单独运行的小服务,别的代码通过 WebSocket(一种保持连接、来回传消息的网络通道)跟它说话。第一个测试确认:服务启动后,除了 WebSocket 地址可用,普通 HTTP 的 /readyz 地址也会返回成功,这通常给监控或部署系统用,表示“我已经能工作了”。第二个测试确认:如果把 Environment 配成远程模式,它会真的去问 exec-server 获取环境信息,而且拿到的结果应该和本地直接获取的一样。测试最后都会主动关掉服务器,避免留下后台进程或占用端口。

函数细节2
exec_server_serves_readyz_alongside_websocket_endpoint10–22 ↗
async fn exec_server_serves_readyz_alongside_websocket_endpoint() -> anyhow::Result<()>

作用:这个测试确认 exec-server 的健康检查地址 /readyz 是可用的。也就是说,服务器在提供 WebSocket 功能的同时,也能用普通 HTTP 告诉外界“我准备好了”。

数据流:测试先启动一个临时 exec-server,拿到它的 WebSocket 地址;然后把地址里的 ws:// 换成普通 HTTP 可访问的基础地址,拼出 /readyz;接着用 reqwest 发 GET 请求;最后检查返回状态是不是 OK。测试结束前会关闭这个临时服务器。

调用关系:它先调用 common 里的 exec_server 来启动测试服务器,再用 format! 拼出健康检查网址,用 reqwest::get 发请求,最后用 assert_eq! 核对状态码。这个测试站在外部使用者角度,验证服务启动后健康检查入口确实能被访问。

调用图:调用 1 个内部函数(exec_server);外部调用 3 个(assert_eq!, format!, get)。

remote_environment_fetches_info_from_exec_server25–36 ↗
async fn remote_environment_fetches_info_from_exec_server() -> anyhow::Result<()>

作用:这个测试确认远程环境对象真的会通过 exec-server 取信息,而且取到的信息和本地环境一致。这样可以防止远程模式看起来启用了,实际却拿错或漏拿环境数据。

数据流:测试先启动一个临时 exec-server;再用它的 WebSocket 地址创建一个测试用 Environment,并确认这个 Environment 是远程模式;随后向远程 Environment 要环境信息,同时也从默认本地 Environment 取一份信息;最后比较两份信息必须相同。测试完成后关闭服务器。

调用关系:它调用 exec_server 准备远程服务,用 Environment::create_for_tests 创建连接到该服务的远程环境,用 Environment::default_for_tests 创建本地对照组,再用 assert! 和 assert_eq! 检查模式和结果。它验证的是 Environment 和 exec-server 之间的配合是否正确。

调用图:调用 3 个内部函数(create_for_tests, default_for_tests, exec_server);外部调用 2 个(assert!, assert_eq!)。

exec-server/tests/websocket.rs源码 ↗
testtest run

exec-server 会通过 WebSocket(一种让客户端和服务器保持长连接、互相发消息的通道)收发 JSON-RPC 消息。JSON-RPC 可以理解成“用 JSON 写的远程调用格式”:客户端发一个带方法名和参数的请求,服务器回一个带结果或错误的响应。这个测试文件像是在给服务器做压力小体检:先故意发一段不是 JSON 的文字,确认服务器会回一个清楚的错误,而且还能继续接受后面的 initialize 初始化请求;再把同样的 JSON-RPC 请求用二进制 WebSocket 帧发过去,确认服务器也认;最后伪装成浏览器网页来源,带上 Origin 请求头,确认握手会被 403 Forbidden 拒绝,避免网页随便连上本地执行服务。这些测试重要,因为执行服务器一旦在输入格式或连接来源上处理不好,要么容易被坏消息弄挂,要么可能留下安全入口。

函数细节3
exec_server_reports_malformed_websocket_json_and_keeps_running21–68 ↗
async fn exec_server_reports_malformed_websocket_json_and_keeps_running() -> anyhow::Result<()>

作用:这个测试确认服务器收到一条完全不是 JSON 的 WebSocket 文本消息时,不会直接崩掉。它还会继续发正常的初始化请求,证明服务器在报错后仍然能正常工作。

数据流:测试先启动一个临时 exec-server,然后往 WebSocket 里发入字符串“not-json”。服务器应当吐出一个 JSON-RPC 错误,测试检查错误 id、错误码和错误消息开头是否符合预期。接着测试再发送 initialize 请求,把客户端名称和空的恢复会话 id 传进去,等待服务器返回初始化结果,并把返回的 session_id 当作 UUID(一种标准格式的唯一编号)解析,确认它真的是合法会话编号。最后关闭服务器。

调用关系:它在测试流程里扮演“坏输入后还能不能继续服务”的检查员。它先通过 common 测试工具 exec_server 启动服务器,再用服务器测试句柄发送原始文本、等待事件、发送正式请求;过程中借助 JSON 解析和断言工具确认服务器给出的错误与后续响应都正确。

调用图:调用 1 个内部函数(exec_server);外部调用 6 个(parse_str, assert!, assert_eq!, panic!, from_value, to_value)。

exec_server_accepts_binary_websocket_json71–104 ↗
async fn exec_server_accepts_binary_websocket_json() -> anyhow::Result<()>

作用:这个测试确认服务器不只接受普通文本 WebSocket 消息,也能接受二进制 WebSocket 消息里的 JSON-RPC 请求。这样客户端用不同帧类型发送同一份 JSON 时,服务器不会误拒绝。

数据流:测试先启动临时 exec-server,手动组装一个 initialize JSON-RPC 请求,里面包含请求 id、方法名 initialize 和初始化参数。然后它把这条请求序列化成字节数组,也就是二进制数据,通过 WebSocket 发给服务器。服务器返回响应后,测试检查响应 id 和原请求 id 一样,再把 result 解析成 InitializeResponse,并验证 session_id 是合法 UUID。最后关闭服务器。

调用关系:它位于 WebSocket 输入兼容性的测试环节。它同样依赖 exec_server 测试工具拉起服务器,但这次不走普通请求封装,而是自己构造 JSONRPCMessage::Request,再交给原始二进制发送接口,最后等待对应 id 的响应来证明这条通路是通的。

调用图:调用 1 个内部函数(exec_server);外部调用 8 个(parse_str, Request, Integer, assert_eq!, panic!, from_value, to_value, to_vec)。

exec_server_rejects_browser_origin_websocket_handshake107–125 ↗
async fn exec_server_rejects_browser_origin_websocket_handshake() -> anyhow::Result<()>

作用:这个测试确认带有浏览器网页来源 Origin 的 WebSocket 握手会被拒绝。它是在防止恶意网页偷偷连到本机 exec-server。

数据流:测试先启动临时 exec-server,拿到它的 WebSocket 地址,并构造一个连接请求。然后它往请求头里塞入 Origin: https://evil.example,模拟一个网页从外部站点发起连接。测试尝试连接服务器:如果连接成功就立刻报错;如果失败,则要求失败原因是 HTTP 响应,并检查状态码是 403 Forbidden。最后关闭服务器。

调用关系:它是安全边界测试的一部分。它通过 exec_server 启动真实测试服务器,再直接调用 connect_async 发起 WebSocket 握手,而不是使用已封装好的服务器客户端;这样可以精确控制 Origin 请求头,验证服务器在握手阶段就把这类连接挡住。

调用图:调用 1 个内部函数(exec_server);外部调用 4 个(from_static, bail!, assert_eq!, connect_async)。

exec-server/src/server/handler/tests.rs源码 ↗
testtest run

这个文件不是真正给用户运行的服务器代码,而是专门“验货”的测试代码。它先准备一个已经初始化好的 ExecServerHandler,也就是测试里的服务器处理器,然后让它启动一些很短的小命令,比如睡一会儿、打印几行字。测试重点不是命令本身,而是服务器在复杂情况里的表现:两个请求同时用同一个进程编号时,只能有一个成功;进程已经退出后再终止,应该如实说“已经不在运行”;旧连接正在等输出时,如果同一个会话被新连接接管,旧的等待要失败;正在使用的会话不能被别人抢走;就算通知接收端关掉了,进程输出和退出码也不能丢。文件里还有一些小工具函数,像搭建测试参数、按 Windows 或 Unix 选择命令写法、反复读取进程输出等。它像一套安全检查清单,保证服务器遇到断线、重连、并发和通知失败时仍然行为清楚、可预期。

函数细节14
exec_params22–24 ↗
fn exec_params(process_id: &str) -> ExecParams

作用:给测试快速做一份“启动进程”的参数,默认启动一个很短时间就结束的睡眠命令。测试不关心具体命令时,就用它省事。

数据流:进去一个进程编号字符串 → 它先向 sleep_argv 要到适合当前系统的睡眠命令,再交给 exec_params_with_argv 组装完整启动参数 → 出来一份 ExecParams,里面带着进程编号、命令、当前目录和环境变量。

调用关系:它是更省心的包装函数。terminate_reports_false_after_process_exit 和 output_and_exit_are_retained_after_notification_receiver_closes 会用它启动默认短命令;它自己把具体组装工作交给 exec_params_with_argv 和 sleep_argv。

调用图:调用 2 个内部函数(exec_params_with_argv, sleep_argv);被 2 处调用(output_and_exit_are_retained_after_notification_receiver_closes, terminate_reports_false_after_process_exit)。

exec_params_with_argv26–37 ↗
fn exec_params_with_argv(process_id: &str, argv: Vec<String>) -> ExecParams

作用:把“进程编号”和“要执行的命令参数”包装成服务器能看懂的启动请求。需要测试不同命令时,会直接用它。

数据流:进去一个进程编号和一组命令参数 → 它把编号转成 ProcessId,把当前工作目录转成路径 URI(一种能在协议里传输的路径写法),再只继承 PATH 环境变量 → 出来一份完整 ExecParams,并明确关闭伪终端和标准输入管道。

调用关系:exec_params 会调用它生成默认参数;long_poll_read_fails_after_session_resume 和 output_and_exit_are_retained_after_notification_receiver_closes 会直接调用它,传入自己定制的命令脚本。它还依赖 inherited_path_env 准备环境变量。

调用图:调用 3 个内部函数(from, inherited_path_env, from_path);被 3 处调用(exec_params, long_poll_read_fails_after_session_resume, output_and_exit_are_retained_after_notification_receiver_closes);外部调用 1 个(current_dir)。

inherited_path_env39–45 ↗
fn inherited_path_env() -> HashMap<String, String>

作用:只从当前测试进程里继承 PATH 环境变量。PATH 是系统用来找可执行程序的搜索路径,保留它可以让 shell、cmd 等命令更容易被找到。

数据流:它不接收参数 → 它新建一个环境变量表,查看系统里有没有 PATH,如果有就放进去 → 出来一个 HashMap,也就是键值表,最多包含 PATH 这一项。

调用关系:它只服务于 exec_params_with_argv。这样测试启动子进程时环境尽量干净,但仍保留找到系统命令所需的最基本信息。

调用图:被 1 处调用(exec_params_with_argv);外部调用 2 个(new, var_os)。

sleep_argv47–49 ↗
fn sleep_argv() -> Vec<String>

作用:生成一个“短暂等待后退出”的命令参数列表,用来做不需要复杂输出的测试进程。

数据流:它不接收参数 → 它准备 Unix 和 Windows 两套等价脚本,再交给 shell_argv 按当前系统选择 → 出来一组可以传给服务器执行的命令参数。

调用关系:exec_params 会调用它。它不直接关心操作系统差异,而是把这件事交给 shell_argv。

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

shell_argv51–65 ↗
fn shell_argv(unix_script: &str, windows_script: &str) -> Vec<String>

作用:把一段 Unix 脚本或 Windows 脚本变成当前系统能执行的命令参数。这样同一套测试可以在不同操作系统上跑。

数据流:进去两段脚本:一段给 Unix/Linux/macOS 的 /bin/sh,一段给 Windows 的命令处理器 → 它判断当前是不是 Windows → 出来一组参数:Windows 下类似 cmd /C xxx,非 Windows 下类似 /bin/sh -c xxx。

调用关系:sleep_argv 用它生成默认睡眠命令;long_poll_read_fails_after_session_resume 和 output_and_exit_are_retained_after_notification_receiver_closes 用它生成更特殊的测试脚本。它是这些跨平台测试命令的转换器。

调用图:被 3 处调用(long_poll_read_fails_after_session_resume, output_and_exit_are_retained_after_notification_receiver_closes, sleep_argv);外部调用 2 个(cfg!, vec!)。

windows_command_processor67–69 ↗
fn windows_command_processor() -> String

作用:找出 Windows 上应该用哪个命令解释器来跑脚本。通俗说,就是找“打开命令行并执行这段话”的程序。

数据流:它不接收参数 → 它读取 COMSPEC 环境变量;如果读不到,就用 cmd.exe 当默认值 → 出来一个命令处理器名称或路径。

调用关系:它是 Windows 命令构造时的小帮手。在这个文件的意图里,它配合 shell_argv 让 Windows 测试也能执行脚本。

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

test_runtime_paths71–77 ↗
fn test_runtime_paths() -> ExecServerRuntimePaths

作用:为测试准备服务器运行时需要知道的程序路径。没有这些路径,ExecServerHandler 可能不知道当前可执行文件在哪里。

数据流:它不接收参数 → 它读取当前正在运行的测试可执行文件路径,并把 Linux 沙箱程序路径设为空 → 出来一份 ExecServerRuntimePaths,供测试里的服务器处理器使用。

调用关系:initialized_handler、long_poll_read_fails_after_session_resume、active_session_resume_is_rejected 和 output_and_exit_are_retained_after_notification_receiver_closes 都会用它创建测试用 handler。它把路径准备这件重复活集中在一个地方。

调用图:调用 1 个内部函数(new);被 4 处调用(active_session_resume_is_rejected, initialized_handler, long_poll_read_fails_after_session_resume, output_and_exit_are_retained_after_notification_receiver_closes);外部调用 1 个(current_exe)。

initialized_handler79–97 ↗
async fn initialized_handler() -> Arc<ExecServerHandler>

作用:快速创建一个已经完成初始化的测试服务器处理器。很多测试只想验证后续行为,不想每次都手写初始化步骤。

数据流:它不接收参数 → 它创建通知通道、会话登记表和 ExecServerHandler,再调用 initialize 建立会话,检查返回的会话编号确实是 UUID(一种标准随机唯一编号),最后调用 initialized 标记初始化完成 → 出来一个可共享的 Arc<ExecServerHandler>。Arc 可以理解成多人共用同一个对象的安全引用。

调用关系:duplicate_process_ids_allow_only_one_successful_start 和 terminate_reports_false_after_process_exit 用它拿到现成 handler。它内部会调用 test_runtime_paths,并创建 SessionRegistry 和 RpcNotificationSender。

调用图:调用 4 个内部函数(new, new, test_runtime_paths, new);被 2 处调用(duplicate_process_ids_allow_only_one_successful_start, terminate_reports_false_after_process_exit);外部调用 3 个(new, parse_str, channel)。

duplicate_process_ids_allow_only_one_successful_start100–125 ↗
async fn duplicate_process_ids_allow_only_one_successful_start()

作用:测试两个请求同时用同一个进程编号启动进程时,服务器只允许一个成功。这样可以避免同一个编号背后对应两个真实进程,后面读取和终止都会乱套。

数据流:进去的是测试框架自动启动的异步测试环境 → 它先拿到初始化好的 handler,再复制两个共享引用,同时发起两个 exec 请求,进程编号都叫 proc-1 → 出来是断言结果:一个成功、一个失败,失败错误码是 -32600,错误信息说明进程已存在,最后等待一下并关闭 handler。

调用关系:这是一个顶层测试,由测试框架调用。它使用 initialized_handler 搭环境,用 exec_params 准备请求,并通过 tokio::join 同时发起两次启动,专门检查并发下的去重保护。

调用图:调用 1 个内部函数(initialized_handler);外部调用 5 个(clone, from_millis, assert_eq!, join!, sleep)。

terminate_reports_false_after_process_exit128–154 ↗
async fn terminate_reports_false_after_process_exit()

作用:测试进程自然退出后,再请求终止它时,服务器会回答 running: false。也就是说,它不会假装已经退出的进程还在跑。

数据流:测试开始 → 它创建 handler,启动一个短命进程,然后在最多 1 秒内反复调用 terminate;如果返回 running: false 就通过,否则每隔 25 毫秒再试 → 最后关闭 handler。

调用关系:这是测试框架直接运行的异步测试。它借 initialized_handler 和 exec_params 做准备,然后重点调用 handler.terminate,验证终止接口在“进程已经结束”这个边界情况上的说法是否准确。

调用图:调用 3 个内部函数(from, exec_params, initialized_handler);外部调用 5 个(from_millis, from_secs, assert!, now, sleep)。

long_poll_read_fails_after_session_resume157–227 ↗
async fn long_poll_read_fails_after_session_resume()

作用:测试旧连接正在长时间等待读取输出时,如果会话被新连接恢复接管,旧连接的等待请求必须失败。这样可以避免一个会话同时有两个连接都以为自己还有效。

数据流:测试开始 → 它创建第一个 handler 并初始化会话,启动一个安静但存活较久的进程,然后开一个后台任务执行 exec_read,并设置 wait_ms 让它长轮询等待;接着关闭第一个 handler,再用同一个会话编号创建第二个 handler 恢复会话 → 后台读取任务返回错误,错误信息说明会话已被另一个连接恢复,最后关闭第二个 handler。

调用关系:这是一个完整的断线重连场景测试。它直接创建 SessionRegistry,让两个 handler 共享同一个会话登记表;它用 exec_params_with_argv 和 shell_argv 准备一个不会马上输出的命令,让读取请求只能因为会话接管而结束,而不是因为进程自己输出或退出。

调用图:调用 7 个内部函数(from, new, new, exec_params_with_argv, shell_argv, test_runtime_paths, new);外部调用 7 个(clone, new, from_millis, assert_eq!, channel, spawn, sleep)。

active_session_resume_is_rejected230–270 ↗
async fn active_session_resume_is_rejected()

作用:测试一个仍然活跃的会话不能被第二个连接恢复。这样可以防止两个客户端同时控制同一个会话,造成命令和输出归属混乱。

数据流:测试开始 → 它创建第一个 handler 并初始化出一个会话编号,但不调用 initialized 完成接管后的释放;然后创建第二个 handler,尝试用同一个会话编号恢复 → 出来的是一个错误,错误码为 -32600,消息说明该会话已连接到另一个连接,最后关闭第一个 handler。

调用关系:这是会话登记表规则的测试。它让两个 handler 共用同一个 SessionRegistry,并通过第二次 initialize 触发冲突,确认服务器会拒绝恢复仍被占用的会话。

调用图:调用 4 个内部函数(new, new, test_runtime_paths, new);外部调用 4 个(clone, new, assert_eq!, channel)。

output_and_exit_are_retained_after_notification_receiver_closes273–314 ↗
async fn output_and_exit_are_retained_after_notification_receiver_closes()

作用:测试通知接收端关闭后,服务器仍然保存进程输出和退出码。通知失败不应该导致用户之后读不到已经发生的结果。

数据流:测试开始 → 它创建 handler 和通知通道,启动一个会打印 first、second 并正常退出的进程;随后故意丢掉通知接收端,模拟通知发不出去 → 它通过 read_process_until_closed 主动读取,确认输出完整、退出码是 0;再等一会儿,用同一个进程编号启动新进程,确认旧记录清理后编号可以复用,最后关闭 handler。

调用关系:这是通知失败场景的测试。它用 exec_params_with_argv 和 shell_argv 制造可验证输出,用 read_process_until_closed 读取直到关闭,再用 exec_params 检查进程编号复用。

调用图:调用 9 个内部函数(from, new, new, exec_params, exec_params_with_argv, read_process_until_closed, shell_argv, test_runtime_paths, new);外部调用 5 个(new, from_millis, assert_eq!, channel, sleep)。

read_process_until_closed316–352 ↗
async fn read_process_until_closed(
    handler: &ExecServerHandler,
    process_id: ProcessId,
) -> (String, Option<i32>)

作用:反复读取某个进程的输出,直到服务器告诉它这个进程的输出流已经关闭。它是测试里的小读者,负责把分批到来的输出拼成完整文本。

数据流:进去一个 handler 引用和进程编号 → 它设置 5 秒截止时间,从 after_seq 为空开始调用 exec_read;每次拿到若干输出块,就按顺序追加到字符串,并记录最新序号;如果响应说进程已退出,就保存退出码;如果响应说已关闭,就返回完整输出和退出码 → 如果太久没关闭,就触发测试失败。

调用关系:output_and_exit_are_retained_after_notification_receiver_closes 调用它来验证输出没有丢。它直接把活交给 handler.exec_read,并用返回的序号继续追读下一批内容。

调用图:调用 1 个内部函数(exec_read);被 1 处调用(output_and_exit_are_retained_after_notification_receiver_closes);外部调用 6 个(from_secs, from_utf8_lossy, new, assert!, clone, now)。

exec-server/tests/process.rs源码 ↗
testtest run

这个测试文件只在 Unix 系统上运行,因为它会真的启动像 /bin/shtrue 这样的系统命令。它模拟一个客户端通过 WebSocket(浏览器和服务器之间那种可持续收发消息的连接)和 exec-server 通话,先做初始化,再发送 JSON-RPC 请求。JSON-RPC 可以理解成“用 JSON 写的远程点菜单”:客户端说要调用哪个方法,服务器回对应结果。这里重点检查三件事:第一,服务器收到 process/start 后确实能启动进程,并返回同一个进程编号;第二,如果启动进程时没写 pipeStdin,服务器会默认把标准输入关掉,之后写入会得到“stdin 已关闭”的结果,而不是卡住;第三,WebSocket 断开后再用同一个会话恢复,已经启动的后台进程不会被误杀,还能被读取和终止。这些测试保证了进程执行服务在真实客户端连接不稳定时仍然可靠。

函数细节3
exec_server_starts_process_over_websocket19–79 ↗
async fn exec_server_starts_process_over_websocket() -> anyhow::Result<()>

作用:这个测试确认:客户端通过 WebSocket 发起 process/start 请求后,exec-server 能真的启动一个进程,并把正确的进程编号回给客户端。它用最简单的 true 命令当样例,因为这个命令几乎不会出错,适合验证“启动流程本身”。

数据流:测试先启动一个测试用服务器,再发送 initialize 初始化请求,并等待服务器回话。接着发送 initialized 通知,表示客户端准备好了。然后它发送 process/start,内容里带着进程编号 proc-1、要运行的命令 true、当前目录、环境变量等。服务器返回一条 JSON-RPC 响应后,测试把响应里的 JSON 转成 ExecResponse,再检查返回的 process_id 是否正好是 proc-1。最后关闭测试服务器。

调用关系:它在测试流程中扮演最基础的冒烟测试:先通过 exec_server 搭好一台真实可通信的测试服务器,再用 to_valuejson! 拼请求,用 from_value 解读返回结果,用 assert_eq! 判断服务器有没有按约定回答。如果返回消息类型不对,就用 panic! 直接让测试失败。

调用图:调用 1 个内部函数(exec_server);外部调用 5 个(assert_eq!, panic!, from_value, json!, to_value)。

exec_server_defaults_omitted_pipe_stdin_to_closed_stdin82–172 ↗
async fn exec_server_defaults_omitted_pipe_stdin_to_closed_stdin() -> anyhow::Result<()>

作用:这个测试确认:如果启动进程时没有写 pipeStdin 这个参数,服务器会把标准输入默认当作关闭,而不是默认保持可写。这样可以避免客户端误以为还能往进程里塞输入,导致行为不清楚。

数据流:测试先启动服务器并完成初始化。然后它启动一个 shell 脚本进程,这个脚本会等一下,再尝试从标准输入读一行;如果读不到,就输出 eof。启动请求里故意省略 pipeStdin。服务器确认进程启动后,测试又发送 process/write,试图往这个进程写入一段 base64 编码的内容。服务器返回写入结果后,测试把 JSON 转成 WriteResponse,并检查状态必须是 StdinClosed,意思是标准输入已经关闭,写不进去。最后关闭服务器。

调用关系:它接在初始化和进程启动这条主流程之后,专门验证一个容易被忽略的默认值。它通过 exec_server 建立测试环境,用 json! 构造少了 pipeStdin 的请求,用 from_value 读取服务器回答,再用 assert_eq! 确认默认行为符合约定。如果服务器没有返回预期的 JSON-RPC 响应,就用 panic! 标记测试失败。

调用图:调用 1 个内部函数(exec_server);外部调用 5 个(assert_eq!, panic!, from_value, json!, to_value)。

exec_server_resumes_detached_session_without_killing_processes175–307 ↗
async fn exec_server_resumes_detached_session_without_killing_processes() -> anyhow::Result<()>

作用:这个测试确认:客户端 WebSocket 断开后,如果用原来的会话编号重新连接,之前启动的进程不会被服务器误杀。它验证的是“断线重连后进程还活着”这个很关键的稳定性场景。

数据流:测试先启动服务器并初始化,保存服务器返回的 session_id,也就是这次会话的身份号码。随后它启动一个会睡眠 5 秒的 shell 进程 proc-resume,让这个进程保持运行。接着测试主动断开 WebSocket,再重新连接,并用之前保存的 session_id 再次发送 initialize,表示要恢复旧会话。服务器返回的初始化结果必须和第一次一样。恢复完成后,测试读取 proc-resume 的状态,确认没有失败、没有退出、也没有关闭。最后它发送 process/terminate 终止这个进程,并确认服务器认为该进程当时仍在运行,然后关闭测试服务器。

调用关系:它是这个文件里最完整的一条端到端测试:从 exec_server 建立服务器开始,经历初始化、启动进程、断线、重连、恢复会话、读取进程状态、终止进程。它用 to_valuejson! 准备请求,用 from_value 解析 InitializeResponseReadResponseTerminateResponse,再用 assert_eq!assert! 检查关键事实。如果某一步收到的不是预期响应,就用 panic! 让问题暴露出来。

调用图:调用 1 个内部函数(exec_server);外部调用 6 个(assert!, assert_eq!, panic!, from_value, json!, to_value)。

exec-server/tests/exec_process.rs源码 ↗
test测试运行时

exec-server 的核心工作,是替用户启动一个外部程序,并把它的输出、退出码、输入和错误状态安全地传回来。这个测试文件就像一套验收清单:先搭一个本地或远程的执行环境,然后启动真实命令,比如 shell、true、sleep;再检查能不能读到 stdout 和 stderr,能不能往 stdin 写字,进程退出后事件顺序对不对,断线时会不会把错误告诉等待中的读写方。这里还特别测了一些容易出错的边角情况:进程已经退出但子进程还在晚点输出、订阅事件比较晚但不该丢消息、Windows 上不支持的信号要明确报错。整体上,它保证“执行进程”这个功能像一台可靠的收发机:启动得起来,消息不漏,结束说清楚,坏了也要报明白。

函数细节28
create_process_context52–67 ↗
async fn create_process_context(use_remote: bool) -> Result<ProcessContext>

作用:创建测试用的执行环境。它会按参数决定是直接在本机跑命令,还是先启动一个真正的远程 exec-server 再通过网络连过去。

数据流:进去的是 use_remote 这个开关 → 如果为真,就启动测试服务器并把服务器地址交给测试环境;如果为假,就创建不带远程地址的本地环境 → 出来的是 ProcessContext,里面有统一的执行后端,远程模式下还保存服务器句柄,方便后面关掉它。

调用关系:几乎所有测试辅助函数一开始都会调用它,因为后续测试不关心命令到底是在本地跑还是通过服务器跑。它把环境准备好后,具体启动进程的活交给 backend.start。

调用图:调用 2 个内部函数(create_for_tests, exec_server);被 12 处调用(assert_exec_process_preserves_queued_events_before_subscribe, assert_exec_process_pushes_events, assert_exec_process_rejects_write_without_pipe_stdin, assert_exec_process_replays_events_after_close, assert_exec_process_retains_output_after_exit_until_streams_close, assert_exec_process_signal_interrupts_process, assert_exec_process_signal_reports_unsupported_on_windows, assert_exec_process_starts_and_exits, assert_exec_process_streams_output, assert_exec_process_write_then_read (+2 more))。

assert_exec_process_starts_and_exits69–92 ↗
async fn assert_exec_process_starts_and_exits(use_remote: bool) -> Result<()>

作用:确认最基本的事情:一个进程能启动,并且能正常退出。这里用 true 命令,因为它什么都不做,只会成功退出。

数据流:进去的是是否远程执行的开关 → 它创建环境,启动 process_id 为 proc-1 的 true 命令,再持续读取进程状态 → 出来没有业务结果,但会断言退出码是 0,并且会话已经关闭;不符合就让测试失败。

调用关系:这是 exec_process_starts_and_exits 这个测试入口真正干活的部分。它依赖 create_process_context 准备环境,并把读到结束的工作交给 collect_process_output_from_reads。

调用图:调用 4 个内部函数(from, collect_process_output_from_reads, create_process_context, from_path);被 1 处调用(exec_process_starts_and_exits);外部调用 5 个(default, assert!, assert_eq!, current_dir, vec!)。

read_process_until_change94–111 ↗
async fn read_process_until_change(
    session: Arc<dyn ExecProcess>,
    wake_rx: &mut watch::Receiver<u64>,
    after_seq: Option<u64>,
) -> Result<ReadResponse>

作用:读取一次进程状态;如果暂时没新内容,就等到进程通知“有变化了”再读。它避免测试因为太早读取而误以为什么都没有。

数据流:进去的是一个进程会话、一个唤醒通知接收器,以及从哪个序号之后开始读 → 它先立即读一次;如果没有输出、没关闭、没失败,就最多等 2 秒等变化通知,再读一次 → 出来是一份 ReadResponse,里面可能有新输出、退出信息、关闭状态或失败信息。

调用关系:它是读取类测试的底层小工具。collect_process_output_from_reads 用它循环等输出;信号测试用它等到进程打印 ready;断线测试也用它确认断线错误能被读出来。

调用图:被 3 处调用(assert_exec_process_signal_interrupts_process, collect_process_output_from_reads, remote_exec_process_reports_transport_disconnect);外部调用 3 个(from_secs, changed, timeout)。

collect_process_output_from_reads113–140 ↗
async fn collect_process_output_from_reads(
    session: Arc<dyn ExecProcess>,
    mut wake_rx: watch::Receiver<u64>,
) -> Result<(String, Option<i32>, bool)>

作用:用“主动读取”的方式,把一个进程从开始到关闭期间的输出和退出码收集完整。可以把它理解成一直翻收件箱,直到看到“这封邮件结束了”。

数据流:进去的是进程会话和唤醒通知 → 它反复调用 read_process_until_change,把每个输出块转成文字拼起来,记录退出码,遇到失败就报错,直到 closed 为真 → 出来的是三样东西:完整输出文字、可选退出码、以及表示已关闭的布尔值。

调用关系:很多断言函数都用它做最后验收,比如启动退出、输出流、stdin 写入、信号中断、排队事件等。它自己不启动进程,只负责把已经启动的进程读到结束。

调用图:调用 1 个内部函数(read_process_until_change);被 9 处调用(assert_exec_process_preserves_queued_events_before_subscribe, assert_exec_process_rejects_write_without_pipe_stdin, assert_exec_process_replays_events_after_close, assert_exec_process_retains_output_after_exit_until_streams_close, assert_exec_process_signal_interrupts_process, assert_exec_process_starts_and_exits, assert_exec_process_streams_output, assert_exec_process_write_then_read, assert_exec_process_write_then_read_without_tty);外部调用 4 个(clone, from_utf8_lossy, new, bail!)。

collect_process_output_from_events142–174 ↗
async fn collect_process_output_from_events(
    session: Arc<dyn ExecProcess>,
) -> Result<(String, String, Option<i32>, bool)>

作用:用“事件订阅”的方式收集进程输出。事件订阅就像订阅快递通知:有 stdout、stderr、退出、关闭等消息会主动推过来。

数据流:进去的是进程会话 → 它订阅事件通道,最多每次等 2 秒,按事件类型把 stdout 和 stderr 分开累加,记录退出码,遇到关闭就返回,遇到失败就报错 → 出来的是 stdout 文本、stderr 文本、退出码和关闭标记。

调用关系:它主要服务于“进程关闭后还能重放事件”的测试。该测试先用读取接口读完,再用它确认事件接口也能拿到同样的历史输出。

调用图:被 1 处调用(assert_exec_process_replays_events_after_close);外部调用 5 个(from_secs, from_utf8_lossy, new, bail!, timeout)。

collect_process_event_snapshots176–203 ↗
async fn collect_process_event_snapshots(
    session: Arc<dyn ExecProcess>,
) -> Result<Vec<ProcessEventSnapshot>>

作用:把进程推送出来的事件按顺序拍成快照,方便精确比较事件顺序和序号。它关心的不只是内容,还有先后顺序。

数据流:进去的是进程会话 → 它订阅事件,把输出事件、退出事件、关闭事件转换成 ProcessEventSnapshot;遇到失败就报错;遇到关闭就停止 → 出来是一串事件快照列表。

调用关系:它被 assert_exec_process_pushes_events 使用,用来确认 stdout、stderr、退出、关闭这几类事件按预期顺序出现,并且序号连续。

调用图:被 1 处调用(assert_exec_process_pushes_events);外部调用 6 个(from_secs, from_utf8_lossy, new, bail!, matches!, timeout)。

assert_exec_process_streams_output205–234 ↗
async fn assert_exec_process_streams_output(use_remote: bool) -> Result<()>

作用:确认进程运行时产生的标准输出能被读取出来。这里让 shell 稍微等一下再打印一句话,模拟真实程序边跑边输出。

数据流:进去的是本地或远程开关 → 它创建环境,启动一个会打印 session output 的 shell 命令,然后通过读取接口收集输出 → 出来通过断言确认输出文字完全匹配、退出码为 0、进程已关闭。

调用关系:它是 exec_process_streams_output 测试入口背后的实际检查逻辑。它依赖 create_process_context 启动环境,依赖 collect_process_output_from_reads 收尾。

调用图:调用 3 个内部函数(collect_process_output_from_reads, create_process_context, from_path);被 1 处调用(exec_process_streams_output);外部调用 5 个(default, assert!, assert_eq!, current_dir, vec!)。

assert_exec_process_pushes_events236–281 ↗
async fn assert_exec_process_pushes_events(use_remote: bool) -> Result<()>

作用:确认进程的输出和退出信息会以事件形式推送出来,而且 stdout、stderr、退出、关闭的顺序正确。

数据流:进去的是本地或远程开关 → 它启动一个先打印 stdout、再打印 stderr、最后以退出码 7 结束的 shell 命令 → 出来是一组事件快照,并断言它们正好是预期的四个事件。

调用关系:它由 exec_process_pushes_events 调用。它把环境准备交给 create_process_context,把事件收集交给 collect_process_event_snapshots。

调用图:调用 3 个内部函数(collect_process_event_snapshots, create_process_context, from_path);被 1 处调用(exec_process_pushes_events);外部调用 4 个(default, assert_eq!, current_dir, vec!)。

assert_exec_process_replays_events_after_close283–324 ↗
async fn assert_exec_process_replays_events_after_close(use_remote: bool) -> Result<()>

作用:确认进程结束后,后来才订阅事件的人也能拿到已经发生过的输出。也就是说,消息不会因为订阅晚了就丢掉。

数据流:进去的是本地或远程开关 → 它启动一个快速打印两行后退出的进程,先用读取接口读到结束,再改用事件接口收集历史事件 → 出来通过断言确认两种方式看到的输出和退出码一致。

调用关系:它由 exec_process_replays_events_after_close 这个测试入口调用。它同时使用 collect_process_output_from_reads 和 collect_process_output_from_events,专门比较“拉取读取”和“事件订阅”两条路径。

调用图:调用 4 个内部函数(collect_process_output_from_events, collect_process_output_from_reads, create_process_context, from_path);被 1 处调用(exec_process_replays_events_after_close);外部调用 5 个(clone, default, assert_eq!, current_dir, vec!)。

assert_exec_process_retains_output_after_exit_until_streams_close326–399 ↗
async fn assert_exec_process_retains_output_after_exit_until_streams_close(
    use_remote: bool,
) -> Result<()>

作用:测试一个很容易漏的情况:父进程已经退出了,但它留下的子进程稍后还会往输出流写字。系统必须等输出流真的关了,不能只看父进程退出就把会话关掉。

数据流:进去的是本地或远程开关 → 它启动一个测试辅助程序,让父进程先退出,子进程等文件信号再输出;先读到退出码但未关闭,再写入释放文件让晚到输出出现 → 出来断言晚到文字被完整读到,最后退出码仍是 0,进程会话关闭。

调用关系:它由 exec_process_retains_output_after_exit_until_streams_close 调用。它需要 current_test_binary_helper_paths 找到测试辅助二进制,并用 collect_process_output_from_reads 做最终完整读取。

调用图:调用 4 个内部函数(current_test_binary_helper_paths, collect_process_output_from_reads, create_process_context, from_path);被 1 处调用(exec_process_retains_output_after_exit_until_streams_close);外部调用 11 个(default, from_secs, from_utf8_lossy, new, new, assert!, assert_eq!, current_dir, write, timeout (+1 more))。

assert_exec_process_write_then_read401–439 ↗
async fn assert_exec_process_write_then_read(use_remote: bool) -> Result<()>

作用:确认带伪终端的进程可以接收输入,再把处理结果输出回来。伪终端可以理解成给程序装了一个像真人键盘和屏幕一样的交互环境。

数据流:进去的是本地或远程开关 → 它启动一个 shell,等待从 stdin 读一行;测试稍等后写入 hello 换行,再读取所有输出 → 出来断言输出里包含 from-stdin:hello,退出码为 0,并且已关闭。

调用关系:它由 exec_process_write_then_read 测试入口调用。它先直接调用进程的 write 写输入,再用 collect_process_output_from_reads 验证结果。

调用图:调用 3 个内部函数(collect_process_output_from_reads, create_process_context, from_path);被 1 处调用(exec_process_write_then_read);外部调用 7 个(default, from_millis, assert!, assert_eq!, current_dir, sleep, vec!)。

assert_exec_process_write_then_read_without_tty441–472 ↗
async fn assert_exec_process_write_then_read_without_tty(use_remote: bool) -> Result<()>

作用:确认不使用伪终端时,只要打开了 pipe_stdin,也能通过普通管道给进程写输入。管道就是一根程序之间传字节的“水管”。

数据流:进去的是本地或远程开关 → 它启动一个从 stdin 读一行的 shell,并设置 pipe_stdin 为真;随后写入 hello 换行 → 出来断言写入被接受,读到 from-stdin:hello,退出码为 0,进程关闭。

调用关系:它由 exec_process_write_then_read_without_tty 调用。它验证的是 write 接口在非 TTY 模式下的另一条输入路径,最后仍交给 collect_process_output_from_reads 收集结果。

调用图:调用 3 个内部函数(collect_process_output_from_reads, create_process_context, from_path);被 1 处调用(exec_process_write_then_read_without_tty);外部调用 6 个(default, from_millis, assert_eq!, current_dir, sleep, vec!)。

assert_exec_process_rejects_write_without_pipe_stdin474–506 ↗
async fn assert_exec_process_rejects_write_without_pipe_stdin(use_remote: bool) -> Result<()>

作用:确认如果进程没有打开 stdin 管道,往里面写东西会被明确拒绝,而不是悄悄吞掉或卡住。

数据流:进去的是本地或远程开关 → 它启动一个尝试读取 stdin 的 shell,但设置 tty 为假、pipe_stdin 也为假;测试调用 write → 出来断言写入状态是 StdinClosed,并且进程自己读到的是 eof,正常退出。

调用关系:它由 exec_process_rejects_write_without_pipe_stdin 调用。它先检查 write 的返回状态,再用 collect_process_output_from_reads 确认进程看到的确实是输入结束。

调用图:调用 3 个内部函数(collect_process_output_from_reads, create_process_context, from_path);被 1 处调用(exec_process_rejects_write_without_pipe_stdin);外部调用 5 个(default, assert!, assert_eq!, current_dir, vec!)。

assert_exec_process_signal_interrupts_process508–560 ↗
async fn assert_exec_process_signal_interrupts_process(use_remote: bool) -> Result<()>

作用:确认可以给正在运行的进程发送中断信号。中断信号可以理解成按下 Ctrl+C,请程序停下来或执行自己的收尾逻辑。

数据流:进去的是本地或远程开关 → 它启动一个无限循环的 shell,并让 shell 收到 INT 时打印 signal:2 后以 7 退出;测试先等到 ready,再发送 Interrupt → 出来断言输出包含 signal:2,退出码是 7,进程关闭。

调用关系:它由 exec_process_signal_interrupts_process 调用。它用 read_process_until_change 等待进程准备好,发送 signal 后用 collect_process_output_from_reads 收集剩余输出和退出状态。

调用图:调用 4 个内部函数(collect_process_output_from_reads, create_process_context, read_process_until_change, from_path);被 1 处调用(exec_process_signal_interrupts_process);外部调用 9 个(clone, default, from_utf8_lossy, new, bail!, assert!, assert_eq!, current_dir, vec!)。

assert_exec_process_signal_reports_unsupported_on_windows562–598 ↗
async fn assert_exec_process_signal_reports_unsupported_on_windows(use_remote: bool) -> Result<()>

作用:确认 Windows 上某些不支持的中断方式会清楚报错。测试要的不是成功中断,而是错误信息必须明确。

数据流:进去的是本地或远程开关 → 它启动一个 Windows cmd 命令,然后尝试发送 Interrupt → 出来如果意外成功就让测试失败;如果失败,则检查错误文字包含“发送失败”和“不支持”的说明,最后主动终止进程。

调用关系:它由 exec_process_signal_reports_unsupported_on_windows 调用,只在 Windows 测试条件下运行。它复用 create_process_context 建环境,但主要验证 signal 接口的错误路径。

调用图:调用 3 个内部函数(from, create_process_context, from_path);被 1 处调用(exec_process_signal_reports_unsupported_on_windows);外部调用 5 个(default, bail!, assert!, current_dir, vec!)。

assert_exec_process_preserves_queued_events_before_subscribe600–631 ↗
async fn assert_exec_process_preserves_queued_events_before_subscribe(
    use_remote: bool,
) -> Result<()>

作用:确认进程很快输出并结束时,测试稍晚才开始读取也不会丢掉已经排队的输出。

数据流:进去的是本地或远程开关 → 它启动一个马上打印 queued output 的 shell,故意等 200 毫秒再订阅唤醒和读取 → 出来断言仍能读到完整输出、退出码为 0、会话关闭。

调用关系:它由 exec_process_preserves_queued_events_before_subscribe 调用。它专门覆盖“先发生、后订阅”的场景,最终靠 collect_process_output_from_reads 验证缓存没有丢。

调用图:调用 4 个内部函数(from, collect_process_output_from_reads, create_process_context, from_path);被 1 处调用(exec_process_preserves_queued_events_before_subscribe);外部调用 7 个(default, from_millis, assert!, assert_eq!, current_dir, sleep, vec!)。

remote_exec_process_reports_transport_disconnect637–722 ↗
async fn remote_exec_process_reports_transport_disconnect() -> Result<()>

作用:测试远程 exec-server 连接断开时,进程会话会把这个坏消息通知所有相关方。不能让读操作一直傻等,也不能让写操作假装成功。

数据流:进去没有业务参数,由测试框架直接运行 → 它启动远程服务器和一个长时间 sleep 的进程,同时挂起一次长读;随后关掉服务器 → 出来断言事件通道收到 Failed,挂起的 read 返回失败,新的 read 也显示失败且关闭,之后 write 也返回断线错误。

调用关系:这是一个完整的远程断线测试入口,不通过额外包装函数。它调用 create_process_context 建远程环境,用 read_process_until_change 检查断线后的读取状态,并直接操作测试服务器的 shutdown。

调用图:调用 4 个内部函数(from, create_process_context, read_process_until_change, from_path);外部调用 9 个(clone, default, from_secs, bail!, assert!, current_dir, spawn, timeout, vec!)。

exec_process_starts_and_exits730–732 ↗
async fn exec_process_starts_and_exits(use_remote: bool) -> Result<()>

作用:这是测试框架会直接运行的入口,用来覆盖“进程能启动并退出”这个基础场景。

数据流:进去的是 test_case 提供的 use_remote 参数 → 它不自己写细节,只调用 assert_exec_process_starts_and_exits → 出来是测试成功或失败的 Result。

调用关系:它是外层测试壳,负责把本地和远程两种参数送进真正的检查函数 assert_exec_process_starts_and_exits。

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

exec_process_streams_output740–742 ↗
async fn exec_process_streams_output(use_remote: bool) -> Result<()>

作用:这是测试框架入口,用来确认进程运行时输出能被流式读到。

数据流:进去的是 use_remote 参数 → 它调用 assert_exec_process_streams_output 完成实际启动、读取和断言 → 出来是测试结果。

调用关系:它本身只做分发,真实逻辑在 assert_exec_process_streams_output 中。test_case 会让它分别跑本地和远程两遍。

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

exec_process_pushes_events750–752 ↗
async fn exec_process_pushes_events(use_remote: bool) -> Result<()>

作用:这是测试入口,用来确认进程输出、错误输出、退出和关闭都会按事件推送。

数据流:进去的是 use_remote 参数 → 它交给 assert_exec_process_pushes_events → 出来是测试成功或失败。

调用关系:它连接测试框架和具体断言函数,让同一套事件检查覆盖本地后端和远程后端。

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

exec_process_replays_events_after_close760–762 ↗
async fn exec_process_replays_events_after_close(use_remote: bool) -> Result<()>

作用:这是测试入口,用来确认进程结束后再订阅事件,也能看到历史输出。

数据流:进去的是 use_remote 参数 → 它调用 assert_exec_process_replays_events_after_close → 出来是 Result,失败时说明事件重放机制有问题。

调用关系:它只是外层包装,实际比较读取接口和事件接口的工作由 assert_exec_process_replays_events_after_close 完成。

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

exec_process_retains_output_after_exit_until_streams_close770–774 ↗
async fn exec_process_retains_output_after_exit_until_streams_close(
    use_remote: bool,
) -> Result<()>

作用:这是测试入口,用来确认父进程退出后,晚到的输出仍会被保留到输出流真正关闭。

数据流:进去的是 use_remote 参数 → 它调用同名 assert 辅助函数执行复杂场景 → 出来是测试结果。

调用关系:它把测试框架的本地/远程参数传给 assert_exec_process_retains_output_after_exit_until_streams_close,后者负责启动辅助程序和验证晚到输出。

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

exec_process_write_then_read782–784 ↗
async fn exec_process_write_then_read(use_remote: bool) -> Result<()>

作用:这是测试入口,用来验证带伪终端的进程可以写入输入并读回响应。

数据流:进去的是 use_remote 参数 → 它调用 assert_exec_process_write_then_read → 出来是 Result。

调用关系:它是测试壳,真正的写 stdin、读 stdout 和断言都在 assert_exec_process_write_then_read 中。

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

exec_process_write_then_read_without_tty792–794 ↗
async fn exec_process_write_then_read_without_tty(use_remote: bool) -> Result<()>

作用:这是测试入口,用来验证没有伪终端时,通过 stdin 管道也能写入进程。

数据流:进去的是 use_remote 参数 → 它调用 assert_exec_process_write_then_read_without_tty → 出来是测试结果。

调用关系:它把本地和远程两种执行方式送进同一个管道输入测试,具体逻辑由 assert_exec_process_write_then_read_without_tty 执行。

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

exec_process_rejects_write_without_pipe_stdin802–804 ↗
async fn exec_process_rejects_write_without_pipe_stdin(use_remote: bool) -> Result<()>

作用:这是测试入口,用来验证未打开 stdin 管道时,写入会被拒绝。

数据流:进去的是 use_remote 参数 → 它调用 assert_exec_process_rejects_write_without_pipe_stdin → 出来是 Result。

调用关系:它只是触发具体检查;实际验证 WriteStatus::StdinClosed 和进程输出 eof 的工作在 assert_exec_process_rejects_write_without_pipe_stdin 中。

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

exec_process_signal_interrupts_process812–814 ↗
async fn exec_process_signal_interrupts_process(use_remote: bool) -> Result<()>

作用:这是测试入口,用来验证 Unix 类系统上中断信号能传到进程。

数据流:进去的是 use_remote 参数 → 它调用 assert_exec_process_signal_interrupts_process → 出来是测试成功或失败。

调用关系:它在 Unix 条件下运行,并把参数交给真正的信号测试函数。实际等待 ready、发送 Interrupt、检查退出码都由 assert_exec_process_signal_interrupts_process 完成。

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

exec_process_signal_reports_unsupported_on_windows822–824 ↗
async fn exec_process_signal_reports_unsupported_on_windows(use_remote: bool) -> Result<()>

作用:这是 Windows 专用测试入口,用来验证不支持的进程中断会明确报错。

数据流:进去的是 use_remote 参数 → 它调用 assert_exec_process_signal_reports_unsupported_on_windows → 出来是测试结果。

调用关系:它只在 Windows 条件下启用。它把测试框架参数交给具体断言函数,让本地和远程路径都检查同样的错误提示。

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

exec_process_preserves_queued_events_before_subscribe832–834 ↗
async fn exec_process_preserves_queued_events_before_subscribe(use_remote: bool) -> Result<()>

作用:这是测试入口,用来确认进程输出先发生、读取订阅后发生时,消息仍然保留。

数据流:进去的是 use_remote 参数 → 它调用 assert_exec_process_preserves_queued_events_before_subscribe → 出来是 Result。

调用关系:它负责把本地和远程两种模式接到具体排队事件测试上。真正延迟订阅并检查输出不丢的逻辑在 assert_exec_process_preserves_queued_events_before_subscribe 中。

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

utils/pty/src/tests.rs源码 ↗
testtest run

这个文件不是给产品运行时直接用的,而是给开发者验证底层进程功能用的。它会真的启动 Python、shell 等小程序,检查三件大事:能不能把文字写进子进程,能不能把标准输出和标准错误读出来,能不能在退出和终止时收干净尾巴。它还特别照顾 Windows 和 Unix 的差异,比如换行、命令写法、PTY 输出可能比退出通知晚一点到。文件里先放了一批小工具函数:找 Python、拼 shell 命令、等某段输出出现、合并输出流、等待进程退出等。后面的测试就像一套体检项目,分别检查管道、PTY、驱动器封装、窗口 resize、继承文件描述符、杀掉后台子进程等场景。没有这些测试,代码看起来能跑,但很可能在真实终端交互、跨平台运行或异常退出时悄悄丢输出、卡住,或者留下孤儿进程。

函数细节30
find_python19–30 ↗
fn find_python() -> Option<String>

作用:寻找当前机器上能用的 Python 命令。测试里很多地方要临时启动 Python,如果找不到就跳过相关测试,避免因为环境缺少 Python 而误报代码坏了。

数据流:它不接收参数,只依次尝试运行 python3 --versionpython --version → 哪个命令能成功返回,就把这个命令名保存下来 → 返回可用的命令名;如果两个都不行,就返回空。

调用关系:需要 Python 的测试会先问它,例如 PTY 交互测试、管道输入回显测试、大量 stderr 输出测试、继承文件描述符的 Python REPL 测试。它自己只负责探路,不负责启动真正被测的进程。

调用图:被 4 处调用(pipe_drains_stderr_without_stdout_activity, pipe_process_round_trips_stdin, pty_preserving_inherited_fds_keeps_python_repl_running, pty_python_repl_emits_output_and_exits);外部调用 1 个(new)。

setsid_available32–41 ↗
fn setsid_available() -> bool

作用:检查当前系统有没有 setsid 这个工具。setsid 可以让进程脱离原来的会话,测试终止逻辑时需要它。

数据流:它先看是不是 Windows;Windows 没有这套 Unix 会话机制,就直接说不可用 → 在非 Windows 上尝试执行 setsid true → 成功就返回 true,失败或命令不存在就返回 false。

调用关系:只有 pipe_terminate_aborts_detached_readers 会用它。那个测试开始前先确认工具存在,不存在就跳过,避免把机器环境问题当成程序问题。

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

shell_command43–53 ↗
fn shell_command(program: &str) -> (String, Vec<String>)

作用:把一段 shell 脚本文字包装成当前系统能执行的命令。这样同一类测试可以同时跑在 Windows 和 Unix 上。

数据流:输入是一段要执行的命令字符串 → 如果是 Windows,就用 cmd.exe /C ...;否则用 /bin/sh -c ... → 输出可执行程序名和参数列表。

调用关系:多个测试都通过它准备小脚本,比如管道和 PTY 共用接口测试、拆分 stdout/stderr 测试、检查子进程会话测试、终止后台进程测试。它把跨平台差异挡在测试外面。

调用图:被 5 处调用(pipe_and_pty_share_interface, pipe_process_can_expose_split_stdout_and_stderr, pipe_process_detaches_from_parent_session, pipe_terminate_aborts_detached_readers, pty_terminate_kills_background_children_in_same_process_group);外部调用 3 个(cfg!, var, vec!)。

echo_sleep_command55–61 ↗
fn echo_sleep_command(marker: &str) -> String

作用:生成一段“先打印标记,再稍微等一下”的命令。测试用它确认进程真的输出了指定文字,并且不是一启动就立刻消失导致读不到。

数据流:输入是一个标记文字 → Windows 上生成 echoping 延时的命令,Unix 上生成 echosleep 的命令 → 输出完整命令字符串。

调用关系pipe_and_pty_share_interface 用它分别生成管道和 PTY 的小任务,再交给 shell_command 包装成可运行命令。

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

split_stdout_stderr_command63–71 ↗
fn split_stdout_stderr_command() -> String

作用:生成一段会分别向标准输出和标准错误写固定内容的命令。标准输出可以理解为正常说话,标准错误是报错/诊断通道。

数据流:它不接收参数 → 按系统选择 Windows 的 cmd 写法或 Unix 的 shell 写法 → 返回一段确定会输出 split-outsplit-err 的脚本。

调用关系pipe_process_can_expose_split_stdout_and_stderr 用它制造可预测的两路输出,从而检查库有没有把 stdout 和 stderr 混在一起。

调用图:被 1 处调用(pipe_process_can_expose_split_stdout_and_stderr);外部调用 1 个(cfg!)。

collect_split_output73–79 ↗
async fn collect_split_output(mut output_rx: tokio::sync::mpsc::Receiver<Vec<u8>>) -> Vec<u8>

作用:把一个输出通道里的所有小块字节收集成一整段。测试里用它等待 stdout 或 stderr 被读完。

数据流:输入是一个异步接收器,里面会陆续收到字节块 → 它循环接收,收到一块就追加到总列表里,直到通道关闭 → 返回合并后的字节数组。

调用关系:拆分 stdout/stderr 的测试和驱动器测试会把它放进异步任务里,让 stdout、stderr 两路同时被排空,避免某一路没人读而堵住。

调用图:调用 1 个内部函数(recv);被 3 处调用(driver_backed_process_can_expose_split_stdout_and_stderr, driver_backed_process_drains_output_that_arrives_after_exit_signal, pipe_process_can_expose_split_stdout_and_stderr);外部调用 1 个(new)。

combine_spawned_output81–99 ↗
fn combine_spawned_output(
    spawned: SpawnedProcess,
) -> (
    crate::ProcessHandle,
    tokio::sync::broadcast::Receiver<Vec<u8>>,
    tokio::sync::oneshot::Receiver<i32>,
)

作用:把一个已启动进程的标准输出和标准错误合成一条输出流,同时保留进程句柄和退出通知。这样很多测试不用关心两路输出的区别。

数据流:输入是 SpawnedProcess,里面有进程会话、stdout 接收器、stderr 接收器、退出接收器 → 它拆开这些字段,并调用合并输出的工具 → 输出进程句柄、合并后的输出接收器、退出码接收器。

调用关系:大多数测试启动进程后都会先经过它做“整理”。它把底层返回的复杂结构变成测试更方便使用的三件东西。

调用图:被 11 处调用(pipe_and_pty_share_interface, pipe_drains_stderr_without_stdout_activity, pipe_process_detaches_from_parent_session, pipe_process_round_trips_stdin, pipe_spawn_no_stdin_can_preserve_inherited_fds, pipe_terminate_aborts_detached_readers, pty_preserving_inherited_fds_keeps_python_repl_running, pty_python_repl_emits_output_and_exits, pty_spawn_can_preserve_inherited_fds, pty_spawn_with_inherited_fds_supports_resize (+1 more));外部调用 1 个(combine_output_receivers)。

collect_output_until_exit101–141 ↗
async fn collect_output_until_exit(
    mut output_rx: tokio::sync::broadcast::Receiver<Vec<u8>>,
    exit_rx: tokio::sync::oneshot::Receiver<i32>,
    timeout_ms: u64,
) -> (Vec<u8>, i32)

作用:一边收集输出,一边等待进程退出,并带有超时保护。它解决了测试最常见的问题:不能无限等,也不能漏掉退出前后的最后几字节。

数据流:输入是输出接收器、退出码接收器和超时时间 → 它同时监听输出、退出和总超时;收到输出就累计,收到退出码后还会再短暂等待一会儿,把晚到的尾部输出也收掉 → 返回收集到的字节和退出码;超时则用 -1 表示没等到正常退出。

调用关系:很多测试都把最终收尾交给它,比如 PTY Python 测试、管道回显测试、stderr 排空测试、继承文件描述符测试、resize 测试。它是测试里的通用“等进程结束并拿结果”的工具。

调用图:被 8 处调用(pipe_and_pty_share_interface, pipe_drains_stderr_without_stdout_activity, pipe_process_round_trips_stdin, pipe_spawn_no_stdin_can_preserve_inherited_fds, pty_preserving_inherited_fds_keeps_python_repl_running, pty_python_repl_emits_output_and_exits, pty_spawn_can_preserve_inherited_fds, pty_spawn_with_inherited_fds_supports_resize);外部调用 5 个(new, pin!, select!, from_millis, now)。

wait_for_output_contains144–177 ↗
async fn wait_for_output_contains(
    output_rx: &mut tokio::sync::broadcast::Receiver<Vec<u8>>,
    needle: &str,
    timeout_ms: u64,
) -> anyhow::Result<Vec<u8>>

作用:等待 PTY 输出里出现某段指定文字。适合那种必须先看到“准备好了”或“当前尺寸是某值”后,才能进行下一步的测试。

数据流:输入是输出接收器、要找的文字和超时时间 → 它不断收输出并转成可读文本检查 → 找到就返回已收集的字节;通道关闭或超时就返回错误,错误里带上已经看到的内容方便排查。

调用关系pty_spawn_with_inherited_fds_supports_resize 用它先等 shell 报告初始终端大小,然后才发送 resize 和继续执行的输入。

调用图:调用 1 个内部函数(recv);被 1 处调用(pty_spawn_with_inherited_fds_supports_resize);外部调用 6 个(from_utf8_lossy, new, bail!, from_millis, now, timeout)。

wait_for_python_repl_ready179–212 ↗
async fn wait_for_python_repl_ready(
    output_rx: &mut tokio::sync::broadcast::Receiver<Vec<u8>>,
    timeout_ms: u64,
    ready_marker: &str,
) -> anyhow::Result<Vec<u8>>

作用:等待 Python 交互解释器(REPL,也就是输入一行执行一行的 Python 窗口)打印出准备完成的标记。

数据流:输入是输出接收器、超时时间和准备标记 → 它持续收集 PTY 输出,直到文本里包含这个标记 → 成功返回已收集输出;如果输出关闭或等太久,就返回带上下文的错误。

调用关系pty_python_repl_emits_output_and_exits 启动 Python 后用它确认解释器已经能接收命令,然后再写入 printexit()

调用图:调用 1 个内部函数(recv);被 1 处调用(pty_python_repl_emits_output_and_exits);外部调用 6 个(from_utf8_lossy, new, bail!, from_millis, now, timeout)。

wait_for_python_repl_ready_via_probe215–264 ↗
async fn wait_for_python_repl_ready_via_probe(
    writer: &tokio::sync::mpsc::Sender<Vec<u8>>,
    output_rx: &mut tokio::sync::broadcast::Receiver<Vec<u8>>,
    timeout_ms: u64,
    newline: &str,
)

作用:通过反复给 Python 发送一条打印标记的小命令,来确认 Python 交互窗口已经能响应输入。它比单纯等提示符更稳,因为不同环境的提示符可能不一样。

数据流:输入是写入通道、输出接收器、超时时间和换行符 → 它隔一小段时间发送 print('标记'),同时收集输出 → 一旦看到标记就返回已收集输出;如果一直没看到或通道关闭,就返回错误。

调用关系pty_preserving_inherited_fds_keeps_python_repl_running 用它确认带继承文件描述符启动的 Python REPL 还活着、还能执行输入。它既向进程写,也从进程读。

调用图:调用 1 个内部函数(recv);被 1 处调用(pty_preserving_inherited_fds_keeps_python_repl_running);外部调用 10 个(send, from_utf8_lossy, new, bail!, cfg!, format!, min, from_millis, now, timeout)。

process_exists267–279 ↗
fn process_exists(pid: i32) -> anyhow::Result<bool>

作用:在 Unix 上检查某个进程 ID 是否还存在。它用来判断被终止的后台子进程有没有真的死掉。

数据流:输入是进程 ID → 它调用 kill(pid, 0),这个调用不会杀进程,只是问系统“这个进程在不在” → 返回 true 表示存在,false 表示不存在;遇到其他系统错误就返回错误。

调用关系wait_for_process_exit 会反复调用它。终止 PTY 后台子进程的测试靠它确认目标进程是否已经退出。

调用图:被 1 处调用(wait_for_process_exit);外部调用 2 个(last_os_error, kill)。

wait_for_marker_pid282–327 ↗
async fn wait_for_marker_pid(
    output_rx: &mut tokio::sync::broadcast::Receiver<Vec<u8>>,
    marker: &str,
    timeout_ms: u64,
) -> anyhow::Result<i32>

作用:从输出里等到某个标记后面的进程 ID。测试让子进程打印“标记+PID”,这个函数负责把 PID 抠出来。

数据流:输入是输出接收器、标记字符串和超时时间 → 它不断收字节、转成文本、寻找标记,找到后继续读取紧跟着的数字 → 成功返回解析出的进程 ID;超时或输出异常就返回错误。

调用关系:PTY 终止后台子进程测试和 Python REPL 存活测试都会用它拿到真实子进程 PID,随后再用 process_existswait_for_process_exit 做检查。

调用图:调用 1 个内部函数(recv);被 2 处调用(pty_preserving_inherited_fds_keeps_python_repl_running, pty_terminate_kills_background_children_in_same_process_group);外部调用 6 个(from_utf8_lossy, new, bail!, from_millis, now, timeout)。

wait_for_process_exit330–341 ↗
async fn wait_for_process_exit(pid: i32, timeout_ms: u64) -> anyhow::Result<bool>

作用:在限定时间内等待某个 Unix 进程消失。它让测试可以说“最多等几秒,看这个进程有没有被杀掉”。

数据流:输入是进程 ID 和超时时间 → 它循环调用 process_exists,如果发现进程不存在就返回 true;如果超过期限还存在就返回 false;中间每 20 毫秒睡一下,避免忙等占 CPU。

调用关系pty_terminate_kills_background_children_in_same_process_group 在调用终止后用它验证后台 sleep 进程是否真的退出。它的底层判断交给 process_exists

调用图:调用 1 个内部函数(process_exists);被 1 处调用(pty_terminate_kills_background_children_in_same_process_group);外部调用 3 个(from_millis, now, sleep)。

pty_python_repl_emits_output_and_exits344–390 ↗
async fn pty_python_repl_emits_output_and_exits() -> anyhow::Result<()>

作用:测试通过 PTY 启动 Python 交互模式后,能写入命令、读到输出,并正常退出。它模拟真实用户在终端里敲 Python。

数据流:它先找 Python,准备启动参数和环境变量 → 用 PTY 启动 Python,合并输出,等待准备标记 → 向 Python 写入 print('hello from pty')exit() → 收集退出前后的输出,断言看到了文字且退出码是 0。

调用关系:它依赖 find_python 找命令,依赖 combine_spawned_output 整理进程结果,依赖 wait_for_python_repl_ready 等 REPL 可用,最后交给 collect_output_until_exit 收尾。

调用图:调用 5 个内部函数(default, collect_output_until_exit, combine_spawned_output, find_python, wait_for_python_repl_ready);外部调用 10 个(new, from_utf8_lossy, assert!, assert_eq!, cfg!, spawn_pty_process, eprintln!, format!, vars, vec!)。

pipe_process_round_trips_stdin393–441 ↗
async fn pipe_process_round_trips_stdin() -> anyhow::Result<()>

作用:测试普通管道启动的进程能收到标准输入,并把内容回显出来。简单说,就是确认“写进去一行,能读回来一行”。

数据流:它按系统选择一个会读取一行并打印的程序:Windows 用 cmd,Unix 用 Python → 启动管道进程,拿到写入端和输出端 → 写入 roundtrip 后关闭输入 → 等进程退出并检查输出包含这段文字,退出码为 0。

调用关系:它会在 Unix 上先用 find_python,启动后用 combine_spawned_outputcollect_output_until_exit 完成通用的输出收集与退出检查。

调用图:调用 3 个内部函数(collect_output_until_exit, combine_spawned_output, find_python);外部调用 11 个(new, from_utf8_lossy, assert!, assert_eq!, cfg!, spawn_pipe_process, eprintln!, format!, var, vars (+1 more))。

pipe_process_detaches_from_parent_session445–484 ↗
async fn pipe_process_detaches_from_parent_session() -> anyhow::Result<()>

作用:在 Unix 上测试用管道启动的子进程会脱离父进程会话,并成为自己的会话 leader。这样终端信号和进程组管理会更可控。

数据流:它先读取父进程的会话 ID → 启动一个会打印自身 PID 的 shell → 从输出里解析子进程 PID,再读取子进程会话 ID → 断言子会话 ID 等于子 PID,并且不同于父会话 ID,最后确认进程正常退出。

调用关系:它用 shell_command 生成跨 shell 命令,用 spawn_pipe_process 启动,再用 combine_spawned_output 拿输出和退出通知。

调用图:调用 2 个内部函数(combine_spawned_output, shell_command);外部调用 10 个(new, from_utf8_lossy, bail!, assert_eq!, assert_ne!, spawn_pipe_process, getsid, vars, from_millis, timeout)。

pipe_and_pty_share_interface487–525 ↗
async fn pipe_and_pty_share_interface() -> anyhow::Result<()>

作用:测试管道进程和 PTY 进程暴露出来的使用方式是一致的。调用者不应该因为底层连接方式不同,就被迫写两套完全不同的代码。

数据流:它分别准备一个管道命令和一个 PTY 命令,各自打印不同标记 → 启动两个进程并整理输出接口 → 分别等它们退出 → 检查两者都退出成功,而且输出中包含各自标记。

调用关系:它用 echo_sleep_command 生成测试脚本,用 shell_command 包装命令,再通过 combine_spawned_outputcollect_output_until_exit 用同一种方式读取结果。

调用图:调用 5 个内部函数(default, collect_output_until_exit, combine_spawned_output, echo_sleep_command, shell_command);外部调用 7 个(new, assert!, assert_eq!, cfg!, spawn_pipe_process, spawn_pty_process, vars)。

pipe_drains_stderr_without_stdout_activity528–546 ↗
async fn pipe_drains_stderr_without_stdout_activity() -> anyhow::Result<()>

作用:测试即使标准输出没有动静,标准错误里大量数据也能被持续读走。否则子进程可能因为错误输出管道塞满而卡死。

数据流:它先找 Python → 运行一段只往 stderr 写大量字符的脚本 → 启动管道进程并合并输出 → 等进程退出,确认退出码为 0 且确实收到了输出。

调用关系:它通过 find_python 准备环境,使用 spawn_pipe_process 启动,随后由 combine_spawned_outputcollect_output_until_exit 验证输出排空能力。

调用图:调用 3 个内部函数(collect_output_until_exit, combine_spawned_output, find_python);外部调用 7 个(new, assert!, assert_eq!, spawn_pipe_process, eprintln!, vars, vec!)。

pipe_process_can_expose_split_stdout_and_stderr549–592 ↗
async fn pipe_process_can_expose_split_stdout_and_stderr() -> anyhow::Result<()>

作用:测试无标准输入的管道进程能把标准输出和标准错误分开暴露给调用者。调用者有时需要知道一段文字到底是正常输出还是错误输出。

数据流:它生成一段同时写 stdout 和 stderr 的命令 → 用无 stdin 的管道方式启动 → 分别开异步任务收集 stdout 和 stderr → 等进程退出,再断言两路内容分别等于预期文本。

调用关系:它用 split_stdout_stderr_commandshell_command 准备命令,用 collect_split_output 同时排空两路输出,不走 combine_spawned_output,因为这里正是要检查两路分离。

调用图:调用 3 个内部函数(collect_split_output, shell_command, split_stdout_stderr_command);外部调用 8 个(new, assert_eq!, cfg!, spawn_pipe_process_no_stdin, vars, spawn, from_millis, timeout)。

driver_backed_process_can_expose_split_stdout_and_stderr595–643 ↗
async fn driver_backed_process_can_expose_split_stdout_and_stderr() -> anyhow::Result<()>

作用:测试由自定义 ProcessDriver 包出来的进程对象,也能分开提供 stdout 和 stderr。ProcessDriver 可以理解为一套手工接好的输入、输出、退出信号。

数据流:它先手动建立写入通道、stdout 广播通道、stderr 广播通道和退出通道 → 用这些拼成 ProcessDriver 并转换成 SpawnedProcess → 分别发送 stdout/stderr 数据和退出码 → 收集后断言两路内容和退出码正确。

调用关系:它直接测试 spawn_from_driver 的包装能力,并用 collect_split_output 收完两路输出。它不启动真实外部程序,而是用人工通道模拟。

调用图:调用 1 个内部函数(collect_split_output);外部调用 5 个(assert_eq!, spawn_from_driver, spawn, from_secs, timeout)。

driver_backed_process_can_resize_via_resizer_hook646–689 ↗
async fn driver_backed_process_can_resize_via_resizer_hook() -> anyhow::Result<()>

作用:测试驱动器封装的进程能响应终端尺寸调整。resize 就是把终端窗口改成多少行、多少列。

数据流:它创建一个 ProcessDriver,里面放入一个 resize 回调;这个回调收到尺寸后通过一次性通道发出去 → 调用进程会话的 resize,传入 40 行 120 列 → 等回调传回尺寸,并断言尺寸完全一致。

调用关系:它专门验证 spawn_from_driver 是否把会话的 resize 操作正确转发给驱动器提供的 resizer hook。

调用图:外部调用 7 个(new, assert_eq!, spawn_from_driver, new, new, from_secs, timeout)。

driver_backed_process_drains_output_that_arrives_after_exit_signal692–734 ↗
async fn driver_backed_process_drains_output_that_arrives_after_exit_signal() -> anyhow::Result<()>

作用:测试即使退出信号先到了,稍后才到的尾部输出也不会被丢掉。这是异步系统里很容易出现的竞态问题。

数据流:它创建一个只有 stdout 的驱动器进程 → 先发送退出码,再等 50 毫秒发送 tail 输出并关闭输出通道 → 等退出通知和收集任务完成 → 断言最终仍然收到了 tail,退出码也是 0。

调用关系:它用 spawn_from_driver 构造被测对象,用 collect_split_output 检查 stdout 是否被完整排空。这个测试补强了退出和输出到达顺序不固定的场景。

调用图:调用 1 个内部函数(collect_split_output);外部调用 7 个(assert_eq!, spawn_from_driver, spawn, from_millis, from_secs, sleep, timeout)。

pipe_terminate_aborts_detached_readers737–771 ↗
async fn pipe_terminate_aborts_detached_readers() -> anyhow::Result<()>

作用:测试管道进程被终止后,后台脱离会话的输出读取也会停下来。否则测试或真实程序可能继续收到不该来的输出,甚至泄漏读线程。

数据流:它先检查 setsid 是否可用 → 启动一个后台不断打印 tick 的脚本 → 等确认有输出后调用 terminate → 再短时间监听新输出;如果没有输出或通道关闭就算成功,若还收到内容就报错。

调用关系:它用 setsid_available 做环境门槛,用 shell_command 生成命令,用 combine_spawned_output 取得会话和输出流,然后重点验证 session.terminate() 的效果。

调用图:调用 3 个内部函数(combine_spawned_output, setsid_available, shell_command);外部调用 7 个(new, bail!, spawn_pipe_process, eprintln!, vars, from_millis, timeout)。

pty_terminate_kills_background_children_in_same_process_group775–816 ↗
async fn pty_terminate_kills_background_children_in_same_process_group() -> anyhow::Result<()>

作用:在 Unix 上测试 PTY 进程被终止时,同一进程组里的后台子进程也会被杀掉。否则主 shell 死了,后台 sleep 之类的进程还会残留。

数据流:它启动一个 shell,让它后台运行 sleep 1000 并打印后台进程 PID → 从 PTY 输出里解析 PID,并确认该进程存在 → 调用 terminate → 等这个 PID 消失;如果没消失,会尝试强杀清理,然后让测试失败。

调用关系:它用 shell_command 准备脚本,combine_spawned_output 拿输出,wait_for_marker_pid 抠出 PID,wait_for_process_exit 等待终止效果。

调用图:调用 5 个内部函数(default, combine_spawned_output, shell_command, wait_for_marker_pid, wait_for_process_exit);外部调用 6 个(new, assert!, spawn_pty_process, format!, kill, vars)。

pty_spawn_can_preserve_inherited_fds820–863 ↗
async fn pty_spawn_can_preserve_inherited_fds() -> anyhow::Result<()>

作用:在 Unix 上测试通过 PTY 启动进程时,可以保留指定的文件描述符。文件描述符可以理解为系统给打开文件、管道等资源发的号码。

数据流:它创建一根系统管道,把写端的号码放进环境变量并指定要继承 → 启动 shell,让它通过 /dev/fd/号码 写入 __preserved__ → 等进程正常退出 → 从管道读端读内容,确认正好收到这段文字。

调用关系:它直接调用 spawn_process_with_inherited_fds 测 PTY 的继承能力,再用 combine_spawned_outputcollect_output_until_exit 等进程收尾。

调用图:调用 4 个内部函数(default, spawn_process_with_inherited_fds, collect_output_until_exit, combine_spawned_output);外部调用 7 个(new, new, assert_eq!, last_os_error, pipe, vars, from_raw_fd)。

pty_preserving_inherited_fds_keeps_python_repl_running867–941 ↗
async fn pty_preserving_inherited_fds_keeps_python_repl_running() -> anyhow::Result<()>

作用:在 Unix 上测试“保留文件描述符”这个功能不会破坏 Python 交互模式。也就是说,额外继承资源后,PTY 里的 Python 仍然能正常活着、接收命令、退出。

数据流:它先找 Python,创建一根管道并把写端设为要继承 → 通过 PTY 启动 Python → 关闭父进程这边的管道句柄后,反复探测直到 Python 能响应 → 让 Python 打印自己的 PID,确认进程仍存在 → 发送 exit(),收集输出并检查退出码为 0。

调用关系:它组合了多个辅助函数:find_python 找程序,wait_for_python_repl_ready_via_probe 确认 REPL 可用,wait_for_marker_pid 拿 PID,collect_output_until_exit 收尾。核心被测入口是 spawn_process_with_inherited_fds

调用图:调用 7 个内部函数(default, spawn_process_with_inherited_fds, collect_output_until_exit, combine_spawned_output, find_python, wait_for_marker_pid, wait_for_python_repl_ready_via_probe);外部调用 9 个(new, assert!, assert_eq!, last_os_error, eprintln!, format!, pipe, vars, from_raw_fd)。

pty_spawn_with_inherited_fds_reports_exec_failures945–989 ↗
async fn pty_spawn_with_inherited_fds_reports_exec_failures() -> anyhow::Result<()>

作用:在 Unix 上测试使用“继承文件描述符”的 PTY 启动方式时,如果可执行文件不存在,会把错误报告出来,而不是假装启动成功。

数据流:它创建一根管道并指定写端要继承 → 尝试启动一个肯定不存在的命令 /definitely/missing/command → 如果意外启动成功就终止并失败;如果返回错误,就检查错误文字包含“找不到文件”一类信息。

调用关系:它直接针对 spawn_process_with_inherited_fds 的失败路径。这个测试确保复杂启动路径也能正确把 exec 失败传给调用者。

调用图:调用 2 个内部函数(default, spawn_process_with_inherited_fds);外部调用 7 个(new, bail!, assert!, last_os_error, pipe, vars, from_raw_fd)。

pty_spawn_with_inherited_fds_supports_resize993–1054 ↗
async fn pty_spawn_with_inherited_fds_supports_resize() -> anyhow::Result<()>

作用:在 Unix 上测试带继承文件描述符启动的 PTY 仍然支持调整终端大小。继承额外资源不应该影响 resize 功能。

数据流:它创建管道并启动 shell,初始终端大小设为 31 行 101 列 → shell 先打印 stty size 看到的大小并等待输入 → 测试等到初始大小输出后,把终端改成 45 行 132 列,再发送一行让 shell 继续 → 收集最终输出,确认 shell 看到的新大小正确且退出码为 0。

调用关系:它使用 spawn_process_with_inherited_fds 启动,靠 wait_for_output_contains 等初始输出,用会话的 resize 发起尺寸变化,最后由 collect_output_until_exit 验证结果。

调用图:调用 4 个内部函数(spawn_process_with_inherited_fds, collect_output_until_exit, combine_spawned_output, wait_for_output_contains);外部调用 8 个(new, from_utf8_lossy, assert!, assert_eq!, last_os_error, pipe, vars, from_raw_fd)。

pipe_spawn_no_stdin_can_preserve_inherited_fds1058–1100 ↗
async fn pipe_spawn_no_stdin_can_preserve_inherited_fds() -> anyhow::Result<()>

作用:在 Unix 上测试无标准输入的管道启动方式也能保留指定文件描述符。也就是说,不接 stdin 的进程仍可以继承额外打开的资源。

数据流:它创建系统管道,把写端号码放进环境变量并声明要继承 → 用无 stdin 的方式启动 shell,让它往这个继承来的 fd 写入 __pipe_preserved__ → 等进程退出并确认退出码为 0 → 从读端读出内容并断言完全一致。

调用关系:它测试的是 spawn_process_no_stdin_with_inherited_fds,并复用 combine_spawned_outputcollect_output_until_exit 做通用的进程结果收集。

调用图:调用 3 个内部函数(spawn_process_no_stdin_with_inherited_fds, collect_output_until_exit, combine_spawned_output);外部调用 7 个(new, new, assert_eq!, last_os_error, pipe, vars, from_raw_fd)。

文件系统和流语义

本组涵盖共享文件系统测试脚手架、平台专用路径和沙箱行为,以及构建在 exec-server 传输之上的文件和 HTTP 流式 API。

exec-server/tests/file_system/support.rs源码 ↗
testtest setup

文件系统测试不能只测“能不能读写文件”,还要确认权限边界有没有守住,比如只读目录不能被写、符号链接不能偷偷绕出去。这个文件把这些重复准备工作收起来:测试只要说想测 Local(本地)还是 Remote(远程服务器),它就创建对应的 ExecutorFileSystem(执行器用来访问文件的统一接口)。如果是本地,它准备好运行时需要的辅助程序路径;如果是远程,它启动测试用 exec-server,并通过环境对象拿到远程文件系统接口。FileSystemContext 还故意保存 helper 路径和服务器句柄,哪怕字段名前有下划线不直接用,也是为了让这些资源在测试期间别提前被释放。后半部分提供几个造沙箱的小工具:把普通路径确认成绝对路径,再包装成“这个根目录只可读”或“这个工作区可写”的权限上下文。这样每个测试不用重复写一堆权限配置,重点能放在真正要验证的文件系统行为上。

函数细节6
FileSystemImplementation::fmt36–41 ↗
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result

作用:把文件系统实现类型变成好读的文字,比如把 Local 显示成“local”,把 Remote 显示成“remote”。这通常用于测试名字、日志或错误信息,让人一眼知道当前测的是哪一种实现。

数据流:进去的是一个 FileSystemImplementation 值和一个格式化输出器 → 函数根据值是 Local 还是 Remote 选出对应的小写字符串 → 把字符串写进输出器,返回写入是否成功的结果。

调用关系:它是 Rust 的 Display 显示接口的一部分。外部代码在需要把 FileSystemImplementation 放进字符串、日志或测试参数说明时会间接调用它;它本身只把文字交给标准的 write_str 写出,不参与文件系统测试的实际读写。

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

create_file_system_context44–71 ↗
async fn create_file_system_context(
    implementation: FileSystemImplementation,
) -> Result<FileSystemContext>

作用:按测试要求创建一个可用的文件系统测试环境。测试可以传入 Local 或 Remote,然后拿到同一种 ExecutorFileSystem 接口,不用关心背后是本地实现还是远程服务器。

数据流:进去的是想测试的实现类型 → 如果是 Local,就找到测试辅助程序路径,组装运行时路径,再创建 LocalFileSystem;如果是 Remote,就启动测试用 exec-server,按它的 websocket 地址创建测试环境,再从环境里取出文件系统接口 → 出来的是 FileSystemContext,里面有文件系统接口,并保留必要的辅助路径或服务器句柄,防止测试运行中资源消失。

调用关系:很多文件系统测试在开头都会调用它,比如测试复制、创建目录、读取元数据、沙箱路径解析等。它是测试和真实实现之间的接线板:本地分支会交给 test_codex_helper_paths、ExecServerRuntimePaths::new 和 LocalFileSystem::with_runtime_paths;远程分支会先交给 exec_server 启动服务,再用 Environment::create_for_tests 接上这个服务。

调用图:调用 5 个内部函数(create_for_tests, with_runtime_paths, new, exec_server, test_codex_helper_paths);被 34 处调用(assert_canonicalize_resolves_directory_alias, assert_sandboxed_canonicalize_resolves_directory_alias, file_system_copy_copies_directory_recursively, file_system_copy_copies_file, file_system_copy_rejects_copying_directory_into_descendant, file_system_copy_rejects_directory_without_recursive, file_system_create_directory_creates_nested_directories, file_system_get_metadata_reports_files_and_directories, file_system_read_directory_lists_entries, file_system_read_file_returns_bytes (+15 more));外部调用 1 个(new)。

absolute_path73–80 ↗
fn absolute_path(path: std::path::PathBuf) -> AbsolutePathBuf

作用:确认传进来的路径一定是绝对路径,并把它转换成项目里专门表示“绝对路径”的类型。这样后面的沙箱规则不会拿到含糊的相对路径。

数据流:进去的是一个普通 PathBuf 路径 → 函数先检查它是不是绝对路径,不是就让测试立刻失败并说明是哪条路径有问题 → 如果检查通过,就转换成 AbsolutePathBuf 并返回;它不改文件系统,只做路径校验和包装。

调用关系:read_only_sandbox 和 workspace_write_sandbox 都会先调用它,因为权限边界必须建立在明确的绝对路径上。另有测试会直接用它确认权限配置能正确保留绝对路径。它把路径转换这件小事集中起来,避免每个测试自己写断言。

调用图:调用 1 个内部函数(try_from);被 3 处调用(sandbox_context_from_profile_preserves_workspace_write_read_only_subpaths, read_only_sandbox, workspace_write_sandbox);外部调用 1 个(assert!)。

read_only_sandbox82–90 ↗
fn read_only_sandbox(readable_root: std::path::PathBuf) -> FileSystemSandboxContext

作用:创建一个“某个根目录只能读,不能写”的沙箱上下文。测试用它来验证文件系统实现是否真的尊重只读权限。

数据流:进去的是一个应该可读的根目录路径 → 函数先把它确认并转换成绝对路径 → 再做成一条沙箱规则:这个路径允许 Read(读取)访问 → 最后交给 sandbox_context 包装成完整的 FileSystemSandboxContext 返回。

调用关系:很多沙箱读写测试会调用它,比如确认可读根目录能读取、不可写路径会被拒绝、符号链接不能绕出允许范围。它自己不直接构造完整权限配置,而是只准备“只读目录”这一条规则,然后把最后的组装交给 sandbox_context。

调用图:调用 2 个内部函数(absolute_path, sandbox_context);被 7 处调用(assert_sandboxed_canonicalize_resolves_directory_alias, file_system_sandboxed_metadata_and_read_allow_readable_root, file_system_sandboxed_write_allows_additional_write_root, file_system_read_directory_rejects_symlink_escape, file_system_sandboxed_read_rejects_symlink_escape, file_system_sandboxed_read_rejects_symlink_parent_dotdot_escape, file_system_sandboxed_write_rejects_unwritable_path);外部调用 1 个(vec!)。

workspace_write_sandbox92–102 ↗
fn workspace_write_sandbox(
    writable_root: std::path::PathBuf,
) -> FileSystemSandboxContext

作用:创建一个“某个工作区可以写”的沙箱上下文。测试用它来模拟正常工作区:允许创建、删除、复制文件,但仍要挡住越界访问。

数据流:进去的是一个应该可写的根目录路径 → 函数先保证这是绝对路径 → 再做成一条沙箱规则:这个路径允许 Write(写入)访问 → 最后调用 sandbox_context,把规则变成文件系统操作会使用的沙箱上下文并返回。

调用关系:涉及写操作的测试经常调用它,比如复制文件、创建目录、删除文件、检查符号链接是否能逃出工作区。它位于测试准备阶段,负责把“这个目录可写”这句人话翻译成底层权限对象,再交给 sandbox_context 完成统一包装。

调用图:调用 2 个内部函数(absolute_path, sandbox_context);被 11 处调用(sandbox_context_from_profile_preserves_workspace_write_read_only_subpaths, file_system_copy_preserves_symlink_source, file_system_copy_rejects_symlink_escape_destination, file_system_copy_rejects_symlink_escape_source, file_system_create_directory_rejects_symlink_escape, file_system_remove_rejects_symlink_escape, file_system_remove_removes_symlink_not_target, file_system_sandboxed_write_allows_explicit_alias_roots, file_system_sandboxed_write_preserves_existing_hard_link, file_system_sandboxed_write_rejects_symlink_escape (+1 more));外部调用 1 个(vec!)。

sandbox_context104–109 ↗
fn sandbox_context(entries: Vec<FileSystemSandboxEntry>) -> FileSystemSandboxContext

作用:把一组文件系统沙箱规则包装成完整的沙箱上下文。它是只读沙箱和可写沙箱的共同收口点,避免两边重复写同样的权限组装代码。

数据流:进去的是若干条 FileSystemSandboxEntry,也就是“哪些路径有什么访问权限”的清单 → 函数先把这些条目做成受限制的文件系统策略,再配上受限制的网络策略,形成运行时权限配置 → 最后从这个权限配置生成 FileSystemSandboxContext 返回。

调用关系:read_only_sandbox 和 workspace_write_sandbox 都会把准备好的规则交给它。它再调用权限模型里的 restricted、from_runtime_permissions 和 from_permission_profile,把简单规则变成执行器真正能理解的沙箱上下文。这样测试文件只需要关心“读”或“写”,不用到处了解权限对象的内部拼装方式。

调用图:调用 3 个内部函数(from_permission_profile, from_runtime_permissions, restricted);被 2 处调用(read_only_sandbox, workspace_write_sandbox)。

exec-server/tests/file_system/shared.rs源码 ↗
testtest run

文件系统代码很容易出问题:本地能读,远程不一定能读;普通路径能用,带权限限制或符号链接时可能就出错。这个文件专门把这些风险逐项测一遍。它会先创建临时目录和临时文件,再通过统一的 file_system 接口去读元数据、写文件、按块读大文件、复制目录、列目录、删除目录等,最后用断言检查结果。这里还测试了沙箱权限。沙箱可以理解成“文件访问围栏”,只允许程序碰指定范围内的文件;测试会确认只读根目录能读、额外授予的写目录能写、工作区里的只读子路径不会丢。它还提供两个辅助测试函数,给别的测试文件复用,用来检查符号链接或目录别名经过 canonicalize(把别名路径解析成真实路径)后是否正确。

函数细节19
sandbox_context_from_profile_preserves_workspace_write_read_only_subpaths28–51 ↗
fn sandbox_context_from_profile_preserves_workspace_write_read_only_subpaths() -> Result<()>

作用:检查从工作区写权限沙箱转换成权限配置后,工作区里本该只读的子目录不会被弄丢。这里重点看的是 .git 目录,因为它通常不希望被随便改。

数据流:它先创建一个临时工作区和里面的 .git 目录,再生成“工作区可写”的沙箱配置。接着把沙箱权限转成运行时权限策略,取出可写根目录列表。最后确认这个工作区仍然是可写根,并且 .git 这个子路径仍在只读名单里。

调用关系:这是一个独立测试。它直接使用 workspace_write_sandbox 创建沙箱,用 absolute_path 统一路径格式,并用系统的建目录和路径规范化能力准备测试场景。它不测试文件读写接口,而是专门守住权限转换这一步。

调用图:调用 2 个内部函数(absolute_path, workspace_write_sandbox);外部调用 5 个(new, assert!, panic!, canonicalize, create_dir_all)。

file_system_get_metadata_reports_files_and_directories56–103 ↗
async fn file_system_get_metadata_reports_files_and_directories(
    implementation: FileSystemImplementation,
) -> Result<()>

作用:确认文件系统接口能正确分辨“文件”和“目录”,并报告大小、创建时间、修改时间等基本信息。

数据流:它拿到某种文件系统实现后,在临时目录里创建一个文本文件和一个目录。然后分别调用 get_metadata 查询它们。结果应该显示文本文件是文件、不是目录、大小为 5;目录则是目录、不是文件,并且修改时间有效。

调用关系:这个测试通过 create_file_system_context 同时适配本地和远程实现。它把路径转成 PathUri 后交给 file_system.get_metadata,再用断言比较返回的 FileMetadata,确保两种实现给出的信息一致。

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

file_system_create_directory_creates_nested_directories108–128 ↗
async fn file_system_create_directory_creates_nested_directories(
    implementation: FileSystemImplementation,
) -> Result<()>

作用:确认创建目录接口在 recursive 为 true 时,能一次性创建多层不存在的目录。通俗说,就是 mkdir -p 这种效果。

数据流:它先准备一个临时根目录,然后指定一个还不存在的 source/nested 路径。调用 create_directory 并打开 recursive 选项后,测试检查这个嵌套目录确实已经出现在磁盘上。

调用关系:这个测试仍然通过 create_file_system_context 拿到本地或远程文件系统。它把真实路径包装成 PathUri 后交给 file_system.create_directory,最后用系统路径检查结果。

调用图:调用 2 个内部函数(create_file_system_context, from_path);外部调用 2 个(new, assert!)。

file_system_write_file_writes_bytes133–152 ↗
async fn file_system_write_file_writes_bytes(
    implementation: FileSystemImplementation,
) -> Result<()>

作用:确认写文件接口能把一串原始字节写进指定文件。这里测的是最基本的“把内容落到文件里”。

数据流:它创建临时目录,指定 note.txt 作为目标文件。然后调用 write_file 写入字节内容 hello from trait。最后直接从磁盘读取这个文件,确认内容一字不差。

调用关系:它用 create_file_system_context 获取被测实现,用 PathUri 表示目标路径,然后把写入动作交给 file_system.write_file。测试最后用标准文件读取来验证接口真的改了磁盘内容。

调用图:调用 2 个内部函数(create_file_system_context, from_path);外部调用 2 个(new, assert_eq!)。

path_uri_join_and_parent_preserve_lexical_paths155–175 ↗
fn path_uri_join_and_parent_preserve_lexical_paths() -> Result<()>

作用:检查 PathUri 拼接路径和取父路径时,会保留字面上的路径写法,不会偷偷把 .. 这类路径片段提前化简掉。

数据流:它从一个 source 目录生成 PathUri,然后拼接 nested/note.txt,确认结果等于手工拼出的路径。接着取父路径,确认得到 nested。最后拼接 ../outside,确认这个带上级跳转的写法仍按字面保留。

调用关系:这是一个不依赖 file_system 实现的路径工具测试。它直接验证 PathUri::from_path、join 和 parent 的行为,避免后续文件系统操作因为路径对象自己改写路径而产生误判。

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

file_system_read_file_returns_bytes180–197 ↗
async fn file_system_read_file_returns_bytes(
    implementation: FileSystemImplementation,
) -> Result<()>

作用:确认读文件接口能把文件内容作为字节读回来。它不假设内容一定是文字,所以适合任意文件。

数据流:它先在临时目录写入 note.txt,内容是 hello from trait。然后通过 file_system.read_file 读取这个路径。返回的字节数组应该和原始内容完全一致。

调用关系:测试先用系统写文件准备材料,再用被测 file_system.read_file 读取。这样能清楚地区分“准备数据”和“验证接口读取能力”。

调用图:调用 2 个内部函数(create_file_system_context, from_path);外部调用 3 个(new, assert_eq!, write)。

file_system_read_file_stream_returns_bounded_chunks202–236 ↗
async fn file_system_read_file_stream_returns_bounded_chunks(
    implementation: FileSystemImplementation,
) -> Result<()>

作用:确认流式读文件会把大文件拆成大小受限的小块,而且拼回去后内容不变。流式读取可以理解成“分批搬货”,避免一次把大文件全塞进内存。

数据流:它生成一个比两个读取块还大一点的二进制文件。然后调用 read_file_stream 得到一串数据块。测试先确认每块都不是空的,并且长度不超过 FILE_READ_CHUNK_SIZE;再把所有块拼起来,确认和原文件内容完全一样。

调用关系:它通过 create_file_system_context 测本地和远程实现,用 futures 的 try_collect 收集流里的块。这个测试重点卡住 read_file_stream 的两个承诺:块大小有上限,内容顺序和数据不丢。

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

file_system_read_file_text_returns_string241–258 ↗
async fn file_system_read_file_text_returns_string(
    implementation: FileSystemImplementation,
) -> Result<()>

作用:确认读文本文件接口能直接返回字符串,而不是原始字节。适合读取普通文本配置、代码或说明文件。

数据流:它先创建一个文本文件并写入 hello from trait。随后调用 read_file_text 读取。返回结果应该是同样的字符串。

调用关系:这个测试和字节读取测试相似,但走的是 file_system.read_file_text。它验证文本解码这条更方便的读取通道在本地和远程实现上都可用。

调用图:调用 2 个内部函数(create_file_system_context, from_path);外部调用 3 个(new, assert_eq!, write)。

file_system_copy_copies_file263–284 ↗
async fn file_system_copy_copies_file(implementation: FileSystemImplementation) -> Result<()>

作用:确认复制接口能复制单个文件。也就是源文件还在,目标文件出现,并且内容一样。

数据流:它在临时目录创建 source.txt,并写入文本。然后调用 copy,把它复制到 copy.txt,recursive 设为 false,因为复制单文件不需要递归。最后读取目标文件,确认内容正确。

调用关系:测试把源路径和目标路径都转成 PathUri,然后交给 file_system.copy。它验证 CopyOptions 在普通文件场景下的最小用法。

调用图:调用 2 个内部函数(create_file_system_context, from_path);外部调用 3 个(new, assert_eq!, write)。

file_system_copy_copies_directory_recursively289–318 ↗
async fn file_system_copy_copies_directory_recursively(
    implementation: FileSystemImplementation,
) -> Result<()>

作用:确认复制目录时,如果 recursive 为 true,可以连同里面的子目录和文件一起复制。递归复制可以理解成“连箱子里的小箱子一起搬走”。

数据流:它创建 source/nested/note.txt 这样的目录树,并写入文件内容。然后调用 copy,把 source 整个复制到 copied。最后检查 copied/nested/note.txt 是否存在且内容正确。

调用关系:这个测试使用 file_system.copy 的目录复制路径,并明确传入 CopyOptions { recursive: true }。它和拒绝非递归复制目录的测试互相补充,一个证明允许场景,一个证明错误场景。

调用图:调用 2 个内部函数(create_file_system_context, from_path);外部调用 4 个(new, assert_eq!, create_dir_all, write)。

file_system_read_directory_lists_entries323–356 ↗
async fn file_system_read_directory_lists_entries(
    implementation: FileSystemImplementation,
) -> Result<()>

作用:确认列目录接口能列出目录里的直接成员,并标明每个成员是文件还是目录。

数据流:它创建一个 source 目录,里面放一个 nested 子目录和一个 root.txt 文件。调用 read_directory 后得到条目列表。测试先按文件名排序,再确认列表里正好有这两个成员,并且类型标记正确。

调用关系:它通过 file_system.read_directory 获取 ReadDirectoryEntry 列表。排序是为了避免不同系统返回目录顺序不同导致测试不稳定,真正要验证的是名字和类型。

调用图:调用 2 个内部函数(create_file_system_context, from_path);外部调用 4 个(new, assert_eq!, create_dir_all, write)。

file_system_remove_removes_directory361–385 ↗
async fn file_system_remove_removes_directory(
    implementation: FileSystemImplementation,
) -> Result<()>

作用:确认删除接口能删除目录,哪怕目录里还有子目录。这里使用 recursive 和 force,表示“递归删除,并且尽量强制完成”。

数据流:它先创建 remove-me/nested 目录结构。然后调用 remove,传入 recursive: true 和 force: true。最后检查 remove-me 已经不存在。

调用关系:这个测试覆盖 file_system.remove 的目录删除能力。它用临时目录保证不会误删真实文件,并用最终的 exists 检查确认删除真的发生了。

调用图:调用 2 个内部函数(create_file_system_context, from_path);外部调用 3 个(new, assert!, create_dir_all)。

file_system_write_file_reports_missing_parent390–418 ↗
async fn file_system_write_file_reports_missing_parent(
    implementation: FileSystemImplementation,
) -> Result<()>

作用:确认写文件时,如果父目录不存在,接口会报“找不到”的错误,而不是偷偷创建目录或留下半成品文件。

数据流:它构造 missing/note.txt 这个路径,但不创建 missing 目录。调用 write_file 后,测试期待失败。如果意外成功就直接报错;如果失败,就检查错误类型是 NotFound,并确认目标文件没有被创建。

调用关系:这个测试保护 write_file 的边界行为。它使用 anyhow::bail 在不该成功时立刻让测试失败,并用错误类型断言要求本地和远程实现行为一致。

调用图:调用 2 个内部函数(create_file_system_context, from_path);外部调用 4 个(new, bail!, assert!, assert_eq!)。

file_system_copy_rejects_directory_without_recursive423–449 ↗
async fn file_system_copy_rejects_directory_without_recursive(
    implementation: FileSystemImplementation,
) -> Result<()>

作用:确认复制目录时如果没有打开 recursive,接口会明确拒绝。这样可以避免用户以为复制了整个目录,结果只复制了一部分或行为不清楚。

数据流:它创建一个 source 目录,然后调用 copy 试图把目录复制到 dest,但 recursive 设为 false。测试期待得到 InvalidInput 错误,并确认错误文字说明必须设置 recursive: true。

调用关系:这个测试和目录递归复制测试成对存在。file_system.copy 在这里应该走错误分支,返回清楚的错误,而不是执行复制。

调用图:调用 2 个内部函数(create_file_system_context, from_path);外部调用 3 个(new, assert_eq!, create_dir_all)。

file_system_sandboxed_metadata_and_read_allow_readable_root454–490 ↗
async fn file_system_sandboxed_metadata_and_read_allow_readable_root(
    implementation: FileSystemImplementation,
) -> Result<()>

作用:确认在只读沙箱里,位于允许读取范围内的文件可以查询信息,也可以读取内容。沙箱是访问围栏,只读表示能看不能改。

数据流:它创建 allowed 目录和 note.txt 文件,再用 read_only_sandbox 把 allowed 设成可读范围。随后带着这个沙箱调用 get_metadata 和 read_file。返回的文件信息和内容都应该正确。

调用关系:这个测试把普通读取能力和权限检查结合起来。file_system 方法收到 Some(&sandbox) 后,必须先尊重沙箱规则,再完成元数据查询和读取。

调用图:调用 3 个内部函数(create_file_system_context, read_only_sandbox, from_path);外部调用 4 个(new, assert_eq!, create_dir_all, write)。

assert_canonicalize_resolves_directory_alias492–519 ↗
async fn assert_canonicalize_resolves_directory_alias(
    implementation: FileSystemImplementation,
    create_directory_alias: impl FnOnce(&Path, &Path) -> Result<()>,
) -> Result<()>

作用:这是给其他测试复用的辅助函数,用来确认目录别名能被解析成真实路径。目录别名可能是符号链接或 Windows junction,简单说就是“一个门牌指向另一个真实目录”。

数据流:它创建真实目录 source/nested/note.txt,再通过传入的 create_directory_alias 创建一个指向 source 的别名目录。然后构造经由别名访问文件的路径,以及文件的真实规范路径。先确认两者表面上不同,再调用 file_system.canonicalize,最后确认解析结果等于真实路径。

调用关系:它会被 file_system_canonicalize_resolves_directory_symlink 和 file_system_canonicalize_resolves_directory_junction 这类具体测试调用。具体怎么创建别名由调用者传进来;本函数只负责搭场景、调用 canonicalize、验证结果。

调用图:调用 2 个内部函数(create_file_system_context, from_path);被 2 处调用(file_system_canonicalize_resolves_directory_symlink, file_system_canonicalize_resolves_directory_junction);外部调用 6 个(new, assert_eq!, assert_ne!, canonicalize, create_dir_all, write)。

assert_sandboxed_canonicalize_resolves_directory_alias521–549 ↗
async fn assert_sandboxed_canonicalize_resolves_directory_alias(
    implementation: FileSystemImplementation,
    create_directory_alias: impl FnOnce(&Path, &Path) -> Result<()>,
) -> Result<()>

作用:这是带沙箱版本的目录别名解析辅助测试。它确认即使开启文件访问围栏,只要路径在允许范围内,别名仍能被解析成真实路径。

数据流:它创建真实目录和文件,再创建一个目录别名。接着把整个临时目录设成只读沙箱范围。它用别名路径作为请求路径,用系统 canonicalize 得到真实期望路径,然后调用 file_system.canonicalize 并带上沙箱,最后确认两者一致。

调用关系:它被 sandboxed 版本的符号链接和 junction 测试调用。它和 assert_canonicalize_resolves_directory_alias 的区别是多了一层 read_only_sandbox,用来验证 canonicalize 既正确解析路径,又遵守沙箱入口。

调用图:调用 3 个内部函数(create_file_system_context, read_only_sandbox, from_path);被 2 处调用(file_system_sandboxed_canonicalize_resolves_directory_symlink, file_system_sandboxed_canonicalize_resolves_directory_junction);外部调用 6 个(new, assert_eq!, assert_ne!, canonicalize, create_dir_all, write)。

file_system_sandboxed_write_allows_additional_write_root555–603 ↗
async fn file_system_sandboxed_write_allows_additional_write_root(
    implementation: FileSystemImplementation,
) -> Result<()>

作用:确认一个原本只读的沙箱,可以通过“额外权限”临时增加一个可写目录。这样某些操作能只开放很小的写入范围,而不是放开整个文件系统。

数据流:它先创建 readable 和 writable 两个目录,并从只读 readable 沙箱开始。然后构造 AdditionalPermissionProfile,把 writable 加成读写根目录。接着计算合并后的文件系统和网络沙箱策略,更新 sandbox.permissions。最后带着新沙箱写入 writable/note.txt,并确认文件内容是 created。

调用关系:这个测试串起了权限模型转换和文件写入。它调用 effective_file_system_sandbox_policy、effective_network_sandbox_policy 计算最终权限,再用 PermissionProfile::from_runtime_permissions_with_enforcement 生成可执行的权限配置,最后交给 file_system.write_file 验证效果。

调用图:调用 7 个内部函数(create_file_system_context, read_only_sandbox, from_read_write_roots, from_runtime_permissions_with_enforcement, effective_file_system_sandbox_policy, effective_network_sandbox_policy, from_path);外部调用 4 个(new, assert_eq!, create_dir_all, vec!)。

file_system_copy_rejects_copying_directory_into_descendant608–634 ↗
async fn file_system_copy_rejects_copying_directory_into_descendant(
    implementation: FileSystemImplementation,
) -> Result<()>

作用:确认不能把一个目录复制到它自己的子目录里面。否则就像把一个箱子复制进自己内部,可能造成无限套娃或混乱结果。

数据流:它创建 source/nested 目录,然后试图把 source 递归复制到 source/nested/copy。测试期待 copy 返回 InvalidInput 错误,并检查错误文字说明不能复制到自身或后代路径。

调用关系:这个测试保护 file_system.copy 的安全边界。它和正常递归复制目录测试一起说明:目录可以复制,但目标位置不能落在源目录里面。

调用图:调用 2 个内部函数(create_file_system_context, from_path);外部调用 3 个(new, assert_eq!, create_dir_all)。

exec-server/tests/file_system_unix.rs源码 ↗
testtest execution

这个测试文件像是在给文件系统功能做“越界安检”。Unix 里有符号链接(像快捷方式,指向另一个文件或目录)、硬链接(两个名字指向同一个真实文件)和 FIFO(命名管道,一种特殊通信文件),这些都容易让普通的读、写、复制、删除变复杂。文件里的测试会同时跑本地实现和远程实现,先搭临时目录,再制造链接、只读沙箱或可写沙箱,然后调用文件系统接口确认结果。重点不是简单地能不能读写,而是确认:沙箱外的文件不能被链接绕过去碰到;删除符号链接时不会误删目标;复制时符号链接要按链接保留;递归复制时跳过不认识的特殊文件;Linux 下还检查沙箱 helper 能从保留的 PATH 里找到 bwrap。没有这些测试,文件系统安全边界很容易在少见路径上被绕开。

函数细节25
assert_sandbox_denied40–61 ↗
fn assert_sandbox_denied(error: &std::io::Error)

作用:检查一次失败是不是真的因为沙箱拦住了,而不是因为别的奇怪错误。这样测试不会只看到“失败了”就算通过,而是确认失败原因符合安全预期。

数据流:输入一个系统错误 → 它查看错误类型和错误文字,比如“没有权限”“不允许操作”“只读文件系统” → 如果文字和类型像沙箱拒绝,就什么也不返回;如果不像,就让测试直接失败。

调用关系:多个“应该被沙箱挡住”的测试都会调用它。那些测试先故意通过符号链接或只读目录做危险操作,拿到错误后交给它统一判断,避免每个测试重复写同样的检查。

调用图:被 8 处调用(file_system_copy_rejects_symlink_escape_destination, file_system_copy_rejects_symlink_escape_source, file_system_create_directory_rejects_symlink_escape, file_system_read_directory_rejects_symlink_escape, file_system_remove_rejects_symlink_escape, file_system_sandboxed_read_rejects_symlink_escape, file_system_sandboxed_write_rejects_symlink_escape, file_system_sandboxed_write_rejects_unwritable_path);外部调用 4 个(assert!, kind, to_string, panic!)。

assert_normalized_path_rejected63–80 ↗
fn assert_normalized_path_rejected(error: &std::io::Error)

作用:检查带有 .. 的路径被整理后,失败原因仍然是合理的。.. 可以理解成“回到上一级目录”,它可能让路径在进入文件系统前就变了样。

数据流:输入一个系统错误 → 它接受两类合理结果:文件不存在,或被沙箱/权限机制拒绝 → 如果错误不属于这些情况,就让测试失败。

调用关系:它只服务于 file_system_sandboxed_read_rejects_symlink_parent_dotdot_escape。那个测试专门验证路径规范化之后,系统不会被符号链接和 .. 的组合骗过。

调用图:被 1 处调用(file_system_sandboxed_read_rejects_symlink_parent_dotdot_escape);外部调用 4 个(assert!, kind, to_string, panic!)。

alias_root_candidate82–89 ↗
fn alias_root_candidate() -> Result<Option<PathBuf>>

作用:找一个适合测试“路径别名”的临时目录根路径。路径别名指同一个真实目录可能有不同写法,比如 /tmp 可能实际指向别的位置。

数据流:它读取 /tmp 和系统临时目录 → 检查这些目录是否存在,并且规范化后的真实路径是否和原路径不同 → 找到就返回这个候选路径,找不到就返回空。

调用关系file_system_sandboxed_write_allows_explicit_alias_roots 用它决定这个平台有没有可测的别名根目录。如果没有,那个测试会安全跳过。

调用图:被 1 处调用(file_system_sandboxed_write_allows_explicit_alias_roots);外部调用 2 个(new, temp_dir)。

write_fake_bwrap97–143 ↗
fn write_fake_bwrap(bin_dir: &Path) -> Result<PathBuf>

作用:在测试目录里写一个假的 bwrap 程序。bwrap 是 Linux 上常用的沙箱工具,这里用假程序确认文件系统 helper 确实从 PATH 找到了它。

数据流:输入一个 bin 目录 → 创建目录,写入名为 bwrap 的 shell 脚本,脚本会记录收到的参数并执行真正的内部命令 → 给脚本加可执行权限,最后返回这个假 bwrap 的路径。

调用关系:Linux 专用测试 sandboxed_file_system_helper_finds_bwrap_on_preserved_path 会先调用它布置假工具,再启动服务并做一次沙箱写文件,最后检查假工具留下的日志。

调用图:被 1 处调用(sandboxed_file_system_helper_finds_bwrap_on_preserved_path);外部调用 5 个(join, create_dir_all, metadata, set_permissions, write)。

sandboxed_file_system_helper_finds_bwrap_on_preserved_path170–207 ↗
async fn sandboxed_file_system_helper_finds_bwrap_on_preserved_path() -> Result<()>

作用:测试 Linux 下沙箱文件系统 helper 能从保留下来的 PATH 环境变量里找到 bwrap。这很重要,因为找不到沙箱工具,安全写文件流程就可能无法启动。

数据流:它创建临时目录和假 bwrap → 把假工具目录放到 PATH 前面启动执行服务器 → 通过文件系统 helper 在沙箱内写文件 → 检查文件内容真的写入,并检查假 bwrap 日志里出现了预期参数。

调用关系:这个测试会调用 write_fake_bwrap 布置假工具,调用 exec_server_with_env 启动带指定环境的服务,再用 Environment::create_for_tests 拿到文件系统接口执行一次真实写入。

调用图:调用 5 个内部函数(create_for_tests, exec_server_with_env, workspace_write_sandbox, write_fake_bwrap, from_path);外部调用 9 个(new, assert!, assert_eq!, join_paths, split_paths, var_os, create_dir_all, read_to_string, vec!)。

file_system_sandboxed_write_rejects_unwritable_path269–294 ↗
async fn file_system_sandboxed_write_rejects_unwritable_path(
    implementation: FileSystemImplementation,
) -> Result<()>

作用:测试只读沙箱里不能写文件。只读沙箱就像“只能看不能改”的房间。

数据流:它创建临时目录和一个准备写入的文件路径 → 给这个目录套上只读沙箱 → 尝试写入内容 → 预期拿到拒绝错误,并确认文件没有被创建。

调用关系:它用 read_only_sandbox 制造禁止写入的规则,调用文件系统的 write_file,再把错误交给 assert_sandbox_denied 判断是不是正确的安全拒绝。

调用图:调用 4 个内部函数(create_file_system_context, read_only_sandbox, assert_sandbox_denied, from_path);外部调用 3 个(new, bail!, assert!)。

file_system_sandboxed_write_allows_explicit_alias_roots299–326 ↗
async fn file_system_sandboxed_write_allows_explicit_alias_roots(
    implementation: FileSystemImplementation,
) -> Result<()>

作用:测试如果沙箱明确允许一个带别名的根目录,那么通过这个别名路径写文件应该成功。它避免安全检查过度严格,把合法路径误拦掉。

数据流:它先找一个路径别名候选根目录 → 在这个根目录下建临时目录和文件路径 → 创建允许写这个根的沙箱 → 写入文件并读取回来确认内容是 created

调用关系:它依赖 alias_root_candidate 判断当前机器是否有可测场景;如果有,再通过 workspace_write_sandbox 创建可写沙箱,最后调用文件系统的 write_file

调用图:调用 4 个内部函数(create_file_system_context, workspace_write_sandbox, alias_root_candidate, from_path);外部调用 2 个(assert_eq!, new)。

file_system_copy_ignores_unknown_special_files_in_recursive_copy777–816 ↗
async fn file_system_copy_ignores_unknown_special_files_in_recursive_copy(
    implementation: FileSystemImplementation,
) -> Result<()>

作用:测试递归复制目录时,会跳过不认识的特殊文件,比如 FIFO 命名管道,但普通文件仍会复制。这样复制目录不会因为一个特殊节点就全部失败。

数据流:它创建 source 目录,里面放普通文本文件和一个用 mkfifo 命令创建的命名管道 → 递归复制到 copied → 确认普通文件内容复制成功,命名管道没有出现在目标目录。

调用关系:它先用系统命令 mkfifo 布置 Unix 特殊文件,再调用文件系统 copy 的递归模式。这个测试补充了单独复制 FIFO 会失败的场景。

调用图:调用 2 个内部函数(create_file_system_context, from_path);外部调用 7 个(new, bail!, assert!, assert_eq!, new, create_dir_all, write)。

file_system_copy_rejects_standalone_fifo_source821–854 ↗
async fn file_system_copy_rejects_standalone_fifo_source(
    implementation: FileSystemImplementation,
) -> Result<()>

作用:测试把单独的 FIFO 当作复制源时必须失败,并给出明确错误。FIFO 不是普通文件,直接复制它容易造成阻塞或不可预期行为。

数据流:它用 mkfifo 创建一个命名管道 → 调用 copy 尝试把它复制成普通目标 → 预期得到 InvalidInput 错误,并确认错误文字说明只支持普通文件、目录和符号链接。

调用关系:它直接验证 copy 对单个特殊文件的拒绝行为,和 file_system_copy_ignores_unknown_special_files_in_recursive_copy 一起说明:递归目录里遇到特殊文件会跳过,单独复制特殊文件会报错。

调用图:调用 2 个内部函数(create_file_system_context, from_path);外部调用 4 个(new, bail!, assert_eq!, new)。

exec-server/tests/file_system_windows.rs源码 ↗
testtest run on Windows

Windows 里有一种叫“目录联接点”的东西,可以把一个文件夹做成另一个文件夹的入口,有点像门牌号指向了别处的真实房间。这个测试文件就是确认项目里的文件系统代码在做 canonicalize(把路径整理成真实、标准路径)时,不会只停在这个“假入口”,而是能走到真正目标目录。文件先提供一个小工具 create_directory_junction,用 Windows 的 mklink /J 命令创建目录联接点。然后有两个异步测试:一个测普通的路径标准化,另一个测带沙箱限制的路径标准化。每个测试都会跑两遍:一次用本地文件系统实现,一次用远程文件系统实现。这样可以保证无论文件在本机还是通过远程执行服务访问,Windows 的特殊目录别名都能被正确解析。

函数细节3
create_directory_junction19–33 ↗
fn create_directory_junction(target: &Path, alias: &Path) -> Result<()>

作用:这个函数在 Windows 上创建一个目录联接点,也就是让一个路径看起来像目录,其实指向另一个真实目录。测试需要它来搭出“路径别名”这个场景。

数据流:输入是真实目标目录 target 和别名目录 alias → 它启动 Windows 命令行 cmd,执行 mklink /J,把 alias 做成指向 target 的目录联接点 → 如果命令成功就返回成功;如果失败,就把命令输出的错误信息整理出来并返回失败。

调用关系:它是后面两个测试交给共享测试逻辑使用的“造场景工具”。共享断言函数需要先制造一个目录别名,再检查文件系统是否能把这个别名解析回真实目录;这里负责 Windows 上具体怎么制造这个别名。

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

file_system_canonicalize_resolves_directory_junction38–43 ↗
async fn file_system_canonicalize_resolves_directory_junction(
    implementation: FileSystemImplementation,
) -> Result<()>

作用:这个测试确认普通的路径标准化功能能识别 Windows 目录联接点,并最终得到真实目录路径。它会分别测试本地实现和远程实现。

数据流:输入是测试框架传进来的 FileSystemImplementation,表示这次要测本地文件系统还是远程文件系统 → 它把这个实现类型和 create_directory_junction 交给共享测试函数 → 共享测试函数负责建目录、建联接点、调用 canonicalize 并判断结果是否正确,最后返回测试成功或失败。

调用关系:它本身不写完整测试步骤,而是像一个 Windows 专用入口,把“用 mklink /J 创建目录联接点”这件事告诉 shared::assert_canonicalize_resolves_directory_alias。真正的通用检查流程由共享测试模块执行。

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

file_system_sandboxed_canonicalize_resolves_directory_junction48–56 ↗
async fn file_system_sandboxed_canonicalize_resolves_directory_junction(
    implementation: FileSystemImplementation,
) -> Result<()>

作用:这个测试确认在沙箱模式下,路径标准化也能正确解析 Windows 目录联接点。沙箱可以理解成给文件访问加了围栏,测试是为了确保加了围栏后仍然不会认错路。

数据流:输入是要测试的文件系统实现类型,本地或远程 → 它把实现类型和 create_directory_junction 传给共享的沙箱测试函数 → 共享函数搭建目录联接点和沙箱环境,再检查沙箱版 canonicalize 是否能返回正确的真实路径,并把测试结果传回来。

调用关系:它是沙箱路径解析测试的 Windows 版本入口。它把平台特有的“怎么创建目录联接点”交代清楚,然后把主要验证工作交给 shared::assert_sandboxed_canonicalize_resolves_directory_alias。

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

exec-server/src/local_file_system_path_uri_tests.rs源码 ↗
testtest

这个文件只做一件事:确认 DirectFileSystem 这个“直接读本机文件”的组件,不会接受非本机格式的 file URI。URI 可以理解成一种统一写法的地址,比如 file:///tmp/a.txt 表示一个文件地址。但不同操作系统对文件地址的理解不一样:Unix 系统不应该把 Windows 或网络共享风格的地址当成本地文件,Windows 也不应该把 Unix 风格路径当成本地文件。测试里先构造一个“格式合法、但不是当前系统原生风格”的 PathUri,然后让 DirectFileSystem 去读它。正确结果不是读到文件,而是返回 InvalidInput,也就是“你给的输入不合适”。这个测试像门卫检查证件:证件本身是真的,但不是这个场馆能用的证件,所以必须拦下。

函数细节2
direct_file_system_rejects_non_native_uri_as_invalid_input8–15 ↗
async fn direct_file_system_rejects_non_native_uri_as_invalid_input()

作用:这是一个异步测试,用来确认 DirectFileSystem 遇到非本机文件 URI 时会拒绝它,并把错误类型标成“无效输入”。有人改动文件读取逻辑时,这个测试能防止把不该接受的路径放进来。

数据流:它先调用 non_native_uri 得到一个当前系统不该接受的文件地址 → 把这个地址交给 DirectFileSystem.read_file,并且不启用 sandbox(沙箱,可以理解成限制文件访问范围的保护栏)→ 期望读文件失败,然后检查失败原因必须是 io::ErrorKind::InvalidInput。它不产生文件内容,也不改动磁盘,只验证错误是否符合预期。

调用关系:它是这个测试文件的主角,由测试框架 tokio 在运行测试时自动执行。它把“构造特殊 URI”的小活交给 non_native_uri,最后用 assert_eq! 对比实际错误类型和期望错误类型。

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

non_native_uri17–27 ↗
fn non_native_uri() -> PathUri

作用:这个辅助函数专门造出一个“能被解析、但不是当前操作系统原生写法”的文件 URI。这样测试可以稳定地拿到一个应该被拒绝的输入。

数据流:它根据当前编译平台选择一段字符串:在 Unix 上用类似网络共享的 file://server/share/file.txt,在 Windows 上用类似 Unix 路径的 file:///usr/local/file.txt → 调用 PathUri::parse 把字符串解析成 PathUri → 如果解析成功就返回这个 PathUri;如果连解析都失败,就 panic,也就是直接让测试崩掉,因为测试前提不成立。

调用关系:它只被 direct_file_system_rejects_non_native_uri_as_invalid_input 调用,作用是提供测试材料。它自己依赖 PathUri::parse 来确认这个地址在语法上是合法 URI;如果不是合法 URI,就用 panic! 明确告诉测试作者:这里准备的样本有问题。

调用图:调用 1 个内部函数(parse);被 1 处调用(direct_file_system_rejects_non_native_uri_as_invalid_input);外部调用 1 个(panic!)。

exec-server/src/remote_file_system_path_uri_tests.rs源码 ↗
testtest run

这份测试像搭了一个假的远程文件服务器,用 WebSocket(一种保持连接的网络通道)收发 JSON-RPC(一种用 JSON 表示“请求”和“回复”的简单协议)消息。测试先启动一个临时服务器,然后让 RemoteFileSystem 去读两个看起来不像当前系统本地路径的地址,比如 Windows 盘符路径和网络共享路径。重点不是读到内容,而是抓住发出去的 fs/readFile 请求,检查里面的 path 和沙箱工作目录是不是原封不动。这里的“沙箱”可以理解成一圈安全围栏,告诉远端哪些目录能读写。测试还模拟了初始化握手,因为真实客户端正式工作前必须先和服务器打招呼。最后服务器返回空文件内容,测试确认请求参数完全符合预期。

函数细节6
remote_file_system_sends_path_and_sandbox_cwd_uris_without_native_conversion35–79 ↗
async fn remote_file_system_sends_path_and_sandbox_cwd_uris_without_native_conversion()

作用:这是主测试。它验证远程读文件时,Windows 风格路径、网络共享路径,以及沙箱里的当前目录 URI,都会被照原样发送,不会按运行测试的这台机器的路径规则转换。

数据流:它先启动一个会记录请求参数的假服务器,拿到一个 WebSocket 地址;然后创建远程文件系统客户端,准备两条特殊路径和一个带当前目录的沙箱设置;接着分别调用 read_file。每次调用都会让客户端发出读文件请求,假服务器把请求参数存下来并回一个空文件。最后测试把服务器抓到的参数和自己预先算好的参数比较,确认进去的 URI 和沙箱信息,出来时仍是同一套信息,没有被改写。

调用关系:它是整个文件的入口测试。它会调用 record_read_file_params 搭建假服务器,用 non_native_cwd 生成“不是本机风格”的当前目录,并通过项目里的权限构造函数做出沙箱。真正的网络消息读取、写入和初始化握手,都交给下面几个辅助函数完成。

调用图:调用 8 个内部函数(new, websocket_url, new, non_native_cwd, record_read_file_params, from_permission_profile_with_cwd, from_runtime_permissions, restricted);外部调用 2 个(assert_eq!, vec!)。

record_read_file_params81–130 ↗
async fn record_read_file_params(
    expected_requests: usize,
) -> (
    String,
    oneshot::Receiver<Vec<FsReadFileParams>>,
    tokio::task::JoinHandle<()>,
)

作用:这个函数搭一个临时的假远程服务器,用来接收客户端发来的读文件请求,并把请求参数录下来。它让测试不用连接真正的服务器,也能看清客户端到底发了什么。

数据流:输入是预计要收到多少个读文件请求。它绑定本机一个随机空闲端口,生成 WebSocket 地址;再开一个后台任务,等待客户端连进来。连接成功后,它先完成初始化握手,然后循环读取指定数量的 JSON-RPC 请求,只接受 fs/readFile,请求里的参数会被反序列化成 FsReadFileParams 并放进列表。每收到一个请求,它都会回一个空的读文件响应。最后它通过一次性通道把收集到的参数发回给测试,并返回服务器地址、接收结果的通道和后台任务句柄。

调用关系:它由主测试 remote_file_system_sends_path_and_sandbox_cwd_uris_without_native_conversion 调用,是测试里的“录音机”。它自己会调用 complete_websocket_initialize 先走完协议开场白,再用 read_jsonrpc_websocket 收请求,用 write_jsonrpc_websocket 发回复。

调用图:调用 3 个内部函数(complete_websocket_initialize, read_jsonrpc_websocket, write_jsonrpc_websocket);被 1 处调用(remote_file_system_sends_path_and_sandbox_cwd_uris_without_native_conversion);外部调用 11 个(new, bind, with_capacity, Response, format!, channel, panic!, from_value, to_value, spawn (+1 more))。

non_native_cwd132–139 ↗
fn non_native_cwd() -> PathUri

作用:这个函数造出一个“故意不像当前系统本地路径”的当前目录 URI。这样测试才能发现代码有没有擅自把远端路径改成本机路径。

数据流:它不接收外部输入,而是根据编译目标选择一个字符串:在 Unix 系统上用类似 Windows 网络共享的 file://server/share/checkout,在 Windows 上用类似 Unix 的 file:///usr/local/checkout。然后它把这个字符串解析成 PathUri 并返回。

调用关系:它只被主测试调用,用来填进沙箱上下文。它的作用像一个试金石:如果后续发送请求时路径被本机化,这种非本机风格的地址很容易暴露问题。

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

complete_websocket_initialize141–163 ↗
async fn complete_websocket_initialize(websocket: &mut WebSocketStream<TcpStream>)

作用:这个函数帮假服务器完成客户端启动时必须经过的握手。没有这一步,客户端还没进入正式工作状态,后面的读文件请求可能不会按正常流程发送。

数据流:它拿到已经建立好的 WebSocket 连接。先从连接里读一条 JSON-RPC 消息,要求它必须是 initialize 请求;然后写回一个带 session_id 的 initialize 响应;接着再读一条消息,要求它是 initialized 通知。完成后不返回业务数据,只是把这条连接推进到“已初始化”的状态。

调用关系:它由 record_read_file_params 在后台服务器里调用,发生在接收 fs/readFile 请求之前。它内部复用 read_jsonrpc_websocket 读消息,复用 write_jsonrpc_websocket 写响应。

调用图:调用 2 个内部函数(read_jsonrpc_websocket, write_jsonrpc_websocket);被 1 处调用(record_read_file_params);外部调用 3 个(Response, panic!, to_value)。

read_jsonrpc_websocket165–185 ↗
async fn read_jsonrpc_websocket(websocket: &mut WebSocketStream<TcpStream>) -> JSONRPCMessage

作用:这个函数从 WebSocket 连接里读出一条 JSON-RPC 消息。它把底层的网络帧转换成测试能直接判断的请求、响应或通知。

数据流:输入是一条可读写的 WebSocket 连接。它最多等 1 秒读取下一帧;如果读到文本帧,就按 JSON 文本解析;如果读到二进制帧,就按 JSON 字节解析;如果是 ping 或 pong 这类心跳包,就跳过继续等。成功时返回 JSONRPCMessage;如果超时、连接断开、内容不是合法 JSON-RPC,测试会直接失败。

调用关系:它是本文件几个测试辅助函数的底层读消息工具。complete_websocket_initialize 用它读初始化相关消息,record_read_file_params 用它读真正的 fs/readFile 请求。

调用图:被 2 处调用(complete_websocket_initialize, record_read_file_params);外部调用 6 个(from_secs, next, panic!, from_slice, from_str, timeout)。

write_jsonrpc_websocket187–196 ↗
async fn write_jsonrpc_websocket(
    websocket: &mut WebSocketStream<TcpStream>,
    message: JSONRPCMessage,
)

作用:这个函数把一条 JSON-RPC 消息写进 WebSocket 连接。测试里的假服务器用它给客户端回话。

数据流:输入是一条 WebSocket 连接和一个 JSONRPCMessage。它先把消息序列化成 JSON 字符串,再包装成 WebSocket 文本帧发出去。发送成功后不返回内容;如果消息不能序列化或网络写入失败,测试会直接失败。

调用关系:它是本文件的底层写消息工具。complete_websocket_initialize 用它回复初始化请求,record_read_file_params 用它回复每个 fs/readFile 请求。

调用图:被 2 处调用(complete_websocket_initialize, record_read_file_params);外部调用 3 个(send, to_string, Text)。

exec-server/src/sandboxed_file_system_path_uri_tests.rs源码 ↗
testtest run

这个测试文件关注一个很具体但很重要的问题:文件地址 URI(可以理解成一种标准格式的文件位置写法)不一定都适合当前操作系统。比如在 Unix 系统上,Windows 风格的网络共享地址就不是本地路径;在 Windows 上,Unix 风格的路径也不是原生路径。沙盒文件系统本来就是用来限制程序能读哪些文件的,像门卫一样。如果门卫连“这是不是本楼的地址”都不先判断,就可能放进不该放的请求。这里先创建执行服务器运行时需要的路径,再创建 SandboxedFileSystem,然后配一个受限制的文件权限沙盒。测试尝试读取一个“能解析、但不是当前系统原生格式”的 URI,并确认结果必须是 InvalidInput,也就是“输入本身不合法”。辅助函数 non_native_uri 会根据当前系统构造这种测试地址。

函数细节2
sandboxed_file_system_rejects_non_native_uri_as_invalid_input11–31 ↗
async fn sandboxed_file_system_rejects_non_native_uri_as_invalid_input()

作用:这是主测试函数,用来确认沙盒文件系统会拒绝非当前系统原生格式的文件 URI。有人改动文件读取或路径处理代码时,这个测试能及时发现“把不该接受的地址接受了”的问题。

数据流:它先读取当前可执行文件的位置,创建运行时路径信息;再用这些信息创建一个沙盒文件系统。接着它构造一个权限很紧的沙盒配置,并从 non_native_uri 拿到一个“格式合法但不适合当前系统”的文件地址。然后它让文件系统去读这个地址,预期会失败;最后检查失败原因是不是 InvalidInput。结果是:没有读出文件,而是得到一个明确的“输入无效”错误。

调用关系:这是整个文件的核心测试入口,由测试框架 tokio 在运行异步测试时调用。它自己负责搭好测试环境,调用 non_native_uri 准备特殊输入,再把这个输入交给 SandboxedFileSystem.read_file。最后用 assert_eq! 检查错误类型,证明沙盒文件系统在第一道关口就正确拒绝了这个地址。

调用图:调用 6 个内部函数(new, new, non_native_uri, from_permission_profile, from_runtime_permissions, restricted);外部调用 3 个(new, assert_eq!, current_exe)。

non_native_uri33–43 ↗
fn non_native_uri() -> PathUri

作用:这个辅助函数负责制造一个“不是当前操作系统原生文件路径”的 URI,专门给测试使用。它让主测试不用关心 Unix 和 Windows 的差异。

数据流:它不接收外部参数,而是根据编译目标判断当前系统:在 Unix 上使用类似 Windows 网络共享的 file://server/share/file.txt;在 Windows 上使用类似 Unix 路径的 file:///usr/local/file.txt。然后它把这段文字交给 PathUri::parse 解析成 PathUri 对象。解析成功就返回这个 URI;如果解析失败,就直接 panic,表示测试数据本身写错了。

调用关系:它只被 sandboxed_file_system_rejects_non_native_uri_as_invalid_input 调用,作用是提供测试用的特殊输入。它把“跨平台差异”藏在自己内部,让主测试能专心验证沙盒文件系统的行为:遇到这种非原生 URI 时必须拒绝读取。

调用图:调用 1 个内部函数(parse);被 1 处调用(sandboxed_file_system_rejects_non_native_uri_as_invalid_input);外部调用 1 个(panic!)。

exec-server/tests/file_stream.rs源码 ↗
testtest run

这个测试文件把执行服务器当成一个正在运行的小服务来用,然后通过 WebSocket(可以理解成一条持续连接的网络通道)去读本地临时文件。它重点验证几件事:大文件按固定大小切块时不会多读或少读;一个文件流读完后,服务器会释放“文件句柄”(打开文件时占用的名额);不支持的平台沙箱会被明确拒绝;FIFO 或命名管道这种“看起来像文件、其实会等另一端”的特殊对象不会让读取操作一直挂住;直接用底层协议读某一段文件时,可以不按顺序读;每条连接最多只能同时打开 128 个读文件句柄,而且关闭后能腾出名额。简单说,它像一套体检表,防止远程文件读取功能在边界场景里变慢、卡死、读错或耗光资源。

函数细节11
stream_stops_after_an_exact_block_boundary40–58 ↗
async fn stream_stops_after_an_exact_block_boundary() -> Result<()>

作用:这个测试确认:当文件大小刚好等于两个完整读取块时,流式读取会正好返回两个块,不会额外吐出一个空块,也不会漏掉最后一块。

数据流:它先启动测试用执行服务器,连上文件系统接口,再创建一个临时文件,内容是 2 个 1MB 的字节块。然后它用流式读取把文件读成一串数据块,最后检查结果长度是不是正好是两个 1MB。输入是临时文件路径和文件内容;输出是断言通过或失败;过程中不会保留长期状态。

调用关系:这是对高层文件系统接口的黑盒测试。它先通过 exec_server 启动服务,再用 connect_file_system 拿到远程文件系统对象,接着借助 PathUri::from_path 把普通路径变成接口能识别的路径格式。它不直接碰底层打开/关闭协议,而是验证上层 read_file_stream 的整体行为。

调用图:调用 3 个内部函数(exec_server, connect_file_system, from_path);外部调用 4 个(new, assert_eq!, write, vec!)。

completed_streams_release_handle_capacity61–79 ↗
async fn completed_streams_release_handle_capacity() -> Result<()>

作用:这个测试确认:一个流式读文件操作结束后,服务器会把占用的打开文件名额还回来。否则读很多次小文件后,即使每次都读完,也可能因为名额耗尽而失败。

数据流:它启动服务器,连接文件系统,创建一个只含有 repeated 的小文件。然后循环读这个文件,次数比允许同时打开的文件上限还多一次。每次都把流收集成完整内容,并检查内容正确。进去的是同一个文件路径;每轮读完后服务器应该释放内部文件句柄;出来的是每轮都成功读到同样内容。

调用关系:它走的是和真实使用最接近的高层路径:exec_server 启动服务,connect_file_system 取得文件系统接口,然后反复调用 read_file_stream。它关心的不是单次读取是否能成功,而是多次完成后的资源回收是否正常。

调用图:调用 3 个内部函数(exec_server, connect_file_system, from_path);外部调用 3 个(new, assert_eq!, write)。

stream_rejects_platform_sandbox82–105 ↗
async fn stream_rejects_platform_sandbox() -> Result<()>

作用:这个测试确认:流式读文件目前不支持平台沙箱时,会明确报“不支持”,而不是假装支持或绕过限制。沙箱可以理解成给文件访问划出的安全边界。

数据流:它创建一个临时文件,并用 read_only_sandbox 做出一个只允许读该目录的沙箱配置。然后它尝试在这个沙箱下进行流式读取。结果应该不是文件内容,而是一个 Unsupported 类型的错误,并且错误文字说明“streaming file reads do not support platform sandboxing”。

调用关系:这个测试先用 connect_file_system 拿到高层文件系统接口,再调用 read_only_sandbox 生成沙箱上下文,最后交给 read_file_stream。它验证的是接口在遇到暂不支持的安全模式时,会主动拒绝,而不是继续往下读文件。

调用图:调用 4 个内部函数(exec_server, connect_file_system, read_only_sandbox, from_path);外部调用 4 个(new, assert_eq!, panic!, write)。

file_reads_reject_fifo_without_waiting_for_a_writer109–146 ↗
async fn file_reads_reject_fifo_without_waiting_for_a_writer() -> Result<()>

作用:这个 Unix 专用测试确认:读取 FIFO(命名管道,一种像文件但会等待另一端写入的通信口)时,普通读取和流式读取都会立刻拒绝,而不是一直等写入者导致测试或程序卡死。

数据流:它在临时目录里用 mkfifo 创建一个 FIFO,然后分别调用普通 read_file 和流式 read_file_stream。每个调用都包在 1 秒超时里。正确结果是:两种读取都在超时前返回错误,错误内容说明这个路径不是普通文件。输入是 FIFO 路径;处理是尝试读取并等待错误;输出是两个相同的拒绝错误。

调用关系:它通过 exec_server 和 connect_file_system 使用真实的远程文件读取路径。它还借助系统命令 mkfifo 制造特殊文件,并用 timeout 防止测试真的卡住。这个测试覆盖的是 Unix 系统上很容易被忽略的“特殊文件不能当普通文件读”的安全边界。

调用图:调用 3 个内部函数(exec_server, connect_file_system, from_path);外部调用 8 个(from_secs, new, bail!, assert_eq!, new, format!, panic!, timeout)。

file_reads_reject_named_pipes150–194 ↗
async fn file_reads_reject_named_pipes() -> Result<()>

作用:这个 Windows 专用测试确认:读取命名管道时会快速失败,不会挂住。命名管道也是一种进程间通信入口,不是普通磁盘文件。

数据流:它先启动服务器并连接文件系统,然后创建两个 Windows 命名管道路径:一个用于普通读取测试,一个用于流式读取测试。每次读取都设置 1 秒超时。正确结果是两个读取都返回 InvalidInput 错误。输入是命名管道路径;输出是明确的无效输入错误;过程中不应等待另一端连接。

调用关系:它和 Unix 的 FIFO 测试作用相似,只是换成 Windows 的命名管道机制。它通过 connect_file_system 走高层读取接口,通过 Windows 的 ServerOptions 创建测试用管道,确保服务端文件读取逻辑不会误把管道当普通文件。

调用图:调用 4 个内部函数(exec_server, connect_file_system, new, from_path);外部调用 6 个(from_secs, assert_eq!, format!, panic!, new, timeout)。

stream_keeps_reading_the_open_file_after_path_replacement198–223 ↗
async fn stream_keeps_reading_the_open_file_after_path_replacement() -> Result<()>

作用:这个 Unix 专用测试确认:流式读取一旦打开了某个文件,后续就继续读这个已打开的文件本身,而不是每次按路径重新找文件。这样即使原路径被替换,正在读的流也不会突然读到另一个文件。

数据流:它先创建一个内容为很多个 a 的文件,并开始流式读取。读完第一个大块后,它把原路径上的文件删除,再把另一个内容为 b 的文件改名到同一路径。随后继续读取流。正确结果是剩下的内容仍然是原文件里的 a,而不是新文件里的 b;最后流正常结束。

调用关系:它通过高层 read_file_stream 验证底层打开文件句柄的语义。connect_file_system 负责连到服务端,PathUri::from_path 把路径交给接口;remove_file 和 rename 用来模拟“有人在读取过程中替换了文件”。这个测试说明流读取依赖已打开的文件,而不是依赖会变化的路径名。

调用图:调用 3 个内部函数(exec_server, connect_file_system, from_path);外部调用 6 个(new, assert_eq!, remove_file, rename, write, vec!)。

read_block_supports_non_sequential_offsets_and_lengths226–285 ↗
async fn read_block_supports_non_sequential_offsets_and_lengths() -> Result<()>

作用:这个测试直接检查底层协议的按块读取能力:客户端可以指定任意偏移位置和长度来读文件,不必从头到尾顺序读取。

数据流:它启动服务器,建立 ExecServerClient 连接,创建一个内容为 0123456789 的文件。然后用 fs_open 打开文件拿到 handle_id(文件读取的临时编号),接着多次调用 fs_read_block,分别读取不同位置和不同长度的数据。最后检查每次返回的字节和 eof(是否到文件末尾)标记是否正确,并用 fs_close 关闭句柄。

调用关系:这个测试绕过高层 read_file_stream,直接使用协议级的 fs_open、fs_read_block、fs_close。它用于确认服务端最底层的分段读取规则没问题,因为高层流式读取也是建立在这种“打开句柄、按块读、关闭句柄”的能力之上的。

调用图:调用 2 个内部函数(exec_server, from_path);外部调用 7 个(new, new_v4, new, assert_eq!, connect_websocket, new, write)。

open_enforces_the_per_connection_limit_and_close_releases_capacity288–345 ↗
async fn open_enforces_the_per_connection_limit_and_close_releases_capacity() -> Result<()>

作用:这个测试确认:同一条连接最多只能同时打开 128 个文件读取句柄,超过就会被拒绝;但关闭一个句柄后,又可以继续打开新的。这样可以防止一个客户端把服务器文件资源占满。

数据流:它启动服务器并创建一个测试文件,然后用同一个客户端连接连续打开 128 次,每次都保存返回的 handle_id。第 129 次打开应该失败,并返回固定错误码和说明。随后它关闭其中一个旧句柄,再尝试打开一次,应该成功。输入是同一个文件路径和多个新句柄编号;处理是反复打开、触发上限、关闭一个、再打开;输出是限制被正确执行且容量能恢复。

调用关系:它直接使用 ExecServerClient 的底层 fs_open 和 fs_close,而不是高层流式读取接口。这样能精确测试“每连接打开文件上限”这条协议规则。exec_server 提供服务端,connect_websocket 建立客户端连接,Uuid 用来生成不同的句柄编号。

调用图:调用 2 个内部函数(exec_server, from_path);外部调用 8 个(new, new_v4, with_capacity, bail!, assert_eq!, connect_websocket, new, write)。

open_rejects_handle_ids_longer_than_32_bytes348–379 ↗
async fn open_rejects_handle_ids_longer_than_32_bytes() -> Result<()>

作用:这个测试确认:客户端传来的文件读取句柄编号不能超过 32 字节。限制编号长度可以避免协议里出现过大的、恶意的或不合理的标识符。

数据流:它启动服务器,连上客户端,创建一个临时文件。然后调用 fs_open,但故意传入 33 个 x 组成的 handle_id。正确结果是服务端拒绝请求,返回固定错误码和“file read handle ID must not exceed 32 bytes”的错误信息。

调用关系:它直接测协议入口 fs_open 的输入校验。和高层流式读取不同,这里故意构造异常参数,确认服务器在真正打开文件前就拦下坏请求。exec_server 启动测试服务,connect_websocket 建立协议连接。

调用图:调用 2 个内部函数(exec_server, from_path);外部调用 6 个(new, bail!, assert_eq!, connect_websocket, new, write)。

connect_file_system381–384 ↗
fn connect_file_system(websocket_url: &str) -> Result<Arc<dyn ExecutorFileSystem>>

作用:这个小工具函数把测试服务器的 WebSocket 地址包装成一个可用的文件系统接口。测试里不想每次都重复写连接环境的创建代码,所以把这一步抽出来。

数据流:它接收一个 websocket_url 字符串,调用 Environment::create_for_tests 创建测试环境,然后从环境里取出 ExecutorFileSystem。进去的是服务地址;处理是搭好测试环境;出来的是一个可共享的文件系统对象 Arc<dyn ExecutorFileSystem>,Arc 可以理解成多人共用同一个对象的安全引用。

调用关系:多个高层文件读取测试都会先调用它,包括流式读取、沙箱拒绝、FIFO/命名管道拒绝、路径替换等测试。它本身不读文件,只负责把 exec_server 给出的地址变成测试可以调用的文件系统门面。

调用图:调用 1 个内部函数(create_for_tests);被 6 处调用(completed_streams_release_handle_capacity, file_reads_reject_fifo_without_waiting_for_a_writer, file_reads_reject_named_pipes, stream_keeps_reading_the_open_file_after_path_replacement, stream_rejects_platform_sandbox, stream_stops_after_an_exact_block_boundary)。

read_only_sandbox386–396 ↗
fn read_only_sandbox(path: std::path::PathBuf) -> FileSystemSandboxContext

作用:这个小工具函数创建一个“只允许读取指定路径”的沙箱配置,用来测试流式读取遇到平台沙箱时是否会拒绝。

数据流:它接收一个本地路径,先把它转换成 AbsolutePathBuf(绝对路径格式,避免相对路径带来歧义),再创建一个受限文件系统策略:这个路径只给读权限,网络也设为受限。最后把这些权限包装成 FileSystemSandboxContext 返回。

调用关系:它只被 stream_rejects_platform_sandbox 调用。那个测试需要一个真实的沙箱上下文来触发“不支持平台沙箱”的分支;这个函数就是专门造出这份测试输入的工具。

调用图:调用 4 个内部函数(from_permission_profile, from_runtime_permissions, restricted, from_absolute_path);被 1 处调用(stream_rejects_platform_sandbox);外部调用 1 个(vec!)。

exec-server/tests/http_client.rs源码 ↗
test测试运行期间;每个测试启动一个假的 WebSocket 服务端,连接真实客户端,跑完后关闭

这个测试文件像搭了一个“假服务员窗口”:测试里的假 exec-server 通过 WebSocket(一种可双向持续通信的网络连接)和真实的 ExecServerClient 对话。客户端先做初始化握手,然后发送 JSON-RPC(一种用 JSON 包装“请求、响应、通知”的通信格式)消息。测试重点是 HTTP 请求,尤其是响应体一段一段返回的“流式响应”。它会检查几件容易出错的事:普通请求必须强制走一次性返回;流式请求要用客户端自己生成的编号,避免不同请求混在一起;旧请求被取消、body 流被丢弃、网络断开、内部队列塞满时,都必须明确清理或报错,不能悄悄吞掉数据。文件后半部分提供了假服务器和 JSON-RPC 读写小工具,让每个测试都能精确安排服务器先说什么、后说什么。

函数细节20
http_request_forces_buffered_request_params51–112 ↗
async fn http_request_forces_buffered_request_params() -> Result<()>

作用:检查普通的缓冲式 HTTP 请求是否一定按“一次性返回完整响应”的方式发送。即使调用方误填了流式相关字段,客户端也应该把它改成非流式,避免服务器按错误模式处理。

数据流:测试先启动假服务器;假服务器读到客户端发来的 http/request,检查里面的 method、url、request_id 和 stream_response 已被改成正确的缓冲模式;然后假服务器返回状态码 200 和 body。客户端调用 http_request 后,测试拿到完整响应并比对内容,最后关闭客户端并等待假服务器结束。

调用关系:这是一个顶层测试。它先调用 spawn_scripted_exec_server 搭好假服务器,再通过 ScriptedExecServer::connect_client 连接真实客户端;假服务器内部用 JsonRpcPeer::read_http_request 读请求,用 JsonRpcPeer::write_response 回响应。timeout 用来防止测试挂住。

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

http_response_body_stream_uses_generated_ids_and_receives_ordered_deltas118–274 ↗
async fn http_response_body_stream_uses_generated_ids_and_receives_ordered_deltas() -> Result<()>

作用:检查流式 HTTP 响应是否用客户端自己生成的请求编号,并且响应体碎片能按顺序送到调用方。这样可以防止调用方重复使用同一个编号时,不同请求的数据混在一起。

数据流:测试让客户端发起第一个流式请求;假服务器确认线上实际 request_id 是 http-1,而不是调用方随便给的值;随后先返回响应头,再连续发送 hello、world、感叹号三个 body 片段。测试从 body_stream 里逐块读取并拼成完整字节串。然后它再发第二个流式请求,确认客户端已经清理了上一个流,并使用新的 http-2 编号。

调用关系:这是流式响应的主路径测试。它依赖 spawn_scripted_exec_server 建假服务器;测试脚本通过 JsonRpcPeer::read_http_request 接收请求,通过 JsonRpcPeer::write_response 发响应头,通过 JsonRpcPeer::write_body_delta 发 body 片段;客户端侧通过 http_request_stream 暴露响应头和可读取的 body 流。

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

http_response_body_stream_drops_queued_terminal_before_next_generated_id279–394 ↗
async fn http_response_body_stream_drops_queued_terminal_before_next_generated_id() -> Result<()>

作用:检查一种微妙情况:服务器先把“结束帧”排进队列,调用方却没读就丢掉 body 流。客户端必须清掉这条旧路由,让下一次请求使用新的编号。

数据流:假服务器先收到第一个流式请求,立刻发送一个 done=true 的空 body 结束通知,再返回响应头。客户端拿到响应头后直接丢弃 body_stream,不读取已经排队的结束帧。随后客户端发起第二个流式请求;假服务器确认它用的是 http-2,并返回 204 响应。测试确认第二次请求不受第一次残留结束帧影响。

调用关系:这是流清理逻辑的边界测试。它由 spawn_scripted_exec_server 启动假服务器;假服务器用 JsonRpcPeer::write_body_delta 故意把终止消息提前塞进去,再用 JsonRpcPeer::read_http_request 观察下一次请求是否拿到新编号。

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

http_response_body_stream_ignores_late_deltas_after_cancelled_request399–524 ↗
async fn http_response_body_stream_ignores_late_deltas_after_cancelled_request() -> Result<()>

作用:检查流式请求在还没等到响应头时被取消后,旧请求后来到达的 body 片段会不会被忽略。这样可以避免一个已经取消的请求把脏数据塞进后面的新请求里。

数据流:测试先启动一个流式请求,并等假服务器确认已经看见它;随后测试主动中止这个等待响应头的任务。假服务器之后接收第二个请求,先故意发送一条属于旧 http-1 的 stale 数据,再正常给 http-2 返回响应和 fresh 数据。测试读取第二个 body 流,只应该得到 fresh。

调用关系:这是取消场景测试。它使用 oneshot channel(一条只能发一次信号的小通道)协调“服务器已看到请求”和“测试可以取消任务”的时机;使用 tokio::spawn 启动可中止的客户端请求;假服务器用 JsonRpcPeer 写入旧 delta 和新 delta,验证客户端路由不会串台。

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

http_response_body_stream_ignores_late_deltas_after_drop529–676 ↗
async fn http_response_body_stream_ignores_late_deltas_after_drop() -> Result<()>

作用:检查调用方拿到响应头后直接丢掉 body 流时,客户端是否会删除旧路由,并忽略随后才来的旧 body 数据。这样后续请求不会收到前一个请求的残留内容。

数据流:第一个请求返回响应头后,测试立刻 drop 掉 body_stream,并用信号通知假服务器。假服务器收到信号后发送一条属于旧 http-1 的 stale 数据,再接收第二个请求并给 http-2 发送 fresh 数据。测试读取第二个 body 流,确认只拼出 fresh。

调用关系:这是“丢弃已返回 body 流”的清理测试。它用 spawn_scripted_exec_server 搭服务端,用两个 oneshot channel 控制旧数据发送的先后顺序;JsonRpcPeer 负责收请求、写响应、写 body 通知;客户端 http_request_stream 的内部清理行为是被验证对象。

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

http_response_body_stream_fails_when_transport_disconnects681–744 ↗
async fn http_response_body_stream_fails_when_transport_disconnects() -> Result<()>

作用:检查流式响应还没收到结束帧时,如果底层 WebSocket 连接断开,body 流会不会明确报错。正确行为是告诉调用方“传输断了”,而不是一直等下去。

数据流:假服务器收到流式请求后只返回响应头,不发送 done=true 的结束 body 帧,然后服务器任务结束,相当于连接断开。客户端拿到响应头后继续读 body_stream;测试期望下一次读取返回错误,并且错误信息说明 http-1 因传输断开失败。

调用关系:这是断线处理测试。它通过 spawn_scripted_exec_server 让假服务器自然结束连接;JsonRpcPeer::write_response 只发送头部响应;客户端 body_stream.recv 是最终观察点,用来确认内部传输断开事件被转成了流错误。

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

http_response_body_stream_reports_disconnect_when_queue_is_full749–836 ↗
async fn http_response_body_stream_reports_disconnect_when_queue_is_full() -> Result<()>

作用:检查 body 片段队列已经满了时,如果连接又断开,客户端仍然能在读完已排队数据后报告断线错误。这样调用方不会把“队列满后断线”误认为正常结束。

数据流:假服务器在响应头到达前,先发送刚好填满客户端 body 队列容量的 256 个小片段,然后返回响应头并结束连接。客户端拿到 body_stream 后不断读取,先读出这些已排队片段;读完后应该得到一个传输断开的错误,而不是 None 这种干净结束信号。

调用关系:这是队列满加断线的组合边界测试。它使用常量 HTTP_BODY_DELTA_CHANNEL_CAPACITY 控制发送数量;假服务器用 JsonRpcPeer::write_body_delta 填满队列;测试循环读取 body_stream,并用 bail 在出现错误的干净 EOF 时立刻判失败。

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

http_response_body_stream_reports_backpressure_truncation841–936 ↗
async fn http_response_body_stream_reports_backpressure_truncation() -> Result<()>

作用:检查当服务器发送 body 片段太快、超过客户端内部队列承受能力时,客户端会明确报“响应体被截断”。这比静悄悄结束更安全,因为调用方不会误用缺了一截的数据。

数据流:假服务器在响应头之前发送 1024 个 body 片段,数量超过客户端队列容量;然后返回响应头,但保持连接不断开,避免错误被误判成网络断线。客户端读取 body_stream,先读到一部分已排队片段,随后应该收到“body delta channel filled before delivery”的明确错误。测试最后发信号让服务器任务结束。

调用关系:这是背压测试。背压可以理解为“下游来不及收,上游还在倒水,水桶满了”。它用 oneshot channel 暂停假服务器关闭连接;JsonRpcPeer 负责快速写入大量 delta;测试读取 body_stream 来确认客户端把队列溢出报告成协议错误。

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

ScriptedExecServer::connect_client950–957 ↗
async fn connect_client(&self) -> Result<ExecServerClient>

作用:把真实的 ExecServerClient 连接到这个测试刚启动的假 WebSocket 服务器。测试用它来走真实的公开连接路径,而不是绕过客户端内部逻辑。

数据流:它读取自己保存的 websocket_url 和固定的 CLIENT_NAME,组装 RemoteExecServerConnectArgs;然后调用 ExecServerClient::connect_websocket 建立连接。成功时返回一个可用于发 HTTP 请求的客户端;失败时把错误包装成更好懂的上下文。

调用关系:各个顶层测试在 spawn_scripted_exec_server 返回 ScriptedExecServer 后调用它。它把测试服务器和真实客户端接起来,后续通信再由客户端和 JsonRpcPeer 在 WebSocket 上完成。

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

ScriptedExecServer::finish960–965 ↗
async fn finish(self) -> Result<()>

作用:等待假服务器脚本跑完,并把服务器任务里的错误传回测试。没有它,测试可能在后台任务还没结束时就退出,漏掉服务器侧发现的问题。

数据流:它接收并消耗 ScriptedExecServer 本身,等待内部 tokio 任务结束;如果任务 join 失败或脚本返回错误,就把错误继续往外传;全部正常时返回 Ok。

调用关系:每个测试最后都会 drop 客户端,然后调用 finish。它是测试收尾步骤,确保 spawn_scripted_exec_server 启动的后台服务端没有悄悄失败。

spawn_scripted_exec_server969–993 ↗
async fn spawn_scripted_exec_server(script: F) -> Result<ScriptedExecServer>

作用:启动一个只服务一个客户端的假 exec-server,并把测试提供的“剧本”接到连接后面。它让每个测试都能精确规定服务器收什么、发什么。

数据流:它先在本机随机端口绑定 TCP 监听器,生成 ws:// 地址;然后启动后台任务等待客户端连接,完成 WebSocket 握手,创建 JsonRpcPeer,先完成初始化握手,再执行测试传入的脚本。函数返回 ScriptedExecServer,里面有连接地址和后台任务句柄。

调用关系:所有顶层测试都先调用它。它创建 JsonRpcPeer,并在内部调用 JsonRpcPeer::complete_initialize;之后把控制权交给测试脚本,脚本再用 JsonRpcPeer 的读写函数模拟服务器行为。

调用图:被 8 处调用(http_request_forces_buffered_request_params, http_response_body_stream_drops_queued_terminal_before_next_generated_id, http_response_body_stream_fails_when_transport_disconnects, http_response_body_stream_ignores_late_deltas_after_cancelled_request, http_response_body_stream_ignores_late_deltas_after_drop, http_response_body_stream_reports_backpressure_truncation, http_response_body_stream_reports_disconnect_when_queue_is_full, http_response_body_stream_uses_generated_ids_and_receives_ordered_deltas);外部调用 5 个(bind, format!, spawn, timeout, accept_async)。

JsonRpcPeer::complete_initialize1002–1021 ↗
async fn complete_initialize(&mut self) -> Result<()>

作用:完成客户端连接后的初始化握手,并检查客户端发来的初始化内容是否正确。它相当于假服务器先和客户端“互相报到”,确认可以开始后续测试。

数据流:它先读取 initialize 请求,解析出 InitializeParams,并确认 client_name 是测试客户端名、没有恢复旧会话;然后回一个带 session_id 的成功响应;最后读取 initialized 通知。完成后,连接就进入可发送 HTTP 请求的状态。

调用关系:spawn_scripted_exec_server 在执行每个测试脚本前都会调用它。它内部把读取请求交给 JsonRpcPeer::read_request,把参数解析交给 decode_request_params,把响应发送交给 JsonRpcPeer::write_response,并用 JsonRpcPeer::read_notification 等待最后通知。

调用图:调用 4 个内部函数(read_notification, read_request, write_response, decode_request_params);外部调用 1 个(assert_eq!)。

JsonRpcPeer::read_http_request1024–1028 ↗
async fn read_http_request(&mut self) -> Result<(RequestId, HttpRequestParams)>

作用:从客户端读取一条类型明确的 http/request 调用,并把里面的参数解析成 HttpRequestParams。测试用它来检查客户端到底把什么 HTTP 请求发到了线上。

数据流:它等待下一条 JSON-RPC 请求,要求方法名必须是 http/request;然后取出请求参数并反序列化成 HttpRequestParams;最后返回 JSON-RPC 请求 id 和解析后的 HTTP 参数。

调用关系:几乎每个测试脚本都会调用它来接收客户端请求。它把通用读取工作交给 JsonRpcPeer::read_request,把参数转换交给 decode_request_params,返回的信息随后通常会被 assert_eq 检查,并被 JsonRpcPeer::write_response 用来回包。

调用图:调用 2 个内部函数(read_request, decode_request_params)。

JsonRpcPeer::read_request1031–1043 ↗
async fn read_request(&mut self, expected_method: &str) -> Result<JSONRPCRequest>

作用:读取下一条 JSON-RPC 消息,并确认它是一条指定方法名的“请求”。如果收到的不是预期请求,它会立刻报错,让测试失败。

数据流:它从 WebSocket 读取一条 JSON-RPC 消息;如果消息不是 Request 类型,或 method 和 expected_method 不一致,就返回错误;如果匹配,就把 JSONRPCRequest 返回给调用方。

调用关系:JsonRpcPeer::complete_initialize 和 JsonRpcPeer::read_http_request 都靠它做基础检查。它再往下调用 JsonRpcPeer::read_message 处理 WebSocket 和 JSON 解码细节。

调用图:调用 1 个内部函数(read_message);被 2 处调用(complete_initialize, read_http_request);外部调用 1 个(bail!)。

JsonRpcPeer::read_notification1046–1058 ↗
async fn read_notification(&mut self, expected_method: &str) -> Result<JSONRPCNotification>

作用:读取下一条 JSON-RPC 消息,并确认它是一条指定方法名的“通知”。通知是不需要响应的消息,这里主要用来验证初始化完成信号。

数据流:它先读取一条底层消息;如果不是 Notification 类型,或者方法名不等于 expected_method,就返回错误;符合预期时返回 JSONRPCNotification。

调用关系:目前主要由 JsonRpcPeer::complete_initialize 调用,用来等待 initialized 通知。它依赖 JsonRpcPeer::read_message 做实际网络读取和 JSON 解析。

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

JsonRpcPeer::write_response1061–1070 ↗
async fn write_response(&mut self, id: RequestId, result: T) -> Result<()>

作用:向客户端发送一条成功的 JSON-RPC 响应。测试用它模拟服务器对 initialize 或 http/request 的正常回答。

数据流:它接收请求 id 和任意可序列化的 result;先把 result 转成 JSON 值,再包装成 JSONRPCResponse,最后通过 WebSocket 发出去。发送成功返回 Ok,编码或网络发送失败则返回错误。

调用关系:初始化握手和多个测试脚本都会用它回客户端。它把具体写 WebSocket 的工作交给 JsonRpcPeer::write_message。

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

JsonRpcPeer::write_body_delta1073–1079 ↗
async fn write_body_delta(&mut self, delta: HttpRequestBodyDeltaNotification) -> Result<()>

作用:向客户端发送一条流式 HTTP 响应体片段通知。测试用它模拟服务器把 body 一块一块推给客户端。

数据流:它接收 HttpRequestBodyDeltaNotification,其中包含 request_id、顺序号、字节片段、是否结束和错误信息;把它转成 JSON,并包装成 method 为 http/request/bodyDelta 的通知;最后写入 WebSocket。

调用关系:所有流式响应测试都用它制造正常片段、结束帧、旧请求残留片段、队列溢出片段等场景。它底层调用 JsonRpcPeer::write_message 完成实际发送。

调用图:调用 1 个内部函数(write_message);外部调用 2 个(Notification, to_value)。

JsonRpcPeer::read_message1082–1094 ↗
async fn read_message(&mut self) -> Result<JSONRPCMessage>

作用:从 WebSocket 连接里读取一条原始消息,并解析成 JSON-RPC 消息。它是所有“读请求、读通知”的底层入口。

数据流:它在固定超时时间内等待 WebSocket 下一条消息;如果连接提前关闭、读取失败、收到关闭帧或收到非文本/二进制消息,就报错。若收到文本或二进制内容,就按 JSON 解析成 JSONRPCMessage 并返回。

调用关系:JsonRpcPeer::read_request 和 JsonRpcPeer::read_notification 都调用它。它把网络层的 Message 转换成测试脚本更容易使用的协议层对象。

调用图:被 2 处调用(read_notification, read_request);外部调用 5 个(next, bail!, from_slice, from_str, timeout)。

JsonRpcPeer::write_message1097–1106 ↗
async fn write_message(&mut self, message: JSONRPCMessage) -> Result<()>

作用:把一条 JSON-RPC 消息编码成文本,并通过 WebSocket 发给客户端。它是所有服务器侧发送动作的底层出口。

数据流:它接收 JSONRPCMessage,先序列化成字符串,再包装成 WebSocket 文本消息;随后在超时时间内发送。成功时没有额外输出;超时、编码失败或发送失败都会变成错误。

调用关系:JsonRpcPeer::write_response 和 JsonRpcPeer::write_body_delta 都调用它。上层决定要发响应还是通知,它负责统一完成编码和网络发送。

调用图:被 2 处调用(write_body_delta, write_response);外部调用 4 个(send, to_string, timeout, Text)。

decode_request_params1110–1119 ↗
fn decode_request_params(request: &JSONRPCRequest) -> Result<T>

作用:把 JSON-RPC 请求里的 params 字段转换成测试需要的具体类型。它让测试不用手动从 JSON 里一个字段一个字段抠数据。

数据流:它接收一个 JSONRPCRequest,先确认里面有 params;然后用反序列化把 JSON 值转成调用方指定的 Rust 类型。成功时返回这个具体类型;缺少 params 或格式不匹配时返回带说明的错误。

调用关系:JsonRpcPeer::complete_initialize 用它解析 InitializeParams,JsonRpcPeer::read_http_request 用它解析 HttpRequestParams。它是协议 JSON 和强类型测试断言之间的小桥梁。

调用图:被 2 处调用(complete_initialize, read_http_request);外部调用 1 个(from_value)。

exec-server/tests/http_request.rs源码 ↗
testintegration test

这个测试文件像一个临时搭起来的“假 HTTP 网站”,专门考 exec-server:测试先启动 exec-server,完成 JSON-RPC 初始化握手,然后让 exec-server 去访问本机 TCP 监听器。监听器会把收到的 HTTP 请求逐字读出来,确认方法、路径、请求头和请求体都对;接着它手写 HTTP 响应,再看 exec-server 返回给客户端的 JSON-RPC 消息是否正确。这里既测普通模式,也测流式模式。普通模式要求响应体一次性放进结果里;流式模式要求先返回状态和响应头,再用 http/request/bodyDelta 通知一段段送出响应体。文件还特别测试了两个容易出事故的边界:流式请求的 requestId 在结束前不能重复使用,以及不写超时时请求可以等下去、写了短超时就必须失败。它只在 Unix 系统上运行,是偏真实链路的端到端测试。

函数细节14
exec_server_http_request_buffers_response_body46–109 ↗
async fn exec_server_http_request_buffers_response_body() -> anyhow::Result<()>

作用:测试普通 HTTP 请求模式:exec-server 发出一次 POST 请求,并把整个 HTTP 响应体攒好后放进 JSON-RPC 响应里返回。有人改动 HTTP 请求功能时,这个测试能确认“非流式返回”没有坏。

数据流:测试先启动 exec-server,并发送初始化请求;然后创建本地 HTTP 监听器,把一个带请求头和请求体的 http/request 发给 exec-server。监听器收到真实 HTTP 请求后检查请求行、请求头、请求体,再回一个固定长度的 201 响应。最后测试从 exec-server 等到对应 JSON-RPC 响应,确认状态码、响应头和完整响应体都和假服务器写出的一样。

调用关系:它是一个完整场景测试,开头调用 exec_serverinitialize_exec_server 准备服务;中间用 accept_http_request 抓住 exec-server 发来的 HTTP 请求,用 respond_with_status_and_headers 回包;最后用 wait_for_response 取回 JSON-RPC 结果,并用 response_header 辅助检查响应头。

调用图:调用 5 个内部函数(exec_server, accept_http_request, initialize_exec_server, respond_with_status_and_headers, wait_for_response);外部调用 5 个(bind, assert_eq!, format!, to_value, vec!)。

exec_server_http_request_streams_response_body_notifications115–198 ↗
async fn exec_server_http_request_streams_response_body_notifications() -> anyhow::Result<()>

作用:测试流式 HTTP 响应:exec-server 应该先返回响应状态和响应头,然后把响应体分成多条通知按顺序发出来。这样可以验证大响应或持续响应不会被错误地一次性缓存。

数据流:测试启动并初始化 exec-server 后,请它用 GET 请求访问本地监听器,并设置 stream_response: true。监听器确认收到的请求正确,然后写出分块传输的 HTTP 响应。测试先读取第一条 JSON-RPC 事件,确认它是请求结果,里面有状态和响应头但没有响应体;之后继续收集 bodyDelta 通知,把每段字节拼起来,检查序号连续、内容是 hello world,最后一帧标记完成且没有错误。

调用关系:这个场景依赖 initialize_exec_server 做握手,accept_http_request 抓请求,respond_with_chunked_body 制造分块 HTTP 响应,collect_response_body_deltas 收尾读取所有流式通知。它刻意检查响应事件和 bodyDelta 通知的先后顺序。

调用图:调用 5 个内部函数(exec_server, accept_http_request, collect_response_body_deltas, initialize_exec_server, respond_with_chunked_body);外部调用 7 个(bind, bail!, assert_eq!, format!, from_value, to_value, vec!)。

exec_server_http_request_rejects_duplicate_stream_request_ids203–275 ↗
async fn exec_server_http_request_rejects_duplicate_stream_request_ids() -> anyhow::Result<()>

作用:测试流式请求的 requestId 不能在上一个流还没结束时重复使用。这个规则很重要,因为客户端要靠这个 ID 分清哪些 bodyDelta 属于哪一次请求。

数据流:测试先发起一个流式 GET 请求,假 HTTP 服务器返回一段分块数据但暂时不结束连接。exec-server 先返回这次请求的响应头信息,此时流还活着。测试随后用同一个 requestId 再发第二个流式请求,期待 exec-server 立刻返回 JSON-RPC 参数错误。最后测试放行第一个响应结束,收完它的 bodyDelta,再关闭服务。

调用关系:它使用 respond_with_chunked_body_until_finish 故意把第一个流卡住,让 ID 保持占用;用 wait_for_response 等第一个请求进入流式阶段;再通过 exec-server 的事件等待机制确认第二个请求变成错误;结束时调用 collect_response_body_deltas 把第一个流收干净。

调用图:调用 6 个内部函数(exec_server, accept_http_request, collect_response_body_deltas, initialize_exec_server, respond_with_chunked_body_until_finish, wait_for_response);外部调用 8 个(bind, new, bail!, assert_eq!, format!, channel, to_value, spawn)。

exec_server_http_request_honors_optional_timeout280–349 ↗
async fn exec_server_http_request_honors_optional_timeout() -> anyhow::Result<()>

作用:测试 timeoutMs 这个超时参数确实是可选的:不填就不要超时,填了很短时间就应该按时失败。这样可以避免慢服务被误杀,也避免需要超时时一直挂住。

数据流:测试先发一个没有超时的 GET,请本地服务器延迟 100 毫秒再回复,最后确认 exec-server 仍然拿到了 slow-success。接着再发一个超时只有 10 毫秒的请求,同样让服务器延迟 100 毫秒,测试期待 exec-server 返回 JSON-RPC 错误。由于客户端超时后可能已经断开连接,测试还会判断写响应时出现的断管、连接重置或意外 EOF 是否属于正常现象。

调用关系:它复用 initialize_exec_server 做启动握手,用 accept_http_request 接住两次 HTTP 请求,用 respond_with_status_and_headers 模拟慢响应;成功路径通过 wait_for_response 检查,失败路径通过 wait_for_error_response 检查,并用 is_expected_peer_disconnect 分辨超时导致的正常断连。

调用图:调用 7 个内部函数(exec_server, accept_http_request, initialize_exec_server, is_expected_peer_disconnect, respond_with_status_and_headers, wait_for_error_response, wait_for_response);外部调用 9 个(from_millis, bind, new, assert!, assert_eq!, format!, to_value, spawn, sleep)。

initialize_exec_server352–367 ↗
async fn initialize_exec_server(server: &mut ExecServerHarness) -> anyhow::Result<()>

作用:完成 exec-server 在测试里必须先做的 JSON-RPC 初始化握手。没有这一步,后面的 executor 方法通常还不能合法调用。

数据流:它接收一个测试用的 exec-server 连接,发送 initialize 请求,里面带客户端名字和空的恢复会话 ID;等到服务器返回初始化响应后,再发一条 initialized 通知。完成后不返回业务数据,只表示服务已经进入可用状态。

调用关系:四个主要测试都会先调用它。它内部把发送请求、等待响应、发送通知这三步串起来,其中等待响应交给 wait_for_response,实际通信由测试框架的 send_requestsend_notification 完成。

调用图:调用 3 个内部函数(send_notification, send_request, wait_for_response);被 4 处调用(exec_server_http_request_buffers_response_body, exec_server_http_request_honors_optional_timeout, exec_server_http_request_rejects_duplicate_stream_request_ids, exec_server_http_request_streams_response_body_notifications);外部调用 2 个(json!, to_value)。

wait_for_response370–389 ↗
async fn wait_for_response(
    server: &mut ExecServerHarness,
    request_id: RequestId,
) -> anyhow::Result<T>

作用:等待某个指定 JSON-RPC 请求 ID 对应的成功响应,并把里面的 JSON 结果转成测试想要的类型。它让测试不用手写一遍遍“找响应、拆响应、反序列化”的代码。

数据流:它拿到 exec-server 连接和请求 ID 后,不断等事件,直到看到 ID 匹配的 JSON-RPC Response。随后取出 result 字段,把 JSON 数据转换成调用者指定的类型;如果等到的不是成功响应,或者转换失败,就返回错误。

调用关系:初始化流程和多个测试场景都会用它来拿成功结果。它把找事件的工作交给 wait_for_event,把 JSON 转 Rust 类型的工作交给 serde_json::from_value

调用图:调用 1 个内部函数(wait_for_event);被 4 处调用(exec_server_http_request_buffers_response_body, exec_server_http_request_honors_optional_timeout, exec_server_http_request_rejects_duplicate_stream_request_ids, initialize_exec_server);外部调用 2 个(bail!, from_value)。

wait_for_error_response392–408 ↗
async fn wait_for_error_response(
    server: &mut ExecServerHarness,
    request_id: RequestId,
) -> anyhow::Result<codex_app_server_protocol::JSONRPCErrorError>

作用:等待某个指定 JSON-RPC 请求 ID 对应的错误响应。它主要用来测试“应该失败”的情况,比如短超时触发错误。

数据流:它接收 exec-server 连接和请求 ID,然后等到 ID 匹配的 JSON-RPC Error 事件。拿到后返回里面的错误对象,让测试检查错误码和错误消息;如果事件类型不对,就把情况当成测试失败。

调用关系:目前由超时测试 exec_server_http_request_honors_optional_timeout 使用。它和 wait_for_response 是一对:一个等成功结果,一个等失败结果,都依赖测试框架的 wait_for_event 来筛选事件。

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

accept_http_request411–446 ↗
async fn accept_http_request(listener: &TcpListener) -> anyhow::Result<CapturedHttpRequest>

作用:在测试里的假 HTTP 服务器上接收一条 HTTP/1.1 请求,并把肉眼可见的请求行、请求头、请求体都抓出来。它让测试能确认 exec-server 真正发到网络上的内容是否正确。

数据流:它从 TCP 监听器等待一个连接,最多等 5 秒。连上后逐行读取第一行请求行,再读取所有请求头,头名统一转成小写方便比较;如果有 content-length,就按长度继续读请求体。最后返回一个 CapturedHttpRequest,里面保留原始连接流,方便后续继续写 HTTP 响应。

调用关系:所有主测试都用它接住 exec-server 发出的真实 HTTP 请求。它是测试中“假网站”的入口,后续通常会把它返回的 stream 交给 respond_with_status_and_headersrespond_with_chunked_bodyrespond_with_chunked_body_until_finish 来回响应。

调用图:被 4 处调用(exec_server_http_request_buffers_response_body, exec_server_http_request_honors_optional_timeout, exec_server_http_request_rejects_duplicate_stream_request_ids, exec_server_http_request_streams_response_body_notifications);外部调用 7 个(new, new, from_secs, new, accept, timeout, vec!)。

respond_with_status_and_headers449–467 ↗
async fn respond_with_status_and_headers(
    mut stream: TcpStream,
    status: &str,
    headers: &[(&str, &str)],
    body: &[u8],
) -> anyhow::Result<()>

作用:向已经接收到的 TCP 连接写回一个普通的、带固定长度响应体的 HTTP 响应。它用于模拟简单服务器回包。

数据流:它拿到连接、状态文本、额外响应头和响应体字节。先拼出 HTTP 状态行、默认 content-typecontent-length、关闭连接头和额外头;然后把这些头部和响应体写进 TCP 流并刷新。完成后返回成功或写入错误。

调用关系:普通缓存响应测试和超时测试会调用它。它通常接在 accept_http_request 后面,用来给 exec-server 一个明确长度的响应,从而测试 exec-server 是否正确读取完整响应体。

调用图:被 2 处调用(exec_server_http_request_buffers_response_body, exec_server_http_request_honors_optional_timeout);外部调用 3 个(flush, write_all, format!)。

is_expected_peer_disconnect469–480 ↗
fn is_expected_peer_disconnect(err: &anyhow::Error) -> bool

作用:判断一个错误是不是测试中可以接受的“对方已经断开连接”。在超时测试里,exec-server 可能先超时关连接,假服务器再写数据时出错,这不代表功能坏了。

数据流:它接收一个错误对象,沿着错误链一层层查看有没有底层的输入输出错误。如果错误类型是断管、连接重置或意外 EOF,就返回 true;否则返回 false。

调用关系:只有超时测试会用它。它帮助 exec_server_http_request_honors_optional_timeout 区分两类情况:因为超时导致的正常断连可以忽略,其他写响应错误则应该让测试失败。

调用图:被 1 处调用(exec_server_http_request_honors_optional_timeout);外部调用 1 个(chain)。

respond_with_chunked_body483–507 ↗
async fn respond_with_chunked_body(
    mut stream: TcpStream,
    headers: &[(&str, &str)],
    chunks: &[&[u8]],
) -> anyhow::Result<()>

作用:写出一个使用 HTTP 分块传输的响应,让 exec-server 必须走流式读取路径。分块传输可以理解成“边做边端上菜”,不是一次把整盘菜端完。

数据流:它拿到连接、额外响应头和若干块字节数据。先写 HTTP 200 响应头,并声明 transfer-encoding: chunked;然后每一块都按 HTTP 分块格式写入长度、内容和换行,并刷新;最后写入长度为 0 的结束块,表示响应体结束。

调用关系:流式响应测试调用它来制造真实的 chunked 响应。exec_server_http_request_streams_response_body_notifications 依靠它触发 exec-server 发送多条 http/request/bodyDelta 通知。

调用图:被 1 处调用(exec_server_http_request_streams_response_body_notifications);外部调用 3 个(flush, write_all, format!)。

respond_with_chunked_body_until_finish510–536 ↗
async fn respond_with_chunked_body_until_finish(
    mut stream: TcpStream,
    headers: &[(&str, &str)],
    chunks: &[&[u8]],
    finish_rx: oneshot::Receiver<()>,
) -> anyhow::Result<()>

作用:写出分块响应,但在最后结束前故意等测试放行。它用来制造一个“响应还没结束”的流式请求,从而测试重复 requestId 会不会被拒绝。

数据流:它先像普通分块响应一样写状态行、响应头和若干数据块,并刷新到连接里。然后等待一个一次性信号;收到信号后才写入 HTTP 分块结束标记并刷新。也就是说,输入的 finish_rx 决定这个响应什么时候真正结束。

调用关系:重复流 ID 测试会把它放到后台任务里运行。测试先不发送结束信号,让第一个流保持活跃;确认重复 ID 被拒绝后,再通过信号让它收尾,之后由 collect_response_body_deltas 收完通知。

调用图:被 1 处调用(exec_server_http_request_rejects_duplicate_stream_request_ids);外部调用 3 个(flush, write_all, format!)。

collect_response_body_deltas539–560 ↗
async fn collect_response_body_deltas(
    server: &mut ExecServerHarness,
    request_id: &str,
) -> anyhow::Result<Vec<HttpRequestBodyDeltaNotification>>

作用:持续收集 exec-server 发来的流式响应体通知,直到看到最后一帧。它把一条条 http/request/bodyDelta 事件整理成一个列表,方便测试检查顺序和内容。

数据流:它接收 exec-server 连接和期望的请求 ID。循环读取下一个 JSON-RPC 事件,要求它必须是 http/request/bodyDelta 通知;再把通知参数转成 HttpRequestBodyDeltaNotification,并确认里面的 request_id 正确。每条都放进数组,直到某条通知的 done 为 true,然后返回全部通知。

调用关系:流式响应测试和重复 ID 测试都会在响应进入流式阶段后调用它。它依赖 next_event 从服务器取消息,并把 JSON 参数解析成通知结构,是测试检查流式 body 的统一收集器。

调用图:调用 1 个内部函数(next_event);被 2 处调用(exec_server_http_request_rejects_duplicate_stream_request_ids, exec_server_http_request_streams_response_body_notifications);外部调用 4 个(new, bail!, assert_eq!, from_value)。

response_header563–568 ↗
fn response_header(headers: &[HttpHeader], name: &str) -> Option<String>

作用:从响应头列表里按名字找一个头的值,而且不在乎大小写。HTTP 头名本来就不区分大小写,所以测试不能因为 X-Testx-test 写法不同就失败。

数据流:它接收一组 HttpHeader 和要找的头名,逐个比较头名,比较时忽略大小写。找到后返回这个头的值;找不到就返回空。

调用关系:主测试用它检查 exec-server 返回的 HTTP 响应头。它是一个小辅助函数,避免每个断言都重复写大小写无关的查找逻辑。

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

Noise 中继和远程传输

这些测试从底层 Noise 分帧和中继机制,延伸到远程环境重连逻辑和完整的中继 exec-server 流量。

exec-server/src/noise_channel_tests.rs源码 ↗
testtest

这个文件专门测试 Noise 加密通道。Noise 是一种加密握手协议,可以让两端先确认“你是谁”,再生成只有双方知道的密钥来传消息。测试里会临时生成发起方和响应方的身份,拼出 prologue(握手前双方都认可的一段上下文,比如环境、注册号、数据流号),然后跑完整握手。它检查双方能互相认证,密文不是明文,解密能还原原文;也检查很多坏情况:响应方密钥不对、上下文不一致、密文被改、同一段密文被重复使用、公钥标错加密套件、握手载荷太大。这样可以防止加密代码看起来能跑,但实际漏掉关键安全边界。

函数细节9
hybrid_ik_roundtrip_authenticates_both_endpoints13–64 ↗
fn hybrid_ik_roundtrip_authenticates_both_endpoints()

作用:测试一整套正常握手流程:发起方和响应方都能确认对方身份,并且握手后能双向加密通信。它证明这条加密通道在最基本、最重要的成功场景下是可用的。

数据流:进去的是两端新生成的身份、公钥、同一份 prologue,以及一段授权数据 → 测试让发起方生成握手请求,响应方读取并确认请求里带着正确的发起方公钥和授权数据,再完成握手 → 出来的是两端各自拿到的加密传输对象;随后明文 request 和 response 被加密成不同于原文的密文,再被另一端成功解回原文。

调用关系:这是测试运行器执行的主成功路径测试。它会用 generate 准备双方身份,用 noise_channel_prologue 准备双方共同认可的上下文,用 start 发起握手,再用 read_request 让响应方接住请求;最后通过断言检查认证和加密收发都成立。

调用图:调用 3 个内部函数(start, generate, read_request);外部调用 3 个(assert_eq!, assert_ne!, noise_channel_prologue)。

initiator_rejects_wrong_responder_key67–84 ↗
fn initiator_rejects_wrong_responder_key()

作用:测试发起方指定了一个期望的响应方公钥时,真正收到请求的另一个响应方不能冒充成功。换句话说,消息不能被拿去给“错的人”解开。

数据流:进去的是发起方身份、一个期望响应方身份、一个实际响应方身份,以及同一份 prologue → 发起方按期望响应方的公钥制作握手请求 → 实际响应方尝试读取这个请求时失败,结果是错误而不是握手成功。

调用关系:测试运行器调用它来覆盖冒名响应方的情况。它先用 generate 造出三套身份,用 noise_channel_prologue 造上下文,再把 start 产生的请求交给错误的接收者,最后用断言确认 read_request 不会放行。

调用图:调用 2 个内部函数(start, generate);外部调用 2 个(assert!, noise_channel_prologue)。

responder_rejects_mismatched_prologue87–103 ↗
fn responder_rejects_mismatched_prologue()

作用:测试双方握手时的上下文必须完全一致。即使身份都对,只要 prologue 不同,也不能建立通道。

数据流:进去的是同一对身份,但发起方和响应方各用一份不同的 prologue,其中数据流编号不同 → 发起方按自己的 prologue 创建请求 → 响应方用另一份 prologue 读取时失败,输出是错误结果。

调用关系:测试运行器用它检查“同一把钥匙不能跨场景乱用”。它调用 generate 准备身份,调用 noise_channel_prologue 做出两份不同上下文,调用 start 生成请求,再确认响应方读取请求时会拒绝。

调用图:调用 2 个内部函数(start, generate);外部调用 2 个(assert!, noise_channel_prologue)。

prologue_encoding_is_stable_and_unambiguous106–117 ↗
fn prologue_encoding_is_stable_and_unambiguous()

作用:测试 prologue 的字节格式是固定且不含糊的。这样不同版本、不同机器生成的上下文才能一致,不会因为字符串拼接歧义导致安全判断出错。

数据流:进去的是环境名、注册名和数据流名三个文本 → noise_channel_prologue 把它们连同协议版本名编码成一串字节,并给每段加上长度 → 测试把生成结果和预期字节逐个比较,确认格式没有悄悄变。

调用关系:测试运行器调用它来保护协议格式不被无意改坏。它只依赖 noise_channel_prologue 生成结果,再用 assert_eq! 把结果钉死在一个明确的字节模板上。

调用图:外部调用 2 个(assert_eq!, noise_channel_prologue)。

transport_rejects_tampered_ciphertext120–146 ↗
fn transport_rejects_tampered_ciphertext()

作用:测试加密后的消息被改动一个字节后,接收方会发现并拒绝。它验证的不只是保密,还包括防篡改。

数据流:进去的是正常生成的双方身份、prologue 和授权数据 → 测试先完成一次正常握手,并让发起方加密 request → 然后故意翻转密文里的一个比特,再交给响应方解密;出来的是解密失败,而不是错误地得到某段明文。

调用关系:这个测试先复用正常握手流程:generate、noise_channel_prologue、start、read_request。建立通道后,它不测正常收发,而是人为破坏密文,再用断言确认传输层不会接受被改过的内容。

调用图:调用 3 个内部函数(start, generate, read_request);外部调用 2 个(assert!, noise_channel_prologue)。

transport_rejects_replayed_ciphertext149–183 ↗
fn transport_rejects_replayed_ciphertext()

作用:测试同一段密文不能被重复使用。第一次收到可以解密,第二次再发同样内容应该被当成可疑消息拒绝。

数据流:进去的是正常握手所需的身份、prologue 和授权数据 → 发起方加密一条 request,响应方第一次解密成功并得到原文 → 同一段密文第二次送进去时,结果变成传输错误,说明系统记住了消息顺序或编号,拒绝重放。

调用关系:测试运行器调用它来覆盖重放攻击。它和正常握手测试一样先调用 generate、noise_channel_prologue、start、read_request 建好通道,再用一次成功解密作为对照,随后用断言确认重复解密会失败。

调用图:调用 3 个内部函数(start, generate, read_request);外部调用 3 个(assert!, assert_eq!, noise_channel_prologue)。

public_key_validation_rejects_unknown_suite186–198 ↗
fn public_key_validation_rejects_unknown_suite()

作用:测试公钥里写着未知加密套件时,不能拿来发起握手。加密套件可以理解为“用哪套加密配方”,配方不认识就不能硬用。

数据流:进去的是一个正常生成的公钥 → 测试先把它转成 JSON,再把其中 suite 字段改成 unknown,然后再读回公钥对象 → 用这个被改过的公钥发起握手时,结果应该是错误。

调用关系:测试运行器用它检查公钥的安全校验。它调用 generate 生成原始身份,借助 to_value、json!、Object 和 from_value 模拟一个外部传来的异常公钥,最后让 start 尝试使用它,并断言会被拒绝。

调用图:调用 1 个内部函数(generate);外部调用 5 个(assert!, Object, from_value, json!, to_value)。

public_key_serializes_with_expected_suite201–209 ↗
fn public_key_serializes_with_expected_suite()

作用:测试公钥导出成 JSON 时,会写上项目期望的加密套件名称。这样别的组件收到公钥后,能知道该按哪套加密规则处理。

数据流:进去的是一个新生成身份的公钥 → 测试把公钥序列化成 JSON → 出来的是 JSON 里的 suite 字段;它必须等于 NOISE_CHANNEL_SUITE 这个固定值。

调用关系:测试运行器调用它来保护对外数据格式。它用 generate 造身份,用 to_value 把公钥变成可检查的 JSON,再用 assert_eq! 确认 suite 字段没有偏离约定。

调用图:调用 1 个内部函数(generate);外部调用 2 个(assert_eq!, to_value)。

initiator_rejects_oversized_handshake_payload212–226 ↗
fn initiator_rejects_oversized_handshake_payload()

作用:测试握手时附带的载荷太大时,发起方会直接拒绝。这样可以避免有人塞入超大数据,把握手流程拖垮或造成资源浪费。

数据流:进去的是两端身份,以及一段长度达到 MAX_MESSAGE_LEN 的零字节载荷 → 测试调用 start 试图把这段数据放进握手请求 → 出来的是 InvalidMessage 错误,错误信息说明握手载荷太大,没有生成请求。

调用关系:测试运行器调用它来检查大小限制。它用 generate 准备身份,用 vec! 构造过大的载荷,然后把数据交给 start,最后用断言确认返回的是预期的错误类型和提示。

调用图:调用 2 个内部函数(start, generate);外部调用 2 个(assert!, vec!)。

exec-server/src/noise_relay/message_framing_tests.rs源码 ↗
testtest

这个文件是测试代码,不是正式运行时直接干活的部分。它像是在检查快递打包规则:一条很大的 JSON-RPC 消息不能一次塞进固定大小的 Noise 传输记录里,所以要先切成多段;收到时再一段段拼回来,最后必须和原消息一模一样。这里还测试了两个防护点:如果对方只声明“我要发一个超大消息”,哪怕还没发正文,也要立刻拒绝;如果单个明文记录本身超过允许大小,也要拒绝。这样做很重要,因为网络协议如果不限制大小,坏客户端可能用超大长度或超大包拖垮服务器。主要部件是 frame_jsonrpc_message(把消息打包)和 JsonRpcMessageDecoder(逐块收包并还原消息)。

函数细节3
fragments_and_reassembles_large_jsonrpc_message12–29 ↗
fn fragments_and_reassembles_large_jsonrpc_message()

作用:这个测试确认:一条很大的 JSON-RPC 通知消息被切成多个 Noise 记录后,接收端还能完整拼回原来的消息。简单说,就是检查“拆开寄”和“收齐再组装”不会丢字、乱序或损坏内容。

数据流:进去的是一个带有 128KB 字符串数据的 JSON-RPC 通知消息。测试先调用 frame_jsonrpc_message 把它编码成适合传输的字节数据,并确认它确实很大;然后按 NOISE_RECORD_PLAINTEXT_LEN 这个每段最大明文长度切块,逐块喂给 JsonRpcMessageDecoder。最后出来的是 decoder 还原出的消息列表,测试要求它正好只包含原来那一条消息。

调用关系:这个测试站在发送端和接收端之间模拟完整流程:先把活交给 frame_jsonrpc_message 做打包,再把每个小块交给 JsonRpcMessageDecoder 的 push 方法做解包。它用断言确认两边配合正常,防止以后改协议或改大小限制时把大消息传输弄坏。

调用图:外部调用 7 个(new, Notification, assert!, assert_eq!, default, json!, frame_jsonrpc_message)。

rejects_declared_message_length_above_limit_without_payload32–41 ↗
fn rejects_declared_message_length_above_limit_without_payload()

作用:这个测试确认:只要消息头里声明的长度超过上限,解码器马上报错,不会等正文真的发过来。这样可以防止别人用一个假的超大长度吓唬或拖垮服务器。

数据流:进去的是 4 个字节,表示一个比 MAX_NOISE_JSONRPC_MESSAGE_LEN 还大 1 的消息长度。测试把这几个字节直接交给默认创建的 JsonRpcMessageDecoder。结果应该是 ExecServerError::Protocol,并且错误文字说明 JSON-RPC 消息长度无效;解码器不会继续等待后续 payload,也不会分配大内存。

调用关系:这个测试只检查接收端解码器的第一道门槛。它不需要调用打包函数,因为它是在模拟一个不可信对方直接发来坏长度的情况,用来保证 JsonRpcMessageDecoder 在刚看到长度字段时就能拦住问题。

调用图:外部调用 2 个(assert!, default)。

rejects_oversized_plaintext_record44–52 ↗
fn rejects_oversized_plaintext_record()

作用:这个测试确认:单个 Noise 明文记录如果比允许的最大长度还大,解码器会拒绝。也就是说,每一小包本身也要守规矩,不能因为总消息还没解析就放过超大输入。

数据流:进去的是一段全是 0 的字节数组,长度是 NOISE_RECORD_PLAINTEXT_LEN 加 1,刚好超过单个明文记录的上限。测试把它交给默认创建的 JsonRpcMessageDecoder。出来的结果应是协议错误,并带有“plaintext record exceeds maximum length”这类明确提示;解码器不会尝试把这段超大数据当正常消息处理。

调用关系:这个测试模拟接收端收到一个过大的传输块。它直接调用 JsonRpcMessageDecoder 的 push 方法,确认解码器在真正解析 JSON-RPC 消息前,先检查每个传输记录的大小,作为协议安全边界的一部分。

调用图:外部调用 2 个(assert!, default)。

exec-server/src/noise_relay/ordered_ciphertext_tests.rs源码 ↗
testtest run

这个文件是测试用的,不是正式处理网络数据的代码。它检查 OrderedCiphertextFrames 这个小组件:收到带序号的密文后,只有从当前期待的序号开始连续齐了,才把它们一次性放出来。可以把它想成电影院检票口:2 号观众先来了也要等 1 号,不能跳着进场。这里还测了两个重要边界:重复来的同一个序号不能覆盖之前已经存下来的内容;以及乱序缓存不能无限变大,否则别人可以故意发很靠后的编号或超大的数据,把内存塞爆。MAX_PENDING_BYTES 就是允许暂存数据的大小上限。

函数细节3
releases_ciphertexts_only_in_nonce_order7–18 ↗
fn releases_ciphertexts_only_in_nonce_order()

作用:这个测试确认密文只能按序号顺序放出来。即使编号 1 先到,也必须等编号 0 到了以后,才能一起交给后续处理。

数据流:进去的是一个空的 OrderedCiphertextFrames 队列,以及两段带序号的字节数据:先放入序号 1 的“second”,再放入序号 0 的“first”。第一次放入后,因为缺少序号 0,所以出来的是空列表;第二次放入后,队列发现 0 和 1 都连续齐了,就按“first”“second”的顺序输出。

调用关系:它直接创建默认队列,然后调用队列的 push 行为,并用 assert_eq! 检查结果是否符合预期。它验证的是整个排序缓存最核心的承诺:外面即使乱序送进来,里面也只能按正确顺序吐出去。

调用图:外部调用 2 个(assert_eq!, default)。

ignores_duplicate_ciphertexts_without_replacing_buffered_record21–40 ↗
fn ignores_duplicate_ciphertexts_without_replacing_buffered_record()

作用:这个测试确认重复序号不会覆盖已经缓存的密文。也就是说,第一次收到的那份会被保留,后来的同号“替换品”会被忽略。

数据流:进去的是一个空队列。先放入序号 1 的“first copy”,它被暂存但不输出;再放入同样序号 1 的“replacement”,队列不让它替换原来的内容,也不输出;之后放入序号 0 的“zero”,队列终于能连续输出 0 和 1,于是结果是“zero”和最早那份“first copy”。最后再放一次序号 0 的“duplicate”,因为它已经处理过了,所以输出空列表。

调用关系:它也是围绕 OrderedCiphertextFrames 的 push 行为做检查,用 assert_eq! 对比输出。它补上了乱序场景里的另一个安全细节:重复包不能改变已经等待释放的数据,否则后来的重复数据可能悄悄篡改结果。

调用图:外部调用 2 个(assert_eq!, default)。

rejects_unbounded_reordering43–52 ↗
fn rejects_unbounded_reordering()

作用:这个测试确认队列不会接受无限乱序或超大缓存。它防止有人发很远的序号或特别大的数据,让程序一直攒着不释放,最终耗光内存。

数据流:进去的是一个空队列。第一次尝试放入序号 65 的空数据,因为它离当前期待的序号太远,push 返回错误;第二次尝试放入序号 1、但内容大小超过 MAX_PENDING_BYTES 的数据,也返回错误。测试用 assert! 确认这两种情况都会失败。

调用关系:它创建默认队列后,把两个危险输入交给 push,并只关心是否报错。它在整体测试里负责检查资源保护这条底线:排序缓存可以临时等一等,但不能无限等、无限存。

调用图:外部调用 2 个(assert!, default)。

exec-server/src/noise_relay/executor_stream_tests.rs源码 ↗
testtest run

这个测试像是在搭一个小型演练场:先临时生成两边的加密身份,一边假装是执行器,另一边假装是测试工具,然后完成一次 Noise 握手。Noise 可以理解成一套“先确认身份、再加密说话”的通信办法。接着它启动一条虚拟流,并准备一个通道来接收“流已关闭”的消息。测试工具把一条 JSON-RPC 响应消息加密后塞进这条流里。JSON-RPC 是一种用 JSON 格式表达请求和响应的通信约定。这里刻意发的是响应而不是新请求,目的是让执行器端处理完后退出。最后测试等待最多 1 秒,确认真的收到了 ClosedNoiseVirtualStream,而且里面的 stream_id 和 instance_id 都对。这个文件的重要点不在业务功能,而在防止一个很隐蔽的故障:处理器结束了,但虚拟流关闭事件没有上报。

函数细节1
processor_exit_reports_closed_virtual_stream22–70 ↗
async fn processor_exit_reports_closed_virtual_stream() -> Result<()>

作用:这个测试函数检查:当虚拟流里的处理器结束时,系统会不会把“流关闭了”这件事报告出来。有人改动加密流、连接处理器或关闭通知机制时,可以靠它发现是否破坏了这个保证。

数据流:进去的是测试里临时造出来的两套通信身份、一个测试用握手前缀、一个虚拟流编号和实例编号。它先让两端完成加密握手,再创建虚拟流和一个用来接关闭通知的通道;然后把一条 JSON-RPC 响应消息打包、加密,作为 RelayData 喂给虚拟流。出来的结果不是普通返回值,而是断言:关闭通知通道里必须在 1 秒内收到一条 ClosedNoiseVirtualStream,并且 stream_id 是 “stream-1”、instance_id 是 7;如果没有收到或内容不对,测试失败。

调用关系:它是由 Tokio 测试运行器在测试阶段调用的异步测试。它会调用 NoiseChannelIdentity::generate 生成双方身份,用 InitiatorHandshake::start 和 PendingResponderHandshake::read_request/complete 模拟握手,用 ExecServerRuntimePaths::new 和 ConnectionProcessor::new 准备执行器侧处理环境,再用 spawn_noise_virtual_stream 启动被测的虚拟流。消息部分交给 frame_jsonrpc_message 打包,再交给加密传输对象加密,最后通过 stream.receive_data 触发整个关闭流程,并从 closed_stream_rx 里观察结果。

调用图:调用 6 个内部函数(start, generate, read_request, frame_jsonrpc_message, new, new);外部调用 6 个(Response, Integer, assert!, channel, current_exe, spawn_noise_virtual_stream)。

exec-server/src/relay_noise_tests.rs源码 ↗
test测试运行期间

这里测试的是“中继”里的握手阶段。可以把一条 WebSocket 连接想成一根总水管,里面分出很多小水管,每个小水管叫一个 stream。每个 stream 开始前都要做 Noise 握手,也就是双方先确认身份并准备加密通信。文件里造了一个假的认证器 BlockingValidator:它会记录自己被叫了几次,但故意卡住不返回,用来模拟“认证服务很慢”。测试会启动一个本地 WebSocket,把一边当成环境服务,另一边当成 harness 客户端,然后发送各种握手帧。重点检查几件事:一个 stream 的认证卡住时,新的 stream 还能继续开始;同一个 stream 重复握手会被 reset,次数太多会关闭整条连接;认证字符串太大时,服务器会在真正认证前直接拒绝;坏掉的握手包或过早发来的数据,累计超过失败预算后也会断开物理连接。这样能保证服务器既不太脆弱,也不会无限容忍异常输入。

函数细节6
BlockingValidator::validate_harness_key43–55 ↗
fn validate_harness_key(
        &self,
        _harness_public_key: &NoiseChannelPublicKey,
        _authorization: &str,
    ) -> impl std::future::Future<Output = Result<(), ExecServerError>> + Sen

作用:这是测试用的假认证函数。它不像真实认证那样立刻判断 key 对不对,而是先把调用次数加一,然后一直等别人放行,用来制造“认证还没完成”的场景。

数据流:进去的是 harness 的公钥和认证字符串,但这个假实现并不真的检查它们。它读取共享的计数器和通知器,把计数器加一,然后等待通知;等通知来了,就返回成功。结果是测试可以知道认证被触发了几次,也可以故意让认证悬在那里不结束。

调用关系:它实现了 HarnessKeyValidator,所以 run_multiplexed_environment 在收到握手后会调用它。各个测试把 BlockingValidator 交给环境任务,用它来观察握手是否进入认证阶段,以及认证卡住时中继的其他行为是否正常。

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

pending_harness_key_validation_does_not_block_new_handshakes59–109 ↗
async fn pending_harness_key_validation_does_not_block_new_handshakes() -> Result<()>

作用:这个测试确认:第一个握手的认证如果卡住了,不应该挡住第二个 stream 的握手。也就是说,一条连接里某个小通道慢了,不能把其他小通道也堵死。

数据流:它先在本机开一个临时 TCP/WebSocket 连接,生成环境端和 harness 端的 Noise 身份,然后启动中继环境,并交给它一个会卡住的 BlockingValidator。接着它连续发送两个不同 stream 的握手帧。最后它等计数器变成 2,证明两个握手都进入了认证流程;随后关闭客户端连接,等待环境任务正常结束。

调用关系:这个函数由 tokio 的异步测试框架启动。它用 NoiseChannelIdentity::generate 造身份,用 InitiatorHandshake::start 造握手请求,用 noise_channel_prologue 绑定环境、注册号和 stream,用 encode_relay_message_frame 包成中继帧,再把真正的运行交给 run_multiplexed_environment。

调用图:调用 6 个内部函数(start, generate, noise_channel_prologue, encode_relay_message_frame, new, new);外部调用 16 个(clone, new, new, from_secs, new, bind, handshake, format!, current_exe, run_multiplexed_environment (+6 more))。

duplicate_handshakes_exhaust_failure_budget112–206 ↗
async fn duplicate_handshakes_exhaust_failure_budget() -> Result<()>

作用:这个测试检查同一个 stream 反复发送握手时,服务器会先用 reset 拒绝这些重复请求,次数到达上限后会关闭整条 WebSocket 连接。它防止恶意客户端靠重复握手无限消耗服务器。

数据流:它建立本地 WebSocket,生成双方身份,启动一个认证会卡住的环境任务。然后它为同一个 stream 生成一份握手帧,先发一次让认证挂起,再不断重发同一帧。每次重复握手后,它从 WebSocket 里读回 reset 帧,并检查 reset 的 stream_id 和类型是否正确。重复失败次数达到预算后,它再发一次,等待环境任务结束,说明物理连接已经被关闭。

调用关系:这个测试同样由异步测试框架调用。它把帧编码后发给 run_multiplexed_environment,收到回应后用 decode_relay_message_frame 解回中继帧,再用 validate 判断是不是 reset。它围绕 MAX_FAILED_NOISE_HANDSHAKES 这个失败预算,验证中继层的限流和断连规则。

调用图:调用 7 个内部函数(start, generate, noise_channel_prologue, decode_relay_message_frame, encode_relay_message_frame, new, new);外部调用 18 个(clone, new, new, from_secs, new, bind, bail!, assert_eq!, handshake, format! (+8 more))。

oversized_harness_authorization_is_rejected_before_validation209–262 ↗
async fn oversized_harness_authorization_is_rejected_before_validation() -> Result<()>

作用:这个测试确认:如果握手里带的认证字符串太大,服务器会在调用认证器之前就拒绝它。这样可以避免别人塞一个超大字段,逼服务器做没必要的认证工作。

数据流:它启动本地 WebSocket 和环境任务,准备一个长度超过 MAX_HARNESS_KEY_AUTHORIZATION_BYTES 的认证内容,并用它生成握手请求。发出握手帧后,它等待服务器返回二进制 reset 帧,解码并确认类型是 reset。最后它检查 BlockingValidator 的调用次数仍然是 0,说明请求在真正认证前就被挡下来了。

调用关系:这个测试连接了握手生成、帧编码、环境处理和帧解码几步。InitiatorHandshake::start 负责造出带超大认证内容的握手,run_multiplexed_environment 负责处理,decode_relay_message_frame 负责把服务器回包拆开检查。

调用图:调用 7 个内部函数(start, generate, noise_channel_prologue, decode_relay_message_frame, encode_relay_message_frame, new, new);外部调用 18 个(clone, new, new, from_secs, new, bind, bail!, assert_eq!, handshake, format! (+8 more))。

repeated_malformed_handshakes_close_the_physical_relay265–309 ↗
async fn repeated_malformed_handshakes_close_the_physical_relay() -> Result<()>

作用:这个测试检查:连续发送损坏的握手包,服务器不会一直陪它耗,而是在失败次数达到上限后关闭整条中继连接。这里的“损坏”就是故意把握手请求最后一个字节改坏。

数据流:它建立本地 WebSocket,生成身份并启动环境任务。然后循环多次,每次创建一个新的 stream 握手请求,再把请求的最后一个字节翻转,让它变成无法通过 Noise 校验的坏包。它把这些坏握手发给服务器。循环次数达到失败预算后,它等待环境任务结束,结果表示服务器已经关闭物理 relay。

调用关系:这个测试由测试框架触发,主要把 InitiatorHandshake::start 生成的正常请求改坏,再通过 encode_relay_message_frame 发入 run_multiplexed_environment。它验证的是中继环境面对解析或加密校验失败时的整体断连策略。

调用图:调用 6 个内部函数(start, generate, noise_channel_prologue, encode_relay_message_frame, new, new);外部调用 14 个(new, new, from_secs, new, bind, handshake, format!, current_exe, run_multiplexed_environment, spawn (+4 more))。

repeated_early_data_during_validation_closes_the_physical_relay312–358 ↗
async fn repeated_early_data_during_validation_closes_the_physical_relay() -> Result<()>

作用:这个测试确认:如果客户端在握手认证还没完成时就急着发数据,反复这么做会被当成严重错误,最终关闭整条连接。它防止客户端跳过“先握手再传数据”的规矩。

数据流:它先启动本地 WebSocket 和环境任务,并使用会卡住的 BlockingValidator,让每个握手都停在认证中。然后循环创建多个 stream:每个 stream 先发送握手帧,紧接着马上发送一帧普通数据。因为此时握手还没完成,这些数据都属于过早数据。发送次数达到失败预算后,它等待环境任务结束,说明服务器关闭了物理连接。

调用关系:这个测试把 RelayMessageFrame::handshake 和 RelayMessageFrame::data 组合起来,模拟“不等握手完成就发业务数据”的客户端。run_multiplexed_environment 接收这些帧并执行规则,测试只关心最后是否按 MAX_FAILED_NOISE_HANDSHAKES 的限制断开连接。

调用图:调用 6 个内部函数(start, generate, noise_channel_prologue, encode_relay_message_frame, new, new);外部调用 16 个(new, new, from_secs, new, bind, data, handshake, format!, current_exe, run_multiplexed_environment (+6 more))。

exec-server/src/remote/noise_tests.rs源码 ↗
testtest time

这个文件不是真正上线运行的功能代码,而是给远程连接流程做“安全体检”。这里的 Noise 可以理解成一条加密通信通道,中继服务器像中转站,执行服务器要先向环境注册服务登记,再去连这个中转站。测试会搭一个假的注册服务和假的 WebSocket 服务,像排练一样模拟正常断线、被拒绝、校验失败等场景。重点有三件事:第一,普通断线后应该继续用原来的注册链接,不要没必要地重新登记;第二,如果连接地址被服务器用 4xx 这类“明确拒绝”回应打回来,就要丢掉旧登记,重新走注册;第三,校验 harness key,也就是对端身份授权信息时,必须收到明确有效的答复才算通过,而且就算注册服务报错,也不能把敏感授权内容写进错误提示里。

函数细节5
StaticRegistryAuthProvider::add_auth_headers30–35 ↗
fn add_auth_headers(&self, headers: &mut HeaderMap)

作用:这个函数给发往假注册服务的 HTTP 请求加上固定的认证头。简单说,它像是在测试请求上贴一张固定的“通行证”,让测试服务器能确认请求带了正确身份。

数据流:进去的是一组还没补完的 HTTP 请求头 → 它往里面插入 Authorization: Bearer registry-token 这个固定值 → 出来时请求头被改过,后续发请求时就会带上这个测试用令牌。

调用关系:它是测试用认证提供者 StaticRegistryAuthProvider 的核心动作。后面的测试通过 static_registry_auth_provider 拿到它,再交给环境注册客户端使用;注册客户端真正发请求前会调用它来补认证信息。

调用图:外部调用 2 个(insert, from_static)。

static_registry_auth_provider38–40 ↗
fn static_registry_auth_provider() -> SharedAuthProvider

作用:这个函数创建一个测试用的认证提供者,方便多个测试重复使用同一套固定认证方式。它避免每个测试都手写一遍“怎么加认证头”。

数据流:进去不需要参数 → 它新建一个 StaticRegistryAuthProvider,并用可共享的指针包起来 → 出来的是一个可以传给注册客户端或远程环境配置的认证对象。

调用关系:它被三个测试使用:重连注册测试、harness key 明确拒绝测试、以及错误信息不泄密测试。它把认证这件小事封装起来,让这些测试可以专心检查连接和安全规则。

调用图:被 3 处调用(reconnect_reuses_registration_until_url_is_rejected, validate_harness_key_does_not_expose_error_body, validate_harness_key_requires_explicit_valid_response);外部调用 1 个(new)。

reconnect_reuses_registration_until_url_is_rejected43–93 ↗
async fn reconnect_reuses_registration_until_url_is_rejected() -> Result<()>

作用:这个测试确认远程环境断线重连时不会乱重新注册,但如果连接地址被明确拒绝,就会丢掉旧注册再注册。它保护的是连接状态的正确性,避免系统一断线就反复登记,或被拒绝后还死抱着旧地址不放。

数据流:开始时它启动一个本地 TCP 监听器当假的中继地址,又启动一个假的环境注册服务 → 注册服务被设置成最多收到两次注册请求,并返回同一个中继地址和注册编号 → 测试启动远程环境任务,让它先成功连上 WebSocket,然后主动关闭连接 → 第二次连接时,测试假装中继返回 HTTP 401 拒绝 → 之后远程环境应该重新注册并再次连接 → 最后测试确认注册服务确实被调用了预期次数,并停止后台任务。

调用关系:这个测试会调用 static_registry_auth_provider 准备认证,也会创建 RemoteEnvironmentConfig 并启动 run_remote_environment。它像导演一样安排假注册服务、假中继服务器和远程环境任务互动,用这些互动来验证重连策略。

调用图:调用 3 个内部函数(new, static_registry_auth_provider, new);外部调用 13 个(from_secs, given, start, new, bind, format!, json!, current_exe, spawn, timeout (+3 more))。

validate_harness_key_requires_explicit_valid_response96–131 ↗
async fn validate_harness_key_requires_explicit_valid_response()

作用:这个测试确认 harness key 的校验必须得到“明确有效”的回答才算成功。即使注册服务正常返回 HTTP 200,只要内容说 valid: false,系统也必须拒绝,不能侥幸放行。

数据流:开始时它启动假的注册服务,并生成一个测试用的 Noise 公钥 → 假服务被设置为只接受带正确认证头、注册编号、公钥和授权字符串的校验请求,然后返回 { valid: false } → 测试创建注册客户端和 key 校验器,发起校验 → 结果必须是错误,并且错误内容必须表明注册服务拒绝了这个 harness key。

调用关系:它使用 static_registry_auth_provider 给客户端加认证,使用 EnvironmentRegistryClient 访问假注册服务,再通过 RegistryHarnessKeyValidator::validate_harness_key 执行真正的校验。它重点验证校验器在“服务明确说无效”时会安全地失败。

调用图:调用 3 个内部函数(generate, new, static_registry_auth_provider);外部调用 9 个(given, start, new, assert!, json!, body_partial_json, header, method, path)。

validate_harness_key_does_not_expose_error_body134–163 ↗
async fn validate_harness_key_does_not_expose_error_body()

作用:这个测试确认注册服务报错时,错误信息里不会泄露敏感的 harness key 授权字符串。它防的是一种常见安全事故:把服务器返回的原始错误内容直接打印出来,结果把秘密写进日志或界面。

数据流:开始时它启动假的注册服务,并生成一个测试用公钥 → 假服务在校验接口上返回 HTTP 500,而且响应正文故意放入敏感授权字符串 → 测试发起 harness key 校验并期待失败 → 然后把错误转成文字检查,确认里面不包含那段敏感字符串,同时确认错误被归类为注册服务校验失败。

调用关系:它同样通过 static_registry_auth_provider 创建带认证能力的注册客户端,再把客户端交给 RegistryHarnessKeyValidator。它和前一个校验测试互补:一个检查“拒绝时不能放行”,另一个检查“报错时不能泄密”。

调用图:调用 3 个内部函数(generate, new, static_registry_auth_provider);外部调用 6 个(given, start, new, assert!, method, path)。

exec-server/tests/relay.rs源码 ↗
testtest run

这个测试文件像一个小型演练场:它假装有注册中心、假装有中继服务器,再启动真实的远程执行环境和客户端,看它们能不能隔着这层“转发站”正常说话。这里的重点是 Noise 加密通道,Noise 可以理解成双方先握手、再用密文聊天的一套安全协议。测试先检查一个失败场景:如果连接资料获取失败,系统下一次连接时必须重新获取,而不是偷懒复用旧结果。另一个大测试搭了一个假的 WebSocket 中继,把环境端和客户端端发来的二进制消息原样转发,同时偷偷记录这些消息。测试会让客户端执行 true 命令,再读一个较大的文件,确认功能可用。最后它解开外层中继消息格式,但不解密内容,检查里面看不到 initializeprocess/start 等明文,证明中继只是在搬密文,不知道双方真正聊了什么。

函数细节10
StaticRegistryAuthProvider::add_auth_headers67–72 ↗
fn add_auth_headers(&self, headers: &mut HeaderMap)

作用:给发往假注册中心的 HTTP 请求加上固定的认证头。简单说,就是给请求贴上一张“我有权限”的票。

数据流:进去的是一组还可以修改的 HTTP 请求头 → 它往里面放入 Authorization: Bearer registry-token → 出来时请求头被改过,后面的假注册中心就能按这个固定令牌识别请求。

调用关系:这个方法属于测试里的固定认证提供者。static_registry_auth_provider 会把这个提供者包装起来,远程环境注册和校验自己时会间接用到它,这样测试里的 wiremock 假注册中心才能验证请求确实带了认证信息。

调用图:外部调用 2 个(insert, from_static)。

FailingNoiseConnectProvider::connect_bundle80–91 ↗
fn connect_bundle(
        &self,
        _: NoiseChannelPublicKey,
    ) -> BoxFuture<'_, Result<NoiseRendezvousConnectBundle, ExecServerError>>

作用:故意模拟“拿不到 Noise 中继连接资料”的失败情况。它的价值是测试系统失败后会不会重新尝试,而不是缓存坏结果。

数据流:进去的是对方的 Noise 公钥,但这个测试实现不使用它 → 它先把尝试次数加一 → 然后返回一个协议错误,错误文字固定为 test registry connect failure

调用关系:它被放进 noise_environment_refreshes_bundle_for_each_connection_attempt 创建的测试环境里。每次后端尝试启动进程时,都会来这里拿连接包并失败;测试最后通过计数器确认它被调用了两次。

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

static_registry_auth_provider94–96 ↗
fn static_registry_auth_provider() -> codex_api::SharedAuthProvider

作用:创建一个共享的固定认证提供者,供测试里的远程环境访问假注册中心时使用。

数据流:进去没有参数 → 它新建一个 StaticRegistryAuthProvider,再用 Arc 包起来;Arc 可以理解成多人共用同一个对象的安全引用 → 出来的是注册中心客户端可使用的共享认证对象。

调用关系:它只在 remote_environment_routes_encrypted_exec_server_rpc 这个大测试里被调用。这个认证对象随后进入 RemoteEnvironmentConfig,让远程环境注册和校验时都带上正确的测试令牌。

调用图:被 1 处调用(remote_environment_routes_encrypted_exec_server_rpc);外部调用 1 个(new)。

noise_environment_refreshes_bundle_for_each_connection_attempt99–135 ↗
async fn noise_environment_refreshes_bundle_for_each_connection_attempt() -> Result<()>

作用:测试 Noise 环境每次连接失败后,下一次连接都会重新获取连接资料。这样可以避免网络或注册中心短暂出错后,系统一直卡在旧的失败结果上。

数据流:进去没有外部输入,测试自己创建一个尝试次数计数器和一个空环境管理器 → 它把一个总是失败的连接提供者塞进环境,再连续两次启动同一个后端里的进程 → 两次都应该得到同样的协议错误,同时计数器最后必须是 2,说明确实试了两次。

调用关系:这是一个独立的异步测试。它使用 FailingNoiseConnectProvider::connect_bundle 来制造失败,并通过执行后端的 start 入口触发连接流程;最后用断言检查系统没有把第一次失败缓存起来。

调用图:调用 3 个内部函数(without_environments, new, from_path);外部调用 9 个(clone, new, new, new, assert!, assert_eq!, format!, current_dir, vec!)。

remote_environment_routes_encrypted_exec_server_rpc138–250 ↗
async fn remote_environment_routes_encrypted_exec_server_rpc() -> Result<()>

作用:这是本文件最完整的端到端测试:确认远程环境、客户端、假注册中心、假中继站一起工作时,执行命令和读文件都能成功,并且中继站看到的是密文。

数据流:进去没有外部输入,测试自己启动 TCP 监听、wiremock 假注册中心和远程环境任务 → 它接受环境端 WebSocket,取出远程环境注册时提交的公钥,再创建客户端连接;随后让一个代理任务在客户端和环境端之间转发所有帧并记录它们 → 出来时客户端成功执行 true,成功读取一个 128KB 文件,记录到的中继数据也通过加密检查;最后测试主动停止后台任务。

调用关系:这个测试串起了本文件几乎所有辅助函数。它用 static_registry_auth_provider 准备认证,用 accept_websocket 接住两端连接,用 registered_executor_public_key 从注册请求里拿公钥,用 proxy_relay_frames 假装中继站搬运消息,最后调用 assert_relay_data_is_encrypted 确认搬运过程中没有泄露明文。

调用图:调用 11 个内部函数(generate, from, new, new, current_test_binary_helper_paths, accept_websocket, assert_relay_data_is_encrypted, proxy_relay_frames, registered_executor_public_key, static_registry_auth_provider (+1 more));外部调用 23 个(clone, new, new, given, start, new, new, bind, new, new (+13 more))。

accept_websocket252–263 ↗
async fn accept_websocket(
    listener: &TcpListener,
    role: &str,
) -> Result<WebSocketStream<TcpStream>>

作用:在测试里的假中继服务器上等待一方连进来,并把普通 TCP 连接升级成 WebSocket 连接。WebSocket 可以理解成一条持续打开、双方都能随时发消息的通道。

数据流:进去的是 TCP 监听器和一个角色名,比如 environmentharness → 它在限定时间内等待连接,再完成 WebSocket 握手 → 出来的是可收发 WebSocket 消息的连接;如果超时或握手失败,就返回带说明的错误。

调用关系:它被 remote_environment_routes_encrypted_exec_server_rpc 调用两次:一次接住远程环境端,一次接住测试客户端端。拿到这两条连接后,测试才可以交给 proxy_relay_frames 来模拟真正的中继转发。

调用图:被 1 处调用(remote_environment_routes_encrypted_exec_server_rpc);外部调用 3 个(accept, timeout, accept_async)。

registered_executor_public_key265–277 ↗
async fn registered_executor_public_key(registry: &MockServer) -> Result<NoiseChannelPublicKey>

作用:从假注册中心收到的注册请求里,找出远程执行器上报的 Noise 公钥。客户端需要这个公钥,才能和远程环境建立加密通道。

数据流:进去的是 wiremock 假注册中心 → 它读取已经收到的 HTTP 请求,找到路径以 /register 结尾的那一条,把请求体当 JSON 解析,再取出 executor_public_key 字段 → 出来的是可用于连接参数的 NoiseChannelPublicKey

调用关系:它在 remote_environment_routes_encrypted_exec_server_rpc 中使用。远程环境先向假注册中心注册自己,测试随后用这个函数把注册时的公钥挖出来,再把公钥放进客户端的连接包里,让双方能完成加密握手。

调用图:被 1 处调用(remote_environment_routes_encrypted_exec_server_rpc);外部调用 3 个(received_requests, from_slice, from_value)。

proxy_relay_frames279–305 ↗
async fn proxy_relay_frames(
    mut environment: WebSocketStream<TcpStream>,
    mut harness: WebSocketStream<TcpStream>,
    captured_frames: Arc<Mutex<Vec<Vec<u8>>>>,
) -> Result<()>

作用:扮演一个最简单的中继站:环境端发来的消息转给客户端端,客户端端发来的消息转给环境端。它不理解消息内容,只负责搬运。

数据流:进去的是环境端 WebSocket、客户端端 WebSocket,以及一个用来保存原始二进制帧的共享列表 → 它在循环里同时等两边消息,哪边来了就先记录二进制内容,再原样发给另一边 → 出来通常是连接关闭后返回成功;过程中共享列表会积累所有经过中继的二进制数据。

调用关系:它由 remote_environment_routes_encrypted_exec_server_rpc 启动成后台任务。每收到一条消息,它会调用 capture_binary_frame 保存可检查的原始数据;测试完成后,这些数据会交给 assert_relay_data_is_encrypted 判断是否泄露明文。

调用图:被 1 处调用(remote_environment_routes_encrypted_exec_server_rpc);外部调用 1 个(select!)。

capture_binary_frame307–314 ↗
fn capture_binary_frame(captured_frames: &Mutex<Vec<Vec<u8>>>, message: &Message)

作用:把 WebSocket 消息里的二进制帧保存下来,方便测试结束后检查中继站看到的内容。非二进制消息会被忽略。

数据流:进去的是共享的帧列表和一条 WebSocket 消息 → 如果消息是二进制,它就加锁打开列表,把字节复制进去;如果不是二进制,就什么也不做 → 出来没有返回值,但共享列表可能多了一条记录。

调用关系:它服务于 proxy_relay_frames。中继代理每转发一条消息前都会让它看一眼,这样既不影响转发,又能留下证据给后面的加密断言使用。

assert_relay_data_is_encrypted316–337 ↗
fn assert_relay_data_is_encrypted(captured_frames: &Mutex<Vec<Vec<u8>>>) -> Result<()>

作用:检查中继站记录到的数据看起来确实是加密后的内容,而不是能直接读懂的请求文字。它不是破解加密,而是确认常见明文关键词没有裸露出来。

数据流:进去的是保存了中继二进制帧的共享列表 → 它加锁读取所有帧,把每一帧按 RelayMessageFrame 这种外层消息格式解码;遇到真正的数据帧时,把载荷当作可能的文字查看,确认里面不包含 initializeprocess/startnoise-relay-test 等明文 → 出来是成功或断言失败;同时要求至少看到 4 个数据帧,避免测试根本没传数据却误判通过。

调用关系:它在 remote_environment_routes_encrypted_exec_server_rpc 的命令执行和文件读取都完成后调用。前面的 proxy_relay_framescapture_binary_frame 负责留下原始中继数据,这个函数负责做最后的安全检查。

调用图:被 1 处调用(remote_environment_routes_encrypted_exec_server_rpc);外部调用 3 个(from_utf8_lossy, assert!, decode)。

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

远程控制通道里,一条消息可能太大,不能直接一次发完,所以系统会把它切成小块;另一端收到后,要像拼拼图一样把小块拼回原消息。这个测试文件就是给这套“切开再拼回”的机制做安全检查。它会造出假的客户端消息和服务器消息,确认客户端分片能正确重组,服务器大消息会被拆成不超过大小限制的小包。它还专门测试一些容易出事故的情况:某个流被取消后,没拼完的旧消息不能继续生效;同一个客户端换了新流后,旧流剩下的碎片不能污染新流;过期的碎片、空的坏碎片、重复的坏碎片,都应该被丢掉,但不能害得当前正在拼的正常消息失败。最后的 chunk_envelope 是一个小工具,用来快速造出“客户端发来的某一片消息”。

函数细节8
reassembles_client_message_chunks20–70 ↗
fn reassembles_client_message_chunks()

作用:这个测试确认:客户端把一条 JSON-RPC 消息拆成两片发来时,系统能把它们重新拼成原来的完整消息。JSON-RPC 可以理解成一种用 JSON 格式表达请求和通知的通信格式。

数据流:进去的是一条假的 initialized 通知,它先被转成原始字节,再人为切成前后两半。测试把第一半交给重组器,应该只得到“还在等待”;再把第二半交给重组器,应该得到完整消息。出来的结果要保留原来的 client_id、stream_id、seq_id,并且消息内容和一开始那条通知完全一样。

调用关系:它是最基础的拼接成功案例。测试中会用 chunk_envelope 造出分片外壳,再把这些外壳交给 ClientSegmentReassembler 的 observe 方法,最后用断言检查重组器有没有把拼好的消息往后传。

调用图:调用 1 个内部函数(chunk_envelope);外部调用 8 个(Notification, new, new, default, assert!, assert_eq!, panic!, to_vec)。

splits_large_server_messages_into_wire_chunks73–105 ↗
fn splits_large_server_messages_into_wire_chunks()

作用:这个测试确认:服务器要发的消息很大时,系统会自动把它拆成多个能在线路上传的小块。这样可以避免单个网络包或协议消息超过限制。

数据流:进去的是一个服务器发给客户端的配置警告通知,里面故意放了很长的 summary 文本,让它超过单片限制。测试调用 split_server_envelope_for_transport 后,出来应该是多个分片;每个分片都应该是 ServerMessageChunk,序号 seq_id 仍然保持为 9,并且每片序列化成 JSON 后都不超过 REMOTE_CONTROL_SEGMENT_MAX_BYTES 这个最大字节数。

调用关系:它直接验证 split_server_envelope_for_transport 这个拆包函数。这个测试站在“消息准备发到传输层之前”的位置,检查大消息是否已经被切成安全大小,避免真正发送时出问题。

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

invalidates_incomplete_stream_assemblies108–144 ↗
fn invalidates_incomplete_stream_assemblies()

作用:这个测试确认:某个通信流已经被判定失效后,之前没拼完的消息不能再被后续碎片补齐。简单说,就是“已经作废的拼图不能突然又拼成有效消息”。

数据流:进去的是一条被切成两半的客户端通知。测试先送入第一半,重组器返回 Pending,表示还差一半;然后调用 invalidate_stream,把这个 client_id 和 stream_id 对应的未完成拼接作废。之后再送入第二半,出来应该是 Dropped,也就是被丢弃,而不是拼成完整消息。

调用关系:它验证 ClientSegmentReassembler 在流被取消或重置时的清理行为。这个场景通常发生在远程控制连接切换、关闭或重新开始时,确保旧数据不会在之后误伤新流程。

调用图:外部调用 6 个(Notification, new, new, default, assert!, to_vec)。

resets_incomplete_client_assembly_when_stream_changes147–213 ↗
fn resets_incomplete_client_assembly_when_stream_changes()

作用:这个测试确认:同一个客户端换了新的 stream_id 后,旧流里没拼完的消息会被替换掉,不会和新流的数据混在一起。stream_id 可以理解成一次通信会话的编号。

数据流:进去的是同一条消息的两半,以及同一个 client_id 下两个不同的 stream_id。测试先给旧流送第一半,再给新流送第一半;随后给新流送第二半,应该成功拼出新流的完整消息。最后再把旧流剩下的第二半送来,应该被 Dropped,说明旧流的半截消息已经不能复活。

调用关系:它用 chunk_envelope 构造不同 stream_id 的分片,然后交给重组器 observe。这个测试补上了“客户端没变,但会话变了”的边界情况,防止旧会话残留数据干扰新会话。

调用图:调用 1 个内部函数(chunk_envelope);外部调用 8 个(Notification, new, new, default, assert!, assert_eq!, panic!, to_vec)。

ignores_stale_chunks_without_dropping_newer_assembly216–263 ↗
fn ignores_stale_chunks_without_dropping_newer_assembly()

作用:这个测试确认:如果系统正在拼一条较新的消息,突然来了更旧序号的碎片,旧碎片会被忽略,但新的拼接不会被破坏。stale 这里就是“过期的、来晚的”。

数据流:进去的是同一个客户端和同一个流里的两组分片序号:新的 seq_id 是 8,旧的 seq_id 是 7。测试先送入 seq_id 8 的第一片,重组器开始等待;再送入 seq_id 7 的旧第一片,应该直接 Dropped;最后送入 seq_id 8 的第二片,应该 Forward,表示新消息仍然能正常拼好并继续往后传。

调用关系:它检查重组器对消息序号的保护。这个测试模拟网络里旧包晚到的情况,确保 observe 不会因为看到旧包就清空当前正在进行的正常拼接。

调用图:外部调用 6 个(Notification, new, new, default, assert!, to_vec)。

ignores_invalid_stale_chunks_without_dropping_newer_assembly266–313 ↗
fn ignores_invalid_stale_chunks_without_dropping_newer_assembly()

作用:这个测试确认:哪怕过期碎片本身还是坏的,比如内容为空,也只能丢掉它,不能影响当前较新的正常拼接。

数据流:进去的是 seq_id 8 的正常第一片、seq_id 7 的过期坏第二片,以及 seq_id 8 的正常第二片。测试先让重组器开始拼 seq_id 8;再喂一个旧序号且内容为空的坏碎片,结果应该是 Dropped;最后喂 seq_id 8 的第二片,仍然应该 Forward,说明新消息没有被坏旧包拖下水。

调用关系:它是在“忽略过期碎片”基础上的更严格测试。它验证 observe 在遇到又旧又不合法的数据时,不会误删当前状态,也不会把错误扩散到新消息。

调用图:外部调用 6 个(Notification, new, new, default, assert!, to_vec)。

ignores_invalid_duplicate_chunks_without_dropping_current_assembly316–363 ↗
fn ignores_invalid_duplicate_chunks_without_dropping_current_assembly()

作用:这个测试确认:如果同一条消息的同一片又来了一次,而且这次还是坏内容,系统会丢掉这个重复坏片,但继续保留原来那片好的数据。

数据流:进去的是 seq_id 8 的第一片正常数据、同一个 seq_id 和同一个 segment_id 的空内容重复片,以及 seq_id 8 的第二片。重组器先进入 Pending;遇到重复的空片时返回 Dropped;最后收到第二片后仍然 Forward,说明一开始保存的好碎片没有被坏的重复碎片覆盖。

调用关系:它测试的是重复包场景。网络或重试机制可能让同一个分片出现多次,这个测试保证 ClientSegmentReassembler 不会让后来的坏副本覆盖先来的好副本。

调用图:外部调用 6 个(Notification, new, new, default, assert!, to_vec)。

chunk_envelope365–386 ↗
fn chunk_envelope(
    client_id: ClientId,
    stream_id: Option<StreamId>,
    seq_id: u64,
    segment_id: usize,
    segment_count: usize,
    message_size_bytes: usize,
    chunk: &[u8],
) -> Cli

作用:这是测试用的小帮手,用来把一段原始字节包装成“客户端消息分片”的格式。这样每个测试不用重复手写一大段 ClientEnvelope 结构。

数据流:进去的是 client_id、可选的 stream_id、消息序号 seq_id、当前是第几片、总共有几片、原消息总大小,以及这一片的原始字节。函数会把字节用 base64 编码成文本;base64 是一种把二进制数据安全写成普通字符串的方式。出来的是一个 ClientEnvelope,里面的 event 是 ClientMessageChunk,并带上这些编号和大小信息。

调用关系:它被测试函数在需要伪造客户端分片时调用,特别是在拼接成功和切换流的测试里很关键。它不做真正的重组,只负责把测试材料包装成重组器 observe 能看懂的输入。

调用图:被 2 处调用(reassembles_client_message_chunks, resets_incomplete_client_assembly_when_stream_changes)。

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

远程控制就像给本机应用开了一条“云端遥控通道”:先向后端登记自己,再用 WebSocket(一种能长期保持双向通信的网络连接)收发远程客户端的消息。这个测试文件专门防止这条通道出错。它会临时搭一个本地 TCP 监听器来假装后端服务器,手写 HTTP 响应,捕获请求头和请求体,再检查应用是否按正确顺序做事:有权限才连、没登录先等、旧登记能刷新、令牌失效会重取、禁用后不再连、远程客户端初始化后才算真正打开连接。文件里还准备了很多小工具,比如造假登录、造测试数据库、收发 WebSocket 消息。它的重要点是:这些测试不只看函数返回值,还模拟真实网络来确认远程控制在异常情况下不会误连、乱转发或丢掉持久化偏好。

函数细节40
remote_control_auth_manager76–78 ↗
fn remote_control_auth_manager() -> Arc<AuthManager>

作用:造一个测试用的登录管理器。它不用真的登录 ChatGPT,而是塞入一份假的、可用的 ChatGPT 身份,方便测试远程控制需要认证的流程。

数据流:进去没有参数 → 它创建一份测试专用的假 ChatGPT 登录信息,再交给测试辅助函数包装成 AuthManager(登录管理器)→ 出来一个可共享的登录管理器,测试可以拿它当作已登录用户来用。

调用关系:很多测试在启动远程控制前会先调用它,例如启动、启用禁用、WebSocket 连接和消息转发测试。它把造假身份的活交给 create_dummy_chatgpt_auth_for_testing 和 auth_manager_from_auth。

调用图:调用 2 个内部函数(auth_manager_from_auth, create_dummy_chatgpt_auth_for_testing);被 12 处调用(ephemeral_enable_preserves_durable_preference, explicit_disabled_start_ignores_persisted_enable, managed_disable_overrides_startup_and_persisted_enablement, plain_start_resolves_persisted_remote_control_preference, remote_control_handle_enable_disable_stops_and_restarts_connections, remote_control_http_mode_enrolls_before_connecting, remote_control_start_allows_remote_control_invalid_url_when_disabled, remote_control_start_reports_missing_state_db_as_disabled_when_enabled, remote_control_transport_clears_outgoing_buffer_when_backend_acks, remote_control_transport_manages_virtual_clients_and_routes_messages (+2 more))。

remote_control_auth_manager_with_home80–85 ↗
fn remote_control_auth_manager_with_home(codex_home: &TempDir) -> Arc<AuthManager>

作用:造一个带指定用户目录的测试登录管理器。这样测试可以把登录文件和状态文件都放进临时目录,避免污染真实环境。

数据流:进去一个临时目录 → 它取出目录路径,配上一份假 ChatGPT 登录信息 → 出来一个绑定这个目录的 AuthManager,后续测试可以验证从磁盘读写登录和登记信息的行为。

调用关系:需要测试“已保存登记”“刷新旧登记”“按客户端名区分登记”的用例会调用它。它底层依赖 auth_manager_from_auth_with_home 和假登录创建函数。

调用图:调用 2 个内部函数(auth_manager_from_auth_with_home, create_dummy_chatgpt_auth_for_testing);被 6 处调用(remote_control_http_mode_preserves_enrollment_after_generic_websocket_404, remote_control_http_mode_preserves_stale_enrollment_when_reenrollment_fails, remote_control_http_mode_reenrolls_after_explicit_missing_server_404, remote_control_http_mode_reenrolls_when_refresh_reports_stale_enrollment, remote_control_http_mode_refreshes_persisted_enrollment_before_connecting, remote_control_stdio_mode_waits_for_client_name_before_connecting);外部调用 1 个(path)。

remote_control_auth_dot_json87–125 ↗
fn remote_control_auth_dot_json(account_id: Option<&str>) -> AuthDotJson

作用:生成一份假的 auth.json 登录文件内容。它可以选择带不带账号 ID,用来测试远程控制是否会等待账号 ID 准备好。

数据流:进去一个可选账号 ID → 它拼出一个假的 JWT(登录令牌格式),解析出用户信息,再填入访问令牌、刷新令牌和账号字段 → 出来一份 AuthDotJson,可被保存到临时用户目录。

调用关系:等待账号 ID 和切换账号的测试会用它先写入不同登录状态。它会调用 parse_chatgpt_jwt_claims 来把假 JWT 解析成测试可用的数据。

调用图:调用 1 个内部函数(parse_chatgpt_jwt_claims);被 2 处调用(persisted_enable_does_not_follow_auth_to_an_account_without_a_preference, remote_control_waits_for_account_id_before_enrolling);外部调用 4 个(now, format!, json!, to_vec)。

remote_control_state_runtime127–131 ↗
async fn remote_control_state_runtime(codex_home: &TempDir) -> Arc<StateRuntime>

作用:创建测试用的状态数据库运行时。远程控制会把“这个账号是否启用、登记过哪个服务器”这类信息存进去。

数据流:进去一个临时目录 → 它在这个目录下初始化 StateRuntime(状态数据库运行环境)→ 出来一个可共享的数据库句柄,测试用它写入或读取远程控制登记记录。

调用关系:几乎所有涉及持久化偏好、登记记录、启用禁用的测试都会先调用它。它把真正初始化数据库的工作交给 StateRuntime::init。

调用图:调用 1 个内部函数(init);被 19 处调用(ephemeral_enable_preserves_durable_preference, explicit_disabled_start_ignores_persisted_enable, managed_disable_overrides_startup_and_persisted_enablement, persisted_enable_does_not_follow_auth_to_an_account_without_a_preference, plain_start_resolves_persisted_remote_control_preference, remote_control_handle_enable_disable_stops_and_restarts_connections, remote_control_http_mode_enrolls_before_connecting, remote_control_http_mode_preserves_enrollment_after_generic_websocket_404, remote_control_http_mode_preserves_stale_enrollment_when_reenrollment_fails, remote_control_http_mode_reenrolls_after_explicit_missing_server_404 (+9 more));外部调用 1 个(path)。

plain_start_resolves_persisted_remote_control_preference134–205 ↗
async fn plain_start_resolves_persisted_remote_control_preference()

作用:测试普通启动时,远程控制会不会正确读取以前保存的启用偏好。重点是确认“已启用”会继续启用,而“禁用、未设置、缺失”都不会误启用。

数据流:进去没有外部输入 → 它先在临时数据库里写入几种不同偏好,再创建 RemoteControlWebsocket 并让它解析 Unknown 状态 → 出来是断言后的期望状态,同时 desired_state_tx 里的期望状态被更新。

调用关系:这是直接测试 RemoteControlWebsocket::resolve_unknown_desired_state 的用例。它会借助 remote_control_state_runtime、remote_control_auth_manager 和 test_server_name 搭好测试环境。

调用图:调用 7 个内部函数(new, normalize_remote_control_url, remote_control_auth_manager, remote_control_state_runtime, test_server_name, new, new);外部调用 9 个(new, new, new, new, assert!, assert_eq!, format!, channel, channel)。

explicit_disabled_start_ignores_persisted_enable208–263 ↗
async fn explicit_disabled_start_ignores_persisted_enable()

作用:测试如果启动参数明确说“这次先禁用”,即使数据库里以前保存的是启用,也不能偷偷连上。

数据流:进去没有参数 → 它写入一条持久化启用记录,再用 DisabledEphemeral 模式启动远程控制 → 出来是断言:当前状态为 Disabled,数据库里的旧记录仍原样保留。

调用关系:它围绕 start_remote_control 做启动行为验证。它会用 remote_control_state_runtime 准备数据库,用 remote_control_auth_manager 准备登录。

调用图:调用 3 个内部函数(normalize_remote_control_url, remote_control_auth_manager, remote_control_state_runtime);外部调用 4 个(new, new, assert_eq!, channel)。

managed_disable_overrides_startup_and_persisted_enablement266–362 ↗
async fn managed_disable_overrides_startup_and_persisted_enablement()

作用:测试“被管理策略禁用”时,任何启动模式和历史启用记录都不能覆盖它。这里的管理策略类似公司管控开关,优先级最高。

数据流:进去没有参数 → 它准备一个假的后端监听器和一条已启用的数据库记录,再用 DisabledByRequirements 策略启动 → 出来是断言:状态是 Disabled,启用和禁用接口都返回权限错误,而且后端没有收到连接。

调用关系:它测试 start_remote_control 和 RemoteControlHandle 的策略检查。会调用 remote_control_url_for_listener、remote_control_state_runtime 和 remote_control_auth_manager 来搭场景。

调用图:调用 4 个内部函数(normalize_remote_control_url, remote_control_auth_manager, remote_control_state_runtime, remote_control_url_for_listener);外部调用 8 个(new, from_millis, bind, new, assert!, assert_eq!, channel, timeout)。

remote_control_url_for_listener364–369 ↗
fn remote_control_url_for_listener(listener: &TcpListener) -> String

作用:把测试用 TCP 监听器的地址转换成远程控制 URL。这样本来要连真实后端的代码,会改连本机假服务器。

数据流:进去一个 TcpListener → 它读取监听器实际绑定的 IP 和端口 → 出来一个形如 http://地址/backend-api/ 的字符串。

调用关系:大量网络测试都会先开本地监听器,再调用它生成配置 URL。它只负责拼地址,不参与真正通信。

调用图:被 17 处调用(managed_disable_overrides_startup_and_persisted_enablement, persisted_enable_does_not_follow_auth_to_an_account_without_a_preference, remote_control_handle_enable_disable_stops_and_restarts_connections, remote_control_http_mode_enrolls_before_connecting, remote_control_http_mode_preserves_enrollment_after_generic_websocket_404, remote_control_http_mode_preserves_stale_enrollment_when_reenrollment_fails, remote_control_http_mode_reenrolls_after_explicit_missing_server_404, remote_control_http_mode_reenrolls_when_refresh_reports_stale_enrollment, remote_control_http_mode_refreshes_persisted_enrollment_before_connecting, remote_control_start_allows_missing_auth_when_enabled (+7 more));外部调用 2 个(local_addr, format!)。

test_server_name371–373 ↗
fn test_server_name() -> String

作用:取得当前机器名,作为测试里的服务器名称。远程控制登记时会把这个名字报给后端。

数据流:进去没有参数 → 它读取系统主机名,转成字符串并去掉两端空白 → 出来一个服务器名字符串。

调用关系:需要比较状态通知或登记信息里的 server_name 时会调用它。它依赖 gethostname 这个系统调用包装。

调用图:被 5 处调用(plain_start_resolves_persisted_remote_control_preference, remote_control_handle_enable_disable_stops_and_restarts_connections, remote_control_handle_with_current_enrollment, remote_control_http_mode_preserves_enrollment_after_generic_websocket_404, remote_control_http_mode_preserves_stale_enrollment_when_reenrollment_fails);外部调用 1 个(gethostname)。

remote_control_handle_with_current_enrollment375–418 ↗
fn remote_control_handle_with_current_enrollment(
    remote_control_url: &str,
    auth_manager: Arc<AuthManager>,
) -> RemoteControlHandle

作用:手工造一个已经有当前登记信息的 RemoteControlHandle。它用来测试句柄本身的行为,不需要真的启动网络任务。

数据流:进去远程控制 URL 和登录管理器 → 它创建期望状态通道、状态通道、规范化后的目标地址和一份带令牌的当前登记 → 出来一个 RemoteControlHandle,里面已经装好可用的登记状态。

调用关系:目前由 ephemeral_enable_preserves_durable_preference 使用。它会调用 normalize_remote_control_url 和 test_server_name 来填齐远程控制句柄所需字段。

调用图:调用 3 个内部函数(new, normalize_remote_control_url, test_server_name);被 1 处调用(ephemeral_enable_preserves_durable_preference);外部调用 4 个(new, from_unix_timestamp, new, channel)。

ephemeral_enable_preserves_durable_preference421–456 ↗
async fn ephemeral_enable_preserves_durable_preference()

作用:测试“临时启用”不会破坏用户已经保存的长期偏好。临时启用像临时开灯,不应该改掉墙上的总开关设置。

数据流:进去没有参数 → 它造一个带当前登记的句柄,先设置持久偏好为启用再临时启用,又设置为禁用后再临时启用 → 出来是断言:已有持久启用偏好被保留,从禁用临时启用时偏好为空。

调用关系:它直接调用 remote_control_handle_with_current_enrollment 来得到测试对象,并用 remote_control_state_runtime 补上数据库。核心验证对象是 RemoteControlHandle::enable_ephemeral。

调用图:调用 3 个内部函数(remote_control_auth_manager, remote_control_handle_with_current_enrollment, remote_control_state_runtime);外部调用 2 个(new, assert_eq!)。

remote_control_server_token_response458–469 ↗
fn remote_control_server_token_response(
    server_id: &str,
    environment_id: &str,
    remote_control_token: &str,
) -> serde_json::Value

作用:生成后端返回的远程控制服务器令牌 JSON。测试用它模拟登记或刷新成功后的响应。

数据流:进去 server_id、environment_id 和 remote_control_token → 它把这些字段加上固定的过期时间组成 JSON → 出来一个 serde_json::Value,可直接写进 HTTP 响应体。

调用关系:所有需要假装后端返回登记结果或刷新结果的测试都会用它。它只负责造数据,通常随后交给 respond_with_json 发回客户端。

调用图:被 13 处调用(persisted_enable_does_not_follow_auth_to_an_account_without_a_preference, remote_control_handle_enable_disable_stops_and_restarts_connections, remote_control_http_mode_enrolls_before_connecting, remote_control_http_mode_preserves_enrollment_after_generic_websocket_404, remote_control_http_mode_reenrolls_after_explicit_missing_server_404, remote_control_http_mode_reenrolls_when_refresh_reports_stale_enrollment, remote_control_http_mode_refreshes_persisted_enrollment_before_connecting, remote_control_stdio_mode_waits_for_client_name_before_connecting, remote_control_transport_clears_outgoing_buffer_when_backend_acks, remote_control_transport_manages_virtual_clients_and_routes_messages (+3 more));外部调用 1 个(json!)。

expect_remote_control_status471–487 ↗
async fn expect_remote_control_status(
    status_rx: &mut watch::Receiver<RemoteControlStatusChangedNotification>,
    expected_status: Option<RemoteControlConnectionStatus>,
    expected_environment

作用:等待远程控制状态发生一次变化,并检查状态内容是否符合预期。它帮测试避免“还没更新就断言”的竞态问题。

数据流:进去状态接收器、可选的期望连接状态、可选环境 ID → 它最多等 5 秒收到状态变化,再检查服务器名、安装 ID、环境 ID 和可选状态 → 出来没有返回值,失败时测试直接报错。

调用关系:连接、重连、刷新、登记等测试都会用它确认状态通知。它读取 watch 通道中的 RemoteControlStatusChangedNotification。

调用图:被 9 处调用(remote_control_handle_enable_disable_stops_and_restarts_connections, remote_control_http_mode_enrolls_before_connecting, remote_control_http_mode_preserves_enrollment_after_generic_websocket_404, remote_control_http_mode_reenrolls_after_explicit_missing_server_404, remote_control_http_mode_reenrolls_when_refresh_reports_stale_enrollment, remote_control_transport_clears_outgoing_buffer_when_backend_acks, remote_control_transport_manages_virtual_clients_and_routes_messages, remote_control_transport_reconnects_after_disconnect, remote_control_transport_refreshes_server_token_after_websocket_unauthorized);外部调用 5 个(from_secs, assert_eq!, borrow, changed, timeout)。

expect_remote_control_status_snapshot489–515 ↗
async fn expect_remote_control_status_snapshot(
    status_rx: &mut watch::Receiver<RemoteControlStatusChangedNotification>,
    expected_status: RemoteControlStatusChangedNotification,
)

作用:等待状态通道最终出现某个完整快照。它适合状态可能跳几次的场景,只要最后等到目标状态就算通过。

数据流:进去状态接收器和完整期望状态 → 如果当前已经匹配就立即返回,否则循环等待变化,最多等 5 秒 → 出来没有返回值,超时会打印期望状态和当前最新状态。

调用关系:启用禁用切换、泛化 404 后恢复连接等测试会用它。它比 expect_remote_control_status 更严格,因为要整条通知完全相等。

调用图:被 2 处调用(remote_control_handle_enable_disable_stops_and_restarts_connections, remote_control_http_mode_preserves_enrollment_after_generic_websocket_404);外部调用 6 个(from_secs, clone, assert!, borrow, changed, timeout)。

remote_control_transport_manages_virtual_clients_and_routes_messages518–812 ↗
async fn remote_control_transport_manages_virtual_clients_and_routes_messages()

作用:测试远程控制能把云端来的“虚拟客户端”当成普通客户端接入,并正确转发双方消息。这里的虚拟客户端就是通过 WebSocket 远程连进来的客户端。

数据流:进去没有参数 → 它启动远程控制、模拟登记和 WebSocket,发送 ping、非初始化消息、initialize 请求、后续通知和关闭事件 → 出来是断言:连接打开、消息转入主传输层、服务端消息转回 WebSocket、关闭后状态回到 unknown。

调用关系:这是文件里覆盖面很大的端到端测试。它调用 accept_http_request、accept_remote_control_connection、send_client_event、read_server_event 等工具,来驱动 start_remote_control 创建的后台任务。

调用图:调用 11 个内部函数(new, normalize_remote_control_url, accept_http_request, accept_remote_control_connection, expect_remote_control_status, remote_control_auth_manager, remote_control_server_token_response, remote_control_state_runtime, remote_control_url_for_listener, respond_with_json (+1 more));外部调用 15 个(new, from_secs, ConfigWarning, bind, new, Notification, Request, Integer, AppServerNotification, new (+5 more))。

remote_control_transport_reconnects_after_disconnect815–914 ↗
async fn remote_control_transport_reconnects_after_disconnect()

作用:测试 WebSocket 断开后远程控制会自动重连。网络连接掉线很常见,这个测试保证通道不会一次断开就彻底死掉。

数据流:进去没有参数 → 它先完成登记并接受第一次 WebSocket,再主动关闭它,然后等待第二次连接并发送 initialize → 出来是断言:第二次连接仍带正确令牌,并能打开新的远程客户端连接。

调用关系:它通过 accept_remote_control_backend_connection 捕获两次握手请求,通过 transport_event_rx 验证重连后仍会产生 ConnectionOpened。

调用图:调用 9 个内部函数(accept_http_request, accept_remote_control_backend_connection, expect_remote_control_status, remote_control_auth_manager, remote_control_server_token_response, remote_control_state_runtime, remote_control_url_for_listener, respond_with_json, send_client_event);外部调用 11 个(new, from_secs, bind, new, Request, Integer, new, assert_eq!, json!, panic! (+1 more))。

remote_control_transport_refreshes_server_token_after_websocket_unauthorized917–1000 ↗
async fn remote_control_transport_refreshes_server_token_after_websocket_unauthorized()

作用:测试 WebSocket 连接收到 401 Unauthorized(令牌无效)后,会先刷新服务器令牌再重新连接。

数据流:进去没有参数 → 它让第一次 WebSocket 握手返回 401,再模拟后端 refresh 接口返回新令牌 → 出来是断言:后续 WebSocket 握手使用刷新后的 Bearer 令牌。

调用关系:它测试登记令牌失效时的恢复路径。它用 accept_http_request 捕获普通 HTTP 请求,用 accept_remote_control_backend_connection 捕获最终成功的 WebSocket 握手。

调用图:调用 9 个内部函数(accept_http_request, accept_remote_control_backend_connection, expect_remote_control_status, remote_control_auth_manager, remote_control_server_token_response, remote_control_state_runtime, remote_control_url_for_listener, respond_with_json, respond_with_status);外部调用 4 个(new, bind, new, assert_eq!)。

remote_control_start_allows_remote_control_invalid_url_when_disabled1003–1028 ↗
async fn remote_control_start_allows_remote_control_invalid_url_when_disabled()

作用:测试远程控制处于禁用启动路径时,不会因为 URL 看起来不适合连接就启动失败。禁用状态下本来就不会连,没必要过早报错。

数据流:进去没有参数 → 它传入一个不可用于测试连接的 URL,并用 ResolvePersisted 模式启动但没有状态数据库 → 出来是断言:启动成功,取消令牌后后台任务能正常退出。

调用关系:它直接验证 start_remote_control 的宽容启动行为。只需要 remote_control_auth_manager 和取消令牌,不需要假后端真正接收连接。

调用图:调用 1 个内部函数(remote_control_auth_manager);外部调用 3 个(new, from_secs, timeout)。

remote_control_start_allows_missing_auth_when_enabled1031–1073 ↗
async fn remote_control_start_allows_missing_auth_when_enabled()

作用:测试即使用户还没登录,远程控制也可以先启动后台任务,但必须等登录可用后才连接后端。

数据流:进去没有参数 → 它创建一个没有登录凭据的 AuthManager,按启用模式启动远程控制,然后短时间监听后端是否有连接 → 出来是断言:启动成功,但假后端没有收到任何请求。

调用关系:它围绕 start_remote_control 的“等待认证”行为。它使用 remote_control_state_runtime 准备数据库,用 remote_control_url_for_listener 指向本地假后端。

调用图:调用 4 个内部函数(remote_control_state_runtime, remote_control_url_for_listener, default, shared);外部调用 6 个(new, from_millis, from_secs, bind, new, timeout)。

remote_control_start_reports_missing_state_db_as_disabled_when_enabled1076–1132 ↗
async fn remote_control_start_reports_missing_state_db_as_disabled_when_enabled()

作用:测试没有状态数据库时,即使请求启用远程控制,也应该报告不可用并保持禁用。因为没有数据库就无法安全保存登记和偏好。

数据流:进去没有参数 → 它不给 state_db,却用启用模式启动远程控制,再尝试临时启用 → 出来是断言:状态保持 Disabled、不会连接后端、enable_ephemeral 返回 Unavailable。

调用关系:它验证 start_remote_control 和 RemoteControlHandle 在缺少 StateRuntime 时的保护逻辑。网络侧用 remote_control_url_for_listener 创建但不应收到连接。

调用图:调用 2 个内部函数(remote_control_auth_manager, remote_control_url_for_listener);外部调用 6 个(new, from_millis, from_secs, bind, assert_eq!, timeout)。

remote_control_handle_enable_disable_stops_and_restarts_connections1135–1252 ↗
async fn remote_control_handle_enable_disable_stops_and_restarts_connections()

作用:测试通过句柄禁用远程控制会关闭已有 WebSocket,通过句柄重新启用又会重新连接。它确认开关真的控制网络连接,而不是只改状态文字。

数据流:进去没有参数 → 它启动并连上假后端,然后调用 disable,检查连接关闭且不重连,再调用 enable,检查状态变为 Connecting 并建立第二条 WebSocket → 出来是多次状态和连接行为的断言。

调用关系:它重点测试 RemoteControlHandle::disable 和 RemoteControlHandle::enable。辅助函数 accept_http_request、accept_remote_control_connection、expect_remote_control_status_snapshot 负责模拟后端和确认状态。

调用图:调用 10 个内部函数(accept_http_request, accept_remote_control_connection, expect_remote_control_status, expect_remote_control_status_snapshot, remote_control_auth_manager, remote_control_server_token_response, remote_control_state_runtime, remote_control_url_for_listener, respond_with_json, test_server_name);外部调用 7 个(new, from_millis, from_secs, bind, new, assert_eq!, timeout)。

remote_control_transport_clears_outgoing_buffer_when_backend_acks1255–1434 ↗
async fn remote_control_transport_clears_outgoing_buffer_when_backend_acks()

作用:测试后端确认收到某条服务端消息后,本地会清掉对应的待发送缓存。这样断线重连后不会把已经确认的旧消息再发一遍。

数据流:进去没有参数 → 它建立远程客户端连接,发送一条服务端通知,读取其 stream_id,再让后端发 Ack 确认,随后关闭客户端和 WebSocket 并重连 → 出来是断言:重连后的 ping 只显示 unknown,不会重放那条 stale 消息。

调用关系:它测试远程传输层的确认和缓存清理机制。它会调用 read_server_event_with_stream_id 拿到消息流 ID,再用 send_client_event 发送 Ack。

调用图:调用 11 个内部函数(new, accept_http_request, accept_remote_control_connection, expect_remote_control_status, read_server_event_with_stream_id, remote_control_auth_manager, remote_control_server_token_response, remote_control_state_runtime, remote_control_url_for_listener, respond_with_json (+1 more));外部调用 13 个(new, from_secs, ConfigWarning, bind, new, Request, Integer, AppServerNotification, new, assert_eq! (+3 more))。

remote_control_http_mode_enrolls_before_connecting1437–1661 ↗
async fn remote_control_http_mode_enrolls_before_connecting()

作用:测试 HTTP 模式下必须先向后端登记,再用登记返回的服务器令牌建立 WebSocket。登记就像先办通行证,再进门。

数据流:进去没有参数 → 它启动远程控制,捕获 enroll 请求并检查认证头、安装 ID、机器信息,请求成功后捕获 WebSocket 握手,再模拟远程客户端 initialize 和服务端回包 → 出来是断言:请求顺序、请求头、消息转发都正确。

调用关系:这是登记流程的端到端测试。它使用 accept_http_request 检查 HTTP 登记,用 accept_remote_control_backend_connection 检查 WebSocket 头,用 send_client_event 和 read_server_event 验证消息通路。

调用图:调用 10 个内部函数(new, accept_http_request, accept_remote_control_backend_connection, expect_remote_control_status, remote_control_auth_manager, remote_control_server_token_response, remote_control_state_runtime, remote_control_url_for_listener, respond_with_json, send_client_event);外部调用 15 个(new, from_secs, ConfigWarning, bind, new, Request, Integer, AppServerNotification, Response, new (+5 more))。

remote_control_http_mode_refreshes_persisted_enrollment_before_connecting1664–1768 ↗
async fn remote_control_http_mode_refreshes_persisted_enrollment_before_connecting()

作用:测试如果本地已经保存过登记信息,启动时会先刷新旧登记,而不是重新登记。这样能复用原来的 server_id。

数据流:进去没有参数 → 它先把一条没有令牌的登记写入数据库,再启动远程控制,捕获 refresh 请求并返回新令牌,最后捕获 WebSocket 握手 → 出来是断言:握手使用原 server_id 和刷新后的令牌,数据库里的登记仍可加载。

调用关系:它调用 update_persisted_remote_control_enrollment 预置旧登记,再通过 accept_http_request 和 accept_remote_control_backend_connection 验证刷新后连接。

调用图:调用 9 个内部函数(update_persisted_remote_control_enrollment, normalize_remote_control_url, accept_http_request, accept_remote_control_backend_connection, remote_control_auth_manager_with_home, remote_control_server_token_response, remote_control_state_runtime, remote_control_url_for_listener, respond_with_json);外部调用 4 个(new, bind, new, assert_eq!)。

remote_control_stdio_mode_waits_for_client_name_before_connecting1771–1848 ↗
async fn remote_control_stdio_mode_waits_for_client_name_before_connecting()

作用:测试 stdio 模式下会先等客户端名称到达,再读取对应的持久化登记并连接。stdio 是标准输入输出通信方式,客户端名用于区分不同调用方。

数据流:进去没有参数 → 它保存一条带 app_server_client_name 的登记,启动时传入一个还没发送名称的 oneshot 通道,确认不会连接;发送名称后,再检查 refresh 和 WebSocket → 出来是断言:只有拿到客户端名后才连接,并使用对应 server_id。

调用关系:它测试 start_remote_control 对 app_server_client_name_rx 的等待。预置登记由 update_persisted_remote_control_enrollment 完成,网络请求由测试辅助函数捕获。

调用图:调用 9 个内部函数(update_persisted_remote_control_enrollment, normalize_remote_control_url, accept_http_request, accept_remote_control_backend_connection, remote_control_auth_manager_with_home, remote_control_server_token_response, remote_control_state_runtime, remote_control_url_for_listener, respond_with_json);外部调用 6 个(new, from_millis, bind, new, assert_eq!, timeout)。

remote_control_waits_for_account_id_before_enrolling1851–1943 ↗
async fn remote_control_waits_for_account_id_before_enrolling()

作用:测试登录信息里还没有账号 ID 时,远程控制不会急着登记;等账号 ID 出现后,会立刻继续。

数据流:进去没有参数 → 它先保存一份不带 account_id 的登录文件并启动远程控制,确认后端没请求;再写入带 account_id 的登录文件并 reload → 出来是断言:远程控制被唤醒,发出 enroll 请求并成功连接 WebSocket。

调用关系:它使用 remote_control_auth_dot_json 制造两种登录文件,通过 auth_manager.reload 触发认证变化。accept_http_request 和 accept_remote_control_backend_connection 负责观察后续网络动作。

调用图:调用 10 个内部函数(normalize_remote_control_url, accept_http_request, accept_remote_control_backend_connection, remote_control_auth_dot_json, remote_control_server_token_response, remote_control_state_runtime, remote_control_url_for_listener, respond_with_json, default, shared);外部调用 8 个(new, from_millis, bind, new, assert_eq!, save_auth, gethostname, timeout)。

persisted_enable_does_not_follow_auth_to_an_account_without_a_preference1946–2064 ↗
async fn persisted_enable_does_not_follow_auth_to_an_account_without_a_preference()

作用:测试“账号 A 保存了启用”不会在切换到没有偏好的账号 B 后继续启用。每个账号的远程控制开关必须分开算。

数据流:进去没有参数 → 它给账号 A 写入启用登记并启动连接,然后把登录切到账号 B,关闭旧 WebSocket → 出来是断言:期望状态变为 Disabled,后端不再收到账号 B 的登记请求,数据库也没有账号 B 的登记。

调用关系:它覆盖账号切换后的持久化偏好隔离。它用 remote_control_auth_dot_json 写不同账号登录,用 desired_state_rx 等待远程控制自动禁用。

调用图:调用 11 个内部函数(update_persisted_remote_control_enrollment, normalize_remote_control_url, accept_http_request, accept_remote_control_backend_connection, remote_control_auth_dot_json, remote_control_server_token_response, remote_control_state_runtime, remote_control_url_for_listener, respond_with_json, default (+1 more));外部调用 8 个(new, from_millis, from_secs, bind, new, assert_eq!, save_auth, timeout)。

remote_control_http_mode_reenrolls_when_refresh_reports_stale_enrollment2067–2187 ↗
async fn remote_control_http_mode_reenrolls_when_refresh_reports_stale_enrollment()

作用:测试刷新旧登记时如果后端说 404,说明本地登记过期了,此时应该重新登记并保存新记录。

数据流:进去没有参数 → 它保存一条 stale 旧登记,启动后让 refresh 返回 404,再让 enroll 返回新 server_id 和环境 ID,最后接受 WebSocket → 出来是断言:连接用新 server_id,数据库记录也更新为新登记且保留启用偏好。

调用关系:它测试 refresh 失败后的重新登记路径。会用 expect_remote_control_status 观察环境 ID 从旧登记切换到新登记。

调用图:调用 11 个内部函数(update_persisted_remote_control_enrollment, normalize_remote_control_url, accept_http_request, accept_remote_control_backend_connection, expect_remote_control_status, remote_control_auth_manager_with_home, remote_control_server_token_response, remote_control_state_runtime, remote_control_url_for_listener, respond_with_json (+1 more));外部调用 5 个(new, bind, new, assert_eq!, gethostname)。

remote_control_http_mode_reenrolls_after_explicit_missing_server_4042190–2334 ↗
async fn remote_control_http_mode_reenrolls_after_explicit_missing_server_404()

作用:测试 WebSocket 握手阶段如果后端明确说“远程服务器不存在”,也要重新登记。这里的 404 带有特定错误内容,不是普通网页找不到。

数据流:进去没有参数 → 它先刷新旧登记成功,再让 WebSocket GET 返回包含 Remote app server not found 的 404,然后模拟重新 enroll 并连接 → 出来是断言:新连接使用新的 server_id,数据库更新成新登记。

调用关系:它覆盖“刷新成功但连接时发现 server_id 已失效”的恢复路径。accept_http_request 捕获失败握手,respond_with_status 返回明确 404,随后走重新登记流程。

调用图:调用 11 个内部函数(update_persisted_remote_control_enrollment, normalize_remote_control_url, accept_http_request, accept_remote_control_backend_connection, expect_remote_control_status, remote_control_auth_manager_with_home, remote_control_server_token_response, remote_control_state_runtime, remote_control_url_for_listener, respond_with_json (+1 more));外部调用 6 个(new, bind, new, assert_eq!, gethostname, json!)。

remote_control_http_mode_preserves_stale_enrollment_when_reenrollment_fails2337–2437 ↗
async fn remote_control_http_mode_preserves_stale_enrollment_when_reenrollment_fails()

作用:测试旧登记失效后,如果重新登记也失败,本地不能把旧登记删掉。保留旧数据可以让后续重试还有线索。

数据流:进去没有参数 → 它保存一条旧登记,让 refresh 返回 404,再让 enroll 返回 500,随后下一轮 refresh 也失败 → 出来是断言:内存中的 current_enrollment 和数据库里的旧登记都还在。

调用关系:它测试失败恢复时的数据保护。它用 respond_with_status 制造 404 和 500,最后直接检查 RemoteControlHandle 的当前登记和 StateRuntime 里的记录。

调用图:调用 8 个内部函数(update_persisted_remote_control_enrollment, normalize_remote_control_url, accept_http_request, remote_control_auth_manager_with_home, remote_control_state_runtime, remote_control_url_for_listener, respond_with_status, test_server_name);外部调用 4 个(new, bind, new, assert_eq!)。

remote_control_http_mode_preserves_enrollment_after_generic_websocket_4042440–2569 ↗
async fn remote_control_http_mode_preserves_enrollment_after_generic_websocket_404()

作用:测试 WebSocket 遇到普通 404 时,不要误以为登记失效。只有明确的“服务器不存在”错误才该重新登记。

数据流:进去没有参数 → 它刷新旧登记成功,然后让 WebSocket GET 返回普通 404 和一些追踪头,再检查数据库仍保留旧登记,之后接受下一次 WebSocket 连接 → 出来是断言:仍然使用旧 server_id 和刷新令牌,并最终连接成功。

调用关系:它区分两种 404 的处理方式。respond_with_status_and_headers 用来模拟普通 404,expect_remote_control_status_snapshot 用来确认后续成功连接。

调用图:调用 13 个内部函数(update_persisted_remote_control_enrollment, normalize_remote_control_url, accept_http_request, accept_remote_control_backend_connection, expect_remote_control_status, expect_remote_control_status_snapshot, remote_control_auth_manager_with_home, remote_control_server_token_response, remote_control_state_runtime, remote_control_url_for_listener (+3 more));外部调用 4 个(new, bind, new, assert_eq!)。

accept_remote_control_connection2585–2593 ↗
async fn accept_remote_control_connection(listener: &TcpListener) -> WebSocketStream<TcpStream>

作用:在测试假服务器上接受一个 WebSocket 连接。它只关心连接成功,不额外保存握手请求头。

数据流:进去一个 TcpListener → 它最多等 5 秒接收 TCP 连接,并完成 WebSocket 握手 → 出来一个 WebSocketStream,测试可以继续收发远程控制消息。

调用关系:虚拟客户端路由、启用禁用、Ack 缓存清理等测试会调用它。它把底层握手交给 tokio_tungstenite 的 accept_async。

调用图:被 3 处调用(remote_control_handle_enable_disable_stops_and_restarts_connections, remote_control_transport_clears_outgoing_buffer_when_backend_acks, remote_control_transport_manages_virtual_clients_and_routes_messages);外部调用 4 个(from_secs, accept, timeout, accept_async)。

accept_http_request2595–2640 ↗
async fn accept_http_request(listener: &TcpListener) -> CapturedHttpRequest

作用:在测试假服务器上接收一条普通 HTTP 请求,并把请求行、请求头、请求体都拆出来。这样测试可以精确检查远程控制发了什么。

数据流:进去一个 TcpListener → 它等待连接,逐行读取请求行和头部,再按 content-length 读取请求体 → 出来 CapturedHttpRequest,里面保留原始 TcpStream 以便继续回响应。

调用关系:登记、刷新、401、404 等 HTTP 场景都依赖它。通常调用后会把它返回的 stream 交给 respond_with_json 或 respond_with_status。

调用图:被 14 处调用(persisted_enable_does_not_follow_auth_to_an_account_without_a_preference, remote_control_handle_enable_disable_stops_and_restarts_connections, remote_control_http_mode_enrolls_before_connecting, remote_control_http_mode_preserves_enrollment_after_generic_websocket_404, remote_control_http_mode_preserves_stale_enrollment_when_reenrollment_fails, remote_control_http_mode_reenrolls_after_explicit_missing_server_404, remote_control_http_mode_reenrolls_when_refresh_reports_stale_enrollment, remote_control_http_mode_refreshes_persisted_enrollment_before_connecting, remote_control_stdio_mode_waits_for_client_name_before_connecting, remote_control_transport_clears_outgoing_buffer_when_backend_acks (+4 more));外部调用 8 个(new, new, from_secs, from_utf8, new, accept, timeout, vec!)。

respond_with_json2642–2653 ↗
async fn respond_with_json(mut stream: TcpStream, body: serde_json::Value)

作用:给测试捕获到的 HTTP 请求回一个 200 OK 的 JSON 响应。它用来假装后端接口成功返回。

数据流:进去一个 TCP 流和 JSON 值 → 它把 JSON 转成字符串,拼出带 content-type 和 content-length 的 HTTP 响应 → 写入并刷新到连接中,没有返回值。

调用关系:所有登记成功、刷新成功的测试都会用它。它通常接在 accept_http_request 后面,响应体常来自 remote_control_server_token_response。

调用图:被 13 处调用(persisted_enable_does_not_follow_auth_to_an_account_without_a_preference, remote_control_handle_enable_disable_stops_and_restarts_connections, remote_control_http_mode_enrolls_before_connecting, remote_control_http_mode_preserves_enrollment_after_generic_websocket_404, remote_control_http_mode_reenrolls_after_explicit_missing_server_404, remote_control_http_mode_reenrolls_when_refresh_reports_stale_enrollment, remote_control_http_mode_refreshes_persisted_enrollment_before_connecting, remote_control_stdio_mode_waits_for_client_name_before_connecting, remote_control_transport_clears_outgoing_buffer_when_backend_acks, remote_control_transport_manages_virtual_clients_and_routes_messages (+3 more));外部调用 4 个(flush, write_all, to_string, format!)。

respond_with_status2655–2657 ↗
async fn respond_with_status(stream: TcpStream, status: &str, body: &str)

作用:给 HTTP 请求回一个指定状态码的简单响应。它适合测试 401、404、500 这类错误分支。

数据流:进去 TCP 流、状态文本和响应体 → 它不加额外头,直接转交给 respond_with_status_and_headers → 出来没有返回值,连接收到指定错误响应。

调用关系:它是 respond_with_status_and_headers 的简化版。令牌无效、旧登记失效、重新登记失败等测试会调用它制造后端错误。

调用图:调用 1 个内部函数(respond_with_status_and_headers);被 4 处调用(remote_control_http_mode_preserves_stale_enrollment_when_reenrollment_fails, remote_control_http_mode_reenrolls_after_explicit_missing_server_404, remote_control_http_mode_reenrolls_when_refresh_reports_stale_enrollment, remote_control_transport_refreshes_server_token_after_websocket_unauthorized)。

respond_with_status_and_headers2659–2678 ↗
async fn respond_with_status_and_headers(
    mut stream: TcpStream,
    status: &str,
    headers: &[(&str, &str)],
    body: &str,
)

作用:给 HTTP 请求回一个指定状态码、可附加自定义响应头的响应。它用于模拟更接近真实后端的错误返回。

数据流:进去 TCP 流、状态文本、额外响应头列表和响应体 → 它拼出完整 HTTP 响应,写入并刷新 → 出来没有返回值,客户端会看到指定状态和头部。

调用关系:普通 404 保留登记的测试会直接调用它;respond_with_status 也会把简单错误响应交给它处理。

调用图:被 2 处调用(remote_control_http_mode_preserves_enrollment_after_generic_websocket_404, respond_with_status);外部调用 3 个(flush, write_all, format!)。

accept_remote_control_backend_connection2680–2723 ↗
async fn accept_remote_control_backend_connection(
    listener: &TcpListener,
) -> (CapturedWebSocketRequest, WebSocketStream<TcpStream>)

作用:接受一个 WebSocket 连接,同时把握手请求的路径和请求头抓下来。它让测试能确认远程控制带了正确令牌、server_id 和协议版本。

数据流:进去一个 TcpListener → 它等待 TCP 连接,用带回调的 WebSocket 握手读取请求 URI 和 headers → 出来一份 CapturedWebSocketRequest 加一个 WebSocketStream。

调用关系:需要检查 WebSocket 握手头的测试都会用它,比如登记后连接、刷新后连接、重连和令牌刷新。它底层使用 accept_hdr_async 来边握手边捕获请求。

调用图:被 10 处调用(persisted_enable_does_not_follow_auth_to_an_account_without_a_preference, remote_control_http_mode_enrolls_before_connecting, remote_control_http_mode_preserves_enrollment_after_generic_websocket_404, remote_control_http_mode_reenrolls_after_explicit_missing_server_404, remote_control_http_mode_reenrolls_when_refresh_reports_stale_enrollment, remote_control_http_mode_refreshes_persisted_enrollment_before_connecting, remote_control_stdio_mode_waits_for_client_name_before_connecting, remote_control_transport_reconnects_after_disconnect, remote_control_transport_refreshes_server_token_after_websocket_unauthorized, remote_control_waits_for_account_id_before_enrolling);外部调用 6 个(new, from_secs, accept, new, timeout, accept_hdr_async)。

send_client_event2725–2734 ↗
async fn send_client_event(
    websocket: &mut WebSocketStream<TcpStream>,
    client_envelope: ClientEnvelope,
)

作用:把一个远程客户端事件发进 WebSocket。测试用它模拟云端客户端发 ping、initialize、Ack 或关闭事件。

数据流:进去一个可写的 WebSocket 和 ClientEnvelope → 它把事件序列化成 JSON 文本帧 → 发送到 WebSocket,没有返回值,发送失败会让测试报错。

调用关系:消息路由、重连后初始化、Ack 缓存清理、HTTP 模式消息转发测试都会调用它。它是驱动远程控制协议输入的主要小工具。

调用图:被 4 处调用(remote_control_http_mode_enrolls_before_connecting, remote_control_transport_clears_outgoing_buffer_when_backend_acks, remote_control_transport_manages_virtual_clients_and_routes_messages, remote_control_transport_reconnects_after_disconnect);外部调用 3 个(send, to_string, Text)。

read_server_event2736–2738 ↗
async fn read_server_event(websocket: &mut WebSocketStream<TcpStream>) -> serde_json::Value

作用:从 WebSocket 读取一条服务端发给远程客户端的事件,但忽略 stream_id。适合测试只关心消息内容的情况。

数据流:进去一个 WebSocket → 它调用 read_server_event_with_stream_id 读取完整事件,然后丢掉 stream_id → 出来一个 JSON 值。

调用关系:它是 read_server_event_with_stream_id 的简化包装。需要检查服务端消息内容但不需要确认流 ID 的测试会用它。

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

read_server_event_with_stream_id2740–2779 ↗
async fn read_server_event_with_stream_id(
    websocket: &mut WebSocketStream<TcpStream>,
) -> (serde_json::Value, StreamId)

作用:从 WebSocket 读取服务端事件,并额外取出 stream_id。stream_id 可以理解为消息所在的编号通道,Ack 时要用它告诉对方确认了哪条流。

数据流:进去一个 WebSocket → 它循环等文本帧,遇到 Ping 会回 Pong,遇到关闭或二进制帧会报错;读到文本后解析 JSON,并从里面移除 stream_id → 出来事件 JSON 和 StreamId。

调用关系:Ack 缓存清理测试直接用它拿 stream_id;read_server_event 也调用它作为底层实现。它保证测试不会被 WebSocket 心跳帧干扰。

调用图:被 2 处调用(read_server_event, remote_control_transport_clears_outgoing_buffer_when_backend_acks);外部调用 8 个(from_secs, next, send, new, panic!, from_str, timeout, Pong)。

stdio-to-uds/tests/stdio_to_uds.rs源码 ↗
testtest run

这个测试像搭了一个临时电话线。它先建一个临时目录和一个 Unix socket(Unix 系统里的本机通信插口,可以理解成只在本机用的“小门”),再准备一段输入文字 request。测试一边启动一个假服务器,在 socket 那头等连接、收 request、回 response;另一边启动真正的 codex-stdio-to-uds 程序,把 request 文件当成这个程序的标准输入。正确情况是:程序把标准输入转发给 socket,服务器收到 request;服务器写回 response,程序再把它吐到标准输出。文件里还特别处理了超时、stderr 收集、服务器事件记录等情况。这样如果测试偶尔卡住,报错信息会告诉人卡在哪一步,而不是只说失败。

函数细节1
pipes_stdin_and_stdout_through_socket18–157 ↗
async fn pipes_stdin_and_stdout_through_socket() -> anyhow::Result<()>

作用:这个测试函数验证 codex-stdio-to-uds 程序真的能把 stdin(标准输入)通过 Unix socket 发出去,并把 socket 的回复写到 stdout(标准输出)。有人改了这个工具后,可以靠它发现转发方向错了、没有退出、输出不对等问题。

数据流:进去的是一段测试输入 request、一个临时 socket 路径,以及真实启动的 codex-stdio-to-uds 子进程。函数先把 request 写成文件,建立 socket 监听;然后假服务器读取 socket 收到的数据并写回 response;同时子进程从文件读取标准输入,连接 socket,再把回复写到标准输出。出来的结果是几条断言:子进程必须正常退出,stdout 必须等于 response,服务器收到的内容必须等于 request;过程中还会收集 stderr 和服务器事件,方便失败时看清现场。

调用关系:这是整个文件唯一的测试入口,由 Tokio 测试框架自动运行。它自己搭好临时服务器,并通过系统命令启动 codex-stdio-to-uds 这个真正的可执行程序;中途会调用 socket 绑定、文件写入、进程启动、通道通信和断言等外部能力。测试完成时,它用断言把两边的结果对上:服务器证明“输入被送出去了”,子进程 stdout 证明“回复被带回来了”。

调用图:调用 1 个内部函数(bind);外部调用 11 个(Ok, assert!, assert_eq!, eprintln!, format!, channel, write, new, spawn, spawn_blocking (+1 more))。

沙箱策略和 Linux 执行

本节先组织沙箱策略生成测试,然后是使用这些策略的 Linux 沙箱辅助工具和端到端沙箱执行套件。

sandboxing/src/manager_tests.rs源码 ↗
testtest

这个文件不负责真正运行沙箱,而是像质检员一样,专门检查沙箱选择和命令改写是否符合预期。沙箱可以理解成给程序套一个“安全围栏”,限制它能看哪些文件、能不能联网。测试覆盖了几类容易出错的情况:完全开放权限时是否不用沙箱;有网络托管要求时是否仍要启用平台沙箱;额外权限是否能正确增加网络或文件访问;明确禁止的路径是否不会被额外权限覆盖;受管的 MITM CA 证书文件是否会被加入只读名单。Linux 下还额外检查 WSL1 和 bubblewrap 沙箱工具的兼容性,以及 seccomp 辅助程序启动名是否正确。整体上,它保护的是“权限翻译”这条链路:用户给出的权限配置,最后必须变成安全、可执行、不会误放权的执行请求。

函数细节12
danger_full_access_defaults_to_no_sandbox_without_network_requirements27–37 ↗
fn danger_full_access_defaults_to_no_sandbox_without_network_requirements()

作用:检查在文件系统完全开放、网络也允许、并且没有额外网络托管要求时,系统默认不会套沙箱。意思是:既然用户选择了危险的完全访问,就不要假装还有隔离。

数据流:输入是一份“不限制文件访问”的策略、允许网络的策略、自动选择沙箱的偏好,以及“没有受管网络要求”的标记。测试把这些交给 SandboxManager 做初始选择。结果应该是 SandboxType::None,也就是不使用沙箱;测试用断言确认这一点。

调用关系:它直接测试 SandboxManager::select_initial 的一个基础分支。这个场景是其他限制场景的对照组:没有限制、没有特殊网络要求时,不应该调用平台沙箱。

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

danger_full_access_uses_platform_sandbox_with_network_requirements40–52 ↗
fn danger_full_access_uses_platform_sandbox_with_network_requirements()

作用:检查即使文件系统是完全开放的,只要有受管网络要求,系统也会尽量选择平台沙箱。这里的重点是网络要求会改变沙箱选择。

数据流:输入仍然是不限制文件访问和允许网络,但这次“有受管网络要求”为真。测试先算出当前平台可用的沙箱类型作为期望值,再让 SandboxManager 选择。输出必须等于平台支持的沙箱;如果平台没有可用沙箱,就退回 None。

调用关系:它验证 SandboxManager::select_initial 会把网络托管要求纳入决策。它还通过 get_platform_sandbox 取得当前操作系统上理论应选的沙箱,用来和实际结果对比。

调用图:调用 2 个内部函数(unrestricted, new);外部调用 2 个(assert_eq!, get_platform_sandbox)。

restricted_file_system_uses_platform_sandbox_without_managed_network55–72 ↗
fn restricted_file_system_uses_platform_sandbox_without_managed_network()

作用:检查只要文件系统权限被限制,即使没有受管网络要求,也应该使用平台沙箱。因为限制文件访问必须有真正的围栏来执行。

数据流:输入是一份受限文件策略:根目录只读;网络允许;沙箱偏好为自动;没有受管网络要求。测试让 SandboxManager 选择初始沙箱。结果应等于当前平台可用沙箱,没有可用沙箱时才是 None。

调用关系:它测试 SandboxManager::select_initial 的文件权限分支。和完全开放的测试相对,它说明“文件系统受限”本身就足以触发沙箱选择。

调用图:调用 2 个内部函数(restricted, new);外部调用 3 个(assert_eq!, get_platform_sandbox, vec!)。

transform_preserves_unrestricted_file_system_policy_for_restricted_network75–114 ↗
fn transform_preserves_unrestricted_file_system_policy_for_restricted_network()

作用:检查把命令转换成执行请求时,如果文件系统原本是完全开放的,不会因为网络受限就把文件策略改掉。也就是说,网络限制和文件限制不能互相串味。

数据流:输入包括当前工作目录、一个简单的 true 命令、一份“文件不限制但网络受限”的权限档案,以及不启用沙箱的设置。transform 会把 URI 形式的路径转成绝对路径,并生成可执行请求。输出中工作目录保持正确,文件系统策略仍是 unrestricted,网络策略仍是 Restricted。

调用关系:它测试 SandboxManager::transform 的权限翻译过程。它用 PermissionProfile::from_runtime_permissions 先把运行时权限包装成权限档案,再检查 transform 拆回执行请求时没有改变含义。

调用图:调用 5 个内部函数(from_runtime_permissions, unrestricted, new, current_dir, from_abs_path);外部调用 3 个(new, new, assert_eq!)。

transform_additional_permissions_enable_network_for_external_sandbox117–168 ↗
fn transform_additional_permissions_enable_network_for_external_sandbox()

作用:检查外部沙箱模式下,命令自带的额外权限可以把网络从受限改成允许。这个测试防止额外权限被忽略。

数据流:输入是一份 External 权限档案,网络原本是 Restricted;命令里额外声明 network.enabled=true,并附带一个临时目录的文件权限。transform 合并这些信息后,输出的 permission_profile 和 network_sandbox_policy 都应该变成 Enabled。

调用关系:它测试 SandboxManager::transform 合并 AdditionalPermissionProfile 的行为。临时目录和文件权限主要用来模拟真实命令携带额外授权的情况,但这个测试关注的核心是网络权限是否被正确放开。

调用图:调用 5 个内部函数(from_read_write_roots, new, current_dir, from_absolute_path, from_abs_path);外部调用 6 个(new, new, new, assert_eq!, canonicalize, vec!)。

transform_additional_permissions_preserves_denied_entries171–250 ↗
fn transform_additional_permissions_preserves_denied_entries()

作用:检查新增文件权限时,原来明确禁止访问的路径不会被覆盖。这个很重要,因为“禁止”通常应该比“允许”更硬。

数据流:输入是一份受限文件策略:根目录只读,并且某个 denied 路径明确 Deny;命令额外请求另一个 allowed 路径的写权限。transform 合并后,输出策略应该依次保留根目录只读、保留 denied 路径禁止访问,再追加 allowed 路径写权限;网络仍保持 Restricted。

调用关系:它测试 SandboxManager::transform 在合并文件系统权限时的安全性。它特别防止一个危险错误:追加允许权限时不小心丢掉原有的拒绝规则。

调用图:调用 7 个内部函数(from_read_write_roots, from_runtime_permissions, restricted, new, current_dir, from_absolute_path, from_abs_path);外部调用 7 个(default, new, new, new, assert_eq!, canonicalize, vec!)。

managed_mitm_ca_bundle_becomes_readable_for_restricted_sandbox253–292 ↗
fn managed_mitm_ca_bundle_becomes_readable_for_restricted_sandbox()

作用:检查当使用受限沙箱时,受管的 MITM CA 证书包会被加入可读名单。MITM CA 可以理解成公司或系统用来检查 HTTPS 流量的可信证书文件;如果沙箱看不到它,网络请求可能会失败。

数据流:输入是一份只允许读取当前工作目录的受限权限档案,以及一个受管 CA 证书文件路径。with_managed_mitm_ca_readable_root 会把证书路径作为只读文件权限加入档案。输出再转回运行时文件策略时,应同时包含工作目录只读和证书文件只读。

调用关系:它直接测试 with_managed_mitm_ca_readable_root 这个辅助逻辑。它位于权限生成和命令执行之间,保证受限环境里仍能读取必要的证书文件。

调用图:调用 3 个内部函数(from_runtime_permissions, restricted, from_absolute_path);外部调用 5 个(new, assert_eq!, canonicalize, with_managed_mitm_ca_readable_root, vec!)。

transform_linux_seccomp_request295–322 ↗
fn transform_linux_seccomp_request(
    codex_linux_sandbox_exe: &std::path::Path,
) -> super::SandboxExecRequest

作用:这是 Linux 专用的测试辅助函数,用来快速造出一个使用 LinuxSeccomp 沙箱的执行请求。seccomp 是 Linux 的系统调用过滤机制,可以限制程序能向操作系统请求什么操作。

数据流:输入是 codex-linux-sandbox 辅助程序的路径。函数创建 SandboxManager、取得当前目录、构造一个运行 true 命令的请求,并指定沙箱类型为 LinuxSeccomp。输出是 transform 生成的 SandboxExecRequest,供后面的测试检查其中的启动参数。

调用关系:它不是独立测试,而是被 transform_linux_seccomp_preserves_helper_path_in_arg0_when_available 和 transform_linux_seccomp_uses_helper_alias_when_launcher_is_not_helper_path 调用。这样两个测试不用重复搭建同样的 Linux seccomp 请求。

调用图:调用 3 个内部函数(new, current_dir, from_abs_path);被 2 处调用(transform_linux_seccomp_preserves_helper_path_in_arg0_when_available, transform_linux_seccomp_uses_helper_alias_when_launcher_is_not_helper_path);外部调用 2 个(new, new)。

wsl1_rejects_linux_bubblewrap_path326–361 ↗
fn wsl1_rejects_linux_bubblewrap_path()

作用:检查在 WSL1 环境下,需要走 bubblewrap 的 Linux 沙箱路径会被拒绝。WSL1 是早期 Windows Linux 子系统,和真正 Linux 内核行为不完全一样;bubblewrap 是一种 Linux 沙箱工具,在这里不能安全依赖。

数据流:输入包括受限文件策略、是否使用旧版 landlock、是否为了代理允许网络、以及 is_wsl1=true。测试多次调用 ensure_linux_bubblewrap_is_supported。只要这些组合会走 bubblewrap,就应该返回 Wsl1UnsupportedForBubblewrap 错误。

调用关系:它测试 Linux 平台兼容性检查函数 ensure_linux_bubblewrap_is_supported。这个检查发生在 transform 生成执行请求之前,用来提前阻止不支持的沙箱方案。

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

wsl1_allows_non_bubblewrap_linux_paths365–391 ↗
fn wsl1_allows_non_bubblewrap_linux_paths()

作用:检查 WSL1 并不是一律禁止所有 Linux 沙箱路径,只禁止会用到 bubblewrap 的情况。这样可以避免过度拦截本来安全可用的路径。

数据流:输入是两组 WSL1 情况:一种是文件系统不限制且不需要代理网络,另一种是受限文件策略但使用旧版 landlock 且不允许代理网络。测试调用 ensure_linux_bubblewrap_is_supported,输出都应该是 Ok。

调用关系:它和 wsl1_rejects_linux_bubblewrap_path 配成一组:前者确认该拒绝的会拒绝,这个确认不该拒绝的会放行。两者一起锁住 WSL1 兼容性判断的边界。

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

transform_linux_seccomp_preserves_helper_path_in_arg0_when_available395–403 ↗
fn transform_linux_seccomp_preserves_helper_path_in_arg0_when_available()

作用:检查当提供的 Linux seccomp 辅助程序路径本身就是 codex-linux-sandbox 时,执行请求会把这个完整路径放进 arg0。arg0 是程序启动时看到的“自己叫什么”的名字。

数据流:输入是 /tmp/codex-linux-sandbox 这个辅助程序路径。测试通过 transform_linux_seccomp_request 生成执行请求。输出中的 exec_request.arg0 应该等于这个完整路径的字符串。

调用关系:它调用 transform_linux_seccomp_request 来搭建标准请求,然后只检查 arg0。它验证 transform 在辅助程序路径明确可用时,不会把路径改成别名。

调用图:调用 1 个内部函数(transform_linux_seccomp_request);外部调用 2 个(assert_eq!, from)。

transform_linux_seccomp_uses_helper_alias_when_launcher_is_not_helper_path407–412 ↗
fn transform_linux_seccomp_uses_helper_alias_when_launcher_is_not_helper_path()

作用:检查当启动器路径不是 codex-linux-sandbox 本身时,执行请求会把 arg0 改成 codex-linux-sandbox 这个别名。这样辅助程序即使通过别的启动器进入,也能按正确身份运行。

数据流:输入是 /tmp/codex 这个路径,它不是辅助程序的标准名字。测试用 transform_linux_seccomp_request 生成执行请求。输出中的 exec_request.arg0 应该是 codex-linux-sandbox,而不是原始路径。

调用关系:它也复用 transform_linux_seccomp_request。它和上一个 arg0 测试一起覆盖两种分支:路径已经是辅助程序时保留路径,不是时使用辅助程序别名。

调用图:调用 1 个内部函数(transform_linux_seccomp_request);外部调用 2 个(assert_eq!, from)。

sandboxing/src/landlock_tests.rs源码 ↗
testtest

这个文件不是正式运行时会执行的功能代码,而是一组自动测试。它检查“生成 Linux 沙箱启动命令”这件事有没有按约定办事。可以把沙箱命令想成一张进门清单:哪些目录能看、能不能联网、要不要用旧版 Landlock。Landlock 是 Linux 的一种文件访问限制机制,简单说就是给程序划出能碰和不能碰的范围。这里重点测试三件事:用户要求旧版 Landlock 时,参数里要有对应开关;如果允许代理联网,这个选择要优先,不能同时混进旧版 Landlock 开关;权限配置文件和工作目录要被正确写进参数。最后还单独确认:只有启用了“托管网络”要求时,代理联网才会被允许。这样可以防止一个小小的参数拼错,导致沙箱安全边界变松或功能失效。

函数细节4
legacy_landlock_flag_is_included_when_requested5–33 ↗
fn legacy_landlock_flag_is_included_when_requested()

作用:这个测试确认:只有明确要求使用旧版 Landlock 时,生成的沙箱命令里才会出现 --use-legacy-landlock。这样能防止默认情况下误用旧兼容模式,也能保证需要兼容旧环境时不会漏掉开关。

数据流:它先准备一个简单命令 /bin/true,再准备命令工作目录 /tmp/link 和实际当前目录 /tmp。第一次生成沙箱参数时不要求旧版 Landlock,于是检查结果里没有对应开关;第二次把要求打开,再检查结果里确实有这个开关。输入是命令、路径和布尔开关,输出不是新数据,而是测试通过或失败。

调用关系:这个测试会在测试运行器执行测试套件时被调用。它主要把样例输入交给沙箱参数生成函数,然后用断言检查结果,像质检员一样确认“旧版 Landlock”这个开关没有被漏加或误加。它还会用 Path::new 准备路径,用 vec! 准备命令列表,用 assert_eq! 判断结果是否符合预期。

调用图:外部调用 3 个(new, assert_eq!, vec!)。

proxy_flag_takes_precedence_over_legacy_landlock36–55 ↗
fn proxy_flag_takes_precedence_over_legacy_landlock()

作用:这个测试确认:当代理需要联网时,--allow-network-for-proxy 要优先于 --use-legacy-landlock。也就是说,即使同时传入“使用旧版 Landlock”的请求,代理联网模式也不能和它混在一起。

数据流:它准备一个只读权限配置、一个简单命令和两个路径,然后调用按权限配置生成沙箱参数的函数,并同时打开“旧版 Landlock”和“允许代理联网”两个输入开关。生成结果出来后,它检查参数里有代理联网开关,同时没有旧版 Landlock 开关。结果是测试通过或失败,不会修改外部状态。

调用关系:这个测试位于沙箱参数规则的冲突检查位置:当两个开关看起来都可能被加入时,它确认真正生效的是代理联网。它会先通过 PermissionProfile::read_only 准备只读权限配置,再把这些输入交给参数生成函数,最后用断言判断生成出来的命令参数是否遵守优先级规则。

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

permission_profile_flag_is_included58–83 ↗
fn permission_profile_flag_is_included()

作用:这个测试确认:使用权限配置生成沙箱参数时,命令参数里必须包含权限配置开关,并且命令工作目录也要正确写进去。这样沙箱才知道该按什么权限运行、从哪个目录启动命令。

数据流:它准备 /bin/true 这个命令、/tmp/link 这个命令工作目录、/tmp 这个当前目录,以及一个只读权限配置。然后生成沙箱参数,并在参数列表里找两组相邻内容:一组是 --permission-profile 后面跟着非空配置值,另一组是 --command-cwd 后面跟着 /tmp/link。如果都找到了,测试通过;否则说明参数生成有问题。

调用关系:这个测试检查的是权限配置这条路径是否把关键信息带进最终命令。它使用 PermissionProfile::read_only 创建测试用的权限档案,用路径构造函数准备目录,再调用沙箱参数生成函数。之后它不关心参数完整长什么样,只关心关键片段是否按顺序出现。

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

proxy_network_requires_managed_requirements86–95 ↗
fn proxy_network_requires_managed_requirements()

作用:这个测试确认:代理联网不是随便就能打开,必须在“强制托管网络”开启时才允许。这样可以避免代理绕过项目对网络访问的统一控制。

数据流:它直接把 falsetrue 两种输入分别传给 allow_network_for_proxy。输入为 false 时,期望输出也是 false;输入为 true 时,期望输出是 true。它只验证这个判断规则,不创建命令,也不改动任何状态。

调用关系:这个测试处在更小、更基础的规则检查层。其他生成沙箱参数的逻辑会依赖“代理是否允许联网”这个判断,而这个测试专门确认这个判断没有变形。它把两个最简单的布尔值交给判断函数,再用断言确认结果。

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

sandboxing/src/seatbelt_tests.rs源码 ↗
testtest time

macOS 的 Seatbelt 可以理解成一张“准入清单”:哪些文件能读写、哪些网络能连、哪些本地 socket 能用,都要写清楚。这个测试文件就是反复检查这张清单有没有写错。它会生成沙盒命令参数和策略文本,然后看里面是否包含该有的允许规则、是否没有不该有的大开口。更重要的是,它还会在临时目录里造出 Git 仓库、.codex 配置等敏感位置,真的启动 sandbox-exec 去试写文件,确认这些地方不会被恶意改掉。文件里还覆盖了代理端口、DNS、本地绑定、Unix 域套接字(一种本机进程之间通信的“本地插座”)、不可读路径和通配符规则。没有这些测试,沙盒规则一旦退化,可能会让受限命令偷偷改 Git hook、改 Codex 配置,或者绕过代理直接联网。

函数细节36
assert_seatbelt_denied40–48 ↗
fn assert_seatbelt_denied(stderr: &[u8], path: &Path)

作用:检查一个被沙盒拦下来的命令,报错信息是不是符合预期。这样测试不只看“失败了”,还确认失败原因确实是沙盒拒绝。

数据流:输入是一段标准错误输出和被访问的路径 → 它把错误输出转成文字,并拼出理想中的“Operation not permitted”提示 → 如果实际提示不匹配,就让测试失败;它不返回业务结果,只负责断言。

调用关系:在真正运行 sandbox-exec 的测试里使用,尤其是写 .codex、.git 等敏感文件失败后调用。它是这些集成式测试的验收员,确认失败来自 Seatbelt 拦截。

调用图:被 2 处调用(create_seatbelt_args_with_read_only_git_and_codex_subpaths, create_seatbelt_args_with_read_only_git_pointer_file);外部调用 3 个(from_utf8_lossy, assert!, format!)。

absolute_path50–52 ↗
fn absolute_path(path: &str) -> AbsolutePathBuf

作用:把测试里写死的绝对路径字符串变成项目使用的绝对路径类型。这样后面的沙盒策略不会误拿相对路径当安全路径。

数据流:输入是一个路径字符串 → 它把字符串变成 Path,再要求它必须是绝对路径 → 输出 AbsolutePathBuf;如果不是绝对路径,测试会直接失败。

调用关系:多个测试用它快速准备不可读路径或 Unix socket 路径。它把测试数据整理成 create_seatbelt_command_args 和 dynamic_network_policy 能接受的格式。

调用图:调用 1 个内部函数(from_absolute_path);被 2 处调用(explicit_unreadable_paths_are_excluded_from_full_disk_read_and_write_access, explicit_unreadable_paths_are_excluded_from_readable_roots);外部调用 1 个(new)。

seatbelt_policy_arg54–61 ↗
fn seatbelt_policy_arg(args: &[String]) -> &str

作用:从生成出来的一串 sandbox-exec 命令参数里,找出真正的策略文本。测试需要直接检查策略内容时会用它。

数据流:输入是命令参数列表 → 它找到 “-p” 这个参数的位置,并取后面的字符串 → 输出 Seatbelt 策略文本;如果没有找到,说明参数生成错了,测试失败。

调用关系:很多测试先调用沙盒参数生成函数,再用它抽出策略文本检查。它像从包装盒里拿出说明书,方便后续断言逐条核对规则。

调用图:被 7 处调用(create_seatbelt_args_allowlists_explicit_unix_socket_paths_without_proxy, create_seatbelt_args_block_first_time_dot_codex_creation_with_metadata_name_regex, create_seatbelt_args_for_cwd_as_git_repo, create_seatbelt_args_preserves_full_network_with_explicit_unix_socket_paths, create_seatbelt_args_with_read_only_git_and_codex_subpaths, explicit_unreadable_paths_are_excluded_from_full_disk_read_and_write_access, explicit_unreadable_paths_are_excluded_from_readable_roots)。

seatbelt_protected_metadata_name_requirements63–81 ↗
fn seatbelt_protected_metadata_name_requirements(root: &Path) -> String

作用:生成一段用于保护敏感元数据目录名的 Seatbelt 检查文本,比如 .git、.codex 这类目录。它帮助测试确认策略会阻止新建或改写这些危险位置。

数据流:输入是一个根目录路径 → 它去掉多余的结尾斜杠,把路径和敏感目录名转义成正则表达式安全文本 → 输出一串 require-not 规则,表示这些敏感名字不能被写入。

调用关系:它主要服务于后面的策略文本断言。测试生成 Seatbelt 策略后,会拿它拼出的标准片段去比对,确认 metadata 保护确实出现在策略里。

调用图:外部调用 5 个(ends_with, len, pop, to_string_lossy, escape)。

TestConfigReloader::source_label86–88 ↗
fn source_label(&self) -> String

作用:给测试用的网络代理配置来源起一个名字。这个名字只用于标识,不真的加载配置。

数据流:没有外部输入 → 它返回固定文字“seatbelt test config” → 不修改任何状态。

调用关系:它实现 ConfigReloader 接口,是搭建测试版 NetworkProxy 时需要的一块假组件。真实系统会用配置重载器读配置,这里只提供最小可用替身。

TestConfigReloader::maybe_reload90–92 ↗
fn maybe_reload(&self) -> ConfigReloaderFuture<'_, Option<ConfigState>>

作用:模拟“看看配置要不要重载”,但测试里永远说不需要。这样网络代理对象可以正常创建,却不会真的碰外部配置。

数据流:没有配置输入 → 它返回一个异步结果,内容是 Ok(None),意思是没有新配置 → 不改状态。

调用关系:create_seatbelt_args_merges_proxy_and_explicit_unix_socket_paths 构建 NetworkProxy 时会间接依赖它。它让测试专注于沙盒参数合并,而不是配置热更新。

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

TestConfigReloader::reload_now94–96 ↗
fn reload_now(&self) -> ConfigReloaderFuture<'_, ConfigState>

作用:模拟“立刻重载配置”,但测试中明确不支持这个动作。若有人误调用,它会报错。

数据流:没有配置输入 → 它返回一个异步错误,说明测试配置不能重载 → 不产生新的 ConfigState。

调用关系:它也是 ConfigReloader 接口的一部分。测试版 NetworkProxy 需要这个方法存在,但正常测试流程不会靠它拿配置。

调用图:外部调用 2 个(pin, anyhow!)。

base_policy_allows_node_cpu_sysctls100–109 ↗
fn base_policy_allows_node_cpu_sysctls()

作用:确认基础沙盒策略允许 Node.js 查询 CPU 和硬件型号。否则像 os.cpus() 这样的常见功能可能在沙盒里莫名失败。

数据流:输入是固定的基础策略常量 → 它检查里面是否包含两个 sysctl 名称,也就是 macOS 查询系统信息的入口 → 如果缺少任一规则,测试失败。

调用关系:它直接守住 MACOS_SEATBELT_BASE_POLICY 的兼容性。这个测试不生成新策略,只验证基础策略别把常见运行时能力删掉。

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

create_seatbelt_args_routes_network_through_proxy_ports125–161 ↗
fn create_seatbelt_args_routes_network_through_proxy_ports()

作用:检查有代理端口时,网络只能通向这些本地代理端口,而不是直接全网放行。这样网络请求会被代理接管。

数据流:输入是只读沙盒策略和两个代理端口 → 它调用 dynamic_network_policy 生成网络规则 → 检查策略允许 localhost:43128 和 localhost:48081,同时不允许裸的全网出站、本地监听和 DNS。

调用关系:它验证 dynamic_network_policy 在有代理配置时的核心行为。代理端口是允许的出口,其他网络能力必须保持关闭。

调用图:外部调用 5 个(new_read_only_policy, assert!, default, dynamic_network_policy, vec!)。

dynamic_network_policy_allows_tls_without_darwin_user_cache_write164–184 ↗
fn dynamic_network_policy_allows_tls_without_darwin_user_cache_write()

作用:确认开启网络时仍允许 TLS 证书校验需要的 trustd 服务,但不会顺手开放用户缓存目录写权限。TLS 可以理解成 HTTPS 的安全证书检查。

数据流:输入是带网络权限的工作区写策略 → 它生成动态网络策略 → 检查策略包含 trustd agent 访问,同时不包含 DARWIN_USER_CACHE_DIR 这类宽泛写权限。

调用关系:它守住 dynamic_network_policy 的平衡点:HTTPS 要能正常验证证书,但不能因此把用户缓存目录变成可写漏洞。

调用图:外部调用 4 个(assert!, default, dynamic_network_policy, vec!)。

explicit_unreadable_paths_are_excluded_from_full_disk_read_and_write_access187–257 ↗
fn explicit_unreadable_paths_are_excluded_from_full_disk_read_and_write_access()

作用:确认即使策略给了根目录读写权限,明确标成“不可访问”的路径仍会被挖掉。就像一张全楼通行证里专门划掉了保险库。

数据流:输入是根目录可写和 /tmp/codex-unreadable 拒绝访问的文件系统策略 → 它生成 Seatbelt 命令参数并抽出策略文本 → 检查读写规则都带排除项,参数里也传入了被排除路径和受保护元数据路径。

调用关系:它调用 absolute_path、create_seatbelt_command_args 和 seatbelt_policy_arg,把文件系统策略变成实际 Seatbelt 文本再验收。它覆盖的是“宽权限里再减掉危险区域”的场景。

调用图:调用 3 个内部函数(restricted, absolute_path, seatbelt_policy_arg);外部调用 5 个(new, assert!, assert_eq!, create_seatbelt_command_args, vec!)。

explicit_unreadable_paths_are_excluded_from_readable_roots260–308 ↗
fn explicit_unreadable_paths_are_excluded_from_readable_roots()

作用:确认一个只读目录里面也可以有明确不可读的子目录。这样用户允许读项目时,仍能保护项目里的私密子文件夹。

数据流:输入是 /tmp/codex-readable 可读、其 private 子目录拒绝访问的策略 → 它生成 Seatbelt 参数和策略文本 → 检查可读根目录参数存在,同时不可读子目录作为排除参数出现。

调用关系:它和上一条测试类似,但关注的是只读根目录。它验证 create_seatbelt_command_args 会把 deny 规则正确嵌进 readable root。

调用图:调用 3 个内部函数(restricted, absolute_path, seatbelt_policy_arg);外部调用 4 个(new, assert!, create_seatbelt_command_args, vec!)。

unreadable_globstar_slash_matches_zero_or_more_directories311–321 ↗
fn unreadable_globstar_slash_matches_zero_or_more_directories()

作用:检查不可读通配符里的 / 能匹配零层或多层目录。这样 /tmp/repo//*.env 既能挡住根目录下的 .env,也能挡住子目录里的 env 文件。

数据流:输入是一个 glob 模式字符串 → 它调用 seatbelt_regex_for_unreadable_glob 转成正则表达式 → 再用几个样例路径验证该匹配的匹配、不该匹配的不匹配。

调用关系:它专门验收 glob 转正则的规则。后续生成 Seatbelt 的 deny regex 时,会依赖这个转换结果。

调用图:外部调用 4 个(assert!, assert_eq!, new, seatbelt_regex_for_unreadable_glob)。

unreadable_globs_use_git_style_component_matching324–337 ↗
fn unreadable_globs_use_git_style_component_matching()

作用:检查通配符按 Git 风格匹配路径片段:星号不会跨过斜杠,字符范围和问号按预期工作。这样规则不会意外挡住更深层目录。

数据流:输入是带 *, [0-9], ? 的 glob 模式 → 它转成正则表达式 → 用文件路径样例确认只匹配一层目录里的 file42.txt 这类文件。

调用关系:它继续保护 seatbelt_regex_for_unreadable_glob 的语义。Seatbelt 最终只懂正则,所以这里确保转换没有扩大或缩小范围。

调用图:外部调用 4 个(assert!, assert_eq!, new, seatbelt_regex_for_unreadable_glob)。

unreadable_globs_treat_unclosed_character_classes_as_literals340–349 ↗
fn unreadable_globs_treat_unclosed_character_classes_as_literals()

作用:检查没有闭合的字符组,比如 “[*.env”,会被当成普通字符处理,而不是生成坏掉的规则。这样用户写错 glob 时不会让沙盒策略崩溃或误判。

数据流:输入是一个含未闭合方括号的 glob → 它转换成正则 → 验证只有路径中真的带 [ 的文件会匹配。

调用关系:它保障 glob 转换函数面对异常输入时仍安全保守。后面的不可读 glob 策略构建依赖这种稳定性。

调用图:外部调用 4 个(assert!, assert_eq!, new, seatbelt_regex_for_unreadable_glob)。

unreadable_glob_policy_includes_canonicalized_static_prefix353–384 ↗
fn unreadable_glob_policy_includes_canonicalized_static_prefix()

作用:确认不可读 glob 策略会把固定路径前缀规范化,也就是把符号链接换成真实路径。这样通过软链接绕过规则的机会会减少。

数据流:输入是一个临时目录,其中 link-root 指向 real-root,再给出 link-root/**/*.env 规则 → 它生成不可读 glob 的 Seatbelt 策略 → 检查策略里使用的是真实路径对应的正则。

调用关系:它调用 build_seatbelt_unreadable_glob_policy 和 seatbelt_regex_for_unreadable_glob。这个测试连接了文件系统真实路径和策略文本生成,防止链接路径造成漏拦。

调用图:调用 1 个内部函数(default);外部调用 6 个(new, assert!, format!, create_dir, build_seatbelt_unreadable_glob_policy, seatbelt_regex_for_unreadable_glob)。

seatbelt_args_without_extension_profile_keep_legacy_preferences_read_access387–399 ↗
fn seatbelt_args_without_extension_profile_keep_legacy_preferences_read_access()

作用:确认旧版策略生成方式仍允许读取用户偏好设置,但不允许写。这样老路径保持兼容,同时不扩大写权限。

数据流:输入是一个只读沙盒策略和简单 echo 命令 → 它生成旧版 Seatbelt 参数 → 检查策略包含 user-preference-read,不包含 user-preference-write。

调用关系:它专门覆盖 create_seatbelt_command_args_for_legacy_policy。重点是旧接口在没有 extension profile 时的权限边界别变。

调用图:外部调用 5 个(new_read_only_policy, assert!, temp_dir, create_seatbelt_command_args_for_legacy_policy, vec!)。

create_seatbelt_args_allows_local_binding_when_explicitly_enabled402–434 ↗
fn create_seatbelt_args_allows_local_binding_when_explicitly_enabled()

作用:检查只有明确允许本地绑定时,沙盒才开放本机监听、本机回环连接和 DNS。否则这些能力不会偷偷打开。

数据流:输入是只读策略、一个代理端口和 allow_local_binding=true → 它生成动态网络策略 → 检查本地 bind、localhost 入站/出站和 DNS 出站存在,同时仍没有全网出站。

调用关系:它验证 dynamic_network_policy 对 allow_local_binding 开关的处理。这个开关像一把单独的钥匙,只在显式打开时生效。

调用图:外部调用 5 个(new_read_only_policy, assert!, default, dynamic_network_policy, vec!)。

dynamic_network_policy_preserves_restricted_policy_when_proxy_config_without_ports437–470 ↗
fn dynamic_network_policy_preserves_restricted_policy_when_proxy_config_without_ports()

作用:检查有代理配置但没有可用端口时,策略保持受限,而不是误以为可以放开网络。没有代理端口就应该“失败关闭”。

数据流:输入是带网络意图的工作区策略,但代理端口列表为空 → 它生成动态网络策略 → 检查仍保留受限网络配置,不出现全网出站、代理端口或 DNS 放行。

调用关系:它守住 dynamic_network_policy 的安全默认值。代理配置不完整时,函数不能为了可用性而牺牲隔离。

调用图:外部调用 4 个(assert!, default, dynamic_network_policy, vec!)。

dynamic_network_policy_blocks_dns_when_local_binding_has_no_proxy_ports473–498 ↗
fn dynamic_network_policy_blocks_dns_when_local_binding_has_no_proxy_ports()

作用:确认即使允许本地绑定,只要没有代理端口,也不能放开 DNS 出站。DNS 是把域名查成 IP 的网络请求,也可能泄漏访问意图。

数据流:输入是允许本地绑定但代理端口为空的配置 → 它生成网络策略 → 检查本地绑定规则存在,但 DNS 出站规则不存在。

调用关系:它补充上一条测试的边界情况。dynamic_network_policy 必须区分“可以本地监听”和“可以向外查 DNS”。

调用图:外部调用 4 个(assert!, default, dynamic_network_policy, vec!)。

dynamic_network_policy_preserves_restricted_policy_for_managed_network_without_proxy_config501–530 ↗
fn dynamic_network_policy_preserves_restricted_policy_for_managed_network_without_proxy_config()

作用:检查启用受管理网络但没有代理端点时,策略仍然受限。这样不会因为“应该由系统管网络”这个设定就直接打开外网。

数据流:输入是 enforce_managed_network=true、但没有代理配置和端口 → 它生成网络策略 → 检查保留受限网络规则,并且没有全网出站或 DNS。

调用关系:它验证 dynamic_network_policy 在受管理网络模式下的保守行为。没有实际出口时,策略保持关闭。

调用图:外部调用 4 个(assert!, default, dynamic_network_policy, vec!)。

create_seatbelt_args_allowlists_unix_socket_paths533–567 ↗
fn create_seatbelt_args_allowlists_unix_socket_paths()

作用:检查 Unix 域套接字只允许访问明确列出的路径。Unix socket 是本机进程通信的文件式通道,不应该随便全开。

数据流:输入是代理端口和一个允许的 /tmp/example.sock 路径 → 它生成网络策略 → 检查允许 AF_UNIX socket 创建、绑定和连接到参数指定的子路径,同时不使用旧的笼统 network* 子路径规则。

调用关系:它验证 dynamic_network_policy 和 UnixDomainSocketPolicy::Restricted 的配合。策略应该精确到白名单路径,而不是粗放允许。

调用图:外部调用 4 个(new_read_only_policy, assert!, dynamic_network_policy, vec!)。

create_seatbelt_args_allowlists_explicit_unix_socket_paths_without_proxy570–607 ↗
fn create_seatbelt_args_allowlists_explicit_unix_socket_paths_without_proxy()

作用:确认即使没有网络代理,也能单独允许某些显式传入的 Unix socket 路径。这样浏览器之类本地工具需要的本机通信可以被安全放行。

数据流:输入是只读文件系统策略、受限网络策略和额外允许的 /tmp/codex-browser-use → 它生成 Seatbelt 参数 → 检查策略允许 AF_UNIX 出站,并且参数里带有规范化后的 UNIX_SOCKET_PATH_0。

调用关系:它调用 create_seatbelt_command_args 和 seatbelt_policy_arg。它覆盖的是 extra_allow_unix_sockets 这条不依赖代理配置的通路。

调用图:调用 2 个内部函数(from_legacy_sandbox_policy_for_cwd, seatbelt_policy_arg);外部调用 7 个(new, new, new_read_only_policy, assert!, create_seatbelt_command_args, normalize_path_for_sandbox, vec!)。

create_seatbelt_args_merges_proxy_and_explicit_unix_socket_paths610–666 ↗
async fn create_seatbelt_args_merges_proxy_and_explicit_unix_socket_paths() -> anyhow::Result<()>

作用:检查来自网络代理配置的 Unix socket 路径和调用方额外指定的路径会合并在一起,而且顺序稳定。这样两个来源不会互相覆盖。

数据流:输入是一个测试版 NetworkProxy,其中配置允许 /tmp/codex-proxy-use,另有显式允许 /tmp/codex-browser-use → 它生成 Seatbelt 参数 → 输出中应有两个 UNIX_SOCKET_PATH 参数,先显式路径,后代理路径。

调用关系:它用 TestConfigReloader 搭建网络代理,再交给 create_seatbelt_command_args。这个异步测试验证网络配置和调用参数在最终沙盒参数里正确汇合。

调用图:调用 3 个内部函数(builder, with_reloader, from_legacy_sandbox_policy_for_cwd);外部调用 11 个(new, new, new, new_read_only_policy, assert_eq!, build_config_state, default, default, create_seatbelt_command_args, normalize_path_for_sandbox (+1 more))。

create_seatbelt_args_preserves_full_network_with_explicit_unix_socket_paths669–701 ↗
fn create_seatbelt_args_preserves_full_network_with_explicit_unix_socket_paths()

作用:确认当网络策略本来就是全开时,额外允许 Unix socket 不会把全网权限意外改窄。同时 Unix socket 的专门规则仍会加入。

数据流:输入是 Enabled 网络策略和一个额外 Unix socket 路径 → 它生成 Seatbelt 参数并取出策略 → 检查全出站、全入站仍存在,同时也允许指定 socket 出站。

调用关系:它测试 create_seatbelt_command_args 对“全网络 + 额外 socket”组合的处理。重点是添加一条 socket 规则时不要破坏原本的网络模式。

调用图:调用 2 个内部函数(from_legacy_sandbox_policy_for_cwd, seatbelt_policy_arg);外部调用 5 个(new, new_read_only_policy, assert!, create_seatbelt_command_args, vec!)。

unix_socket_policy_non_empty_output_is_newline_terminated704–724 ↗
fn unix_socket_policy_non_empty_output_is_newline_terminated()

作用:检查生成出来的 Unix socket 策略片段只要不为空,就以换行结尾。这个小细节能避免拼接策略文本时两条规则黏在一起。

数据流:输入分别是受限白名单 socket 配置和允许全部 socket 配置 → 它调用 unix_socket_policy 生成文本 → 检查两段文本最后都是换行符。

调用关系:它直接验收 unix_socket_policy 的输出格式。格式看似小事,但 Seatbelt 策略是文本拼接,缺换行可能让整体策略变坏。

调用图:外部调用 4 个(assert!, default, unix_socket_policy, vec!)。

unix_socket_dir_params_use_stable_param_names727–752 ↗
fn unix_socket_dir_params_use_stable_param_names()

作用:确认 Unix socket 路径参数会去重、排序,并使用稳定的名字。稳定输出让策略可预测,也让测试和调试更简单。

数据流:输入是三个路径,其中 /tmp/a.sock 重复且顺序在 /tmp/b.sock 后面 → 它调用 unix_socket_dir_params → 输出应是 UNIX_SOCKET_PATH_0=/tmp/a.sock、UNIX_SOCKET_PATH_1=/tmp/b.sock。

调用关系:它验证 unix_socket_dir_params 这个参数准备步骤。dynamic_network_policy 生成规则时会引用这些稳定参数名。

调用图:外部调用 4 个(assert_eq!, default, unix_socket_dir_params, vec!)。

normalize_path_for_sandbox_rejects_relative_paths755–757 ↗
fn normalize_path_for_sandbox_rejects_relative_paths()

作用:确认沙盒路径规范化函数拒绝相对路径。相对路径会随当前目录变化,拿来做安全规则很危险。

数据流:输入是 relative.sock 这样的相对路径 → 它调用 normalize_path_for_sandbox → 期望输出 None,表示不能使用。

调用关系:它守住 normalize_path_for_sandbox 的基本安全门槛。其他测试会用这个函数处理真正要传给 Seatbelt 的 socket 路径。

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

create_seatbelt_args_allows_all_unix_sockets_when_enabled760–788 ↗
fn create_seatbelt_args_allows_all_unix_sockets_when_enabled()

作用:检查配置成允许所有 Unix socket 时,策略确实开放所有本机 socket 绑定和连接。这个模式比白名单更宽,所以测试要确认它是显式开启才出现。

数据流:输入是代理端口和 UnixDomainSocketPolicy::AllowAll → 它生成动态网络策略 → 检查允许 AF_UNIX 创建、任意本地 socket bind 和任意远端 socket outbound,同时不使用旧的泛化规则。

调用关系:它覆盖 dynamic_network_policy 里 Unix socket 的 AllowAll 分支。和白名单测试一起,证明两种模式输出不同且清楚。

调用图:外部调用 4 个(new_read_only_policy, assert!, dynamic_network_policy, vec!)。

create_seatbelt_args_full_network_with_proxy_is_still_proxy_only791–820 ↗
fn create_seatbelt_args_full_network_with_proxy_is_still_proxy_only()

作用:确认即使高层策略说“需要网络”,只要代理已配置,最终也只允许连代理端口,而不是直接全网访问。这样代理管控不会被全网络标志绕开。

数据流:输入是带 network_access=true 的工作区策略和一个代理端口 → 它生成网络策略 → 检查允许 localhost:43128,但没有全出站或全入站规则。

调用关系:它验证 dynamic_network_policy 的优先级:有代理时走代理优先。这个测试防止未来改动把代理模式退化成裸联网。

调用图:外部调用 4 个(assert!, default, dynamic_network_policy, vec!)。

create_seatbelt_args_with_read_only_git_and_codex_subpaths823–1070 ↗
fn create_seatbelt_args_with_read_only_git_and_codex_subpaths()

作用:这是一个更接近真实场景的安全测试:工作区可写,但 .git、.codex 等敏感子路径必须只读。它确认攻击者不能通过修改 Git hook 或 Codex 配置来提升权限。

数据流:输入是临时创建的工作区,其中一个根目录含 .git 和 .codex,另一个为空 → 它生成 Seatbelt 参数,检查策略和 -D 参数都包含敏感路径排除项 → 然后实际运行 sandbox-exec 试写 .codex/config.toml 和 .git/hooks/pre-commit,确认失败;再试写普通文件,确认允许。

调用关系:它调用 populate_tmpdir 准备测试仓库,调用 create_seatbelt_command_args_for_legacy_policy 生成沙盒命令,并用 assert_seatbelt_denied 验证拒绝结果。它是本文件最重要的端到端防护测试之一。

调用图:调用 3 个内部函数(assert_seatbelt_denied, populate_tmpdir, seatbelt_policy_arg);外部调用 9 个(from_utf8_lossy, new, assert!, assert_eq!, new, format!, create_dir_all, create_seatbelt_command_args_for_legacy_policy, vec!)。

create_seatbelt_args_block_first_time_dot_codex_creation_with_metadata_name_regex1073–1120 ↗
fn create_seatbelt_args_block_first_time_dot_codex_creation_with_metadata_name_regex()

作用:确认即使 .codex 目录原本不存在,沙盒也会阻止第一次创建它。否则攻击者可以先新建 .codex,再写入危险配置。

数据流:输入是一个刚初始化的 Git 仓库和可写根目录策略 → 它生成 Seatbelt 参数并抽出策略文本 → 检查策略里有针对 repo 根目录下受保护元数据名称的正则拒绝规则。

调用关系:它使用 seatbelt_protected_metadata_name_requirements 生成期望片段进行比较。它补上了“已有敏感目录”和“新建敏感目录”之间的安全空洞。

调用图:调用 1 个内部函数(seatbelt_policy_arg);外部调用 6 个(new, assert!, new, create_dir_all, create_seatbelt_command_args_for_legacy_policy, vec!)。

create_seatbelt_args_with_read_only_git_pointer_file1123–1217 ↗
fn create_seatbelt_args_with_read_only_git_pointer_file()

作用:检查 .git 是指针文件时也受保护,并且它指向的真实 gitdir 配置也不能被改。Git worktree 常用这种结构,不能只保护 .git 目录一种形态。

数据流:输入是一个临时 worktree,其中 .git 文件写着 gitdir: actual-gitdir → 它生成 Seatbelt 命令先试写 .git 文件,再试写 actual-gitdir/config → 两次都应失败,原文件内容保持不变。

调用关系:它用 create_seatbelt_command_args_for_legacy_policy 生成实际运行参数,并用 assert_seatbelt_denied 验证 Seatbelt 拦截。它覆盖 Git 元数据保护的特殊但常见结构。

调用图:调用 1 个内部函数(assert_seatbelt_denied);外部调用 9 个(new, assert!, assert_eq!, new, format!, create_dir_all, write, create_seatbelt_command_args_for_legacy_policy, vec!)。

create_seatbelt_args_for_cwd_as_git_repo1220–1333 ↗
fn create_seatbelt_args_for_cwd_as_git_repo()

作用:检查当前工作目录本身就是 Git 仓库时,默认可写根也会正确保护 .git 和 .codex。很多命令就是在仓库根目录里运行,这个场景很关键。

数据流:输入是一个含 .git 和 .codex 的临时仓库,策略没有显式 writable_roots,而是使用默认的 cwd、TMPDIR 和 /tmp → 它生成 Seatbelt 参数 → 检查 cwd 和临时目录的敏感名称正则存在,且 .git、.codex 的排除参数正确。

调用关系:它调用 populate_tmpdir 准备仓库,再用 seatbelt_policy_arg 读取策略文本。它验证默认根目录逻辑和受保护元数据逻辑能一起工作。

调用图:调用 2 个内部函数(populate_tmpdir, seatbelt_policy_arg);外部调用 8 个(from, new, assert!, assert_eq!, format!, var, create_seatbelt_command_args_for_legacy_policy, vec!)。

populate_tmpdir1355–1395 ↗
fn populate_tmpdir(tmp: &Path) -> PopulatedTmp

作用:为测试搭一个临时工作区:一个带 .git 和 .codex 的“敏感”目录,另一个空目录。这样多个测试不用重复写准备代码。

数据流:输入是临时目录路径 → 它创建 vulnerable_root,运行 git init,写入 .codex/config.toml,再创建 empty_root,并计算这些路径的规范化版本 → 输出 PopulatedTmp,里面装着后续断言要用的各个路径。

调用关系:create_seatbelt_args_with_read_only_git_and_codex_subpaths 和 create_seatbelt_args_for_cwd_as_git_repo 都调用它。它是测试舞台搭建工,负责把真实文件结构准备好。

调用图:被 2 处调用(create_seatbelt_args_for_cwd_as_git_repo, create_seatbelt_args_with_read_only_git_and_codex_subpaths);外部调用 4 个(join, new, create_dir_all, write)。

linux-sandbox/src/linux_run_main_tests.rs源码 ↗
testtest

这个文件不是真正运行沙箱的主代码,而是专门挑容易出事故的地方做测试。它检查 bubblewrap(一个在 Linux 上创建隔离环境的工具,代码里常叫 bwrap)的命令参数有没有拼对,比如是否隔离网络、是否正确设置被运行程序看到的名字。它也测试权限配置能不能被正确解析,哪些复杂文件系统规则必须交给运行时再检查。还有一大块在验证“临时假文件/目录”的清理:沙箱为了挂载可能会临时造一些空文件或目录,测试确保只删自己造的,不误删用户本来就有的东西。最后,它还用 fork(复制出子进程)测试收到终止信号时,只杀掉 bwrap 子进程,不把父进程也带走。整体上,这是沙箱可靠性和安全边界的防回归测试。

函数细节34
read_only_permission_profile18–20 ↗
fn read_only_permission_profile() -> PermissionProfile

作用:准备一个“只读权限配置”,供多个测试重复使用。这样测试不用每次都手写同一套权限。

数据流:进去没有参数 → 它调用 PermissionProfile 的只读构造方法 → 出来一个表示“只能读、不能随便写”的权限配置对象。

调用关系:这是测试里的小帮手。后面的内部命令参数测试、权限解析测试,以及 read_only_file_system_policy 都会先找它要一份标准只读配置。

调用图:调用 1 个内部函数(read_only);被 5 处调用(inner_command_includes_permission_profile_flag, managed_proxy_inner_command_includes_route_spec, non_managed_inner_command_omits_route_spec, read_only_file_system_policy, resolve_permission_profile_derives_runtime_policies)。

read_only_file_system_policy22–24 ↗
fn read_only_file_system_policy() -> FileSystemSandboxPolicy

作用:把“只读权限配置”转换成文件系统沙箱策略。文件系统沙箱策略就是告诉沙箱哪些路径能读、哪些路径能写。

数据流:进去没有参数 → 先拿到 read_only_permission_profile 生成的只读配置 → 再从里面取出对应的文件系统策略 → 出来一份只读文件系统规则。

调用关系:它承接 read_only_permission_profile 的结果,给构造 bwrap 命令的测试使用,例如检查 argv0、网络隔离参数时都需要一份基础文件系统规则。

调用图:调用 1 个内部函数(read_only_permission_profile);被 4 处调用(inserts_bwrap_argv0_before_command_separator, inserts_unshare_net_when_network_isolation_requested, inserts_unshare_net_when_proxy_only_network_mode_requested, rewrites_inner_command_path_when_bwrap_lacks_argv0)。

detects_proc_mount_invalid_argument_failure27–30 ↗
fn detects_proc_mount_invalid_argument_failure()

作用:确认代码能识别一种 /proc 挂载失败:系统返回“参数无效”。/proc 是 Linux 暴露进程信息的特殊目录,沙箱里挂不上它会影响运行。

数据流:进去一段模拟的错误输出文字 → 交给识别函数判断是不是 /proc 挂载失败 → 测试要求结果必须是“是”。

调用关系:这是 /proc 挂载错误识别的一条用例。它和后面几个同类测试一起,保证错误提示分类不会漏掉常见系统报错。

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

detects_proc_mount_operation_not_permitted_failure33–36 ↗
fn detects_proc_mount_operation_not_permitted_failure()

作用:确认代码能识别 /proc 挂载失败里的“操作不被允许”情况。这个错误常见于权限不足或内核限制。

数据流:进去一段 bwrap 的 stderr 错误文字 → 识别函数检查里面是不是 /proc 挂载失败 → 出来应为 true,测试用断言确认。

调用关系:它属于 /proc 挂载失败识别测试组,和 invalid argument、permission denied 两个测试覆盖不同系统给出的错误说法。

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

detects_proc_mount_permission_denied_failure39–42 ↗
fn detects_proc_mount_permission_denied_failure()

作用:确认代码能识别 /proc 挂载失败里的“权限被拒绝”。这能帮助上层给出更准确的错误处理或提示。

数据流:进去一段包含 Permission denied 的 bwrap 错误输出 → 判断函数匹配 /proc 挂载失败格式 → 出来应为 true。

调用关系:它和另外两个 /proc 失败测试一起,守住 is_proc_mount_failure 这类错误识别逻辑。

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

ignores_non_proc_mount_errors45–48 ↗
fn ignores_non_proc_mount_errors()

作用:确认代码不会把别的挂载失败误判成 /proc 挂载失败。误判会让程序走错补救路径。

数据流:进去一段关于 /dev/null 绑定挂载失败的错误文字 → 识别函数检查后应认为这不是 /proc 的问题 → 出来 false,测试断言取反通过。

调用关系:这是前面几个正向识别测试的反面用例,保证错误分类既能认出来,也不会认错。

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

inserts_bwrap_argv0_before_command_separator51–92 ↗
fn inserts_bwrap_argv0_before_command_separator()

作用:检查当 bwrap 支持 --argv0 时,代码会把它插在正确位置。argv0 是程序启动后看到的“自己叫什么名字”。

数据流:进去一个要运行的命令 /bin/true、只读文件系统策略、工作目录和 bwrap 选项 → 先生成 bwrap 参数列表,再应用 argv0 修正 → 出来一串参数,测试确认 --argv0 和名字出现在真正用户命令之前。

调用关系:它使用 read_only_file_system_policy 准备策略,再检查构造 bwrap 启动命令的流程。这个测试保护的是启动器参数顺序,顺序错了 bwrap 可能会把参数理解错。

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

rewrites_inner_command_path_when_bwrap_lacks_argv095–121 ↗
fn rewrites_inner_command_path_when_bwrap_lacks_argv0()

作用:检查老版本 bwrap 不支持 --argv0 时,代码会改用备用办法:直接替换内部命令路径。

数据流:进去普通 bwrap 参数、一个标记说“不支持 argv0”、以及备用 helper 路径 → 函数不再插入 --argv0,而是把 -- 后面的内部命令路径换成备用路径 → 测试确认参数里没有 --argv0,且命令路径已变。

调用关系:它和 inserts_bwrap_argv0_before_command_separator 是一对兼容性测试:新 bwrap 走 --argv0,老 bwrap 走路径替换。

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

rewrites_bwrap_helper_command_not_nested_user_command_when_current_exe_appears_later124–161 ↗
fn rewrites_bwrap_helper_command_not_nested_user_command_when_current_exe_appears_later()

作用:确认备用路径替换只改最外层 bwrap helper,不会误改后面嵌套的用户命令。否则用户真正要运行的程序可能被偷偷换掉。

数据流:进去一串手工构造的参数,里面有两个 -- 分隔段,后面还出现当前可执行文件路径 → 应用 argv0 备用修正 → 出来只替换第一个 -- 后的 helper,后面的嵌套命令保持不变。

调用关系:它专门覆盖复杂参数结构。当前可执行文件路径由 current_exe 取得,然后用 assert_eq 检查最终参数完全符合预期。

调用图:外部调用 3 个(assert_eq!, current_exe, vec!)。

inserts_unshare_net_when_network_isolation_requested164–180 ↗
fn inserts_unshare_net_when_network_isolation_requested()

作用:检查请求网络隔离时,bwrap 参数里会加入 --unshare-net。这个参数的意思是给程序一个单独的网络空间,让它不能随便用宿主机网络。

数据流:进去只读文件系统策略和“网络隔离”选项 → 构造 bwrap 参数 → 出来的参数列表里必须包含 --unshare-net。

调用关系:它使用 read_only_file_system_policy 提供基础文件规则,重点盯住网络模式到 bwrap 参数的转换。

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

inserts_unshare_net_when_proxy_only_network_mode_requested183–199 ↗
fn inserts_unshare_net_when_proxy_only_network_mode_requested()

作用:检查“只能通过代理联网”的模式也会隔离普通网络。这样程序不能绕过代理直接访问外网。

数据流:进去只读文件系统策略和 ProxyOnly 网络模式 → 构造 bwrap 参数 → 出来参数必须包含 --unshare-net。

调用关系:它和网络完全隔离测试类似,但覆盖的是代理专用模式,保证代理控制不会被直接网络访问绕开。

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

proxy_only_mode_takes_precedence_over_full_network_policy202–208 ↗
fn proxy_only_mode_takes_precedence_over_full_network_policy()

作用:确认即使权限策略本身允许联网,只要启用了“代理管理网络”,最终也会选择 ProxyOnly。也就是代理规则优先。

数据流:进去一个“网络启用”的策略,以及 allow_network_for_proxy 为 true → 网络模式选择函数做判断 → 出来必须是 ProxyOnly。

调用关系:这个测试保护 bwrap_network_mode 的优先级规则,避免全网访问策略压过代理控制。

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

split_only_filesystem_policy_requires_direct_runtime_enforcement211–234 ↗
fn split_only_filesystem_policy_requires_direct_runtime_enforcement()

作用:确认某些“分层”的文件系统权限必须在运行时直接检查。比如项目根目录可写,但某个具体目录只读,这种规则单靠外层挂载不够精细。

数据流:进去一个临时目录,里面建 docs 子目录 → 构造一个含特殊项目根路径和具体 docs 路径的受限策略 → 调用判断函数 → 出来应为 true,表示需要运行时再加一道检查。

调用关系:它测试 FileSystemSandboxPolicy 的判断能力。这里用临时目录和绝对路径构造真实场景,防止复杂权限组合被错误地当成简单策略。

调用图:调用 2 个内部函数(restricted, from_absolute_path);外部调用 4 个(assert!, create_dir_all, new, vec!)。

root_write_read_only_carveout_requires_direct_runtime_enforcement237–258 ↗
fn root_write_read_only_carveout_requires_direct_runtime_enforcement()

作用:确认“根目录整体可写,但某个子目录只读”的例外规则,也需要运行时直接执行。否则只靠粗粒度挂载可能保护不住那个只读子目录。

数据流:进去临时目录和 docs 子目录 → 构造根路径可写、docs 只读的策略 → 判断是否需要直接运行时检查 → 出来应为 true。

调用关系:它和 split_only_filesystem_policy_requires_direct_runtime_enforcement 覆盖相近但不同的权限形状,都是为了验证复杂文件规则不会被低估。

调用图:调用 2 个内部函数(restricted, from_absolute_path);外部调用 4 个(assert!, create_dir_all, new, vec!)。

managed_proxy_preflight_argv_is_wrapped_for_full_access_policy261–275 ↗
fn managed_proxy_preflight_argv_is_wrapped_for_full_access_policy()

作用:确认在“托管代理”预检查阶段,即使文件系统策略是完全开放的,也仍然会用 bwrap 包一层。预检查就是正式运行前先探路。

数据流:进去根目录、无限制文件系统策略和由代理规则得出的网络模式 → 构造预检查 bwrap 参数 → 出来的参数里必须有 --,表示后面跟着被包装运行的命令。

调用关系:它先通过网络模式选择得到 ProxyOnly,再检查 build_preflight_bwrap_argv 的输出,保证代理场景不会跳过包装层。

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

cleanup_synthetic_mount_targets_removes_only_empty_mount_targets278–303 ↗
fn cleanup_synthetic_mount_targets_removes_only_empty_mount_targets()

作用:确认清理“合成挂载目标”时,只删除空文件或空目录,不删除有内容的真实文件。合成挂载目标是沙箱为了挂载而临时补出来的位置。

数据流:进去一个临时目录,里面准备空文件、空目录、有内容文件和不存在路径 → 注册这些目标后执行清理 → 出来空的临时目标被删,有内容文件保留,不存在路径也保持不存在。

调用关系:它测试注册和清理合成挂载目标的配合,重点防止清理阶段误伤用户文件。

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

synthetic_mount_registry_root_is_unique_to_effective_user306–314 ↗
fn synthetic_mount_registry_root_is_unique_to_effective_user()

作用:确认合成挂载目标的登记目录按当前有效用户 ID 区分。这样不同用户不会共用同一个临时登记区。

数据流:进去没有业务参数 → 读取当前有效用户 ID → 拼出预期的临时目录路径 → 和实际函数返回值比较。

调用关系:它验证 synthetic_mount_registry_root 的命名规则。这里调用 geteuid 取系统层面的当前有效用户编号。

调用图:外部调用 2 个(assert_eq!, geteuid)。

cleanup_synthetic_mount_targets_waits_for_other_active_registrations317–335 ↗
fn cleanup_synthetic_mount_targets_waits_for_other_active_registrations()

作用:确认如果还有别的登记者正在使用同一个合成目标,清理不会马上删除它。就像有人还在占用会议室,保洁不能立刻撤掉桌椅。

数据流:进去一个临时空文件目标 → 注册后手动放一个“还有人活着”的标记文件 → 第一次清理时文件保留;移除标记后重新注册并清理 → 文件最终被删除。

调用关系:它检查清理逻辑如何处理并发使用。register_synthetic_mount_targets 负责登记,cleanup_synthetic_mount_targets 必须尊重其他活动标记。

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

cleanup_synthetic_mount_targets_removes_transient_file_after_concurrent_owner_exits338–359 ↗
fn cleanup_synthetic_mount_targets_removes_transient_file_after_concurrent_owner_exits()

作用:确认一个临时造出来的空文件,在并发使用者都退出后会被删掉。它区分“本来就存在的用户文件”和“沙箱临时造的文件”。

数据流:进去一个临时路径 → 第一个登记者认为它缺失,随后创建空文件并写入合成标记;第二个登记者看到它是现有空文件 → 第一个清理时因还有活动标记而保留;标记移除后第二个清理把它删掉。

调用关系:它覆盖合成挂载目标最容易出错的并发场景,使用 missing、existing_empty_file、登记和清理函数共同模拟两个拥有者先后退出。

调用图:调用 2 个内部函数(existing_empty_file, missing);外部调用 5 个(assert!, remove_file, symlink_metadata, write, new)。

cleanup_synthetic_mount_targets_preserves_real_pre_existing_empty_file362–379 ↗
fn cleanup_synthetic_mount_targets_preserves_real_pre_existing_empty_file()

作用:确认用户原本就有的空文件不会因为被当作挂载目标而被删掉。空文件也可能是有意义的配置或标记文件。

数据流:进去一个临时目录,先创建一个真实存在的空文件并读取它的元数据 → 两次把它登记为 existing_empty_file → 两次清理后 → 文件仍然存在。

调用关系:它和前一个“临时文件会删除”的测试形成对照,确保清理逻辑看得懂文件来源,不是见到空文件就删。

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

cleanup_protected_create_targets_removes_created_path_and_reports_violation382–393 ↗
fn cleanup_protected_create_targets_removes_created_path_and_reports_violation()

作用:确认受保护的路径如果原本不存在,却在运行中被创建了,清理会删除它并报告违规。比如不允许程序偷偷造 .git 目录。

数据流:进去一个临时路径 .git,登记为受保护且原本缺失 → 测试中手动创建这个目录 → 清理函数发现新建路径,删除它并返回 true → 测试确认报告了违规且路径不存在。

调用关系:它测试 protected create 机制的基本效果:register_protected_create_targets 做登记,cleanup_protected_create_targets 做事后检查和清理。

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

cleanup_protected_create_targets_waits_for_other_active_registrations396–416 ↗
fn cleanup_protected_create_targets_waits_for_other_active_registrations()

作用:确认受保护路径清理也会尊重其他活动登记者:先报告违规,但在别人还在用时不急着删。

数据流:进去一个受保护 .git 路径 → 注册后放入活动标记,并创建该路径 → 第一次清理返回违规但保留路径;移除标记后再次登记清理 → 仍报告违规,并最终删除路径。

调用关系:它测试 protected create 清理里的并发安全,和合成挂载目标的并发清理测试类似,但对象是“不该被创建的路径”。

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

bwrap_signal_forwarder_terminates_child_and_keeps_parent_alive419–430 ↗
fn bwrap_signal_forwarder_terminates_child_and_keeps_parent_alive()

作用:测试信号转发器收到终止信号时,会终止 bwrap 子进程,但不会把负责监督的父进程一起杀掉。信号可以理解成操作系统发给进程的“请停止”等通知。

数据流:进去没有业务参数 → fork 出一个监督进程;子进程里运行专门的测试监督逻辑 → 父进程等待监督进程结束 → 出来要求监督进程正常退出且退出码为 0。

调用关系:它调用 run_bwrap_signal_forwarder_test_supervisor 做真正的子进程场景,再用 wait_for_bwrap_child 等待结果,是信号转发行为的外层验收。

调用图:调用 2 个内部函数(wait_for_bwrap_child, run_bwrap_signal_forwarder_test_supervisor);外部调用 3 个(assert!, assert_eq!, fork)。

run_bwrap_signal_forwarder_test_supervisor433–460 ↗
fn run_bwrap_signal_forwarder_test_supervisor() -> !

作用:在子进程里搭出一个真实的信号转发测试场景:再 fork 一个会一直暂停等待信号的孩子,然后验证 SIGTERM 会被转发给它。

数据流:进去没有参数 → fork 出被监督的孩子;孩子无限 pause 等信号;监督者安装信号转发器后给自己 raise 一个 SIGTERM → 转发器应把信号送给孩子;监督者等待孩子并根据孩子是否被 SIGTERM 杀死决定退出码。

调用关系:它只被 bwrap_signal_forwarder_terminates_child_and_keeps_parent_alive 调用。它把 install_bwrap_signal_forwarders 和 wait_for_bwrap_child 放到真实进程关系里验证。

调用图:调用 2 个内部函数(install_bwrap_signal_forwarders, wait_for_bwrap_child);被 1 处调用(bwrap_signal_forwarder_terminates_child_and_keeps_parent_alive);外部调用 6 个(WIFSIGNALED, WTERMSIG, _exit, fork, pause, raise)。

managed_proxy_inner_command_includes_route_spec463–476 ↗
fn managed_proxy_inner_command_includes_route_spec()

作用:确认使用托管代理时,内部 seccomp 命令会带上代理路由说明。seccomp 是 Linux 的系统调用过滤机制,用来限制程序能向内核请求什么操作。

数据流:进去只读权限配置、工作目录、允许代理联网标记、路由 JSON 字符串和真实命令 → 构造内部命令参数 → 出来参数里必须包含 --proxy-route-spec 和对应 JSON。

调用关系:它使用 read_only_permission_profile 准备权限,再检查 build_inner_seccomp_command 在代理模式下是否把路由信息传给内层。

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

inner_command_includes_permission_profile_flag479–495 ↗
fn inner_command_includes_permission_profile_flag()

作用:确认内部 seccomp 命令会带上权限配置参数,并正确传入命令工作目录。否则内层不知道该按哪套规则限制程序。

数据流:进去只读权限配置、沙箱策略目录、命令工作目录和真实命令 → 构造内部参数 → 出来参数里应包含 --permission-profile,并且 --command-cwd 后面跟着指定目录。

调用关系:它调用 read_only_permission_profile 提供配置,检查 build_inner_seccomp_command 的基础参数组装。

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

non_managed_inner_command_omits_route_spec498–510 ↗
fn non_managed_inner_command_omits_route_spec()

作用:确认没有启用托管代理时,内部命令不会带代理路由参数。没用到的参数不该乱传,避免内层误以为处在代理模式。

数据流:进去只读权限配置、目录、allow_network_for_proxy 为 false、没有路由说明和真实命令 → 构造内部参数 → 出来不应出现 --proxy-route-spec。

调用关系:它和 managed_proxy_inner_command_includes_route_spec 是正反两面,保护 build_inner_seccomp_command 对代理开关的判断。

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

managed_proxy_inner_command_requires_route_spec513–526 ↗
fn managed_proxy_inner_command_requires_route_spec()

作用:确认启用托管代理时必须提供路由说明;如果没提供,代码会直接报错。这样能早点暴露配置不完整,而不是运行到一半才出问题。

数据流:进去一个故意缺少 proxy_route_spec 的代理模式参数 → 用 catch_unwind 捕获 panic(严重错误) → 出来应捕获到错误。

调用关系:它测试 build_inner_seccomp_command 的防呆规则。catch_unwind 用来把预期中的 panic 当成测试结果检查,而不是让整个测试进程崩掉。

调用图:外部调用 2 个(assert!, catch_unwind)。

resolve_permission_profile_derives_runtime_policies529–543 ↗
fn resolve_permission_profile_derives_runtime_policies()

作用:确认给定一个普通权限配置时,解析函数能推导出实际运行要用的文件系统策略和网络策略。

数据流:进去一个只读权限配置 → resolve_permission_profile 解析它 → 出来包含原始权限配置、对应只读文件系统策略,以及受限网络策略。

调用关系:它调用 read_only_permission_profile 准备输入,并和 read_only_file_system_policy 的期望结果对比,验证权限配置到运行策略的转换。

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

resolve_permission_profile_preserves_direct_runtime_profile546–579 ↗
fn resolve_permission_profile_preserves_direct_runtime_profile()

作用:确认如果权限配置本来就是从运行时策略生成的,解析后不会把它简化或改写。复杂策略必须原样保留。

数据流:进去一个临时 docs 路径 → 构造根只读、docs 可写的受限文件系统策略,再生成权限配置 → 解析后 → 出来的权限配置、文件系统策略、网络策略都必须和原始设定一致。

调用关系:它测试 resolve_permission_profile 对“直接运行时权限”的保真能力,使用 from_runtime_permissions 和 restricted 构造复杂输入。

调用图:调用 3 个内部函数(from_runtime_permissions, restricted, from_absolute_path);外部调用 4 个(assert_eq!, create_dir_all, new, vec!)。

resolve_permission_profile_rejects_missing_configuration582–587 ↗
fn resolve_permission_profile_rejects_missing_configuration()

作用:确认没有提供权限配置时,解析会返回明确的“缺少配置”错误。没有规则就启动沙箱是不安全的。

数据流:进去 None,也就是没有权限配置 → 调用解析函数 → 出来应是 MissingConfiguration 错误。

调用关系:它保护 resolve_permission_profile 的输入校验,确保调用方不能在配置缺失时悄悄继续。

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

apply_seccomp_then_exec_with_legacy_landlock_panics590–597 ↗
fn apply_seccomp_then_exec_with_legacy_landlock_panics()

作用:确认两种不兼容的内层模式不能同时开启:先应用 seccomp 再 exec,和旧版 Landlock。Landlock 是 Linux 的文件访问限制机制。

数据流:进去 apply_seccomp_then_exec 为 true、use_legacy_landlock 为 true → 调用模式校验函数 → 预期发生 panic,并被 catch_unwind 捕获。

调用关系:它测试 ensure_inner_stage_mode_is_valid 的防错规则,避免程序进入一个设计上不支持的组合。

调用图:外部调用 2 个(assert!, catch_unwind)。

legacy_landlock_rejects_split_only_filesystem_policies600–628 ↗
fn legacy_landlock_rejects_split_only_filesystem_policies()

作用:确认旧版 Landlock 不接受需要直接运行时执行的复杂文件系统策略。旧机制能力不够时,必须拒绝,而不是假装能保护。

数据流:进去临时 docs 路径 → 构造根只读、docs 可写的分层策略 → 在 use_legacy_landlock 为 true 时检查策略 → 预期 panic,被 catch_unwind 捕获。

调用关系:它测试 ensure_legacy_landlock_mode_supports_policy,和前面的复杂策略测试呼应,保证旧模式不会处理自己处理不了的权限形状。

调用图:调用 2 个内部函数(restricted, from_absolute_path);外部调用 5 个(assert!, create_dir_all, catch_unwind, new, vec!)。

valid_inner_stage_modes_do_not_panic631–641 ↗
fn valid_inner_stage_modes_do_not_panic()

作用:确认合法的内层运行模式组合不会报错。也就是说,防错检查只拦危险组合,不误伤正常配置。

数据流:进去三组布尔开关组合:两者都关、只开旧 Landlock、只开 seccomp-then-exec → 分别调用校验函数 → 都应正常返回,没有 panic,也没有输出数据。

调用关系:它是 apply_seccomp_then_exec_with_legacy_landlock_panics 的补充测试,验证 ensure_inner_stage_mode_is_valid 的允许列表也正确。

linux-sandbox/tests/all.rs源码 ↗
testtest run

这个文件的作用很简单:把 tests/suite/ 里的测试代码挂到同一个集成测试程序里。集成测试可以理解成“从外面检查整个工具是否按预期工作”的测试,不是只测某个小函数。这里的 mod suite; 就是在告诉 Rust:去加载名叫 suite 的测试模块。开头的 #![allow(clippy::expect_used)] 是给代码检查工具 Clippy 的说明:测试里允许使用 expectexpect 通常是在出错时直接中断并显示说明,在普通程序里要慎用,但在测试里很常见,因为测试失败就应该清楚地停下来。没有这个文件,测试模块可能不会被统一编译和运行,测试入口也会变得分散。

linux-sandbox/tests/suite/mod.rs源码 ↗
testtest discovery / test compile

这个文件本身不写具体测试步骤,它的作用更像一本书的目录。Rust 里用 mod 声明模块,也就是告诉编译器:“这里还有两个测试文件要一起编进来。”这里挂入了 landlockmanaged_proxy 两组测试。这样做的好处是,测试不需要全堆在一个大文件里,每组测试可以按主题分开放,但运行测试时又能作为同一个测试套件被统一处理。没有这个文件,这些被拆出去的测试模块可能不会被测试框架看到,也就不会自动运行。文件顶部的注释也说明了它的背景:它是在汇总以前独立存在的集成测试。

linux-sandbox/tests/suite/landlock.rs源码 ↗
testtest run

这个文件像一套“沙箱安检清单”。沙箱就是把命令关在一个受限的小房间里:能读什么、能写什么、能不能联网,都要按规则来。测试先准备统一的运行环境和权限配置,再通过 codex-linux-sandbox 这个辅助程序去执行真实命令,比如 ls、bash、curl、git。它会检查根目录能读但不能随便写,指定的临时目录可以写,/dev/null 和 /dev/shm 这类特殊系统位置是否被正确处理,网络工具是否被挡住,还会测试 .git、.codex、.agents 这类敏感元数据目录不会被偷偷创建或覆盖。文件里还特别照顾了 bubblewrap 不可用的机器:bubblewrap 是 Linux 上常用的隔离工具,如果测试环境缺它,就跳过相关测试,避免把环境问题误判成代码错误。

函数细节35
create_env_from_core_vars45–48 ↗
fn create_env_from_core_vars() -> HashMap<String, String>

作用:生成一份给被测命令使用的环境变量。这样测试里的命令运行时,不是拿到一堆随意的系统环境,而是按 Codex 默认规则整理过的环境。

数据流:它不接收参数;先创建默认的 ShellEnvironmentPolicy,也就是“命令能继承哪些环境变量”的规则;再调用 create_env 生成 HashMap,最后返回这份环境变量表。

调用关系:它是所有执行命令前的准备步骤之一。run_cmd_result_with_permission_profile_for_cwd 和 assert_network_blocked 会用它来填 ExecParams,然后把真正执行交给 process_exec_tool_call。

调用图:调用 2 个内部函数(create_env, default);被 2 处调用(assert_network_blocked, run_cmd_result_with_permission_profile_for_cwd)。

codex_linux_sandbox_exe50–56 ↗
fn codex_linux_sandbox_exe() -> PathBuf

作用:找到测试要调用的 codex-linux-sandbox 可执行文件。没有它,测试就不知道该用哪个沙箱程序去包住命令。

数据流:它从编译时环境变量里取出二进制路径,转成 PathBuf;如果能转成真实绝对路径就返回真实路径,否则返回原路径。

调用关系:它给命令执行器提供沙箱辅助程序的位置。通用执行函数、网络阻断测试,以及手写权限策略的测试都会调用它。

调用图:被 4 处调用(assert_network_blocked, run_cmd_result_with_permission_profile_for_cwd, sandbox_blocks_explicit_split_policy_carveouts_under_bwrap, sandbox_reenables_writable_subpaths_under_unreadable_parents);外部调用 2 个(from, env!)。

run_cmd59–66 ↗
async fn run_cmd(cmd: &[&str], writable_roots: &[PathBuf], timeout_ms: u64)

作用:运行一个沙箱里的命令,并要求它必须成功。适合测试“这件事应该被允许”的场景。

数据流:输入命令、允许写入的目录、超时时间;它调用 run_cmd_output 真正执行;如果退出码不是 0,就打印标准输出和错误输出,然后让测试失败。

调用关系:这是最简单的测试入口。test_root_read、test_root_write、test_writable_root、test_timeout 用它表达“跑这个命令并看结果”。

调用图:调用 1 个内部函数(run_cmd_output);被 4 处调用(test_root_read, test_root_write, test_timeout, test_writable_root);外部调用 2 个(panic!, println!)。

run_cmd_output68–82 ↗
async fn run_cmd_output(
    cmd: &[&str],
    writable_roots: &[PathBuf],
    timeout_ms: u64,
) -> codex_protocol::exec_output::ExecToolCallOutput

作用:运行一个沙箱命令并返回完整输出,但默认要求执行过程本身不能出异常。它比 run_cmd 多给调用者一个机会去自己检查退出码和输出内容。

数据流:输入命令、可写目录和超时;它使用默认的新沙箱路径、禁止网络访问,调用 run_cmd_result_with_writable_roots;成功时返回输出,失败时直接报错。

调用关系:run_cmd 用它做简单成功检查,test_no_new_privs_is_enabled 用它拿到 /proc/self/status 的内容再自己断言。

调用图:调用 1 个内部函数(run_cmd_result_with_writable_roots);被 2 处调用(run_cmd, test_no_new_privs_is_enabled)。

run_cmd_result_with_writable_roots84–111 ↗
async fn run_cmd_result_with_writable_roots(
    cmd: &[&str],
    writable_roots: &[PathBuf],
    timeout_ms: u64,
    use_legacy_landlock: bool,
    network_access: bool,
) -> Result<codex_protocol:

作用:按“哪些目录可写、网络是否可用”来运行命令。它是很多测试共用的沙箱启动包装器。

数据流:输入命令、可写目录列表、超时、是否使用旧版 Landlock、是否允许网络;它把普通路径转成绝对路径,构造 PermissionProfile,也就是沙箱权限清单;最后把执行交给 run_cmd_result_with_permission_profile。

调用关系:大部分文件读写和 bubblewrap 测试都通过它进入统一执行流程。它把简单参数变成正式权限配置,再交给下一层函数。

调用图:调用 2 个内部函数(run_cmd_result_with_permission_profile, workspace_write_with);被 9 处调用(bwrap_populates_minimal_dev_nodes, bwrap_preserves_writable_dev_shm_bind_mount, run_cmd_output, sandbox_blocks_codex_symlink_replacement_attack, sandbox_blocks_git_and_codex_writes_inside_writable_root, sandbox_ignores_missing_writable_roots_under_bwrap, sandbox_reports_codex_symlink_build_failure_without_panicking, should_skip_bwrap_tests, test_dev_null_write);外部调用 1 个(iter)。

run_cmd_result_with_permission_profile113–128 ↗
async fn run_cmd_result_with_permission_profile(
    cmd: &[&str],
    permission_profile: PermissionProfile,
    timeout_ms: u64,
    use_legacy_landlock: bool,
) -> Result<codex_protocol::exec_outpu

作用:用一份已经建好的权限配置运行命令,并默认把当前目录当作命令工作目录。适合测试自定义沙箱规则。

数据流:输入命令、PermissionProfile、超时和旧版 Landlock 开关;它读取当前工作目录,转成绝对路径;然后调用 run_cmd_result_with_permission_profile_for_cwd。

调用关系:它连接“已有权限规则”和“真正执行命令”。普通可写目录包装器会调用它,一些复杂权限 carveout 测试也直接调用它。

调用图:调用 2 个内部函数(run_cmd_result_with_permission_profile_for_cwd, current_dir);被 4 处调用(run_cmd_result_with_writable_roots, sandbox_blocks_explicit_split_policy_carveouts_under_bwrap, sandbox_blocks_root_read_carveouts_under_bwrap, sandbox_reenables_writable_subpaths_under_unreadable_parents)。

run_cmd_result_with_cwd_and_writable_roots130–161 ↗
async fn run_cmd_result_with_cwd_and_writable_roots(
    cmd: &[&str],
    cwd: &std::path::Path,
    writable_roots: &[PathBuf],
    timeout_ms: u64,
    use_legacy_landlock: bool,
    network_access

作用:在指定工作目录里运行命令,同时指定哪些目录可写。它用于测试“当前目录不一定是测试进程目录”的情况。

数据流:输入命令、工作目录、可写目录、超时、沙箱模式和网络开关;它把可写目录和工作目录转成绝对路径,生成权限配置;最后交给 run_cmd_result_with_permission_profile_for_cwd 执行。

调用关系:sandbox_keeps_parent_repo_discovery_while_blocking_child_metadata 用它模拟子目录里的真实项目操作,比如在仓库子目录里跑 git。

调用图:调用 3 个内部函数(run_cmd_result_with_permission_profile_for_cwd, workspace_write_with, try_from);被 1 处调用(sandbox_keeps_parent_repo_discovery_while_blocking_child_metadata);外部调用 1 个(iter)。

run_cmd_result_with_permission_profile_for_cwd163–196 ↗
async fn run_cmd_result_with_permission_profile_for_cwd(
    cmd: &[&str],
    cwd: AbsolutePathBuf,
    permission_profile: PermissionProfile,
    timeout_ms: u64,
    use_legacy_landlock: bool,
) ->

作用:这是本测试文件里真正把命令送进 Codex 执行系统的核心辅助函数。它把命令、目录、环境、权限和沙箱程序路径全部装进一次执行请求。

数据流:输入命令、工作目录、权限配置、超时和旧版 Landlock 开关;它创建 ExecParams,填入命令、cwd、超时、输出捕获方式、环境变量等;再找到 codex-linux-sandbox;最后调用 process_exec_tool_call,返回执行结果或沙箱错误。

调用关系:上层所有 run_cmd_result 系列函数最终都会走到这里。它不自己实现沙箱,而是把准备好的材料交给 codex_core 的 process_exec_tool_call。

调用图:调用 3 个内部函数(process_exec_tool_call, codex_linux_sandbox_exe, create_env_from_core_vars);被 2 处调用(run_cmd_result_with_cwd_and_writable_roots, run_cmd_result_with_permission_profile);外部调用 2 个(from_ref, clone)。

is_bwrap_unavailable_output198–207 ↗
fn is_bwrap_unavailable_output(output: &codex_protocol::exec_output::ExecToolCallOutput) -> bool

作用:判断一次命令输出是不是在说明 bubblewrap 不可用。这样测试能区分“沙箱功能坏了”和“当前机器缺少必要能力”。

数据流:输入一次命令输出;它检查 stderr 里是否包含找不到 bwrap,或挂载 /proc 失败并带有权限错误;最后返回 true 或 false。

调用关系:should_skip_bwrap_tests 用它判断要不要跳过 bubblewrap 专属测试。它只做文本判断,不负责执行命令。

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

should_skip_bwrap_tests209–228 ↗
async fn should_skip_bwrap_tests() -> bool

作用:检查当前测试机器是否适合跑 bubblewrap 相关测试。如果环境不支持,就返回需要跳过,避免无意义失败。

数据流:它尝试在允许网络的沙箱下运行 true;如果输出显示 bwrap 不可用,或探测超时,就返回 true;如果遇到预料外错误,就让测试失败。

调用关系:许多 bubblewrap 测试开头都会先调用它。它内部通过 run_cmd_result_with_writable_roots 发起探测,再用 is_bwrap_unavailable_output 分析结果。

调用图:调用 2 个内部函数(is_bwrap_unavailable_output, run_cmd_result_with_writable_roots);被 11 处调用(bwrap_populates_minimal_dev_nodes, bwrap_preserves_writable_dev_shm_bind_mount, sandbox_blocks_codex_symlink_replacement_attack, sandbox_blocks_explicit_split_policy_carveouts_under_bwrap, sandbox_blocks_git_and_codex_writes_inside_writable_root, sandbox_blocks_root_read_carveouts_under_bwrap, sandbox_ignores_missing_writable_roots_under_bwrap, sandbox_keeps_parent_repo_discovery_while_blocking_child_metadata, sandbox_reenables_writable_subpaths_under_unreadable_parents, sandbox_reports_codex_symlink_build_failure_without_panicking (+1 more));外部调用 1 个(panic!)。

expect_denied230–242 ↗
fn expect_denied(
    result: Result<codex_protocol::exec_output::ExecToolCallOutput>,
    context: &str,
) -> codex_protocol::exec_output::ExecToolCallOutput

作用:把“这个操作应该被沙箱拒绝”写成统一检查。它接受命令结果,并确认结果确实是不成功的。

数据流:输入一个 Result 和说明文字;如果命令正常返回但退出码非零,也接受并返回输出;如果收到 Sandbox Denied 错误,也取出输出返回;如果命令成功退出或出现其他错误,就让测试失败。

调用关系:用于多个安全测试,比如禁止写 .git、禁止读被 carveout 拦住的目录。它让这些测试不用重复写相同的错误匹配代码。

调用图:被 5 处调用(sandbox_blocks_codex_symlink_replacement_attack, sandbox_blocks_explicit_split_policy_carveouts_under_bwrap, sandbox_blocks_git_and_codex_writes_inside_writable_root, sandbox_blocks_root_read_carveouts_under_bwrap, sandbox_keeps_parent_repo_discovery_while_blocking_child_metadata);外部调用 2 个(assert_ne!, panic!)。

test_root_read245–247 ↗
async fn test_root_read()

作用:确认沙箱里的命令仍然可以读取系统根目录下的普通内容。沙箱不能严到连 /bin 都看不到,否则很多命令无法运行。

数据流:它发出 ls -l /bin;run_cmd 负责放进沙箱执行;如果命令成功,测试通过,否则失败。

调用关系:这是最基础的允许读取测试。它通过 run_cmd 进入通用执行流程。

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

test_root_write251–260 ↗
async fn test_root_write()

作用:确认默认情况下沙箱不能随便写系统或临时文件。这个测试预期会 panic,因为写入不该成功。

数据流:它创建一个临时文件路径,然后尝试在沙箱里用 echo 覆盖它;run_cmd 发现命令失败后会 panic;由于测试标了 should_panic,所以 panic 才是正确结果。

调用关系:它复用 run_cmd 的“必须成功”语义,反过来证明写入会被拦住。

调用图:调用 1 个内部函数(run_cmd);外部调用 2 个(new, format!)。

test_dev_null_write263–282 ↗
async fn test_dev_null_write()

作用:确认沙箱里仍然可以写 /dev/null。/dev/null 像系统里的“垃圾桶”,很多程序会把不需要的输出丢进去,所以不能被误封。

数据流:它先检查是否该跳过 bwrap 测试;不跳过时,在沙箱里运行 echo blah > /dev/null;最后断言退出码是 0。

调用关系:它依赖 should_skip_bwrap_tests 做环境保护,再用 run_cmd_result_with_writable_roots 执行具体命令。

调用图:调用 2 个内部函数(run_cmd_result_with_writable_roots, should_skip_bwrap_tests);外部调用 2 个(assert_eq!, eprintln!)。

bwrap_populates_minimal_dev_nodes285–306 ↗
async fn bwrap_populates_minimal_dev_nodes()

作用:确认 bubblewrap 沙箱里会准备最基本的 /dev 设备节点,比如 null、zero、random。缺这些节点,很多 Linux 程序会莫名其妙跑不起来。

数据流:它先判断是否跳过;然后在沙箱里循环检查 /dev/null、/dev/zero、/dev/full、/dev/random、/dev/urandom、/dev/tty 是否是字符设备;最后要求退出码为 0。

调用关系:这是 bubblewrap 构造运行环境的测试。它通过 run_cmd_result_with_writable_roots 调用统一沙箱执行器。

调用图:调用 2 个内部函数(run_cmd_result_with_writable_roots, should_skip_bwrap_tests);外部调用 2 个(assert_eq!, eprintln!)。

bwrap_preserves_writable_dev_shm_bind_mount309–348 ↗
async fn bwrap_preserves_writable_dev_shm_bind_mount()

作用:确认 /dev/shm 这种共享内存目录如果被设为可写,沙箱里的写入能真实落到宿主机对应文件上。它防止可写挂载被沙箱错误替换或丢失。

数据流:它先检查 bwrap 和 /dev/shm 是否可用;在 /dev/shm 建临时文件并写入初始内容;再让沙箱命令覆盖这个文件;最后检查宿主机读到的新内容是 sandbox-after。

调用关系:它结合临时文件准备和 run_cmd_result_with_writable_roots 执行,专门验证 bubblewrap 的 bind mount,也就是“把宿主目录接到沙箱里”的行为。

调用图:调用 2 个内部函数(run_cmd_result_with_writable_roots, should_skip_bwrap_tests);外部调用 7 个(new_in, from, assert_eq!, eprintln!, format!, write, new)。

test_writable_root351–366 ↗
async fn test_writable_root()

作用:确认被明确列为可写的目录确实能写。否则沙箱会太严格,正常工作区文件也没法改。

数据流:它创建临时目录和目标文件路径;把临时目录作为 writable root 传入;在沙箱里 echo 写文件;run_cmd 要求命令成功。

调用关系:这是可写目录的正向测试。它通过 run_cmd 使用默认权限构造流程。

调用图:调用 1 个内部函数(run_cmd);外部调用 2 个(format!, tempdir)。

sandbox_ignores_missing_writable_roots_under_bwrap369–392 ↗
async fn sandbox_ignores_missing_writable_roots_under_bwrap()

作用:确认可写目录列表里有不存在的路径时,bubblewrap 不会因此让整个命令失败。现实配置里偶尔会出现这种路径,沙箱应该宽容处理。

数据流:它创建一个存在的目录,同时准备一个不存在的目录;两个都传给沙箱;命令只打印 sandbox-ok;最后检查退出码为 0 且 stdout 正确。

调用关系:它先用 should_skip_bwrap_tests 保护环境,再通过 run_cmd_result_with_writable_roots 验证路径处理逻辑。

调用图:调用 2 个内部函数(run_cmd_result_with_writable_roots, should_skip_bwrap_tests);外部调用 4 个(assert_eq!, eprintln!, create_dir, tempdir)。

test_no_new_privs_is_enabled395–411 ↗
async fn test_no_new_privs_is_enabled()

作用:确认沙箱内启用了 NoNewPrivs。NoNewPrivs 是 Linux 的安全开关,意思是进程不能靠执行新程序来获得更多权限。

数据流:它在沙箱里读取 /proc/self/status 中的 NoNewPrivs 行;从输出里找到对应行;最后断言值是 1,表示已开启。

调用关系:它通过 run_cmd_output 获取命令输出,再自己分析文本内容。

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

test_timeout415–417 ↗
async fn test_timeout()

作用:确认沙箱执行会遵守超时时间。命令卡住时必须能被及时停掉,否则测试和真实用户请求都会挂住。

数据流:它让沙箱运行 sleep 2,但只给 50 毫秒;run_cmd 最终收到超时错误并 panic;测试标明期望这个 panic。

调用关系:它通过 run_cmd 进入标准执行路径,专门验证 process_exec_tool_call 的超时效果。

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

assert_network_blocked423–477 ↗
async fn assert_network_blocked(cmd: &[&str])

作用:运行一个可能联网的命令,并确认它不能成功联网。它把 curl、wget、ssh 等测试里的重复逻辑集中起来。

数据流:输入一条命令;它构造只读权限、当前目录、环境变量和沙箱程序路径;调用 process_exec_tool_call;如果结果是 Denied 或非零退出就接受,如果退出码是 0,就判定网络沙箱被突破并失败。

调用关系:所有 sandbox_blocks_* 网络测试都调用它。它直接调用 process_exec_tool_call,而不是走 writable roots 包装器,因为网络测试只需要只读权限。

调用图:调用 5 个内部函数(process_exec_tool_call, codex_linux_sandbox_exe, create_env_from_core_vars, read_only, current_dir);被 7 处调用(sandbox_blocks_curl, sandbox_blocks_dev_tcp_redirection, sandbox_blocks_getent, sandbox_blocks_nc, sandbox_blocks_ping, sandbox_blocks_ssh, sandbox_blocks_wget);外部调用 3 个(dbg!, panic!, from_ref)。

sandbox_blocks_curl480–482 ↗
async fn sandbox_blocks_curl()

作用:确认 curl 不能在沙箱里成功访问外部网站。curl 是常见 HTTP 工具,能挡住它说明网络限制覆盖了常见路径。

数据流:它把 curl -I http://openai.com 传给 assert_network_blocked;该辅助函数执行并检查退出码不能为 0。

调用关系:这是网络阻断测试之一,具体判断全部交给 assert_network_blocked。

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

sandbox_blocks_wget485–487 ↗
async fn sandbox_blocks_wget()

作用:确认 wget 不能在沙箱里成功下载网页。wget 是另一种常见联网工具。

数据流:它把 wget -qO- http://openai.com 传给 assert_network_blocked;如果命令成功退出,测试失败。

调用关系:它复用 assert_network_blocked,补充覆盖不同联网程序。

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

sandbox_blocks_ping490–493 ↗
async fn sandbox_blocks_ping()

作用:确认 ping 这类使用原始网络能力的命令会被挡住。ping 常用来测试网络连通性,也不该绕过沙箱。

数据流:它运行 ping -c 1 8.8.8.8;assert_network_blocked 负责在沙箱中执行并要求非零退出。

调用关系:它是网络阻断测试的一部分,覆盖 ICMP/raw socket 这种不同于 HTTP 的网络方式。

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

sandbox_blocks_nc496–499 ↗
async fn sandbox_blocks_nc()

作用:确认 nc 不能在沙箱里建立 TCP 连接。nc 是很通用的网络探测工具。

数据流:它尝试 nc -z 127.0.0.1 80;assert_network_blocked 运行后确认不能成功退出。

调用关系:它借助 assert_network_blocked 覆盖本机端口连接这种网络行为。

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

sandbox_blocks_git_and_codex_writes_inside_writable_root502–550 ↗
async fn sandbox_blocks_git_and_codex_writes_inside_writable_root()

作用:确认即使整个工作目录是可写的,里面的 .git 和 .codex 仍然不能被写。它保护版本库和 Codex 配置不被沙箱内命令篡改。

数据流:它创建临时目录以及 .git、.codex 子目录;尝试分别写入 config 文件;expect_denied 检查两次写入都被拒绝或非零退出;最后确认退出码不是 0。

调用关系:它先用 should_skip_bwrap_tests 判断环境,再通过 run_cmd_result_with_writable_roots 发起写入尝试,并用 expect_denied 统一判断拒绝结果。

调用图:调用 3 个内部函数(expect_denied, run_cmd_result_with_writable_roots, should_skip_bwrap_tests);外部调用 5 个(assert_ne!, eprintln!, format!, create_dir_all, tempdir)。

sandbox_keeps_parent_repo_discovery_while_blocking_child_metadata642–762 ↗
async fn sandbox_keeps_parent_repo_discovery_while_blocking_child_metadata()

作用:确认在仓库子目录工作时,git 仍能发现父目录仓库,但沙箱不允许在子目录里新建 .git、.codex、.agents 等元数据目录。它兼顾可用性和安全性。

数据流:它先检查 git 和 python3 是否存在;创建一个父级 Git 仓库和子目录;在子目录运行脚本确认 git rev-parse 能找到父仓库,且 git status 不暴露敏感元数据;随后尝试 git init 和 mkdir .codex,要求都被拒绝;最后确认普通脚本文件仍可创建。

调用关系:这是一个综合场景测试。它多次调用 run_cmd_result_with_cwd_and_writable_roots 来模拟在子目录中工作,并用 expect_denied 检查应被阻止的操作。

调用图:调用 3 个内部函数(expect_denied, run_cmd_result_with_cwd_and_writable_roots, should_skip_bwrap_tests);外部调用 9 个(assert!, assert_eq!, assert_ne!, new, eprintln!, format!, create_dir_all, from_ref, tempdir)。

sandbox_blocks_explicit_split_policy_carveouts_under_bwrap765–829 ↗
async fn sandbox_blocks_explicit_split_policy_carveouts_under_bwrap()

作用:确认手写权限策略里的“拒绝区域”优先级有效。即使父目录可写,只要某个子目录被明确标为 Deny,就不能写。

数据流:它创建临时目录和 blocked 子目录;构造文件系统沙箱策略:最小系统路径可读、沙箱 helper 可读、临时目录可写、blocked 明确拒绝;把策略转成 PermissionProfile;尝试写 blocked/secret.txt,并要求被拒绝。

调用关系:它绕过常规 workspace_write 包装,直接创建 FileSystemSandboxPolicy,再通过 run_cmd_result_with_permission_profile 执行。codex_linux_sandbox_exe 用来保证沙箱 helper 所在目录可读。

调用图:调用 6 个内部函数(codex_linux_sandbox_exe, expect_denied, run_cmd_result_with_permission_profile, should_skip_bwrap_tests, from_runtime_permissions, restricted);外部调用 6 个(assert_ne!, eprintln!, format!, create_dir_all, tempdir, vec!)。

sandbox_reenables_writable_subpaths_under_unreadable_parents832–906 ↗
async fn sandbox_reenables_writable_subpaths_under_unreadable_parents()

作用:确认权限策略可以在一个被拒绝的父目录下面重新开放某个子目录写入。就像整栋楼封闭,但其中一个房间被单独授权进入。

数据流:它创建 blocked/allowed;策略先允许临时目录写入,再拒绝 blocked,最后重新允许 allowed 写入;沙箱中写 allowed/note.txt 并读取;最后检查退出码为 0 且输出是 allowed。

调用关系:它直接构造 FileSystemSandboxPolicy,并通过 run_cmd_result_with_permission_profile 验证复杂权限叠加规则。codex_linux_sandbox_exe 用来把 helper 目录加入可读范围。

调用图:调用 5 个内部函数(codex_linux_sandbox_exe, run_cmd_result_with_permission_profile, should_skip_bwrap_tests, from_runtime_permissions, restricted);外部调用 6 个(assert_eq!, eprintln!, format!, create_dir_all, tempdir, vec!)。

sandbox_blocks_root_read_carveouts_under_bwrap909–955 ↗
async fn sandbox_blocks_root_read_carveouts_under_bwrap()

作用:确认即使策略允许读取整个根目录,也可以对其中某个目录单独禁止读取。它测试“总体允许,局部拉黑”是否生效。

数据流:它创建 blocked/secret.txt 并写入 secret;策略允许 Root 读取,但对 blocked 设置 Deny;沙箱里尝试 cat 这个秘密文件;expect_denied 要求读取失败。

调用关系:它通过 from_runtime_permissions 建立自定义权限配置,再交给 run_cmd_result_with_permission_profile 执行,并用 expect_denied 检查拒绝结果。

调用图:调用 5 个内部函数(expect_denied, run_cmd_result_with_permission_profile, should_skip_bwrap_tests, from_runtime_permissions, restricted);外部调用 7 个(assert_ne!, eprintln!, format!, create_dir_all, write, tempdir, vec!)。

sandbox_blocks_ssh958–970 ↗
async fn sandbox_blocks_ssh()

作用:确认 ssh 不能在沙箱中建立外部连接。ssh 是常见远程登录工具,如果能连出去就说明网络沙箱有漏洞。

数据流:它传入 ssh github.com,并设置 BatchMode 和短连接超时,避免等待密码或长时间卡住;assert_network_blocked 负责确认命令不能成功。

调用关系:它是网络阻断测试之一,具体执行和判断交给 assert_network_blocked。

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

sandbox_blocks_getent973–975 ↗
async fn sandbox_blocks_getent()

作用:确认 getent 不能通过 DNS 查询等方式访问网络。DNS 查询也是联网行为,不能只挡 HTTP。

数据流:它运行 getent ahosts openai.com;assert_network_blocked 在沙箱中执行,并要求不能成功退出。

调用关系:它用 assert_network_blocked 覆盖系统名称解析这类网络路径。

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

sandbox_blocks_dev_tcp_redirection978–983 ↗
async fn sandbox_blocks_dev_tcp_redirection()

作用:确认 shell 的 /dev/tcp 语法不能绕过网络限制。这个语法看起来像写文件,实际会发起 TCP 连接。

数据流:它用 bash 执行 echo hi > /dev/tcp/127.0.0.1/80;assert_network_blocked 要求这次连接尝试不能成功。

调用关系:它补充测试非传统网络工具的绕过方式,仍然复用 assert_network_blocked 的统一判断。

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

linux-sandbox/tests/suite/managed_proxy.rs源码 ↗
testtest run

这个文件测试的是 codex-linux-sandbox 的网络代理保护。简单说,沙箱像一个临时隔离房间,里面运行的命令不该随便连外网;如果允许网络,也应该只通过指定的代理出口走。文件先准备一份干净环境,把用户机器上已有的代理变量清掉,避免测试被外部设置影响。然后它会实际启动沙箱二进制,传入权限配置和命令,看命令成功还是失败、标准输出和错误信息是什么。它还会判断当前机器是否缺少 bubblewrap(Linux 上常用的隔离工具)或缺少创建网络命名空间的权限;这种情况下测试不是失败,而是说明环境不具备条件并跳过。核心测试覆盖三件事:没有代理变量时必须拒绝启动;有代理时请求能通过代理桥接出去但不能直连;Unix 域套接字这种本机通信能力要被细分限制,普通 AF_UNIX socket 被拒绝,但 socketpair 这种进程内配对通信仍可用。

函数细节10
create_env_from_core_vars45–48 ↗
fn create_env_from_core_vars() -> HashMap<String, String>

作用:准备一份和主项目一致的命令运行环境。测试用它来模拟真实程序启动沙箱时会带进去的环境变量。

数据流:进去没有显式参数 → 它先拿默认的 ShellEnvironmentPolicy,也就是“怎么挑选 shell 环境变量”的默认规则,再调用核心库的 create_env → 出来一张环境变量表,后面的测试会继续删掉或加入代理变量。

调用关系:它是多个测试和跳过检查的起点。should_skip_bwrap_tests、managed_proxy_skip_reason 以及三个正式测试都会先找它要一份基础环境,然后再交给 strip_proxy_env 清理,最后传给 run_linux_sandbox_direct 去真正启动沙箱。

调用图:调用 2 个内部函数(create_env, default);被 5 处调用(managed_proxy_mode_denies_af_unix_socket_but_allows_socketpair, managed_proxy_mode_fails_closed_without_proxy_env, managed_proxy_mode_routes_through_bridge_and_blocks_direct_egress, managed_proxy_skip_reason, should_skip_bwrap_tests)。

strip_proxy_env50–56 ↗
fn strip_proxy_env(env: &mut HashMap<String, String>)

作用:把环境变量里的代理设置全部删掉。这样测试不会被开发者电脑或 CI 机器上已有的 HTTP_PROXY、HTTPS_PROXY 等设置偷偷影响。

数据流:进去一张可修改的环境变量表 → 它逐个删除常见代理变量名,同时也删除小写形式 → 出来的还是同一张表,但里面不再带这些代理地址。

调用关系:它通常接在 create_env_from_core_vars 后面使用。跳过检查和三个测试都会先清空代理,再按测试需要选择是否手动塞入 HTTP_PROXY,确保每个场景都由测试自己控制。

调用图:被 5 处调用(managed_proxy_mode_denies_af_unix_socket_but_allows_socketpair, managed_proxy_mode_fails_closed_without_proxy_env, managed_proxy_mode_routes_through_bridge_and_blocks_direct_egress, managed_proxy_skip_reason, should_skip_bwrap_tests)。

is_bwrap_unavailable_output58–60 ↗
fn is_bwrap_unavailable_output(output: &Output) -> bool

作用:判断一次沙箱运行失败是不是因为系统没有 bubblewrap。bubblewrap 可以理解成 Linux 上帮忙搭隔离房间的工具。

数据流:进去一个进程运行结果 Output → 它把错误输出 stderr 按 UTF-8 文本尽量转成字符串 → 如果里面包含“找不到系统 bwrap”的固定提示,就返回 true,否则返回 false。

调用关系:它只被 should_skip_bwrap_tests 使用。should_skip_bwrap_tests 先试跑一个最简单的沙箱命令,然后用它判断是否应该跳过依赖 bubblewrap 的测试。

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

should_skip_bwrap_tests62–75 ↗
async fn should_skip_bwrap_tests() -> bool

作用:检查当前测试环境是否根本跑不了 bubblewrap 沙箱。如果连最简单的沙箱命令都因为缺 bubblewrap 失败,后面的代理测试就不该硬跑。

数据流:进去没有参数 → 它创建干净环境,删掉代理变量,用只读权限配置运行 bash -c true 这个空操作命令 → 再查看错误输出是否说明 bubblewrap 不可用 → 出来一个布尔值,true 表示应该跳过相关测试。

调用关系:它是 managed_proxy_skip_reason 的第一道门。managed_proxy_skip_reason 会先问它环境是否缺少 bubblewrap;如果是,就直接给出跳过原因,不再继续做更复杂的代理权限探测。

调用图:调用 5 个内部函数(create_env_from_core_vars, is_bwrap_unavailable_output, run_linux_sandbox_direct, strip_proxy_env, read_only);被 1 处调用(managed_proxy_skip_reason)。

is_managed_proxy_permission_error77–81 ↗
fn is_managed_proxy_permission_error(stderr: &str) -> bool

作用:判断错误信息是不是因为当前 Linux 内核或容器权限不允许建立受管代理需要的隔离网络。它是在区分“功能坏了”和“测试环境没权限”。

数据流:进去一段 stderr 错误文本 → 它拿这段文字和几条已知权限错误片段逐个比对 → 只要命中一条,就返回 true;都没有命中,就返回 false。

调用关系:它被 managed_proxy_skip_reason 调用。managed_proxy_skip_reason 在试跑受管代理失败后,用它判断失败是否属于环境权限不足;如果是,就把测试跳过,而不是报告产品逻辑失败。

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

managed_proxy_skip_reason83–113 ↗
async fn managed_proxy_skip_reason() -> Option<String>

作用:统一判断受管代理测试是否应该跳过,并给出人能看懂的原因。这样每个测试不用重复写一大段环境探测代码。

数据流:进去没有参数 → 它先检查 bubblewrap 是否可用;再创建干净环境并放入一个假的 HTTP_PROXY,试着以受管代理方式运行 bash -c true → 如果成功,说明可以测试,返回 None;如果失败且像权限问题,就返回一段跳过说明;否则也返回 None,让正式测试继续暴露真正失败。

调用关系:三个正式测试开头都会调用它。它内部会用 should_skip_bwrap_tests 做基础探测,用 run_linux_sandbox_direct 实际启动沙箱,并用 is_managed_proxy_permission_error 解释失败原因。

调用图:调用 5 个内部函数(create_env_from_core_vars, is_managed_proxy_permission_error, run_linux_sandbox_direct, should_skip_bwrap_tests, strip_proxy_env);被 3 处调用(managed_proxy_mode_denies_af_unix_socket_but_allows_socketpair, managed_proxy_mode_fails_closed_without_proxy_env, managed_proxy_mode_routes_through_bridge_and_blocks_direct_egress);外部调用 2 个(from_utf8_lossy, format!)。

run_linux_sandbox_direct115–150 ↗
async fn run_linux_sandbox_direct(
    command: &[&str],
    permission_profile: &PermissionProfile,
    allow_network_for_proxy: bool,
    env: HashMap<String, String>,
    timeout_ms: u64,
) -> Outp

作用:直接启动 codex-linux-sandbox 测试二进制,并收集它的退出状态、标准输出和错误输出。它是这些测试真正“按下运行按钮”的地方。

数据流:进去要运行的命令、权限配置、是否允许代理网络、环境变量表和超时时间 → 它把当前目录、权限配置 JSON、代理开关和命令拼成沙箱参数,清空默认环境后换成传入环境,并限制运行时间 → 出来一个 Output,里面有进程是否成功、stdout 和 stderr。

调用关系:它是所有探测和正式测试共用的执行器。should_skip_bwrap_tests、managed_proxy_skip_reason 和三个测试都把准备好的场景交给它;它不判断测试对错,只负责把沙箱真实跑起来并把结果带回来。

调用图:被 5 处调用(managed_proxy_mode_denies_af_unix_socket_but_allows_socketpair, managed_proxy_mode_fails_closed_without_proxy_env, managed_proxy_mode_routes_through_bridge_and_blocks_direct_egress, managed_proxy_skip_reason, should_skip_bwrap_tests);外部调用 9 个(from_millis, null, piped, new, env!, to_string, current_dir, timeout, vec!)。

managed_proxy_mode_fails_closed_without_proxy_env153–177 ↗
async fn managed_proxy_mode_fails_closed_without_proxy_env()

作用:测试“没有代理地址时,受管代理模式必须拒绝运行”。这叫 fail closed,可以理解成门禁系统没拿到通行规则时默认锁门,而不是默认放行。

数据流:进去没有参数,由测试框架自动运行 → 它先询问是否需要跳过;不跳过时创建干净环境并删掉所有代理变量,然后用受管代理开关运行 bash -c true → 最后断言命令必须失败,而且错误信息要明确说需要代理环境变量。

调用关系:这是一个正式异步测试。它依赖 managed_proxy_skip_reason 处理不合格环境,依赖 create_env_from_core_vars 和 strip_proxy_env 做出“完全没有代理”的场景,再通过 run_linux_sandbox_direct 验证沙箱行为。

调用图:调用 4 个内部函数(create_env_from_core_vars, managed_proxy_skip_reason, run_linux_sandbox_direct, strip_proxy_env);外部调用 4 个(from_utf8_lossy, assert!, assert_eq!, eprintln!)。

managed_proxy_mode_routes_through_bridge_and_blocks_direct_egress180–257 ↗
async fn managed_proxy_mode_routes_through_bridge_and_blocks_direct_egress()

作用:测试受管代理模式的核心承诺:允许的流量要能通过代理桥出去,直接连外网的尝试要被挡住。

数据流:进去没有参数,由测试框架自动运行 → 它先可能跳过;然后在本机开一个临时 TCP 监听器假装 HTTP 代理,把 HTTP_PROXY 指向它;接着让沙箱里的 bash 连接这个代理并发送 HTTP 请求 → 它检查沙箱收到了 200 OK,代理监听器确实收到请求;最后再让沙箱尝试直连 192.0.2.1:80,并断言这次必须失败。

调用关系:这是最完整的一条端到端测试。它用 managed_proxy_skip_reason 避免在无权限机器上误报,用 run_linux_sandbox_direct 分别跑“走代理”和“直连外网”两个命令;临时 TCP 监听线程充当外部代理,帮助确认请求真的按指定路线走。

调用图:调用 4 个内部函数(create_env_from_core_vars, managed_proxy_skip_reason, run_linux_sandbox_direct, strip_proxy_env);外部调用 9 个(from_secs, from_utf8_lossy, bind, assert!, assert_eq!, eprintln!, format!, channel, spawn)。

managed_proxy_mode_denies_af_unix_socket_but_allows_socketpair260–303 ↗
async fn managed_proxy_mode_denies_af_unix_socket_but_allows_socketpair()

作用:测试沙箱对 Unix 域套接字的限制是否够精细。Unix 域套接字是同一台机器上进程通信的一种方式;这里要拒绝创建普通 AF_UNIX socket,但允许 socketpair 这种成对的本地通信工具。

数据流:进去没有参数,由测试框架自动运行 → 它先检查是否该跳过,再检查系统有没有 python3;然后准备带 HTTP_PROXY 的环境,在沙箱里运行一段 Python:尝试创建 AF_UNIX socket 时应被 PermissionError 拒绝,随后创建 socketpair 并互发 ok 应成功 → 最后断言 Python 退出码是 0。

调用关系:这是受管代理模式下套接字权限的专项测试。它和其他测试一样先走 managed_proxy_skip_reason,再用 create_env_from_core_vars、strip_proxy_env 准备环境,最后把 Python 小程序交给 run_linux_sandbox_direct 执行。

调用图:调用 4 个内部函数(create_env_from_core_vars, managed_proxy_skip_reason, run_linux_sandbox_direct, strip_proxy_env);外部调用 3 个(assert_eq!, new, eprintln!)。

exec/tests/suite/sandbox.rs源码 ↗
testtest execution

这个文件把沙箱当成一间“只能按规矩活动的房间”来测试。它会启动真实命令,比如 python3、bash、touch,并给它们不同的权限,再检查结果是不是符合预期。Linux 上还会先探测 Landlock(Linux 的文件访问限制机制,像系统级门禁)是否真的生效;如果宿主机不支持,就跳过相关测试,避免误报。测试内容包括:Python 多进程锁能不能用、读取当前用户信息能不能用、命令自己的工作目录和沙箱允许写入的目录是否被正确区分、首次创建 .codex 配置目录是否被拦住,以及 Unix 本地 socket(同一台机器内进程通信的管道)是否还能正常收发数据。文件里的辅助函数负责统一启动“被关进沙箱”的子进程,测试函数则像验收清单一样逐项确认安全边界没有破洞。

函数细节10
spawn_command_under_sandbox84–108 ↗
async fn spawn_command_under_sandbox(
    command: Vec<String>,
    command_cwd: AbsolutePathBuf,
    permission_profile: &PermissionProfile,
    sandbox_cwd: &AbsolutePathBuf,
    stdio_policy: Stdio

作用:这个函数把一条命令放到沙箱里启动。测试用它来模拟真实用户运行命令时,系统应该施加的安全限制。

数据流:进去的是命令参数、命令运行目录、权限配置、沙箱认定的工作目录、标准输入输出策略,以及环境变量。它根据当前操作系统选择合适的沙箱启动方式:Linux 上找到专门的沙箱可执行文件再启动,macOS 上先构造受限执行请求再启动。出来的是一个正在运行的子进程句柄;调用者可以继续等待它结束,并检查成功还是失败。

调用关系:它是本文件大多数测试的共同入口。各个测试先准备目录和权限,再把命令交给它;它把命令真正送进沙箱。Linux 的能力探测函数也会用它先跑一个最小命令,确认沙箱机制可用。

调用图:被 6 处调用(can_apply_linux_sandbox_policy, python_getpwuid_works_under_sandbox, python_multiprocessing_lock_works_under_sandbox, run_code_under_sandbox, sandbox_blocks_first_time_dot_codex_creation, sandbox_distinguishes_command_and_policy_cwds);外部调用 6 个(inherit, null, piped, new, find_codex_linux_sandbox_exe, from_ref)。

linux_sandbox_test_env117–135 ↗
async fn linux_sandbox_test_env() -> Option<HashMap<String, String>>

作用:这个函数判断当前 Linux 机器是否真的能运行这些沙箱测试。如果系统环境不支持有效沙箱,它会让测试安静跳过,而不是给出误导性的失败。

数据流:进去没有显式参数;它读取当前目录,创建一个只读权限配置,然后调用探测函数试跑一个很小的命令。如果沙箱能生效,就返回一份空环境变量表;如果不能,就打印跳过原因并返回空值,表示后续测试不要继续。

调用关系:Linux 上的多个测试会先问它“这个场地能不能测”。它把具体试跑工作交给 can_apply_linux_sandbox_policy,自己负责把结果转成测试是否继续的决定。

调用图:调用 3 个内部函数(can_apply_linux_sandbox_policy, read_only, current_dir);被 4 处调用(python_getpwuid_works_under_sandbox, python_multiprocessing_lock_works_under_sandbox, sandbox_blocks_first_time_dot_codex_creation, sandbox_distinguishes_command_and_policy_cwds);外部调用 2 个(new, eprintln!)。

can_apply_linux_sandbox_policy143–166 ↗
async fn can_apply_linux_sandbox_policy(
    permission_profile: &PermissionProfile,
    command_cwd: &AbsolutePathBuf,
    sandbox_cwd: &AbsolutePathBuf,
    env: HashMap<String, String>,
) -> bool

作用:这个函数用一个最简单的命令检查 Linux 沙箱规则能不能真正套上去。它像开考前先试一下门禁卡,确认门禁不是摆设。

数据流:进去的是权限配置、命令运行目录、沙箱目录和环境变量。它在沙箱里运行 /usr/bin/true,这个命令什么也不做,只要能正常退出就算成功。出来的是一个布尔值:true 表示命令在沙箱下成功跑完,false 表示启动失败、等待失败或退出状态不成功。

调用关系:它只被 linux_sandbox_test_env 调用。linux_sandbox_test_env 用它的结果决定后面的沙箱行为测试是否值得执行。它本身把实际启动工作交给 spawn_command_under_sandbox。

调用图:调用 1 个内部函数(spawn_command_under_sandbox);被 1 处调用(linux_sandbox_test_env);外部调用 2 个(clone, vec!)。

python_multiprocessing_lock_works_under_sandbox169–229 ↗
async fn python_multiprocessing_lock_works_under_sandbox()

作用:这个测试确认 Python 的多进程锁在沙箱里还能正常工作。这样可以避免沙箱过严,导致常见 Python 程序一开子进程就坏掉。

数据流:进去的是测试运行环境本身;它先按平台准备沙箱环境,Linux 上还允许写 /dev/shm,因为 Python 多进程锁可能需要那里。然后它在沙箱里运行一段 Python 代码:创建锁、启动子进程、子进程拿锁并打印。最后等待 Python 退出,并断言退出必须成功。

调用关系:它先通过 linux_sandbox_test_env 确认 Linux 沙箱可测,再用 PermissionProfile 组装权限,最后把 Python 命令交给 spawn_command_under_sandbox。这个测试关注的是“安全限制不能误伤正常多进程功能”。

调用图:调用 4 个内部函数(linux_sandbox_test_env, spawn_command_under_sandbox, workspace_write_with, current_dir);外部调用 5 个(new, new, assert!, skip_if_sandbox!, vec!)。

python_getpwuid_works_under_sandbox232–275 ↗
async fn python_getpwuid_works_under_sandbox()

作用:这个测试确认 Python 在只读沙箱里仍能查询当前用户信息。很多程序会读取系统用户资料,如果这里被挡住,普通脚本也可能失败。

数据流:它先准备沙箱环境,并检查系统里是否能找到 python3;找不到就跳过。然后用只读权限启动 Python,执行读取当前用户 ID 对应用户信息的代码。最后等待进程结束,并要求它成功退出。

调用关系:它和其他 Linux 测试一样先经过 linux_sandbox_test_env 做能力检查,再把真正的 Python 命令交给 spawn_command_under_sandbox。它验证的是沙箱在限制写入时,仍保留必要的系统读取能力。

调用图:调用 4 个内部函数(linux_sandbox_test_env, spawn_command_under_sandbox, read_only, current_dir);外部调用 6 个(new, assert!, new, skip_if_sandbox!, eprintln!, vec!)。

sandbox_distinguishes_command_and_policy_cwds278–366 ↗
async fn sandbox_distinguishes_command_and_policy_cwds()

作用:这个测试确认“命令实际所在目录”和“沙箱允许写入的目录”不会被混为一谈。否则命令可能因为站在某个目录里,就获得不该有的写权限。

数据流:它创建两个临时目录:一个给命令当当前目录,一个当沙箱允许的根目录。先让命令尝试在自己的当前目录写 forbidden.txt,并要求失败且文件不存在。再让命令写入沙箱允许目录里的 allowed.txt,并要求成功且文件存在。

调用关系:它先准备 Linux 可测环境,再创建临时目录和权限配置,然后两次调用 spawn_command_under_sandbox。第一次验证不该写的地方会被拦住,第二次验证该写的地方不会被误拦。

调用图:调用 3 个内部函数(linux_sandbox_test_env, spawn_command_under_sandbox, workspace_write_with);外部调用 8 个(new, assert!, skip_if_sandbox!, tempdir, canonicalize, create_dir_all, try_exists, vec!)。

sandbox_blocks_first_time_dot_codex_creation369–437 ↗
async fn sandbox_blocks_first_time_dot_codex_creation()

作用:这个测试确认沙箱会阻止命令第一次创建 .codex 配置目录和配置文件。这样可以防止被执行的命令偷偷写入项目配置,改变之后程序的安全设置。

数据流:它创建一个临时仓库目录,并指定 .codex/config.toml 作为攻击目标。然后在沙箱里运行 bash,尝试创建 .codex 目录并写入一个危险配置。命令结束后,它要求进程失败,并检查 .codex 不能作为目录存在,config.toml 也不能被创建。

调用关系:它先通过 linux_sandbox_test_env 做平台检查,再用 workspace_write_with 配出受限权限,最后把可疑写入命令交给 spawn_command_under_sandbox。它验证的是沙箱对敏感配置入口的保护。

调用图:调用 3 个内部函数(linux_sandbox_test_env, spawn_command_under_sandbox, workspace_write_with);外部调用 10 个(new, assert!, assert_eq!, skip_if_sandbox!, panic!, tempdir, create_dir_all, symlink_metadata, try_exists, vec!)。

unix_sock_body439–510 ↗
fn unix_sock_body()

作用:这个函数实际执行 Unix 本地 socket 的收发检查。它确认沙箱没有禁止同一台机器内部进程常用的本地通信方式。

数据流:进去没有参数;它直接调用系统接口创建一对 Unix socket。先用数据报 socket 从一端写入 hello_unix,再从另一端 recvfrom 读出并比较内容。接着又用流式 socket 做一次写入和接收。最后关闭所有文件描述符,清理资源。

调用关系:它不是直接作为普通测试启动,而是被 allow_unix_socketpair_recvfrom 包装后,通过 run_code_under_sandbox 在沙箱内执行。这样测试的是沙箱环境里的真实系统调用结果,而不是普通进程里的结果。

调用图:外部调用 8 个(assert!, assert_eq!, close, recv, recvfrom, socketpair, write, null_mut)。

allow_unix_socketpair_recvfrom513–521 ↗
async fn allow_unix_socketpair_recvfrom()

作用:这个测试确认只读沙箱里允许 Unix socketpair 和 recvfrom 这类本地通信操作。它保护的是“不能联网”和“不能本机通信”这两件事不要被错误地混在一起。

数据流:它没有外部输入;它选择只读权限配置,然后把 unix_sock_body 作为要在沙箱里运行的测试内容交给 run_code_under_sandbox。完成后要求重新执行和沙箱内检查都能正常进行。

调用关系:它是外层测试入口,真正的 socket 检查由 unix_sock_body 做。run_code_under_sandbox 负责把当前测试重新启动到沙箱里,保证检查发生在受限环境中。

调用图:调用 2 个内部函数(run_code_under_sandbox, read_only)。

run_code_under_sandbox525–566 ↗
async fn run_code_under_sandbox(
    test_selector: &str,
    permission_profile: &PermissionProfile,
    child_body: F,
) -> io::Result<Option<ExitStatus>>

作用:这个函数把当前测试程序重新启动一遍,并让指定的小段代码在沙箱里运行。它解决的问题是:有些检查必须发生在已经被沙箱限制的进程内部,不能只从外面启动一个普通命令。

数据流:进去的是测试名称、权限配置,以及一段要在子进程里执行的异步代码。它先看环境变量 IN_SANDBOX:如果没有,说明现在是父进程,就找到当前测试可执行文件,带上测试名重新启动,并加上 IN_SANDBOX=1;如果已经有这个环境变量,说明现在是沙箱里的子进程,就直接运行传入的代码。出来时,父进程返回子进程退出状态,子进程返回空值;过程中会启动并等待子进程。

调用关系:allow_unix_socketpair_recvfrom 用它来实现“同一个测试,外面启动一次,里面真正测一次”的结构。它本身依赖 spawn_command_under_sandbox 来创建受限子进程,并把具体要测的动作交回给调用者传入的 child_body。

调用图:调用 2 个内部函数(spawn_command_under_sandbox, current_dir);被 1 处调用(allow_unix_socketpair_recvfrom);外部调用 5 个(from, args, current_exe, var, vec!)。

cli/tests/sandbox_network_proxy.rs源码 ↗
testtest run

这个测试像是在给沙箱做一次“门禁检查”。它先临时造一个 Codex 配置目录,写入一份配置:允许网络、启用网络代理,并使用沙箱隔离。然后它在本机的 127.0.0.2 上开一个 TCP 监听口,就像在电脑内部开了一个小服务。接着它通过 codex sandbox 启动 curl,让 curl 不走代理,直接访问这个本机地址。正确结果不是访问成功,而是连接失败,退出码应为 7。这样说明沙箱确实拦住了“直接连本机”的路。这里还特别处理了 bubblewrap 不可用的情况;bubblewrap 是 Linux 上常用的沙箱工具,如果机器上没有它,测试就直接跳过,不把环境问题误判成代码问题。

函数细节1
sandbox_with_network_proxy_blocks_direct_loopback_access11–70 ↗
fn sandbox_with_network_proxy_blocks_direct_loopback_access() -> Result<()>

作用:这个测试函数验证:开启网络代理的沙箱不能被命令绕过,不能直接访问本机回环地址。有人改动沙箱、网络代理或权限配置时,它能及时发现安全边界被破坏的问题。

数据流:它先创建一个临时的 Codex 主目录,再在里面写入测试用的 config.toml。随后它在 127.0.0.2 上随机开一个监听端口,把这个地址拼成 URL,然后启动真实的 codex sandbox 命令,在沙箱里运行 curl 去访问这个 URL。命令结束后,它读取标准错误输出:如果发现 bubblewrap 不可用,就打印提示并跳过;否则检查退出码必须是 7,表示连接失败。最后测试成功返回,不保留临时目录。

调用关系:这是测试框架直接调用的测试用例。它自己搭好临时配置和本机监听口,然后把主要验证工作交给外部的 codex 二进制和沙箱机制,再由沙箱里的 curl 尝试联网。测试函数最后根据子进程的退出码和错误输出判断整条链路是否符合预期。

调用图:外部调用 9 个(from_utf8_lossy, bind, new, assert_eq!, new, cargo_bin, eprintln!, format!, write)。

Windows 和跨平台执行桥接

这些文件涵盖 Windows 沙箱和执行适配器,包括 stdio 桥接、包装器协议、统一执行,以及基于 Wine 的远程 exec-server 测试框架。

windows-sandbox-rs/src/stdio_bridge_tests.rs源码 ↗
testtest run

这个文件是测试用的,不是正式运行时直接干活的代码。它检查两个像“传送带”一样的小工具:一个把输入流里的内容切成数据块,送进异步通道;另一个从异步通道拿到数据块,再写到输出目标里。测试里用内存里的假输入、假输出代替真实键盘和屏幕,这样结果稳定、好检查。第一个测试确认输入读完后,数据能完整收到,并且会发出“输入结束”的信号。第二个测试确认输出端连续收到 “alpha” 和 “beta” 两块数据后,会按顺序写成 “alphabeta”。这里还用到互斥锁 Mutex(一把锁,防止两个任务同时改同一份数据)来安全保存写出的内容。

函数细节2
input_forwarder_sends_chunks_and_reports_eof8–23 ↗
async fn input_forwarder_sends_chunks_and_reports_eof() -> anyhow::Result<()>

作用:这个测试确认输入转发器能把一段输入内容完整送到通道里,并且在读到结尾时通知外面“输入已经结束”。有人改动输入桥接代码时,这个测试能防止把数据读丢或忘记发送结束信号。

数据流:进去的是一段内存里的假输入,内容是 “first\nsecond\n”,还有一个用来接收数据块的异步通道,以及一个用来报告结束的一次性通知。测试启动输入转发器后,不断从通道里收数据并拼回一整段。等通道关闭、结束通知到达、转发线程退出后,它把收到的结果和原始内容比较。出来的结果不是业务数据,而是一个测试结论:收到的字节必须和输入完全一样。

调用关系:它在测试运行时由 Tokio 的异步测试框架启动。它会准备 Cursor 这种内存输入源,建立 channel 这种异步队列,然后使用被测的输入转发器把数据送进去。最后它用 assert_eq! 做核对:如果转发器少发、多发,或者没有正确结束,测试就会失败。

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

output_forwarder_writes_all_chunks26–63 ↗
async fn output_forwarder_writes_all_chunks() -> anyhow::Result<()>

作用:这个测试确认输出转发器会把通道里收到的每一块数据都写到目标里,而且顺序不变。它防止输出桥接在多次发送时只写一部分、漏写,或者提前结束。

数据流:进去的是一个异步通道和一个假的写入器。测试先创建当前 Tokio 运行时句柄,再启动输出转发器,然后往通道里发送 “alpha” 和 “beta” 两块字节。发送端被丢弃后,通道关闭,转发器应该自然收尾。假写入器把写入内容存进共享的内存 Vec 里,测试最后加锁取出这份内存,检查它是不是正好等于 “alphabeta”。

调用关系:它同样由 Tokio 的异步测试框架在测试阶段调用。它先用 current 取得当前异步运行环境,用 default 创建共享写入器,用 clone 共享同一份内存缓冲区,然后把真正写数据的工作交给被测的输出转发器。等转发线程和完成通知都结束后,它用 assert_eq! 判断整个输出链路是否可靠。

调用图:外部调用 4 个(assert_eq!, default, clone, current)。

windows-sandbox-rs/src/wrapper_tests.rs源码 ↗
testtest

这个测试文件解决的是一个很实际的问题:Windows 沙箱启动时,需要把工作目录、环境变量、权限规则、可读可写路径、禁止访问路径等信息塞进命令行参数里,再由另一端解析出来。如果这一步出错,沙箱可能在错误目录运行,或者把本该禁止的路径放开,安全边界就会出问题。这里的测试像“打包行李再拆箱检查”:先准备一套比较完整的输入,包括 C 盘工作目录、多个工作区、环境变量、受限网络权限、提升级别沙箱、私有桌面、代理强制、读写路径覆盖和拒绝访问路径。然后调用生成参数的函数,把这些内容变成一串命令行参数。测试先检查关键标记都在,再调用解析函数拆回来,最后逐项比较拆出的内容和原始内容是否完全一致。它的重点不是测试业务功能本身,而是保证参数传递这条管道可靠。

函数细节1
windows_wrapper_args_round_trip29–106 ↗
fn windows_wrapper_args_round_trip()

作用:这个测试函数确认 Windows 沙箱包装器的命令行参数可以“来回一趟”不变样:先生成,再解析,结果必须和原始设置完全一致。有人改动参数格式或新增字段时,它能及时发现有没有漏传、错传。

数据流:进去的是测试里手工构造的一整套沙箱启动设置:命令、当前目录、工作区根目录、环境变量、权限档案、沙箱级别、私有桌面开关、代理开关、读写路径和禁止路径。它先用 from_absolute_path 检查并包装绝对路径,用 HashMap 和 vec! 组装数据,再把这些数据交给 create_windows_sandbox_command_args_for_permission_profile 生成命令行参数列表。随后它用 assert! 检查列表里包含所有重要开关,再把除第一个特殊标记外的参数交给 parse_windows_sandbox_wrapper_args 解析。出来的是一个解析后的结构,测试用 assert_eq! 把其中每个字段和原始输入逐一对比;如果有任何不一致,测试就失败。

调用关系:这个函数只在测试运行时由 Rust 测试框架调用。它站在生成函数 create_windows_sandbox_command_args_for_permission_profile 和解析函数 parse_windows_sandbox_wrapper_args 中间,模拟真实启动流程里的“发送参数”和“接收参数”。它不会继续把沙箱跑起来,而是专门检查这两端是否说同一种参数语言。

调用图:调用 1 个内部函数(from_absolute_path);外部调用 7 个(from, new, assert!, assert_eq!, create_windows_sandbox_command_args_for_permission_profile, parse_windows_sandbox_wrapper_args, vec!)。

windows-sandbox-rs/src/unified_exec/tests.rs源码 ↗
testtest

这个文件不负责实现沙箱本身,而是专门“试车”。它会临时建一个干净的家目录,找出当前项目目录,再启动 cmd 或 PowerShell,让它们在沙箱里跑一段简单命令。测试会检查几件很关键的事:普通命令能不能输出文字;交互式终端能不能先收输出、再接收用户输入;标准输入该开的时候是否真能写,该关的时候是否真的关;窗口大小变化会不会被发成消息;取消运行时是不是被当成“用户取消”,而不是误报成“超时”。文件里还有一把全局锁,像排队叫号一样,让一些旧版进程测试不要同时跑,避免 Windows 桌面和进程资源互相干扰。整体上,它保证 Windows 沙箱这条执行通道不是只看起来能启动,而是真的能可靠地输入、输出、退出和报错。

函数细节21
legacy_process_test_guard38–42 ↗
fn legacy_process_test_guard() -> MutexGuard<'static, ()>

作用:给旧版 Windows 沙箱进程测试上一把全局锁。这样这些容易互相影响的测试一次只跑一个,避免并发时抢同一类 Windows 资源。

数据流:进去没有参数 → 它去锁住一个全局互斥锁(互斥锁就是一把锁,防止多个测试同时进来)→ 出来一个锁的守卫;只要守卫还活着,其他同类测试就得等。

调用关系:多个旧版沙箱测试一开始都会先调用它,例如 cmd、PowerShell、捕获输出和取消测试。它不把活交给别的项目函数,主要是在测试真正启动沙箱前先排好队。

调用图:被 6 处调用(legacy_capture_cancellation_is_not_reported_as_timeout, legacy_capture_powershell_emits_output, legacy_non_tty_cmd_emits_output, legacy_non_tty_cmd_rejects_deny_read_overrides, legacy_non_tty_powershell_emits_output, legacy_tty_powershell_emits_output_and_accepts_input)。

current_thread_runtime44–49 ↗
fn current_thread_runtime() -> tokio::runtime::Runtime

作用:创建一个 Tokio 异步运行时。Tokio 可以理解成 Rust 里跑异步任务的小调度器,这些测试需要它来等待进程输出和退出。

数据流:进去没有参数 → 它创建一个只用当前线程的异步运行时,并打开定时器、IO 等能力 → 出来一个可以执行 async 代码的 Runtime。

调用关系:需要跑异步测试代码的函数都会先用它,然后通过 block_on 把异步流程跑完。它内部调用 Tokio 的运行时构建器,后续测试再去启动沙箱或检查通道消息。

调用图:被 10 处调用(finish_driver_spawn_closes_stdin_when_not_requested, finish_driver_spawn_keeps_stdin_open_when_requested, legacy_non_tty_cmd_emits_output, legacy_non_tty_cmd_rejects_deny_read_overrides, legacy_non_tty_powershell_emits_output, legacy_tty_cmd_default_desktop_emits_output_and_accepts_input, legacy_tty_cmd_emits_output_and_accepts_input, legacy_tty_powershell_emits_output_and_accepts_input, runner_resizer_sends_resize_frame, runner_stdin_writer_sends_close_stdin_after_input_eof);外部调用 1 个(new_current_thread)。

pwsh_path51–55 ↗
fn pwsh_path() -> Option<PathBuf>

作用:寻找 PowerShell 7 的 pwsh.exe 在不在机器上。因为有些测试依赖 PowerShell 7,如果没装就直接跳过。

数据流:进去没有参数 → 它读取 Windows 的 ProgramFiles 环境变量,拼出 PowerShell 7 的常见安装路径,并检查文件是否存在 → 出来 Some(路径) 或 None。

调用关系:所有 PowerShell 相关测试都会先问它要路径。拿不到路径时测试直接返回,避免在没有安装 PowerShell 7 的机器上误报失败。

调用图:被 4 处调用(legacy_capture_cancellation_is_not_reported_as_timeout, legacy_capture_powershell_emits_output, legacy_non_tty_powershell_emits_output, legacy_tty_powershell_emits_output_and_accepts_input);外部调用 2 个(from, var_os)。

sandbox_cwd57–66 ↗
fn sandbox_cwd() -> PathBuf

作用:决定测试里的沙箱命令应该在哪个工作目录启动。它让测试尽量在项目根目录附近运行,路径稳定,方便沙箱权限设置。

数据流:进去没有参数 → 它先看 INSTA_WORKSPACE_ROOT 环境变量有没有指定工作区;没有的话,就从当前 crate 的目录往上找仓库根目录 → 出来一个 PathBuf 路径。

调用关系:大多数真正启动沙箱的测试都会调用它。得到的目录会继续传给 workspace_roots_for 和沙箱启动函数,作为允许访问的工作区和进程当前目录。

调用图:被 8 处调用(legacy_capture_cancellation_is_not_reported_as_timeout, legacy_capture_powershell_emits_output, legacy_non_tty_cmd_emits_output, legacy_non_tty_cmd_rejects_deny_read_overrides, legacy_non_tty_powershell_emits_output, legacy_tty_cmd_default_desktop_emits_output_and_accepts_input, legacy_tty_cmd_emits_output_and_accepts_input, legacy_tty_powershell_emits_output_and_accepts_input);外部调用 3 个(from, env!, var)。

sandbox_home68–74 ↗
fn sandbox_home(name: &str) -> TempDir

作用:为每个测试创建一个临时的 Codex home 目录。这样测试之间不会共用日志、配置或缓存,互不污染。

数据流:进去一个测试名字 → 它用全局计数器拼出独一份临时目录名,先删掉旧残留,再创建目录,并在里面建 TempDir → 出来一个会自动清理的临时目录对象。

调用关系:启动沙箱的测试都会用它准备隔离环境。后续如果测试失败,collect_stdout_and_exit 之类的辅助函数还能根据这个目录去找沙箱日志。

调用图:被 8 处调用(legacy_capture_cancellation_is_not_reported_as_timeout, legacy_capture_powershell_emits_output, legacy_non_tty_cmd_emits_output, legacy_non_tty_cmd_rejects_deny_read_overrides, legacy_non_tty_powershell_emits_output, legacy_tty_cmd_default_desktop_emits_output_and_accepts_input, legacy_tty_cmd_emits_output_and_accepts_input, legacy_tty_powershell_emits_output_and_accepts_input);外部调用 5 个(format!, create_dir_all, remove_dir_all, temp_dir, new_in)。

sandbox_log76–80 ↗
fn sandbox_log(codex_home: &Path) -> String

作用:读取某个测试沙箱的日志文本。它主要用于失败或超时时,把原因打印出来,方便人排查。

数据流:进去一个 codex_home 路径 → 它找到 .sandbox 下面当前日志文件的位置,并尝试读取成字符串 → 出来日志内容;如果读不到,就返回一段说明失败原因的文字。

调用关系:它是调试辅助。等待进程退出或等待输出超时时,会用它把沙箱日志附到 panic 信息里,让测试失败不只是说“超时”,还给线索。

调用图:外部调用 3 个(join, current_log_file_path, read_to_string)。

workspace_roots_for82–84 ↗
fn workspace_roots_for(root: &Path) -> Vec<AbsolutePathBuf>

作用:把普通路径包装成沙箱权限系统认识的“绝对工作区根目录”。这告诉沙箱:哪些目录算用户工作区,可以按权限规则访问。

数据流:进去一个路径 root → 它确认并转换成 AbsolutePathBuf(绝对路径类型,避免相对路径带来歧义),再放进一个列表 → 出来包含一个工作区根目录的 Vec。

调用关系:所有启动沙箱的测试都会把 sandbox_cwd 的结果交给它。生成的列表再传给沙箱启动或捕获函数,用来配置文件访问范围。

调用图:被 8 处调用(legacy_capture_cancellation_is_not_reported_as_timeout, legacy_capture_powershell_emits_output, legacy_non_tty_cmd_emits_output, legacy_non_tty_cmd_rejects_deny_read_overrides, legacy_non_tty_powershell_emits_output, legacy_tty_cmd_default_desktop_emits_output_and_accepts_input, legacy_tty_cmd_emits_output_and_accepts_input, legacy_tty_powershell_emits_output_and_accepts_input);外部调用 1 个(vec!)。

wait_for_frame_count86–116 ↗
fn wait_for_frame_count(frames_path: &Path, expected_frames: usize) -> Vec<Message>

作用:等待某个消息帧文件里至少出现指定数量的消息。它用于测试“输入关闭”“窗口大小变化”等消息有没有真的写出去。

数据流:进去一个帧文件路径和期望数量 → 它在最多 2 秒内反复打开文件、从头读取,用 read_frame 一帧帧解析 → 如果帧数够了就返回消息列表;等不到就让测试失败。

调用关系:runner_stdin_writer_sends_close_stdin_after_input_eof 和 runner_resizer_sends_resize_frame 会用它检查底层消息。它把文件里的二进制帧读成 Message,后面的测试再判断内容对不对。

调用图:调用 1 个内部函数(read_frame);被 2 处调用(runner_resizer_sends_resize_frame, runner_stdin_writer_sends_close_stdin_after_input_eof);外部调用 8 个(from_millis, from_secs, now, new, Start, new, assert!, sleep)。

collect_stdout_and_exit118–150 ↗
async fn collect_stdout_and_exit(
    spawned: codex_utils_pty::SpawnedProcess,
    codex_home: &Path,
    timeout_duration: Duration,
) -> (Vec<u8>, i32)

作用:收集一个已启动进程的标准输出,并等待它退出。标准输出就是程序正常打印出来的内容。

数据流:进去一个 SpawnedProcess、它的 codex_home、以及最长等待时间 → 它开一个异步任务持续收 stdout 通道里的字节,同时等待退出码;如果等太久就带着沙箱日志报错 → 出来收集到的 stdout 字节和退出码。

调用关系:多个启动类测试都会把 spawn_windows_sandbox_session_legacy 返回的进程交给它。它处在“沙箱已启动”和“断言结果”之间,负责把异步输出整理成测试好判断的数据。

调用图:被 5 处调用(legacy_non_tty_cmd_emits_output, legacy_non_tty_powershell_emits_output, legacy_tty_cmd_default_desktop_emits_output_and_accepts_input, legacy_tty_cmd_emits_output_and_accepts_input, legacy_tty_powershell_emits_output_and_accepts_input);外部调用 3 个(new, spawn, timeout)。

legacy_non_tty_cmd_emits_output153–189 ↗
fn legacy_non_tty_cmd_emits_output()

作用:测试旧版沙箱在非交互模式下运行 cmd.exe 时,能不能正常打印输出并以 0 退出。非交互模式就是不像终端那样持续等用户输入。

数据流:进去没有参数,由测试框架调用 → 它加锁、建运行时、准备工作目录和临时 home,启动 cmd /c echo → 收集 stdout 和退出码 → 断言退出码是 0,并且输出里包含指定文字。

调用关系:它串起 legacy_process_test_guard、current_thread_runtime、sandbox_cwd、sandbox_home、workspace_roots_for、spawn_windows_sandbox_session_legacy 和 collect_stdout_and_exit。这个测试验证最基础的“启动命令并拿到输出”链路。

调用图:调用 7 个内部函数(workspace_write, collect_stdout_and_exit, current_thread_runtime, legacy_process_test_guard, sandbox_cwd, sandbox_home, workspace_roots_for);外部调用 8 个(from_secs, new, from_utf8_lossy, assert!, assert_eq!, println!, spawn_windows_sandbox_session_legacy, vec!)。

legacy_non_tty_cmd_rejects_deny_read_overrides192–228 ↗
fn legacy_non_tty_cmd_rejects_deny_read_overrides()

作用:测试旧版沙箱遇到“禁止读取某个文件”的额外规则时,会明确拒绝。因为这种 deny-read 规则需要更高级的后端支持,旧版后端不能假装能做。

数据流:进去没有参数 → 它准备一个要禁止读取的假 secret 文件路径,尝试用旧版非交互 cmd 启动沙箱 → 期望得到错误 → 检查错误文字说明必须使用 elevated Windows sandbox backend。

调用关系:它和普通 cmd 输出测试类似,也用锁、运行时、工作区和临时 home。但它关注的不是命令输出,而是 spawn_windows_sandbox_session_legacy 在不支持的权限配置下必须失败。

调用图:调用 7 个内部函数(workspace_write, from_absolute_path, current_thread_runtime, legacy_process_test_guard, sandbox_cwd, sandbox_home, workspace_roots_for);外部调用 5 个(new, assert!, from_ref, spawn_windows_sandbox_session_legacy, vec!)。

legacy_non_tty_powershell_emits_output231–271 ↗
fn legacy_non_tty_powershell_emits_output()

作用:测试旧版沙箱在非交互模式下运行 PowerShell 7 时,能不能正常输出文字。它补充验证不只是 cmd.exe 可用,PowerShell 也可用。

数据流:进去没有参数 → 它先找 pwsh.exe,找不到就跳过;找到后准备沙箱环境,运行 Write-Output 命令 → 收集输出和退出码 → 断言退出成功且 stdout 包含测试文字。

调用关系:它先依赖 pwsh_path 决定能不能测,再走和非交互 cmd 类似的启动、收集、断言流程。它帮助发现 PowerShell 启动方式或参数处理上的问题。

调用图:调用 8 个内部函数(workspace_write, collect_stdout_and_exit, current_thread_runtime, legacy_process_test_guard, pwsh_path, sandbox_cwd, sandbox_home, workspace_roots_for);外部调用 8 个(from_secs, new, from_utf8_lossy, assert!, assert_eq!, println!, spawn_windows_sandbox_session_legacy, vec!)。

finish_driver_spawn_keeps_stdin_open_when_requested274–303 ↗
fn finish_driver_spawn_keeps_stdin_open_when_requested()

作用:测试 finish_driver_spawn 在要求保持标准输入打开时,确实允许后续继续写入。标准输入就是外部喂给程序的内容。

数据流:进去没有参数 → 它搭一个假的 ProcessDriver,设置 stdin_open 为 true → 通过生成的 session 发送一段字节 → 从底层 writer_rx 收到同样字节,证明输入通道还开着。

调用关系:它直接测试 super::finish_driver_spawn 的封装行为,不需要真正启动 Windows 沙箱。这个测试保护的是“调用方想流式输入时,不要被提前关门”的细节。

调用图:调用 1 个内部函数(current_thread_runtime);外部调用 2 个(assert_eq!, finish_driver_spawn)。

finish_driver_spawn_closes_stdin_when_not_requested306–337 ↗
fn finish_driver_spawn_closes_stdin_when_not_requested()

作用:测试 finish_driver_spawn 在不要求保持标准输入打开时,会把输入通道关掉。这样不会让程序一直以为还有输入可等。

数据流:进去没有参数 → 它搭一个假的 ProcessDriver,设置 stdin_open 为 false → 尝试通过 session 发送字节 → 期望发送失败,证明 stdin 已经关闭。

调用关系:它和 keeps_stdin_open 测试是一正一反,直接围绕 super::finish_driver_spawn。两者一起确认输入通道的开关完全听 stdin_open 参数。

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

runner_stdin_writer_sends_close_stdin_after_input_eof340–383 ↗
fn runner_stdin_writer_sends_close_stdin_after_input_eof()

作用:测试 runner 的标准输入写入器在输入源结束后,会额外发一条“关闭标准输入”的消息。否则对面的进程可能一直等输入,不知道已经没了。

数据流:进去没有参数 → 它创建临时帧文件,启动管道写入器和 stdin 写入器,发送 hello 后关闭发送端 → 等写入任务结束,再读取两个消息帧 → 断言第一帧是 Stdin 且内容是 hello,第二帧是 CloseStdin。

调用关系:它调用 start_runner_pipe_writer 和 start_runner_stdin_writer 来模拟 runner 发消息,再用 wait_for_frame_count 读取结果,并用 decode_bytes 检查消息里的 base64 数据。它验证的是输入结束信号不会丢。

调用图:调用 3 个内部函数(decode_bytes, current_thread_runtime, wait_for_frame_count);外部调用 6 个(new, new, assert_eq!, panic!, start_runner_pipe_writer, start_runner_stdin_writer)。

runner_resizer_sends_resize_frame386–416 ↗
fn runner_resizer_sends_resize_frame()

作用:测试终端窗口大小变化时,会被转换成一条 Resize 消息发出去。交互式终端需要这个,否则程序不知道屏幕有多大。

数据流:进去没有参数 → 它创建临时帧文件和管道写入器,生成一个 resizer 回调,然后传入 45 行、132 列的终端大小 → 读取一条消息帧 → 断言消息类型是 Resize,行列数正确。

调用关系:它直接使用 make_runner_resizer 生成“改窗口大小”的函数,并通过 start_runner_pipe_writer 把结果写进文件。wait_for_frame_count 负责把文件里的消息读回来给测试检查。

调用图:调用 2 个内部函数(current_thread_runtime, wait_for_frame_count);外部调用 6 个(new, new, assert_eq!, panic!, make_runner_resizer, start_runner_pipe_writer)。

legacy_capture_powershell_emits_output419–455 ↗
fn legacy_capture_powershell_emits_output()

作用:测试一次性捕获模式运行 PowerShell 时,能拿到输出和退出码。捕获模式就是运行完再把 stdout、stderr、退出码一起交回来。

数据流:进去没有参数 → 它找 PowerShell,准备工作区和临时 home,调用 run_windows_sandbox_capture 运行 Write-Output → 得到包含 stdout、stderr、退出码和超时标记的结果 → 断言退出码为 0 且 stdout 包含指定文字。

调用关系:它不走异步 SpawnedProcess 收集流程,而是直接测试 run_windows_sandbox_capture 这条同步捕获通道。它仍然使用锁和共同的沙箱环境辅助函数。

调用图:调用 6 个内部函数(workspace_write, legacy_process_test_guard, pwsh_path, sandbox_cwd, sandbox_home, workspace_roots_for);外部调用 7 个(new, from_utf8_lossy, assert!, assert_eq!, run_windows_sandbox_capture, println!, vec!)。

legacy_capture_cancellation_is_not_reported_as_timeout458–506 ↗
fn legacy_capture_cancellation_is_not_reported_as_timeout()

作用:测试用户取消长时间运行的捕获任务时,结果不能被误标成超时。取消和超时含义不同,前者是主动停止,后者是跑太久。

数据流:进去没有参数 → 它准备一个原子布尔值作为取消开关,另起线程 200 毫秒后打开开关;沙箱里运行会睡 30 秒的 PowerShell 命令 → run_windows_sandbox_capture 看到取消后提前结束 → 断言结束很快、timed_out 为 false、退出码不是 0。

调用关系:它使用 WindowsSandboxCancellationToken 把“是否取消”的检查交给捕获执行流程。它保护的是取消路径的语义,避免上层用户看到错误的“超时”提示。

调用图:调用 7 个内部函数(workspace_write, new, legacy_process_test_guard, pwsh_path, sandbox_cwd, sandbox_home, workspace_roots_for);外部调用 11 个(clone, new, new, new, now, assert!, assert_ne!, run_windows_sandbox_capture, eprintln!, spawn (+1 more))。

legacy_tty_powershell_emits_output_and_accepts_input509–563 ↗
fn legacy_tty_powershell_emits_output_and_accepts_input()

作用:测试旧版沙箱的交互式 PowerShell 终端既能输出初始内容,也能接收后续输入。TTY 可以理解成带键盘和屏幕的终端模式。

数据流:进去没有参数 → 它找到 PowerShell,启动带 TTY 且 stdin_open 的沙箱会话,让 PowerShell 先输出 ready → 通过 writer 发送第二条命令和 exit,再关闭 stdin → 收集输出和退出码 → 断言退出成功,并且输出里有 ready 和 second。

调用关系:它连接了启动交互式会话、通过 session 写输入、最后 collect_stdout_and_exit 收结果这几步。它验证的是“开一个终端、继续打命令、正常退出”这条完整链路。

调用图:调用 8 个内部函数(workspace_write, collect_stdout_and_exit, current_thread_runtime, legacy_process_test_guard, pwsh_path, sandbox_cwd, sandbox_home, workspace_roots_for);外部调用 8 个(from_secs, new, from_utf8_lossy, assert!, assert_eq!, println!, spawn_windows_sandbox_session_legacy, vec!)。

legacy_tty_cmd_emits_output_and_accepts_input567–614 ↗
fn legacy_tty_cmd_emits_output_and_accepts_input()

作用:测试旧版沙箱的交互式 cmd.exe 能输出并接受后续输入。不过这个测试被标记为忽略,因为 CI 里旧版 ConPTY 跑 cmd.exe 有已知失败。

数据流:进去没有参数,通常不会自动执行 → 它准备沙箱,启动 cmd /K echo ready,之后发送 echo second 和 exit,再关闭 stdin → 收集输出和退出码 → 如果运行,期望退出码为 0 且输出包含 ready 和 second。

调用关系:它的流程和交互式 PowerShell 测试很像,只是程序换成 cmd.exe。因为有已知环境问题,它更多像保留的回归测试或本地排查工具。

调用图:调用 6 个内部函数(workspace_write, collect_stdout_and_exit, current_thread_runtime, sandbox_cwd, sandbox_home, workspace_roots_for);外部调用 8 个(from_secs, new, from_utf8_lossy, assert!, assert_eq!, println!, spawn_windows_sandbox_session_legacy, vec!)。

legacy_tty_cmd_default_desktop_emits_output_and_accepts_input618–668 ↗
fn legacy_tty_cmd_default_desktop_emits_output_and_accepts_input()

作用:测试交互式 cmd.exe 在默认桌面而不是私有桌面下,也能输出并接受输入。这个测试同样因 CI 已知问题被忽略。

数据流:进去没有参数,通常不会自动执行 → 它启动带 TTY 的 cmd.exe,但 use_private_desktop 传 false → 发送第二条 echo 命令和 exit,关闭 stdin,收集结果 → 期望输出包含 ready 和 second,并且退出成功。

调用关系:它和 legacy_tty_cmd_emits_output_and_accepts_input 几乎是同一条链路,区别是桌面隔离选项不同。它用来覆盖“默认桌面”这个运行环境分支。

调用图:调用 6 个内部函数(workspace_write, collect_stdout_and_exit, current_thread_runtime, sandbox_cwd, sandbox_home, workspace_roots_for);外部调用 8 个(from_secs, new, from_utf8_lossy, assert!, assert_eq!, println!, spawn_windows_sandbox_session_legacy, vec!)。

core/tests/suite/windows_sandbox.rs源码 ↗
testtest execution

这个文件像一组安全演习。它先准备一个临时的 CODEX_HOME,也就是 Codex 放自己状态文件的家目录,再准备一个临时工作目录,里面放公开文件和秘密文件。测试会构造文件系统沙箱策略:有的地方允许读写,有些文件按路径或通配符禁止读取。然后它真的调用命令执行入口 process_exec_tool_call,让 Windows 沙箱跑一段 cmd.exe 命令,看看命令能不能偷看秘密文件。这里测试了两种 Windows 沙箱级别:RestrictedToken 是不提权的受限令牌沙箱,它不能直接保证“禁止读取”,所以必须拒绝执行,不能假装安全;Elevated 是提权后使用的沙箱,它应该能挡住秘密文件,还要保护自己的 setup_marker.json,不让被沙箱里的命令读取或篡改。EnvVarGuard 像临时借用一把钥匙:测试期间改环境变量,结束自动还原,避免污染别的测试。

函数细节7
EnvVarGuard::set31–37 ↗
fn set(key: &'static str, value: &std::ffi::OsStr) -> Self

作用:临时设置一个环境变量,并记住它原来的值。测试需要临时把 CODEX_HOME 指到测试目录,但不能影响后面的测试,所以要先把旧值存起来。

数据流:进去的是环境变量名和新值;它先读取这个变量原来有没有值,再把变量改成新值;出来的是一个 EnvVarGuard 对象,里面保存了变量名和旧值,等测试结束时可以恢复现场。

调用关系:两个 Windows 沙箱测试都会用它来临时设置 CODEX_HOME。它把“改环境变量”的活儿做完后,后续真正执行命令的是测试函数里的 process_exec_tool_call;恢复工作则交给 EnvVarGuard::drop 自动完成。

调用图:外部调用 2 个(set_var, var_os)。

EnvVarGuard::drop41–48 ↗
fn drop(&mut self)

作用:在 EnvVarGuard 离开作用范围时自动恢复环境变量。这样测试就算中途失败,也尽量不会把 CODEX_HOME 留成测试用的路径。

数据流:进去的是 EnvVarGuard 保存的变量名和旧值;如果旧值存在,就把环境变量改回旧值;如果旧值本来不存在,就删除这个变量;它不返回业务结果,只改变进程的环境变量。

调用关系:它不是被测试代码手动调用,而是 Rust 在对象销毁时自动调用。EnvVarGuard::set 负责改,EnvVarGuard::drop 负责还原,二者配合让这两个串行测试不会互相弄乱环境。

调用图:外部调用 2 个(remove_var, set_var)。

TestCodexHome::path57–62 ↗
fn path(&self) -> &Path

作用:取出测试用 CODEX_HOME 的真实路径。不管这个目录是固定目录还是临时目录,测试代码都可以用同一种方式拿到路径。

数据流:进去的是一个 TestCodexHome;如果它装的是 Persistent,就返回里面保存的 PathBuf 路径;如果它装的是 Temporary,就返回临时目录的路径;它只读取路径,不创建也不修改文件。

调用关系:codex_home_for_windows_sandbox_test 先创建 TestCodexHome,后面的测试函数用 TestCodexHome::path 把它变成可用路径,再交给 EnvVarGuard::set 设置 CODEX_HOME,或用来检查沙箱 setup_marker.json。

codex_home_for_windows_sandbox_test65–77 ↗
fn codex_home_for_windows_sandbox_test(name: &str) -> anyhow::Result<TestCodexHome>

作用:为 Windows 沙箱测试准备一个安全的 CODEX_HOME 目录。它会根据运行环境选择固定目录或临时目录,避免测试重试时沙箱状态对不上。

数据流:进去的是测试名字;它先看有没有 TEST_TMPDIR 这个环境变量。如果有,就在这个测试临时根目录下创建一个稳定的子目录,并返回 Persistent;如果没有,就创建一个真正的临时目录,并返回 Temporary。失败时返回带上下文的错误,方便看出是哪一步出问题。

调用关系:两个主要测试一开始都会调用它,因为 Windows 沙箱的准备状态会写到 CODEX_HOME。它准备好目录后,测试再用 EnvVarGuard::set 临时告诉 Codex 使用这个目录。

调用图:被 2 处调用(windows_elevated_enforces_deny_read_and_protects_setup_marker, windows_restricted_token_rejects_exact_and_glob_deny_read_policy);外部调用 6 个(from, new, Persistent, Temporary, var_os, create_dir_all)。

stage_windows_sandbox_helpers79–115 ↗
fn stage_windows_sandbox_helpers() -> anyhow::Result<()>

作用:把 Windows 沙箱运行所需的辅助程序复制到测试可找到的位置。没有这些小工具,提权沙箱测试就无法真正启动完整的 Windows 沙箱流程。

数据流:它不需要外部输入;它先找到当前测试可执行文件所在目录,在旁边创建 codex-resources 文件夹;然后找到 codex-windows-sandbox-setup 和 codex-command-runner 这两个编译好的 exe,并复制进去。如果目标文件正被上一次残留进程占用,它会在已有文件存在时容忍这个权限错误;最后成功时返回空结果,失败时返回带说明的错误。

调用关系:只有 elevated 沙箱测试会调用它,因为 elevated 模式需要这些辅助 exe。它在真正调用 process_exec_tool_call 之前完成准备,相当于先把工具摆到沙箱启动器能拿到的工具箱里。

调用图:被 1 处调用(windows_elevated_enforces_deny_read_and_protects_setup_marker);外部调用 5 个(new, cargo_bin, current_exe, copy, create_dir_all)。

windows_restricted_token_rejects_exact_and_glob_deny_read_policy119–197 ↗
async fn windows_restricted_token_rejects_exact_and_glob_deny_read_policy() -> anyhow::Result<()>

作用:测试不提权的 RestrictedToken Windows 沙箱遇到“禁止读取文件”规则时,必须直接拒绝执行。重点不是让命令跑完,而是确认系统不会在无法保证安全时冒险运行。

数据流:它先准备 CODEX_HOME 和临时工作目录,再创建 secret.env、future.env、public.txt 等文件路径;接着构造沙箱策略:根目录可读、项目目录可写,但所有 .env 文件和指定文件禁止访问;然后把策略变成 PermissionProfile,调用 process_exec_tool_call 运行一段 cmd.exe 命令。预期结果是调用失败;最后它检查错误文字,确认失败原因正是 restricted-token 沙箱不能强制执行 deny-read,所以拒绝无沙箱运行。

调用关系:这是一个异步测试,由测试框架启动。它依赖 codex_home_for_windows_sandbox_test 准备目录,用 EnvVarGuard::set 临时设置 CODEX_HOME,再把核心验证交给 process_exec_tool_call。它和另一个 elevated 测试形成对照:一个确认“不支持就拒绝”,另一个确认“支持时要真正挡住”。

调用图:调用 5 个内部函数(set, process_exec_tool_call, codex_home_for_windows_sandbox_test, from_runtime_permissions, restricted);外部调用 7 个(new, new, assert_eq!, canonicalize, write, from_ref, vec!)。

windows_elevated_enforces_deny_read_and_protects_setup_marker201–322 ↗
async fn windows_elevated_enforces_deny_read_and_protects_setup_marker() -> anyhow::Result<()>

作用:测试提权版 Windows 沙箱真的能挡住被禁止读取的文件,并且不能让沙箱里的命令偷看或修改沙箱自己的准备标记文件。它验证的是沙箱最关键的安全承诺。

数据流:它先准备 CODEX_HOME、复制沙箱辅助程序、创建临时工作目录,再写入 glob_secret、exact_secret 和 public 文件;随后构造策略:普通区域可读写,但 .env 文件和 exact-secret.txt 禁止读取。它调用 process_exec_tool_call 运行 cmd.exe,让命令尝试读取两个秘密文件、读取和写入 setup_marker.json、再读取公开文件。返回后它检查退出码和输出:秘密文件必须显示 DENIED,公开文件必须可读,setup_marker 必须读写都失败;最后还调用 sandbox_setup_is_complete,确认标记文件没有被破坏。

调用关系:这是 elevated 模式的主测试,由测试框架运行。它先用 codex_home_for_windows_sandbox_test 和 EnvVarGuard::set 安排测试环境,再用 stage_windows_sandbox_helpers 放好辅助 exe,最后通过 process_exec_tool_call 触发真实沙箱执行,并用 sandbox_setup_is_complete 做收尾安全检查。

调用图:调用 6 个内部函数(set, process_exec_tool_call, codex_home_for_windows_sandbox_test, stage_windows_sandbox_helpers, from_runtime_permissions, restricted);外部调用 8 个(new, new, assert!, assert_eq!, canonicalize, write, from_ref, vec!)。

exec-server/testing/wine_exec_server.rs源码 ↗
testtest setup and teardown

这个文件像一个“临时开店助手”。测试开始时,它先找到名叫 wine-windows-exec-server 的可执行程序,然后用 WineTestCommand 在 Wine 里把它跑起来。Wine 可以简单理解成一层兼容工具,让 Linux 或 macOS 之类的系统能运行 Windows 程序。服务器启动后会在标准输出里打印自己的 WebSocket 地址;WebSocket 是一种让两边能持续通信的网络连接方式。这个文件会一行一行读服务器输出,直到看到以 ws:// 开头的地址,再把这个地址交给调用者提供的测试操作。等测试操作结束后,它通过 scope 的方式把服务器一起关掉。这里重要的一点是:如果服务器还没来得及报地址就退出了,代码会给出清楚的错误原因,而不是让测试莫名其妙卡住或失败。

函数细节1
WineExecServer::scope16–42 ↗
async fn scope(self, operation: F) -> Result<T>

作用:这个函数在一次测试范围内启动 Windows exec-server,把它的 WebSocket 地址交给测试代码,测试结束后再自动关闭服务器。有人会用它来写集成测试,因为它把“启动、等地址、运行测试、清理进程”这些麻烦事包好了。

数据流:输入是一段测试操作 operation,它需要一个服务器地址才能运行。函数先通过 cargo_bin(ext) 找到测试用的 wine-windows-exec-server 程序,再用 WineTestCommand::new(ext) 创建 Wine 启动命令,设置 CODEX_HOME 环境变量后启动进程。接着它读取这个进程的标准输出,一行行找以 ws:// 开头的地址;找到后,把这个地址传给 operation 并等待它完成。最后输出 operation 的结果;同时,服务器进程会随着这个范围结束而被清理掉。如果进程提前退出且没报地址,就返回一条带上下文的错误。

调用关系:它是测试代码进入 Windows exec-server 的小门口:外部测试会调用 WineExecServer::scope,把真正要测的事情作为 operation 交进来。函数内部把准备工作交给 cargo_bin(ext) 来定位可执行文件,把启动 Wine 进程的工作交给 WineTestCommand::new(ext) 创建出的命令对象;等服务器报出地址后,它再把流程交还给测试操作。

调用图:外部调用 3 个(new, new, cargo_bin)。

core/tests/remote_env_windows/remote_env_windows_test.rs源码 ↗
testintegration test / CI under Bazel

这个文件是两段端到端集成测试。可以把它理解成一次“假装真的有用户、真的有模型、真的有 Windows 远程机器”的彩排。测试会启动一个 WineExecServer,也就是在 Wine 里模拟 Windows 执行服务器;Wine 是一种让非 Windows 系统运行 Windows 程序的工具。第一段测试让模型假装要求执行一条 PowerShell 命令,并检查 Codex 是否选了 Windows 原生的 pwsh.exe、是否把当前目录设成 C:\windows、命令是否成功结束,还确认命令输出被送回了模型。第二段测试从应用服务器协议入口开始,发送 thread/start 和 turn/start 请求,确认带 Windows 路径的远程环境不会把线程启动流程搞坏。这里很多断言还记录了当前尚未完善的行为,比如 thread/start 暂时返回的是宿主机目录,而不是远程 Windows 目录。

函数细节2
windows_exec_server_runs_with_native_shell_and_cwd50–173 ↗
async fn windows_exec_server_runs_with_native_shell_and_cwd() -> Result<()>

作用:这个测试确认 Codex 通过 Windows 远程执行服务器跑命令时,真的会使用 Windows 原生 shell,并且能进入指定的 Windows 当前目录。没有这个测试,代码可能看似能连上远程服务器,却偷偷用错 shell 或路径,导致真实 Windows 命令失败。

数据流:进去的是一个临时启动的 Wine 执行服务器地址、一个假模型服务器,以及用户发出的“运行 Windows smoke command”请求。测试先让假模型返回一次 exec_command 工具调用,命令内容是检查当前位置必须是 C:\windows;然后把这个请求提交给 Codex,并监听事件流。出来的是一组断言结果:开始事件里的命令必须以 pwsh.exe 结尾,参数必须是 -NoProfile、-Command 和原命令;结束事件必须表示退出码为 0 且已完成;最后还检查模型收到了命令执行结果,而且没有被标成失败。

调用关系:这个函数由 Tokio 异步测试框架在测试时启动。它先通过 WineExecServer.scope 准备 Windows 执行环境,再用 start_mock_server 和 mount_sse_sequence 搭出一个会发工具调用的假模型服务;接着用 test_codex 构造 Codex 测试实例,把远程环境选择和权限设置塞进一次 UserInput 操作里。提交请求后,它不断调用 wait_for_event 等待 ExecCommandBegin、ExecCommandEnd 和 TurnComplete,最后从 mock 响应服务器取回请求,验证执行结果确实回传给模型。

app_server_starts_thread_with_windows_environment_native_cwd176–252 ↗
async fn app_server_starts_thread_with_windows_environment_native_cwd() -> Result<()>

作用:这个测试确认应用服务器这一层能接受 Windows 风格的远程环境目录,比如 C:\windows,并且线程启动和后续对话启动都能正常走完。它重点保护的是“从客户端协议进来”的路径处理,而不只是底层执行命令是否可用。

数据流:进去的是一个 Wine 执行服务器地址、一个临时的 Codex 主目录、一个总是回答“done”的假模型服务器,以及 thread/start 请求里带的远程环境信息。测试先写好 mock 配置,再启动 TestAppServer,并把执行服务器地址通过环境变量传进去;随后发送 thread/start,请求里声明远程环境 id 和 Windows 当前目录。出来的是 thread/start 的响应和 turn/start 的响应:测试确认线程 id 不为空,并确认当前实现返回的 cwd、工作区根目录、指令来源、权限配置等字段符合预期;最后再发送一轮用户消息,等到 turn/completed 通知,证明整条应用服务器流程没有卡住。

调用关系:这个函数同样由 Tokio 异步测试框架运行。它用 WineExecServer.scope 提供远程执行服务,用 create_mock_responses_server_repeating_assistant 准备假模型回复,用 write_mock_responses_config_toml 写应用服务器配置,再通过 TestAppServer.new_with_env 启动真正的应用服务器测试实例。之后它按客户端会做的顺序调用 initialize、send_thread_start_request、read_stream_until_response_message、send_turn_start_request 和 read_stream_until_notification_message,检查 Windows 远程环境信息能从协议入口一路传到线程和回合流程里。

RMCP 远程客户端流程

最后一组聚焦 RMCP 客户端测试框架,以及通过 stdio 和可流式 HTTP 传输的端到端远程执行路径。

rmcp-client/tests/streamable_http_test_support.rs源码 ↗
testintegration test setup and request handling

这些测试不是只测一小段代码,而是让一个真的 HTTP 测试服务器、RMCP 客户端,甚至可选的 exec-server 一起跑起来,像真实使用一样互相通信。这个文件把重复的准备工作都收在一起:先找服务器二进制文件,挑一个空端口启动它,等它真的能连上;再创建 RMCP 客户端并完成初始化握手。它还提供一些“遥控开关”,可以告诉测试服务器:下一次请求故意返回 401、404,或者返回 JSON-RPC 错误。JSON-RPC 可以理解成一种用 JSON 包装请求和错误的通信格式。这样测试就能验证客户端遇到临时故障时会不会按预期重试,遇到不该恢复的错误时会不会乱来。文件里还有 exec-server 相关辅助,用来测试远程 HTTP 通道。整体上,它像测试现场的道具组和场务:把演员叫上场,布置故障,再让测试剧本专心验证行为。

函数细节18
streamable_http_server_bin53–55 ↗
fn streamable_http_server_bin() -> Result<PathBuf, CargoBinError>

作用:找到名叫 test_streamable_http_server 的测试服务器程序在哪里。测试要启动服务器,先得知道这个可执行文件的路径。

数据流:进去没有参数 → 它询问 cargo 测试环境里对应二进制文件的位置 → 出来一个文件路径;如果找不到,就返回错误。

调用关系:它是启动 HTTP 测试服务器前的第一步。spawn_streamable_http_server 会调用它拿到程序路径,然后才能真正拉起服务器进程。

调用图:被 1 处调用(spawn_streamable_http_server);外部调用 1 个(cargo_bin)。

init_params57–70 ↗
fn init_params() -> InitializeRequestParams

作用:准备 RMCP 客户端初始化时要发给服务器的自我介绍。这里说明客户端支持哪些能力,以及使用哪个协议版本。

数据流:进去没有参数 → 它创建默认客户端能力,打开 elicitation 能力,也就是服务器可以向客户端询问额外信息的能力,再填入测试客户端名称、版本和协议版本 → 出来一份初始化请求参数。

调用关系:initialize_client 和 create_remote_client 都会用它。它保证普通客户端和远程客户端初始化时说的是同一套“开场白”。

调用图:被 2 处调用(create_remote_client, initialize_client);外部调用 3 个(default, new, new)。

expected_echo_result72–79 ↗
fn expected_echo_result(message: &str) -> CallToolResult

作用:生成 echo 工具调用后应该得到的标准答案。测试用它来比较实际返回是否正确。

数据流:进去一段 message 文本 → 它把文本包装成结构化 JSON,内容是 ECHOING: 加上原消息,并把 env 设为空 → 出来一个成功的 CallToolResult。

调用关系:它不负责发请求,只负责做“答案模板”。调用 echo 工具的测试可以拿它和 call_echo_tool 的返回值比较。

调用图:外部调用 3 个(new, json!, success)。

create_client81–84 ↗
async fn create_client(base_url: &str) -> anyhow::Result<RmcpClient>

作用:用默认测试 HTTP 客户端创建一个已经初始化好的 RMCP 客户端。大多数测试只想要一个能直接用的客户端,就调用它。

数据流:进去服务器 base_url → 它从测试环境拿默认 HTTP 客户端,再交给 create_client_with_http_client 创建并初始化 RMCP 客户端 → 出来一个可直接调用工具的 RmcpClient。

调用关系:很多 Streamable HTTP 测试都会先调用它。它把“选默认 HTTP 实现”和“建客户端”的细节交给 create_client_with_http_client。

调用图:调用 2 个内部函数(default_for_tests, create_client_with_http_client);被 11 处调用(streamable_http_401_does_not_trigger_recovery, streamable_http_403_finds_bearer_challenge_in_later_header_value, streamable_http_403_scope_challenge_returns_insufficient_scope, streamable_http_404_recovery_only_retries_once, streamable_http_404_session_expiry_recovers_and_retries_once, streamable_http_initialize_retries_json_rpc_transient_status, streamable_http_initialize_retries_transient_http_status, streamable_http_non_session_failure_does_not_trigger_recovery, streamable_http_retries_initialized_notification_status, streamable_http_tools_list_retries_json_rpc_transient_status (+1 more))。

create_client_with_http_client86–106 ↗
async fn create_client_with_http_client(
    base_url: &str,
    http_client: Arc<dyn HttpClient>,
) -> anyhow::Result<RmcpClient>

作用:用指定的 HTTP 传输实现创建 RMCP 客户端,并立刻完成初始化。它适合测试普通 HTTP,也适合插入特殊 HTTP 客户端来模拟远程行为。

数据流:进去服务器 base_url 和一个 HTTP 客户端对象 → 它拼出 /mcp 地址,设置测试用 bearer token,创建 Streamable HTTP RMCP 客户端,然后调用 initialize_client 做握手 → 出来初始化完成的 RmcpClient。

调用关系:create_client 会把默认 HTTP 客户端交给它;某些测试也会直接调用它,换成远程或特殊 HTTP 客户端。它建好对象后,把初始化工作交给 initialize_client。

调用图:调用 3 个内部函数(default, new_streamable_http_client, initialize_client);被 3 处调用(streamable_http_initialize_retries_remote_no_response_error, streamable_http_session_recovery_retries_initialize_failure, create_client);外部调用 1 个(format!)。

initialize_client108–126 ↗
async fn initialize_client(client: &RmcpClient) -> anyhow::Result<()>

作用:让 RMCP 客户端和服务器完成初始化握手。没有这一步,后面调用工具就像还没办入场手续,不能正常开始。

数据流:进去一个 RmcpClient 引用 → 它生成初始化参数,设置 5 秒超时,并提供一个总是接受的询问回调 → 成功时不返回额外数据,但客户端状态变成已初始化;失败时返回错误。

调用关系:create_client_with_http_client 会用它;OAuth 启动子流程测试也会用它。它内部调用 init_params 准备握手内容,再调用客户端的 initialize。

调用图:调用 2 个内部函数(initialize, init_params);被 2 处调用(oauth_startup_child, create_client_with_http_client);外部调用 2 个(new, from_secs)。

create_remote_client130–165 ↗
async fn create_remote_client(
    base_url: &str,
    http_client: ExecServerClient,
) -> anyhow::Result<RmcpClient>

作用:创建一个通过 exec-server 远程运行时转发 HTTP 请求的 RMCP 客户端。它用来测试“客户端不直接联网,而是让远程执行服务代发请求”的场景。

数据流:进去服务器 base_url 和 ExecServerClient → 它把 ExecServerClient 包成 HTTP 客户端,拼出 /mcp 地址,创建 Streamable HTTP RMCP 客户端,并用标准初始化参数完成握手 → 出来一个可用的远程 RMCP 客户端。

调用关系:streamable_http_remote_client_round_trips_through_exec_server 这类远程往返测试会调用它。它和 create_client_with_http_client 做的事相似,但专门使用 exec-server 作为 HTTP 通道。

调用图:调用 3 个内部函数(default, new_streamable_http_client, init_params);被 1 处调用(streamable_http_remote_client_round_trips_through_exec_server);外部调用 4 个(new, new, from_secs, format!)。

call_echo_tool167–179 ↗
async fn call_echo_tool(
    client: &RmcpClient,
    message: &str,
) -> anyhow::Result<CallToolResult>

作用:调用服务器上的 echo 工具,让服务器把消息回显回来。测试用它来验证一次完整请求是否成功穿过客户端、HTTP 通道和服务器。

数据流:进去一个 RmcpClient 和 message 文本 → 它把 message 放进 JSON 参数,调用名为 echo 的工具,并设置 5 秒超时 → 出来服务器返回的 CallToolResult,或者调用失败的错误。

调用关系:大量恢复和重试测试都会在布置故障后调用它。它是测试流程里的“实际发一笔业务请求”的动作。

调用图:调用 1 个内部函数(call_tool);被 12 处调用(streamable_http_401_does_not_trigger_recovery, streamable_http_403_finds_bearer_challenge_in_later_header_value, streamable_http_403_scope_challenge_returns_insufficient_scope, streamable_http_404_recovery_only_retries_once, streamable_http_404_session_expiry_recovers_and_retries_once, streamable_http_initialize_retries_json_rpc_transient_status, streamable_http_initialize_retries_remote_no_response_error, streamable_http_initialize_retries_transient_http_status, streamable_http_non_session_failure_does_not_trigger_recovery, streamable_http_retries_initialized_notification_status (+2 more));外部调用 2 个(from_secs, json!)。

arm_session_post_failure181–199 ↗
async fn arm_session_post_failure(
    base_url: &str,
    status: u16,
    remaining: usize,
    www_authenticate_headers: &[&str],
) -> anyhow::Result<()>

作用:告诉测试服务器:接下来的会话 POST 请求要故意失败几次,并返回指定 HTTP 状态码。这样测试可以验证客户端遇到会话失败时的恢复逻辑。

数据流:进去服务器 base_url、状态码、剩余失败次数,以及可选的 WWW-Authenticate 认证提示头 → 它向测试服务器的控制接口 POST 一段 JSON 配置 → 如果服务器返回 204 No Content,就说明故障布置成功;否则断言失败。

调用关系:很多 401、403、404、会话过期和工具列表重试测试会先调用它,再调用 call_echo_tool 或其他请求,看客户端是否按预期处理。

调用图:调用 1 个内部函数(new);被 8 处调用(streamable_http_401_does_not_trigger_recovery, streamable_http_403_finds_bearer_challenge_in_later_header_value, streamable_http_403_scope_challenge_returns_insufficient_scope, streamable_http_404_recovery_only_retries_once, streamable_http_404_session_expiry_recovers_and_retries_once, streamable_http_non_session_failure_does_not_trigger_recovery, streamable_http_session_recovery_retries_initialize_failure, streamable_http_tools_list_retries_transient_http_status);外部调用 3 个(assert_eq!, format!, json!)。

arm_session_post_json_rpc_failure201–226 ↗
async fn arm_session_post_json_rpc_failure(
    base_url: &str,
    status: u16,
    remaining: usize,
) -> anyhow::Result<()>

作用:告诉测试服务器:接下来的会话 POST 请求返回一个 JSON-RPC 格式的错误,而不是普通空错误。它用来测试客户端能不能识别这种“包装在 JSON 里的临时失败”。

数据流:进去服务器 base_url、HTTP 状态码和失败次数 → 它向控制接口发送 JSON,里面包含 content_type 为 application/json,以及一段 JSON-RPC error 正文 → 服务器确认后返回成功;否则测试断言失败。

调用关系:streamable_http_tools_list_retries_json_rpc_transient_status 会用它布置工具列表请求的临时 JSON-RPC 错误,然后观察客户端是否重试。

调用图:调用 1 个内部函数(new);被 1 处调用(streamable_http_tools_list_retries_json_rpc_transient_status);外部调用 3 个(assert_eq!, format!, json!)。

arm_initialized_notification_post_json_rpc_failure228–255 ↗
async fn arm_initialized_notification_post_json_rpc_failure(
    base_url: &str,
    status: u16,
    remaining: usize,
) -> anyhow::Result<()>

作用:让测试服务器在 initialized 通知这一步故意返回 JSON-RPC 错误。initialized 通知可以理解成客户端握手后告诉服务器“我准备好了”的消息。

数据流:进去服务器 base_url、状态码和失败次数 → 它向 initialized-notification 专用控制接口发送故障配置 JSON → 服务器返回 204 表示布置成功,否则断言失败。

调用关系:streamable_http_retries_initialized_notification_status 会调用它。这个函数专门覆盖初始化后通知失败时的重试行为。

调用图:调用 1 个内部函数(new);被 1 处调用(streamable_http_retries_initialized_notification_status);外部调用 3 个(assert_eq!, format!, json!)。

arm_initialize_post_failure257–273 ↗
async fn arm_initialize_post_failure(
    base_url: &str,
    status: u16,
    remaining: usize,
) -> anyhow::Result<()>

作用:告诉测试服务器:初始化请求本身要故意失败几次,并返回指定 HTTP 状态码。它用来测试客户端刚开始握手时的重试能力。

数据流:进去服务器 base_url、状态码和失败次数 → 它向 initialize 专用控制接口 POST 故障配置 → 如果收到 204 No Content,说明服务器已准备好制造失败。

调用关系:streamable_http_initialize_retries_transient_http_status 会在创建客户端前调用它。之后 create_client 会触发初始化,从而撞上这个被安排好的失败。

调用图:调用 1 个内部函数(new);被 1 处调用(streamable_http_initialize_retries_transient_http_status);外部调用 3 个(assert_eq!, format!, json!)。

arm_initialize_post_json_rpc_failure275–300 ↗
async fn arm_initialize_post_json_rpc_failure(
    base_url: &str,
    status: u16,
    remaining: usize,
) -> anyhow::Result<()>

作用:让初始化请求返回 JSON-RPC 格式的临时错误。它检查客户端是否不只会处理 HTTP 状态码,也会处理响应正文里的协议错误。

数据流:进去服务器 base_url、状态码和失败次数 → 它发送一份包含 JSON-RPC error 正文的故障配置给 initialize 控制接口 → 成功时服务器返回 204;否则测试断言失败。

调用关系:streamable_http_initialize_retries_json_rpc_transient_status 会用它布置初始化阶段的协议错误,然后通过 create_client 触发并验证重试。

调用图:调用 1 个内部函数(new);被 1 处调用(streamable_http_initialize_retries_json_rpc_transient_status);外部调用 3 个(assert_eq!, format!, json!)。

spawn_streamable_http_server302–316 ↗
async fn spawn_streamable_http_server() -> anyhow::Result<(Child, String)>

作用:启动 Streamable HTTP 测试服务器,并返回服务器进程和它的访问地址。测试需要一个真的本地服务器,这个函数负责把它安全拉起来。

数据流:进去没有参数 → 它先占用一个随机空端口再释放,拼出绑定地址和 base_url,启动测试服务器进程,并把绑定地址放进环境变量 → 等服务器端口能连上后,出来子进程句柄和 base_url。

调用关系:几乎所有 Streamable HTTP 集成测试都会先调用它。它先用 streamable_http_server_bin 找程序,再用 wait_for_streamable_http_server 等服务器真正就绪。

调用图:调用 2 个内部函数(streamable_http_server_bin, wait_for_streamable_http_server);被 14 处调用(streamable_http_401_does_not_trigger_recovery, streamable_http_403_finds_bearer_challenge_in_later_header_value, streamable_http_403_scope_challenge_returns_insufficient_scope, streamable_http_404_recovery_only_retries_once, streamable_http_404_session_expiry_recovers_and_retries_once, streamable_http_initialize_retries_json_rpc_transient_status, streamable_http_initialize_retries_remote_no_response_error, streamable_http_initialize_retries_transient_http_status, streamable_http_non_session_failure_does_not_trigger_recovery, streamable_http_retries_initialized_notification_status (+4 more));外部调用 4 个(from_secs, bind, new, format!)。

ExecServerProcess::drop327–329 ↗
fn drop(&mut self)

作用:当 ExecServerProcess 被丢弃时,尽量停止它拥有的 exec-server 子进程。这样测试结束后不容易留下后台进程。

数据流:进去的是即将销毁的 ExecServerProcess 自身 → 它对 child 调用 start_kill,请操作系统结束进程 → 没有正常返回值,也故意忽略停止失败的结果。

调用关系:这是 Rust 的 Drop 清理钩子,会在对象离开作用域时自动运行。spawn_exec_server 创建的 ExecServerProcess 依靠它做测试后的收尾。

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

spawn_exec_server333–356 ↗
async fn spawn_exec_server() -> anyhow::Result<ExecServerProcess>

作用:启动本地 codex exec-server,并连上它,得到一个可用的 ExecServerClient。这个服务用于测试远程 HTTP 请求转发。

数据流:进去没有参数 → 它创建临时 CODEX_HOME,启动 codex exec-server,让它监听随机 WebSocket 地址,从 stdout 读取真实监听地址,再用这个地址建立 WebSocket 连接 → 出来一个 ExecServerProcess,里面包含临时目录、子进程和已连接客户端。

调用关系:远程客户端往返测试会调用它。它把读取监听地址的细活交给 read_exec_server_listen_url,随后用 connect_websocket 建立连接。

调用图:调用 1 个内部函数(read_exec_server_listen_url);被 1 处调用(streamable_http_remote_client_round_trips_through_exec_server);外部调用 8 个(inherit, null, piped, new, new, cargo_bin, connect_websocket, new)。

read_exec_server_listen_url359–382 ↗
async fn read_exec_server_listen_url(child: &mut Child) -> anyhow::Result<String>

作用:从 exec-server 的标准输出里读出它实际监听的 WebSocket 地址。因为端口是随机分配的,测试只能等服务器自己打印出来。

数据流:进去一个 exec-server 子进程引用 → 它接管子进程 stdout,最多等 10 秒,一行行读取输出,寻找以 ws:// 开头的地址 → 找到就返回地址字符串;超时、stdout 关闭或读失败就返回错误。

调用关系:spawn_exec_server 启动进程后马上调用它。拿到地址后,spawn_exec_server 才能创建 ExecServerClient 连接过去。

调用图:被 1 处调用(spawn_exec_server);外部调用 5 个(new, from_secs, now, bail!, timeout)。

wait_for_streamable_http_server384–423 ↗
async fn wait_for_streamable_http_server(
    server_child: &mut Child,
    address: &str,
    timeout: Duration,
) -> anyhow::Result<()>

作用:等待 Streamable HTTP 测试服务器真的可以接受 TCP 连接。它避免测试刚启动服务器就发请求,结果因为服务器还没准备好而误失败。

数据流:进去服务器子进程、地址和最长等待时间 → 它循环检查子进程是否提前退出,并尝试连接地址;连上就成功返回,没连上就短暂睡眠后重试 → 超时或进程提前退出时返回清楚的错误信息。

调用关系:spawn_streamable_http_server 启动服务器后调用它。它是启动流程里的“等绿灯”步骤,确认服务器可用后才让测试继续。

调用图:被 1 处调用(spawn_streamable_http_server);外部调用 7 个(try_wait, from_millis, now, connect, anyhow!, sleep, timeout)。

rmcp-client/tests/streamable_http_remote.rs源码 ↗
testtest run

这份测试像是在做一次真实演练:先启动一个假的 MCP Streamable HTTP 服务端,再启动一个本地的 exec-server 进程。exec-server 可以理解成“代跑网络请求的中间人”,客户端不直接碰网络,而是把 HTTP 请求交给它去发。接着测试创建一个远程模式的 RMCP 客户端,并让它初始化连接。最后,它调用一个 echo 工具,也就是把输入原样返回的测试工具,检查返回结果是不是和预期完全一样。这个测试重要的地方不是 echo 本身,而是确认整条链路都走了真正的远程执行通道:客户端 → exec-server → HTTP 服务端 → 返回结果。这样可以发现本地直连测试发现不了的问题,比如远程传输、请求转发、响应格式保持是否正常。

函数细节1
streamable_http_remote_client_round_trips_through_exec_server21–37 ↗
async fn streamable_http_remote_client_round_trips_through_exec_server() -> anyhow::Result<()>

作用:这个测试函数验证:RMCP 客户端在远程 Streamable HTTP 模式下,能通过真实的 exec-server 完成初始化和工具调用。有人改动远程传输代码时,它能及时发现“没真正走远程通道”或“返回格式变了”的问题。

数据流:进去的是测试辅助函数提供的服务端、exec-server 和客户端创建能力;它先启动 Streamable HTTP 测试服务,拿到访问地址,再启动 exec-server,接着用这两者创建远程客户端。然后它让客户端调用 echo 工具并传入“remote”,最后把实际结果和 expected_echo_result("remote") 生成的预期结果比较;如果不一样,测试失败,如果一样,就说明这条远程请求链路跑通了。

调用关系:它是整场测试的主流程。它先把启动服务的工作交给 spawn_streamable_http_server 和 spawn_exec_server,再把创建客户端的工作交给 create_remote_client,随后把实际工具调用交给 call_echo_tool,最后用 assert_eq! 做结果核对。整个过程把多个测试辅助零件串起来,模拟一次完整的远程 RMCP 调用。

调用图:调用 4 个内部函数(call_echo_tool, create_remote_client, spawn_exec_server, spawn_streamable_http_server);外部调用 1 个(assert_eq!)。

rmcp-client/tests/process_group_cleanup.rs源码 ↗
testtest run

这个测试文件专门检查“进程组清理”。进程组可以简单理解成一串由同一个命令带起来的进程,全家桶要一起关掉。文件里先准备了一些小工具:找到测试服务器程序、生成客户端初始化参数、判断某个进程号是否还活着、等待进程号文件出现、等待进程退出。然后它做两类真实场景测试:第一类是客户端启动了一个 shell,shell 又启动了一个很久才结束的 sleep 子进程;测试直接丢掉客户端,看这个孙进程会不会被杀掉。第二类是客户端正常初始化一个 stdio 服务器,并让它执行一个长时间工具调用;测试在调用还没结束时主动 shutdown,确认服务器进程退出,挂起的调用也能收场。这里的 stdio 是“标准输入输出”,意思是客户端和本地服务器通过输入输出管道说话,而不是走网络。

函数细节7
stdio_server_bin23–25 ↗
fn stdio_server_bin() -> Result<std::path::PathBuf>

作用:这个函数负责找到测试用的本地 stdio 服务器可执行文件。测试需要真的启动一个小服务器,所以必须先知道这个程序在哪里。

数据流:进去没有业务参数 → 它调用测试工具按名字寻找名为 test_stdio_server 的可执行文件 → 出来是这个程序的路径;如果找不到,就返回错误,让测试失败并说明环境不对。

调用关系:它是第二个测试 shutdown_kills_initialized_stdio_server_with_in_flight_operation 的准备步骤。那个测试要启动真实服务器时,会先向它要服务器程序路径,再交给 RmcpClient::new_stdio_client 去运行。

调用图:被 1 处调用(shutdown_kills_initialized_stdio_server_with_in_flight_operation);外部调用 1 个(cargo_bin)。

init_params27–33 ↗
fn init_params() -> InitializeRequestParams

作用:这个函数拼出客户端初始化服务器时要发的基本身份信息和协议版本。可以把它理解成客户端第一次见服务器时递上的“名片”。

数据流:进去没有参数 → 它创建默认客户端能力,填入测试客户端的名字、版本和标题,再指定要使用的协议版本 → 出来是一份 InitializeRequestParams,供客户端初始化连接时使用。

调用关系:它只在 shutdown_kills_initialized_stdio_server_with_in_flight_operation 里被使用。测试先启动服务器,再用这份初始化参数完成握手,之后才能发起工具调用并测试 shutdown 行为。

调用图:被 1 处调用(shutdown_kills_initialized_stdio_server_with_in_flight_operation);外部调用 3 个(default, new, new)。

process_exists35–43 ↗
fn process_exists(pid: u32) -> bool

作用:这个函数检查某个进程号对应的进程是否还活着。它不杀进程,只是像敲门一样确认“这个人还在不在”。

数据流:进去是一个 pid,也就是进程号 → 它运行 Unix 命令 kill -0,这个命令只做存在性检查,并把错误输出丢掉 → 出来是 true 或 false,表示该进程是否仍存在;如果检查命令本身出问题,也按不存在处理。

调用关系:它是 wait_for_process_exit 的底层探测器。测试不会直接反复写检查逻辑,而是让 wait_for_process_exit 调它来判断目标进程有没有退出。

调用图:被 1 处调用(wait_for_process_exit);外部调用 2 个(new, null)。

wait_for_pid_file45–70 ↗
async fn wait_for_pid_file(path: &Path) -> Result<u32>

作用:这个异步函数等待某个文件里出现进程号。测试启动服务器或子进程后,需要先拿到它的 pid,才能继续检查它是否被正确关闭。

数据流:进去是一个 pid 文件路径 → 它最多循环 50 次,每次尝试读文件;文件还没出现或内容为空就等 100 毫秒再试;读到内容后把它解析成数字进程号 → 出来是 pid;如果文件读不了、内容不是数字,或等太久,就返回带说明的错误。

调用关系:两个测试都会用它。启动 shell 子进程或测试服务器后,被启动的进程会把自己的 pid 写进临时文件;测试再通过这个函数拿到 pid,接着交给 process_exists 或 wait_for_process_exit 做后续验证。

调用图:被 2 处调用(drop_kills_wrapper_process_group, shutdown_kills_initialized_stdio_server_with_in_flight_operation);外部调用 4 个(from_millis, bail!, read_to_string, sleep)。

wait_for_process_exit72–81 ↗
async fn wait_for_process_exit(pid: u32) -> Result<()>

作用:这个异步函数等待某个进程真正消失。它让测试不用立刻下结论,而是给系统一点时间完成关闭和清理。

数据流:进去是一个 pid → 它最多检查 50 次,每次用 process_exists 看进程还在不在;如果还在,就睡 100 毫秒再看 → 如果进程消失就返回成功;如果一直没消失,就返回超时错误。

调用关系:两个测试在触发客户端 drop 或 shutdown 之后都会调用它。它把底层的存在性检查包装成“等到退出为止”的动作,是判断清理是否成功的最后关卡。

调用图:调用 1 个内部函数(process_exists);被 2 处调用(drop_kills_wrapper_process_group, shutdown_kills_initialized_stdio_server_with_in_flight_operation);外部调用 3 个(from_millis, bail!, sleep)。

drop_kills_wrapper_process_group84–116 ↗
async fn drop_kills_wrapper_process_group() -> Result<()>

作用:这个测试确认:就算客户端只是被丢弃,没有显式调用 shutdown,它启动的包装进程和它带起来的子进程也会被一起清掉。

数据流:进去没有外部输入,由测试自己创建临时目录和 pid 文件 → 它启动 /bin/sh,让 shell 在后台运行一个长时间 sleep,并把这个 sleep 的 pid 写入文件;随后确认 sleep 确实还活着 → 然后 drop 掉 client,也就是让客户端对象结束生命 → 最后等待那个 sleep 进程退出;如果它没退出,测试失败。

调用关系:这是针对“对象被释放时自动清理”的场景。它用 RmcpClient::new_stdio_client 启动本地命令,用 LocalStdioServerLauncher 执行启动动作,用 wait_for_pid_file 拿到孙进程 pid,再用 wait_for_process_exit 验证进程组是否被成功杀掉。

调用图:调用 4 个内部函数(new_stdio_client, new, wait_for_pid_file, wait_for_process_exit);外部调用 7 个(new, from, from, assert!, current_dir, tempdir, vec!)。

shutdown_kills_initialized_stdio_server_with_in_flight_operation119–180 ↗
async fn shutdown_kills_initialized_stdio_server_with_in_flight_operation() -> Result<()>

作用:这个测试确认:客户端已经初始化、服务器正在处理一个很久才结束的请求时,调用 shutdown 仍然能杀掉服务器,并让挂起的调用结束收尾。

数据流:进去没有外部输入,由测试创建临时 pid 文件 → 它找到测试服务器程序,启动 stdio 客户端,把 pid 文件路径通过环境变量交给服务器;然后用 init_params 完成初始化,并提供一个简单的 elicitation 回答函数,elicitation 可以理解成服务器向客户端追问信息时的答复机制 → 接着读到服务器 pid,确认服务器活着;再开一个后台任务发起会睡很久的 call_tool 调用 → 等调用进入运行状态后执行 client.shutdown → 最后等待服务器进程退出,并确认后台调用任务在短时间内结束。

调用关系:这是针对“主动关闭且还有请求没完成”的场景。它先通过 stdio_server_bin 和 init_params 做启动与握手准备,再用 RmcpClient 发起长时间工具调用,最后把关闭动作交给 client.shutdown,并用 wait_for_process_exit 检查服务器进程有没有被清理干净。

调用图:调用 6 个内部函数(new_stdio_client, new, init_params, stdio_server_bin, wait_for_pid_file, wait_for_process_exit);外部调用 15 个(clone, new, new, from_millis, from_secs, from, from, new, assert!, json! (+5 more))。