Skip to content

上下文与压缩测试方法

从 Codex 测试源码出发,学习如何验证上下文归一化、压缩边界、远程协议、恢复和分叉,而不把单个断言误读为完整结论。

基于rust-v0.150.0
CodexRustContextCompactTesting

上下文与压缩测试方法 ​

本文收束上下文与压缩系列,面向已经读过 Context归一化算法、AutoCompact触发算法、本地Compact执行与写回 和 远程Compact-V2尝试与降级 的读者。目标不是列出所有测试名称,而是从真实测试源码建立一套可迁移的方法:如何安排输入、选择 harness、观察模型可见结果、写出不变量断言,并标记未覆盖边界。

本文覆盖四层测试:纯函数/状态单元测试、请求级集成测试、v1/v2 parity 测试、rollout resume/fork 测试。当前源码还要求测试关注 ResponseItemEnvelope metadata、WorldState snapshot 和 content kind 标注。取消、远程失败、本地失败和恢复都放在相应层次中讨论;测试不能覆盖 provider 摘要质量、真实网络延迟或所有操作系统上的行为。读完后,读者应能为一个新的 context 变更选择合适的 fixture,解释断言为何足够或不够,并把“看到一个请求”与“状态已提交”区分开。

1. 测试分层 ​

测试层次决定可观察对象。单元测试直接调用纯函数,集成测试通过 TestCodexHarness 驱动真实 Session,parity 测试比较两个实现的规范化结果,恢复测试则把内存结果写入 rollout 后重新启动消费者。

图中“综合结论”不是把四层结果简单相加:每层都有自己的 owner 和盲区。纯函数测试不能证明 HTTP headers,parity 不能证明恢复,resume/fork 也不能证明摘要内容正确。

2. 测试入口 ​

生产代码中的 core/src/*_tests.rs 适合快速验证数据变换;core/tests/all.rs 聚合 suite 集成测试。测试入口不是装饰:如果只运行错误的 Cargo target,得到的 0 tests 不能算验证。

源码位置:codex-rs/core/src/compact_tests.rs :: process_compacted_history_with_test_session

rust
async fn process_compacted_history_with_test_session(
    compacted_history: Vec<ResponseItem>,
    previous_turn_settings: Option<&PreviousTurnSettings>,
) -> (Vec<ResponseItem>, Vec<ResponseItem>) {
    let (session, turn_context) = crate::session::tests::make_session_and_context().await;
    let turn_context = Arc::new(turn_context);
    session
        .set_previous_turn_settings(previous_turn_settings.cloned())
        .await;
    let step_context =
        crate::session::step_context::StepContext::for_test(Arc::clone(&turn_context));
    let world_state = Arc::new(
        session
            .build_world_state_for_step(&step_context)
            .await
            .expect("world state should build"),
    );
    let initial_context = session
        .build_initial_context_with_world_state(&turn_context, world_state.as_ref())
        .await;
    let initial_context_injection = InitialContextInjection::BeforeLastUserMessage {
        world_state,
        step_context,
    };
    let (refreshed, _) = crate::compact_remote::process_compacted_history(
        &session,
        compacted_history,
        &initial_context_injection,
    )
    .await;
    (refreshed, initial_context)
}

这个 helper 的价值在于同时返回 refreshed 和 initial_context。断言可以精确区分“远程 compact 输出被保留”和“当前 session 上下文被重新注入”,不会把两者拼在一起后只比较字符串。

3. 输入构造 ​

测试 fixture 应先表达语义,再选择协议 item。user_message、developer_msg、function call 和 tool output 这些小构造器让每个测试只改变一个变量。

源码位置:codex-rs/core/src/compact_tests.rs :: user_message

rust
fn user_message(text: &str) -> ResponseItem {
    ResponseItem::Message {
        id: None,
        role: "user".to_string(),
        content: vec![ContentItem::InputText {
            text: text.to_string(),
        }],
        phase: None,
        internal_chat_message_metadata_passthrough: None,
    }
}

输入应覆盖正常消息和容易误判的消息:session prefix、environment context、legacy warning、developer instruction、summary marker、图片、reasoning、function-call pair。不要只用三条普通 user message 证明过滤算法。

4. 纯函数 ​

纯函数测试的第一原则是断言输出结构和边界,而不是实现细节。collect_annotated_user_messages 的 fixture 同时放入 assistant、session prefix 和真实 user,断言只得到最后一个可见 user message。

源码位置:codex-rs/core/src/compact_tests.rs :: collect_annotated_user_messages_extracts_user_text_only

rust
let items = vec![
    ResponseItem::Message {
        id: None,
        role: "user".to_string(),
        content: vec![ContentItem::InputText {
            text: "# AGENTS.md instructions for project\n\n<INSTRUCTIONS>do things</INSTRUCTIONS>"
                .to_string(),
        }],
        phase: None,
        internal_chat_message_metadata_passthrough: None,
    },
    ResponseItem::Message {
        id: None,
        role: "user".to_string(),
        content: vec![ContentItem::InputText {
            text: "real user message".to_string(),
        }],
        phase: None,
        internal_chat_message_metadata_passthrough: None,
    },
];

let envelopes = items
    .into_iter()
    .map(ResponseItemEnvelope::new)
    .collect::<Vec<_>>();
let collected = collect_annotated_user_messages(&envelopes);

assert_eq!(vec![compacted_user_message("real user message")], collected);

这个断言覆盖 prefix 过滤规则,不覆盖 for_prompt 的编码、UI 投影或真实 AGENTS 文件发现。

5. 历史不变量 ​

压缩历史测试应写出不变量:摘要在末尾、旧 developer 不进入结果、mid-turn 初始上下文位于最后真实 user 之前、summary 仍保持最后 item。测试名称只是索引,真正的教材信息在输入和断言之间。

源码位置:codex-rs/core/src/compact_tests.rs :: process_compacted_history_inserts_context_before_last_real_user_message_only

rust
let compacted_history = vec![
    user_message("older user"),
    user_message(&format!("{SUMMARY_PREFIX}\nsummary text")),
    user_message("latest user"),
];

let (refreshed, initial_context) = process_compacted_history_with_test_session(
    compacted_history,
    /*previous_turn_settings*/ None,
)
.await;

let context_end = refreshed
    .iter()
    .position(|item| item == initial_context.last().expect("context should not be empty"))
    .expect("initial context should be inserted");
assert!(context_end < refreshed.len() - 1);

测试中的具体断言还会比较完整向量;学习时要关注位置关系,而不是只关注文本。位置关系是后续模型训练假设和 continuation 行为的可验证接口。

6. 窗口边界 ​

窗口测试必须控制模型窗口和 auto compact limit,否则输入长度与模型目录默认值会同时变化,失败时无法知道是触发器还是 fixture 本身过大。

源码位置:codex-rs/core/tests/suite/compact.rs :: snapshot_request_shape_pre_turn_compaction_context_window_exceeded

rust
let harness = TestCodexHarness::with_builder(
    test_codex().with_config(|config| {
        config.model_context_window = Some(100);
        config.model_auto_compact_token_limit = Some(90);
    }),
)
.await?;

之后测试通过 mock response 驱动初始 turn、compact 和后续请求,检查 compact 是否在 context window exceeded 前发生,且 replacement history 没有把旧超长工具输出继续带入。

这里的 OldWindow 是失败边界,不是自动恢复保证。测试只能证明本次调用没有安装 replacement;下一次 turn 是否再次触发,由新的 token 状态和触发算法决定。

7. 协议形状 ​

请求级集成测试使用 TestCodexHarness、mock server 和 request log,观察生产代码真正发出的 JSON、headers、事件和 follow-up 请求。它比直接调用 build_* 更接近用户可见行为,但成本也更高。

源码位置:codex-rs/core/tests/suite/compact_remote.rs :: remote_compact_v2_retries_failures_with_stream_retry_budget

rust
let responses_mock = responses::mount_response_sequence(
    harness.server(),
    vec![
        responses::sse_response(responses::sse(vec![
            responses::ev_assistant_message("m1", "FIRST_REMOTE_REPLY"),
            responses::ev_completed("resp-1"),
        ])),
        ResponseTemplate::new(500).set_body_string("first compact open failed"),
        responses::sse_response(responses::sse(vec![
            serde_json::json!({
                "type": "response.output_item.done",
                "item": { "type": "compaction", "encrypted_content": "FAILED" }
            })
        ])),
        responses::sse_response(responses::sse(vec![
            serde_json::json!({
                "type": "response.output_item.done",
                "item": { "type": "compaction", "encrypted_content": "RETRIED" }
            }),
            responses::ev_completed("resp-compact-retry"),
        ])),
    ],
)
.await;

测试最后检查 request 数量、每次 compact 的 trigger 和 follow-up 中只出现 RETRIED。这证明失败 attempt 的 output 被隔离,不证明服务器在第一次连接失败后没有继续计算。

8. Parity比较 ​

compact_remote_parity.rs 把同一组 scenario 分别跑在 Legacy 和 V2 上,再对 normalized body、replacement history 和 follow-up 做比较。它不是要求 JSON 字节完全相同,而是先移除动态 ID、路径和实现专属字段,再比较语义投影。

源码位置:codex-rs/core/tests/suite/compact_remote_parity.rs :: assert_follow_up_and_history_eq

rust
fn assert_follow_up_and_history_eq(label: &str, legacy: &Capture, v2: &Capture) {
    assert_eq!(
        legacy.replacement_history,
        v2.replacement_history,
        "{label}: replacement histories differ",
    );
    assert_eq!(
        legacy.follow_up_body,
        v2.follow_up_body,
        "{label}: follow-up requests differ",
    );
}

parity 的输入矩阵包含 assistant-only、reasoning/image、tool mix 和 full mix,并额外覆盖 API key service tier、hook 和不同初始 history。它能发现 v1/v2 的语义漂移,但不能证明两个 endpoint 的延迟、计费或 provider 内部摘要算法相同。

9. 恢复验证 ​

compact 的持久化测试不能只检查当前 request。compact_resume_fork.rs 会先 compact,关闭 thread,再 resume,再 fork,最后比较三个阶段的模型输入前缀。

源码位置:codex-rs/core/tests/suite/compact_resume_fork.rs :: compact_resume_and_fork_preserve_model_history_view

rust
let compact_arr = input_after_compact
    .as_array()
    .expect("input after compact should be an array");
let resume_arr = input_after_resume
    .as_array()
    .expect("input after resume should be an array");
let fork_arr = input_after_fork
    .as_array()
    .expect("input after fork should be an array");

assert!(compact_arr.len() <= resume_arr.len());
assert_eq!(compact_arr.as_slice(), &resume_arr[..compact_arr.len()]);
assert!(compact_arr.len() <= fork_arr.len());
assert_eq!(compact_arr.as_slice(), &fork_arr[..compact_arr.len()]);

这个前缀不变量证明 replacement history 是 resume/fork 的共同 base;它不证明所有 rollout metadata 都一致,也不证明 fork 后的新 suffix 与原 thread 共享同一 live lock。

10. 失败取消 ​

失败和取消必须分别构造。HTTP 500、stream closed before completed、多个 compaction output 属于协议/传输失败;PreCompactHookOutcome::Stopped 和 TurnAborted 属于控制面取消。前者可能进入 retry 或 error event,后者不应伪装成模型失败。

源码位置:codex-rs/core/src/compact_remote_v2.rs :: collect_compaction_output

rust
if !saw_completed {
    return Err(CodexErr::Stream(
        "remote compaction v2 stream closed before response.completed".to_string(),
    ));
}
if compaction_count != 1 {
    return Err(CodexErr::Fatal(format!(
        "remote compaction v2 expected exactly one compaction output item, got {compaction_count} from {output_item_count} output items"
    )));
}

同一个测试应断言:错误返回、旧 history 仍可用于后续诊断、没有完成事件或 replacement checkpoint。不要只断言 Result::Err,因为那无法证明状态没有部分提交。

11. 断言边界 ​

每个测试说明至少包含四个字段:输入、断言、覆盖范围、未覆盖范围。下面的矩阵可作为新增测试的设计参考:

场景输入控制关键断言能证明不能证明
残缺工具对function call 无 output、或 output 无 callnormalize 后成对/丢弃规则history 不变量provider 解析
超长 user小 token limit + 大文本截断标记、摘要末尾本地预算算法tokenizer 精确账单
远程失败500、断流、错误 outputretry 次数、旧结果丢弃客户端失败隔离服务器资源回收
v1/v2 parity相同 scenario 矩阵normalized replacement/follow-up 相等语义投影一致延迟、计费、摘要质量
resume/forkcompact 后关闭、恢复、分叉输入前缀和 suffix持久化模型视图所有事件顺序
取消pre-hook stop 或取消 tokenTurnAborted、无写回控制面中断边界provider 端取消即时性

这条工作流把测试写作本身变成可复现对象:没有输入控制就无法解释失败,没有结构断言就无法支持状态结论;运行命令时还要确认它确实命中了目标测试。

12. 源码练习 ​

  1. 从 compact_tests.rs 复制一个过滤测试,增加 HookPrompt 和 CompactionTrigger,先写预期历史再运行测试;解释为什么“user role”不等于“真实用户输入”。
  2. 在 V2 截断测试中把 max_tokens 降到 3,记录最新 group、attached notice 和 image-only message 的变化,验证预算是按 group 而不是按数组索引消费。
  3. 给 parity scenario 增加一个只有 function tool 的输入,比较 v1/v2 replacement history;若差异出现,先定位 normalized projection,再判断是否真是实现回归。
  4. 在 resume/fork 测试中故意删除 rollout replacement_history 字段,观察恢复器如何退回更早记录;这能区分“测试 fixture 缺字段”和“恢复代码错误”。
  5. 将远程 500 改成 TurnAborted 模拟控制面停止,确认没有触发 fallback、window advance 或 completed event。

13. 可执行验证 ​

在本版本源码对应的 codex-rs workspace 中运行与本文相对应的本地、请求和恢复测试。若测试被环境条件跳过,应说明跳过原因,并区分“未执行”与“断言通过”。

bash
cd codex-rs
RUST_MIN_STACK=16777216 cargo test -p codex-core --lib compact::tests -- --nocapture
RUST_MIN_STACK=16777216 cargo test -p codex-core --lib compact_remote_v2::tests -- --nocapture
RUST_MIN_STACK=16777216 cargo test -p codex-core --test all compact_resume_and_fork_preserve_model_history_view -- --nocapture
RUST_MIN_STACK=16777216 cargo test -p codex-core --test all remote_compaction_parity_manual_transcripts -- --nocapture

本地单元测试验证输入输出不变量;请求级测试验证 harness 观察到的 wire shape;恢复和 parity 测试验证跨实现或跨生命周期的投影。没有一条命令单独证明完整的上下文系统正确。