Memories、rollout、状态和持久化测试
这一阶段主要是幕后“质检”和测试工具,不是用户直接看到的主流程。它像给系统做体检:先用假事件搭出现场,检查 rollout 追踪能不能把模型回复、工具调用、终端、子代理和对话历史整理清楚;再测记忆功能能不能启动、写盘、引用、清理资源;还会检查状态数据库、消息历史、线程存储、外部代理账本和会话记录文件。重点是保证断电、文件坏掉、压缩、改名、旧格式等麻烦情况里,记录不丢、不串、不乱删。
跟踪 reducer 夹具与场景
先提供共享 reducer 测试夹具,然后针对代码单元、对话、推理,以及工具专用的 agent 和终端行为进行重点重放与归约测试。
rollout-trace/src/reducer/test_support.rs源码 ↗
reducer 可以理解成一个“整理员”:它把一串原始 trace 事件整理成更好读、更有结构的结果。测试 reducer 时,经常要先搭一堆固定背景,比如创建临时 trace 文件、声明某个线程已经开始、某个对话回合已经开始、某次模型调用已经发出和完成。如果每个测试都手写这些铺垫,既啰嗦又容易写错。这个文件就是公共夹具,像厨房里提前备好的葱姜蒜。它提供默认线程、默认 agent 线程、上下文对象、模型请求和响应的写入方法,还提供一个专门检查“重放应该失败”的断言工具。重要的是,它只负责通用脚手架;真正决定测试含义的事件序列仍留在各个测试里,这样测试读起来不会被公共代码遮住重点。
message20–26 ↗
fn message(role: &str, text: &str) -> serde_json::Value
作用:快速造出一条测试用的聊天消息。测试里需要模拟用户或助手说了一句话时,就用它生成固定格式的 JSON。
数据流:进去的是角色名和文字内容 → 它把它们包成一个带 type、role、content 字段的 JSON 对象 → 出来的是一段可以放进模型请求里的消息数据,不会改动外部状态。
调用关系:它是很多 reducer 测试的基础小积木,常被压缩上下文、加密 reasoning、请求复用等测试调用。它只负责造消息本身,后续通常会交给写入模型请求的函数放进 trace。
调用图:被 5 处调用(compaction_boundary_repeats_prefix_and_reuses_replacement_items, context_compaction_boundary_repeats_prefix_and_reuses_replacement_items, encrypted_reasoning_reuses_response_item_in_later_request, encrypted_reasoning_upgrades_when_later_sighting_has_more_readable_body, same_encrypted_reasoning_with_different_text_reuses_first_readable_body);外部调用 1 个(json!)。
generic_summary28–34 ↗
fn generic_summary(label: &str) -> ToolCallSummary
作用:造一个最普通的工具调用摘要。测试不关心工具输入输出细节、只需要一个“这里发生过一次工具调用”的标记时会用它。
数据流:进去的是一个标签文字 → 它把标签放进 ToolCallSummary::Generic,并把输入预览和输出预览留空 → 出来的是一个测试用摘要对象,不写文件也不触发事件。
调用关系:它被终端操作、写 stdin、执行工具等 reducer 测试使用。它给测试提供统一的摘要形状,真正写入 trace 或断言结果的工作由调用它的测试完成。
调用图:被 4 处调用(code_mode_write_stdin_result_projects_structured_exec_fields, dispatch_write_stdin_payload_reduces_to_terminal_operation, exec_tool_reduces_to_terminal_operation_and_session, write_stdin_operation_reuses_existing_terminal_session)。
create_started_writer36–38 ↗
fn create_started_writer(temp: &TempDir) -> anyhow::Result<TraceWriter>
作用:创建一个已经写入“根线程已开始”事件的 TraceWriter。测试想从一个正常启动的默认线程开始时,用它省掉重复步骤。
数据流:进去的是临时目录 → 它使用默认根线程 ID 和默认 agent 路径调用 create_started_writer_for_thread → 出来的是一个可继续追加事件的 TraceWriter,同时临时目录里已经有了 trace 文件和线程开始事件。
调用关系:很多普通 reducer 测试一开始都会调用它。它本身不直接造完整场景,而是把创建 writer 和启动线程的细节交给 create_started_writer_for_thread。
调用图:调用 1 个内部函数(create_started_writer_for_thread);被 29 处调用(cancelled_turn_terminates_unfinished_code_cell, code_cell_lifecycle_links_nested_tools_waits_and_outputs, fast_code_cell_lifecycle_waits_for_source_item, runtime_code_cell_ids_can_repeat_across_threads, agent_messages_preserve_routing_and_content, compaction_boundary_repeats_prefix_and_reuses_replacement_items, context_compaction_boundary_repeats_prefix_and_reuses_replacement_items, encrypted_reasoning_reuses_response_item_in_later_request, encrypted_reasoning_upgrades_when_later_sighting_has_more_readable_body, full_request_snapshot_can_reorder_existing_items_and_insert_summary (+15 more))。
create_started_agent_writer40–42 ↗
fn create_started_agent_writer(temp: &TempDir) -> anyhow::Result<TraceWriter>
作用:创建一个已经写入“agent 根线程已开始”事件的 TraceWriter。测试子 agent、消息路由、agent 结果回传时会用这个入口。
数据流:进去的是临时目录 → 它使用固定的 agent 根线程 ID 和默认路径调用 create_started_writer_for_thread → 出来的是一个已经准备好 agent 根线程的 TraceWriter。
调用关系:它服务于 agent 相关测试,比如 spawn、send message、followup、close 等场景。它把实际创建和写 ThreadStarted 的动作交给 create_started_writer_for_thread。
调用图:调用 1 个内部函数(create_started_writer_for_thread);被 9 处调用(agent_result_edge_falls_back_to_child_thread_without_result_message, agent_result_edge_links_child_result_to_parent_notification, close_agent_runtime_payload_targets_thread, followup_activity_targets_delivered_child_message, send_message_activity_targets_delivered_child_message, send_message_runtime_payload_targets_delivered_child_message, spawn_runtime_payload_falls_back_to_child_thread_without_delivery_item, spawn_runtime_payload_targets_delivered_child_message, sub_agent_started_activity_creates_spawn_edge)。
create_started_writer_for_thread44–57 ↗
fn create_started_writer_for_thread(
temp: &TempDir,
thread_id: &str,
agent_path: &str,
) -> anyhow::Result<TraceWriter>
作用:按指定线程 ID 创建 TraceWriter,并立刻写入这个线程的开始事件。需要测试多个线程或特殊线程 ID 时,用这个更底层的版本。
数据流:进去的是临时目录、线程 ID、agent 路径 → 它在临时目录创建 trace writer,然后调用 start_thread 写入 ThreadStarted 事件 → 出来的是可继续追加事件的 writer,磁盘上已有基础 trace 文件。
调用关系:create_started_writer 和 create_started_agent_writer 都依赖它。它把“建文件”和“宣布线程开始”合在一起,让测试不会忘记最基本的开场事件。
调用图:调用 2 个内部函数(start_thread, create);被 2 处调用(create_started_agent_writer, create_started_writer);外部调用 1 个(path)。
start_thread59–70 ↗
fn start_thread(
writer: &TraceWriter,
thread_id: &str,
agent_path: &str,
) -> anyhow::Result<()>
作用:往 trace 里追加一条“某个线程开始了”的事件。测试需要手动引入新线程、子线程或 agent 线程时会调用它。
数据流:进去的是 writer、线程 ID、agent 路径 → 它把这些信息包装成 ThreadStarted 原始事件并追加到 trace → 出来是成功或失败结果,成功时 trace 多了一条线程启动记录。
调用关系:它被创建 writer 的工具调用,也被不少 agent 测试直接调用。后面的回合、模型调用等事件通常都要建立在这个线程已经存在的前提上。
调用图:调用 1 个内部函数(append);被 10 处调用(create_started_writer_for_thread, agent_result_edge_falls_back_to_child_thread_without_result_message, agent_result_edge_links_child_result_to_parent_notification, close_agent_runtime_payload_targets_thread, followup_activity_targets_delivered_child_message, send_message_activity_targets_delivered_child_message, send_message_runtime_payload_targets_delivered_child_message, spawn_runtime_payload_falls_back_to_child_thread_without_delivery_item, spawn_runtime_payload_targets_delivered_child_message, sub_agent_started_activity_creates_spawn_edge)。
start_turn72–74 ↗
fn start_turn(writer: &TraceWriter, turn_id: &str) -> anyhow::Result<()>
作用:在默认根线程上写入“一个 Codex 回合开始了”的事件。大多数普通对话测试用它来打开一轮交互。
数据流:进去的是 writer 和回合 ID → 它补上默认根线程 ID,再调用 start_turn_for_thread → 成功后 trace 里多了一条默认线程的回合开始事件。
调用关系:它是普通 reducer 测试常用的开场动作。它不自己拼事件细节,而是把真正追加事件的工作交给 start_turn_for_thread。
调用图:调用 1 个内部函数(start_turn_for_thread);被 27 处调用(cancelled_turn_terminates_unfinished_code_cell, code_cell_lifecycle_links_nested_tools_waits_and_outputs, fast_code_cell_lifecycle_waits_for_source_item, agent_messages_preserve_routing_and_content, compaction_boundary_repeats_prefix_and_reuses_replacement_items, context_compaction_boundary_repeats_prefix_and_reuses_replacement_items, encrypted_reasoning_reuses_response_item_in_later_request, encrypted_reasoning_upgrades_when_later_sighting_has_more_readable_body, full_request_snapshot_can_reorder_existing_items_and_insert_summary, incremental_request_carries_prior_request_and_response_items_forward (+15 more))。
start_agent_turn76–78 ↗
fn start_agent_turn(writer: &TraceWriter, turn_id: &str) -> anyhow::Result<()>
作用:在默认 agent 根线程上写入“一个 Codex 回合开始了”的事件。agent 相关测试需要模拟 agent 正在处理一轮任务时会用它。
数据流:进去的是 writer 和回合 ID → 它补上固定 agent 根线程 ID,再调用 start_turn_for_thread → 成功后 trace 里多了一条 agent 线程的回合开始事件。
调用关系:它常出现在 agent 消息投递、子 agent 生命周期、结果回传等测试里。它是 start_turn_for_thread 的一个更方便的 agent 版包装。
调用图:调用 1 个内部函数(start_turn_for_thread);被 9 处调用(agent_result_edge_falls_back_to_child_thread_without_result_message, agent_result_edge_links_child_result_to_parent_notification, close_agent_runtime_payload_targets_thread, followup_activity_targets_delivered_child_message, send_message_activity_targets_delivered_child_message, send_message_runtime_payload_targets_delivered_child_message, spawn_runtime_payload_falls_back_to_child_thread_without_delivery_item, spawn_runtime_payload_targets_delivered_child_message, sub_agent_started_activity_creates_spawn_edge)。
start_turn_for_thread80–90 ↗
fn start_turn_for_thread(
writer: &TraceWriter,
thread_id: &str,
turn_id: &str,
) -> anyhow::Result<()>
作用:给指定线程追加一条“回合开始”事件。测试需要控制事件属于哪个线程时,会直接用这个函数。
数据流:进去的是 writer、线程 ID、回合 ID → 它把它们组成 CodexTurnStarted 原始事件并追加到 trace → 出来是成功或失败结果,成功时该线程下面有了一个可挂载后续事件的回合。
调用关系:start_turn 和 start_agent_turn 都调用它。许多多线程测试也会直接用它,以确保后续模型调用、工具调用或上下文都能挂到正确线程和回合上。
调用图:调用 1 个内部函数(append);被 10 处调用(runtime_code_cell_ids_can_repeat_across_threads, start_agent_turn, start_turn, agent_result_edge_falls_back_to_child_thread_without_result_message, agent_result_edge_links_child_result_to_parent_notification, followup_activity_targets_delivered_child_message, send_message_activity_targets_delivered_child_message, send_message_runtime_payload_targets_delivered_child_message, spawn_runtime_payload_targets_delivered_child_message, sub_agent_started_activity_creates_spawn_edge)。
trace_context92–94 ↗
fn trace_context(turn_id: &str) -> RawTraceEventContext
作用:生成默认根线程下某个回合的事件上下文。上下文可以理解成事件的“地址标签”,告诉 reducer 这条事件属于哪条线程、哪一轮对话。
数据流:进去的是回合 ID → 它补上默认根线程 ID,并调用 trace_context_for_thread → 出来的是一个 RawTraceEventContext,里面带有线程 ID 和回合 ID。
调用关系:很多测试在追加带上下文的事件时会用它。它本身只是默认线程的快捷入口,真正创建上下文对象的工作由 trace_context_for_thread 完成。
调用图:调用 1 个内部函数(trace_context_for_thread);被 10 处调用(cancelled_turn_terminates_unfinished_code_cell, code_cell_lifecycle_links_nested_tools_waits_and_outputs, fast_code_cell_lifecycle_waits_for_source_item, compaction_boundary_repeats_prefix_and_reuses_replacement_items, context_compaction_boundary_repeats_prefix_and_reuses_replacement_items, tool_call_links_model_call_and_followup_output_items, code_mode_write_stdin_result_projects_structured_exec_fields, dispatch_write_stdin_payload_reduces_to_terminal_operation, exec_tool_reduces_to_terminal_operation_and_session, write_stdin_operation_reuses_existing_terminal_session)。
trace_context_for_agent96–98 ↗
fn trace_context_for_agent(turn_id: &str) -> RawTraceEventContext
作用:生成默认 agent 根线程下某个回合的事件上下文。agent 测试用它标明某条事件属于 agent 的那轮处理。
数据流:进去的是回合 ID → 它补上 agent 根线程 ID,并调用 trace_context_for_thread → 出来的是带 agent 线程和回合信息的上下文对象。
调用关系:它被 spawn agent、send message、followup、close 等 agent 生命周期测试使用。它和 trace_context 一样是快捷包装,只是默认线程换成了 agent 根线程。
调用图:调用 1 个内部函数(trace_context_for_thread);被 6 处调用(append_spawn_agent_tool_lifecycle, close_agent_runtime_payload_targets_thread, followup_activity_targets_delivered_child_message, send_message_activity_targets_delivered_child_message, send_message_runtime_payload_targets_delivered_child_message, sub_agent_started_activity_creates_spawn_edge)。
trace_context_for_thread100–105 ↗
fn trace_context_for_thread(thread_id: &str, turn_id: &str) -> RawTraceEventContext
作用:为任意线程和任意回合生成上下文标签。测试需要精确指定事件归属时用它。
数据流:进去的是线程 ID 和回合 ID → 它把两者放进 RawTraceEventContext 的可选字段里 → 出来的是上下文对象,不写文件、不追加事件。
调用关系:它是 trace_context、trace_context_for_agent 以及一些多线程测试的共同底层工具。后续 append_with_context 之类的写入动作会拿这个对象给事件贴上归属标签。
调用图:被 6 处调用(runtime_code_cell_ids_can_repeat_across_threads, append_completed_inference, trace_context, trace_context_for_agent, agent_result_edge_falls_back_to_child_thread_without_result_message, agent_result_edge_links_child_result_to_parent_notification)。
append_inference_start107–120 ↗
fn append_inference_start(
writer: &TraceWriter,
inference_call_id: &str,
codex_turn_id: &str,
request_payload: RawPayloadRef,
) -> anyhow::Result<()>
作用:在默认根线程上追加一条“模型调用开始了”的事件。测试需要模拟一次模型请求发出时,会用这个快捷函数。
数据流:进去的是 writer、模型调用 ID、回合 ID、请求 payload 引用 → 它补上默认根线程 ID,并交给 append_inference_start_for_thread → 成功后 trace 多了一条 InferenceStarted 事件。
调用关系:大量模型请求相关 reducer 测试会调用它,比如请求增量、响应复用、未知回合报错等。它是默认线程版入口,实际写事件由 append_inference_start_for_thread 完成。
调用图:调用 1 个内部函数(append_inference_start_for_thread);被 21 处调用(agent_messages_preserve_routing_and_content, compaction_boundary_repeats_prefix_and_reuses_replacement_items, context_compaction_boundary_repeats_prefix_and_reuses_replacement_items, encrypted_reasoning_reuses_response_item_in_later_request, encrypted_reasoning_upgrades_when_later_sighting_has_more_readable_body, full_request_snapshot_can_reorder_existing_items_and_insert_summary, incremental_request_carries_prior_request_and_response_items_forward, inference_start_rejects_unknown_codex_turn, later_full_request_reuses_prior_json_tool_call_by_position, missing_request_input_is_reducer_error (+11 more))。
append_inference_start_for_thread122–138 ↗
fn append_inference_start_for_thread(
writer: &TraceWriter,
thread_id: &str,
codex_turn_id: &str,
inference_call_id: &str,
request_payload: RawPayloadRef,
) -> anyhow::Result<()>
作用:给指定线程追加一条“模型调用开始了”的事件。它会写入测试固定的模型名和服务商名,让测试不用关心这些无关细节。
数据流:进去的是 writer、线程 ID、回合 ID、模型调用 ID、请求 payload 引用 → 它组装 InferenceStarted 事件,填入 gpt-test 和 test-provider → 成功后 trace 里多了一条模型请求开始记录。
调用关系:append_inference_start 和 append_inference_request 都把最终写开始事件的工作交给它。它是模型调用生命周期里“开始”这一步的核心测试工具。
调用图:调用 1 个内部函数(append);被 2 处调用(append_inference_request, append_inference_start)。
append_inference_completion140–153 ↗
fn append_inference_completion(
writer: &TraceWriter,
inference_call_id: &str,
response_id: &str,
response_payload: RawPayloadRef,
) -> anyhow::Result<()>
作用:追加一条“模型调用完成了”的事件。测试已经有响应 payload,并想让 reducer 看见模型返回结果时会用它。
数据流:进去的是 writer、模型调用 ID、响应 ID、响应 payload 引用 → 它组装 InferenceCompleted 事件并追加到 trace → 成功后 trace 多了一条模型完成记录,响应 ID 会被保存,上游请求 ID 留空。
调用关系:它常跟 append_inference_start 搭配使用:先说模型请求开始,再说模型响应完成。调用它的测试通常关注响应里的输出项如何进入对话、如何连接工具调用或 reasoning 内容。
调用图:调用 1 个内部函数(append);被 7 处调用(encrypted_reasoning_reuses_response_item_in_later_request, incremental_request_carries_prior_request_and_response_items_forward, later_full_request_reuses_prior_json_tool_call_by_position, reasoning_body_preserves_text_summary_and_encoded_content, response_outputs_enter_thread_conversation_on_completion, same_encrypted_reasoning_with_different_text_reuses_first_readable_body, tool_call_links_model_call_and_followup_output_items)。
append_inference_request155–165 ↗
fn append_inference_request(
writer: &TraceWriter,
thread_id: &str,
turn_id: &str,
inference_id: &str,
input: Vec<serde_json::Value>,
) -> anyhow::Result<()>
作用:写入一份模型请求 payload,并追加对应的“模型调用开始”事件。测试只想给一组输入消息,不想手动先写 payload 再写开始事件时会用它。
数据流:进去的是 writer、线程 ID、回合 ID、模型调用 ID、输入 JSON 列表 → 它先把 { input: ... } 写成 InferenceRequest payload,再调用 append_inference_start_for_thread 引用这份 payload → 出来是成功或失败结果,成功时磁盘上多了请求 payload,trace 多了开始事件。
调用关系:它是更高级的模型请求夹具,被 append_completed_inference 和多个 agent 消息测试调用。它把“保存请求正文”和“登记模型调用开始”这两步连起来。
调用图:调用 2 个内部函数(append_inference_start_for_thread, write_json_payload);被 8 处调用(append_completed_inference, agent_result_edge_falls_back_to_child_thread_without_result_message, agent_result_edge_links_child_result_to_parent_notification, followup_activity_targets_delivered_child_message, send_message_activity_targets_delivered_child_message, send_message_runtime_payload_targets_delivered_child_message, spawn_runtime_payload_targets_delivered_child_message, sub_agent_started_activity_creates_spawn_edge);外部调用 1 个(json!)。
append_completed_inference167–193 ↗
fn append_completed_inference(
writer: &TraceWriter,
thread_id: &str,
turn_id: &str,
inference_id: &str,
input: Vec<serde_json::Value>,
output_items: Vec<serde_json::Value>,
)
作用:一次性模拟完整的模型调用:先发请求,再收到响应。测试需要一个已经跑完的模型调用时,用它最省事。
数据流:进去的是 writer、线程 ID、回合 ID、模型调用 ID、输入列表、输出项列表 → 它先调用 append_inference_request 写请求和开始事件,再写 InferenceResponse payload,最后带着线程/回合上下文追加 InferenceCompleted 事件 → 成功后 trace 里出现一整套完成的模型调用记录。
调用关系:它把 append_inference_request、trace_context_for_thread 和带上下文追加事件串成一条小流水线。agent 结果回传等测试用它快速准备一个完整可被 reducer 消化的模型响应。
调用图:调用 4 个内部函数(append_inference_request, trace_context_for_thread, append_with_context, write_json_payload);被 1 处调用(agent_result_edge_links_child_result_to_parent_notification);外部调用 2 个(format!, json!)。
expect_replay_error195–202 ↗
fn expect_replay_error(temp: &TempDir, expected: &str) -> anyhow::Result<()>
作用:断言重放这个 trace 时一定会报错,而且错误信息里必须包含指定文字。它用来测试 reducer 对坏数据的防御能力。
数据流:进去的是临时目录和期望出现的错误片段 → 它调用 replay_bundle 尝试重放目录里的 trace;如果没有报错就直接让测试失败,如果报错则检查错误文字是否包含期望内容 → 出来是成功的测试结果,或触发断言失败。
调用关系:它被未知回合、缺少请求输入、重复调用 ID、未知响应 ID、不支持模型项等错误场景测试调用。它位于测试最后一步,用来确认前面故意造出的坏 trace 确实被 reducer 拒绝。
调用图:被 5 处调用(inference_start_rejects_unknown_codex_turn, missing_request_input_is_reducer_error, model_visible_call_id_reuse_with_different_content_is_reducer_error, unknown_previous_response_id_is_reducer_error, unsupported_model_item_is_reducer_error);外部调用 4 个(path, assert!, replay_bundle, panic!)。
rollout-trace/src/reducer/code_cell_tests.rs源码 ↗
这里测试的是 reducer,也就是“日志整理器”:它把一条条原始事件,像拼拼图一样还原成一次对话里的代码执行记录。文件先用临时目录造出一份假的追踪日志,再写入模型请求、模型回复、代码单元开始/结束、嵌套工具调用、等待工具调用、轮次取消等事件,最后用 replay_bundle 重新读取并整理这些事件。测试重点不是代码真的跑了什么,而是确认关系有没有接对:代码单元要连到它来自的模型工具调用;代码里再调用的工具要挂在它下面;模型后续收到的代码输出也要标明是谁产生的;如果一轮被取消,没结束的代码单元要被标成终止。还有一个容易出错的点:运行时里的 cell id 可以在不同线程重复,所以不能只靠这个数字判断是不是同一个代码单元。
code_cell_lifecycle_links_nested_tools_waits_and_outputs23–190 ↗
fn code_cell_lifecycle_links_nested_tools_waits_and_outputs() -> anyhow::Result<()>
作用:这个测试模拟一个完整的代码单元生命周期,确认它能正确连上来源、内部工具调用、等待工具调用和输出结果。有人会用它来防止“代码执行记录看起来跑了,但和对话内容、工具调用对不上”的问题。
数据流:进去的是一串人工写入的假事件:用户请求、模型发起 exec 自定义工具调用、代码单元开始、模型回复稍后才到、代码单元先返回 yielded、代码内部调用一个工具、第二轮模型用 wait 等待这个 cell,并且代码单元结束。测试把这些事件写进临时日志,再调用 replay_bundle 把日志整理成 rollout。出来的是一组断言:确认代码单元在线程 thread-root 下,最终完成,运行时 id 是 1,嵌套工具、等待工具和输出项都被挂到正确位置,输出项也标明由这个代码单元产生。
调用关系:它先借助 create_started_writer 创建测试日志写入器,用 start_turn 开始轮次,用 trace_context 给事件加上轮次上下文,用 json! 构造请求和响应内容。事件写完后交给 replay_bundle 做真正的回放整理。最后用 test_reduced_code_cell_id 算出测试里预期的代码单元 id,再用 assert_eq! 检查整理结果。
调用图:调用 4 个内部函数(test_reduced_code_cell_id, create_started_writer, start_turn, trace_context);外部调用 4 个(new, assert_eq!, replay_bundle, json!)。
fast_code_cell_lifecycle_waits_for_source_item193–269 ↗
fn fast_code_cell_lifecycle_waits_for_source_item() -> anyhow::Result<()>
作用:这个测试检查一种乱序情况:代码单元已经开始、失败、结束了,但模型回复里那个“发起它的工具调用”稍后才出现。它保证整理器会耐心等来源信息补齐,而不是提前造出一条断开的代码记录。
数据流:进去的是一段临时追踪日志:先有模型请求,然后立刻出现 CodeCellStarted、CodeCellInitialResponse 失败、CodeCellEnded 失败,最后才写入包含 custom_tool_call 的模型完成事件。测试回放这些事件后,读取整理出的代码单元。出来的结果应该是:代码单元属于 thread-root,运行状态和执行状态都是失败,运行时 cell id 是 1,而且 source_item_id 指向的对话项确实是那个自定义工具调用。
调用关系:它和完整生命周期测试一样,使用 create_started_writer、start_turn、trace_context 和 json! 造日志,用 replay_bundle 触发 reducer 的整理流程。test_reduced_code_cell_id 只负责把模型可见的 call id 变成测试期望的代码单元 id,assert_eq! 用来确认 reducer 没被事件顺序骗到。
调用图:调用 4 个内部函数(test_reduced_code_cell_id, create_started_writer, start_turn, trace_context);外部调用 4 个(new, assert_eq!, replay_bundle, json!)。
cancelled_turn_terminates_unfinished_code_cell272–334 ↗
fn cancelled_turn_terminates_unfinished_code_cell() -> anyhow::Result<()>
作用:这个测试确认:如果一轮对话被取消,而里面的代码单元还没正常结束,系统会把这个代码单元标成“被终止”。这能避免界面或报告里出现一个永远悬着、看不出结果的执行记录。
数据流:进去的是一组事件:模型请求和回复先正常发生,回复里要求执行一段较慢的代码;随后代码单元开始;接着整轮 Codex turn 被标记为 Cancelled,也就是取消。测试保存取消事件的序号,再回放整个日志。出来的结果应该是:对应代码单元的运行状态变成 Terminated,执行状态是 Cancelled,并且结束序号正好等于那条取消事件的序号。
调用关系:这个函数通过 create_started_writer 和 start_turn 搭好测试场景,用 json! 写模型请求和响应,用 trace_context 把代码单元和取消事件放进同一个轮次。replay_bundle 负责把“轮次取消”传播到未完成的代码单元上。test_reduced_code_cell_id 帮它定位被检查的代码单元。
调用图:调用 4 个内部函数(test_reduced_code_cell_id, create_started_writer, start_turn, trace_context);外部调用 4 个(new, assert_eq!, replay_bundle, json!)。
runtime_code_cell_ids_can_repeat_across_threads337–423 ↗
fn runtime_code_cell_ids_can_repeat_across_threads() -> anyhow::Result<()>
作用:这个测试检查多线程场景下的一个坑:不同线程里的运行时 cell id 可以都叫“1”,但它们不能被整理器误认为同一个代码单元。它保证系统按线程和模型调用关系区分代码执行,而不是只看一个容易重复的数字。
数据流:进去的是两个线程的假日志:根线程和子线程各自开始一轮,各自发起一次模型请求,各自启动一个 runtime_cell_id 都是 1 的代码单元,然后各自收到模型回复并结束代码单元。测试把两套事件写入同一份日志,再回放。出来的结果应该是两个不同的 code_cell:一个属于 thread-root,一个属于 thread-child;它们的运行时 id 都可以是 1,但不会互相覆盖。
调用关系:它先写入 ThreadStarted 造出子线程,再用 start_turn_for_thread 分别启动两个线程里的轮次。循环中用 trace_context_for_thread 给每个事件贴上正确线程和轮次,用 format! 生成不同响应 id,用 replay_bundle 检查 reducer 是否分清两条线。最后仍通过 test_reduced_code_cell_id 找到各自的代码单元。
调用图:调用 4 个内部函数(test_reduced_code_cell_id, create_started_writer, start_turn_for_thread, trace_context_for_thread);外部调用 5 个(new, assert_eq!, replay_bundle, format!, json!)。
test_reduced_code_cell_id425–427 ↗
fn test_reduced_code_cell_id(model_visible_call_id: &str) -> String
作用:这是一个测试用的小帮手,用模型里能看到的 call id 拼出 reducer 预期生成的代码单元 id。它让多个测试不用重复写同一段字符串拼接规则。
数据流:进去的是 model_visible_call_id,也就是模型回复里那个可见的工具调用编号。函数用 format! 在前面加上固定前缀 code_cell:。出来的是一个字符串,例如输入 call-code 会得到 code_cell:call-code;它不改动外部状态。
调用关系:它被本文件里的四个测试调用,用来定位 replay_bundle 整理出来的 code_cells 条目。它本身不参与回放,只是把测试里的期望 id 写得更统一,避免每个测试手工拼字符串时出错。
调用图:被 4 处调用(cancelled_turn_terminates_unfinished_code_cell, code_cell_lifecycle_links_nested_tools_waits_and_outputs, fast_code_cell_lifecycle_waits_for_source_item, runtime_code_cell_ids_can_repeat_across_threads);外部调用 1 个(format!)。
rollout-trace/src/reducer/conversation_tests.rs源码 ↗
这份文件不负责正式运行产品,而是像质检员一样,反复搭一个临时的追踪记录现场,写入用户请求、模型回复、工具调用、历史压缩等事件,然后调用 replay_bundle 重新“回放”这些记录。reducer(还原器,就是把零散流水账整理成结构化结果的代码)必须把相同的旧消息复用、把新消息单独建档、把模型输出放回线程历史里,还要在数据矛盾或缺字段时明确报错。可以把它想成整理聊天记录的档案员:旧档案要认得出来,新档案不能误合并,工具执行结果要挂到对应工具上,压缩后的摘要也要有清楚来源。
request_snapshots_reuse_history_without_deduping_new_identical_items27–68 ↗
fn request_snapshots_reuse_history_without_deduping_new_identical_items() -> anyhow::Result<()>
作用:测试完整请求快照里,老消息应该复用原来的编号,但新出现的、内容一样的消息不能被误认为同一条。这样可以避免把用户重复说的同一句话合并丢掉。
数据流:输入是一段临时写出的两轮推理记录:第一轮只有一条用户“ok”,第二轮包含旧的“ok”、助手“ack”和新的用户“ok”。测试回放这些记录后,检查第二轮第一条复用了第一轮编号,而第二轮最后那条虽然文字一样,却拿到了新编号;同时线程历史变成第二轮完整列表。
调用关系:它先用 create_started_writer 建临时记录器,用 start_turn 开两轮,再用 append_inference_start 登记两次模型请求,最后把活交给 replay_bundle 还原,并用断言确认还原器的去重边界没有越界。
调用图:调用 3 个内部函数(append_inference_start, create_started_writer, start_turn);外部调用 5 个(new, assert_eq!, assert_ne!, replay_bundle, json!)。
response_outputs_enter_thread_conversation_on_completion71–111 ↗
fn response_outputs_enter_thread_conversation_on_completion() -> anyhow::Result<()>
作用:测试模型回复完成后,回复里的输出消息会真正进入线程的会话历史。否则系统只记得请求,不记得模型说了什么。
数据流:输入是一轮用户请求和一条模型响应,响应里有助手文本“tests passed”。回放后,测试取出这次推理的请求项和响应项,把它们拼起来,检查线程里的会话项正好等于这个顺序。
调用关系:它通过 append_inference_start 写入请求,通过 append_inference_completion 写入完成事件,然后让 replay_bundle 整理结果;这个测试验证完成事件是把模型输出接到会话主线上的关键时刻。
调用图:调用 4 个内部函数(append_inference_completion, append_inference_start, create_started_writer, start_turn);外部调用 4 个(new, assert_eq!, replay_bundle, json!)。
agent_messages_preserve_routing_and_content114–195 ↗
fn agent_messages_preserve_routing_and_content() -> anyhow::Result<()>
作用:测试 agent_message 这种“代理之间互相发消息”的内容,作者、收件人和正文都不能丢。它保证多代理场景下,消息不会只剩一段文字而失去来龙去脉。
数据流:输入是两条 agent_message:一条普通文本,一条加密内容。回放后,测试逐条取出会话项,检查角色是助手、频道是分析频道,并且 author、recipient、正文类型和内容都和原始 JSON 对得上。
调用关系:它借助 create_started_writer 和 append_inference_start 造出一轮请求,再交给 replay_bundle;断言部分重点检查 reducer 是否把原始代理路由信息转换成 AgentMessageMetadata 和 ConversationPart。
调用图:调用 3 个内部函数(append_inference_start, create_started_writer, start_turn);外部调用 4 个(new, assert_eq!, replay_bundle, json!)。
later_full_request_reuses_prior_json_tool_call_by_position198–256 ↗
fn later_full_request_reuses_prior_json_tool_call_by_position() -> anyhow::Result<()>
作用:测试后一轮完整请求里再次出现同一个工具调用时,应该复用前一轮模型回复里创建的那条工具调用项。这样工具调用不会在历史里被复制成两份。
数据流:输入是第一轮用户请求、模型返回一个 function_call,然后第二轮完整请求又包含用户消息和同一个 function_call。回放后,测试确认第二轮请求的两项分别复用了第一轮用户项和第一轮响应里的工具调用项,总会话项数量仍然只有两条。
调用关系:它先写请求和完成事件,再开始下一轮写完整快照;replay_bundle 负责识别“位置和内容对应的旧工具调用”,测试用断言确认这个复用规则生效。
调用图:调用 4 个内部函数(append_inference_completion, append_inference_start, create_started_writer, start_turn);外部调用 4 个(new, assert_eq!, replay_bundle, json!)。
incremental_request_carries_prior_request_and_response_items_forward259–335 ↗
fn incremental_request_carries_prior_request_and_response_items_forward() -> anyhow::Result<()>
作用:测试增量请求,也就是只提交“上次响应之后新增内容”的请求,会自动带上之前的请求和响应历史。否则下一轮模型看到的上下文会断掉。
数据流:输入是第一轮用户请求、模型工具调用响应和 token 用量,第二轮请求用 previous_response_id 指向上次响应,只追加工具输出。回放后,测试确认第二轮请求项包含旧用户消息、旧工具调用和新工具输出,并确认线程历史同步成这三项;同时检查第一轮用量被记录。
调用关系:它用 append_inference_completion 建立一个可追溯的 response_id,再在下一轮请求中引用它;replay_bundle 必须根据这个引用把旧历史补齐,测试就是守住这条增量请求链。
调用图:调用 4 个内部函数(append_inference_completion, append_inference_start, create_started_writer, start_turn);外部调用 4 个(new, assert_eq!, replay_bundle, json!)。
full_request_snapshot_can_reorder_existing_items_and_insert_summary338–378 ↗
fn full_request_snapshot_can_reorder_existing_items_and_insert_summary() -> anyhow::Result<()>
作用:测试完整请求快照可以重新排列已有消息,也可以插入一条新的摘要消息。这样历史被压缩或重排时,系统既能认出旧消息,也能接纳新摘要。
数据流:输入是第一轮 developer 消息加 user 消息,第二轮把 user 放前面,中间插入摘要,再把 developer 放后面。回放后,测试确认第二轮首尾复用了第一轮旧项,中间摘要是新项,总共三条会话项。
调用关系:它只写两次推理开始事件,不需要模型完成;replay_bundle 在处理第二个完整快照时负责匹配旧项和创建新项,断言检查这种重排没有破坏身份识别。
调用图:调用 3 个内部函数(append_inference_start, create_started_writer, start_turn);外部调用 5 个(new, assert_eq!, assert_ne!, replay_bundle, json!)。
reasoning_body_preserves_text_summary_and_encoded_content381–428 ↗
fn reasoning_body_preserves_text_summary_and_encoded_content() -> anyhow::Result<()>
作用:测试 reasoning(模型推理内容)里可读文本、摘要和加密内容都会被保留下来。这样既能展示能看的部分,也不会丢掉只能加密保存的部分。
数据流:输入是一轮请求和一条 reasoning 响应,里面有 raw reasoning、brief summary 和 encrypted_content。回放后,测试找到响应生成的会话项,检查正文 parts 依次包含文本、摘要和带标签的加密内容。
调用关系:它通过 append_inference_completion 写入模型响应,再由 replay_bundle 把 reasoning JSON 转成 ConversationPart;测试确保转换不是只取其中一部分。
调用图:调用 4 个内部函数(append_inference_completion, append_inference_start, create_started_writer, start_turn);外部调用 4 个(new, assert_eq!, replay_bundle, json!)。
encrypted_reasoning_reuses_response_item_in_later_request431–528 ↗
fn encrypted_reasoning_reuses_response_item_in_later_request() -> anyhow::Result<()>
作用:测试后续请求里只带加密身份的 reasoning,能复用之前模型响应中那条更完整的 reasoning 项。这样同一段加密推理不会被重复建档。
数据流:输入是第一轮用户请求,模型返回可读 reasoning 和工具调用;第二轮请求包含同一用户消息、只剩加密内容的 reasoning、同一工具调用和工具输出。回放后,测试确认第二轮前三项复用第一轮已有项,工具输出是新项,并且原 reasoning 保留可读文本和加密内容。
调用关系:它用 message 辅助创建普通消息,用 append_inference_completion 建立旧响应,再让 replay_bundle 根据 encrypted_content 认出同一段推理;这个测试连接了加密推理复用和工具输出追加两条规则。
调用图:调用 5 个内部函数(append_inference_completion, append_inference_start, create_started_writer, message, start_turn);外部调用 4 个(new, assert_eq!, replay_bundle, json!)。
encrypted_reasoning_upgrades_when_later_sighting_has_more_readable_body531–595 ↗
fn encrypted_reasoning_upgrades_when_later_sighting_has_more_readable_body() -> anyhow::Result<()>
作用:测试同一段加密 reasoning 后来出现了互补的可读信息时,系统会把缺的部分补进去,而不是当成冲突。比如第一次只有正文,第二次只有摘要,最后应该两者都有。
数据流:输入是两轮请求,它们都包含同一个 encrypted_content,但第一轮带文本,第二轮带摘要。回放后,测试确认第二轮复用了第一轮 reasoning 的编号,并且这条会话项的正文合并成文本、摘要、加密内容三部分。
调用关系:它连续写两次推理开始事件,replay_bundle 在第二次看到同一加密身份时尝试升级已有会话项;断言检查这种升级只补充信息,没有创建重复项。
调用图:调用 4 个内部函数(append_inference_start, create_started_writer, message, start_turn);外部调用 4 个(new, assert_eq!, replay_bundle, json!)。
same_encrypted_reasoning_with_different_text_reuses_first_readable_body598–672 ↗
fn same_encrypted_reasoning_with_different_text_reuses_first_readable_body() -> anyhow::Result<()>
作用:测试同一段加密 reasoning 如果后来带了不同的可读文本,系统仍复用同一项,但不会用新文本覆盖旧文本。这样可以避免不可靠的后见内容改写历史。
数据流:输入是第一轮模型响应中同一 encrypted_content 搭配“first text”,第二轮请求里同一 encrypted_content 搭配“different text”。回放后,测试确认第二轮复用了第一轮 reasoning 项,并且正文仍保留 first text 和加密内容,总项数不增加。
调用关系:它先用 append_inference_completion 记录第一份可读证据,再在后续请求制造冲突证据;replay_bundle 的职责是认出同一加密身份,同时拒绝危险覆盖,测试验证这一点。
调用图:调用 5 个内部函数(append_inference_completion, append_inference_start, create_started_writer, message, start_turn);外部调用 4 个(new, assert_eq!, replay_bundle, json!)。
model_visible_call_id_reuse_with_different_content_is_reducer_error675–711 ↗
fn model_visible_call_id_reuse_with_different_content_is_reducer_error() -> anyhow::Result<()>
作用:测试同一个模型可见的 call_id 如果对应了不同工具调用内容,必须报错。call_id 就像工具调用的票号,同一张票不能指向两件不同的事。
数据流:输入是两轮请求,都使用 call-1,但第一轮参数是 cargo test,第二轮参数变成 cargo check。测试不期待正常结果,而是要求回放失败,并包含“call id 被不同内容复用”的错误文字。
调用关系:它用 append_inference_start 写入两条相互矛盾的请求,然后把检查交给 expect_replay_error;这个测试保证 reducer 遇到身份冲突时停下来,而不是悄悄选一个。
调用图:调用 4 个内部函数(append_inference_start, create_started_writer, expect_replay_error, start_turn);外部调用 2 个(new, json!)。
unsupported_model_item_is_reducer_error714–736 ↗
fn unsupported_model_item_is_reducer_error() -> anyhow::Result<()>
作用:测试遇到未知的模型项类型时,reducer 必须报错,不能默默跳过。否则新格式数据来了以后,系统可能看似成功,实际丢了重要内容。
数据流:输入是一轮请求,里面有 type 为 new_unhandled_model_item 的未知项。测试回放时预期失败,并检查错误信息指出这个类型不受支持。
调用关系:它构造最小坏输入,通过 append_inference_start 写入,再用 expect_replay_error 验证;这是 reducer 面对新协议字段时的安全网。
调用图:调用 4 个内部函数(append_inference_start, create_started_writer, expect_replay_error, start_turn);外部调用 2 个(new, json!)。
missing_request_input_is_reducer_error739–753 ↗
fn missing_request_input_is_reducer_error() -> anyhow::Result<()>
作用:测试推理请求如果没有 input 字段,必须被判定为坏数据。没有 input,就不知道模型到底看到了什么上下文。
数据流:输入是一轮只有 model、没有 input 的请求。测试把它写入记录后,要求回放失败,并检查错误里说明请求不包含 input。
调用关系:它通过 create_started_writer 和 start_turn 搭好正常外壳,再故意写入缺字段请求;expect_replay_error 负责确认 reducer 没有把缺失输入当成空输入处理。
调用图:调用 4 个内部函数(append_inference_start, create_started_writer, expect_replay_error, start_turn);外部调用 2 个(new, json!)。
unknown_previous_response_id_is_reducer_error756–771 ↗
fn unknown_previous_response_id_is_reducer_error() -> anyhow::Result<()>
作用:测试增量请求引用一个不存在的 previous_response_id 时必须报错。因为系统无法知道这次请求应该接在哪段历史后面。
数据流:输入是一轮请求,声明 previous_response_id 为 resp-missing,同时带一条用户消息。回放时测试期望失败,并检查错误信息说这个 previous_response_id 未知。
调用关系:它只写一个带错误引用的推理开始事件;replay_bundle 在解析增量链时找不到前置响应,expect_replay_error 验证这个断链会被明确报告。
调用图:调用 4 个内部函数(append_inference_start, create_started_writer, expect_replay_error, start_turn);外部调用 2 个(new, json!)。
compaction_boundary_repeats_prefix_and_reuses_replacement_items774–874 ↗
fn compaction_boundary_repeats_prefix_and_reuses_replacement_items() -> anyhow::Result<()>
作用:测试普通 compaction(历史压缩)安装后,后续请求里重复出现的压缩替代历史会复用压缩生成的项目,并带上正确来源。压缩就像把旧聊天折叠成摘要,但档案关系不能乱。
数据流:输入是第一轮 developer 和 user 历史,随后写入一个压缩检查点:旧输入历史被 replacement_history 替换,里面有 user、摘要消息和加密压缩摘要。第二轮请求包含 developer 加这些替代项。回放后,测试检查压缩记录的输入项、替代项、标记项、来源 ProducerRef、摘要频道和加密正文都正确,并确认替代项不是误复用旧项。
调用关系:它除了写推理开始事件,还用 trace_context 和 append_with_context 写入 CompactionInstalled;replay_bundle 必须在压缩边界建立新身份和来源标记,测试覆盖了压缩事件与后续请求匹配的配合。
调用图:调用 5 个内部函数(append_inference_start, create_started_writer, message, start_turn, trace_context);外部调用 5 个(new, assert_eq!, assert_ne!, replay_bundle, json!)。
context_compaction_boundary_repeats_prefix_and_reuses_replacement_items877–943 ↗
fn context_compaction_boundary_repeats_prefix_and_reuses_replacement_items() -> anyhow::Result<()>
作用:测试 context_compaction 这种上下文压缩项,也会被当成摘要频道里的消息保存,并保留加密内容。它和普通 compaction 类型不同,但结果规则类似。
数据流:输入是一段旧历史、一个压缩检查点,以及 replacement_history 中的 context_compaction 加密摘要。回放后,测试找到压缩替代项的第三项,确认它在 Summary 频道、类型是 Message,正文是 encrypted_content。
调用关系:它和普通压缩测试走同一条事件路线:写请求、安装压缩、再写压缩后的请求;replay_bundle 负责识别 context_compaction 类型,测试专门确认这个变体没有漏处理。
调用图:调用 5 个内部函数(append_inference_start, create_started_writer, message, start_turn, trace_context);外部调用 4 个(new, assert_eq!, replay_bundle, json!)。
tool_call_links_model_call_and_followup_output_items946–1040 ↗
fn tool_call_links_model_call_and_followup_output_items() -> anyhow::Result<()>
作用:测试模型发起的工具调用、实际工具执行记录、以及后续发回模型的工具输出,会被连成同一条链。这样读者能看懂“模型叫了哪个工具,工具结果又回到了哪里”。
数据流:输入是第一轮用户请求,模型响应一个 function_call;随后记录 ToolCallStarted 和 ToolCallEnded;第二轮请求引用 previous_response_id,并带 function_call_output。回放后,测试确认第一轮推理知道自己启动了 tool-1,工具调用记录指向模型可见的调用项和输出项,输出项来源标成这个工具。
调用关系:它把 append_inference_completion 产生的模型调用、RawTraceEventPayload 的工具生命周期事件、以及下一轮 append_inference_start 的工具输出串起来;replay_bundle 是把三段记录对齐的地方,断言检查每个链接都接上了。
调用图:调用 5 个内部函数(append_inference_completion, append_inference_start, create_started_writer, start_turn, trace_context);外部调用 4 个(new, assert_eq!, replay_bundle, json!)。
inference_start_rejects_unknown_codex_turn1043–1056 ↗
fn inference_start_rejects_unknown_codex_turn() -> anyhow::Result<()>
作用:测试推理开始事件如果引用了不存在的 turn,必须报错。turn 可以理解为一轮工作编号,引用错了就没法把请求放到正确时间线上。
数据流:输入是一个临时记录器和一条推理请求,但 append_inference_start 使用 turn-missing,而没有先创建这个 turn。测试要求回放失败,并检查错误说明引用了未知的 codex turn。
调用关系:它故意省略 start_turn,只写一个悬空的推理开始事件;expect_replay_error 验证 reducer 在建立推理和轮次关系时会检查这个前提。
调用图:调用 3 个内部函数(append_inference_start, create_started_writer, expect_replay_error);外部调用 2 个(new, json!)。
rollout-trace/src/reducer/inference_tests.rs源码 ↗
这个文件不是正式运行代码,而是一组自动测试。它会临时造一个假的追踪日志目录,像真实系统一样写入“回合开始”“推理开始”“推理取消”“回合结束”等事件,然后调用 replay_bundle 把这些原始事件重新整理成一份可读的 rollout(可以理解成“还原后的完整执行记录”)。重点检查三件事:第一,推理被取消时,如果已经有半截回复,这半截回复也要变成对话项保存下来;第二,如果整个回合被取消,正在跑的推理也要跟着关闭;第三,如果回合已经用失败状态结束,之后才收到推理取消通知,系统不能把原来的失败状态错误覆盖成取消。它像是在给事故现场复盘流程做演练,确保各种打断、迟到消息、半成品输出都能被正确归档。
cancelled_inference_reduces_partial_response_items16–70 ↗
fn cancelled_inference_reduces_partial_response_items() -> anyhow::Result<()>
作用:这个测试确认:一次推理被取消时,如果取消事件里带着已经生成的部分回复,系统会把这段半成品回复保存成正常的对话内容。这样用户或调试者不会因为取消而完全看不到模型已经说了什么。
数据流:进去的是一个临时目录和一串模拟事件:先创建追踪写入器,开始一个回合,写入用户请求,再记录推理开始。随后它写入一份“partial”的半截模型回复,并追加一个推理取消事件。接着 replay_bundle 读取这些日志,把它们还原成 rollout。最后测试检查:推理状态变成 Cancelled,外部请求编号被保存,回复项数量是 1,而且这个回复项被标记为由这次推理产生。
调用关系:它由 Rust 的测试框架在跑测试时自动调用。它把造测试数据的活交给 create_started_writer、start_turn 和 append_inference_start,把“重放日志并还原结果”的核心工作交给 replay_bundle,最后用 assert_eq! 对还原结果逐项核对。
调用图:调用 3 个内部函数(append_inference_start, create_started_writer, start_turn);外部调用 4 个(new, assert_eq!, replay_bundle, json!)。
cancelled_turn_closes_running_inference_call73–97 ↗
fn cancelled_turn_closes_running_inference_call() -> anyhow::Result<()>
作用:这个测试确认:如果一个回合被取消,而里面还有正在运行的推理调用,系统会把这个推理也标记为取消并记录结束位置。否则日志里会留下一个“永远没结束”的推理调用。
数据流:它先创建临时日志环境,写入一个回合开始事件,再写入一个用户请求和推理开始事件。然后它直接写入回合结束事件,状态是 Cancelled。replay_bundle 读取这些事件后,测试取出 inference-1 这次推理,检查它的执行状态是否也变成 Cancelled,并确认它的 ended_seq 指向回合结束那条事件的序号。结果是:回合结束会把未结束的推理一起关掉。
调用关系:它也是测试框架自动执行的用例。前半段依赖 create_started_writer、start_turn、append_inference_start 搭出“推理正在运行”的场景;后半段依赖 replay_bundle 触发 reducer 的还原逻辑;assert_eq! 用来证明还原后的推理确实被回合取消事件收尾。
调用图:调用 3 个内部函数(append_inference_start, create_started_writer, start_turn);外部调用 4 个(new, assert_eq!, replay_bundle, json!)。
late_cancelled_inference_preserves_turn_end_status100–158 ↗
fn late_cancelled_inference_preserves_turn_end_status() -> anyhow::Result<()>
作用:这个测试确认一个很细但很重要的边界情况:如果回合已经以 Failed(失败)结束,之后才收到推理取消事件,系统不能把失败状态改成取消。它还要保存这条迟到取消事件带来的半截回复和请求编号。
数据流:它先建临时日志,写入回合开始、请求和推理开始。接着写入回合结束事件,状态是 Failed,并记住这条结束事件的序号。之后它再写入一份“late partial”的半截回复,并追加一个迟到的 InferenceCancelled 事件。replay_bundle 还原后,测试检查推理状态仍然是 Failed,结束序号仍然是原来的回合结束事件;同时确认半截回复的原始载荷编号、外部请求编号和生成出的对话文本都被保存下来。
调用关系:这个用例模拟“消息来晚了”的真实问题。它用 create_started_writer、start_turn、append_inference_start 准备日志,用 replay_bundle 让 reducer 按顺序重放这些事件。它验证 reducer 的优先级规则:已经由回合结束确定的失败状态不能被后来的取消事件覆盖,但取消事件附带的有用信息仍然要被吸收。
调用图:调用 3 个内部函数(append_inference_start, create_started_writer, start_turn);外部调用 4 个(new, assert_eq!, replay_bundle, json!)。
rollout-trace/src/reducer/tool/agents_tests.rs源码 ↗
这份测试文件不参与正式运行,而是像验收清单一样,专门检查 reducer(把原始日志整理成可读追踪结果的组件)能不能正确理解代理之间的互动。它会先用 TraceWriter 在临时目录里伪造一段运行日志,比如父代理调用 spawn_agent 创建子代理、send_message 给子代理发消息、followup_task 追加任务、close_agent 关闭子代理,或者子代理把结果通知回父代理。然后调用 replay_bundle 重新播放这些日志,检查最终生成的 thread(线程/代理会话)、conversation item(对话条目)和 interaction edge(互动边,也就是“谁把什么交给谁”的连线)是否符合预期。一个重要点是:如果能找到真正承载消息的对话条目,边就指向那个精确条目;如果找不到,就退回指向整个线程,宁可粗一点,也不乱指。
child_thread_metadata_creates_spawn_origin_without_delivery_edge29–84 ↗
fn child_thread_metadata_creates_spawn_origin_without_delivery_edge() -> anyhow::Result<()>
作用:测试“子线程启动时带的元数据”能不能让系统认出它是被哪个父线程创建的。它还确认:光有出生证明还不够,不能提前画出实际消息送达的连线。
数据流:进去的是一段临时写出的日志:里面有子线程的启动事件和会话元数据,说明父线程是谁、任务名是什么、代理角色是什么。测试把这些日志 replay 成整理后的结果,然后检查子线程的昵称、默认模型和来源都被读出来了;同时检查互动边里还没有 spawn 送达边。结果是:子代理身份成立,但消息投递关系不会被凭空创建。
调用关系:这个测试直接创建 TraceWriter 和临时目录,写入元数据后交给 replay_bundle。它不依赖本文件的辅助函数,重点验证 reducer 在最早的 ThreadStarted 阶段只记录来源,不误判完整交互。
调用图:调用 1 个内部函数(create);外部调用 5 个(new, assert!, assert_eq!, replay_bundle, json!)。
spawn_runtime_payload_targets_delivered_child_message87–147 ↗
fn spawn_runtime_payload_targets_delivered_child_message() -> anyhow::Result<()>
作用:测试父代理创建子代理后,如果子代理的时间线里真的出现了那条任务消息,spawn 这条连线要准确指向那条对话消息。
数据流:进去的是父代理一次 spawn_agent 工具调用的完整生命周期,加上子线程启动、子线程回合启动、子线程收到的任务消息。测试重放后检查生成的互动边:来源是父代理的工具调用,目标是子线程里的具体对话条目,并且边上带着工具调用过程中产生的原始 payload 编号。最后确认这条消息确实属于子线程。
调用关系:它先用 create_started_agent_writer 和 start_agent_turn 搭好父代理场景,再调用 append_spawn_agent_tool_lifecycle 写入 spawn 工具过程,接着用 inter_agent_message 构造子代理收到的消息,用 append_inference_request 放进子线程,最后用 target_conversation_item_id 取出边指向的对话条目做检查。
调用图:调用 8 个内部函数(append_inference_request, create_started_agent_writer, start_agent_turn, start_thread, start_turn_for_thread, append_spawn_agent_tool_lifecycle, inter_agent_message, target_conversation_item_id);外部调用 4 个(new, assert_eq!, replay_bundle, vec!)。
spawn_runtime_payload_falls_back_to_child_thread_without_delivery_item150–195 ↗
fn spawn_runtime_payload_falls_back_to_child_thread_without_delivery_item() -> anyhow::Result<()>
作用:测试创建子代理时,如果找不到子代理收到任务的那条对话消息,系统要退而求其次,把连线指向子线程本身。
数据流:进去的是父代理 spawn_agent 的工具调用日志,以及一个刚启动但没有收到任务消息的子线程。重放后,测试检查互动边仍然存在,来源仍是工具调用,但目标变成整个子线程;同时没有声称携带任何对话条目,只保留原始工具 payload 作为证据。
调用关系:它复用 append_spawn_agent_tool_lifecycle 生成父侧工具过程,但故意不调用 append_inference_request 写入子侧消息。这样专门覆盖 reducer 的兜底路径:没有精确消息时,不乱造目标。
调用图:调用 4 个内部函数(create_started_agent_writer, start_agent_turn, start_thread, append_spawn_agent_tool_lifecycle);外部调用 4 个(new, assert!, assert_eq!, replay_bundle)。
sub_agent_started_activity_creates_spawn_edge198–286 ↗
fn sub_agent_started_activity_creates_spawn_edge() -> anyhow::Result<()>
作用:测试另一种 spawn 记录格式:工具运行结束时只报告“子代理已启动”的活动事件,也应该能生成创建子代理的连线。
数据流:进去的是一个 spawn_agent 工具开始事件、一个带有 child_thread_id 和 agent_path 的运行活动 payload,以及子线程中实际收到的新任务消息。重放后,测试检查 spawn 边存在,目标指向子线程中的任务对话条目,并且边上携带工具调用 payload 和活动 payload 的原始编号。
调用关系:它手写工具开始和运行结束事件,用 trace_context_for_agent 把事件挂到父代理回合上;再启动子线程并写入子侧消息。最后通过 target_conversation_item_id 检查 reducer 是否把活动事件和子侧消息正确配对。
调用图:调用 7 个内部函数(append_inference_request, create_started_agent_writer, start_agent_turn, start_thread, start_turn_for_thread, trace_context_for_agent, target_conversation_item_id);外部调用 6 个(new, assert_eq!, replay_bundle, format!, json!, vec!)。
send_message_runtime_payload_targets_delivered_child_message289–392 ↗
fn send_message_runtime_payload_targets_delivered_child_message() -> anyhow::Result<()>
作用:测试 send_message 这种“给已有子代理发消息”的工具调用,能不能连到子代理真正收到的那条消息。
数据流:进去的是 send_message 工具开始、运行开始、运行结束的日志,里面写着发送方线程、接收方线程和消息内容;随后子线程里出现同样内容的对话消息。重放后,测试检查生成的边类型是 SendMessage,来源是工具调用,目标是子线程的具体对话条目,并且结束时间被记录。
调用关系:它用 inter_agent_message 生成模拟的代理间消息,用 append_inference_request 放进接收方线程。replay_bundle 负责整理日志,target_conversation_item_id 帮它确认边没有只指到线程,而是指到了精确消息。
调用图:调用 8 个内部函数(append_inference_request, create_started_agent_writer, start_agent_turn, start_thread, start_turn_for_thread, trace_context_for_agent, inter_agent_message, target_conversation_item_id);外部调用 6 个(new, assert!, assert_eq!, replay_bundle, json!, vec!)。
send_message_activity_targets_delivered_child_message395–478 ↗
fn send_message_activity_targets_delivered_child_message() -> anyhow::Result<()>
作用:测试新版活动格式里的 send_message 记录,也能准确找到接收方看到的消息。
数据流:进去的是 send_message 工具调用 payload 和一个 kind 为 interacted 的运行活动 payload,活动里标出子线程和子代理路径;之后子线程收到内容为“hello again”的消息。重放后,测试确认边类型是 SendMessage,目标是子线程里的那条消息,并且边携带了调用 payload 和活动 payload。
调用关系:它和 send_message_runtime_payload_targets_delivered_child_message 测的是同一类业务,但输入日志格式不同。它通过 trace_context_for_agent 写父侧事件,通过 inter_agent_message 和 append_inference_request 写子侧送达消息,再让 replay_bundle 验证配对。
调用图:调用 8 个内部函数(append_inference_request, create_started_agent_writer, start_agent_turn, start_thread, start_turn_for_thread, trace_context_for_agent, inter_agent_message, target_conversation_item_id);外部调用 5 个(new, assert_eq!, replay_bundle, json!, vec!)。
followup_activity_targets_delivered_child_message481–564 ↗
fn followup_activity_targets_delivered_child_message() -> anyhow::Result<()>
作用:测试 followup_task,也就是给子代理追加任务时,系统能把这次派活连到子代理收到的任务消息。
数据流:进去的是一个 AssignAgentTask 类型的工具调用、一个 interacted 活动 payload,以及子线程里带 trigger_turn=true 的代理间消息。重放后,测试检查互动边类型是 AssignAgentTask,目标是子线程中实际承载“continue”的对话条目,原始 payload 也被挂在边上。
调用关系:它的流程和 send_message_activity_targets_delivered_child_message 很像,只是工具种类换成追加任务。它用相同的测试辅助函数搭场景,确保 reducer 不把 followup_task 错当成普通消息或 spawn。
调用图:调用 8 个内部函数(append_inference_request, create_started_agent_writer, start_agent_turn, start_thread, start_turn_for_thread, trace_context_for_agent, inter_agent_message, target_conversation_item_id);外部调用 5 个(new, assert_eq!, replay_bundle, json!, vec!)。
close_agent_runtime_payload_targets_thread567–687 ↗
fn close_agent_runtime_payload_targets_thread() -> anyhow::Result<()>
作用:测试 close_agent 关闭子代理时,连线应该指向被关闭的线程,而不是某条对话消息。
数据流:进去的是一个已存在的子线程、父代理的一次 close_agent 工具调用、运行开始和结束 payload、工具结果 payload,以及子线程结束事件。重放后,测试检查 CloseAgent 边的来源是工具调用,目标是子线程本身,不携带对话条目,但携带所有相关原始 payload;同时确认子线程状态变成 Completed,而整个 rollout 仍保持 Running。
调用关系:它先启动子线程,再在父代理回合中写入 close_agent 的工具生命周期。replay_bundle 把关闭动作整理成边,并更新线程执行状态;测试用断言确认关闭一个子代理不会错误地把整个运行都标成结束。
调用图:调用 4 个内部函数(create_started_agent_writer, start_agent_turn, start_thread, trace_context_for_agent);外部调用 5 个(new, assert!, assert_eq!, replay_bundle, json!)。
agent_result_edge_links_child_result_to_parent_notification690–774 ↗
fn agent_result_edge_links_child_result_to_parent_notification() -> anyhow::Result<()>
作用:测试子代理完成任务并通知父代理时,系统能把“子代理的结果消息”和“父代理收到的通知消息”连起来。
数据流:进去的是子线程里一次完成的推理:输入是任务,输出是“done”;随后写入 AgentResultObserved 事件,表示父代理会收到结果通知;再把这条通知放进父线程的对话输入。重放后,测试检查 AgentResult 边的来源是子线程里的结果对话条目,目标是父线程收到通知的对话条目,并且携带 AgentResult 原始 payload。
调用关系:它用 append_completed_inference 伪造子代理产出结果,用 trace_context_for_thread 把 AgentResultObserved 事件挂到子代理回合上,再用 inter_agent_message 和 append_inference_request 模拟父代理收信。text_body 用来确认源头消息正文确实是“done”。
调用图:调用 9 个内部函数(append_completed_inference, append_inference_request, create_started_agent_writer, start_agent_turn, start_thread, start_turn_for_thread, trace_context_for_thread, inter_agent_message, target_conversation_item_id);外部调用 6 个(new, assert_eq!, replay_bundle, json!, panic!, vec!)。
agent_result_edge_falls_back_to_child_thread_without_result_message777–868 ↗
fn agent_result_edge_falls_back_to_child_thread_without_result_message() -> anyhow::Result<()>
作用:测试子代理没有产出结果消息但仍通知父代理失败时,系统不能把子代理收到的任务误当成结果。
数据流:进去的是一个子线程收到任务但没有 assistant 输出的场景,接着写入失败通知的 AgentResult payload,并让父线程收到这条失败通知。重放后,测试确认 AgentResult 边的来源退回到子线程本身,目标仍然精确指向父线程收到的通知消息,原始 payload 也被保留。
调用关系:这个测试专门防止误配。它用 append_inference_request 只写子代理的入站任务,不写完成输出;再通过 trace_context_for_thread 写观察到的结果事件。replay_bundle 必须明白:没有子侧结果条目时,只能指向子线程,不能乱拿任务消息充数。
调用图:调用 8 个内部函数(append_inference_request, create_started_agent_writer, start_agent_turn, start_thread, start_turn_for_thread, trace_context_for_thread, inter_agent_message, target_conversation_item_id);外部调用 5 个(new, assert_eq!, replay_bundle, json!, vec!)。
append_spawn_agent_tool_lifecycle877–966 ↗
fn append_spawn_agent_tool_lifecycle(
writer: &TraceWriter,
turn_id: &str,
) -> anyhow::Result<SpawnAgentToolPayloads>
作用:这是测试用的辅助函数,用来一次性写出父代理调用 spawn_agent 的完整工具过程。这样多个 spawn 测试不用重复写同一大段准备代码。
数据流:进去的是 TraceWriter 和父代理回合 id。函数依次写入工具调用 payload、工具运行开始 payload、工具运行结束 payload、工具结果 payload,并把这些事件都挂到指定回合上下文里。出来的是 SpawnAgentToolPayloads,里面保存四个 payload 的引用,方便测试后来检查互动边是否携带了这些原始证据。
调用关系:它被 spawn_runtime_payload_targets_delivered_child_message 和 spawn_runtime_payload_falls_back_to_child_thread_without_delivery_item 调用。它自己把细节工作交给 writer.write_json_payload、writer.append_with_context 和 trace_context_for_agent,让测试主体只关注子线程那边有没有收到消息。
调用图:调用 3 个内部函数(trace_context_for_agent, append_with_context, write_json_payload);被 2 处调用(spawn_runtime_payload_falls_back_to_child_thread_without_delivery_item, spawn_runtime_payload_targets_delivered_child_message);外部调用 1 个(json!)。
inter_agent_message968–977 ↗
fn inter_agent_message(author: &str, recipient: &str, content: &str, trigger_turn: bool) -> String
作用:这是测试用的小工具,用来生成一段“代理发给另一个代理”的 JSON 字符串消息。
数据流:进去的是发送者路径、接收者路径、消息内容,以及这条消息是否触发接收方新回合的布尔值。函数把它们包成固定格式的 JSON,并转成字符串。出来的是可直接塞进测试推理请求里的消息文本。
调用关系:很多测试都会用它来模拟代理间通信,包括 spawn、send_message、followup 和 agent result 场景。它只负责造消息外形,真正把消息写入日志的是 append_inference_request。
调用图:被 6 处调用(agent_result_edge_falls_back_to_child_thread_without_result_message, agent_result_edge_links_child_result_to_parent_notification, followup_activity_targets_delivered_child_message, send_message_activity_targets_delivered_child_message, send_message_runtime_payload_targets_delivered_child_message, spawn_runtime_payload_targets_delivered_child_message);外部调用 1 个(json!)。
target_conversation_item_id979–984 ↗
fn target_conversation_item_id(anchor: &TraceAnchor) -> &String
作用:这是测试用的检查助手,用来从一条边的目标里取出对话条目 id。
数据流:进去的是 TraceAnchor,也就是追踪图里某个位置的锚点。函数要求这个锚点必须是 ConversationItem;如果是,就返回其中的 item_id;如果不是,就直接让测试失败。出来的是目标对话条目的 id 引用。
调用关系:它被多个测试用在重放之后,检查边是否指向“具体消息”而不是“整个线程”。如果 reducer 退回到了线程目标,这个函数会 panic,让测试立刻暴露问题。
调用图:被 7 处调用(agent_result_edge_falls_back_to_child_thread_without_result_message, agent_result_edge_links_child_result_to_parent_notification, followup_activity_targets_delivered_child_message, send_message_activity_targets_delivered_child_message, send_message_runtime_payload_targets_delivered_child_message, spawn_runtime_payload_targets_delivered_child_message, sub_agent_started_activity_creates_spawn_edge);外部调用 1 个(panic!)。
text_body986–991 ↗
fn text_body(item: &crate::model::ConversationItem) -> &str
作用:这是测试用的检查助手,用来读取一个对话条目里唯一的纯文本内容。
数据流:进去的是一个 ConversationItem。函数要求它的正文只包含一个 Text 文本片段;如果符合,就返回文本;如果结构不是这样,就让测试失败。出来的是这条对话消息的文字内容。
调用关系:它在 agent_result_edge_links_child_result_to_parent_notification 中使用,用来确认 AgentResult 边的源头确实连到了子代理输出的“done”,而不是连到了别的消息。
调用图:外部调用 1 个(panic!)。
rollout-trace/src/reducer/tool/terminal_tests.rs源码 ↗
系统会记录很多很碎的原始事件,比如模型请求执行命令、工具开始运行、工具结束、命令输出返回等。这个测试文件就像验收清单:它先在临时目录里造出一段假的追踪记录,再调用 replay_bundle 把这些记录“重放”成整理后的结果,最后逐项对比结果是不是预期的样子。这里重点测的是终端相关内容:执行命令会生成 TerminalOperation(一次终端操作),有进程编号时还会生成 TerminalSession(一个持续存在的终端会话);往已有终端写输入时,应该接到同一个会话里,而不是另开一个;工具返回的 stdout、exit_code、chunk_id 等字段也要被放到正确位置。它不做真实命令执行,只是在模拟事件流,确认“ reducer(把零散事件压缩整理成结构化状态的部件)”没有把故事讲错。
exec_tool_reduces_to_terminal_operation_and_session27–215 ↗
fn exec_tool_reduces_to_terminal_operation_and_session() -> anyhow::Result<()>
作用:这个测试检查一次普通的 exec_command,也就是“执行终端命令”,能不能被整理成一条终端操作,并且建立对应的终端会话。它还检查工具调用、原始载荷、模型看到的调用和输出之间有没有正确连起来。
数据流:进去的是测试自己写入的一串假事件:先有模型请求执行 cargo test,再有工具启动、运行开始、运行结束、返回结果,最后还有下一轮模型收到工具输出。函数把这些事件写进临时追踪目录,然后用 replay_bundle 重新读出来并整理。出来的是 rollout 这个整理后的结果;测试会确认里面的 tool_call、terminal_operation、terminal_session 都带着正确的编号、时间、命令、输出和状态。
调用关系:它是这个文件里最完整的一条端到端测试。它会借助 create_started_writer 和 start_turn 搭好追踪环境,用 append_inference_with_tool_call 写入“模型要求调用工具”的前半段,用 append_followup_with_tool_output 写入“模型收到工具输出”的后半段,再通过 replay_bundle 检查 reducer 的最终产物。
调用图:调用 6 个内部函数(create_started_writer, generic_summary, start_turn, trace_context, append_followup_with_tool_output, append_inference_with_tool_call);外部调用 4 个(new, assert_eq!, replay_bundle, json!)。
write_stdin_operation_reuses_existing_terminal_session218–318 ↗
fn write_stdin_operation_reuses_existing_terminal_session() -> anyhow::Result<()>
作用:这个测试检查“往终端里继续输入文字”时,系统会不会复用已经存在的终端会话。简单说,先开了一个 bash,后面输入 echo hi,应该算同一个终端窗口里的第二步,而不是新开一个窗口。
数据流:进去的是两段模拟事件:第一段启动一个带 process_id 为 pty-1 的终端进程,第二段对同一个 pty-1 写入 stdin,也就是标准输入。函数把事件写入后重放。出来的结果应该是同一个 terminal_session 里有两个 operation_id;第二个操作的类型是 WriteStdin,内容是 echo hi,并且状态仍是 Running,因为测试里没有写入结束事件。
调用关系:它直接搭建原始工具事件,不依赖后面的辅助函数。它用 create_started_writer、start_turn 和 trace_context 准备上下文,用 generic_summary 填测试摘要,最后交给 replay_bundle 让真正的整理逻辑工作,再用断言确认“复用会话”这个关键行为。
调用图:调用 4 个内部函数(create_started_writer, generic_summary, start_turn, trace_context);外部调用 4 个(new, assert_eq!, replay_bundle, json!)。
dispatch_write_stdin_payload_reduces_to_terminal_operation321–439 ↗
fn dispatch_write_stdin_payload_reduces_to_terminal_operation() -> anyhow::Result<()>
作用:这个测试检查另一种 write_stdin 的来源:当写入终端的请求直接藏在工具调用参数里时,系统能不能从参数里读出会话号、输入内容和限制选项。它保证即使没有运行时开始事件,也能整理出一条终端操作。
数据流:进去的是一个 ToolInvocation 载荷,里面写着 session_id、chars、yield_time_ms 和 max_output_tokens,后面再跟一个工具结束事件,带着输出 hi。函数重放这些记录后,期望得到一条 WriteStdin 类型的 terminal_operation;它的 terminal_id 来自 session_id,stdin 来自 chars,结果 stdout 来自工具返回内容,同时 raw_payload_ids 记录请求和响应两个原始载荷。
调用关系:它测试的是 reducer 对“工具调用参数”的解析能力,而不是运行时事件解析。流程上,它先用 writer 写入请求载荷和结束载荷,再调用 replay_bundle,让整理器把这些零散 JSON 转成 TerminalOperation 和 TerminalSession。
调用图:调用 4 个内部函数(create_started_writer, generic_summary, start_turn, trace_context);外部调用 4 个(new, assert_eq!, replay_bundle, json!)。
code_mode_write_stdin_result_projects_structured_exec_fields442–521 ↗
fn code_mode_write_stdin_result_projects_structured_exec_fields() -> anyhow::Result<()>
作用:这个测试检查代码模式下的 write_stdin 返回值能不能被正确拆成结构化字段。代码模式可以理解成模型写了一小段代码去调用工具,返回的结果不是普通文本,而是带 exit_code、chunk_id 等字段的对象。
数据流:进去的是一次代码单元开始事件、一次 write_stdin 工具开始事件,以及一个 code_mode_response 类型的工具结果。结果里有 chunk_id、exit_code、original_token_count 和 output。重放之后,测试只关心 terminal_operation 的 result:它应该把 exit_code 变成退出码,把 output 变成 stdout 和 formatted_output,把 original_token_count 和 chunk_id 原样保存。
调用关系:它覆盖的是比较特殊的调用链:CodeCellStarted 表示代码单元启动,ToolCallStarted 表示这个代码单元触发了终端写入,ToolCallEnded 带回结构化结果。它把这些交给 replay_bundle,确认 reducer 没有把代码模式的字段当成普通字符串而丢掉。
调用图:调用 4 个内部函数(create_started_writer, generic_summary, start_turn, trace_context);外部调用 4 个(new, assert_eq!, replay_bundle, json!)。
append_inference_with_tool_call523–558 ↗
fn append_inference_with_tool_call(writer: &TraceWriter) -> anyhow::Result<()>
作用:这个辅助函数负责给测试追踪里加上一段“模型决定调用终端命令”的记录。它让主测试不用反复手写模型请求和模型响应这两类事件。
数据流:进去的是 TraceWriter,也就是往测试追踪文件里写事件的工具。它先写一个 InferenceRequest 载荷,表示用户说“run tests”;再追加 InferenceStarted 事件。然后写一个 InferenceResponse 载荷,里面有 function_call,表示模型要调用 exec_command 并传入 cargo test;最后追加 InferenceCompleted 事件。出来的是写入成功或失败的结果;成功时追踪文件多了完整的一次模型推理记录。
调用关系:它只被 exec_tool_reduces_to_terminal_operation_and_session 使用,用来铺垫“工具调用是模型发起的”这个背景。它内部把具体写文件工作交给 writer.write_json_payload 和 writer.append。
调用图:调用 2 个内部函数(append, write_json_payload);被 1 处调用(exec_tool_reduces_to_terminal_operation_and_session);外部调用 1 个(json!)。
append_followup_with_tool_output560–581 ↗
fn append_followup_with_tool_output(writer: &TraceWriter) -> anyhow::Result<()>
作用:这个辅助函数负责给测试追踪里加上一段“模型下一轮收到了工具输出”的记录。它用来验证终端操作不仅有执行结果,还能和模型后来看到的输出对应起来。
数据流:进去的是 TraceWriter。它写入一个新的 InferenceRequest 载荷,其中 previous_response_id 指向上一轮响应,input 里放着 function_call_output,也就是 call-1 的输出 ok。然后追加 InferenceStarted 事件,表示第二轮模型推理开始。出来的是写入结果;成功后追踪里多了一条模型收到工具输出的记录。
调用关系:它只被 exec_tool_reduces_to_terminal_operation_and_session 调用,补上完整故事的最后一段:命令执行完以后,输出又回到模型上下文里。这样 replay_bundle 才能把 TerminalModelObservation,也就是“模型看到了哪些终端信息”,连到正确的终端操作上。
调用图:调用 2 个内部函数(append, write_json_payload);被 1 处调用(exec_tool_reduces_to_terminal_operation_and_session);外部调用 1 个(json!)。
跟踪协议与线程记录
这些测试验证协议事件映射,然后检查线程作用域的跟踪 API,该 API 会发出可重放的包。
rollout-trace/src/protocol_event_tests.rs源码 ↗
这个测试关心一件很具体的事:当系统收到一个“子代理活动”事件时,比如某个子代理开始工作了,追踪模块应该把它当成一次已经结束的工具运行记录来输出。这里的“子代理”可以理解成主流程临时叫来的帮手;“轨迹事件”就是系统留下的操作流水账。测试先手工造出一个事件,里面有事件编号、发生时间、子代理线程编号、路径和活动类型。然后它调用真正的转换函数 tool_runtime_trace_event,看结果是不是 ToolRuntimeTraceEvent::Ended,也就是“终态事件”。最后它逐项检查:工具调用编号是不是原来的事件编号,状态是不是 Completed,保存下来的 JSON 内容是不是完整保留了原始事件信息。这样可以保证日志、回放或调试工具看到的记录不会丢关键信息。
sub_agent_activity_is_a_terminal_tool_runtime_event14–46 ↗
fn sub_agent_activity_is_a_terminal_tool_runtime_event() -> anyhow::Result<()>
作用:这个测试函数验证:SubAgentActivity 这种协议事件,经过转换后必须变成一个“工具运行已结束”的追踪事件,并且状态是完成。有人改动事件转换逻辑时,它能第一时间发现是否破坏了这个约定。
数据流:进去的是测试里临时构造的一条子代理活动事件:包含新建的线程编号、路径“/root/reviewer”、事件编号“call-spawn”、时间 1234 和类型 Started。函数把这条事件交给 tool_runtime_trace_event 做转换,然后检查出来的结果:必须是 Ended,tool_call_id 必须等于原事件编号,status 必须是 Completed,payload 转成 JSON 后必须和预期字段完全一致。它不改外部状态,只是在测试失败时让测试报错。
调用关系:这个函数由测试框架在跑测试时自动调用。它自己先调用 ThreadId::new 生成线程编号,用 AgentPath::try_from 检查并生成代理路径,再用 EventMsg::SubAgentActivity 包装成协议事件;核心动作是把事件交给 tool_runtime_trace_event。随后它用 assert_eq! 做结果比对,如果转换结果不是预期的终态事件,就用 panic! 明确让测试失败。
调用图:调用 2 个内部函数(try_from, new);外部调用 4 个(assert_eq!, panic!, SubAgentActivity, tool_runtime_trace_event)。
rollout-trace/src/thread_tests.rs源码 ↗
这个文件不提供线上功能,而是专门检查追踪系统是否靠谱。这里的“线程”可以理解成一次主任务或子任务,“追踪”就是把它什么时候开始、什么时候结束、发生了哪些事件写进日志,方便事后复盘。测试会先在临时目录里启动一个假的根任务,再让它结束,然后读取生成的记录,确认状态、线程编号、代理路径这些信息都能被还原。它还检查子任务不会另起一套记录,而是追加到根任务同一个包里;检查“禁用追踪”时,各种记录函数都能安全调用但不写文件;也检查协议事件里有些重要事件会被保存成原始记录。两个小工具函数负责生成最小测试元数据,以及从临时目录里找出唯一的追踪包目录。
create_in_root_writes_replayable_lifecycle_events24–56 ↗
fn create_in_root_writes_replayable_lifecycle_events() -> anyhow::Result<()>
作用:这个测试确认:从根目录启动一个根线程追踪后,开始和结束这类生命周期事件会被写进磁盘,并且之后能被重新读回来。没有这个保证,用户看到的复盘结果可能和真实运行不一致。
数据流:进去的是一个新建的临时目录、一串模拟的线程启动信息和一个新线程编号。测试用这些信息创建根线程追踪,记录它已完成,然后找到生成的追踪包并回放读取。出来的是一组断言:回放出的整体状态必须是完成,根线程编号和代理路径必须对得上,原始记录数量也必须符合预期。
调用关系:它是追踪系统最基础的验收测试之一。它先调用 ThreadTraceContext::start_root_in_root_for_test 建立测试用根追踪,再通过 single_bundle_dir 找到生成目录,最后交给 replay_bundle 复盘验证。
调用图:调用 3 个内部函数(new, start_root_in_root_for_test, single_bundle_dir);外部调用 5 个(from, new, assert_eq!, replay_bundle, format!)。
spawned_thread_start_appends_to_root_bundle59–110 ↗
fn spawned_thread_start_appends_to_root_bundle() -> anyhow::Result<()>
作用:这个测试确认:根线程里派生出来的子线程,会写进同一个根追踪包,而不是到处生成零散文件。这样复盘时才能看到主任务和子任务之间的完整关系。
数据流:进去的是临时目录、一个根线程编号、一个子线程编号,以及根线程和子线程的模拟启动信息。测试先创建根追踪,再从根追踪启动子线程追踪,记录子线程完成,然后读取唯一的追踪包。出来的是一组检查:目录里只能有一个包,回放结果里有两个线程,子线程路径正确,子线程状态是完成,而整个 rollout 仍保持运行状态。
调用关系:它承接根线程测试,进一步模拟“主任务叫来一个子助手干活”的情况。它用 minimal_metadata 准备根线程的基础信息,用 start_child_thread_trace_or_disabled 创建子线程追踪,再用 single_bundle_dir 和 replay_bundle 验证所有记录确实汇总在根包里。
调用图:调用 5 个内部函数(try_from, new, start_root_in_root_for_test, minimal_metadata, single_bundle_dir);外部调用 6 个(from, new, SubAgent, assert_eq!, replay_bundle, format!)。
disabled_thread_context_accepts_trace_calls_without_writing113–165 ↗
fn disabled_thread_context_accepts_trace_calls_without_writing() -> anyhow::Result<()>
作用:这个测试确认:当追踪被禁用时,代码仍然可以照常调用各种记录函数,但不会真的写任何文件。这样调用方不需要到处写“如果追踪开启才记录”的判断,系统也不会因为关闭追踪而崩掉。
数据流:进去的是一个空临时目录和一个禁用状态的线程追踪对象。测试对它连续调用结束记录、协议事件记录、回合事件记录、工具调用记录、子代理结果记录、推理记录、压缩记录和工具分发记录。结果是这些调用都安全通过,工具分发的构造函数没有被执行,临时目录里也没有产生任何文件。
调用关系:它验证的是追踪系统的“空操作”模式,也就是看起来能接活,但实际不落盘。它从 ThreadTraceContext::disabled 开始,随后覆盖多个追踪入口,确保上层流程无论是否开启追踪,都可以用同一套调用方式。
调用图:调用 2 个内部函数(new, disabled);外部调用 6 个(new, new, assert!, assert_eq!, Completed, json!)。
protocol_wrapper_records_selected_events_as_raw_payloads168–190 ↗
fn protocol_wrapper_records_selected_events_as_raw_payloads() -> anyhow::Result<()>
作用:这个测试确认:某些协议事件会被包装并保存成原始追踪记录。这里的“协议事件”可以理解成系统内部传递的标准消息,比如关闭完成。
数据流:进去的是临时目录、一个线程编号和最小启动信息。测试启动根追踪后记录一个 ShutdownComplete 事件,再打开生成的 trace.jsonl 文本文件逐行读取,把每行解析成原始追踪事件。出来的结果是必须能找到一个类型为 shutdown_complete 的协议事件记录。
调用关系:它专门检查协议事件到追踪日志之间的桥接是否正常。它用 minimal_metadata 生成测试配置,用 single_bundle_dir 找到追踪包,再直接读 trace.jsonl,确认 record_protocol_event 写下了应该写的内容。
调用图:调用 4 个内部函数(new, start_root_in_root_for_test, minimal_metadata, single_bundle_dir);外部调用 3 个(new, assert!, read_to_string)。
minimal_metadata192–207 ↗
fn minimal_metadata(thread_id: ThreadId) -> ThreadStartedTraceMetadata
作用:这个小工具函数生成一份够测试用的线程启动信息,避免每个测试都重复写一大段相同字段。它就像测试里的“默认表格模板”。
数据流:进去的是一个线程编号。函数把它转成字符串,并配上固定的代理路径、工作目录、模型名、服务商名、审批策略、沙箱策略等测试默认值。出来的是一个 ThreadStartedTraceMetadata 对象,供测试拿去启动线程追踪。
调用关系:它服务于需要普通根线程配置的测试,被 spawned_thread_start_appends_to_root_bundle 和 protocol_wrapper_records_selected_events_as_raw_payloads 调用。更复杂的测试会自己手写元数据,这个函数则负责省掉常见重复内容。
调用图:被 2 处调用(protocol_wrapper_records_selected_events_as_raw_payloads, spawned_thread_start_appends_to_root_bundle);外部调用 2 个(from, to_string)。
single_bundle_dir209–216 ↗
fn single_bundle_dir(root: &Path) -> anyhow::Result<PathBuf>
作用:这个小工具函数从一个测试临时目录里找出唯一的追踪包目录,并确认真的只有一个。它用来防止测试不小心生成多个包而没被发现。
数据流:进去的是一个根目录路径。函数读取这个目录下的所有条目,把它们转成路径并排序,然后断言数量必须正好是一个。出来的是这个唯一目录的路径;如果没有目录或多出目录,测试会失败。
调用关系:它是多个测试共同使用的检查工具,被根线程写入测试、子线程追加测试和协议事件写入测试调用。它把“找到刚生成的追踪包”这一步统一起来,让每个测试可以专心验证回放内容或日志内容。
调用图:被 3 处调用(create_in_root_writes_replayable_lifecycle_events, protocol_wrapper_records_selected_events_as_raw_payloads, spawned_thread_start_appends_to_root_bundle);外部调用 2 个(assert_eq!, read_dir)。
运行时与持久化支持
运行时状态辅助工具支撑外部 agent 配置导入持久化和数据库恢复行为的测试。
state/src/runtime/test_support.rs源码 ↗
这个文件像一个“测试道具箱”。真实程序运行时不会用到它,因为所有内容都被 #[cfg(test)] 包起来了,意思是只在跑测试时编译进来。它主要解决两个问题:第一,测试常常要写临时文件,如果大家用同一个位置,就可能互相污染;unique_temp_dir 会生成一个几乎不重复的临时目录名。第二,很多测试需要一份看起来像真实会话的 ThreadMetadata(线程元数据,也就是一段对话/任务的基本档案),但又不想每个测试手写几十个字段;test_thread_metadata 会填好固定时间、模型名、沙箱策略、审批模式等默认值。这样测试更短、更稳定,也更容易看出真正要测的行为。
unique_temp_dir28–36 ↗
fn unique_temp_dir() -> PathBuf
作用:生成一个专给测试用的临时目录路径。它不会真的创建目录,只是给出一个带时间和随机编号的名字,尽量保证每个测试拿到的位置都不一样。
数据流:进去不需要任何参数;它读取当前系统时间,取出从 1970 年到现在的纳秒数,再加上一个随机的 UUID(一串几乎不会重复的编号);最后把这些拼到系统临时目录下面,出来一个 PathBuf 路径。它不改动磁盘,只返回路径。
调用关系:很多运行时测试会先调用它来拿一个干净的位置,比如测试导入历史、回填状态、任务结果上报等场景。它把“给我一个不撞车的测试目录”这件小事集中起来,后续测试再拿这个路径去创建文件或数据库。
调用图:被 98 处调用(report_agent_job_item_result_completes_item_atomically, report_agent_job_item_result_rejects_late_reports, backfill_claim_is_singleton_until_stale_and_blocked_when_complete, backfill_state_persists_progress_and_completion, get_backfill_state_repairs_a_missing_singleton_row, get_backfill_state_succeeds_while_another_connection_holds_writer_slot, reads_all_history_records, records_completion_by_import_id, test_runtime, init_configures_logs_db_with_incremental_auto_vacuum (+15 more));外部调用 3 个(now, format!, temp_dir)。
test_thread_metadata39–71 ↗
fn test_thread_metadata(
codex_home: &Path,
thread_id: ThreadId,
cwd: PathBuf,
) -> ThreadMetadata
作用:快速造出一份测试用的线程元数据。线程元数据可以理解成一段会话的档案,里面有会话编号、保存位置、创建时间、模型信息、工作目录、权限设置等。
数据流:进去的是 Codex 主目录路径、线程 ID、当前工作目录;它先制造一个固定时间,避免测试因为当前时间不同而不稳定;然后把传入的线程 ID 和目录拼进各个字段里,再填入一批测试默认值,比如模型是 gpt-5、来源是 cli、沙箱是只读策略、审批模式是按需请求;出来是一整个 ThreadMetadata 对象。它不写文件,只组装数据。
调用关系:许多测试在需要插入或更新测试线程时会用它,例如测试任务领取规则、删除线程、清理记忆数据等。它内部会调用枚举转字符串的工具,把沙箱策略和审批模式变成可存储、可比较的文字,让测试数据更接近真实运行时的数据格式。
调用图:调用 1 个内部函数(enum_to_string);被 50 处调用(upsert_test_thread, claim_stage1_jobs_bounds_state_scan_before_memory_probes, claim_stage1_jobs_enforces_global_running_cap, claim_stage1_jobs_filters_by_age_idle_and_current_thread, claim_stage1_jobs_processes_two_full_batches_across_startup_passes, claim_stage1_jobs_skips_threads_with_disabled_memory_mode, clear_memory_data_clears_rows_and_preserves_thread_memory_modes, delete_thread_removes_stage1_output_and_enqueues_phase2_when_selected, get_phase2_input_selection_excludes_polluted_previous_selection, get_phase2_input_selection_excludes_stale_used_memories_but_keeps_fresh_never_used (+15 more));外部调用 5 个(from_timestamp, join, new, new_read_only_policy, format!)。
state/src/runtime/external_agent_config_imports_tests.rs源码 ↗
这个测试文件关心的是一件很实际的事:当系统从外部代理导入配置时,导入结果不能只在当下显示一下就丢了,而要能按导入编号查回详情,也能列出历史。文件里的测试会先创建一个临时的 StateRuntime(可以理解成一套临时的状态仓库),再模拟几次“导入完成”的记录写入。第一个测试重点看同一个 import_id 被记录两次时,最终详情是不是合并成最新、完整的成功和失败清单,并且历史里只有这一次导入。第二个测试则看不同 import_id 的导入是否都能被读出来。这里用 tokio::test 表示这些测试运行在异步环境里,因为被测的状态读写函数本身是异步的;用临时目录是为了让每个测试都有干净的独立存储,不互相污染。
records_completion_by_import_id6–119 ↗
async fn records_completion_by_import_id() -> anyhow::Result<()>
作用:这个测试确认:同一个导入编号的完成记录可以被正确保存和读取。它特别检查成功项、失败项、完成时间,以及按编号查详情和读历史这两条路径是否一致。
数据流:进入测试时,它先用 unique_temp_dir 造一个干净的临时目录,再用 init 建一个测试用的 StateRuntime。接着它用同一个 import_id 写入两次导入完成结果:第一次只有一个成功项,第二次有两个成功项和一个失败项。最后它读取这个 import_id 的详情和全部历史记录,用 assert_eq! 对比期望结果;如果保存、覆盖或合并规则不对,测试就会失败。
调用关系:它是测试流程里的一个独立检查点。测试开始时依赖 unique_temp_dir 准备隔离环境,再交给 init 建好运行时状态;之后它直接调用运行时的记录和读取能力,最后用 assert_eq! 做验收,确保外部配置导入记录这条链路没有断。
调用图:调用 2 个内部函数(init, unique_temp_dir);外部调用 1 个(assert_eq!)。
reads_all_history_records122–145 ↗
async fn reads_all_history_records() -> anyhow::Result<()>
作用:这个测试确认:系统不只会记住一条导入历史,而是能把多条不同导入编号的历史都读出来。它防止历史列表功能只返回最新一条或漏掉某些记录。
数据流:进入测试时,它同样先用 unique_temp_dir 准备空目录,再用 init 创建测试运行时。然后它分别写入 import-1 和 import-2 两条完成记录,读取全部历史后按导入编号排序,最后只取出 import_id 列表并和期望的两个编号比较。结果是:如果两条历史都在,测试通过;如果少了或多了,测试失败。
调用关系:它补充检查历史读取这一侧的行为。和前一个测试一样,它先通过 unique_temp_dir 和 init 搭好一次性的测试环境,然后让运行时写入两条记录,再用 assert_eq! 验证读取出来的历史集合是否完整。
调用图:调用 2 个内部函数(init, unique_temp_dir);外部调用 1 个(assert_eq!)。
state/src/runtime/recovery_tests.rs源码 ↗
这个文件不是正式功能代码,而是一组自动化测试。它关心的是运行时用的 SQLite 数据库,也就是一种保存在本地文件里的小型数据库。程序启动时如果发现数据库坏了,通常需要把坏文件搬到备份目录,再重新开始;但这里很容易出错,比如误搬别的数据库文件、备份目录被普通文件挡住、把路径里的“corrupt”误当成数据库损坏信息。这个测试文件就像灾难演练:先在临时目录里造出各种文件和坏数据库,再调用恢复相关函数,最后检查结果是不是安全、准确。它还测试错误文字的分类:哪些算“数据库损坏”,哪些只是“数据库被占用或锁住”。这些测试很重要,因为恢复逻辑一旦写错,轻则启动失败,重则可能误移动用户数据。
backup_moves_only_requested_runtime_db_files_to_backup_folder6–41 ↗
async fn backup_moves_only_requested_runtime_db_files_to_backup_folder() -> std::io::Result<()>
作用:这个测试确认:当某一个运行时数据库出问题时,程序只会备份那一个数据库对应的 SQLite 文件,不会顺手把其他运行时数据库文件也搬走。它是在防止“救一个文件,却误伤一堆文件”。
数据流:进去的是一个新建的临时 SQLite 主目录。测试先用 runtime_db_paths 找出运行时数据库路径,再给每个数据库造出 SQLite 可能产生的相关文件;接着指定 logs 数据库像是失败的那个,调用备份函数。出来的结果应该是:失败数据库的那些文件被移到备份目录,其他数据库文件还留在原地,备份记录里的路径也确实存在。
调用关系:它先借 unique_temp_dir 准备一个不会污染真实环境的临时目录,再用 runtime_db_paths 和 logs_db_path 找到要模拟的数据库文件位置,用 create_dir_all 和 write 布置现场。随后它验证备份恢复流程是否只处理目标数据库,这是恢复逻辑最核心的安全边界之一。
调用图:调用 1 个内部函数(unique_temp_dir);外部调用 7 个(new, assert!, assert_eq!, logs_db_path, runtime_db_paths, create_dir_all, write)。
backup_replaces_blocking_sqlite_home_file44–64 ↗
async fn backup_replaces_blocking_sqlite_home_file() -> std::io::Result<()>
作用:这个测试确认:如果本该是数据库目录的位置,竟然已经有一个普通文件挡在那里,恢复逻辑能把这个障碍处理掉,而不是直接崩溃。它模拟的是磁盘上目录结构被意外弄坏的情况。
数据流:进去的是一个临时目录。测试先在里面创建一个名叫 sqlite-home 的普通文件,而不是目录;然后把 state 数据库路径传给备份函数。函数跑完后,测试检查 sqlite-home 已经变成目录,原来挡路的文件被备份到了带备份名的地方,并且只产生了一条备份记录。
调用关系:它用 unique_temp_dir 建隔离环境,用 state_db_path 算出数据库应该放在哪里,再通过 create_dir_all 和 write 人为制造“目录被文件占用”的坏现场。它检验的是备份流程在启动恢复前处理文件系统异常的能力。
调用图:调用 1 个内部函数(unique_temp_dir);外部调用 5 个(assert!, assert_eq!, state_db_path, create_dir_all, write)。
sqlite_error_detail_classifies_corruption_and_lock_errors67–75 ↗
fn sqlite_error_detail_classifies_corruption_and_lock_errors()
作用:这个测试确认错误文字能被正确分类:哪些表示数据库真的坏了,哪些只是暂时被锁住或忙碌。这样程序才能决定是走“备份重建”,还是不要误判。
数据流:进去的是几段 SQLite 报错文字。测试把这些文字分别交给损坏判断和锁定判断函数;出来的是一组真假结果:例如“file is not a database”和“database disk image is malformed”应被看成损坏,而“database is locked”不算损坏但算锁定。
调用关系:它不需要准备文件,只直接喂入典型错误文本。它服务于更大的恢复判断流程:只有错误确实像数据库损坏时,后续代码才应该尝试定位失败数据库并备份。
调用图:外部调用 1 个(assert!)。
runtime_db_path_for_corruption_error_returns_failed_database_path78–92 ↗
async fn runtime_db_path_for_corruption_error_returns_failed_database_path() -> std::io::Result<()>
作用:这个测试确认:如果启动运行时数据库时真的碰到损坏文件,程序能从错误里找出到底是哪一个数据库文件坏了。这样后续恢复才知道该备份谁。
数据流:进去的是一个临时 SQLite 主目录。测试先创建 state 数据库文件,并故意写入“not sqlite”这种无效内容;然后调用 StateRuntime::init 尝试启动。启动应该失败,测试拿到这个错误,再调用路径提取函数;出来的结果应该正好是那个坏掉的 state 数据库路径。
调用关系:它用 unique_temp_dir 和 state_db_path 搭建坏数据库现场,再让 StateRuntime::init 走真实初始化路径。初始化失败后,它检查 runtime_db_path_for_corruption_error 能否从错误链里提取正确路径,这是恢复流程连接“发现坏了”和“备份哪个文件”的关键一步。
调用图:调用 2 个内部函数(init, unique_temp_dir);外部调用 5 个(assert_eq!, panic!, state_db_path, create_dir_all, write)。
runtime_db_path_for_corruption_error_ignores_corrupt_word_in_path95–105 ↗
fn runtime_db_path_for_corruption_error_ignores_corrupt_word_in_path()
作用:这个测试确认:程序不会因为文件路径里含有“corrupt”这个词,就误以为数据库损坏。它防止把普通错误,比如权限不足,错误地当成数据库损坏处理。
数据流:进去的是一个手工构造的错误:路径里有 sqlite_corrupt,但真正的错误原因是 permission denied,也就是权限被拒绝。测试把这个错误交给路径提取函数;出来的结果应该是 None,意思是“不认为这是数据库损坏,也不给出要恢复的数据库路径”。
调用关系:它用 PathBuf::from 构造带有迷惑性单词的路径,再用 RuntimeDbInitError::new 和 anyhow 错误包装出一个真实形态的初始化错误。它验证恢复判断不会只靠路径文字猜测,而是看真正的错误内容。
调用图:调用 1 个内部函数(new);外部调用 4 个(from, anyhow!, assert_eq!, new)。
记忆流水线与存储
这些文件涵盖记忆提示词渲染、引用解析、启动编排、工作区 diff、剪枝,以及磁盘存储同步。
ext/memories/src/prompts_tests.rs源码 ↗
这个测试像是在临时搭一个小型“用户家目录”,里面放一个 memories 文件夹和一份 memory_summary.md。然后它调用真正生成说明文字的函数 build_memory_tool_developer_instructions,看看生成结果是不是符合预期。它重点检查三件事:第一,说明文字里会标出这份摘要文件的路径,并提醒“已经放在下面了,不要再打开”;第二,摘要文件里的文字真的被嵌进了说明;第三,用来标记摘要开始的标题只出现一次,防止重复拼接。这里用到的 tokio::test 表示这是一个异步测试,也就是测试里可以等待文件创建、写入这类操作完成。临时目录会在测试结束后自动清理,所以不会污染真实电脑上的文件。
build_memory_tool_developer_instructions_renders_embedded_template8–35 ↗
async fn build_memory_tool_developer_instructions_renders_embedded_template()
作用:这个测试函数确认“生成记忆工具开发者说明”的功能能正确读取并嵌入 memory_summary.md。有人改模板或改文件读取逻辑时,它能及时发现摘要漏放、路径提示不对、或者重复插入的问题。
数据流:进去时没有外部输入,测试自己先创建一个临时目录,把它当成 codex_home,再在里面建 memories/memory_summary.md,并写入一句测试用摘要。接着它把这个临时目录交给 build_memory_tool_developer_instructions,拿到生成好的说明文字。最后它检查这段文字里是否包含摘要文件路径、是否包含摘要内容,并数一数“MEMORY_SUMMARY BEGINS”这个开始标记是不是只出现一次;测试本身会改动临时目录里的文件,但不会改真实项目数据。
调用关系:它在测试运行时由测试框架自动调用。它先借助 tempdir 创建临时地盘,用 from_absolute_path 把普通路径包装成项目需要的绝对路径类型,再用 create_dir_all 和 write 准备测试文件。准备好后,它把核心工作交给 build_memory_tool_developer_instructions,最后用 assert! 和 assert_eq! 判断结果是否达标。
调用图:调用 1 个内部函数(from_absolute_path);外部调用 5 个(assert!, assert_eq!, tempdir, create_dir_all, write)。
memories/read/src/citations_tests.rs源码 ↗
这份文件不参与正式运行,而是在跑测试时充当“验收清单”。项目里的记忆引用是一段带标签的文本,比如里面可能写着引用了哪个 MEMORY.md 的哪几行,也可能带着一次对话的编号。这个文件专门检查解析器能不能看懂这些文本。它测了三件重要的事:第一,老格式里的 thread_ids 还能继续支持,而且遇到乱写的编号会跳过,不会把程序弄崩;第二,新格式 rollout_ids 能被识别;第三,引用条目本身,比如文件路径、起止行、备注,也能被拆出来,同时重复的 rollout id 会被去重。可以把它理解成给“读引用的小机器”准备的几张标准答卷:机器读完后,答案必须和这里写的一模一样。
parse_memory_citation_supports_legacy_thread_ids7–21 ↗
fn parse_memory_citation_supports_legacy_thread_ids()
作用:这个测试确认旧版的 thread_ids 写法还没有被新代码抛弃。它还故意塞进一个假的编号,检查解析器会忽略坏数据,只留下真正有效的对话编号。
数据流:进去的是两个新生成的 ThreadId(对话编号)和一段模拟的记忆引用文字,文字里夹着一个 not-a-uuid 这种无效值。测试把这段文字交给 parse_memory_citation 解析,再用 thread_ids_from_memory_citation 取出解析后的编号。出来的结果必须只包含前后两个真实编号;这个测试本身不改业务数据,只是在结果不对时让测试失败。
调用关系:它在自动测试时运行,像一名检查员一样先造出测试材料,再把材料交给 parse_memory_citation 这个真正的解析函数。解析完后,它用断言比较结果,确认后续提取线程编号的流程没有被破坏。
调用图:调用 1 个内部函数(new);外部调用 3 个(assert_eq!, parse_memory_citation, vec!)。
parse_memory_citation_supports_rollout_ids24–34 ↗
fn parse_memory_citation_supports_rollout_ids()
作用:这个测试确认新版的 rollout_ids 标签能被解析器读懂。也就是说,新格式的记忆引用不会因为格式变化而丢掉对话编号。
数据流:进去的是一个新生成的 ThreadId,以及一段只包含 rollout_ids 标签的引用文字。测试调用 parse_memory_citation 把文字变成结构化结果,再提取里面的对话编号。出来的结果应该正好是这个 ThreadId;如果解析器读不到,测试就会失败。
调用关系:它覆盖的是新格式入口,和旧格式测试互相补充。它把样本文字交给 parse_memory_citation,然后用断言确认解析结果能被后续读取编号的代码正常使用。
调用图:调用 1 个内部函数(new);外部调用 3 个(assert_eq!, parse_memory_citation, vec!)。
parse_memory_citation_extracts_entries_and_rollout_ids37–71 ↗
fn parse_memory_citation_extracts_entries_and_rollout_ids()
作用:这个测试一次性检查两类信息:引用了哪些文件行号,以及关联了哪些 rollout id。它还确认重复的编号会被合并,避免同一个对话被记两遍。
数据流:进去的是两个新生成的 ThreadId,以及一段包含 citation_entries 和 rollout_ids 的文字。citation_entries 里写了两个文件路径、行号范围和备注;rollout_ids 里故意把第一个编号写了两次。测试调用 parse_memory_citation 后,分别检查解析出的条目列表和编号列表。出来的结果应该是两个准确的引用条目,以及去重后的两个编号字符串。
调用关系:它是这组测试里最完整的一项,模拟真实引用里同时有“引用位置”和“对话编号”的情况。它把复杂样本交给 parse_memory_citation,再用断言逐项核对解析器拆出来的字段是否正确。
调用图:调用 1 个内部函数(new);外部调用 3 个(assert_eq!, parse_memory_citation, vec!)。
memories/write/src/extensions/prune_tests.rs源码 ↗
这份文件不是正式运行时用的功能代码,而是一组测试。它用临时文件夹搭出一套假的 memories 目录,就像先搭一个小型沙盘,再让清理程序进去工作。测试会准备一个带 instructions.md 的扩展目录,并放入几种资源文件:很旧的、刚好到截止时间的、还算新的、文件名不是时间格式的。然后它调用清理函数,检查结果是不是只有该删的旧文件被删掉。它还特意准备了另一个没有说明文件的扩展目录,确认里面的旧资源不会被碰。第二个测试专门看资源文件名开头的时间能不能被正确读出来,因为清理旧文件靠的就是这个时间。整体上,这个文件像质检员,确保“按时间清理扩展资源”这件事既不会手软,也不会乱删。
prunes_only_old_resources_from_extensions_with_instructions7–76 ↗
async fn prunes_only_old_resources_from_extensions_with_instructions()
作用:这个测试确认清理程序只会删除有 instructions.md 的扩展里的过期资源。它也确认新文件、时间格式不对的文件,以及不该参与清理的扩展文件都不会被误删。
数据流:进去的是一个测试临时目录和一个固定的“当前时间”。测试先在临时目录里造出扩展资源文件,包括旧文件、刚好到期文件、新文件、无效文件名文件,以及一个应被忽略的扩展目录。然后它运行清理函数。出来的结果不是返回值,而是文件系统状态发生变化:旧文件和刚好到期的文件消失,其他文件仍然存在,最后用断言逐个确认。
调用关系:这个测试会在测试运行器执行异步测试时启动。它先借助临时目录、目录创建、文件写入和时间解析这些工具搭好场景,再调用被测的清理逻辑,最后用文件是否存在来判断清理逻辑是否正确。它的作用是保护扩展资源清理流程,避免以后改代码时不小心扩大删除范围。
调用图:外部调用 7 个(from_naive_utc_and_offset, parse_from_str, new, assert!, memory_extensions_root, create_dir_all, write)。
parses_timestamp_prefix_from_resource_file_name79–85 ↗
fn parses_timestamp_prefix_from_resource_file_name()
作用:这个测试确认系统能从资源文件名开头读出时间戳。因为清理旧资源时,判断文件老不老主要靠文件名里的这段时间。
数据流:进去的是两个文件名字符串:一个带合法时间前缀,一个完全不像时间。测试把合法文件名交给时间解析函数,得到一个时间值,并确认它等于预期的 Unix 时间戳;再把非法文件名交进去,确认结果是“没有解析出时间”。它不改动磁盘,只检查解析结果。
调用关系:这个测试是清理测试的基础校验。前一个测试关心“删哪些文件”,而这个测试关心“怎么从文件名判断文件时间”。它通过普通断言和相等断言确认资源文件名解析规则没有跑偏。
调用图:外部调用 2 个(assert!, assert_eq!)。
memories/write/src/startup_tests.rs源码 ↗
这份文件像一套“开机体检清单”。项目里的记忆功能会在程序启动时偷偷跑后台任务,把聊天记录提炼成长期记忆,再把多次提炼结果合并整理。这里的测试用临时目录、假的模型服务器、假的数据库状态,模拟真实启动场景:有的测试看目录会不会被创建,有的测试看第二阶段合并时会不会只处理新变化,有的测试看过期扩展资源会不会被删掉,还有的测试确认请求模型的选择规则。文件里也放了很多小帮手,比如搭建测试版 Codex、往数据库塞假历史、等待后台任务完成、读取生成文件等。这样做的重点是:记忆功能大多是异步后台运行,肉眼很难看出哪里坏了;这些测试能把“之前是什么、启动后应该变成什么”固定下来,避免以后改代码时悄悄破坏用户的长期记忆体验。
memories_startup_creates_memory_root50–62 ↗
async fn memories_startup_creates_memory_root() -> anyhow::Result<()>
作用:检查记忆启动任务会不会自动创建记忆根目录。没有这个目录,后续保存长期记忆、摘要和扩展资源都会没地方放。
数据流:输入是一个假的服务器和一个全新的临时 home 目录;它先确认 memories 目录不存在,然后启动记忆后台任务,等待目录出现;最后关闭测试用的 Codex 实例,结果是确认启动流程确实创建了目录。
调用关系:这是最基础的启动测试。它通过 build_test_codex 搭好测试环境,用 trigger_memories_startup 触发后台任务,再交给 wait_for_dir 等待文件系统变化,结束时用 shutdown_test_codex 收尾。
调用图:调用 5 个内部函数(start_mock_server, build_test_codex, shutdown_test_codex, trigger_memories_startup, wait_for_dir);外部调用 3 个(new, new, assert!)。
memories_startup_phase2_tracks_workspace_diff_across_runs65–150 ↗
async fn memories_startup_phase2_tracks_workspace_diff_across_runs() -> anyhow::Result<()>
作用:检查第二阶段记忆合并能不能识别“这次启动以来的新变化”,而不是把旧内容重复处理。它保护记忆整理结果不被旧数据污染。
数据流:输入是临时数据库、旧的阶段一输出、已有的记忆文件,以及后来新增的一条阶段一输出;它先把旧内容写进磁盘并建立干净的 git 基线,再启动记忆流程;最后检查模型提示里包含工作区差异文件,并确认最终文件只留下新记忆 B,不再包含旧记忆 A。
调用关系:这个测试把 init_state_db、seed_stage1_output、reset_git_repository、trigger_memories_startup 串起来模拟两次运行。它用 mock SSE 响应假装模型完成第二阶段,再用 phase2_prompt_text、wait_for_phase2_workspace_reset 和 read_rollout_summary_bodies 验证结果。
调用图:调用 12 个内部函数(mount_sse_once, sse, start_mock_server, build_test_codex, init_state_db, phase2_prompt_text, read_rollout_summary_bodies, seed_stage1_output, shutdown_test_codex, trigger_memories_startup (+2 more));外部调用 11 个(new, new, assert!, assert_eq!, hours, now, reset_git_repository, create_dir_all, read_to_string, write (+1 more))。
memories_startup_phase2_prunes_old_extension_resources153–220 ↗
async fn memories_startup_phase2_prunes_old_extension_resources() -> anyhow::Result<()>
作用:检查第二阶段运行时会不会清理太旧的扩展资源文件。这样可以避免记忆目录越用越膨胀,留下很多没用的旧材料。
数据流:输入是一个有阶段一输出的数据库、一个 8 天前的旧资源文件和一个 6 天前的新资源文件;它启动记忆整理后,观察模型请求和文件系统;结果是旧文件被删,新文件保留,说明清理规则生效。
调用关系:它先用 seed_stage1_output 准备可合并的记忆,再用 trigger_memories_startup 启动完整后台流程。模型响应由 mount_sse_once 提供,清理完成由 wait_for_file_removed 和 wait_for_phase2_workspace_reset 等待确认。
调用图:调用 12 个内部函数(mount_sse_once, sse, start_mock_server, build_test_codex, init_state_db, phase2_prompt_text, seed_stage1_output, shutdown_test_codex, trigger_memories_startup, wait_for_file_removed (+2 more));外部调用 9 个(new, new, assert!, hours, now, format!, create_dir_all, write, vec!)。
memories_startup_phase2_prunes_old_extension_resources_without_stage1_input223–272 ↗
async fn memories_startup_phase2_prunes_old_extension_resources_without_stage1_input() -> anyhow::Result<()>
作用:检查即使没有新的阶段一记忆输入,第二阶段也会清理过期扩展资源。也就是说,清理不是依赖“有新聊天记忆”才顺便发生。
数据流:输入是一个只排队了全局合并任务的数据库,以及一个过期扩展资源文件;它启动记忆流程,等待第二阶段请求发出并完成;输出是旧资源文件被删除,记忆工作区也恢复干净状态。
调用关系:它用 init_state_db 建库后直接 enqueue_global_consolidation 排队,不调用 seed_stage1_output。随后仍然通过 trigger_memories_startup 跑流程,用 phase2_prompt_text、wait_for_file_removed 和 wait_for_phase2_workspace_reset 做验证。
调用图:调用 11 个内部函数(mount_sse_once, sse, start_mock_server, build_test_codex, init_state_db, phase2_prompt_text, shutdown_test_codex, trigger_memories_startup, wait_for_file_removed, wait_for_phase2_workspace_reset (+1 more));外部调用 8 个(new, new, assert!, now, format!, create_dir_all, write, vec!)。
memories_startup_phase1_uses_live_thread_service_tier_and_detached_metadata275–350 ↗
async fn memories_startup_phase1_uses_live_thread_service_tier_and_detached_metadata() -> anyhow::Result<()>
作用:检查第一阶段记忆提取会使用当前线程最新的服务档位,并且发送的是“后台记忆请求”的元数据。这样后台任务不会被误认为普通用户对话的一部分。
数据流:输入是一个测试会话和后来提交的线程设置,其中服务档位改成 Fast;它等待配置快照更新,再创建记忆启动上下文并发出阶段一请求;最后检查请求里使用了 Fast 档位,并且元数据写明 request_kind 是 memory,同时不带 session_id、thread_id、turn_id 等普通会话标识。
调用关系:这个测试直接使用 MemoryStartupContext,先通过 wait_for_service_tier 等配置变化,再调用 stage_one_request_context 和 stream_stage_one_prompt。请求由 mock 服务器接住,最后用 shutdown_test_codex 结束。
调用图:调用 9 个内部函数(default, mount_sse_once, sse, start_mock_server, new, build_test_codex, shutdown_test_codex, wait_for_service_tier, wait_for_single_request);外部调用 10 个(clone, new, default, new, assert!, assert_eq!, reset_git_repository, submit_thread_settings, from_str, vec!)。
memories_startup_phase1_provider_default_drives_request_model353–366 ↗
async fn memories_startup_phase1_provider_default_drives_request_model() -> anyhow::Result<()>
作用:检查第一阶段在没有手动指定模型时,会使用模型提供方推荐的记忆提取模型。这样默认配置也能选到适合做记忆提取的模型。
数据流:输入是默认的测试记忆配置;它运行一个阶段一模型请求测试;输出是捕获到的请求体里 model 字段等于假的提供方指定的阶段一模型名。
调用关系:这是一个薄测试壳,主要把工作交给 run_memory_phase_one_model_request_test。它使用 startup_test_memories_config 生成默认配置,并用断言检查请求模型。
调用图:调用 3 个内部函数(start_mock_server, run_memory_phase_one_model_request_test, startup_test_memories_config);外部调用 3 个(new, new, assert_eq!)。
memories_startup_phase2_provider_default_drives_request_model369–382 ↗
async fn memories_startup_phase2_provider_default_drives_request_model() -> anyhow::Result<()>
作用:检查第二阶段在没有手动指定模型时,会使用模型提供方推荐的记忆合并模型。这样整理长期记忆时会默认选对模型。
数据流:输入是默认的测试记忆配置;它运行阶段二模型请求测试;输出是捕获到的请求体里 model 字段等于假的提供方指定的阶段二模型名。
调用关系:它把搭环境、塞数据、跑阶段二的细节交给 run_memory_phase_two_model_request_test,自己只负责准备配置和验证模型名。
调用图:调用 3 个内部函数(start_mock_server, run_memory_phase_two_model_request_test, startup_test_memories_config);外部调用 3 个(new, new, assert_eq!)。
memories_startup_phase1_explicit_model_override_drives_request_model385–399 ↗
async fn memories_startup_phase1_explicit_model_override_drives_request_model() -> anyhow::Result<()>
作用:检查如果配置里明确指定了第一阶段模型,程序会尊重这个指定,而不是使用提供方默认值。用户或配置文件的选择应该优先。
数据流:输入是测试记忆配置,并把 extract_model 改成 override.phase-one;它运行阶段一请求;输出是请求体中的 model 字段正好是这个手动覆盖值。
调用关系:它复用 run_memory_phase_one_model_request_test 来执行流程。和默认模型测试相比,它只改变配置中的 extract_model,用来确认优先级规则。
调用图:调用 3 个内部函数(start_mock_server, run_memory_phase_one_model_request_test, startup_test_memories_config);外部调用 3 个(new, new, assert_eq!)。
memories_startup_phase2_explicit_model_override_drives_request_model402–416 ↗
async fn memories_startup_phase2_explicit_model_override_drives_request_model() -> anyhow::Result<()>
作用:检查如果配置里明确指定了第二阶段合并模型,程序会按配置发送请求。这样高级用户可以控制记忆合并使用哪个模型。
数据流:输入是测试记忆配置,并把 consolidation_model 改成 override.phase-two;它运行阶段二请求;输出是请求体中的 model 字段等于这个覆盖值。
调用关系:它复用 run_memory_phase_two_model_request_test 完成复杂准备工作,只在测试入口处设置 consolidation_model,然后验证请求。
调用图:调用 3 个内部函数(start_mock_server, run_memory_phase_two_model_request_test, startup_test_memories_config);外部调用 3 个(new, new, assert_eq!)。
run_memory_phase_one_model_request_test418–457 ↗
async fn run_memory_phase_one_model_request_test(
server: &wiremock::MockServer,
home: Arc<TempDir>,
memories: MemoriesConfig,
) -> anyhow::Result<ResponsesRequest>
作用:搭建一个专门测试第一阶段模型选择的完整小场景。调用者用它拿到真实发给模型服务器的请求,然后检查里面的模型名。
数据流:输入是假的模型服务器、临时 home 目录和记忆配置;它创建测试 Codex,换上 MockMemoryModelProvider,往数据库塞一条可提取的候选聊天记录,挂好假的 SSE 模型响应,然后运行 phase1;输出是被 mock 服务器捕获到的那一次 ResponsesRequest,并关闭测试环境。
调用关系:它被两个第一阶段模型选择测试调用。内部会用 build_test_codex_with_memories_config 建环境,用 seed_stage1_candidate 准备待提取记录,用 memory_startup_context_with_provider 注入假模型提供方,最后由 wait_for_single_request 取回请求。
调用图:调用 8 个内部函数(mount_sse_once, sse, new, build_test_codex_with_memories_config, memory_startup_context_with_provider, seed_stage1_candidate, shutdown_test_codex, wait_for_single_request);被 2 处调用(memories_startup_phase1_explicit_model_override_drives_request_model, memories_startup_phase1_provider_default_drives_request_model);外部调用 6 个(clone, new, hours, now, run, vec!)。
run_memory_phase_two_model_request_test459–502 ↗
async fn run_memory_phase_two_model_request_test(
server: &wiremock::MockServer,
home: Arc<TempDir>,
memories: MemoriesConfig,
) -> anyhow::Result<ResponsesRequest>
作用:搭建一个专门测试第二阶段模型选择的完整小场景。调用者用它检查记忆合并请求到底发给了哪个模型。
数据流:输入是假的模型服务器、临时 home 目录和记忆配置;它创建测试 Codex,注入 MockMemoryModelProvider,往数据库塞一条已经完成阶段一的输出,准备记忆根目录和扩展说明,再运行 phase2;输出是捕获到的阶段二模型请求,并等待工作区恢复干净后关闭环境。
调用关系:它被两个第二阶段模型选择测试调用。它会调用 seed_stage1_output 准备合并输入,seed_extension_instructions 放入扩展说明,再直接调用 phase2::run 执行阶段二。
调用图:调用 11 个内部函数(mount_sse_once, sse, seed_extension_instructions, run, new, build_test_codex_with_memories_config, memory_startup_context_with_provider, seed_stage1_output, shutdown_test_codex, wait_for_phase2_workspace_reset (+1 more));被 2 处调用(memories_startup_phase2_explicit_model_override_drives_request_model, memories_startup_phase2_provider_default_drives_request_model);外部调用 5 个(new, now, memory_root, create_dir_all, vec!)。
startup_test_memories_config504–510 ↗
fn startup_test_memories_config() -> MemoriesConfig
作用:生成一份适合测试的记忆配置。它把门槛调低,让测试不用等很久、也不用准备大量数据。
数据流:输入为空;它从 MemoriesConfig 的默认值开始,改成最多 1 条原始记忆就能合并、对话空闲时间要求为 0;输出是一份测试专用配置。
调用关系:很多测试入口和 build_test_codex 都用它作为默认记忆配置。需要测试覆盖模型时,调用者会先拿到这份配置再改其中一个字段。
调用图:调用 1 个内部函数(default);被 5 处调用(build_test_codex, memories_startup_phase1_explicit_model_override_drives_request_model, memories_startup_phase1_provider_default_drives_request_model, memories_startup_phase2_explicit_model_override_drives_request_model, memories_startup_phase2_provider_default_drives_request_model)。
build_test_codex512–517 ↗
async fn build_test_codex(
server: &wiremock::MockServer,
home: Arc<TempDir>,
) -> anyhow::Result<TestCodex>
作用:用默认测试记忆配置创建一个测试版 Codex。它让测试不用每次重复写同样的搭建代码。
数据流:输入是 mock 服务器和临时 home 目录;它调用 startup_test_memories_config 得到配置,再转交 build_test_codex_with_memories_config;输出是一个可用于测试的 TestCodex。
调用关系:多个启动流程测试都会调用它。它是默认版本的环境搭建入口,真正细节由 build_test_codex_with_memories_config 完成。
调用图:调用 2 个内部函数(build_test_codex_with_memories_config, startup_test_memories_config);被 5 处调用(memories_startup_creates_memory_root, memories_startup_phase1_uses_live_thread_service_tier_and_detached_metadata, memories_startup_phase2_prunes_old_extension_resources, memories_startup_phase2_prunes_old_extension_resources_without_stage1_input, memories_startup_phase2_tracks_workspace_diff_across_runs)。
build_test_codex_with_memories_config519–535 ↗
async fn build_test_codex_with_memories_config(
server: &wiremock::MockServer,
home: Arc<TempDir>,
memories: MemoriesConfig,
) -> anyhow::Result<TestCodex>
作用:按指定记忆配置创建测试版 Codex。它允许不同测试改动记忆功能的参数,比如模型覆盖值。
数据流:输入是 mock 服务器、临时 home 目录和 MemoriesConfig;它启用 SQLite 特性,把记忆配置写进测试配置,然后构建 TestCodex;输出是完整测试运行环境。
调用关系:build_test_codex、run_memory_phase_one_model_request_test 和 run_memory_phase_two_model_request_test 都依赖它。它把测试框架 test_codex 和本文件的记忆配置连接起来。
调用图:调用 1 个内部函数(test_codex);被 3 处调用(build_test_codex, run_memory_phase_one_model_request_test, run_memory_phase_two_model_request_test)。
init_state_db537–542 ↗
async fn init_state_db(home: &Arc<TempDir>) -> anyhow::Result<Arc<codex_state::StateRuntime>>
作用:初始化测试用状态数据库,并标记历史回填已经完成。这样记忆任务可以直接处理测试塞进去的数据。
数据流:输入是临时 home 目录;它在目录里初始化 StateRuntime,并调用 mark_backfill_complete;输出是可共享的数据库运行对象。
调用关系:需要手工往数据库放阶段一输出或排队合并任务的测试会调用它。它为 seed_stage1_output 等数据准备函数提供基础。
调用图:调用 1 个内部函数(init);被 3 处调用(memories_startup_phase2_prunes_old_extension_resources, memories_startup_phase2_prunes_old_extension_resources_without_stage1_input, memories_startup_phase2_tracks_workspace_diff_across_runs)。
trigger_memories_startup544–559 ↗
async fn trigger_memories_startup(test: &TestCodex)
作用:在测试里手动触发记忆启动后台任务。它相当于模拟程序真正启动时打开记忆功能的那一下。
数据流:输入是 TestCodex;它读取当前配置快照,克隆配置并启用 MemoryTool 特性,然后调用 start_memories_startup_task;输出没有直接返回值,但会启动后台异步任务并改动磁盘或数据库。
调用关系:完整启动流程测试都通过它来启动被测逻辑。后续通常配合 wait_for_dir、wait_for_single_request 或 wait_for_phase2_workspace_reset 等等待结果出现。
调用图:被 4 处调用(memories_startup_creates_memory_root, memories_startup_phase2_prunes_old_extension_resources, memories_startup_phase2_prunes_old_extension_resources_without_stage1_input, memories_startup_phase2_tracks_workspace_diff_across_runs);外部调用 3 个(clone, new, start_memories_startup_task)。
memory_startup_context_with_provider561–583 ↗
async fn memory_startup_context_with_provider(
test: &TestCodex,
provider: SharedModelProvider,
) -> (Arc<MemoryStartupContext>, Arc<codex_core::config::Config>)
作用:创建一个带自定义模型提供方的记忆启动上下文。测试用它把真实模型提供方换成可控的假提供方。
数据流:输入是 TestCodex 和一个 SharedModelProvider;它读取配置快照,启用 MemoryTool,构造 MemoryStartupContext::new_for_testing;输出是上下文和对应配置。
调用关系:阶段一、阶段二模型选择测试都调用它。它让 run_memory_phase_one_model_request_test 和 run_memory_phase_two_model_request_test 能直接运行 phase1 或 phase2,同时控制推荐模型名。
调用图:调用 1 个内部函数(new_for_testing);被 2 处调用(run_memory_phase_one_model_request_test, run_memory_phase_two_model_request_test);外部调用 2 个(clone, new)。
MockMemoryModelProvider::new594–598 ↗
fn new(info: ModelProviderInfo, auth_manager: Option<Arc<AuthManager>>) -> Self
作用:创建一个假的记忆模型提供方。它大部分行为仍交给真实提供方,只把记忆阶段的推荐模型名换成测试固定值。
数据流:输入是模型提供方信息和可选的登录管理器;它调用 create_model_provider 创建内部代理;输出是 MockMemoryModelProvider。
调用关系:阶段一和阶段二模型请求测试会用它。后续方法会把普通能力转发给 delegate,但记忆模型推荐方法返回本文件里的常量。
调用图:被 2 处调用(run_memory_phase_one_model_request_test, run_memory_phase_two_model_request_test);外部调用 1 个(create_model_provider)。
MockMemoryModelProvider::info602–604 ↗
fn info(&self) -> &ModelProviderInfo
作用:返回模型提供方的基本信息。这个假提供方不自己保存复杂信息,而是问内部真实代理要。
数据流:输入是自身对象;它读取 delegate 的 info;输出是 ModelProviderInfo 的引用。
调用关系:这是 ModelProvider 接口要求的方法。运行记忆请求时,外部代码可能像对待真实提供方一样读取这些信息。
调用图:外部调用 1 个(info)。
MockMemoryModelProvider::memory_extraction_preferred_model606–608 ↗
fn memory_extraction_preferred_model(&self) -> &'static str
作用:告诉程序第一阶段记忆提取默认该用哪个模型。这里故意返回固定假模型名,方便测试确认请求是否听从提供方推荐。
数据流:输入是自身对象;它不读取外部状态,直接返回 MOCK_PROVIDER_PHASE_ONE_MODEL;输出是阶段一推荐模型名。
调用关系:run_memory_phase_one_model_request_test 注入这个提供方后,phase1 选择模型时会用到它。相关测试再检查请求体中的 model 字段。
MockMemoryModelProvider::memory_consolidation_preferred_model610–612 ↗
fn memory_consolidation_preferred_model(&self) -> &'static str
作用:告诉程序第二阶段记忆合并默认该用哪个模型。这里返回固定假模型名,方便测试默认选择规则。
数据流:输入是自身对象;它直接返回 MOCK_PROVIDER_PHASE_TWO_MODEL;输出是阶段二推荐模型名。
调用关系:run_memory_phase_two_model_request_test 注入这个提供方后,phase2 选择模型时会用到它。相关测试会验证请求里是否出现这个模型名。
MockMemoryModelProvider::auth_manager614–616 ↗
fn auth_manager(&self) -> Option<Arc<AuthManager>>
作用:返回登录管理器。假提供方不自己处理登录,而是沿用内部真实代理的登录能力。
数据流:输入是自身对象;它调用 delegate.auth_manager;输出是可选的 AuthManager。
调用关系:这是 ModelProvider 接口的一部分。记忆请求如果需要认证信息,会通过这个方法继续走真实代理。
调用图:外部调用 1 个(auth_manager)。
MockMemoryModelProvider::auth618–621 ↗
fn auth(&self) -> ModelProviderFuture<'_, Option<CodexAuth>>
作用:异步取得认证信息,也就是访问模型服务可能需要的登录凭证。这里仍然交给内部代理处理。
数据流:输入是自身对象;它克隆 delegate,并在异步任务里调用 delegate.auth;输出是一个将来完成的认证结果。
调用关系:模型请求流程需要认证时会用到它。这个方法保证测试假提供方除了模型推荐名之外,其余行为尽量像真实提供方。
调用图:外部调用 2 个(clone, pin)。
MockMemoryModelProvider::account_state623–625 ↗
fn account_state(&self) -> ProviderAccountResult
作用:返回模型账户状态,比如账户是否可用。测试假提供方直接复用真实代理的判断。
数据流:输入是自身对象;它调用 delegate.account_state;输出是账户状态结果。
调用关系:这是 ModelProvider 接口的一环。记忆流程或底层请求代码如果检查账户状态,会通过这里转到真实代理。
调用图:外部调用 1 个(account_state)。
MockMemoryModelProvider::models_manager627–634 ↗
fn models_manager(
&self,
codex_home: PathBuf,
config_model_catalog: Option<ModelsResponse>,
) -> codex_models_manager::manager::SharedModelsManager
作用:返回模型列表管理器,用来处理可用模型目录。假提供方不改这部分行为。
数据流:输入是 codex_home 和可选的模型目录配置;它把这些参数转给 delegate.models_manager;输出是共享的模型管理器。
调用关系:模型请求周边代码可能需要它来查模型信息。这里保持转发,避免假提供方影响与本测试无关的模型管理行为。
调用图:外部调用 1 个(models_manager)。
seed_stage1_output637–669 ↗
async fn seed_stage1_output(
db: &codex_state::StateRuntime,
codex_home: &Path,
updated_at: chrono::DateTime<chrono::Utc>,
raw_memory: &str,
rollout_summary: &str,
rollout_slug
作用:往测试数据库里塞一条“阶段一已经成功提取”的记忆结果。它让第二阶段测试不用真的跑完整第一阶段。
数据流:输入是数据库、home 路径、更新时间、原始记忆、对话摘要和摘要文件名;它创建线程元数据,写入工作区、模型提供方和 git 分支信息,再调用 seed_stage1_output_for_existing_thread 标记阶段一成功;输出是新建的 ThreadId。
调用关系:第二阶段相关测试和 run_memory_phase_two_model_request_test 都用它准备输入。它自己再把真正的阶段一成功标记交给 seed_stage1_output_for_existing_thread。
调用图:调用 3 个内部函数(seed_stage1_output_for_existing_thread, new, new);被 3 处调用(memories_startup_phase2_prunes_old_extension_resources, memories_startup_phase2_tracks_workspace_diff_across_runs, run_memory_phase_two_model_request_test);外部调用 4 个(timestamp, join, format!, upsert_thread)。
seed_stage1_candidate671–710 ↗
async fn seed_stage1_candidate(
db: &codex_state::StateRuntime,
codex_home: &Path,
updated_at: chrono::DateTime<chrono::Utc>,
rollout_slug: &str,
) -> anyhow::Result<ThreadId>
作用:往测试环境里塞一条“可以被阶段一处理”的候选聊天记录。它模拟用户曾经说过一段需要记住的话。
数据流:输入是数据库、home 路径、更新时间和 rollout 名;它写一个 jsonl 聊天记录文件,创建线程元数据,设置预览文字和第一条用户消息,并把该线程的记忆模式设为 enabled;输出是新线程的 ThreadId。
调用关系:run_memory_phase_one_model_request_test 用它给 phase1 准备待处理材料。随后 phase1 会读取这条候选记录并向模型服务器发请求。
调用图:调用 2 个内部函数(new, new);被 1 处调用(run_memory_phase_one_model_request_test);外部调用 9 个(to_rfc3339, join, format!, ResponseItem, to_string, set_thread_memory_mode, upsert_thread, write, vec!)。
wait_for_single_request712–714 ↗
async fn wait_for_single_request(mock: &ResponseMock) -> ResponsesRequest
作用:等待 mock 模型服务器收到至少一个请求,并取出第一个。异步测试里请求不是立刻到,所以需要这种等待工具。
数据流:输入是 ResponseMock;它调用 wait_for_request 等到 1 个请求,然后移除并返回列表里的第一个;输出是 ResponsesRequest。
调用关系:多个测试在触发记忆流程后都用它拿到模型请求。它只是 wait_for_request 的简化版,适合只期待一次请求的场景。
调用图:调用 1 个内部函数(wait_for_request);被 6 处调用(memories_startup_phase1_uses_live_thread_service_tier_and_detached_metadata, memories_startup_phase2_prunes_old_extension_resources, memories_startup_phase2_prunes_old_extension_resources_without_stage1_input, memories_startup_phase2_tracks_workspace_diff_across_runs, run_memory_phase_one_model_request_test, run_memory_phase_two_model_request_test)。
wait_for_file_removed716–729 ↗
async fn wait_for_file_removed(path: &Path) -> anyhow::Result<()>
作用:等待某个文件被删除。它用于确认后台清理任务真的完成,而不是刚启动就检查导致误判。
数据流:输入是文件路径;它最多循环等待 10 秒,每 50 毫秒检查一次文件是否还存在;输出是成功结果,若超时则让测试失败。
调用关系:扩展资源清理测试直接用它,wait_for_phase2_workspace_reset 也用它等待临时差异文件消失。
调用图:被 3 处调用(memories_startup_phase2_prunes_old_extension_resources, memories_startup_phase2_prunes_old_extension_resources_without_stage1_input, wait_for_phase2_workspace_reset);外部调用 6 个(from_millis, from_secs, now, assert!, try_exists, sleep)。
wait_for_dir731–744 ↗
async fn wait_for_dir(path: &Path) -> anyhow::Result<()>
作用:等待某个目录被创建。它用于检查异步启动任务是否已经把需要的文件夹建好。
数据流:输入是目录路径;它在 10 秒内反复检查路径是否存在且是目录;输出是成功结果,超时会让测试失败。
调用关系:memories_startup_creates_memory_root 在触发启动后调用它,确认 memories 根目录最终出现。
调用图:被 1 处调用(memories_startup_creates_memory_root);外部调用 7 个(from_millis, from_secs, now, is_dir, assert!, try_exists, sleep)。
wait_for_request746–760 ↗
async fn wait_for_request(mock: &ResponseMock, expected_count: usize) -> Vec<ResponsesRequest>
作用:等待 mock 服务器收到指定数量的请求。它是这些异步请求测试的通用等待器。
数据流:输入是 ResponseMock 和期望请求数;它反复读取 mock.requests,直到数量足够或超过 10 秒;输出是已收到的请求列表。
调用关系:wait_for_single_request 调用它完成实际等待。其他测试通常不直接用它,而是通过更简单的 wait_for_single_request。
调用图:调用 1 个内部函数(requests);被 1 处调用(wait_for_single_request);外部调用 5 个(from_millis, from_secs, now, assert!, sleep)。
wait_for_service_tier762–779 ↗
async fn wait_for_service_tier(
test: &TestCodex,
expected_service_tier: Option<String>,
) -> anyhow::Result<codex_core::ThreadConfigSnapshot>
作用:等待线程配置里的服务档位变成期望值。服务档位可以理解为请求模型服务时的速度或资源等级。
数据流:输入是 TestCodex 和期望的 service_tier;它反复读取 config_snapshot,直到字段匹配;输出是匹配时的配置快照,超时则报错。
调用关系:memories_startup_phase1_uses_live_thread_service_tier_and_detached_metadata 用它确认线程设置已经生效,然后再创建记忆请求上下文。
调用图:被 1 处调用(memories_startup_phase1_uses_live_thread_service_tier_and_detached_metadata);外部调用 5 个(from_millis, from_secs, now, ensure!, sleep)。
phase2_prompt_text781–787 ↗
fn phase2_prompt_text(request: &ResponsesRequest) -> String
作用:从捕获到的模型请求里找出第二阶段提示词正文。它专门寻找包含“Memory workspace diff:”的用户输入。
数据流:输入是 ResponsesRequest;它取出角色为 user 的文本消息,找到包含记忆工作区差异标记的那段;输出是这段提示词字符串,找不到会让测试失败。
调用关系:第二阶段相关测试用它检查提示词里是否包含 phase2_workspace_diff.md。它帮助测试确认第二阶段确实把工作区变化交给模型看。
调用图:调用 1 个内部函数(message_input_texts);被 3 处调用(memories_startup_phase2_prunes_old_extension_resources, memories_startup_phase2_prunes_old_extension_resources_without_stage1_input, memories_startup_phase2_tracks_workspace_diff_across_runs)。
wait_for_phase2_workspace_reset789–804 ↗
async fn wait_for_phase2_workspace_reset(memory_root: &Path) -> anyhow::Result<()>
作用:等待第二阶段结束后记忆工作区恢复干净。这里的“干净”指临时差异文件已删除,并且 git 差异里没有未提交变化。
数据流:输入是 memory_root 路径;它先等 phase2_workspace_diff.md 被删除,再反复调用 diff_since_latest_init 检查是否还有改动;输出是成功结果,超时则让测试失败。
调用关系:第二阶段测试和 run_memory_phase_two_model_request_test 都用它确认后台收尾完成。它内部依赖 wait_for_file_removed,并通过 git 差异检查最终状态。
调用图:调用 1 个内部函数(wait_for_file_removed);被 4 处调用(memories_startup_phase2_prunes_old_extension_resources, memories_startup_phase2_prunes_old_extension_resources_without_stage1_input, memories_startup_phase2_tracks_workspace_diff_across_runs, run_memory_phase_two_model_request_test);外部调用 7 个(from_millis, from_secs, now, join, assert!, diff_since_latest_init, sleep)。
seed_stage1_output_for_existing_thread806–842 ↗
async fn seed_stage1_output_for_existing_thread(
db: &codex_state::StateRuntime,
thread_id: ThreadId,
updated_at: i64,
raw_memory: &str,
rollout_summary: &str,
rollout_slug: Op
作用:把已有线程标记成“阶段一任务已成功完成”。它是更底层的数据造假工具,用来模拟真实阶段一处理后的数据库状态。
数据流:输入是数据库、线程 ID、更新时间、原始记忆、摘要和可选 rollout 名;它先尝试领取阶段一任务,拿到所有权令牌,再用 mark_stage1_job_succeeded 写入成功结果并排队全局合并;输出是成功结果,异常状态会直接让测试失败。
调用关系:seed_stage1_output 创建线程后调用它。它让第二阶段测试能跳过真正的模型提取,直接得到可合并的阶段一输出。
调用图:调用 2 个内部函数(new, memories);被 1 处调用(seed_stage1_output);外部调用 2 个(assert!, panic!)。
read_rollout_summary_bodies844–852 ↗
async fn read_rollout_summary_bodies(memory_root: &Path) -> anyhow::Result<Vec<String>>
作用:读取记忆目录里所有 rollout 摘要文件的内容。rollout 摘要可以理解为一次对话或一次运行的简短总结。
数据流:输入是 memory_root 路径;它打开 rollout_summaries 目录,逐个读取文件内容,排序后返回;输出是摘要正文列表。
调用关系:memories_startup_phase2_tracks_workspace_diff_across_runs 用它检查第二阶段最终只保留了新摘要,没有混入旧摘要。
调用图:被 1 处调用(memories_startup_phase2_tracks_workspace_diff_across_runs);外部调用 4 个(join, new, read_dir, read_to_string)。
shutdown_test_codex854–858 ↗
async fn shutdown_test_codex(test: &TestCodex) -> anyhow::Result<()>
作用:干净地关闭测试用 Codex。这样后台任务不会残留,也不会影响后面的测试。
数据流:输入是 TestCodex;它提交 Shutdown 操作,然后等待 ShutdownComplete 事件;输出是成功结果,表示测试实例已经停好。
调用关系:几乎所有会启动 TestCodex 的测试或辅助流程最后都会调用它。它负责统一收尾,避免异步后台任务继续跑。
调用图:被 7 处调用(memories_startup_creates_memory_root, memories_startup_phase1_uses_live_thread_service_tier_and_detached_metadata, memories_startup_phase2_prunes_old_extension_resources, memories_startup_phase2_prunes_old_extension_resources_without_stage1_input, memories_startup_phase2_tracks_workspace_diff_across_runs, run_memory_phase_one_model_request_test, run_memory_phase_two_model_request_test);外部调用 1 个(wait_for_event)。
memories/write/src/storage_tests.rs源码 ↗
这个测试文件像是在给记忆存储系统做“验收检查”。它先造出假的记忆数据,里面有线程编号、摘要、原始记忆、工作目录等信息,然后检查系统生成摘要文件名时是否稳定、安全:没有标题时要用时间和编号生成固定名字,有标题时要把空格、斜杠、符号这些不适合放进文件名的字符处理掉,并且太长要截短。最后一个异步测试会真的建一个临时目录,放入旧的摘要文件,再让同步和重建逻辑运行,确认旧文件会被删掉,只留下当前有效的摘要,同时原始记忆总表里会写入正确内容和对应的摘要文件名。简单说,它防止“文件名乱套”和“旧文件赖着不走”。
stage1_output_with_slug18–30 ↗
fn stage1_output_with_slug(thread_id: ThreadId, rollout_slug: Option<&str>) -> Stage1Output
作用:这个辅助函数用来快速造一条假的记忆处理结果,方便多个测试重复使用。测试可以指定线程编号和可选的摘要标题,不用每次手写一大堆字段。
数据流:进去的是一个线程编号和一个可能存在的标题文字 → 它把这些和固定的时间、原始记忆、摘要、路径等假数据拼成一个 Stage1Output(第一阶段输出,也就是一条待保存的记忆记录)→ 出来的是一条完整的测试用记忆数据,不会改动外部东西。
调用关系:它是测试里的造数工具,会被三个文件名相关测试使用。那些测试先用 fixed_thread_id 准备稳定编号,再调用它生成记忆记录,然后拿这条记录去检查文件名生成结果。
调用图:被 3 处调用(rollout_summary_file_stem_sanitizes_and_truncates_slug, rollout_summary_file_stem_uses_uuid_timestamp_and_hash_when_slug_is_empty, rollout_summary_file_stem_uses_uuid_timestamp_and_hash_when_slug_missing);外部调用 1 个(from)。
fixed_thread_id32–34 ↗
fn fixed_thread_id() -> ThreadId
作用:这个辅助函数提供一个固定不变的线程编号,让测试结果每次运行都一样。没有它,随机编号会让文件名跟着变化,测试就不好判断对错。
数据流:它不接收输入 → 把一段写死的字符串转换成 ThreadId(线程编号,一种带格式校验的 ID)→ 返回这个固定编号;如果字符串格式不对,测试会直接失败。
调用关系:它被三个文件名测试调用,用来保证这些测试都站在同一个稳定编号上。它内部把字符串交给 try_from 做格式转换。
调用图:调用 1 个内部函数(try_from);被 3 处调用(rollout_summary_file_stem_sanitizes_and_truncates_slug, rollout_summary_file_stem_uses_uuid_timestamp_and_hash_when_slug_is_empty, rollout_summary_file_stem_uses_uuid_timestamp_and_hash_when_slug_missing)。
rollout_summary_file_stem_uses_uuid_timestamp_and_hash_when_slug_missing37–42 ↗
fn rollout_summary_file_stem_uses_uuid_timestamp_and_hash_when_slug_missing()
作用:这个测试确认:当记忆没有提供标题时,摘要文件名的主体部分会退回到一套稳定规则,而不是生成空名或乱名。
数据流:进去的是测试中固定准备的线程编号和“没有标题”的记忆数据 → 测试让系统算出摘要文件名主体,并和预期的 FIXED_PREFIX 比较 → 如果一样,测试通过;如果不一样,说明无标题时的命名规则坏了。
调用关系:它先调用 fixed_thread_id 拿固定编号,再调用 stage1_output_with_slug 造一条没有标题的记忆,最后用 assert_eq! 做结果核对。它专门覆盖“标题缺失”这个边界情况。
调用图:调用 2 个内部函数(fixed_thread_id, stage1_output_with_slug);外部调用 1 个(assert_eq!)。
rollout_summary_file_stem_sanitizes_and_truncates_slug45–61 ↗
fn rollout_summary_file_stem_sanitizes_and_truncates_slug()
作用:这个测试确认:用户给的标题即使包含空格、斜杠、特殊符号,或者特别长,也会被整理成安全、长度合适的文件名片段。
数据流:进去的是一个固定线程编号和一段故意写得很乱很长的标题 → 系统生成文件名主体后,测试取出后面的标题部分 → 检查它长度正好被限制住,并且内容已经变成小写、下划线替代不安全字符、超长部分被截断的安全形式。
调用关系:它使用 fixed_thread_id 和 stage1_output_with_slug 准备输入,再调用 rollout_summary_file_stem 生成文件名主体。随后用 format! 拼出应有前缀,用 assert_eq! 确认整理和截断规则都符合预期。
调用图:调用 2 个内部函数(fixed_thread_id, stage1_output_with_slug);外部调用 3 个(assert_eq!, format!, rollout_summary_file_stem)。
rollout_summary_file_stem_uses_uuid_timestamp_and_hash_when_slug_is_empty64–69 ↗
fn rollout_summary_file_stem_uses_uuid_timestamp_and_hash_when_slug_is_empty()
作用:这个测试确认:标题字段存在但内容是空字符串时,系统会把它当成“没有可用标题”,仍然使用稳定的默认命名规则。
数据流:进去的是固定线程编号和标题为空字符串的记忆数据 → 测试计算摘要文件名主体 → 出来的结果必须等于固定前缀;如果出现多余的分隔符或空标题后缀,测试就会失败。
调用关系:它和“标题缺失”的测试类似,先用 fixed_thread_id 和 stage1_output_with_slug 造数据,再用 assert_eq! 对结果。它补上的是“有字段但没内容”这个容易漏掉的情况。
调用图:调用 2 个内部函数(fixed_thread_id, stage1_output_with_slug);外部调用 1 个(assert_eq!)。
sync_rollout_summaries_and_raw_memories_file_keeps_latest_memories_only72–149 ↗
async fn sync_rollout_summaries_and_raw_memories_file_keeps_latest_memories_only()
作用:这个异步测试确认:同步摘要文件和重建原始记忆总表时,只保留当前这批有效记忆,旧格式或已经淘汰的文件会被清掉。
数据流:进去的是一个临时目录、两个预先写好的旧摘要文件,以及一条当前有效的记忆 → 测试先创建存储目录,再调用同步摘要和重建原始记忆文件的函数 → 之后检查旧文件已经不存在,摘要目录里只剩一个规范的新文件,并且 raw memories 总表里包含原始记忆、线程编号、工作目录、rollout 路径和新摘要文件名。
调用关系:它是这个文件里最接近真实使用场景的测试。它会调用 ensure_layout 建目录,借助 rollout_summaries_dir 和 raw_memories_file 找到该放文件的位置,再把核心工作交给 sync_rollout_summaries_from_memories 和 rebuild_raw_memories_file_from_memories。最后用文件系统读取和断言来确认整条写盘流程没有留下旧垃圾,也没有漏写关键信息。
调用图:调用 1 个内部函数(default);外部调用 14 个(new, assert!, assert_eq!, ensure_layout, raw_memories_file, rebuild_raw_memories_file_from_memories, rollout_summaries_dir, sync_rollout_summaries_from_memories, format!, tempdir (+4 more))。
memories/write/src/workspace_tests.rs源码 ↗
这个文件不是正式运行时给用户用的代码,而是给开发者和持续集成系统跑的测试。它围绕一个“记忆工作区”做检查:这个工作区像一个小仓库,用来保存 MEMORY.md 这类记忆文件,并用 Git 风格的差异记录哪里改了。测试会临时造一个干净目录,不碰用户真实文件;然后写入文件、生成差异、重置基线,再确认结果符合预期。这里特别关注几个容易被忽略的情况:差异内容太大时要截断,不能把输出撑爆;重置后生成的差异文件要被删掉;如果 .git 目录坏了,准备流程要能恢复;按字节截断文字时,不能把像“é”这种多字节字符切成半个。它像是在给工作区的几个安全护栏做体检。
render_workspace_diff_file_bounds_large_diff9–23 ↗
fn render_workspace_diff_file_bounds_large_diff()
作用:这个测试确认:当工作区差异内容特别大时,渲染出来的文本会被安全截断,并且仍然保留文件改动摘要和正确的代码块结尾。这样可以避免日志或提示内容被超大 diff 撑爆。
数据流:进去的是一个人为构造的巨大差异:它说 MEMORY.md 被修改了,并放入超过上限的一大串文本。测试把这个差异交给渲染函数,得到一段要展示给人的文本。最后它检查三件事:文本里有“MEMORY.md 被修改”的提示;文本里有“已在 4194304 字节截断”的说明;文本最后仍然用 Markdown 代码块的结束符收尾。
调用关系:它是在测试运行时由测试框架单独调用的。函数内部主要用断言来检查结果是否符合预期,并用 vec! 构造改动列表;它的角色是给差异渲染这条流程设置一个“大文件边界”的保护测试。
reset_memory_workspace_baseline_removes_generated_diff26–55 ↗
async fn reset_memory_workspace_baseline_removes_generated_diff()
作用:这个测试确认:重置记忆工作区的基线后,之前生成的差异文件会被清掉,而且工作区看起来会回到“没有未提交改动”的状态。
数据流:进去的是一个临时目录,测试先在里面准备记忆工作区,再写入 MEMORY.md,并人为写入一份表示“新增 MEMORY.md”的差异记录。接着它调用重置基线的流程。出来的结果应该是:保存差异的那个文件已经不存在;重新读取工作区差异时,改动列表是空的。
调用关系:它由异步测试框架 tokio 在测试阶段运行,因为被测流程里有异步文件或仓库操作。它先搭好临时环境,再驱动准备、写差异、重置、读取差异这条完整小流程,最后用 assert 和 assert_eq 检查外部可见结果。
调用图:外部调用 5 个(new, assert!, assert_eq!, write, vec!)。
prepare_memory_workspace_recovers_unusable_git_dir58–72 ↗
async fn prepare_memory_workspace_recovers_unusable_git_dir()
作用:这个测试确认:如果记忆工作区里已经有一个坏掉或不可用的 .git 目录,准备流程不会直接失败,而是能把工作区修到可用状态。
数据流:进去的是一个临时目录。测试先故意创建一个看起来像 .git、但其实不可用的目录,再写入 MEMORY.md。然后它运行准备工作区的函数。最后重新读取工作区差异,期望得到空的改动列表,表示这个工作区已经被整理成一个干净可用的状态。
调用关系:它同样由 tokio 异步测试框架调用。它模拟一种现实里可能出现的坏状态:目录里残留了不完整的 Git 信息。测试把这个坏状态交给准备流程处理,再用断言确认后续读取差异不会报出奇怪改动。
调用图:外部调用 4 个(new, assert_eq!, create_dir_all, write)。
previous_char_boundary_handles_multibyte_text75–78 ↗
fn previous_char_boundary_handles_multibyte_text()
作用:这个测试确认:按字节位置往前找字符边界时,遇到多字节字符也不会切错。它保护的是字符串截断这类功能,避免把一个字符拆坏。
数据流:进去的是字符串“aé”和最大字节位置 2。“é”在内存里不是 1 个字节,所以位置 2 正好落在这个字符中间。测试调用查找前一个安全字符边界的函数,期望返回 1,也就是只保留完整的“a”,不要切进“é”的内部。
调用关系:它由普通测试框架调用,内容很小但很关键。它只用 assert_eq 检查结果,主要服务于那些需要按字节限制输出长度的流程,比如大 diff 截断,确保截断后的文本仍是合法字符串。
调用图:外部调用 1 个(assert_eq!)。
消息与线程存储夹具
消息历史的持久化测试与可复用的本地线程存储夹具配套,用于贴近真实的 rollout 支撑存储场景。
message-history/src/tests.rs源码 ↗
消息历史一般会存在一个一行一条的 JSON 文件里,像账本一样不断往后追加。这个测试文件就是给这本“账本”做体检:先确认系统能数清里面有多少条记录,也能按位置取出某一条;再确认文件变大、跨过读取缓冲区边界时,换行数量也不会数错;还会检查追加新记录后,旧的定位标识仍然能用。最后两组测试关注空间限制:当历史文件超过配置的最大字节数时,系统要删掉前面的旧记录;而且不是刚好卡在上限就停,而是会裁到一个更舒服的“软上限”,给后续写入留余地。整个文件不直接面向用户运行,但它很重要,因为历史记录一旦出错,用户看到的上下文就可能乱掉,或者磁盘被越占越多。
lookup_reads_history_entries9–42 ↗
async fn lookup_reads_history_entries()
作用:这个测试确认:已经写进历史文件的多条记录,系统能正确统计数量,并且能按偏移位置取回指定那一条。简单说,就是检查“账本写了两行,能不能数出两行,并翻到第二行”。
数据流:它先创建一个临时目录和历史文件路径,再准备两条 HistoryEntry 记录,把它们逐行序列化成 JSON 写入文件。接着调用历史元数据读取功能拿到日志标识和记录条数,确认条数等于 2。最后用日志标识和偏移量 1 去查第二条记录,结果应该和最初写入的第二条完全一样。
调用关系:这是读取链路的基础测试。它模拟外部世界已经有历史文件的情况,然后把检查交给 history_metadata_for_file 和 lookup_history_entry;前者像先看目录和页数,后者像按页码翻到具体记录。
调用图:外部调用 5 个(create, new, assert_eq!, vec!, writeln!)。
history_metadata_counts_newlines_across_read_boundaries45–64 ↗
async fn history_metadata_counts_newlines_across_read_boundaries()
作用:这个测试确认:历史文件很大、需要分块读取时,系统仍然能正确数出换行符数量。因为一行通常代表一条历史记录,换行数数错了,记录数量就会错。
数据流:它创建一个比读取缓冲区大好几倍的字节数组,在几个关键位置放入换行符,包括刚好在缓冲区边界前后的位置。然后把这些内容写成历史文件,再调用 history_metadata_for_file 统计记录数。最后检查统计出来的数量,必须等于人工放进去的换行符数量。
调用关系:它专门盯住 history_metadata_for_file 的边界情况。平时小文件不容易暴露这个问题,但大文件会分段读;这个测试就是防止分段处的换行被漏数或重复数。
调用图:外部调用 4 个(new, assert_eq!, write, vec!)。
lookup_uses_stable_log_id_after_appends67–107 ↗
async fn lookup_uses_stable_log_id_after_appends()
作用:这个测试确认:系统拿到一个历史文件的日志标识后,即使文件后来又追加了新记录,也还能用这个标识查到新追加的内容。换句话说,历史文件“长高了”,原来的定位方式不应该立刻失效。
数据流:它先创建历史文件并写入第一条记录,然后读取元数据,得到日志标识和当时的记录数。之后它用追加模式打开同一个文件,写入第二条记录。最后用之前拿到的日志标识,加上偏移量 1 去查新追加的第二条,结果必须等于追加的那条记录。
调用关系:它连接了“读取元数据”和“后续查记录”两个动作。测试重点是 lookup_history_entry 对 log_id 的使用是否稳定,尤其是在 append 追加写入之后仍能正确工作。
调用图:外部调用 5 个(create, new, assert_eq!, new, writeln!)。
append_entry_trims_history_when_beyond_max_bytes110–149 ↗
async fn append_entry_trims_history_when_beyond_max_bytes()
作用:这个测试确认:追加新历史时,如果文件超过配置的最大大小,系统会删掉旧记录,让文件重新回到限制以内。它防止历史文件一直膨胀,占满用户磁盘。
数据流:它先创建一个临时的 codex_home 目录和默认历史配置,写入一条较长记录,并测出文件大小。然后把最大允许大小设置成“第一条大小再多一点点”,再追加第二条记录。追加后,它读回整个历史文件,把每一行解析成 HistoryEntry,确认只剩第二条记录,第一条已经被清掉,并且文件大小不超过限制。
调用关系:它主要测试 append_entry 和 HistoryConfig::new 配合后的裁剪行为。流程上,配置先告诉 append_entry 最大能写多大;append_entry 写入新记录后,如果超限,就要把较早的记录移走。
调用图:调用 1 个内部函数(new);外部调用 7 个(new, assert!, assert_eq!, default, metadata, read_to_string, try_from)。
append_entry_trims_history_to_soft_cap152–220 ↗
async fn append_entry_trims_history_to_soft_cap()
作用:这个测试确认:历史文件超限时,系统不是只删到刚好低于硬上限,而是会尽量裁到更低的“软上限”。软上限可以理解成给文件留一点余量,避免每次追加都马上又超。
数据流:它先写一条短记录,再写一条长记录,并通过文件大小差算出长记录大概占多少空间。然后设置一个最大大小:如果只删最早的短记录,其实已经能满足硬上限,但仍然高于软上限。接着再追加一条长记录,触发裁剪。最后读回文件,确认只剩一条最新的长记录;同时检查文件大小既不超过最大限制,也符合软上限策略的预期。
调用关系:它测试的是 append_entry 更细的空间控制规则。HistoryConfig::new 提供 max_bytes,append_entry 在写入后决定删多少旧内容;这个测试特意构造一种场景,证明它会按软上限更积极地清理,而不是只满足最低要求。
调用图:调用 1 个内部函数(new);外部调用 7 个(new, assert!, assert_eq!, default, metadata, read_to_string, try_from)。
thread-store/src/local/test_support.rs源码 ↗
本地线程存储要处理一种叫 rollout 的会话记录文件,可以把它理解成“聊天历史账本”:每一行都是一条 JSON 记录。测试这些功能时,如果每个测试都手写目录、文件名、元数据和用户消息,会很啰嗦,也容易写错。这个文件就把这些重复活包起来。它先能生成一份测试用配置,告诉代码“家目录”和 SQLite 数据库目录在哪里;SQLite 是一种把数据存在本地文件里的小数据库。然后它能在指定目录下写出标准格式的会话文件,或者写到归档目录里,模拟“已经被归档的聊天”。更底层的函数还允许指定第一条用户消息、模型提供方,以及这个会话是否从另一个会话分叉而来。这样测试就能像搭积木一样造出不同场景,重点检查真正的业务行为,而不是反复关心文件格式细节。
test_config11–17 ↗
fn test_config(codex_home: &Path) -> LocalThreadStoreConfig
作用:这个函数生成一份最简单的测试配置。测试只要给它一个临时目录,它就把这个目录当成本地数据和数据库的位置,并固定使用一个假的模型提供方。
数据流:进去的是一个路径 codex_home,也就是测试用的临时家目录;它把这个路径复制成可拥有的 PathBuf,分别填到 codex_home 和 sqlite_home;出来的是 LocalThreadStoreConfig,里面还带着固定字符串 test-provider。它不写文件,只是准备配置数据。
调用关系:很多本地线程存储测试一开始都会调用它,比如归档、删除、列出线程、读取线程这些测试。它相当于测试开场时发的“通行证”,后面的代码拿着这份配置才知道去哪里找会话文件和数据库。
调用图:被 65 处调用(archive_thread_moves_rollout_to_archived_collection, archive_thread_updates_sqlite_metadata_when_present, delete_rollout_file_treats_vanished_path_as_already_deleted, delete_thread_removes_active_and_archived_rollouts, delete_thread_reports_missing_thread, list_threads_preserves_sqlite_title_search_results, list_threads_rejects_invalid_cursor, list_threads_returns_local_rollout_summary, list_threads_selects_active_or_archived_collection, list_threads_uses_default_provider_when_rollout_omits_provider (+15 more));外部调用 1 个(to_path_buf)。
write_session_file19–28 ↗
fn write_session_file(root: &Path, ts: &str, uuid: Uuid) -> std::io::Result<PathBuf>
作用:这个函数写一个普通的、未归档的测试会话文件。测试用它快速制造一段看起来像真实聊天记录的本地历史。
数据流:进去的是根目录 root、时间戳 ts 和会话 uuid;它把目标目录固定拼成 sessions/2025/01/03,然后交给 write_session_file_with,默认第一条用户消息是 Hello from user,模型提供方是 test-provider;出来的是新建文件的完整路径,或者写文件失败时返回错误。
调用关系:它是最常用的测试入口之一。归档、删除、读取、列表展示等测试会先用它造出一个活跃会话文件,再让被测代码去移动、删除或读取这个文件。真正写文件的细活由 write_session_file_with 继续下放处理。
调用图:调用 1 个内部函数(write_session_file_with);被 34 处调用(archive_thread_moves_rollout_to_archived_collection, archive_thread_updates_sqlite_metadata_when_present, delete_rollout_file_treats_vanished_path_as_already_deleted, delete_thread_removes_active_and_archived_rollouts, list_threads_returns_local_rollout_summary, list_threads_selects_active_or_archived_collection, read_thread_accepts_legacy_sandbox_policy_metadata, read_thread_applies_sqlite_thread_name, read_thread_by_rollout_path_prefers_sqlite_git_info, read_thread_falls_back_to_rollout_search_when_sqlite_path_is_stale (+15 more));外部调用 1 个(join)。
write_archived_session_file30–43 ↗
fn write_archived_session_file(
root: &Path,
ts: &str,
uuid: Uuid,
) -> std::io::Result<PathBuf>
作用:这个函数写一个已经归档的测试会话文件。它用来模拟“这段聊天不在当前会话列表里,而是在归档区里”的情况。
数据流:进去的是根目录 root、时间戳 ts 和会话 uuid;它把目标目录拼到项目定义的归档子目录 ARCHIVED_SESSIONS_SUBDIR 下,再交给 write_session_file_with;默认用户消息是 Archived user message,模型提供方是 test-provider;出来的是归档会话文件路径,或者文件创建错误。
调用关系:需要测试归档区行为时会用到它,比如删除同时清理活跃和归档文件、读取归档会话、取消归档、从归档历史加载内容等。它自己不关心 JSON 怎么写,而是把共同格式交给 write_session_file_with。
调用图:调用 1 个内部函数(write_session_file_with);被 11 处调用(delete_thread_removes_active_and_archived_rollouts, list_threads_selects_active_or_archived_collection, read_thread_prefers_active_rollout_over_archived, read_thread_returns_archived_rollout_when_requested, read_thread_sqlite_fallback_loads_archived_history, load_history_uses_live_writer_rollout_path_for_archived_source, unarchive_thread_restores_rollout_and_returns_updated_thread, unarchive_thread_updates_sqlite_metadata_when_present, update_thread_metadata_keeps_archived_thread_archived_in_sqlite, update_thread_metadata_keeps_live_archived_thread_archived_in_sqlite (+1 more));外部调用 1 个(join)。
write_session_file_with45–62 ↗
fn write_session_file_with(
root: &Path,
day_dir: PathBuf,
ts: &str,
uuid: Uuid,
first_user_message: &str,
model_provider: Option<&str>,
) -> std::io::Result<PathBuf>
作用:这个函数写一个可定制内容的测试会话文件。相比 write_session_file,它允许测试指定目录、第一条用户消息,以及是否带模型提供方。
数据流:进去的是根目录、目标日期目录、时间戳、会话 uuid、第一条用户消息和可选的模型提供方;它不直接写文件,而是把这些信息继续传给 write_session_file_with_fork,并把 forked_from_id 固定为 None,表示这个会话不是从别的会话分叉来的;出来的是写好的文件路径或错误。
调用关系:它夹在简单入口和最底层写入函数之间。write_session_file 和 write_archived_session_file 用它来避免重复参数;某些测试也会直接用它制造“没有模型提供方”等特殊文件。真正落盘的工作交给 write_session_file_with_fork。
调用图:调用 1 个内部函数(write_session_file_with_fork);被 3 处调用(list_threads_uses_default_provider_when_rollout_omits_provider, write_archived_session_file, write_session_file)。
write_session_file_with_fork64–107 ↗
fn write_session_file_with_fork(
root: &Path,
day_dir: PathBuf,
ts: &str,
uuid: Uuid,
first_user_message: &str,
model_provider: Option<&str>,
forked_from_id: Option<Uuid>,
作用:这是实际创建测试会话文件的核心函数。它负责建目录、生成文件名、写入会话元数据和第一条用户消息,还能标记这个会话是从哪个会话分叉出来的。
数据流:进去的是根目录、目标目录、时间戳、会话 uuid、用户消息、可选模型提供方,以及可选的 forked_from_id;它先确保目录存在,再创建名为 rollout-时间戳-uuid.jsonl 的文件;接着写两行 JSON:第一行是 session_meta,记录会话编号、时间、工作目录、版本、模型提供方、Git 信息和分叉来源;第二行是 event_msg,记录用户发出的第一条消息;出来的是文件路径,过程中会真实改动磁盘,失败则返回输入输出错误。
调用关系:它是这个文件里所有造会话文件函数的终点。普通会话和归档会话最终都靠它写出符合格式的 JSONL 文件;测试分叉会话读取时也会直接或间接用它,以便检查被测代码能不能正确读出 forked_from_id。
调用图:被 2 处调用(read_thread_returns_forked_from_id, write_session_file_with);外部调用 6 个(join, format!, create, create_dir_all, json!, writeln!)。
外部 agent 会话账本
本重点组验证缺失文件、已完成导入和元数据刷新更新的账本行为。
external-agent-sessions/src/ledger_tests.rs源码 ↗
这个文件测试的是导入账本的几个边界情况。这里的“账本”可以理解成一本登记簿:某个外部会话文件被导入后,就把文件路径、内容指纹和导入后的线程编号记下来。这样以后系统不用反复打开原文件,也能知道哪些东西已经导入过。测试会临时创建一个目录,模拟真实的用户目录和会话文件;有的测试还会故意把源文件删掉,确认记录导入结果时不会依赖这个文件还存在。它也检查同一个源文件再次导入时,不会多记一条重复记录,而是更新原来的记录,尤其是换成新的线程编号。这里的 SHA-256 是一种内容指纹算法,像给文件内容按出来的“唯一手印”,用来确认内容有没有变。
empty_ledger_does_not_read_source10–19 ↗
fn empty_ledger_does_not_read_source()
作用:这个测试确认:一个空账本不会为了判断某个文件是否已导入,就去读取那个文件。即使给它一个根本不存在的文件路径,它也应该只是回答“没有记录过”。
数据流:进去的是一个临时目录里拼出来的、不存在的会话文件路径,以及一个默认创建的空账本。测试把这个路径交给账本查询。结果应该是 false,也就是“账本里没有这个当前来源”;同时不会因为文件不存在而报错。
调用关系:这是最基础的安全网测试。它直接调用 ImportedExternalAgentSessionLedger::default() 创建空账本,再调用 contains_current_source 做判断,用断言确认空账本只看自己的记录,不去碰源文件。
completed_imports_do_not_read_source_files22–47 ↗
fn completed_imports_do_not_read_source_files()
作用:这个测试确认:记录一次已经完成的导入时,即使原始会话文件后来被删掉,账本仍然能保存必要信息。它防止系统把“记账”做成必须重新读取源文件的脆弱流程。
数据流:先在临时目录里写一个会话文件,算出它内容的 SHA-256 指纹,然后把文件路径转成标准绝对路径,接着马上删掉源文件。测试把路径、内容指纹和新生成的 ThreadId(线程编号)交给 record_completed_session_imports。之后再从 codex_home 里读取账本,结果应该只有一条记录;这条记录保存了原路径和导入后的线程编号,并且 source_modified_at 是 None,因为源文件已经不存在,无法再读到修改时间。
调用关系:这个测试模拟“导入已经成功,但源文件随后消失”的场景。它把导入结果交给 record_completed_session_imports 写入账本,再用 load_import_ledger 读回来检查,确认写入过程没有偷偷依赖被删除的源文件。
调用图:调用 1 个内部函数(new);外部调用 8 个(new, assert_eq!, canonicalize, remove_file, write, load_import_ledger, record_completed_session_imports, vec!)。
completed_import_refreshes_existing_record_metadata50–85 ↗
fn completed_import_refreshes_existing_record_metadata()
作用:这个测试确认:同一个源文件、同一份内容再次导入时,账本不会留下两条重复记录,而是刷新已有记录。特别是导入后的线程编号会更新成最新的一次。
数据流:先创建一个真实存在的会话文件,算出它的内容指纹,并生成两个不同的 ThreadId。第一次把源路径、内容指纹和第一个线程编号写进账本;第二次用同一个源路径和同一个内容指纹,但换成第二个线程编号再次记录。最后读取账本,结果应该仍然只有一条记录;路径不变,线程编号变成第二个,并且 source_modified_at 有值,因为源文件还在,可以读取到它的修改时间。
调用关系:这个测试覆盖“重复导入同一个来源”的流程。它连续两次调用 record_completed_session_imports,然后通过 load_import_ledger 验证最终账本状态,确保账本更新旧记录,而不是越记越多。
调用图:调用 1 个内部函数(new);外部调用 9 个(new, assert!, assert_eq!, format!, canonicalize, write, load_import_ledger, record_completed_session_imports, vec!)。
Rollout 索引、元数据与记录
Rollout 子系统的测试范围从底层压缩和索引,到元数据/状态集成、记录器行为,以及端到端文件系统扫描。
rollout/src/compression_tests.rs源码 ↗
这里测的是 rollout 文件,也就是一行一条 JSON 的会话流水账文件。系统为了省空间,会把旧的 .jsonl 文件压成 .jsonl.zst,zstd 是一种压缩格式。测试会先在临时目录里造出假的会话记录,再模拟压缩、追加新消息、恢复会话、后台 worker 自动清理等场景。重点不是压缩算法本身,而是“压缩后对用户来说还像原文件一样好用”:能读取,能搜索,按线程 ID 能找到,追加时会自动解压回普通文件,文件权限和修改时间也不能乱变。它还检查临时文件和运行标记,避免后台压缩任务反复跑、覆盖已有文件,或者把没完成的压缩半成品当成真文件。
load_rollout_items_reads_compressed_rollout29–46 ↗
async fn load_rollout_items_reads_compressed_rollout() -> anyhow::Result<()>
作用:测试读取会话记录时,即使原来的普通文件已经被压成 .zst,系统也能照样读出来。这样用户打开历史记录时不会因为文件被压缩而看不到内容。
数据流:进去的是一个临时目录、一个固定线程 ID 和一条测试消息;它先生成普通 rollout 文件,再调用 compress_now 把它压缩并删除原文件;然后用 RolloutRecorder::load_rollout_items 读取原路径。出来的结果应该是读到 2 条记录、线程 ID 正确、没有解析错误,并且磁盘上只剩压缩文件。
调用关系:这是对读取入口 load_rollout_items 的压缩兼容性测试。它借助 rollout_path 造路径、write_rollout 写测试数据、compress_now 制造压缩状态,然后检查真正的读取逻辑是否把压缩文件当作同一份历史来处理。
调用图:调用 5 个内部函数(from_string, compress_now, rollout_path, write_rollout, load_rollout_items);外部调用 4 个(new, from_u128, assert!, assert_eq!)。
rollout_file_from_path_normalizes_compressed_file_names49–63 ↗
fn rollout_file_from_path_normalizes_compressed_file_names() -> anyhow::Result<()>
作用:测试系统看到压缩后的文件名时,能认出它原本对应哪个普通 rollout 文件名。这样列表展示或逻辑比较时,不会把 .jsonl.zst 当成另一种陌生文件。
数据流:进去的是临时目录、时间戳和 UUID;它生成一个普通 rollout 路径,再推导出压缩路径;随后把压缩路径交给 RolloutFile::from_path。出来的结果应该包含真实压缩路径,同时把“逻辑上的普通文件名”还原成 .jsonl。
调用关系:这个测试围绕 RolloutFile::from_path 展开。它使用 rollout_path 准备标准文件名,验证文件识别层能把压缩文件和普通文件归为同一个会话记录。
调用图:调用 1 个内部函数(rollout_path);外部调用 3 个(new, from_u128, assert_eq!)。
rollout_file_from_path_hides_compressed_sibling_when_plain_exists66–78 ↗
fn rollout_file_from_path_hides_compressed_sibling_when_plain_exists() -> anyhow::Result<()>
作用:测试当普通文件和它的压缩兄弟文件同时存在时,系统会忽略压缩兄弟。这样可以避免同一段会话在列表里出现两次。
数据流:进去的是临时目录、线程 ID 和一条消息;它先写出普通 rollout 文件,然后把对应的压缩路径交给 RolloutFile::from_path。出来的结果应该是 None,意思是这份压缩兄弟不该被当成独立文件显示或处理。
调用关系:它用 write_rollout 制造“普通文件已存在”的场景,再测试 RolloutFile::from_path 的去重规则。这个规则保护上层文件扫描逻辑不会重复处理同一个会话。
调用图:调用 3 个内部函数(from_string, rollout_path, write_rollout);外部调用 3 个(new, from_u128, assert_eq!)。
append_rollout_item_materializes_compressed_rollout81–106 ↗
async fn append_rollout_item_materializes_compressed_rollout() -> anyhow::Result<()>
作用:测试往已经压缩的会话记录里追加新消息时,系统会先把它恢复成普通文件。这样追加内容不会直接写坏压缩包,也不会丢掉旧记录。
数据流:进去的是一份被压缩的 rollout 和一条新的用户消息;它先写两条旧记录并压缩,再调用 append_rollout_item_to_path 追加新记录。出来后普通 .jsonl 文件应该重新出现,.zst 文件消失,读取时能看到 3 条记录且线程 ID 正确。
调用关系:这个测试检查追加函数 append_rollout_item_to_path 的前置动作:如果目标只有压缩版本,就先“实体化”为普通文件。之后再用 load_rollout_items 验证追加结果完整。
调用图:调用 5 个内部函数(from_string, compress_now, rollout_path, write_rollout, load_rollout_items);外部调用 8 个(default, new, from_u128, assert!, assert_eq!, append_rollout_item_to_path, UserMessage, EventMsg)。
search_rollout_matches_uses_logical_path_for_compressed_rollout109–130 ↗
async fn search_rollout_matches_uses_logical_path_for_compressed_rollout() -> anyhow::Result<()>
作用:测试搜索压缩会话记录时,返回结果使用原本的普通路径,而不是压缩文件路径。这样调用方看到的是同一个“会话文件”,不用关心底层是否压缩。
数据流:进去的是一份含有目标关键词的 rollout;它写文件、压缩文件,然后调用 search_rollout_matches 搜索“search term”。出来的匹配表应该用普通 .jsonl 路径作为键,并返回命中的那条消息。
调用关系:它验证搜索层对压缩文件做了路径归一化。测试中故意传了一个不存在的 rg 路径,用来逼迫代码走内部可控路径,但重点仍是 search_rollout_matches 返回的逻辑路径。
调用图:调用 4 个内部函数(from_string, compress_now, rollout_path, write_rollout);外部调用 5 个(new, from_u128, assert_eq!, search_rollout_matches, new)。
worker_compresses_old_active_and_archived_rollouts133–176 ↗
async fn worker_compresses_old_active_and_archived_rollouts() -> anyhow::Result<()>
作用:测试后台压缩 worker 会压缩足够旧的活跃和归档会话,但不会碰刚生成的新会话。它还检查旧的临时文件会被清掉,新的临时文件会保留。
数据流:进去的是临时目录里几种文件:旧的活跃 rollout、旧的归档 rollout、新的 rollout、旧临时文件和新临时文件;它把部分文件修改时间改成很久以前,再运行 worker::run。出来后旧正式文件应变成 .zst,新正式文件保持普通格式,旧临时文件被删除,新临时文件还在,并留下运行标记。
调用关系:这是对压缩后台任务 worker::run 的综合测试。它用 rollout_path、archived_rollout_path、write_rollout 和 set_old_mtime 搭好现场,再观察 worker 是否按年龄和位置正确取舍。
调用图:调用 5 个内部函数(from_string, archived_rollout_path, rollout_path, set_old_mtime, write_rollout);外部调用 5 个(new, from_u128, assert!, write, run)。
resume_materializes_compressed_rollout_path179–228 ↗
async fn resume_materializes_compressed_rollout_path() -> anyhow::Result<()>
作用:测试从压缩会话恢复时,系统会把压缩文件恢复成普通路径,并继续往这份普通文件写新记录。这样“继续上次会话”不会卡在只读压缩包上。
数据流:进去的是配置、一个压缩后的 rollout 路径和一条追加消息;它先确认 get_rollout_history 能把压缩文件识别为可恢复历史,再用 RolloutRecorder::new 以恢复模式打开它。出来后 recorder 的路径应是普通 .jsonl,压缩文件消失;写入新消息、刷新和关闭后,再读取应有 3 条记录。
调用关系:这个测试串起恢复流程的多个关键点:get_rollout_history 负责读历史,RolloutRecorderParams::resume 表示恢复已有文件,RolloutRecorder::new 负责打开并实体化文件,最后 record_canonical_items、flush、shutdown 验证后续写入正常。
调用图:调用 8 个内部函数(from_string, compress_now, rollout_path, write_rollout, get_rollout_history, load_rollout_items, new, resume);外部调用 8 个(default, new, from_u128, assert!, assert_eq!, panic!, UserMessage, EventMsg)。
compression_preserves_rollout_permissions232–250 ↗
async fn compression_preserves_rollout_permissions() -> anyhow::Result<()>
作用:在 Unix 系统上,测试压缩归档会话时不会改变文件权限。比如原文件只有本人可读写,压缩后也应该保持这个限制。
数据流:进去的是一份归档 rollout,权限被设成 0600,也就是只有文件主人能读写;它把文件设为旧文件后运行 worker::run。出来后普通文件应消失,压缩文件存在,并且权限仍是 0600。
调用关系:它测试 worker::run 在压缩时是否保留安全属性。archived_rollout_path 和 write_rollout 负责造归档文件,set_old_mtime 让 worker 有理由压缩它。
调用图:调用 4 个内部函数(from_string, archived_rollout_path, set_old_mtime, write_rollout);外部调用 7 个(new, from_u128, assert!, assert_eq!, from_mode, set_permissions, run)。
append_materialization_preserves_compressed_rollout_permissions254–280 ↗
async fn append_materialization_preserves_compressed_rollout_permissions() -> anyhow::Result<()>
作用:在 Unix 系统上,测试从压缩文件恢复成普通文件并追加内容时,原压缩文件的权限会被保留下来。这样受保护的历史记录不会在追加后突然变得更开放。
数据流:进去的是一份已压缩 rollout,压缩文件权限被设成 0600,以及一条要追加的消息;它调用 append_rollout_item_to_path。出来后普通文件存在、压缩文件消失,普通文件权限仍然是 0600。
调用关系:它检查追加流程里的“实体化”步骤是否尊重权限。compress_now 制造压缩状态,append_rollout_item_to_path 触发恢复和写入,最后读文件元数据确认权限没变。
调用图:调用 4 个内部函数(from_string, compress_now, rollout_path, write_rollout);外部调用 10 个(default, new, from_u128, assert!, assert_eq!, from_mode, append_rollout_item_to_path, set_permissions, UserMessage, EventMsg)。
persist_temp_file_noclobber_installs_completed_temp283–294 ↗
fn persist_temp_file_noclobber_installs_completed_temp() -> anyhow::Result<()>
作用:测试一个完成的临时文件能被安全地放到最终位置。这里的“noclobber”意思是不要覆盖已有目标文件。
数据流:进去的是一个已有内容的临时文件路径和一个还不存在的目标路径;它调用 persist_temp_file_noclobber。出来后临时文件应消失,目标文件出现,内容就是原来的临时文件内容。
调用关系:这个测试关注压缩流程里常见的安全落盘动作:先写临时文件,写完再挪到正式位置。它验证没有竞争时,persist_temp_file_noclobber 会完成安装。
调用图:外部调用 4 个(new, assert!, assert_eq!, write)。
persist_temp_file_noclobber_does_not_replace_existing_destination297–309 ↗
fn persist_temp_file_noclobber_does_not_replace_existing_destination() -> anyhow::Result<()>
作用:测试目标文件已经存在时,临时文件不会覆盖它。这样两个任务同时生成结果时,后来的半成品不会把已经存在的正式文件冲掉。
数据流:进去的是一个临时文件和一个已经有内容的目标文件;它调用 persist_temp_file_noclobber。出来后临时文件被删除,但目标文件内容保持原样。
调用关系:它补上 persist_temp_file_noclobber 的冲突场景。这个函数在后台压缩保存结果时很重要,因为它像“只在空位停车”的规则,防止覆盖别人已经停好的车。
调用图:外部调用 4 个(new, assert!, assert_eq!, write)。
compression_preserves_read_only_rollout_permissions313–331 ↗
async fn compression_preserves_read_only_rollout_permissions() -> anyhow::Result<()>
作用:在 Unix 系统上,测试只读会话文件被压缩后仍然只读,而且修改时间不变。这样压缩不会偷偷改变文件的访问限制或历史时间线。
数据流:进去的是一份归档 rollout,权限设成 0400,也就是只有主人可读,不能写;它记录原修改时间,然后运行 worker::run。出来后普通文件消失,压缩文件存在,权限仍是 0400,修改时间也等于原文件。
调用关系:它是权限保留测试的更严格版本。set_old_mtime 让文件满足压缩条件,worker::run 执行压缩,最后通过文件元数据检查权限和时间戳。
调用图:调用 4 个内部函数(from_string, archived_rollout_path, set_old_mtime, write_rollout);外部调用 8 个(new, from_u128, assert!, assert_eq!, from_mode, metadata, set_permissions, run)。
worker_skips_existing_compressed_archived_rollouts334–354 ↗
async fn worker_skips_existing_compressed_archived_rollouts() -> anyhow::Result<()>
作用:测试归档会话如果已经是压缩状态,后台 worker 不会重复压缩或破坏它。重复处理压缩包可能浪费时间,也可能带来文件损坏风险。
数据流:进去的是一份已压缩的归档 rollout;它把压缩文件时间设旧,再运行 worker::run。出来后普通文件仍不存在,压缩文件仍存在;再用 load_rollout_items 读取,确认内容和线程 ID 仍然正确。
调用关系:这个测试保证 worker::run 对已经完成的压缩结果是幂等的。幂等就是跑一次和跑多次效果一样,不会越跑越乱。
调用图:调用 6 个内部函数(from_string, archived_rollout_path, compress_now, set_old_mtime, write_rollout, load_rollout_items);外部调用 5 个(new, from_u128, assert!, assert_eq!, run)。
worker_skips_when_fresh_run_marker_exists357–373 ↗
async fn worker_skips_when_fresh_run_marker_exists() -> anyhow::Result<()>
作用:测试如果最近已经有压缩 worker 跑过,新的 worker 会跳过本轮。这个运行标记像门口的“刚打扫过”牌子,避免后台任务过于频繁。
数据流:进去的是一份本该被压缩的旧归档 rollout,以及 .tmp/rollout-compression.lock 这个新鲜标记文件;它运行 worker::run。出来后 rollout 仍是普通文件,没有生成压缩文件。
调用关系:它验证 worker::run 会先看运行标记,再决定是否扫描和压缩文件。write_rollout 和 set_old_mtime 制造可压缩对象,标记文件则让 worker 主动放弃。
调用图:调用 4 个内部函数(from_string, archived_rollout_path, set_old_mtime, write_rollout);外部调用 6 个(new, from_u128, assert!, create_dir_all, write, run)。
run_marker_is_removed_unless_persisted376–394 ↗
fn run_marker_is_removed_unless_persisted() -> anyhow::Result<()>
作用:测试压缩运行标记的生命周期:如果只是临时占用,结束时会自动删除;如果明确保留,就会留下来阻止近期重复运行。
数据流:进去的是一个临时 home 目录;它先调用 worker::CompressionRunMarker::try_claim 取得标记但不持久化,作用域结束后标记应消失;再 claim 一次并调用 persist,出来后标记文件存在,后续再 claim 会失败。
调用关系:这个测试直接检查 CompressionRunMarker,也就是 worker 用来节流的“小锁牌”。try_claim 尝试拿牌,persist 决定牌子是否留在磁盘上给下一次运行看。
find_thread_path_by_id_handles_compressed_rollout_filenames397–420 ↗
async fn find_thread_path_by_id_handles_compressed_rollout_filenames() -> anyhow::Result<()>
作用:测试按线程 ID 查找会话文件时,系统能找到压缩后的 rollout 文件名。这样用户恢复某个会话时,不会因为它被压缩就查不到。
数据流:进去的是一个 UUID 对应的会话记录;它写普通文件、压缩,再调用 find_thread_path_by_id_str 查这个 UUID。出来应得到压缩文件路径;如果传入不是 UUID 的字符串,则应返回 None。
调用关系:这个测试覆盖线程查找功能和压缩文件名的配合。compress_now 制造 .jsonl.zst,find_thread_path_by_id_str 负责根据 ID 在目录里定位对应记录。
调用图:调用 4 个内部函数(from_string, compress_now, rollout_path, write_rollout);外部调用 3 个(new, from_u128, assert_eq!)。
find_thread_path_by_id_ignores_compression_temp_matches423–442 ↗
async fn find_thread_path_by_id_ignores_compression_temp_matches() -> anyhow::Result<()>
作用:测试按线程 ID 查找时,不会把压缩过程中的临时文件误认为真正的会话记录。否则用户可能打开一个没写完的半成品。
数据流:进去的是一个文件名看起来含有 rollout 和 UUID、但后缀是压缩临时文件的路径;它把测试记录写进去,然后调用 find_thread_path_by_id_str。出来应是 None,表示这个临时文件被正确忽略。
调用关系:它专门保护查找逻辑的文件名过滤规则。rollout_path 先提供标准名字,再改造成 .tmp 风格的临时名,最后让 find_thread_path_by_id_str 证明自己不会误判。
调用图:调用 3 个内部函数(from_string, rollout_path, write_rollout);外部调用 4 个(new, from_u128, assert_eq!, format!)。
rollout_path444–447 ↗
fn rollout_path(home: &std::path::Path, ts: &str, uuid: Uuid) -> std::path::PathBuf
作用:生成测试用的普通活跃会话文件路径。测试里很多场景都需要同一种标准文件名,避免每个测试自己手写路径。
数据流:进去的是 home 目录、时间戳字符串和 UUID;它把这些拼成 sessions/2025/01/03/rollout-时间-UUID.jsonl。出来的是一个路径对象,不会实际创建文件。
调用关系:它是多个测试的路径小工具,被读取、追加、恢复、搜索、线程查找等测试调用。它只负责统一命名,真正写文件交给 write_rollout。
调用图:被 10 处调用(append_materialization_preserves_compressed_rollout_permissions, append_rollout_item_materializes_compressed_rollout, find_thread_path_by_id_handles_compressed_rollout_filenames, find_thread_path_by_id_ignores_compression_temp_matches, load_rollout_items_reads_compressed_rollout, resume_materializes_compressed_rollout_path, rollout_file_from_path_hides_compressed_sibling_when_plain_exists, rollout_file_from_path_normalizes_compressed_file_names, search_rollout_matches_uses_logical_path_for_compressed_rollout, worker_compresses_old_active_and_archived_rollouts);外部调用 2 个(join, format!)。
archived_rollout_path449–452 ↗
fn archived_rollout_path(home: &std::path::Path, ts: &str, uuid: Uuid) -> std::path::PathBuf
作用:生成测试用的归档会话文件路径。归档文件放在 archived_sessions 下面,用来测试后台压缩旧历史的行为。
数据流:进去的是 home 目录、时间戳字符串和 UUID;它拼出 archived_sessions/rollout-时间-UUID.jsonl。出来的是路径对象,本身不创建目录或文件。
调用关系:它被 worker、权限保留和跳过压缩等测试使用。和 rollout_path 的区别是目录不同,帮助测试同时覆盖活跃会话和归档会话。
调用图:被 5 处调用(compression_preserves_read_only_rollout_permissions, compression_preserves_rollout_permissions, worker_compresses_old_active_and_archived_rollouts, worker_skips_existing_compressed_archived_rollouts, worker_skips_when_fresh_run_marker_exists);外部调用 2 个(join, format!)。
write_rollout454–499 ↗
fn write_rollout(path: &std::path::Path, thread_id: ThreadId, message: &str) -> anyhow::Result<()>
作用:往指定路径写一份最小但完整的测试会话记录。它让每个测试不用重复构造 JSON 行格式和会话元信息。
数据流:进去的是目标路径、线程 ID 和一条用户消息;它先创建父目录,再构造两行 rollout:第一行是会话元信息,第二行是用户消息;然后把它们转成 JSON Lines,也就是一行一个 JSON,写到磁盘。出来是一个真实存在的测试文件。
调用关系:这是整个测试文件最常用的造数工具。各个测试先用 rollout_path 或 archived_rollout_path 得到路径,再用它写内容,之后才交给压缩、读取、搜索、追加或查找逻辑。
调用图:被 13 处调用(append_materialization_preserves_compressed_rollout_permissions, append_rollout_item_materializes_compressed_rollout, compression_preserves_read_only_rollout_permissions, compression_preserves_rollout_permissions, find_thread_path_by_id_handles_compressed_rollout_filenames, find_thread_path_by_id_ignores_compression_temp_matches, load_rollout_items_reads_compressed_rollout, resume_materializes_compressed_rollout_path, rollout_file_from_path_hides_compressed_sibling_when_plain_exists, search_rollout_matches_uses_logical_path_for_compressed_rollout (+3 more));外部调用 8 个(default, parent, format!, create_dir_all, write, UserMessage, EventMsg, SessionMeta)。
compress_now501–511 ↗
fn compress_now(path: &std::path::Path) -> anyhow::Result<()>
作用:立刻把一份普通 rollout 文件压缩成 .zst,并删除原文件。它用于在测试里快速制造“文件已经被压缩”的状态。
数据流:进去的是普通 rollout 路径;它打开原文件,创建对应的压缩文件,用 zstd 编码器把内容复制进去,完成压缩后删除原 .jsonl。出来后磁盘上只剩 .jsonl.zst。
调用关系:它是测试辅助函数,不是被测的后台 worker。许多测试用它跳过等待和扫描过程,直接进入压缩后的场景,再检查读取、追加、恢复、搜索或查找功能。
调用图:被 7 处调用(append_materialization_preserves_compressed_rollout_permissions, append_rollout_item_materializes_compressed_rollout, find_thread_path_by_id_handles_compressed_rollout_filenames, load_rollout_items_reads_compressed_rollout, resume_materializes_compressed_rollout_path, search_rollout_matches_uses_logical_path_for_compressed_rollout, worker_skips_existing_compressed_archived_rollouts);外部调用 6 个(create, open, remove_file, new, copy, new)。
set_old_mtime513–523 ↗
fn set_old_mtime(path: &std::path::Path) -> anyhow::Result<()>
作用:把文件的修改时间改成大约 8 天前。这样测试可以让后台 worker 认为这个文件已经够旧,应该被压缩或清理。
数据流:进去的是一个文件路径;它计算当前时间往前 8 天的时间点,打开文件并设置修改时间。出来后文件内容不变,但磁盘元数据里的修改时间变旧。
调用关系:它服务于所有需要触发“旧文件”规则的测试。worker::run 通常只处理足够旧的文件,所以这些测试先用 set_old_mtime 搭出符合条件的现场。
调用图:被 5 处调用(compression_preserves_read_only_rollout_permissions, compression_preserves_rollout_permissions, worker_compresses_old_active_and_archived_rollouts, worker_skips_existing_compressed_archived_rollouts, worker_skips_when_fresh_run_marker_exists);外部调用 4 个(from_secs, new, now, new)。
rollout/src/metadata_tests.rs源码 ↗
这里测试的是 rollout 文件,也就是一行一条 JSON 的会话流水账。代码会临时造出假的会话文件,再调用真正的元数据提取和回填逻辑,看看结果是不是符合预期。它覆盖了几类容易出错的情况:文件里有 SessionMeta 时要优先用它;没有足够信息时要能从文件名里猜出线程编号和创建时间;回填旧会话时要能从上次停下的位置继续,而不是重头来;已有数据库记录里的 Git 分支不能被随便覆盖,但缺失的 Git 提交号和远端地址要补上;保存前还要把工作目录路径整理成统一格式。可以把它理解成一套“验收清单”:每次改动元数据或回填代码时,这些测试会帮忙发现会不会把老会话、路径、Git 信息弄坏。
extract_metadata_from_rollout_uses_session_meta27–78 ↗
async fn extract_metadata_from_rollout_uses_session_meta()
作用:这个测试确认:当 rollout 文件里写了 SessionMeta,也就是会话的基本说明时,提取逻辑会真正使用这份说明来生成线程元数据。它防止程序退回去乱用文件名或默认值,导致会话信息不准。
数据流:进去的是一个临时目录、一个随机线程编号,以及手写的一行 rollout JSON。测试把这行 JSON 写进文件,然后调用元数据提取函数。出来的是提取结果;测试再用同一份 SessionMeta 手工拼出“应该得到的结果”,补上文件修改时间,最后比较两边完全一致,并确认没有内存模式、也没有解析错误。
调用关系:这是由测试框架直接运行的异步测试。它会准备文件和 SessionMeta,交给 extract_metadata_from_rollout 做真正提取;随后用 builder_from_session_meta、apply_rollout_item 和 file_modified_time_utc 拼出参照答案,用来判断被测流程有没有偏差。
调用图:调用 1 个内部函数(from_string);外部调用 9 个(create, new_v4, default, assert_eq!, format!, SessionMeta, to_string, tempdir, writeln!)。
extract_metadata_from_rollout_returns_latest_memory_mode81–144 ↗
async fn extract_metadata_from_rollout_returns_latest_memory_mode()
作用:这个测试确认:如果同一个 rollout 文件里后来又写入了新的 memory_mode,提取结果会返回最新的一份。memory_mode 可以理解为会话的记忆使用方式;如果拿旧值,后续行为就可能跟用户最后的设置不一致。
数据流:进去的是两行按时间排列的 SessionMeta:第一行没有 memory_mode,第二行带有“polluted”。测试把它们写进同一个 rollout 文件,再调用提取函数。出来的结果里,memory_mode 应该是第二行的“polluted”,说明代码看到了后写入的更新。
调用关系:这是一个面向 extract_metadata_from_rollout 的测试场景。它不关心完整元数据的每个字段,只盯住 memory_mode 这个会随日志更新的字段,确保提取流程在读多行 rollout 时会采用最后出现的值。
调用图:调用 1 个内部函数(from_string);外部调用 8 个(create, new_v4, default, assert_eq!, format!, tempdir, vec!, writeln!)。
builder_from_items_falls_back_to_filename147–173 ↗
fn builder_from_items_falls_back_to_filename()
作用:这个测试确认:如果 rollout 内容本身没有 SessionMeta,代码还能从文件名里兜底拿到线程编号和创建时间。这样即使旧文件或不完整文件缺少元数据,系统也不至于完全无法建档。
数据流:进去的是一个带标准命名格式的 rollout 文件路径,以及一个不含会话信息的普通条目。测试调用 builder_from_items 后,又手工从文件名里的时间和 UUID 算出预期的 ThreadMetadataBuilder。出来的是 builder;测试要求它和预期值一样。
调用关系:这是对 builder_from_items 的兜底规则测试。它用 ThreadMetadataBuilder::new 构造参照物,验证当正文项目帮不上忙时,构建器会把工作交给文件名解析这条备用路线。
调用图:调用 2 个内部函数(from_string, new);外部调用 8 个(from_naive_utc_and_offset, parse_from_str, new_v4, default, assert_eq!, format!, tempdir, vec!)。
backfill_sessions_resumes_from_watermark_and_marks_complete176–242 ↗
async fn backfill_sessions_resumes_from_watermark_and_marks_complete()
作用:这个测试确认:回填旧会话时,会从上次记录的“水位线”之后继续,不会重复处理已经完成的文件;处理完后还会把状态标成完成。水位线可以理解成书签,告诉程序上次看到哪里了。
数据流:进去的是一个临时的 codex_home 目录,里面放了两个 session rollout 文件。测试先初始化状态库,把回填状态标成运行中,并把水位线设到第一个文件,然后等租约过期,再执行 backfill_sessions。出来的结果应该是:第一个线程没有被重新写入,第二个线程被写入;回填状态变成 Complete,最后水位线指向第二个文件,并记录了成功时间。
调用关系:这个测试会调用辅助函数 write_rollout_in_sessions 造两个文件,再通过 StateRuntime 操作状态库,最后把流程交给 backfill_sessions。它验证的是整个回填编排流程:读取水位线、跳过旧文件、写入新文件、更新完成状态。
调用图:调用 3 个内部函数(from_string, write_rollout_in_sessions, init);外部调用 6 个(new_v4, assert!, assert_eq!, from_secs, tempdir, sleep)。
backfill_sessions_preserves_existing_git_branch_and_fills_missing_git_fields245–290 ↗
async fn backfill_sessions_preserves_existing_git_branch_and_fills_missing_git_fields()
作用:这个测试确认:回填时遇到数据库里已经有的线程,不会粗暴覆盖已有 Git 分支,但会补上缺失的 Git 提交号和远端地址。这样既保留数据库里更可信的字段,也能从 rollout 文件补齐空白。
数据流:进去的是一个带 Git 信息的 rollout 文件,以及状态库里一条故意改过的已有线程记录:提交号为空,分支是“sqlite-branch”,远端地址为空。测试运行 backfill_sessions 后再读回这条线程。出来的结果应该是:提交号来自 rollout,分支仍保留数据库原来的值,远端地址也从 rollout 补上。
调用关系:这个测试先用 write_rollout_in_sessions 造文件,再用 extract_metadata_from_rollout 生成一份基础记录并写入 StateRuntime,最后调用 backfill_sessions。它关注的是回填流程和已有数据库记录合并时的取舍规则。
调用图:调用 4 个内部函数(new, from_string, write_rollout_in_sessions, init);外部调用 3 个(new_v4, assert_eq!, tempdir)。
backfill_sessions_normalizes_cwd_before_upsert293–322 ↗
async fn backfill_sessions_normalizes_cwd_before_upsert()
作用:这个测试确认:回填保存线程前,会把 cwd,也就是会话当时的工作目录,整理成数据库期望的标准路径格式。比如路径里有多余的“.”时,不能原样留下来造成同一目录看起来像两个目录。
数据流:进去的是一个 cwd 带有“.”的 rollout 文件。测试初始化状态库,运行 backfill_sessions,然后按线程编号把保存的记录读回来。出来的记录里,rollout_path 应该指向刚才的文件,cwd 应该等于 normalize_cwd_for_state_db 处理后的标准路径。
调用关系:这个测试使用 write_rollout_in_sessions_with_cwd 专门造出带特殊 cwd 的文件,再把它交给 backfill_sessions。最后它用 normalize_cwd_for_state_db 作为参照,确认回填写库之前确实做了路径清理。
调用图:调用 3 个内部函数(from_string, write_rollout_in_sessions_with_cwd, init);外部调用 3 个(new_v4, assert_eq!, tempdir)。
write_rollout_in_sessions324–339 ↗
fn write_rollout_in_sessions(
codex_home: &Path,
filename_ts: &str,
event_ts: &str,
thread_uuid: Uuid,
git: Option<GitInfo>,
) -> PathBuf
作用:这个辅助函数用来快速在 sessions 目录下写一个标准的 rollout 文件。测试里经常需要造假会话文件,有它就不用每个测试重复写一大段文件创建代码。
数据流:进去的是 codex_home、文件名里的时间、事件时间、线程 UUID,以及可选 Git 信息。它把 cwd 默认设成 codex_home,然后把所有参数转交给 write_rollout_in_sessions_with_cwd。出来的是新建 rollout 文件的完整路径。
调用关系:它是更省事的一层包装,被 backfill_sessions_resumes_from_watermark_and_marks_complete 和 backfill_sessions_preserves_existing_git_branch_and_fills_missing_git_fields 调用。真正写文件的活儿交给 write_rollout_in_sessions_with_cwd,它只负责补默认 cwd。
调用图:调用 1 个内部函数(write_rollout_in_sessions_with_cwd);被 2 处调用(backfill_sessions_preserves_existing_git_branch_and_fills_missing_git_fields, backfill_sessions_resumes_from_watermark_and_marks_complete);外部调用 1 个(to_path_buf)。
write_rollout_in_sessions_with_cwd341–384 ↗
fn write_rollout_in_sessions_with_cwd(
codex_home: &Path,
filename_ts: &str,
event_ts: &str,
thread_uuid: Uuid,
cwd: PathBuf,
git: Option<GitInfo>,
) -> PathBuf
作用:这个辅助函数负责真正造出一个可供测试使用的 rollout 文件,并允许测试指定 cwd。它像测试里的“小型会话文件生成器”,保证生成的文件格式和真实系统要读的格式一致。
数据流:进去的是 codex_home、文件名时间、事件时间、线程 UUID、cwd 和可选 Git 信息。它创建 sessions 目录,按 rollout 的命名规则拼出文件名,构造 SessionMeta 和 RolloutLine,把它序列化成 JSON 后写入文件。出来的是这个文件的路径,同时磁盘上多了一个真实的 JSONL rollout 文件。
调用关系:它被 write_rollout_in_sessions 作为底层实现调用,也被 backfill_sessions_normalizes_cwd_before_upsert 直接调用,以便定制 cwd。它的产物随后会被 backfill_sessions 或 extract_metadata_from_rollout 读取,用来模拟真实用户留下的会话记录。
调用图:调用 1 个内部函数(from_string);被 2 处调用(backfill_sessions_normalizes_cwd_before_upsert, write_rollout_in_sessions);外部调用 9 个(create, join, to_string, default, format!, SessionMeta, to_string, create_dir_all, writeln!)。
rollout/src/session_index_tests.rs源码 ↗
会话索引可以理解成一本不断往后追加的登记簿:同一个会话可能登记多次,越靠后的记录越新。这个测试文件专门检查程序读这本登记簿时是不是从最后往前找,而不是被旧记录骗到。它还模拟真实的会话记录文件,也就是 rollout 文件,确认查找会话元数据时会跳过“只有索引但没有实际文件”的记录,以及空文件这种半成品记录。测试都放在临时目录里跑,不会碰用户真实数据。辅助函数负责写假索引和假会话文件,各个测试再用这些假数据验证查找函数的行为。
write_index13–20 ↗
fn write_index(path: &Path, lines: &[SessionIndexEntry]) -> std::io::Result<()>
作用:这个辅助函数把几条假的会话索引记录写成一个测试用索引文件。测试需要先造出一份“小登记簿”,后面才能检查查找逻辑是否正确。
数据流:进去的是一个文件路径和一组 SessionIndexEntry 记录;它把每条记录转成一行 JSON 文本,JSON 是一种常见的文本数据格式,然后每行后面加换行;出来的是磁盘上的索引文件,如果写入失败就把错误交回给调用者。
调用关系:它是多个测试的准备工具。像 find_thread_id_by_name_prefers_latest_entry、find_thread_name_by_id_prefers_latest_entry、scan_index_returns_none_when_entry_missing 等测试都会先调用它造好索引,再去调用真正被测的查找函数。它自己把具体写文件的活交给标准库的 write。
调用图:被 8 处调用(find_thread_id_by_name_prefers_latest_entry, find_thread_meta_by_name_str_ignores_historical_name_after_rename, find_thread_meta_by_name_str_skips_newest_entry_without_rollout, find_thread_meta_by_name_str_skips_partial_rollout, find_thread_name_by_id_prefers_latest_entry, find_thread_names_by_ids_prefers_latest_entry, scan_index_finds_latest_match_among_mixed_entries, scan_index_returns_none_when_entry_missing);外部调用 3 个(new, to_string, write)。
write_rollout_with_metadata22–51 ↗
fn write_rollout_with_metadata(path: &Path, thread_id: ThreadId) -> std::io::Result<()>
作用:这个辅助函数写出一个带会话元数据的假 rollout 文件。rollout 文件可以理解成某次会话的详细记录,里面至少要有“这个会话是谁”的信息,查找元数据的测试才有真实东西可读。
数据流:进去的是一个文件路径和一个 ThreadId,也就是会话编号;它组装一条 SessionMeta 元数据记录,填入固定时间、工作目录、来源、版本等测试值,再转成 JSON 写到文件里;出来的是一个看起来像真实会话记录的文件,或一个写入错误。
调用关系:它被三个检查会话元数据查找的异步测试使用。这些测试先用它造出可读取的 rollout 文件,再故意制造缺文件、空文件、改名记录等场景,最后交给 find_thread_meta_by_name_str 去验证。
调用图:被 3 处调用(find_thread_meta_by_name_str_ignores_historical_name_after_rename, find_thread_meta_by_name_str_skips_newest_entry_without_rollout, find_thread_meta_by_name_str_skips_partial_rollout);外部调用 4 个(format!, SessionMeta, to_string, write)。
find_thread_id_by_name_prefers_latest_entry54–76 ↗
fn find_thread_id_by_name_prefers_latest_entry() -> std::io::Result<()>
作用:这个测试确认:如果同一个会话名字在索引里出现多次,按名字找会话编号时应该选最后追加的那条。这样用户看到的名字不会指向过期会话。
数据流:它先创建临时目录和索引路径,再生成两个不同的会话编号,给它们写入相同名字但不同时间的索引记录;随后从索引末尾开始查找名字为 same 的记录;最后断言找到的是第二个编号,也就是更靠后的记录。
调用关系:测试运行器会调用它。它把造索引的工作交给 write_index,然后验证 scan_index_from_end 的行为是否符合“从登记簿最后一页往前找”的规则。
调用图:调用 2 个内部函数(new, write_index);外部调用 3 个(new, assert_eq!, vec!)。
find_thread_meta_by_name_str_skips_newest_entry_without_rollout79–112 ↗
async fn find_thread_meta_by_name_str_skips_newest_entry_without_rollout() -> std::io::Result<()>
作用:这个测试确认:如果最新的名字记录只有索引、没有真正的会话文件,它不能挡住更早但已经保存好的会话。否则程序可能会找到一个根本打不开的会话。
数据流:它创建一个临时环境,先为 saved_id 写出真实 rollout 文件,再写入两条同名索引:旧的一条有文件,新的一条没有文件;然后按名字 same 查找会话元数据;最后断言结果回到旧的真实文件,并且元数据里的编号是 saved_id。
调用关系:测试运行器通过异步测试框架调用它。它先调用 write_rollout_with_metadata 造真实会话文件,再调用 write_index 造索引,最后把检查重点交给 find_thread_meta_by_name_str,看它会不会跳过没有 rollout 文件的新记录。
调用图:调用 3 个内部函数(new, write_index, write_rollout_with_metadata);外部调用 5 个(new, assert_eq!, format!, create_dir_all, vec!)。
find_thread_meta_by_name_str_skips_partial_rollout115–146 ↗
async fn find_thread_meta_by_name_str_skips_partial_rollout() -> std::io::Result<()>
作用:这个测试确认:如果最新会话文件存在但内容是空的,也要把它当成没保存完整,继续找更早的可用会话。这样可以避免程序读到半截文件后误判。
数据流:它在临时目录里建一个会话目录,写一个正常 rollout 文件,再写一个空的 rollout 文件;索引里两条记录名字相同,空文件那条更新;然后按名字查找元数据;最后断言返回的是正常文件路径,而不是空文件。
调用关系:它由测试运行器启动。它调用 write_rollout_with_metadata 写完整文件,直接用文件写入函数制造空文件,再调用 write_index 写索引,最终检查 find_thread_meta_by_name_str 对“半成品会话记录”的容错能力。
调用图:调用 3 个内部函数(new, write_index, write_rollout_with_metadata);外部调用 6 个(new, assert_eq!, format!, create_dir_all, write, vec!)。
find_thread_meta_by_name_str_ignores_historical_name_after_rename149–184 ↗
async fn find_thread_meta_by_name_str_ignores_historical_name_after_rename() -> std::io::Result<()>
作用:这个测试确认:一个会话改名以后,它过去用过的旧名字不应该继续占用查找结果。否则用户按旧名字找,可能会被已经改名的会话误导。
数据流:它生成两个会话编号:一个代表后来改名的会话,一个代表当前仍叫 same 的会话;索引先记录改名前的 same,再记录当前会话的 same,最后记录第一个会话改成 different;然后按 same 查找元数据;结果应该指向当前仍叫 same 的会话文件。
调用关系:测试运行器会调用它。它用 write_rollout_with_metadata 准备当前会话的真实文件,用 write_index 写出带改名历史的索引,再让 find_thread_meta_by_name_str 证明自己会避开已经改名的历史记录。
调用图:调用 3 个内部函数(new, write_index, write_rollout_with_metadata);外部调用 5 个(new, assert_eq!, format!, create_dir_all, vec!)。
find_thread_name_by_id_prefers_latest_entry187–211 ↗
fn find_thread_name_by_id_prefers_latest_entry() -> std::io::Result<()>
作用:这个测试确认:按会话编号查名字时,如果同一个编号出现多次,应该返回最新名字。它主要保护“重命名会话”这个场景。
数据流:它创建一个会话编号,给这个编号写两条索引记录,名字先是 first 后是 second;然后按编号从索引末尾往前查;最后断言拿到的名字是 second。
调用关系:测试运行器调用它。它先用 write_index 写测试索引,再检查 scan_index_from_end_by_id 是否按追加顺序取最新记录。
调用图:调用 2 个内部函数(new, write_index);外部调用 3 个(new, assert_eq!, vec!)。
scan_index_returns_none_when_entry_missing214–231 ↗
fn scan_index_returns_none_when_entry_missing() -> std::io::Result<()>
作用:这个测试确认:索引里没有目标名字或目标编号时,查找函数应该明确返回“没找到”,而不是随便返回一条记录。这样上层代码才能安全处理不存在的会话。
数据流:它只写入一条名字为 present 的索引记录;接着按不存在的名字 missing 查一次,又按一个全新的会话编号查一次;两次结果都应该是 None,也就是没有结果。
调用关系:测试运行器调用它。它借助 write_index 准备最小索引,然后分别验证 scan_index_from_end 和 scan_index_from_end_by_id 在查不到时的表现。
调用图:调用 2 个内部函数(new, write_index);外部调用 3 个(new, assert_eq!, vec!)。
find_thread_names_by_ids_prefers_latest_entry234–269 ↗
async fn find_thread_names_by_ids_prefers_latest_entry() -> std::io::Result<()>
作用:这个测试确认:一次查询多个会话编号的名字时,每个编号都应该拿到自己的最新名字。它防止批量显示会话列表时出现旧名字。
数据流:它写入三条索引:id1 的旧名字、id2 的名字、id1 的新名字;然后把 id1 和 id2 放进一个集合里请求批量查找;最后断言返回的映射表里,id1 对应 latest,id2 对应 other。
调用关系:这是一个异步测试,由测试运行器启动。它用 write_index 准备索引,再检查 find_thread_names_by_ids 这个批量查找函数是否和单个查找一样遵守“最新记录优先”。
调用图:调用 2 个内部函数(new, write_index);外部调用 5 个(new, new, new, assert_eq!, vec!)。
scan_index_finds_latest_match_among_mixed_entries272–313 ↗
fn scan_index_finds_latest_match_among_mixed_entries() -> std::io::Result<()>
作用:这个测试确认:索引里夹杂很多记录时,查找仍然按文件追加顺序选最新匹配项,而不是单纯看 updated_at 这个时间字符串。它把“最后写入才算最新”这条规则钉牢。
数据流:它准备多个会话编号和多条混合索引记录,其中目标名字 target 出现多次,不同编号也混在一起;然后分别按名字、按目标编号、按另一个编号查找;最后断言每次都拿到从文件末尾倒着看时遇到的第一条匹配记录。
调用关系:测试运行器会调用它。它用 write_index 造出复杂索引,再同时验证 scan_index_from_end 和 scan_index_from_end_by_id 在混合数据里的选择规则。
调用图:调用 2 个内部函数(new, write_index);外部调用 3 个(new, assert_eq!, vec!)。
rollout/src/state_db_tests.rs源码 ↗
这不是产品运行时直接使用的代码,而是给开发者和持续集成系统用的安全网。项目里有一种叫 rollout 的会话日志文件,里面按行记录一次对话发生了什么;还有一个状态数据库,用来更快地查询这些会话。这个测试文件模拟真实场景:先造临时目录和假会话文件,再初始化状态数据库,然后检查结果是否符合预期。它验证三类容易出问题的地方:第一,文件名或游标里的时间格式要能被统一成标准时间;第二,如果另一个任务正在做“回填”(把旧日志补进数据库),初始化过程要等它完成,但不能无限等;第三,重新扫描 rollout 文件时,如果数据库里已经有用户明确设置的标题,就不能随便用首条用户消息覆盖它。可以把它理解成一组验收题,防止以后改代码时把这些细节弄坏。
cursor_to_anchor_normalizes_timestamp_format19–31 ↗
fn cursor_to_anchor_normalizes_timestamp_format()
作用:这个测试确认一种带短横线的时间字符串能被正确识别,并转换成统一的 UTC 时间。它防止列表分页或定位历史记录时,因为时间格式不一致而找错位置。
数据流:输入是一段字符串时间“2026-01-27T12-34-56”。测试先用 parse_cursor 把它解析成游标,再把游标交给 cursor_to_anchor 转成数据库查询用的锚点;同时用 chrono 的 parse_from_str 和 from_naive_utc_and_offset 手工算出应该得到的 UTC 时间。最后用 assert_eq! 比较两边,确认锚点里的时间已经被规范化,并且纳秒被清零。
调用关系:它是独立的小测试,专门盯住“游标 → 查询锚点”这条转换链。它会调用 parse_cursor 解析外部传入的游标格式,也用标准时间库算一个对照答案,最后用断言证明项目自己的转换没有跑偏。
调用图:调用 1 个内部函数(parse_cursor);外部调用 3 个(from_naive_utc_and_offset, parse_from_str, assert_eq!)。
try_init_waits_for_concurrent_startup_backfill34–63 ↗
async fn try_init_waits_for_concurrent_startup_backfill() -> anyhow::Result<()>
作用:这个测试确认:如果启动时发现另一个任务已经在做数据库回填,新的初始化过程会耐心等它完成,而不是抢着重复做或读到半成品数据。
数据流:测试先创建一个临时 home 目录,再初始化 StateRuntime,也就是状态数据库运行环境。它让这个运行环境拿到回填租约,表示“我正在回填”;随后启动一个异步小任务,等 25 毫秒后把回填标记为完成。与此同时,测试调用 try_init_with_roots_and_backfill_lease 再次初始化同一个目录。结果应该是:第二次初始化没有失败,而是等到回填完成;最后读取回填状态,确认状态已经是 Complete。
调用关系:它模拟两个启动流程撞在一起的情况。测试本身先用 init 建好数据库,再用 tokio::spawn 启动后台任务;后台任务通过 sleep 和 mark_backfill_complete 模拟另一个进程完成工作。主流程调用 try_init_with_roots_and_backfill_lease,验证这个初始化函数会正确等待并接上后续流程。
调用图:调用 1 个内部函数(init);外部调用 6 个(new, assert!, assert_eq!, from_millis, spawn, sleep)。
try_init_times_out_waiting_for_stuck_startup_backfill66–92 ↗
async fn try_init_times_out_waiting_for_stuck_startup_backfill() -> anyhow::Result<()>
作用:这个测试确认:如果已有回填任务卡住不结束,初始化不会永远等下去,而是会在超时后报错。这样程序不会因为一个坏状态而一直挂住。
数据流:测试先创建临时目录并初始化状态数据库,然后让它成功领取回填租约,但故意不标记完成。接着调用 try_init_with_roots_and_backfill_lease 尝试再次初始化。因为回填一直没有结束,函数应该返回错误;测试把错误取出来,并检查错误文字里包含“timed out waiting for state db backfill”。如果函数反而成功了,就用 panic! 让测试失败。
调用关系:它和前一个测试是一正一反:前一个证明“别人很快做完时要等”,这个证明“别人卡住时不能无限等”。它用 init 和 try_claim_backfill 制造一个被占用的回填状态,然后把 try_init_with_roots_and_backfill_lease 放进这个坏场景里,检查它是否有超时保护。
reconcile_rollout_preserves_existing_explicit_title95–130 ↗
async fn reconcile_rollout_preserves_existing_explicit_title() -> anyhow::Result<()>
作用:这个测试确认:重新扫描 rollout 会话文件时,不会覆盖数据库里已经明确保存的标题。它保护用户或系统后来设置的标题,避免被旧日志里的第一句话改回去。
数据流:测试先创建临时目录和一个新的 thread_id,再调用 write_rollout_with_user_message 写出一个假的会话日志,里面第一条用户消息是“Hey”。然后初始化状态数据库,从这个 rollout 文件提取元数据,确认默认标题和第一条用户消息都是“Hey”。接着测试把标题改成“math”并写入数据库。之后调用 reconcile_rollout 重新协调文件和数据库。最后再从数据库读出这个会话,确认标题仍然是“math”,而 first_user_message 仍然是“Hey”。
调用关系:它覆盖的是“rollout 文件重新导入/同步到状态数据库”的流程。它先让辅助函数 write_rollout_with_user_message 准备测试材料,再调用 extract_metadata_from_rollout 模拟正常解析,接着用 StateRuntime::init 和 upsert_thread 把显式标题写进数据库,最后通过 reconcile_rollout 验证同步逻辑尊重已有标题。
调用图:调用 4 个内部函数(new, extract_metadata_from_rollout, write_rollout_with_user_message, init);外部调用 2 个(new, assert_eq!)。
write_rollout_with_user_message132–181 ↗
fn write_rollout_with_user_message(
home: &Path,
thread_id: ThreadId,
message: &str,
) -> anyhow::Result<std::path::PathBuf>
作用:这个辅助函数专门给测试造一个最小可用的 rollout 日志文件。它把一段用户消息写成项目真实会读取的 JSONL 文件,方便测试不用依赖现成数据。
数据流:输入是临时 home 目录、一个 thread_id,以及要写入的用户消息。函数先创建类似 sessions/2026/06/01 的目录,再拼出一个带时间和 thread_id 的 rollout 文件名。然后它组装两行记录:第一行是会话元信息,包含线程编号、时间、工作目录、来源和模型提供方;第二行是用户消息事件。最后把这些结构转成 JSON,每行一条写入文件,并返回写好的文件路径。
调用关系:它被 reconcile_rollout_preserves_existing_explicit_title 调用,用来搭建测试现场。这个函数自己不验证业务结果,只负责把 SessionMeta、EventMsg、UserMessageEvent 等数据包装成真实 rollout 文件的样子;后面的元数据提取和 reconcile_rollout 才会读取这个文件并接受测试。
调用图:被 1 处调用(reconcile_rollout_preserves_existing_explicit_title);外部调用 9 个(default, join, to_path_buf, format!, UserMessage, EventMsg, SessionMeta, create_dir_all, write)。
rollout/src/recorder_tests.rs源码 ↗
这份文件不是真正给产品运行时调用的代码,而是给开发者和持续集成系统跑的“体检清单”。它会临时造出假的 Codex 主目录、会话文件和状态数据库,然后模拟各种容易出问题的场景:第一次启动时要不要先把旧的 JSONL 记录补进数据库;读旧记录时要不要跳过已经废弃的 ghost_snapshot;写文件失败后会不会保留待写内容并重试;列出历史会话时,文件系统和 SQLite 状态库(一种本地小数据库)信息不一致该信谁。它还特别检查“延迟落盘”行为:记录器一开始不急着创建文件,等真的有内容或 flush(把缓存写出去)时才生成。整体像是在测试一个账本系统:既要能补旧账,也要能发现账本索引错了,还不能因为一次写入失败就把账弄丢。
test_config29–37 ↗
fn test_config(codex_home: &Path) -> RolloutConfig
作用:创建一份专门给测试用的 RolloutConfig,也就是会话记录器需要的基本设置。它把所有路径都指向临时目录,避免测试碰到用户真实文件。
数据流:输入一个临时主目录路径 → 把这个路径复制成 codex_home、sqlite_home 和当前工作目录等配置项,并填入固定的测试模型提供方名称 → 输出一份可以交给记录器或状态数据库初始化使用的配置对象,不改动磁盘。
调用关系:很多测试在开始时都会先调用它,相当于给后续的 RolloutRecorder、state_db 初始化和会话列表查询准备同一套干净环境。它只做准备工作,不把活儿继续交给别的项目函数。
调用图:被 10 处调用(list_threads_db_disabled_does_not_skip_paginated_items, list_threads_db_enabled_drops_missing_rollout_paths, list_threads_db_enabled_repairs_stale_rollout_paths, list_threads_default_filter_returns_filesystem_scan_results, list_threads_metadata_filter_overlays_state_db_list_metadata, list_threads_search_repairs_stale_state_db_hits_before_returning, list_threads_state_db_only_skips_jsonl_repair_scan, persist_reports_filesystem_error_and_retries_buffered_items, recorder_materializes_on_flush_with_pending_items, state_db_init_backfills_before_returning);外部调用 1 个(to_path_buf)。
write_session_file39–69 ↗
fn write_session_file(root: &Path, ts: &str, uuid: Uuid) -> std::io::Result<PathBuf>
作用:在临时目录里写一个最小可用的会话记录文件。测试用它快速造出“历史会话”,方便后面检查扫描、修复和过滤逻辑。
数据流:输入根目录、时间戳字符串和会话 UUID → 创建 sessions/2025/01/03 这样的目录,写入一行 session_meta 和一行 user_message 的 JSONL 内容 → 输出新建文件的路径,同时磁盘上多了一个假的会话文件。
调用关系:它是多个列表类测试的造数工具。后续 RolloutRecorder::list_threads 或 resume_candidate_matches_cwd 会读取它写出的文件,看看系统能不能正确发现这个会话。
调用图:被 6 处调用(list_threads_db_disabled_does_not_skip_paginated_items, list_threads_db_enabled_repairs_stale_rollout_paths, list_threads_default_filter_returns_filesystem_scan_results, list_threads_metadata_filter_overlays_state_db_list_metadata, list_threads_search_repairs_stale_state_db_hits_before_returning, resume_candidate_matches_cwd_reads_latest_turn_context);外部调用 6 个(create, join, format!, create_dir_all, json!, writeln!)。
state_db_init_backfills_before_returning72–145 ↗
async fn state_db_init_backfills_before_returning() -> anyhow::Result<()>
作用:检查状态数据库初始化时,会不会在返回前先把已有会话文件补录进去。这样界面刚启动就能看到旧会话,不会出现数据库还没补完、列表暂时缺东西的情况。
数据流:测试先在临时目录写出一个带 session_meta 和用户消息的 rollout 文件 → 调用 crate::state_db::init 初始化数据库 → 再按 thread_id 查询数据库,确认会话已经存在,并确认回填状态是完成。
调用关系:它调用 test_config 准备配置,然后把重点交给 state_db::init。测试结束时通过运行时对象查询线程和回填状态,验证初始化流程没有提前返回。
调用图:调用 3 个内部函数(from_string, test_config, init);外部调用 11 个(default, new, new_v4, new, assert_eq!, format!, create_dir_all, write, UserMessage, EventMsg (+1 more))。
load_rollout_items_skips_legacy_ghost_snapshot_lines148–220 ↗
async fn load_rollout_items_skips_legacy_ghost_snapshot_lines() -> std::io::Result<()>
作用:检查读取旧会话文件时,会跳过已经废弃的 ghost_snapshot 行。这样旧数据里的历史包袱不会污染现在的会话内容。
数据流:测试写入三行 JSONL:会话元信息、旧 ghost_snapshot、正常助手消息 → 调用 RolloutRecorder::load_rollout_items 读取 → 得到的结果只保留元信息和正常消息,线程 ID 正确,解析错误数为 0。
调用关系:它直接测试 RolloutRecorder::load_rollout_items 的兼容旧文件能力。这个读取函数在恢复或列出会话时很关键,测试确保它遇到特定旧格式时是“安静跳过”而不是报错。
调用图:调用 2 个内部函数(new, load_rollout_items);外部调用 5 个(create, new, assert!, assert_eq!, writeln!)。
load_rollout_items_preserves_legacy_guardian_assessment_lines223–282 ↗
async fn load_rollout_items_preserves_legacy_guardian_assessment_lines() -> std::io::Result<()>
作用:检查旧格式里的 guardian_assessment 记录仍然能被保留下来。也就是说,系统只丢弃明确废弃的内容,不会把仍有意义的旧记录误删。
数据流:测试写入会话元信息和一条 guardian_assessment 事件 → 调用 RolloutRecorder::load_rollout_items → 得到两条记录,并确认 guardian_assessment 的 id、turn_id 等字段被正确读出,缺失的 started_at_ms 被补成默认值 0。
调用关系:它和 ghost_snapshot 的测试形成对照:同样是旧数据,load_rollout_items 应该区分哪些要跳过、哪些要保留。
调用图:调用 2 个内部函数(new, load_rollout_items);外部调用 5 个(create, new, assert_eq!, panic!, writeln!)。
load_rollout_items_filters_legacy_ghost_snapshots_from_compaction_history285–362 ↗
async fn load_rollout_items_filters_legacy_ghost_snapshots_from_compaction_history() -> std::io::Result<()>
作用:检查压缩历史里的旧 ghost_snapshot 也会被过滤掉。否则即使顶层记录跳过了,藏在摘要历史里的废弃内容仍可能重新冒出来。
数据流:测试写入会话元信息和一条 compacted 记录,其中 replacement_history 同时包含正常消息和 ghost_snapshot → 调用 RolloutRecorder::load_rollout_items → 输出的 compacted 记录里只剩正常消息,解析错误为 0。
调用关系:它继续覆盖 RolloutRecorder::load_rollout_items 的旧数据清理逻辑,只是这次 ghost_snapshot 不在最外层,而是藏在压缩记录内部。
调用图:调用 2 个内部函数(new, load_rollout_items);外部调用 6 个(create, new, assert!, assert_eq!, panic!, writeln!)。
recorder_materializes_on_flush_with_pending_items365–443 ↗
async fn recorder_materializes_on_flush_with_pending_items() -> std::io::Result<()>
作用:检查记录器在有待写内容并执行 flush 时,才真正创建会话文件。这样空会话不会提前生成垃圾文件,同时已有缓存也不会丢。
数据流:测试创建 RolloutRecorder 后先确认文件不存在 → 记录一条助手消息并 flush → 文件被创建;再记录用户消息并 flush、persist 两次 → 文件里包含会话元信息,两条消息顺序正确,第二次 persist 不会重复写内容。
调用关系:它调用 test_config 和 RolloutRecorder::new 搭好记录器,再调用 record_canonical_items、flush、persist、shutdown。这个测试把记录器从“延迟状态”推到“真实落盘状态”,验证整个写入流程。
调用图:调用 5 个内部函数(default, new, new, new, test_config);外部调用 9 个(default, new, new, assert!, assert_eq!, AgentMessage, UserMessage, EventMsg, read_to_string)。
persist_reports_filesystem_error_and_retries_buffered_items446–497 ↗
async fn persist_reports_filesystem_error_and_retries_buffered_items() -> std::io::Result<()>
作用:检查 persist 遇到文件系统错误时,会把错误报告出来,并保留还没写成功的内容。等问题解除后,再 flush 应该能把之前的内容补写进去。
数据流:测试先缓存一条消息 → 故意创建一个名为 sessions 的普通文件,挡住记录器创建 sessions 目录 → persist 返回错误且 rollout 文件不存在 → 删除这个阻碍文件后 flush → 最终文件里能看到之前缓存的消息。
调用关系:它通过 test_config 和 RolloutRecorder::new 创建记录器,然后刻意制造磁盘路径冲突来考验 persist。之后再调用 flush,验证失败后的重试链路没有丢数据。
调用图:调用 5 个内部函数(default, new, new, new, test_config);外部调用 9 个(create, new, new, assert!, assert_ne!, remove_file, AgentMessage, EventMsg, read_to_string)。
writer_state_retries_write_error_before_reporting_flush_success500–527 ↗
async fn writer_state_retries_write_error_before_reporting_flush_success() -> std::io::Result<()>
作用:检查底层写入状态遇到写文件错误时,会重新打开文件并重试,而不是假装 flush 成功。这样能防止“程序说写好了,其实没写进去”的危险情况。
数据流:测试创建一个 rollout 文件,并用只读方式打开它作为错误的写入句柄 → 把一条消息加入 RolloutWriterState 缓存 → 调用 flush → 状态对象重新处理写入,最后磁盘文件包含这条消息。
调用关系:它绕过高层 RolloutRecorder,直接测试 RolloutWriterState::flush 的容错能力。这个底层部件是记录器真正写 JSONL 文件时依赖的零件。
调用图:调用 1 个内部函数(new);外部调用 7 个(create, new, assert!, new, read_to_string, from_std, vec!)。
list_threads_db_disabled_does_not_skip_paginated_items530–574 ↗
async fn list_threads_db_disabled_does_not_skip_paginated_items() -> std::io::Result<()>
作用:检查不用状态数据库、只扫文件系统列会话时,翻页不会跳过项目。用户点“下一页”时,应该看到紧接着的下一条,而不是漏掉一条。
数据流:测试用 write_session_file 写出新、中、旧三个会话文件 → 第一次 list_threads 请求每页 1 条,拿到最新的文件和游标 → 第二次带游标请求下一页,拿到中间那个文件。
调用关系:它调用 test_config 和 write_session_file 造环境,然后调用 RolloutRecorder::list_threads。这里特意传入 None 作为状态数据库上下文,测试纯文件扫描路径。
调用图:调用 3 个内部函数(list_threads, test_config, write_session_file);外部调用 3 个(new, from_u128, assert_eq!)。
list_threads_db_enabled_drops_missing_rollout_paths577–639 ↗
async fn list_threads_db_enabled_drops_missing_rollout_paths() -> std::io::Result<()>
作用:检查状态数据库里如果记录了一个已经不存在的 rollout 文件,列表结果会把它去掉,并清理数据库里的旧路径。这样用户不会看到点不开的历史会话。
数据流:测试往状态数据库插入一条指向不存在文件的会话元数据 → 调用 RolloutRecorder::list_threads → 返回列表为空;再查数据库,发现这个 thread_id 的路径也被清掉了。
调用关系:它用 test_config 准备配置,用 StateRuntime::init 建数据库,再把 stale_path 写入数据库。list_threads 负责发现文件不存在,并把修复动作传回状态数据库。
调用图:调用 5 个内部函数(from_string, list_threads, test_config, new, init);外部调用 4 个(new, from_u128, assert_eq!, format!)。
list_threads_db_enabled_repairs_stale_rollout_paths642–707 ↗
async fn list_threads_db_enabled_repairs_stale_rollout_paths() -> std::io::Result<()>
作用:检查数据库里的 rollout 路径过期但磁盘上能找到真实文件时,列表会自动修好路径。这样旧索引不会让系统找错文件。
数据流:测试先写出真实会话文件,又往数据库塞入同一个 thread_id 但错误路径的记录 → 调用 RolloutRecorder::list_threads → 返回的项目指向真实路径;之后查询数据库,也已经更新成真实路径。
调用关系:它用 write_session_file 造真实文件,用 StateRuntime 写入过期元数据。RolloutRecorder::list_threads 在列会话时同时做校验和修复。
调用图:调用 6 个内部函数(from_string, list_threads, test_config, write_session_file, new, init);外部调用 4 个(new, from_u128, assert_eq!, format!)。
list_threads_state_db_only_skips_jsonl_repair_scan710–805 ↗
async fn list_threads_state_db_only_skips_jsonl_repair_scan() -> std::io::Result<()>
作用:检查只从状态数据库列会话时,不会偷偷扫描 JSONL 文件来修复缺失记录。这个模式更快,但只相信数据库里已有的东西。
数据流:测试先创建空的状态数据库并标记回填完成,再手工写一个数据库不知道的会话 JSONL 文件 → 调用 list_threads_from_state_db 得到 0 条 → 调用完整的 list_threads 后扫描到文件并修复数据库 → 再调用 list_threads_from_state_db 就能看到 1 条。
调用关系:它同时对比 RolloutRecorder::list_threads_from_state_db 和 RolloutRecorder::list_threads。前者是“只看索引”,后者是“会扫文件并补索引”。
调用图:调用 4 个内部函数(list_threads, list_threads_from_state_db, test_config, init);外部调用 8 个(create, new, from_u128, assert_eq!, format!, create_dir_all, json!, writeln!)。
list_threads_default_filter_returns_filesystem_scan_results808–896 ↗
async fn list_threads_default_filter_returns_filesystem_scan_results() -> std::io::Result<()>
作用:检查默认列表路径会以文件系统扫描结果为准,并修正数据库中过时的筛选信息。特别是工作目录 cwd 这类字段,数据库如果旧了,不能让筛选结果误中。
数据流:测试写出真实会话文件,但在数据库里把它的 cwd 写成一个过期目录 → 只查数据库时,按过期 cwd 能查到 1 条 → 调用完整 list_threads 扫描真实文件后,按这个过期 cwd 查不到 → 再只查数据库也变成查不到,说明状态已被修正。
调用关系:它用 write_session_file、StateRuntime 和两个列表函数共同构造对比。重点是证明 list_threads 不只是读数据库,还会用文件内容校准数据库。
调用图:调用 7 个内部函数(from_string, list_threads, list_threads_from_state_db, test_config, write_session_file, new, init);外部调用 3 个(new, from_u128, assert_eq!)。
list_threads_metadata_filter_overlays_state_db_list_metadata899–963 ↗
async fn list_threads_metadata_filter_overlays_state_db_list_metadata() -> std::io::Result<()>
作用:检查列表结果在需要时会叠加状态数据库里的补充元数据,比如 Git 分支、提交号和远程仓库地址。这样列表页能显示更丰富的信息。
数据流:测试写出一个真实会话文件,再往数据库为同一个会话填入 git_branch、git_sha、git_origin_url 等字段 → 调用 RolloutRecorder::list_threads 并按来源过滤 → 返回的项目路径来自真实文件,同时 Git 字段来自数据库。
调用关系:它用 write_session_file 提供文件基础信息,用 StateRuntime 提供额外元数据。list_threads 负责把两边的信息合并成最终 ThreadItem。
调用图:调用 6 个内部函数(from_string, list_threads, test_config, write_session_file, new, init);外部调用 3 个(new, from_u128, assert_eq!)。
fill_missing_thread_item_metadata_preserves_identity_and_prefers_state_git_fields966–1031 ↗
fn fill_missing_thread_item_metadata_preserves_identity_and_prefers_state_git_fields()
作用:检查补全 ThreadItem 元数据时,不会改掉文件系统确认的身份信息,但会优先采用状态数据库里的 Git 信息。这样既不认错会话,又能拿到较新的展示字段。
数据流:测试准备一个来自文件系统的 item 和一个来自状态数据库的 state_item → 调用 fill_missing_thread_item_metadata → item 的 path、thread_id、首条用户消息等身份和核心文本保持原样;缺失字段被补上;Git 分支、提交号和远程地址改用数据库里的值。
调用关系:这是一个普通同步测试,直接验证 fill_missing_thread_item_metadata 这个合并小工具。它服务于会话列表把文件扫描结果和数据库结果合成一条展示记录的过程。
调用图:调用 1 个内部函数(new);外部调用 2 个(from, assert_eq!)。
list_threads_search_repairs_stale_state_db_hits_before_returning1034–1121 ↗
async fn list_threads_search_repairs_stale_state_db_hits_before_returning() -> std::io::Result<()>
作用:检查搜索命中数据库里的旧记录时,完整列表流程会先用真实文件校验,发现不该命中就修掉再返回。这样搜索结果不会因为数据库里的过期标题而显示错误会话。
数据流:测试写出真实会话文件,但在数据库里把标题设成包含 needle 的旧文本 → 只查数据库搜索 needle 能命中 1 条 → 调用完整 list_threads 搜索时返回 0 条,并修复数据库 → 再只查数据库搜索也返回 0 条。
调用关系:它对比 list_threads_from_state_db 和 list_threads。前者暴露过期索引会命中的问题,后者负责读取文件、判断真实内容,并把数据库修正。
调用图:调用 7 个内部函数(from_string, list_threads, list_threads_from_state_db, test_config, write_session_file, new, init);外部调用 3 个(new, from_u128, assert_eq!)。
resume_candidate_matches_cwd_reads_latest_turn_context1124–1168 ↗
async fn resume_candidate_matches_cwd_reads_latest_turn_context() -> std::io::Result<()>
作用:检查判断一个会话是否适合恢复时,会读取最新的 turn context(一次对话轮次的上下文),而不是只看旧的会话元信息。这样用户在工作目录变化后,恢复候选仍能按最新目录判断。
数据流:测试先写一个普通会话文件,再追加一条 TurnContextItem,其中 cwd 是 latest_cwd → 调用 resume_candidate_matches_cwd,传入旧 cwd 和期望的 latest_cwd → 返回 true,说明函数看到了最新轮次上下文。
调用关系:它用 write_session_file 造基础文件,再追加 TurnContextItem。最后调用 resume_candidate_matches_cwd,验证恢复会话候选筛选逻辑会深入读取 rollout 内容。
调用图:调用 1 个内部函数(write_session_file);外部调用 8 个(new, from_u128, new_read_only_policy, assert!, create_dir_all, TurnContext, new, writeln!)。
rollout/src/tests.rs源码 ↗
项目会把一次对话保存成一个 JSONL 文件,也就是“一行一个 JSON 记录”的文本文件;同时也可能把会话路径等信息写进状态数据库。这个测试文件专门造出各种临时会话文件和临时数据库,然后检查主代码是否能按预期工作:数据库里的路径过期时,要能回头从文件夹里找到正确文件并修好数据库;列会话时,要按新到旧排序、支持分页游标、按来源和模型提供商过滤;摘要里要能读到第一条用户消息,或者在目标型会话里用目标文字当预览;更新时间要来自文件修改时间。文件里的辅助函数就像“布景工”,负责搭临时目录、写假会话文件、塞假数据库记录;真正的测试函数则像“检查员”,调用 get_threads、find_thread_path_by_id_str、read_head_for_summary 等主功能,确认结果完全符合要求。
provider_vec48–53 ↗
fn provider_vec(providers: &[&str]) -> Vec<String>
作用:把一组字符串形式的模型提供商名字,变成测试接口需要的字符串列表。很多测试都要传“只看这些提供商”的过滤条件,所以用它少写重复代码。
数据流:输入是一片字符串切片,比如 ["openai"] → 它逐个复制成可拥有的 String → 输出一个 Vec<String>,也就是 Rust 里可增长的字符串数组,不改动外部状态。
调用关系:它是测试里的小工具。列会话、分页、摘要、时间排序、提供商过滤等测试在调用 get_threads 前会先用它准备 provider 过滤参数。
调用图:被 13 处调用(test_base_instructions_missing_in_meta_defaults_to_null, test_base_instructions_present_in_meta_is_preserved, test_created_at_sort_uses_file_mtime_for_updated_at, test_get_thread_contents, test_goal_first_thread_reads_later_user_message, test_list_conversations_latest_first, test_list_threads_scans_past_head_for_user_event, test_list_threads_uses_goal_objective_as_preview, test_model_provider_filter_selects_only_matching_sessions, test_pagination_cursor (+3 more))。
thread_id_from_uuid55–57 ↗
fn thread_id_from_uuid(uuid: Uuid) -> ThreadId
作用:把测试里常用的 UUID 转成项目自己的 ThreadId。UUID 可以理解成一串几乎不会重复的身份证号,ThreadId 是项目内部对会话身份证的包装。
数据流:输入一个 Uuid → 先转成字符串,再交给 ThreadId::from_string 校验和包装 → 输出一个合法的 ThreadId;如果转不成,测试会直接失败。
调用关系:它主要被 write_goal_started_session_file 使用,用来给“目标已开始”的假事件填上正确线程编号。
调用图:调用 1 个内部函数(from_string);被 1 处调用(write_goal_started_session_file);外部调用 1 个(to_string)。
insert_state_db_thread59–95 ↗
async fn insert_state_db_thread(
home: &Path,
thread_id: ThreadId,
rollout_path: &Path,
archived: bool,
) -> crate::state_db::StateDbHandle
作用:在临时状态数据库里插入一条假的会话记录。这样测试就能模拟“数据库知道某个会话文件在哪里”的情况,包括故意写错路径的情况。
数据流:输入 home 目录、线程 ID、会话文件路径、是否归档 → 初始化状态数据库,标记回填完成,组装一份线程元数据,包括创建时间、来源、模型提供商、预览文字等 → 写入数据库并返回数据库运行句柄。
调用关系:它是数据库相关测试的布景工具。几个 find_thread_path... 测试先用它塞入数据库记录,再调用 find_thread_path_by_id_str 检查主逻辑是否相信、修正或绕过这条记录。
调用图:调用 2 个内部函数(new, init);被 3 处调用(find_thread_path_accepts_existing_state_db_path_without_canonical_filename, find_thread_path_falls_back_when_db_path_is_stale, find_thread_path_falls_back_when_db_path_points_to_another_thread);外部调用 1 个(to_path_buf)。
find_thread_path_falls_back_when_db_path_is_stale98–130 ↗
async fn find_thread_path_falls_back_when_db_path_is_stale()
作用:测试当数据库里的会话文件路径已经过期、不存在时,查找逻辑会不会从磁盘文件夹里重新找到正确文件。这个场景很重要,因为用户移动、清理或升级数据后,数据库可能落后于真实文件。
数据流:先在临时目录写一个真实会话文件,又在数据库里写一个指向 2099 年假位置的旧路径 → 调用 find_thread_path_by_id_str 查找这个线程 → 期望返回真实文件路径,并通过 assert_state_db_rollout_path 确认数据库也被修正。
调用关系:它调用 write_session_file 搭真实文件,调用 insert_state_db_thread 制造坏数据库记录,最后把核心检查交给 find_thread_path_by_id_str 和数据库断言函数。
调用图:调用 4 个内部函数(from_string, assert_state_db_rollout_path, insert_state_db_thread, write_session_file);外部调用 5 个(new, from_u128, assert_eq!, find_thread_path_by_id_str, format!)。
find_thread_path_falls_back_when_db_path_points_to_another_thread133–175 ↗
async fn find_thread_path_falls_back_when_db_path_points_to_another_thread()
作用:测试数据库里的路径虽然存在,但指向了另一个会话文件时,查找逻辑不会被误导。没有这个保护,用户可能打开错的对话。
数据流:先写目标会话文件,再写另一个会话文件,然后把目标线程在数据库中的路径故意指向另一个文件 → 调用查找函数 → 结果应回到文件系统中找到目标 UUID 对应的文件,并修好数据库路径。
调用关系:它和上一个测试类似,但坏数据更隐蔽:路径存在却内容不匹配。它依赖 write_session_file、insert_state_db_thread 和 assert_state_db_rollout_path 配合验证。
调用图:调用 4 个内部函数(from_string, assert_state_db_rollout_path, insert_state_db_thread, write_session_file);外部调用 5 个(new, from_u128, assert_eq!, find_thread_path_by_id_str, format!)。
find_thread_path_repairs_missing_db_row_after_filesystem_fallback178–208 ↗
async fn find_thread_path_repairs_missing_db_row_after_filesystem_fallback()
作用:测试数据库里完全没有这条会话记录时,系统能不能从磁盘找到文件,并顺手把数据库补上。这样以后再查同一个会话就更快、更可靠。
数据流:先写真实会话文件,再初始化一个空状态数据库并标记回填完成 → 调用线程路径查找 → 输出应是文件系统里的路径,同时数据库里新增对应路径记录。
调用关系:这个测试不使用 insert_state_db_thread,因为它要模拟“缺行”。它直接初始化数据库,然后通过 find_thread_path_by_id_str 触发文件系统兜底和数据库修复。
调用图:调用 4 个内部函数(from_string, assert_state_db_rollout_path, write_session_file, init);外部调用 5 个(new, from_u128, assert_eq!, find_thread_path_by_id_str, format!)。
find_thread_path_accepts_existing_state_db_path_without_canonical_filename211–231 ↗
async fn find_thread_path_accepts_existing_state_db_path_without_canonical_filename()
作用:测试数据库里已有的路径只要文件真实存在,就算文件名不是标准 rollout 命名,也应该被接受。这样可以兼容迁移、旧文件或特殊命名。
数据流:创建一个自定义名字的空 jsonl 文件,把它作为会话路径写进数据库 → 调用查找函数 → 返回的就是数据库里的这个自定义路径,不强迫按标准文件名重找。
调用关系:它用 insert_state_db_thread 写入数据库记录,再调用 find_thread_path_by_id_str。这个测试说明数据库路径在有效时优先级很高。
调用图:调用 2 个内部函数(from_string, insert_state_db_thread);外部调用 6 个(new, from_u128, assert_eq!, find_thread_path_by_id_str, create_dir_all, write)。
rollout_date_parts_extracts_directory_components234–241 ↗
fn rollout_date_parts_extracts_directory_components()
作用:测试从标准会话文件名里提取年月日是否正确。这个能力用于把文件放进或查找在 sessions/年/月/日 这样的目录结构里。
数据流:输入文件名 rollout-2025-03-01T09-00-00-123.jsonl → 调用 rollout_date_parts 拆出日期 → 期望得到 2025、03、01 三段字符串。
调用关系:这是对日期解析小逻辑的直接测试,不搭临时文件,也不碰数据库。
调用图:外部调用 3 个(new, assert_eq!, rollout_date_parts)。
assert_state_db_rollout_path243–256 ↗
async fn assert_state_db_rollout_path(
home: &Path,
thread_id: ThreadId,
expected_path: Option<&Path>,
)
作用:检查状态数据库里某个线程记录的会话文件路径是不是预期值。它是多个路径修复测试的专用断言工具。
数据流:输入 home 目录、线程 ID、期望路径 → 重新打开状态数据库,按线程 ID 查询路径 → 把查到的路径和期望路径比较;不返回业务值,失败时测试报错。
调用关系:它被三个数据库兜底测试调用,用来确认 find_thread_path_by_id_str 不只是返回了正确结果,还真的把数据库修好了。
调用图:调用 1 个内部函数(init);被 3 处调用(find_thread_path_falls_back_when_db_path_is_stale, find_thread_path_falls_back_when_db_path_points_to_another_thread, find_thread_path_repairs_missing_db_row_after_filesystem_fallback);外部调用 2 个(to_path_buf, assert_eq!)。
write_session_file258–273 ↗
fn write_session_file(
root: &Path,
ts_str: &str,
uuid: Uuid,
num_records: usize,
source: Option<SessionSource>,
) -> std::io::Result<(OffsetDateTime, Uuid)>
作用:写一个最常见的假会话文件,默认使用测试模型提供商。绝大多数测试只关心“有个正常会话”,所以用这个简化入口。
数据流:输入根目录、时间字符串、UUID、额外记录数、会话来源 → 把模型提供商固定为 test-provider 后转交给 write_session_file_with_provider → 输出写入文件时解析出的时间和 UUID。
调用关系:它是许多测试的默认造数工具。真正写文件的细节由 write_session_file_with_provider 完成。
调用图:调用 1 个内部函数(write_session_file_with_provider);被 9 处调用(find_thread_path_falls_back_when_db_path_is_stale, find_thread_path_falls_back_when_db_path_points_to_another_thread, find_thread_path_repairs_missing_db_row_after_filesystem_fallback, test_created_at_sort_uses_file_mtime_for_updated_at, test_get_thread_contents, test_list_conversations_latest_first, test_pagination_cursor, test_source_filter_excludes_non_matching_sessions, test_timestamp_only_cursor_skips_same_second_filesystem_ties)。
write_session_file_with_provider275–344 ↗
fn write_session_file_with_provider(
root: &Path,
ts_str: &str,
uuid: Uuid,
num_records: usize,
source: Option<SessionSource>,
model_provider: Option<&str>,
) -> std::io::Resul
作用:写一个带指定模型提供商的假会话文件。它模拟真实 rollout 文件的基本格式,让列表和过滤逻辑有东西可读。
数据流:输入根目录、时间、UUID、响应记录数量、来源、可选模型提供商 → 按时间创建 sessions/年/月/日 目录,写入 session_meta 元信息、第一条用户消息,再写若干响应记录,并把文件修改时间设成会话时间 → 输出解析后的时间和 UUID。
调用关系:它被 write_session_file 包装,也被模型提供商过滤测试直接使用。get_threads 后续会读取它造出的文件,验证排序、摘要和过滤行为。
调用图:被 2 处调用(test_model_provider_filter_selects_only_matching_sessions, write_session_file);外部调用 11 个(create, new, join, parse, format!, format_description!, create_dir_all, String, json!, to_value (+1 more))。
write_goal_started_session_file346–423 ↗
fn write_goal_started_session_file(
root: &Path,
ts_str: &str,
uuid: Uuid,
objective: &str,
later_user_message: Option<&str>,
) -> std::io::Result<()>
作用:写一个“先有目标、可能 later 才有用户消息”的假会话文件。它用来测试目标型会话的预览文字是否来自目标,而不是只能依赖第一条用户消息。
数据流:输入根目录、时间、UUID、目标文字、可选的后续用户消息 → 创建会话文件,写 session_meta,再写一个 ThreadGoalUpdated 事件,必要时再写用户消息,并设置文件修改时间 → 输出为空,结果体现在磁盘文件中。
调用关系:它会用 thread_id_from_uuid 生成事件里的线程 ID。两个目标相关测试用它造文件,然后调用 get_threads 看预览和 first_user_message 是否正确。
调用图:调用 1 个内部函数(thread_id_from_uuid);被 2 处调用(test_goal_first_thread_reads_later_user_message, test_list_threads_uses_goal_objective_as_preview);外部调用 10 个(create, new, join, parse, format!, format_description!, create_dir_all, ThreadGoalUpdated, json!, writeln!)。
write_session_file_with_delayed_user_event425–480 ↗
fn write_session_file_with_delayed_user_event(
root: &Path,
ts_str: &str,
uuid: Uuid,
meta_lines_before_user: usize,
) -> std::io::Result<()>
作用:写一个用户消息出现得很靠后的假会话文件。它用来确认列表摘要不会只看文件开头几行就放弃。
数据流:输入根目录、时间、UUID、用户消息前面要放多少条 meta 记录 → 创建会话文件,先写多条 session_meta,再写用户消息,并设置修改时间 → 输出为空,文件内容被后续测试读取。
调用关系:它只被 test_list_threads_scans_past_head_for_user_event 使用,专门测试 get_threads 在摘要扫描时能越过很多头部记录找到用户消息。
调用图:被 1 处调用(test_list_threads_scans_past_head_for_user_event);外部调用 10 个(create, new, join, parse, from_u128, format!, format_description!, create_dir_all, json!, writeln!)。
write_session_file_with_meta_payload482–522 ↗
fn write_session_file_with_meta_payload(
root: &Path,
ts_str: &str,
uuid: Uuid,
payload: serde_json::Value,
) -> std::io::Result<()>
作用:用自定义的 session_meta 内容写假会话文件。它方便测试某些元信息字段缺失或存在时,读取逻辑如何处理。
数据流:输入根目录、时间、UUID、自定义 JSON payload → 创建标准日期目录和 rollout 文件,写入这份元信息,再写一条用户消息,并设置文件修改时间 → 输出为空,生成的文件供摘要读取测试使用。
调用关系:它被 base_instructions 两个测试调用。测试随后通过 get_threads 找到文件,再用 read_head_for_summary 检查读取出的字段。
调用图:被 2 处调用(test_base_instructions_missing_in_meta_defaults_to_null, test_base_instructions_present_in_meta_is_preserved);外部调用 9 个(create, new, join, parse, format!, format_description!, create_dir_all, json!, writeln!)。
test_list_conversations_latest_first525–660 ↗
async fn test_list_conversations_latest_first()
作用:测试会话列表是否按创建时间从新到旧排列。用户打开历史记录时,最新对话通常应该排在最前面。
数据流:先写 1 月 1、2、3 日三个会话 → 调用 get_threads 取列表 → 期望返回顺序是 3 日、2 日、1 日,并且每个条目的路径、线程 ID、预览、来源、模型提供商、创建时间等字段都正确。
调用关系:它用 write_session_file 准备数据,用 provider_vec 准备过滤条件,核心行为由 get_threads 完成,最后用整体页面对象比对结果。
调用图:调用 3 个内部函数(get_threads, provider_vec, write_session_file);外部调用 5 个(new, from_u128, assert_eq!, format!, vec!)。
test_pagination_cursor663–905 ↗
async fn test_pagination_cursor()
作用:测试分页游标是否能让会话列表一页一页往后翻。分页游标可以理解成书签,告诉下一次从哪里继续看。
数据流:先写 5 个按日期递增的会话 → 第一次取 2 条,得到最新两条和一个游标;第二次带游标再取 2 条;第三次再带游标取最后 1 条 → 每一页的内容、下一页游标和扫描文件数量都必须符合预期。
调用关系:它反复调用 get_threads,每次把上一页的 next_cursor 传回去。write_session_file 负责搭五个文件,provider_vec 负责过滤参数。
调用图:调用 3 个内部函数(get_threads, provider_vec, write_session_file);外部调用 6 个(new, from_u128, assert_eq!, format!, from_str, vec!)。
test_list_threads_scans_past_head_for_user_event908–933 ↗
async fn test_list_threads_scans_past_head_for_user_event()
作用:测试会话列表在做摘要时,会继续往下扫描,找到不在文件开头的第一条用户消息。否则有些会话可能因为前面元信息太多而被误判。
数据流:先写一个用户消息前有 12 条 meta 记录的文件 → 调用 get_threads → 期望列表里仍然有这个会话,并能识别出正确线程 ID。
调用关系:它使用 write_session_file_with_delayed_user_event 制造特殊文件,然后让 get_threads 的摘要读取逻辑接受考验。
调用图:调用 3 个内部函数(get_threads, provider_vec, write_session_file_with_delayed_user_event);外部调用 3 个(new, from_u128, assert_eq!)。
test_list_threads_uses_goal_objective_as_preview936–970 ↗
async fn test_list_threads_uses_goal_objective_as_preview()
作用:测试如果会话是以“目标”开始的,列表预览会显示目标文字。这样即使还没有用户普通消息,历史列表也不会空白。
数据流:写一个只有目标事件、没有后续用户消息的会话 → 调用 get_threads → 期望条目的 preview 是目标 optimize the benchmark,而 first_user_message 为空。
调用关系:它用 write_goal_started_session_file 造目标会话,再通过 get_threads 检查目标事件如何影响列表摘要。
调用图:调用 3 个内部函数(get_threads, provider_vec, write_goal_started_session_file);外部调用 3 个(new, from_u128, assert_eq!)。
test_goal_first_thread_reads_later_user_message973–1010 ↗
async fn test_goal_first_thread_reads_later_user_message()
作用:测试目标先出现、用户消息后出现时,系统既用目标做预览,也能记住后来的第一条用户消息。两个信息各有用途,不能互相覆盖错。
数据流:写一个先有目标 optimize the benchmark、后有用户消息 run the benchmark 的会话 → 调用 get_threads → 期望 preview 是目标文字,first_user_message 是后来的用户消息。
调用关系:它和前一个目标测试共用 write_goal_started_session_file,只是多传了后续用户消息,用来检查摘要逻辑更完整的情况。
调用图:调用 3 个内部函数(get_threads, provider_vec, write_goal_started_session_file);外部调用 3 个(new, from_u128, assert_eq!)。
test_get_thread_contents1013–1101 ↗
async fn test_get_thread_contents()
作用:测试列表返回的文件路径不仅字段正确,而且真的能读到完整会话内容。它同时检查“列表元数据”和“原始文件内容”两件事。
数据流:写一个带两条响应记录的会话 → 用 get_threads 找到它的路径 → 读取整个文件文本 → 先比对页面条目,再比对文件每一行 JSON 是否正好是预期的 meta、用户消息和两条响应。
调用关系:它通过 write_session_file 造标准文件,通过 get_threads 找文件,再用异步文件读取确认路径可用且内容没变形。
调用图:调用 3 个内部函数(get_threads, provider_vec, write_session_file);外部调用 7 个(new, new_v4, assert_eq!, format!, json!, read_to_string, vec!)。
test_base_instructions_missing_in_meta_defaults_to_null1104–1143 ↗
async fn test_base_instructions_missing_in_meta_defaults_to_null()
作用:测试元信息里缺少 base_instructions 字段时,摘要读取会把它补成 JSON 的 null。null 可以理解成明确的“没有值”,比字段消失更方便前端处理。
数据流:写一个没有 base_instructions 的自定义 meta 文件 → 用 get_threads 找到文件 → 调用 read_head_for_summary 读取摘要头部 → 期望第一条摘要里 base_instructions 存在且值为 null。
调用关系:它用 write_session_file_with_meta_payload 构造缺字段场景,再让 read_head_for_summary 的兼容逻辑接受检查。
调用图:调用 4 个内部函数(get_threads, read_head_for_summary, provider_vec, write_session_file_with_meta_payload);外部调用 4 个(new, from_u128, assert_eq!, json!)。
test_base_instructions_present_in_meta_is_preserved1146–1188 ↗
async fn test_base_instructions_present_in_meta_is_preserved()
作用:测试如果 base_instructions 已经存在,读取摘要时不能把它改掉。这个字段可能包含系统基础指令,对复盘会话很重要。
数据流:写一个带 base_instructions: { text: ... } 的 meta 文件 → 通过 get_threads 找到文件,再用 read_head_for_summary 读取 → 期望读出的 text 仍是原来的自定义文字。
调用关系:它和缺字段测试成对出现,一个测默认补 null,一个测已有值保持不变,都依赖 write_session_file_with_meta_payload。
调用图:调用 4 个内部函数(get_threads, read_head_for_summary, provider_vec, write_session_file_with_meta_payload);外部调用 4 个(new, from_u128, assert_eq!, json!)。
test_created_at_sort_uses_file_mtime_for_updated_at1191–1242 ↗
async fn test_created_at_sort_uses_file_mtime_for_updated_at() -> Result<()>
作用:测试按创建时间排序时,条目里的 updated_at 仍然来自文件修改时间。也就是说“创建时间”和“最后更新”是两回事。
数据流:写一个创建时间为 8 点的会话,然后手动把文件修改时间改成 10 点 → 调用按 CreatedAt 排序的 get_threads → 期望 created_at 还是 8 点字符串,updated_at 是文件修改时间对应的 RFC3339 时间。
调用关系:它用 write_session_file 先造文件,再直接改文件时间,最后检查 get_threads 生成的 ThreadItem 时间字段。
调用图:调用 3 个内部函数(get_threads, provider_vec, write_session_file);外部调用 9 个(hours, new, parse, new, from_u128, assert_eq!, format!, format_description!, new)。
test_updated_at_uses_file_mtime1245–1340 ↗
async fn test_updated_at_uses_file_mtime() -> Result<()>
作用:测试按更新时间排序时,updated_at 使用文件系统的修改时间,而不是文件内部最后一条消息的时间。文件系统修改时间更能代表这份记录最后被写入的时刻。
数据流:手工写一个会话文件,里面有 meta、用户消息和多条助手回复 → 调用按 UpdatedAt 排序的 get_threads → 期望 created_at 来自 meta,而 updated_at 接近当前文件修改时间。
调用关系:这个测试没有使用通用写文件助手,而是直接用协议结构写更真实的 rollout 行。然后把检查交给 get_threads。
调用图:调用 3 个内部函数(from_string, get_threads, provider_vec);外部调用 16 个(default, create, new, from_u128, new, assert!, assert_eq!, now, format!, create_dir_all (+6 more))。
test_timestamp_only_cursor_skips_same_second_filesystem_ties1343–1472 ↗
async fn test_timestamp_only_cursor_skips_same_second_filesystem_ties()
作用:测试当多个文件有完全相同的秒级时间戳时,旧式只含时间的游标会跳过同秒剩余文件。这是一个重要的边界行为,避免分页重复,但也说明文件系统兜底没有毫秒级数据库那么精细。
数据流:写三个同一秒创建的会话 → 第一页取 2 条,得到只含该时间戳的游标 → 第二页带这个游标继续取 → 期望第二页为空,因为同秒的文件都被游标视为已经越过。
调用关系:它通过 write_session_file 制造同秒并列场景,多次调用 get_threads 验证文件系统分页兜底的已知行为。
调用图:调用 3 个内部函数(get_threads, provider_vec, write_session_file);外部调用 7 个(new, from_u128, new, assert_eq!, format!, from_str, vec!)。
test_source_filter_excludes_non_matching_sessions1475–1547 ↗
async fn test_source_filter_excludes_non_matching_sessions()
作用:测试按会话来源过滤是否有效。来源比如 CLI、VSCode、Exec,过滤可以让界面只显示交互式会话,不混进后台执行记录。
数据流:写一个 CLI 来源会话和一个 Exec 来源会话 → 用交互式来源过滤调用 get_threads,期望只看到 CLI;再不传来源过滤调用一次,期望两个都看到。
调用关系:它用 write_session_file 造不同 source 的文件,用 provider_vec 提供模型过滤,核心验证在 get_threads 的来源过滤逻辑。
调用图:调用 3 个内部函数(get_threads, provider_vec, write_session_file);外部调用 4 个(new, from_u128, assert!, assert_eq!)。
test_model_provider_filter_selects_only_matching_sessions1550–1654 ↗
async fn test_model_provider_filter_selects_only_matching_sessions() -> Result<()>
作用:测试按模型提供商过滤是否正确。模型提供商可以理解成这次会话使用的是哪家模型服务,历史列表需要能按它筛选。
数据流:写三个会话:一个 provider 是 openai,一个是 beta,一个没有 provider → 用 openai 过滤时,期望 openai 和缺 provider 的会话出现;用 beta 过滤时,只出现 beta;用 unknown 过滤时为空;不传过滤时三个都出现。
调用关系:它直接调用 write_session_file_with_provider 控制 provider 字段,然后多次调用 get_threads 检查不同过滤条件下的返回结果。
调用图:调用 3 个内部函数(get_threads, provider_vec, write_session_file_with_provider);外部调用 4 个(new, from_u128, assert!, assert_eq!)。