use super::*;
use crate::config::Verbosity;
use crate::logger::Colors;
#[test]
fn test_parse_codex_thread_started() {
let parser = CodexParser::new(Colors { enabled: false }, Verbosity::Normal);
let json = r#"{"type":"thread.started","thread_id":"xyz789"}"#;
let output = parser.parse_event(json);
assert!(output.is_some());
assert!(output.unwrap().contains("Thread started"));
}
#[test]
fn test_parse_codex_turn_completed() {
let parser = CodexParser::new(Colors { enabled: false }, Verbosity::Normal);
let json = r#"{"type":"turn.completed","usage":{"input_tokens":100,"output_tokens":50}}"#;
let output = parser.parse_event(json);
assert!(output.is_some());
assert!(output.unwrap().contains("Turn completed"));
}
#[test]
fn test_codex_reasoning_completed_normal_verbosity_uses_thinking_label() {
use crate::json_parser::terminal::TerminalMode;
let parser = CodexParser::new(Colors { enabled: false }, Verbosity::Normal)
.with_terminal_mode(TerminalMode::None);
let json = r#"{"type":"item.completed","item":{"type":"reasoning","id":"item_1","text":"some reasoning text"}}"#;
let output = parser.parse_event(json);
assert!(output.is_some());
let out = output.unwrap();
assert!(
out.contains("Thinking"),
"Expected 'Thinking' in normal-verbosity output: {out}"
);
assert!(
!out.contains("Thought:"),
"Should not use 'Thought:' in normal-verbosity: {out}"
);
}
#[test]
fn test_codex_file_operations_shown() {
let parser = CodexParser::new(Colors { enabled: false }, Verbosity::Verbose);
let json = r#"{"type":"item.started","item":{"type":"file_read","path":"/src/main.rs"}}"#;
let output = parser.parse_event(json);
assert!(output.is_some());
let out = output.unwrap();
assert!(out.contains("file_read"));
assert!(out.contains("/src/main.rs"));
}
#[test]
fn test_codex_reasoning_event() {
let parser = CodexParser::new(Colors { enabled: false }, Verbosity::Verbose);
let json = r#"{"type":"item.started","item":{"type":"reasoning","id":"item_1"}}"#;
let output = parser.parse_event(json);
assert!(output.is_some());
assert!(output.unwrap().contains("Reasoning"));
}
#[test]
fn test_codex_reasoning_completed_shows_text() {
use crate::json_parser::terminal::TerminalMode;
let parser = CodexParser::new(Colors { enabled: false }, Verbosity::Verbose)
.with_terminal_mode(TerminalMode::None);
let json = r#"{"type":"item.completed","item":{"type":"reasoning","id":"item_1","text":"I should analyze this file first"}}"#;
let output = parser.parse_event(json);
assert!(output.is_some());
let out = output.unwrap();
assert!(out.contains("Thought"));
assert!(out.contains("analyze"));
}
#[test]
fn test_codex_mcp_tool_call() {
use crate::json_parser::terminal::TerminalMode;
let parser = CodexParser::new(Colors { enabled: false }, Verbosity::Normal)
.with_terminal_mode(TerminalMode::Full);
let json = r#"{"type":"item.started","item":{"type":"mcp_tool_call","tool":"search_files","arguments":{"query":"main"}}}"#;
let output = parser.parse_event(json);
assert!(output.is_some());
let out = output.unwrap();
assert!(out.contains("MCP Tool"));
assert!(out.contains("search_files"));
}
#[test]
fn test_codex_mcp_tool_call_none_mode_is_plain_text() {
use crate::json_parser::terminal::TerminalMode;
let parser = CodexParser::new(Colors { enabled: true }, Verbosity::Normal)
.with_terminal_mode(TerminalMode::None)
.with_display_name("ccs/codex");
let json = r#"{"type":"item.started","item":{"type":"mcp_tool_call","tool":"search_files","arguments":{"query":"main"}}}"#;
let output = parser.parse_event(json).unwrap_or_default();
assert!(output.contains("[ccs/codex] MCP Tool: search_files"));
assert!(output.contains("[ccs/codex] └─"));
assert!(
!output.contains("\x1b["),
"Unexpected ANSI escapes: {output}"
);
}
#[test]
fn test_codex_web_search() {
let parser = CodexParser::new(Colors { enabled: false }, Verbosity::Normal);
let json =
r#"{"type":"item.started","item":{"type":"web_search","query":"rust async tutorial"}}"#;
let output = parser.parse_event(json);
assert!(output.is_some());
let out = output.unwrap();
assert!(out.contains("Search"));
assert!(out.contains("rust async tutorial"));
}
#[test]
fn test_codex_plan_update() {
let parser = CodexParser::new(Colors { enabled: false }, Verbosity::Verbose);
let json = r#"{"type":"item.started","item":{"type":"plan_update","id":"item_1"}}"#;
let output = parser.parse_event(json);
assert!(output.is_some());
assert!(output.unwrap().contains("Updating plan"));
}
#[test]
fn test_codex_turn_completed_with_cached_tokens() {
let parser = CodexParser::new(Colors { enabled: false }, Verbosity::Normal);
let json = r#"{"type":"turn.completed","usage":{"input_tokens":24763,"cached_input_tokens":24448,"output_tokens":122}}"#;
let output = parser.parse_event(json);
assert!(output.is_some());
let out = output.unwrap();
assert!(out.contains("Turn completed"));
assert!(out.contains("in:24763"));
assert!(out.contains("out:122"));
}
#[test]
fn test_codex_item_with_status() {
let parser = CodexParser::new(Colors { enabled: false }, Verbosity::Normal);
let json = r#"{"type":"item.started","item":{"id":"item_1","type":"command_execution","command":"ls","status":"in_progress"}}"#;
let output = parser.parse_event(json);
assert!(output.is_some());
let out = output.unwrap();
assert!(out.contains("Exec"));
assert!(out.contains("ls"));
}
#[test]
fn test_codex_file_write_completed() {
let parser = CodexParser::new(Colors { enabled: false }, Verbosity::Normal);
let json = r#"{"type":"item.completed","item":{"type":"file_write","path":"/src/main.rs"}}"#;
let output = parser.parse_event(json);
assert!(output.is_some());
let out = output.unwrap();
assert!(out.contains("File"));
assert!(out.contains("/src/main.rs"));
}
#[test]
fn test_codex_mcp_completed() {
let parser = CodexParser::new(Colors { enabled: false }, Verbosity::Normal);
let json = r#"{"type":"item.completed","item":{"type":"mcp_tool_call","tool":"read_file"}}"#;
let output = parser.parse_event(json);
assert!(output.is_some());
let out = output.unwrap();
assert!(out.contains("MCP"));
assert!(out.contains("read_file"));
assert!(out.contains("done"));
}
#[test]
fn test_codex_web_search_completed() {
let parser = CodexParser::new(Colors { enabled: false }, Verbosity::Normal);
let json = r#"{"type":"item.completed","item":{"type":"web_search"}}"#;
let output = parser.parse_event(json);
assert!(output.is_some());
assert!(output.unwrap().contains("Search completed"));
}
#[test]
fn test_codex_parser_non_json_passthrough() {
let parser = CodexParser::new(Colors { enabled: false }, Verbosity::Normal);
let output = parser.parse_event("Error: something went wrong");
assert!(output.is_some());
assert!(output.unwrap().contains("Error: something went wrong"));
}
#[test]
fn test_with_terminal_mode() {
use crate::json_parser::terminal::TerminalMode;
let parser = CodexParser::new(Colors { enabled: false }, Verbosity::Normal)
.with_terminal_mode(TerminalMode::None);
let json = r#"{"type":"thread.started","thread_id":"test123"}"#;
let output = parser.parse_event(json);
assert!(output.is_some());
}
#[test]
fn test_codex_result_event() {
let parser = CodexParser::new(Colors { enabled: false }, Verbosity::Normal);
let json =
r#"{"type":"result","result":"This is the accumulated content from agent_message items"}"#;
let output = parser.parse_event(json);
assert!(output.is_none() || output.unwrap().is_empty());
}
#[test]
fn test_codex_result_event_debug_mode() {
let parser = CodexParser::new(Colors { enabled: false }, Verbosity::Debug);
let json = r#"{"type":"result","result":"Debug result content"}"#;
let output = parser.parse_event(json);
assert!(output.is_some());
let out = output.unwrap();
assert!(out.contains("Debug result content"));
}
#[test]
fn test_codex_result_event_is_control_event() {
let parser = CodexParser::new(Colors { enabled: false }, Verbosity::Normal);
let json = r#"{"type":"result","result":"test content"}"#;
let output = parser.parse_event(json);
assert!(output.is_none() || output.unwrap().is_empty());
}
#[test]
fn test_codex_reasoning_no_spam_regression() {
use crate::json_parser::printer::{SharedPrinter, TestPrinter};
use crate::json_parser::terminal::TerminalMode;
use crate::workspace::MemoryWorkspace;
use std::cell::RefCell;
use std::io::Cursor;
use std::rc::Rc;
let workspace = MemoryWorkspace::new_test();
let test_printer = Rc::new(RefCell::new(TestPrinter::new()));
let printer: SharedPrinter = test_printer.clone();
let mut parser =
CodexParser::with_printer(Colors { enabled: false }, Verbosity::Normal, printer)
.with_terminal_mode(TerminalMode::None) .with_display_name("ccs/codex");
let input = r#"{"type":"item.started","item":{"type":"reasoning","text":"**Reading diff in chunks** The diff is too large to read all at once"}}
{"type":"item.started","item":{"type":"reasoning","text":", so I need to break it down"}}
{"type":"item.started","item":{"type":"reasoning","text":" into smaller pieces using an offset and limit."}}
{"type":"item.started","item":{"type":"reasoning","text":" The instructions"}}
{"type":"item.completed","item":{"type":"reasoning"}}
"#;
let reader = Cursor::new(input);
parser.parse_stream(reader, &workspace).unwrap();
let printer_ref = test_printer.borrow();
let output = printer_ref.get_output();
let thinking_line_count = output
.lines()
.filter(|line| line.contains("[ccs/codex]") && line.contains("Thinking:"))
.count();
assert!(
thinking_line_count <= 1,
"Expected at most 1 thinking line in non-TTY mode, got {thinking_line_count}. Output:\n{output}"
);
if thinking_line_count == 1 {
let thinking_line = output
.lines()
.find(|line| line.contains("Thinking:"))
.unwrap();
assert!(
thinking_line.contains("diff") || thinking_line.contains("Reading"),
"Thinking line should contain accumulated content: {thinking_line}"
);
}
}
#[test]
fn test_codex_reasoning_full_mode_in_place_updates() {
use crate::json_parser::printer::{SharedPrinter, TestPrinter};
use crate::json_parser::terminal::TerminalMode;
use crate::workspace::MemoryWorkspace;
use std::cell::RefCell;
use std::io::Cursor;
use std::rc::Rc;
let workspace = MemoryWorkspace::new_test();
let test_printer = Rc::new(RefCell::new(TestPrinter::new()));
let printer: SharedPrinter = test_printer.clone();
let mut parser =
CodexParser::with_printer(Colors { enabled: false }, Verbosity::Normal, printer)
.with_terminal_mode(TerminalMode::Full) .with_display_name("ccs/codex");
let input = r#"{"type":"item.started","item":{"type":"reasoning","text":"First chunk"}}
{"type":"item.started","item":{"type":"reasoning","text":" second chunk"}}
{"type":"item.started","item":{"type":"reasoning","text":" third chunk"}}
{"type":"item.completed","item":{"type":"reasoning"}}
"#;
let reader = Cursor::new(input);
parser.parse_stream(reader, &workspace).unwrap();
let printer_ref = test_printer.borrow();
let output = printer_ref.get_output();
assert!(
!output.contains('\r'),
"Append-only pattern should NOT use carriage return. Output:\n{output}"
);
assert!(
!output.contains("\x1b[1A"),
"Should not contain cursor up in append-only pattern. Output:\n{output}"
);
assert!(
!output.contains("\x1b[2K"),
"Should not contain line clear in append-only pattern. Output:\n{output}"
);
assert!(
!output.contains("\x1b[1B"),
"Should not contain cursor down in append-only pattern. Output:\n{output}"
);
assert!(
output.ends_with('\n'),
"Expected newline at completion. Output:\n{output}"
);
assert!(
output.contains("First chunk second chunk third chunk"),
"Expected accumulated reasoning content. Output:\n{output}"
);
}
#[test]
fn test_codex_concurrent_tool_items_tracked_independently() {
use std::sync::atomic::{AtomicU32, Ordering};
use std::sync::Arc;
let tracker = Arc::new(AtomicU32::new(0));
let parser = CodexParser::new(Colors { enabled: false }, Verbosity::Normal)
.with_tool_activity_tracker(Arc::clone(&tracker));
let item_started_1 = r#"{"type":"item.started","item":{"type":"file_write","path":"/a.rs"}}"#;
parser.parse_event(item_started_1);
assert_eq!(
tracker.load(Ordering::Acquire),
1,
"counter should be 1 after first ItemStarted"
);
let item_started_2 = r#"{"type":"item.started","item":{"type":"file_write","path":"/b.rs"}}"#;
parser.parse_event(item_started_2);
assert_eq!(
tracker.load(Ordering::Acquire),
2,
"counter should be 2 after second ItemStarted"
);
let item_completed_1 =
r#"{"type":"item.completed","item":{"type":"file_change","id":"item_a","path":"/a.rs"}}"#;
parser.parse_event(item_completed_1);
assert_eq!(
tracker.load(Ordering::Acquire),
1,
"counter should be 1 after first ItemCompleted: second item still in flight"
);
let item_completed_2 =
r#"{"type":"item.completed","item":{"type":"file_change","id":"item_b","path":"/b.rs"}}"#;
parser.parse_event(item_completed_2);
assert_eq!(
tracker.load(Ordering::Acquire),
0,
"counter should be 0 after both items complete"
);
}
#[test]
fn test_codex_turn_completed_hard_resets_tracker() {
use std::sync::atomic::{AtomicU32, Ordering};
use std::sync::Arc;
let tracker = Arc::new(AtomicU32::new(0));
let parser = CodexParser::new(Colors { enabled: false }, Verbosity::Normal)
.with_tool_activity_tracker(Arc::clone(&tracker));
parser.parse_event(r#"{"type":"item.started","item":{"type":"file_write","path":"/x.rs"}}"#);
parser.parse_event(r#"{"type":"item.started","item":{"type":"file_write","path":"/y.rs"}}"#);
assert_eq!(
tracker.load(Ordering::Acquire),
2,
"counter should be 2 with two in-flight items"
);
parser
.parse_event(r#"{"type":"turn.completed","usage":{"input_tokens":10,"output_tokens":5}}"#);
assert_eq!(
tracker.load(Ordering::Acquire),
0,
"TurnCompleted must hard-reset counter to 0"
);
}
#[test]
fn test_codex_clear_on_zero_counter_does_not_underflow() {
use std::sync::atomic::{AtomicU32, Ordering};
use std::sync::Arc;
let tracker = Arc::new(AtomicU32::new(0));
let parser = CodexParser::new(Colors { enabled: false }, Verbosity::Normal)
.with_tool_activity_tracker(Arc::clone(&tracker));
parser.parse_event(
r#"{"type":"item.completed","item":{"type":"file_change","id":"x","path":"/x.rs"}}"#,
);
assert_eq!(
tracker.load(Ordering::Acquire),
0,
"saturating_sub must keep counter at 0, not wrap to u32::MAX"
);
}