Files
codex/codex-rs/core/tests/suite/resume_warning.rs
T
Charley CunninghamandGitHub bb0ac5be70 Fix compaction context reinjection and model baselines (#12252)
## Summary
- move regular-turn context diff/full-context persistence into
`run_turn` so pre-turn compaction runs before incoming context updates
are recorded
- after successful pre-turn compaction, rely on a cleared
`reference_context_item` to trigger full context reinjection on the
follow-up regular turn (manual `/compact` keeps replacement history
summary-only and also clears the baseline)
- preserve `<model_switch>` when full context is reinjected, and inject
it *before* the rest of the full-context items
- scope `reference_context_item` and `previous_model` to regular user
turns only so standalone tasks (`/compact`, shell, review, undo) cannot
suppress future reinjection or `<model_switch>` behavior
- make context-diff persistence + `reference_context_item` updates
explicit in the regular-turn path, with clearer docs/comments around the
invariant
- stop persisting local `/compact` `RolloutItem::TurnContext` snapshots
(only regular turns persist `TurnContextItem` now)
- simplify resume/fork previous-model/reference-baseline hydration by
looking up the last surviving turn context from rollout lifecycle
events, including rollback and compaction-crossing handling
- remove the legacy fallback that guessed from bare `TurnContext`
rollouts without lifecycle events
- update compaction/remote-compaction/model-visible snapshots and
compact test assertions (including remote compaction mock response
shape)

## Why
We were persisting incoming context items before spawning the regular
turn task, which let pre-turn compaction requests accidentally include
incoming context diffs without the new user message. Fixing that exposed
follow-on baseline issues around `/compact`, resume/fork, and standalone
tasks that could cause duplicate context injection or suppress
`<model_switch>` instructions.

This PR re-centers the invariants around regular turns:
- regular turns persist model-visible context diffs/full reinjection and
update the `reference_context_item`
- standalone tasks do not advance those regular-turn baselines
- compaction clears the baseline when replacement history may have
stripped the referenced context diffs

## Follow-ups (TODOs left in code)
- `TODO(ccunningham)`: fix rollback/backtracking baseline handling more
comprehensively
- `TODO(ccunningham)`: include pending incoming context items in
pre-turn compaction threshold estimation
- `TODO(ccunningham)`: inject updated personality spec alongside
`<model_switch>` so some model-switch paths can avoid forced full
reinjection
- `TODO(ccunningham)`: review task turn lifecycle
(`TurnStarted`/`TurnComplete`) behavior and emit task-start context
diffs for task types that should have them (excluding `/compact`)

## Validation
- `just fmt`
- CI should cover the updated compaction/resume/model-visible snapshot
expectations and rollout-hydration behavior
- I did **not** rerun the full local test suite after the latest
resume-lookup / rollout-persistence simplifications
2026-02-20 23:13:08 -08:00

117 lines
4.3 KiB
Rust

#![allow(clippy::unwrap_used, clippy::expect_used)]
use codex_core::CodexAuth;
use codex_core::NewThread;
use codex_core::protocol::EventMsg;
use codex_core::protocol::InitialHistory;
use codex_core::protocol::ResumedHistory;
use codex_core::protocol::RolloutItem;
use codex_core::protocol::TurnCompleteEvent;
use codex_core::protocol::TurnContextItem;
use codex_core::protocol::TurnStartedEvent;
use codex_core::protocol::UserMessageEvent;
use codex_core::protocol::WarningEvent;
use codex_protocol::ThreadId;
use codex_protocol::config_types::ModeKind;
use core::time::Duration;
use core_test_support::load_default_config_for_test;
use core_test_support::wait_for_event;
use tempfile::TempDir;
fn resume_history(
config: &codex_core::config::Config,
previous_model: &str,
rollout_path: &std::path::Path,
) -> InitialHistory {
let turn_id = "resume-warning-seed-turn".to_string();
let turn_ctx = TurnContextItem {
turn_id: Some(turn_id.clone()),
cwd: config.cwd.clone(),
approval_policy: config.permissions.approval_policy.value(),
sandbox_policy: config.permissions.sandbox_policy.get().clone(),
network: None,
model: previous_model.to_string(),
personality: None,
collaboration_mode: None,
effort: config.model_reasoning_effort,
summary: config.model_reasoning_summary,
user_instructions: None,
developer_instructions: None,
final_output_json_schema: None,
truncation_policy: None,
};
InitialHistory::Resumed(ResumedHistory {
conversation_id: ThreadId::default(),
history: vec![
RolloutItem::EventMsg(EventMsg::TurnStarted(TurnStartedEvent {
turn_id: turn_id.clone(),
model_context_window: None,
collaboration_mode_kind: ModeKind::Default,
})),
RolloutItem::EventMsg(EventMsg::UserMessage(UserMessageEvent {
message: "seed".to_string(),
images: None,
local_images: vec![],
text_elements: vec![],
})),
RolloutItem::TurnContext(turn_ctx),
RolloutItem::EventMsg(EventMsg::TurnComplete(TurnCompleteEvent {
turn_id,
last_agent_message: None,
})),
],
rollout_path: rollout_path.to_path_buf(),
})
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn emits_warning_when_resumed_model_differs() {
// Arrange a config with a current model and a prior rollout recorded under a different model.
let home = TempDir::new().expect("tempdir");
let mut config = load_default_config_for_test(&home).await;
config.model = Some("current-model".to_string());
// Ensure cwd is absolute (the helper sets it to the temp dir already).
assert!(config.cwd.is_absolute());
let rollout_path = home.path().join("rollout.jsonl");
std::fs::write(&rollout_path, "").expect("create rollout placeholder");
let initial_history = resume_history(&config, "previous-model", &rollout_path);
let thread_manager = codex_core::test_support::thread_manager_with_models_provider(
CodexAuth::from_api_key("test"),
config.model_provider.clone(),
);
let auth_manager =
codex_core::test_support::auth_manager_from_auth(CodexAuth::from_api_key("test"));
// Act: resume the conversation.
let NewThread {
thread: conversation,
..
} = thread_manager
.resume_thread_with_history(config, initial_history, auth_manager, false)
.await
.expect("resume conversation");
// Assert: a Warning event is emitted describing the model mismatch.
let warning = wait_for_event(&conversation, |ev| {
matches!(
ev,
EventMsg::Warning(WarningEvent { message })
if message.contains("previous-model") && message.contains("current-model")
)
})
.await;
let EventMsg::Warning(WarningEvent { message }) = warning else {
panic!("expected warning event");
};
assert!(message.contains("previous-model"));
assert!(message.contains("current-model"));
// Drain the TurnComplete/Shutdown window to avoid leaking tasks between tests.
// The warning is emitted during initialization, so a short sleep is sufficient.
tokio::time::sleep(Duration::from_millis(50)).await;
}