use anyhow::Result;use codex_protocol::ThreadId;use codex_protocol::protocol::EventMsg;use codex_protocol::protocol::GitInfo;use codex_protocol::protocol::SessionMeta;use codex_protocol::protocol::SessionMetaLine;use codex_protocol::protocol::SessionSource;use codex_protocol::protocol::TokenCountEvent;use codex_protocol::protocol::TokenUsage;use codex_protocol::protocol::TokenUsageInfo;use serde_json::json;use std::fs;use std::fs::FileTimes;use std::path::Path;use std::path::PathBuf;use uuid::Uuid;pub fn rollout_path(codex_home: &Path, filename_ts: &str, thread_id: &str) -> PathBuf { let year = &filename_ts[0..4]; let month = &filename_ts[5..7]; let day = &filename_ts[8..10]; codex_home .join("sessions") .join(year) .join(month) .join(day) .join(format!("rollout-{filename_ts}-{thread_id}.jsonl"))}/// Create a minimal rollout file under `CODEX_HOME/sessions/YYYY/MM/DD/`.////// - `filename_ts` is the filename timestamp component in `YYYY-MM-DDThh-mm-ss` format./// - `meta_rfc3339` is the envelope timestamp used in JSON lines./// - `preview` is the user message preview text./// - `model_provider` optionally sets the provider in the session meta payload.////// Returns the generated conversation/session UUID as a string.pub fn create_fake_rollout( codex_home: &Path, filename_ts: &str, meta_rfc3339: &str, preview: &str, model_provider: Option<&str>, git_info: Option<GitInfo>,) -> Result<String> { create_fake_rollout_with_source( codex_home, filename_ts, meta_rfc3339, preview, model_provider, git_info, SessionSource::Cli, )}/// Creates a minimal rollout whose history includes a persisted token usage event.////// Resume and fork tests use this fixture to verify lifecycle replay of restored/// usage without starting a model turn. The exact token values are intentionally/// non-zero and asymmetric so assertions catch swapped total/last fields and/// dropped cached or reasoning counters.pub fn create_fake_rollout_with_token_usage( codex_home: &Path, filename_ts: &str, meta_rfc3339: &str, preview: &str, model_provider: Option<&str>,) -> Result<String> { let thread_id = create_fake_rollout( codex_home, filename_ts, meta_rfc3339, preview, model_provider, /*git_info*/ None, )?; let payload = serde_json::to_value(EventMsg::TokenCount(TokenCountEvent { info: Some(TokenUsageInfo { total_token_usage: TokenUsage { input_tokens: 120, cached_input_tokens: 20, output_tokens: 30, reasoning_output_tokens: 10, total_tokens: 150, }, last_token_usage: TokenUsage { input_tokens: 70, cached_input_tokens: 10, output_tokens: 20, reasoning_output_tokens: 5, total_tokens: 90, }, model_context_window: Some(200_000), }), rate_limits: None, }))?; let file_path = rollout_path(codex_home, filename_ts, &thread_id); let line = json!({ "timestamp": meta_rfc3339, "type": "event_msg", "payload": payload }) .to_string(); fs::write( &file_path, format!("{}{}\n", fs::read_to_string(&file_path)?, line), )?; Ok(thread_id)}/// Create a minimal rollout file with an explicit session source.pub fn create_fake_rollout_with_source( codex_home: &Path, filename_ts: &str, meta_rfc3339: &str, preview: &str, model_provider: Option<&str>, git_info: Option<GitInfo>, source: SessionSource,) -> Result<String> { create_fake_rollout_with_source_and_parent_thread_id( codex_home, filename_ts, meta_rfc3339, preview, model_provider, git_info, source, /*parent_thread_id*/ None, )}/// Create a minimal rollout file with an explicit session source and control parent.#[allow(clippy::too_many_arguments)]pub fn create_fake_parented_rollout_with_source( codex_home: &Path, filename_ts: &str, meta_rfc3339: &str, preview: &str, model_provider: Option<&str>, git_info: Option<GitInfo>, source: SessionSource, parent_thread_id: ThreadId,) -> Result<String> { create_fake_rollout_with_source_and_parent_thread_id( codex_home, filename_ts, meta_rfc3339, preview, model_provider, git_info, source, Some(parent_thread_id), )}#[allow(clippy::too_many_arguments)]fn create_fake_rollout_with_source_and_parent_thread_id( codex_home: &Path, filename_ts: &str, meta_rfc3339: &str, preview: &str, model_provider: Option<&str>, git_info: Option<GitInfo>, source: SessionSource, parent_thread_id: Option<ThreadId>,) -> Result<String> { let uuid = Uuid::new_v4(); let uuid_str = uuid.to_string(); let conversation_id = ThreadId::from_string(&uuid_str)?; let file_path = rollout_path(codex_home, filename_ts, &uuid_str); let dir = file_path .parent() .ok_or_else(|| anyhow::anyhow!("missing rollout parent directory"))?; fs::create_dir_all(dir)?; // Build JSONL lines let meta = SessionMeta { id: conversation_id, forked_from_id: None, parent_thread_id, timestamp: meta_rfc3339.to_string(), cwd: PathBuf::from("/"), originator: "codex".to_string(), cli_version: "0.0.0".to_string(), source, thread_source: None, agent_path: None, agent_nickname: None, agent_role: None, model_provider: model_provider.map(str::to_string), base_instructions: None, dynamic_tools: None, memory_mode: None, multi_agent_version: None, }; let payload = serde_json::to_value(SessionMetaLine { meta, git: git_info, })?; let lines = [ json!({ "timestamp": meta_rfc3339, "type": "session_meta", "payload": payload }) .to_string(), json!({ "timestamp": meta_rfc3339, "type":"response_item", "payload": { "type":"message", "role":"user", "content":[{"type":"input_text","text": preview}] } }) .to_string(), json!({ "timestamp": meta_rfc3339, "type":"event_msg", "payload": { "type":"user_message", "message": preview, "kind": "plain" } }) .to_string(), ]; fs::write(&file_path, lines.join("\n") + "\n")?; let parsed = chrono::DateTime::parse_from_rfc3339(meta_rfc3339)?.with_timezone(&chrono::Utc); let times = FileTimes::new().set_modified(parsed.into()); std::fs::OpenOptions::new() .append(true) .open(&file_path)? .set_times(times)?; Ok(uuid_str)}pub fn create_fake_rollout_with_text_elements( codex_home: &Path, filename_ts: &str, meta_rfc3339: &str, preview: &str, text_elements: Vec<serde_json::Value>, model_provider: Option<&str>, git_info: Option<GitInfo>,) -> Result<String> { let uuid = Uuid::new_v4(); let uuid_str = uuid.to_string(); let conversation_id = ThreadId::from_string(&uuid_str)?; // sessions/YYYY/MM/DD derived from filename_ts (YYYY-MM-DDThh-mm-ss) let year = &filename_ts[0..4]; let month = &filename_ts[5..7]; let day = &filename_ts[8..10]; let dir = codex_home.join("sessions").join(year).join(month).join(day); fs::create_dir_all(&dir)?; let file_path = dir.join(format!("rollout-{filename_ts}-{uuid}.jsonl")); // Build JSONL lines let meta = SessionMeta { id: conversation_id, forked_from_id: None, parent_thread_id: None, timestamp: meta_rfc3339.to_string(), cwd: PathBuf::from("/"), originator: "codex".to_string(), cli_version: "0.0.0".to_string(), source: SessionSource::Cli, thread_source: None, agent_path: None, agent_nickname: None, agent_role: None, model_provider: model_provider.map(str::to_string), base_instructions: None, dynamic_tools: None, memory_mode: None, multi_agent_version: None, }; let payload = serde_json::to_value(SessionMetaLine { meta, git: git_info, })?; let lines = [ json!( { "timestamp": meta_rfc3339, "type": "session_meta", "payload": payload }) .to_string(), json!( { "timestamp": meta_rfc3339, "type":"response_item", "payload": { "type":"message", "role":"user", "content":[{"type":"input_text","text": preview}] } }) .to_string(), json!( { "timestamp": meta_rfc3339, "type":"event_msg", "payload": { "type":"user_message", "message": preview, "text_elements": text_elements, "local_images": [] } }) .to_string(), ]; fs::write(file_path, lines.join("\n") + "\n")?; Ok(uuid_str)}