#![allow(clippy::expect_used, clippy::panic, clippy::unwrap_used)]
use chrono::{DateTime, Utc};
use txcript::common;
use txcript::harness::codex;
use txcript::{Codec, Common, Store, Transcript};
fn ts(s: &str) -> DateTime<Utc> {
s.parse().unwrap()
}
fn exercise_rollout() -> String {
[
r#"{"timestamp":"2026-04-01T00:00:00Z","type":"turn_context","payload":{"turn_id":"turn-1","model":"gpt-5.2-codex"}}"#,
r#"{"timestamp":"2026-04-01T00:00:01Z","type":"response_item","payload":{"type":"message","role":"user","content":[{"type":"input_text","text":"Please inspect this image"},{"type":"input_image","image_url":"data:image/png;base64,Zm9v"}]}}"#,
r#"{"timestamp":"2026-04-01T00:00:02Z","type":"response_item","payload":{"type":"reasoning","summary":[{"type":"summary_text","text":"Inspecting project files"}],"content":null,"encrypted_content":"secret"}}"#,
r#"{"timestamp":"2026-04-01T00:00:03Z","type":"response_item","payload":{"type":"function_call","name":"exec_command","arguments":"{\"cmd\":\"ls\"}","call_id":"call-shell"}}"#,
r#"{"timestamp":"2026-04-01T00:00:04Z","type":"event_msg","payload":{"type":"exec_command_end","call_id":"call-shell","aggregated_output":"file1\nfile2\n","stdout":"","stderr":"","exit_code":0}}"#,
r#"{"timestamp":"2026-04-01T00:00:05Z","type":"response_item","payload":{"type":"function_call_output","call_id":"call-shell","output":"mirror output"}}"#,
r#"{"timestamp":"2026-04-01T00:00:06Z","type":"response_item","payload":{"type":"custom_tool_call","status":"completed","call_id":"call-patch","name":"apply_patch","input":"*** Begin Patch\n*** Update File: src/main.rs\n@@\n-old\n+new\n*** End Patch"}}"#,
r#"{"timestamp":"2026-04-01T00:00:07Z","type":"response_item","payload":{"type":"custom_tool_call_output","call_id":"call-patch","output":"{\"output\":\"Success. Updated the following files:\\nM src/main.rs\\n\",\"metadata\":{\"exit_code\":0,\"duration_seconds\":0.0}}"}}"#,
r#"{"timestamp":"2026-04-01T00:00:08Z","type":"event_msg","payload":{"type":"web_search_end","call_id":"ws-1","query":"Next.js docs","action":{"type":"search","query":"Next.js docs","queries":["Next.js docs","Next.js cache docs"]}}}"#,
r#"{"timestamp":"2026-04-01T00:00:09Z","type":"response_item","payload":{"type":"web_search_call","status":"completed","action":{"type":"search","query":"Next.js docs","queries":["Next.js docs","Next.js cache docs"]}}}"#,
r#"{"timestamp":"2026-04-01T00:00:10Z","type":"response_item","payload":{"type":"message","role":"assistant","content":[{"type":"output_text","text":"Done."}]}}"#,
r#"{"timestamp":"2026-04-01T00:00:11Z","type":"event_msg","payload":{"type":"token_count","info":{"last_token_usage":{"input_tokens":100,"cached_input_tokens":25,"output_tokens":40,"reasoning_output_tokens":5,"total_tokens":145}}}}"#,
r#"{"timestamp":"2026-04-01T00:00:12Z","type":"event_msg","payload":{"type":"task_complete","turn_id":"turn-1"}}"#,
]
.join("\n")
+ "\n"
}
#[test]
fn to_common_runs_the_full_aggregation() {
let dir = tempfile::tempdir().unwrap();
let store = codex::CodexStore::new(dir.path());
let src = dir.path().join("rollout-x.jsonl");
std::fs::write(&src, exercise_rollout()).unwrap();
let msgs = codex::Codex::to_common(&store.load(&src).unwrap())
.unwrap()
.body;
assert_eq!(msgs.len(), 9);
assert!(
matches!(&msgs[0].content[0], common::Block::Text { text } if text == "Please inspect this image")
);
assert!(matches!(&msgs[0].content[1], common::Block::Image { .. }));
assert!(
matches!(&msgs[1].content[0], common::Block::Thinking { text, .. } if text == "Inspecting project files")
);
assert!(matches!(
&msgs[2].content[0],
common::Block::ToolUse { id, tool: common::Tool::Bash { command, .. } } if id == "call-shell" && command == "ls"
));
assert!(matches!(
&msgs[3].content[0],
common::Block::ToolResult { tool_use_id, content: common::ToolOutput::Text(t), is_error: false }
if tool_use_id == "call-shell" && t == "file1\nfile2\n"
));
assert!(matches!(
&msgs[4].content[0],
common::Block::ToolUse { tool: common::Tool::Edit { file_path, old_string, new_string, .. }, .. }
if file_path == "src/main.rs" && old_string == "old" && new_string == "new"
));
assert!(matches!(
&msgs[5].content[0],
common::Block::ToolResult { tool_use_id, .. } if tool_use_id == "call-patch"
));
assert!(matches!(
&msgs[6].content[0],
common::Block::ToolResult { tool_use_id, content: common::ToolOutput::Text(t), .. }
if tool_use_id == "ws-1" && t == "Next.js docs\nNext.js cache docs"
));
assert!(matches!(
&msgs[7].content[0],
common::Block::ToolUse { id, tool: common::Tool::Raw { tool_name, .. } } if id == "ws-1" && tool_name == "WebSearch"
));
assert!(matches!(&msgs[8].content[0], common::Block::Text { text } if text == "Done."));
assert_eq!(msgs[8].model.as_deref(), Some("gpt-5.2-codex"));
let usage = msgs[8].usage.unwrap();
assert_eq!(usage.input_tokens, 100);
assert_eq!(usage.output_tokens, 40);
assert_eq!(usage.cache_read_input_tokens, Some(25));
}
#[test]
fn legacy_shell_array_command_normalizes_to_bash() {
let dir = tempfile::tempdir().unwrap();
let store = codex::CodexStore::new(dir.path());
let src = dir.path().join("rollout-y.jsonl");
std::fs::write(
&src,
[
r#"{"timestamp":"2026-04-01T00:00:00Z","type":"response_item","payload":{"type":"function_call","name":"shell","arguments":"{\"command\":[\"bash\",\"-lc\",\"ls\"],\"workdir\":\"/repo\"}","call_id":"c1"}}"#,
r#"{"timestamp":"2026-04-01T00:00:01Z","type":"response_item","payload":{"type":"function_call_output","call_id":"c1","output":"out"}}"#,
]
.join("\n"),
)
.unwrap();
let msgs = codex::Codex::to_common(&store.load(&src).unwrap())
.unwrap()
.body;
assert!(matches!(
&msgs[0].content[0],
common::Block::ToolUse { tool: common::Tool::Bash { command, workdir: Some(w), .. }, .. }
if command == "ls" && w == "/repo"
));
}
#[test]
fn store_round_trip_is_lossless_on_disk() {
let dir = tempfile::tempdir().unwrap();
let store = codex::CodexStore::new(dir.path());
let src = dir.path().join("rollout-z.jsonl");
let body = format!(
"{}\n{}\n",
r#"{"timestamp":"2026-01-02T03:04:05.000Z","type":"session_meta","payload":{"id":"sess-1","timestamp":"2026-01-02T03:04:05.000Z","cwd":"/repo","cli_version":"0.104.0","source":"cli","originator":"codex_cli_rs","model_provider":null,"base_instructions":null,"git":{"branch":"main"}}}"#,
r#"{"timestamp":"2026-01-02T03:04:06.000Z","type":"response_item","payload":{"type":"message","role":"user","content":[{"type":"input_text","text":"hi"}]}}"#,
);
std::fs::write(&src, &body).unwrap();
let loaded = store.load(&src).unwrap();
let saved = store.save(&loaded).unwrap();
let reloaded = store.load(&saved.reference).unwrap();
assert_eq!(loaded.body, reloaded.body);
let components: Vec<_> = saved
.reference
.components()
.map(|c| c.as_os_str().to_string_lossy().into_owned())
.collect();
assert!(components.windows(3).any(|w| w == ["2026", "01", "02"]));
assert!(saved.reference.to_string_lossy().contains("rollout-"));
assert!(saved.reference.to_string_lossy().ends_with("sess-1.jsonl"));
}
#[test]
fn discover_extracts_metadata() {
let dir = tempfile::tempdir().unwrap();
let nested = dir.path().join("2026").join("01").join("02");
std::fs::create_dir_all(&nested).unwrap();
std::fs::write(
nested.join("rollout-2026-01-02T03-04-05-sess-1.jsonl"),
format!(
"{}\n{}\n",
r#"{"timestamp":"2026-01-02T03:04:05.000Z","type":"session_meta","payload":{"id":"sess-1","timestamp":"2026-01-02T03:04:05.000Z","cwd":"/repo","cli_version":"0.104.0","model":"gpt-5.2-codex","git":{"branch":"main"}}}"#,
r#"{"timestamp":"2026-01-02T03:04:06.000Z","type":"response_item","payload":{"type":"message","role":"user","content":[{"type":"input_text","text":"hi"}]}}"#,
),
)
.unwrap();
let found = codex::CodexStore::new(dir.path()).discover().unwrap();
assert_eq!(found.len(), 1);
let meta = &found[0].meta;
assert_eq!(meta.id, "sess-1");
assert_eq!(meta.cwd.as_deref(), Some("/repo"));
assert_eq!(meta.git_branch.as_deref(), Some("main"));
assert_eq!(meta.model.as_deref(), Some("gpt-5.2-codex"));
assert_eq!(meta.cli_version.as_deref(), Some("0.104.0"));
}
fn sample_common() -> Transcript<Common> {
let meta = common::Meta {
id: "sess-1".into(),
timestamp: ts("2026-01-02T03:04:05.000Z"),
cwd: Some("/repo".into()),
git_branch: Some("main".into()),
title: None,
cli_version: Some("0.104.0".into()),
model: Some("gpt-5.2-codex".into()),
};
let model = || Some("gpt-5.2-codex".to_string());
let body = vec![
common::Message {
role: common::Role::User,
content: vec![common::Block::Text {
text: "inspect".into(),
}],
timestamp: ts("2026-01-02T03:04:06.000Z"),
model: None,
stop_reason: None,
usage: None,
},
common::Message {
role: common::Role::Assistant,
content: vec![common::Block::Thinking {
text: "reasoning".into(),
signature: None,
encrypted: None,
}],
timestamp: ts("2026-01-02T03:04:07.000Z"),
model: model(),
stop_reason: None,
usage: None,
},
common::Message {
role: common::Role::Assistant,
content: vec![common::Block::ToolUse {
id: "call-x".into(),
tool: common::Tool::Bash {
command: "ls".into(),
workdir: None,
timeout_ms: None,
description: None,
run_in_background: false,
},
}],
timestamp: ts("2026-01-02T03:04:08.000Z"),
model: model(),
stop_reason: None,
usage: None,
},
common::Message {
role: common::Role::User,
content: vec![common::Block::ToolResult {
tool_use_id: "call-x".into(),
content: common::ToolOutput::Text("file1\nfile2\n".into()),
is_error: false,
}],
timestamp: ts("2026-01-02T03:04:09.000Z"),
model: None,
stop_reason: None,
usage: None,
},
common::Message {
role: common::Role::Assistant,
content: vec![common::Block::Text {
text: "Done.".into(),
}],
timestamp: ts("2026-01-02T03:04:10.000Z"),
model: model(),
stop_reason: None,
usage: Some(common::Usage {
input_tokens: 100,
output_tokens: 40,
cache_read_input_tokens: Some(25),
cache_creation_input_tokens: None,
}),
},
];
Transcript::new(meta, body)
}
#[test]
fn codec_fixpoint_through_common_loses_nothing() {
let common = sample_common();
let native = codex::Codex::from_common(&common).unwrap();
let back = codex::Codex::to_common(&native).unwrap();
assert_eq!(common, back);
}