use crate::constants::FastHashMap;
use crate::models::*;
use crate::pricing::{TierClassifier, TierThresholds};
use crate::session::diagnostics::{ParseDiagnostics, ParsedAnalysis};
use crate::session::state::{ParseMode, SessionParseState};
use crate::utils::{
CodexTokenTotals, get_git_remote_url, parse_iso_timestamp, process_codex_usage,
};
use anyhow::Result;
use regex::Regex;
use serde_json::Value;
use std::borrow::Borrow;
use std::collections::HashSet;
pub fn parse_codex_logs(logs: &[CodexLog], mode: ParseMode) -> Result<CodeAnalysis> {
parse_codex_log_iter(logs, mode)
}
pub(crate) fn parse_codex_log_iter<I>(logs: I, mode: ParseMode) -> Result<CodeAnalysis>
where
I: IntoIterator,
I::Item: Borrow<CodexLog>,
{
Ok(parse_codex_log_iter_with_diagnostics(logs, mode, None)?.analysis)
}
pub(crate) fn parse_codex_log_iter_with_diagnostics<I>(
logs: I,
mode: ParseMode,
tiers: Option<&TierThresholds>,
) -> Result<ParsedAnalysis>
where
I: IntoIterator,
I::Item: Borrow<CodexLog>,
{
let mut classifier = tiers.map(TierClassifier::new);
let mut state = SessionParseState::with_mode(mode);
let mut conversation_usage: FastHashMap<String, Value> = FastHashMap::with_capacity(5);
let mut current_model = String::new();
let mut prev_totals: Option<CodexTokenTotals> = None;
let mut shell_calls: FastHashMap<String, PendingCodexShellCall> =
FastHashMap::with_capacity(50);
let mut custom_calls: FastHashMap<String, CodexCustomCall> = FastHashMap::with_capacity(32);
let mut patch_counted_ids: HashSet<String> = HashSet::new();
let mut diagnostics = ParseDiagnostics::default();
for entry in logs {
let entry = entry.borrow();
let recognized = matches!(
entry.log_type.as_str(),
"session_meta"
| "turn_context"
| "event_msg"
| "response_item"
| "inter_agent_communication_metadata"
| "world_state"
| "compacted"
);
if recognized {
diagnostics.record_recognized_source();
} else {
diagnostics.record_unrecognized();
}
let ts = parse_iso_timestamp(&entry.timestamp);
if ts > state.last_ts {
state.last_ts = ts;
}
match entry.log_type.as_str() {
"session_meta" => {
if state.folder_path.is_empty()
&& let Some(cwd) = &entry.payload.cwd
{
state.folder_path.clone_from(cwd); }
if state.task_id.is_empty()
&& let Some(id) = &entry.payload.id
{
state.task_id.clone_from(id);
}
if state.git_remote.is_empty()
&& let Some(git) = &entry.payload.git
&& let Some(url) = &git.repository_url
{
state.git_remote.clone_from(url);
}
}
"turn_context" => {
if state.folder_path.is_empty()
&& let Some(cwd) = &entry.payload.cwd
{
state.folder_path.clone_from(cwd);
}
if let Some(model) = entry
.payload
.model
.as_ref()
.filter(|model| !model.is_empty())
{
current_model.clone_from(model); }
}
"event_msg" => {
if let Some(payload_type) = &entry.payload.payload_type
&& payload_type == "token_count"
&& let Some(info) = entry.payload.info.as_ref().filter(|info| !info.is_null())
{
if !is_supported_codex_usage(info) {
diagnostics.record_relevant(false);
} else {
diagnostics.record_relevant(true);
let total = info.get("total_token_usage").and_then(Value::as_object);
if current_model.is_empty() {
if let Some(total) = total {
prev_totals = Some(CodexTokenTotals::from_total_object(total));
}
} else {
let delta = total
.map(|total| {
CodexTokenTotals::delta_fields(total, prev_totals.as_ref())
})
.unwrap_or_default();
let above = classifier.as_mut().is_some_and(|classifier| {
let request_context = info
.get("last_token_usage")
.and_then(|last| last.get("input_tokens"))
.and_then(Value::as_i64)
.filter(|tokens| *tokens > 0)
.or_else(|| delta.get("input_tokens").and_then(Value::as_i64))
.unwrap_or(0);
classifier.is_above(¤t_model, request_context)
});
process_codex_usage(
&mut conversation_usage,
¤t_model,
&delta,
info,
above,
);
if let Some(total) = total {
prev_totals = Some(CodexTokenTotals::from_total_object(total));
}
}
}
}
if entry.payload.payload_type.as_deref() == Some("patch_apply_end") {
let already_counted = entry
.payload
.call_id
.as_deref()
.is_some_and(|id| patch_counted_ids.contains(id));
if matches!(entry.payload.success, Some(true)) && !already_counted {
match parse_patch_changes(entry.payload.changes.as_ref()) {
Some(patches) => {
for patch in patches {
state.handle_patch(patch, ts);
}
if let Some(id) = entry.payload.call_id.as_deref() {
patch_counted_ids.insert(id.to_string());
}
diagnostics.record_relevant(true);
}
None => diagnostics.record_relevant(false),
}
}
}
}
"response_item" => {
if let Some(payload_type) = &entry.payload.payload_type {
match payload_type.as_str() {
"function_call" => {
if let Some(name) = entry.payload.name.as_deref()
&& matches!(name, "shell" | "exec_command")
{
let call = entry
.payload
.arguments
.as_deref()
.and_then(|args| parse_function_call(name, args, ts));
if let Some(call_id) = entry.payload.call_id.as_deref() {
if let Some(call) = call {
diagnostics.record_relevant(true);
shell_calls.insert(
call_id.to_string(),
PendingCodexShellCall::Parsed(call),
);
} else {
shell_calls.insert(
call_id.to_string(),
PendingCodexShellCall::InvalidArguments,
);
}
} else {
diagnostics.record_relevant(false);
}
}
}
"function_call_output" => {
if let Some(call_id) = &entry.payload.call_id
&& let Some(call) = shell_calls.remove(call_id)
{
match call {
PendingCodexShellCall::Parsed(call) => {
diagnostics.record_relevant(true);
let output = shell_output(entry.payload.output.as_deref());
state.handle_shell_call(call, output);
}
PendingCodexShellCall::InvalidArguments => {
diagnostics.record_relevant(output_reports_argument_error(
entry.payload.output.as_deref(),
));
}
}
}
}
"custom_tool_call" => {
if let Some(name) = entry.payload.name.as_deref()
&& matches!(name, "exec" | "apply_patch")
{
let call = entry
.payload
.arguments
.as_deref()
.and_then(|input| parse_custom_call(name, input, ts, mode));
let normalized = call.is_some() && entry.payload.call_id.is_some();
diagnostics.record_relevant(normalized);
if let (Some(call), Some(call_id)) =
(call, entry.payload.call_id.as_deref())
{
custom_calls.insert(call_id.to_string(), call);
}
}
}
"custom_tool_call_output" => {
if let Some(call_id) = entry.payload.call_id.as_deref()
&& let Some(call) = custom_calls.remove(call_id)
{
let normalized = dispatch_custom_call(
&mut state,
call,
entry.payload.output.as_deref(),
call_id,
&mut patch_counted_ids,
);
diagnostics.record_relevant(normalized);
}
}
_ => {}
}
}
}
_ => {}
}
}
for call in shell_calls.into_values() {
if matches!(call, PendingCodexShellCall::InvalidArguments) {
diagnostics.record_relevant(false);
}
}
if state.git_remote.is_empty() {
state.git_remote = get_git_remote_url(&state.folder_path);
}
let record = state.into_record(conversation_usage);
let analysis = CodeAnalysis {
user: String::new(),
extension_name: String::new(),
insights_version: String::new(),
machine_id: String::new(),
records: vec![record],
};
Ok(ParsedAnalysis::new(analysis, diagnostics))
}
fn is_supported_codex_usage(info: &Value) -> bool {
let Some(info) = info.as_object() else {
return false;
};
let mut recognized = false;
for key in ["total_token_usage", "last_token_usage"] {
let Some(usage) = info.get(key) else {
continue;
};
if !is_supported_codex_token_usage(usage) {
return false;
}
recognized = true;
}
if let Some(context_window) = info.get("model_context_window") {
if context_window.is_null() {
} else if context_window.as_i64().is_none() {
return false;
} else {
recognized = true;
}
}
recognized
}
fn is_supported_codex_token_usage(usage: &Value) -> bool {
let Some(usage) = usage.as_object() else {
return false;
};
if usage.is_empty() {
return true;
}
let mut recognized = false;
for key in [
"input_tokens",
"cached_input_tokens",
"output_tokens",
"reasoning_output_tokens",
"total_tokens",
] {
if let Some(value) = usage.get(key) {
if value.as_i64().is_none() {
return false;
}
recognized = true;
}
}
recognized
}
enum PendingCodexShellCall {
Parsed(CodexShellCall),
InvalidArguments,
}
fn output_reports_argument_error(output: Option<&str>) -> bool {
let output = shell_output(output).output;
let output = strip_exec_command_metadata_prefix(&output)
.trim_start()
.to_ascii_lowercase();
(output.starts_with("error") || output.starts_with("failed"))
&& (output.contains("failed to parse")
|| output.contains("missing field")
|| output.contains("missing required")
|| output.contains("invalid argument"))
}
fn strip_exec_command_metadata_prefix(output: &str) -> &str {
const MARKER: &str = "\nOutput:\n";
if let Some(idx) = output.find(MARKER) {
&output[idx + MARKER.len()..]
} else if let Some(rest) = output.strip_prefix("Output:\n") {
rest
} else {
output
}
}
fn shell_output(output: Option<&str>) -> CodexShellOutput {
let Some(output) = output else {
return CodexShellOutput {
output: String::new(),
metadata: None,
};
};
serde_json::from_str::<CodexShellOutput>(output).unwrap_or_else(|_| CodexShellOutput {
output: serde_json::from_str::<Value>(output)
.ok()
.and_then(|value| value.get("output")?.as_str().map(str::to_string))
.unwrap_or_else(|| output.to_string()),
metadata: None,
})
}
fn parse_function_call(name: &str, args_str: &str, ts: i64) -> Option<CodexShellCall> {
match name {
"shell" => {
let args = serde_json::from_str::<CodexShellArguments>(args_str).ok()?;
let script = args.command.last().cloned().unwrap_or_default();
Some(CodexShellCall {
timestamp: ts,
script,
full_command: args.command,
})
}
"exec_command" => {
let args = serde_json::from_str::<CodexExecCommandArguments>(args_str).ok()?;
let cmd = args.cmd;
Some(CodexShellCall {
timestamp: ts,
script: cmd.clone(),
full_command: vec![cmd],
})
}
_ => None,
}
}
enum CodexCustomCall {
Exec {
source: Option<String>,
timestamp: i64,
},
ApplyPatch {
patches: Vec<CodexPatch>,
timestamp: i64,
},
}
fn parse_custom_call(
name: &str,
input: &str,
timestamp: i64,
mode: ParseMode,
) -> Option<CodexCustomCall> {
match name {
"exec" if !input.trim().is_empty() => Some(CodexCustomCall::Exec {
source: matches!(mode, ParseMode::Full).then(|| input.to_string()),
timestamp,
}),
"apply_patch" => {
let patches: Vec<_> = parse_apply_patch_script(input)
.into_iter()
.filter(|patch| !patch.file_path.is_empty())
.collect();
(!patches.is_empty()).then_some(CodexCustomCall::ApplyPatch { patches, timestamp })
}
_ => None,
}
}
fn dispatch_custom_call(
state: &mut SessionParseState,
call: CodexCustomCall,
output: Option<&str>,
call_id: &str,
patch_counted_ids: &mut HashSet<String>,
) -> bool {
match call {
CodexCustomCall::Exec { source, timestamp } => {
if let Some(source) = source {
state.add_run_command(&source, "", timestamp);
} else {
state.tool_counts.bash += 1;
}
true
}
CodexCustomCall::ApplyPatch { patches, timestamp } => {
match custom_apply_patch_result(output) {
CustomApplyPatchResult::Success => {
if patch_counted_ids.insert(call_id.to_string()) {
for patch in patches {
state.handle_patch(patch, timestamp);
}
}
true
}
CustomApplyPatchResult::Failure => true,
CustomApplyPatchResult::Unknown => false,
}
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum CustomApplyPatchResult {
Success,
Failure,
Unknown,
}
fn custom_apply_patch_result(output: Option<&str>) -> CustomApplyPatchResult {
let output = normalize_custom_output(output);
let output = output.trim();
if output == "Done!" || output.starts_with("Success. Updated the following files:") {
CustomApplyPatchResult::Success
} else if output.starts_with("Failed")
|| output.starts_with("Error")
|| output.starts_with("apply_patch verification failed:")
|| output.starts_with("Invalid patch")
|| output.starts_with("apply_patch handler received")
|| output.starts_with("apply_patch is unavailable")
{
CustomApplyPatchResult::Failure
} else {
CustomApplyPatchResult::Unknown
}
}
fn normalize_custom_output(output: Option<&str>) -> String {
let body = strip_exec_command_metadata_prefix(output.unwrap_or_default()).trim();
match serde_json::from_str::<Value>(body) {
Ok(Value::Object(object)) => object
.get("output")
.and_then(Value::as_str)
.unwrap_or(body)
.to_string(),
Ok(Value::String(text)) => text,
_ => body.to_string(),
}
}
trait CodexAnalysisExt {
fn handle_shell_call(&mut self, call: CodexShellCall, output: CodexShellOutput);
fn handle_patch(&mut self, patch: CodexPatch, ts: i64);
fn record_run_command(&mut self, call: CodexShellCall);
}
impl CodexAnalysisExt for SessionParseState {
fn handle_shell_call(&mut self, call: CodexShellCall, output: CodexShellOutput) {
if call.script.contains("*** Begin Patch") {
let patches = parse_apply_patch_script(&call.script);
for patch in patches {
self.handle_patch(patch, call.timestamp);
}
return;
}
let output_body = strip_exec_command_metadata_prefix(&output.output);
if let Some(path) = extract_sed_file_path(&call.script) {
self.add_read_detail(&path, output_body, call.timestamp);
return;
}
if let Some((path, content)) = extract_cat_read(&call.script, output_body) {
self.add_read_detail(&path, &content, call.timestamp);
return;
}
self.record_run_command(call);
}
fn handle_patch(&mut self, patch: CodexPatch, ts: i64) {
if patch.file_path.is_empty() {
return;
}
let resolved = self.normalize_path(&patch.file_path);
if resolved.is_empty() {
return;
}
let (old_str, new_str) = extract_patch_strings(&patch.lines);
match patch.action.as_str() {
"add" => {
self.add_write_detail(&resolved, &new_str, ts);
}
"delete" => {
let content = old_str.trim_end_matches('\n');
self.add_edit_detail_raw(&resolved, content, "", ts);
}
_ => {
self.add_edit_detail(&resolved, &old_str, &new_str, ts);
}
}
}
fn record_run_command(&mut self, call: CodexShellCall) {
let command_str = if call.full_command.is_empty() {
call.script.trim()
} else {
&call.full_command.join(" ")
};
self.add_run_command(command_str, "", call.timestamp);
}
}
struct CodexShellCall {
timestamp: i64,
script: String,
full_command: Vec<String>,
}
struct CodexPatch {
action: String,
file_path: String,
lines: Vec<String>,
}
fn parse_apply_patch_script(script: &str) -> Vec<CodexPatch> {
let start = match script.find("*** Begin Patch") {
Some(idx) => idx,
None => return Vec::new(),
};
let segment = &script[start..];
let mut patches = Vec::with_capacity(3);
let mut current: Option<CodexPatch> = None;
for line in segment.lines() {
let line = line.trim_end_matches('\r');
if line.starts_with("*** End Patch") {
if let Some(patch) = current.take() {
patches.push(patch);
}
break;
} else if line.starts_with("*** Begin Patch") {
continue;
} else if line.starts_with("*** Update File:") {
if let Some(patch) = current.take() {
patches.push(patch);
}
let file_path = line
.trim_start_matches("*** Update File:")
.trim()
.to_string();
current = Some(CodexPatch {
action: "update".to_string(),
file_path,
lines: Vec::with_capacity(20), });
} else if line.starts_with("*** Add File:") {
if let Some(patch) = current.take() {
patches.push(patch);
}
let file_path = line.trim_start_matches("*** Add File:").trim().to_string();
current = Some(CodexPatch {
action: "add".to_string(),
file_path,
lines: Vec::with_capacity(20),
});
} else if line.starts_with("*** Delete File:") {
if let Some(patch) = current.take() {
patches.push(patch);
}
let file_path = line
.trim_start_matches("*** Delete File:")
.trim()
.to_string();
current = Some(CodexPatch {
action: "delete".to_string(),
file_path,
lines: Vec::with_capacity(20),
});
} else if let Some(ref mut patch) = current {
patch.lines.push(line.to_string());
}
}
if let Some(patch) = current {
patches.push(patch);
}
patches
}
fn parse_patch_changes(changes: Option<&Value>) -> Option<Vec<CodexPatch>> {
let obj = changes?.as_object()?;
let mut patches = Vec::with_capacity(obj.len());
for (path, entry) in obj {
if let Some(patch) = parse_patch_change_entry(path, entry) {
patches.push(patch);
}
}
if !obj.is_empty() && patches.is_empty() {
return None;
}
Some(patches)
}
fn parse_patch_change_entry(path: &str, entry: &Value) -> Option<CodexPatch> {
if path.is_empty() {
return None;
}
let entry = entry.as_object()?;
let action = entry.get("type").and_then(Value::as_str)?;
if !matches!(action, "add" | "update" | "delete") {
return None;
}
let lines = entry
.get("unified_diff")
.and_then(Value::as_str)
.map(|diff| diff.lines().map(str::to_string).collect())
.unwrap_or_default();
Some(CodexPatch {
action: action.to_string(),
file_path: path.to_string(),
lines,
})
}
fn extract_patch_strings(lines: &[String]) -> (String, String) {
let estimated_size = lines.iter().map(|l| l.len()).sum::<usize>();
let mut old_str = String::with_capacity(estimated_size / 2);
let mut new_str = String::with_capacity(estimated_size / 2);
for line in lines {
if line.is_empty() {
continue;
}
if line.len() > 1 && line.starts_with("@@") {
continue;
}
let Some(first_char) = line.chars().next() else {
continue;
};
match first_char {
'+' => {
new_str.push_str(&line[1..]);
new_str.push('\n');
}
'-' => {
old_str.push_str(&line[1..]);
old_str.push('\n');
}
'\\' => continue,
_ => {}
}
}
let old_len = old_str.trim_end_matches('\n').len();
old_str.truncate(old_len);
let new_len = new_str.trim_end_matches('\n').len();
new_str.truncate(new_len);
(old_str, new_str)
}
fn extract_sed_file_path(script: &str) -> Option<String> {
use std::sync::OnceLock;
static RE: OnceLock<Regex> = OnceLock::new();
let re = RE.get_or_init(|| Regex::new(r"sed\s+-n\s+'[^']*'\s+([^\s]+)").unwrap());
let caps = re.captures(script)?;
Some(
caps.get(1)?
.as_str()
.trim_matches(|c| c == '"' || c == '\'')
.to_string(),
)
}
fn extract_cat_read(script: &str, output: &str) -> Option<(String, String)> {
for line in script.lines() {
let trimmed = line.trim();
if !trimmed.starts_with("cat ") {
continue;
}
let mut fields = trimmed.split_whitespace();
fields.next();
let Some(path_field) = fields.next() else {
continue;
};
let path = path_field.trim_matches(|c| c == '"' || c == '\'');
let clean_output = if let Some(idx) = output.find("\n---") {
output[..idx].trim_end_matches('\n').to_string()
} else {
output.trim_end_matches('\n').to_string()
};
return Some((path.to_string(), clean_output));
}
None
}
#[cfg(test)]
mod tests {
use super::*;
fn response_item(payload: Value) -> CodexLog {
serde_json::from_value(serde_json::json!({
"timestamp": "2026-07-12T00:00:00Z",
"type": "response_item",
"payload": payload
}))
.unwrap()
}
fn custom_output(call_id: &str, blocks: Value) -> CodexLog {
response_item(serde_json::json!({
"type": "custom_tool_call_output",
"call_id": call_id,
"output": blocks
}))
}
fn patch_apply_end(call_id: &str, success: bool, changes: Value) -> CodexLog {
serde_json::from_value(serde_json::json!({
"timestamp": "2026-07-12T00:00:00Z",
"type": "event_msg",
"payload": {
"type": "patch_apply_end",
"call_id": call_id,
"success": success,
"changes": changes
}
}))
.unwrap()
}
fn apply_patch_success_output(file: &str) -> CodexLog {
let wire = serde_json::json!({
"output": format!("Success. Updated the following files:\nM {file}")
})
.to_string();
custom_output("shared-call", Value::String(wire))
}
#[test]
fn legacy_shell_function_parses_into_call() {
let args = r#"{"command":["bash","-lc","ls -la"]}"#;
let call = parse_function_call("shell", args, 42).expect("shell call should parse");
assert_eq!(call.timestamp, 42);
assert_eq!(call.script, "ls -la");
assert_eq!(call.full_command, vec!["bash", "-lc", "ls -la"]);
}
#[test]
fn current_exec_command_function_parses_into_call() {
let args =
r#"{"cmd":"sed -n '1,260p' src/main.rs","workdir":"/repo","yield_time_ms":1000}"#;
let call =
parse_function_call("exec_command", args, 99).expect("exec_command should parse");
assert_eq!(call.timestamp, 99);
assert_eq!(call.script, "sed -n '1,260p' src/main.rs");
assert_eq!(call.full_command, vec!["sed -n '1,260p' src/main.rs"]);
}
#[test]
fn structured_function_arguments_are_normalized_end_to_end() {
let logs = vec![
response_item(serde_json::json!({
"type": "function_call",
"name": "exec_command",
"arguments": { "cmd": "pwd", "workdir": "/repo" },
"call_id": "exec-object"
})),
response_item(serde_json::json!({
"type": "function_call_output",
"call_id": "exec-object",
"output": "done"
})),
];
let analysis = parse_codex_logs(&logs, ParseMode::Full).unwrap();
let record = &analysis.records[0];
assert_eq!(record.tool_call_counts.bash, 1);
assert_eq!(record.run_command_details[0].command, "pwd");
}
#[test]
fn unrelated_function_names_are_ignored() {
assert!(parse_function_call("update_plan", "{}", 0).is_none());
assert!(parse_function_call("_fetch_pr", "{}", 0).is_none());
}
#[test]
fn malformed_arguments_yield_none_instead_of_panicking() {
assert!(parse_function_call("shell", "not json", 0).is_none());
assert!(parse_function_call("exec_command", "not json", 0).is_none());
}
#[test]
fn paired_exec_argument_errors_are_supported_metric_free_lifecycles() {
for arguments in ["{not json", r#"{"command_as_key":""}"#] {
let logs = vec![
response_item(serde_json::json!({
"type": "function_call",
"name": "exec_command",
"arguments": arguments,
"call_id": "bad-exec"
})),
response_item(serde_json::json!({
"type": "function_call_output",
"call_id": "bad-exec",
"output": "Error: failed to parse function arguments"
})),
];
let parsed =
parse_codex_log_iter_with_diagnostics(&logs, ParseMode::Full, None).unwrap();
assert_eq!(parsed.diagnostics.partial_failure_count(), 0);
assert!(!parsed.diagnostics.is_complete_failure());
assert_eq!(parsed.analysis.records[0].tool_call_counts.bash, 0);
assert!(parsed.analysis.records[0].run_command_details.is_empty());
}
}
#[test]
fn invalid_exec_arguments_without_an_explicit_error_remain_drift() {
let logs = vec![
response_item(serde_json::json!({
"type": "function_call",
"name": "exec_command",
"arguments": { "future_command": "pwd" },
"call_id": "future-exec"
})),
response_item(serde_json::json!({
"type": "function_call_output",
"call_id": "future-exec",
"output": "completed"
})),
];
let parsed = parse_codex_log_iter_with_diagnostics(&logs, ParseMode::Full, None).unwrap();
assert!(parsed.diagnostics.is_complete_failure());
assert_eq!(parsed.analysis.records[0].tool_call_counts.bash, 0);
}
#[test]
fn valid_token_count_without_model_context_is_not_schema_failure() {
let logs: Vec<CodexLog> = [
serde_json::json!({
"timestamp": "2026-07-12T00:00:00Z",
"type": "session_meta",
"payload": { "id": "compacted-session", "model_provider": "openai" }
}),
serde_json::json!({
"timestamp": "2026-07-12T00:00:01Z",
"type": "event_msg",
"payload": {
"type": "token_count",
"info": {
"total_token_usage": { "input_tokens": 1, "output_tokens": 1 },
"last_token_usage": { "input_tokens": 1, "output_tokens": 1 },
"model_context_window": 200000
}
}
}),
]
.into_iter()
.map(|value| serde_json::from_value(value).unwrap())
.collect();
let parsed = parse_codex_log_iter_with_diagnostics(&logs, ParseMode::Full, None).unwrap();
assert!(!parsed.diagnostics.is_complete_failure());
assert_eq!(parsed.diagnostics.partial_failure_count(), 0);
assert_eq!(parsed.diagnostics.relevant_records, 1);
assert_eq!(parsed.diagnostics.normalized_records, 1);
assert_eq!(parsed.diagnostics.failed_relevant_records, 0);
assert!(parsed.analysis.records[0].conversation_usage.is_empty());
}
#[test]
fn resumed_usage_subtracts_the_pre_context_replay_baseline() {
let logs: Vec<CodexLog> = [
serde_json::json!({
"timestamp": "2026-07-12T00:00:00Z",
"type": "session_meta",
"payload": { "id": "resumed-session", "model_provider": "openai" }
}),
serde_json::json!({
"timestamp": "2026-07-12T00:00:01Z",
"type": "event_msg",
"payload": {
"type": "token_count",
"info": {
"total_token_usage": {
"input_tokens": 100,
"cached_input_tokens": 20,
"output_tokens": 30,
"reasoning_output_tokens": 10,
"total_tokens": 130
},
"model_context_window": 200000
}
}
}),
serde_json::json!({
"timestamp": "2026-07-12T00:00:02Z",
"type": "turn_context",
"payload": { "model": "gpt-5.3-codex" }
}),
serde_json::json!({
"timestamp": "2026-07-12T00:00:03Z",
"type": "event_msg",
"payload": {
"type": "token_count",
"info": {
"total_token_usage": {
"input_tokens": 160,
"cached_input_tokens": 25,
"output_tokens": 50,
"reasoning_output_tokens": 15,
"total_tokens": 210
},
"last_token_usage": { "input_tokens": 60, "output_tokens": 20 },
"model_context_window": 200000
}
}
}),
serde_json::json!({
"timestamp": "2026-07-12T00:00:04Z",
"type": "event_msg",
"payload": {
"type": "token_count",
"info": {
"total_token_usage": {
"input_tokens": 190,
"cached_input_tokens": 30,
"output_tokens": 60,
"reasoning_output_tokens": 20,
"total_tokens": 250
},
"last_token_usage": { "input_tokens": 30, "output_tokens": 10 },
"model_context_window": 200000
}
}
}),
]
.into_iter()
.map(|value| serde_json::from_value(value).unwrap())
.collect();
let parsed = parse_codex_log_iter_with_diagnostics(&logs, ParseMode::Full, None).unwrap();
assert!(!parsed.diagnostics.is_complete_failure());
assert_eq!(parsed.diagnostics.partial_failure_count(), 0);
let usage = &parsed.analysis.records[0].conversation_usage["gpt-5.3-codex"];
assert_eq!(usage["total_token_usage"]["input_tokens"], 90);
assert_eq!(usage["total_token_usage"]["cached_input_tokens"], 10);
assert_eq!(usage["total_token_usage"]["output_tokens"], 30);
assert_eq!(usage["total_token_usage"]["reasoning_output_tokens"], 10);
assert_eq!(usage["total_token_usage"]["total_tokens"], 120);
assert_eq!(usage["last_token_usage"]["output_tokens"], 10);
assert_eq!(usage["model_context_window"], 200000);
}
#[test]
fn pre_context_usage_is_not_guessed_from_a_later_model() {
let logs: Vec<CodexLog> = [
serde_json::json!({
"timestamp": "2026-07-12T00:00:00Z",
"type": "event_msg",
"payload": {
"type": "token_count",
"info": { "total_token_usage": { "input_tokens": 10 } }
}
}),
serde_json::json!({
"timestamp": "2026-07-12T00:00:01Z",
"type": "turn_context",
"payload": { "model": "gpt-5.3-codex" }
}),
]
.into_iter()
.map(|value| serde_json::from_value(value).unwrap())
.collect();
let parsed = parse_codex_log_iter_with_diagnostics(&logs, ParseMode::Full, None).unwrap();
assert_eq!(parsed.diagnostics.partial_failure_count(), 0);
assert!(parsed.analysis.records[0].conversation_usage.is_empty());
}
#[test]
fn mid_session_model_switch_bills_each_model_its_own_delta() {
let token_count = |input: i64, cached: i64, output: i64, total: i64| {
serde_json::json!({
"timestamp": "2026-07-12T00:00:00Z",
"type": "event_msg",
"payload": {
"type": "token_count",
"info": {
"total_token_usage": {
"input_tokens": input,
"cached_input_tokens": cached,
"output_tokens": output,
"reasoning_output_tokens": 0,
"total_tokens": total
},
"model_context_window": 200000
}
}
})
};
let turn_context = |model: &str| {
serde_json::json!({
"timestamp": "2026-07-12T00:00:00Z",
"type": "turn_context",
"payload": { "model": model }
})
};
let logs: Vec<CodexLog> = [
turn_context("gpt-5.6-luna"),
token_count(20_000, 4_000, 3_289, 23_289),
turn_context("gpt-5.6-sol"),
token_count(60_000, 30_000, 10_000, 70_000),
token_count(90_000, 56_000, 14_576, 104_576),
]
.into_iter()
.map(|value| serde_json::from_value(value).unwrap())
.collect();
let parsed = parse_codex_log_iter_with_diagnostics(&logs, ParseMode::Full, None).unwrap();
assert_eq!(parsed.diagnostics.partial_failure_count(), 0);
let usage = &parsed.analysis.records[0].conversation_usage;
let luna = usage["gpt-5.6-luna"]["total_token_usage"]
.as_object()
.unwrap();
let sol = usage["gpt-5.6-sol"]["total_token_usage"]
.as_object()
.unwrap();
assert_eq!(luna["total_tokens"].as_i64().unwrap(), 23_289);
assert_eq!(sol["total_tokens"].as_i64().unwrap(), 104_576 - 23_289);
assert_eq!(
luna["total_tokens"].as_i64().unwrap() + sol["total_tokens"].as_i64().unwrap(),
104_576,
"the two models' attributed totals must reconstruct the session cumulative"
);
}
#[test]
fn per_turn_tier_classification_uses_the_turns_own_context() {
let event = |cum_input: i64, cum_out: i64, cum_total: i64, last_input: i64| {
serde_json::json!({
"timestamp": "2026-07-12T00:00:00Z",
"type": "event_msg",
"payload": {
"type": "token_count",
"info": {
"total_token_usage": {
"input_tokens": cum_input,
"cached_input_tokens": 0,
"output_tokens": cum_out,
"reasoning_output_tokens": 0,
"total_tokens": cum_total
},
"last_token_usage": { "input_tokens": last_input },
"model_context_window": 400000
}
}
})
};
let logs: Vec<CodexLog> = [
serde_json::json!({
"timestamp": "2026-07-12T00:00:00Z",
"type": "turn_context",
"payload": { "model": "gpt-5.4" }
}),
event(200_000, 1_000, 201_000, 200_000),
event(500_000, 2_500, 502_500, 300_000),
]
.into_iter()
.map(|value| serde_json::from_value(value).unwrap())
.collect();
let tiers =
crate::pricing::TierThresholds::from_entries([("gpt-5.4", 272_000)].into_iter());
let parsed =
parse_codex_log_iter_with_diagnostics(&logs, ParseMode::UsageOnly, Some(&tiers))
.unwrap();
let usage = &parsed.analysis.records[0].conversation_usage["gpt-5.4"];
assert_eq!(usage["total_token_usage"]["total_tokens"], 502_500);
assert_eq!(usage["above_tier"]["input_tokens"], 300_000);
assert_eq!(usage["above_tier"]["output_tokens"], 1_500);
assert!(
usage["above_tier"].get("cache_read_tokens").is_none(),
"no cached tokens in this session"
);
}
#[test]
fn malformed_pre_context_usage_remains_schema_failure() {
let logs: Vec<CodexLog> = [serde_json::json!({
"timestamp": "2026-07-12T00:00:00Z",
"type": "event_msg",
"payload": {
"type": "token_count",
"info": { "total_token_usage": { "input_tokens": "10" } }
}
})]
.into_iter()
.map(|value| serde_json::from_value(value).unwrap())
.collect();
let parsed = parse_codex_log_iter_with_diagnostics(&logs, ParseMode::Full, None).unwrap();
assert!(parsed.diagnostics.is_complete_failure());
assert!(parsed.analysis.records[0].conversation_usage.is_empty());
}
#[test]
fn usage_validation_rejects_unknown_only_and_wrong_typed_token_payloads() {
assert!(is_supported_codex_usage(&serde_json::json!({
"total_token_usage": {}
})));
assert!(is_supported_codex_usage(&serde_json::json!({
"last_token_usage": { "input_tokens": 0, "future_metric": 5 }
})));
assert!(is_supported_codex_usage(&serde_json::json!({
"total_token_usage": { "output_tokens": 0 },
"model_context_window": null
})));
assert!(is_supported_codex_usage(&serde_json::json!({
"model_context_window": 200000
})));
assert!(!is_supported_codex_usage(&serde_json::json!({
"total_token_usage": { "prompt_tokens": 3 }
})));
assert!(!is_supported_codex_usage(&serde_json::json!({
"last_token_usage": { "output_tokens": "3" }
})));
assert!(!is_supported_codex_usage(&serde_json::json!({
"total_token_usage": { "input_tokens": 3 },
"last_token_usage": { "completion_tokens": 1 }
})));
assert!(!is_supported_codex_usage(&serde_json::json!({
"model_context_window": "200000"
})));
}
#[test]
fn exec_command_metadata_prefix_is_stripped() {
let raw = "Chunk ID: deadbeef\n\
Wall time: 0.0000 seconds\n\
Process exited with code 0\n\
Original token count: 100\n\
Output:\n\
line one\n\
line two\n";
assert_eq!(
strip_exec_command_metadata_prefix(raw),
"line one\nline two\n"
);
}
#[test]
fn legacy_shell_output_passes_through_unchanged() {
let raw = "line one\nline two\n";
assert_eq!(strip_exec_command_metadata_prefix(raw), raw);
}
#[test]
fn output_starting_with_marker_handles_no_leading_newline() {
let raw = "Output:\nthe content";
assert_eq!(strip_exec_command_metadata_prefix(raw), "the content");
}
#[test]
fn custom_apply_patch_requires_supported_nonempty_file_headers() {
let drifted = "*** Begin Patch\n*** Future File: src/lib.rs\n+new\n*** End Patch";
assert!(
parse_custom_call("apply_patch", drifted, 1, ParseMode::Full).is_none(),
"an unknown file header must not normalize as a successful patch"
);
for patch in [
"*** Begin Patch\n*** Add File: empty.txt\n*** End Patch",
"*** Begin Patch\n*** Delete File: empty.txt\n*** End Patch",
] {
let Some(CodexCustomCall::ApplyPatch { patches, .. }) =
parse_custom_call("apply_patch", patch, 1, ParseMode::Full)
else {
panic!("supported empty-body patch was rejected");
};
assert_eq!(patches.len(), 1);
assert_eq!(patches[0].file_path, "empty.txt");
}
let empty_path = "*** Begin Patch\n*** Add File:\n+new\n*** End Patch";
assert!(parse_custom_call("apply_patch", empty_path, 1, ParseMode::Full).is_none());
}
#[test]
fn direct_custom_apply_patch_success_is_paired_and_parsed() {
let patch = "*** Begin Patch\n*** Update File: src/lib.rs\n@@\n-old\n+new\n*** End Patch";
let wire_result = serde_json::json!({
"output": "Success. Updated the following files:\nM src/lib.rs",
"metadata": { "exit_code": 0, "duration_seconds": 0.01 }
})
.to_string();
let logs = vec![
response_item(serde_json::json!({
"type": "custom_tool_call",
"name": "apply_patch",
"input": patch,
"call_id": "patch-1"
})),
custom_output("patch-1", Value::String(wire_result)),
];
let analysis = parse_codex_logs(&logs, ParseMode::Full).unwrap();
let record = &analysis.records[0];
assert_eq!(record.tool_call_counts.edit, 1);
assert_eq!(record.edit_file_details.len(), 1);
assert_eq!(record.edit_file_details[0].base.file_path, "src/lib.rs");
}
#[test]
fn direct_object_custom_apply_patch_output_is_paired_and_parsed() {
let patch = "*** Begin Patch\n*** Update File: src/lib.rs\n@@\n-old\n+new\n*** End Patch";
let logs = vec![
response_item(serde_json::json!({
"type": "custom_tool_call",
"name": "apply_patch",
"input": patch,
"call_id": "patch-object"
})),
custom_output(
"patch-object",
serde_json::json!({
"output": "Success. Updated the following files:\nM src/lib.rs",
"metadata": { "exit_code": 0, "duration_seconds": 0.01 }
}),
),
];
let analysis = parse_codex_logs(&logs, ParseMode::Full).unwrap();
let record = &analysis.records[0];
assert_eq!(record.tool_call_counts.edit, 1);
assert_eq!(record.edit_file_details.len(), 1);
}
#[test]
fn failed_direct_custom_apply_patch_is_skipped() {
let patch = "*** Begin Patch\n*** Update File: src/lib.rs\n@@\n-old\n+new\n*** End Patch";
let wire_result = serde_json::json!({
"output": "Failed to find expected lines in src/lib.rs",
"metadata": { "exit_code": 1, "duration_seconds": 0.01 }
})
.to_string();
let logs = vec![
response_item(serde_json::json!({
"type": "custom_tool_call",
"name": "apply_patch",
"input": patch,
"call_id": "patch-failed"
})),
custom_output("patch-failed", Value::String(wire_result)),
];
let parsed = parse_codex_log_iter_with_diagnostics(&logs, ParseMode::Full, None).unwrap();
let record = &parsed.analysis.records[0];
assert_eq!(record.tool_call_counts.edit, 0);
assert!(record.edit_file_details.is_empty());
assert_eq!(parsed.diagnostics.partial_failure_count(), 0);
}
#[test]
fn unknown_direct_custom_apply_patch_result_is_skipped() {
let patch = "*** Begin Patch\n*** Update File: src/lib.rs\n@@\n-old\n+new\n*** End Patch";
for output in [
Value::Null,
serde_json::json!("Patch command finished"),
serde_json::json!({
"success": true,
"updated_files": ["src/lib.rs"]
}),
] {
let logs = vec![
response_item(serde_json::json!({
"type": "custom_tool_call",
"name": "apply_patch",
"input": patch,
"call_id": "patch-unknown"
})),
custom_output("patch-unknown", output),
];
let parsed =
parse_codex_log_iter_with_diagnostics(&logs, ParseMode::Full, None).unwrap();
let record = &parsed.analysis.records[0];
assert_eq!(record.tool_call_counts.edit, 0);
assert!(record.edit_file_details.is_empty());
assert_eq!(parsed.diagnostics.relevant_records, 2);
assert_eq!(parsed.diagnostics.normalized_records, 1);
assert_eq!(parsed.diagnostics.failed_relevant_records, 1);
assert_eq!(parsed.diagnostics.partial_failure_count(), 1);
assert!(!parsed.diagnostics.is_complete_failure());
}
}
#[test]
fn custom_output_envelope_is_stripped_before_json_result() {
let result = serde_json::json!({
"output": "Success. Updated the following files:\nM src/lib.rs",
"metadata": { "exit_code": 0, "duration_seconds": 0.01 }
})
.to_string();
let output = custom_output(
"patch-array",
serde_json::json!([
{ "type": "input_text", "text": "Script completed\nWall time: 0.1 seconds\nOutput:\n" },
{ "type": "input_text", "text": result }
]),
)
.payload
.output;
assert_eq!(
normalize_custom_output(output.as_deref()),
"Success. Updated the following files:\nM src/lib.rs"
);
assert_eq!(
custom_apply_patch_result(output.as_deref()),
CustomApplyPatchResult::Success
);
}
#[test]
fn custom_exec_is_one_bash_cell_not_nested_operations() {
let input = "const results = await Promise.all([tools.exec_command({cmd: \"sed -n '1,2p' src/lib.rs\"}), tools.apply_patch(`*** Begin Patch\n*** Add File: notes.txt\n+hello\n*** End Patch`)]); text(JSON.stringify(results));";
let logs = vec![
response_item(serde_json::json!({
"type": "custom_tool_call",
"name": "exec",
"input": input,
"call_id": "exec-1"
})),
custom_output("exec-1", serde_json::json!("Script completed")),
];
let analysis = parse_codex_logs(&logs, ParseMode::Full).unwrap();
let record = &analysis.records[0];
assert_eq!(record.tool_call_counts.bash, 1);
assert_eq!(record.tool_call_counts.read, 0);
assert_eq!(record.tool_call_counts.write, 0);
assert_eq!(record.tool_call_counts.edit, 0);
assert_eq!(record.run_command_details.len(), 1);
assert_eq!(record.run_command_details[0].command, input);
}
#[test]
fn custom_exec_usage_only_keeps_count_without_source_detail() {
let logs = vec![
response_item(serde_json::json!({
"type": "custom_tool_call",
"name": "exec",
"input": "await tools.exec_command({cmd: dynamicCommand});",
"call_id": "exec-dynamic"
})),
custom_output("exec-dynamic", serde_json::json!("Script completed")),
];
let analysis = parse_codex_log_iter(logs, ParseMode::UsageOnly).unwrap();
let record = &analysis.records[0];
assert_eq!(record.tool_call_counts.bash, 1);
assert!(record.run_command_details.is_empty());
}
#[test]
fn current_exec_command_apply_patch_marker_is_parsed() {
let args = serde_json::json!({
"cmd": "apply_patch <<'PATCH'\n*** Begin Patch\n*** Update File: src/lib.rs\n@@\n-old\n+new\n*** End Patch\nPATCH"
})
.to_string();
let call = parse_function_call("exec_command", &args, 1).unwrap();
let mut state = SessionParseState::new();
state.handle_shell_call(
call,
CodexShellOutput {
output: String::new(),
metadata: None,
},
);
assert_eq!(state.tool_counts.edit, 1);
assert_eq!(state.edit_details.len(), 1);
}
#[test]
fn patch_apply_end_success_counts_add_as_write_and_update_as_edit() {
let changes = serde_json::json!({
"/repo/new.rs": {
"type": "add",
"unified_diff": "@@ -0,0 +1,2 @@\n+fn main() {}\n+// added\n"
},
"/repo/lib.rs": {
"type": "update",
"unified_diff": "@@ -1,1 +1,1 @@\n-old\n+new\n"
}
});
let logs = vec![patch_apply_end("call-1", true, changes)];
let analysis = parse_codex_logs(&logs, ParseMode::Full).unwrap();
let record = &analysis.records[0];
assert_eq!(record.tool_call_counts.write, 1);
assert_eq!(record.tool_call_counts.edit, 1);
assert_eq!(record.total_unique_files, 2);
assert_eq!(record.write_file_details[0].base.file_path, "/repo/new.rs");
assert_eq!(record.edit_file_details[0].base.file_path, "/repo/lib.rs");
}
#[test]
fn patch_apply_end_without_unified_diff_still_counts_the_operation() {
let changes = serde_json::json!({
"/repo/generated.bin": { "type": "add" }
});
let logs = vec![patch_apply_end("call-nodiff", true, changes)];
let parsed = parse_codex_log_iter_with_diagnostics(&logs, ParseMode::Full, None).unwrap();
let record = &parsed.analysis.records[0];
assert_eq!(record.tool_call_counts.write, 1);
assert_eq!(record.total_unique_files, 1);
assert_eq!(record.total_write_lines, 0);
assert_eq!(parsed.diagnostics.partial_failure_count(), 0);
}
#[test]
fn patch_apply_end_failure_creates_no_file_ops() {
let changes = serde_json::json!({
"/repo/x.rs": { "type": "add", "unified_diff": "@@ -0,0 +1,1 @@\n+nope\n" }
});
let logs = vec![patch_apply_end("call-fail", false, changes)];
let parsed = parse_codex_log_iter_with_diagnostics(&logs, ParseMode::Full, None).unwrap();
let record = &parsed.analysis.records[0];
assert_eq!(record.tool_call_counts.write, 0);
assert_eq!(record.total_unique_files, 0);
assert!(record.write_file_details.is_empty());
assert_eq!(parsed.diagnostics.relevant_records, 0);
assert!(!parsed.diagnostics.is_complete_failure());
}
#[test]
fn patch_apply_end_malformed_changes_is_relevant_failure() {
let logs = vec![patch_apply_end(
"call-x",
true,
Value::String("nope".into()),
)];
let parsed = parse_codex_log_iter_with_diagnostics(&logs, ParseMode::Full, None).unwrap();
assert_eq!(parsed.analysis.records[0].tool_call_counts.edit, 0);
assert_eq!(parsed.analysis.records[0].tool_call_counts.write, 0);
assert!(parsed.diagnostics.is_complete_failure());
}
#[test]
fn patch_apply_end_deduped_against_direct_apply_patch_before_output() {
let patch = "*** Begin Patch\n*** Update File: lib.rs\n@@\n-old\n+new\n*** End Patch";
let changes = serde_json::json!({
"lib.rs": { "type": "update", "unified_diff": "@@ -1,1 +1,1 @@\n-old\n+new\n" }
});
let logs = vec![
response_item(serde_json::json!({
"type": "custom_tool_call",
"name": "apply_patch",
"input": patch,
"call_id": "shared-call"
})),
patch_apply_end("shared-call", true, changes),
apply_patch_success_output("lib.rs"),
];
let analysis = parse_codex_logs(&logs, ParseMode::Full).unwrap();
let record = &analysis.records[0];
assert_eq!(record.tool_call_counts.edit, 1);
assert_eq!(record.edit_file_details.len(), 1);
}
#[test]
fn patch_apply_end_deduped_against_direct_apply_patch_after_output() {
let patch = "*** Begin Patch\n*** Update File: lib.rs\n@@\n-old\n+new\n*** End Patch";
let changes = serde_json::json!({
"lib.rs": { "type": "update", "unified_diff": "@@ -1,1 +1,1 @@\n-old\n+new\n" }
});
let logs = vec![
response_item(serde_json::json!({
"type": "custom_tool_call",
"name": "apply_patch",
"input": patch,
"call_id": "shared-call"
})),
apply_patch_success_output("lib.rs"),
patch_apply_end("shared-call", true, changes),
];
let analysis = parse_codex_logs(&logs, ParseMode::Full).unwrap();
let record = &analysis.records[0];
assert_eq!(record.tool_call_counts.edit, 1);
assert_eq!(record.edit_file_details.len(), 1);
}
#[test]
fn unrecognized_direct_output_does_not_suppress_authoritative_event() {
let patch = "*** Begin Patch\n*** Update File: lib.rs\n@@\n-old\n+new\n*** End Patch";
let changes = serde_json::json!({
"lib.rs": { "type": "update", "unified_diff": "@@ -1,1 +1,1 @@\n-old\n+new\n" }
});
let logs = vec![
response_item(serde_json::json!({
"type": "custom_tool_call",
"name": "apply_patch",
"input": patch,
"call_id": "shared-call"
})),
custom_output("shared-call", Value::String("<unknown envelope>".into())),
patch_apply_end("shared-call", true, changes),
];
let analysis = parse_codex_logs(&logs, ParseMode::Full).unwrap();
let record = &analysis.records[0];
assert_eq!(record.tool_call_counts.edit, 1);
assert_eq!(record.edit_file_details.len(), 1);
}
#[test]
fn patch_apply_end_delete_without_diff_counts_the_operation() {
let changes = serde_json::json!({
"/repo/gone.rs": { "type": "delete" }
});
let logs = vec![patch_apply_end("call-del", true, changes)];
let parsed = parse_codex_log_iter_with_diagnostics(&logs, ParseMode::Full, None).unwrap();
let record = &parsed.analysis.records[0];
assert_eq!(record.tool_call_counts.edit, 1);
assert_eq!(record.total_unique_files, 1);
assert_eq!(record.total_edit_lines, 0);
assert_eq!(parsed.diagnostics.partial_failure_count(), 0);
}
#[test]
fn patch_apply_end_scalar_counts_match_across_parse_modes() {
let changes = serde_json::json!({
"/repo/new.rs": {
"type": "add",
"unified_diff": "@@ -0,0 +1,2 @@\n+fn main() {}\n+// added\n"
},
"/repo/lib.rs": {
"type": "update",
"unified_diff": "@@ -1,1 +1,1 @@\n-old\n+new\n"
}
});
let build = || vec![patch_apply_end("call-1", true, changes.clone())];
let full = parse_codex_logs(&build(), ParseMode::Full).unwrap();
let usage = parse_codex_logs(&build(), ParseMode::UsageOnly).unwrap();
let (f, u) = (&full.records[0], &usage.records[0]);
assert_eq!(f.tool_call_counts.write, u.tool_call_counts.write);
assert_eq!(f.tool_call_counts.edit, u.tool_call_counts.edit);
assert_eq!(f.total_unique_files, u.total_unique_files);
assert_eq!(f.total_write_lines, u.total_write_lines);
assert_eq!(f.total_edit_lines, u.total_edit_lines);
assert_eq!(f.total_write_characters, u.total_write_characters);
assert_eq!(f.total_edit_characters, u.total_edit_characters);
assert!(!f.write_file_details.is_empty());
assert!(u.write_file_details.is_empty());
}
}