Codex Handbook
exec/src/event_processor_with_jsonl_output_tests.rs 138 lines
use super::*;use pretty_assertions::assert_eq;use serde_json::json;use tempfile::tempdir;#[test]fn failed_turn_does_not_overwrite_output_last_message_file() {    let tempdir = tempdir().expect("create tempdir");    let output_path = tempdir.path().join("last-message.txt");    std::fs::write(&output_path, "keep existing contents").expect("seed output file");    let mut processor = EventProcessorWithJsonOutput::new(Some(output_path.clone()));    let collected = processor.collect_thread_events(ServerNotification::ItemCompleted(        codex_app_server_protocol::ItemCompletedNotification {            item: ThreadItem::AgentMessage {                id: "msg-1".to_string(),                text: "partial answer".to_string(),                phase: None,                memory_citation: None,            },            thread_id: "thread-1".to_string(),            turn_id: "turn-1".to_string(),            completed_at_ms: 0,        },    ));    assert_eq!(collected.status, CodexStatus::Running);    assert_eq!(processor.final_message(), Some("partial answer"));    let status = processor.process_server_notification(ServerNotification::TurnCompleted(        codex_app_server_protocol::TurnCompletedNotification {            thread_id: "thread-1".to_string(),            turn: codex_app_server_protocol::Turn {                id: "turn-1".to_string(),                items_view: codex_app_server_protocol::TurnItemsView::Full,                items: Vec::new(),                status: TurnStatus::Failed,                error: Some(codex_app_server_protocol::TurnError {                    message: "turn failed".to_string(),                    additional_details: None,                    codex_error_info: None,                }),                started_at: None,                completed_at: Some(0),                duration_ms: None,            },        },    ));    assert_eq!(status, CodexStatus::InitiateShutdown);    assert_eq!(processor.final_message(), None);    EventProcessor::print_final_output(&mut processor);    assert_eq!(        std::fs::read_to_string(&output_path).expect("read output file"),        "keep existing contents"    );}#[test]fn runtime_warning_emits_a_non_fatal_error_item() {    let mut processor = EventProcessorWithJsonOutput::new(/*last_message_path*/ None);    let collected = processor.collect_thread_events(ServerNotification::Warning(        codex_app_server_protocol::WarningNotification {            thread_id: Some("thread-1".to_string()),            message: "invalid global instructions".to_string(),        },    ));    assert_eq!(        collected,        CollectedThreadEvents {            events: vec![ThreadEvent::ItemCompleted(ItemCompletedEvent {                item: ExecThreadItem {                    id: "item_0".to_string(),                    details: ThreadItemDetails::Error(ErrorItem {                        message: "invalid global instructions".to_string(),                    }),                },            })],            status: CodexStatus::Running,        }    );}#[test]fn mcp_tool_call_result_preserves_meta_in_jsonl_event() {    let mut processor = EventProcessorWithJsonOutput::new(/*last_message_path*/ None);    let collected = processor.collect_thread_events(ServerNotification::ItemCompleted(        codex_app_server_protocol::ItemCompletedNotification {            item: ThreadItem::McpToolCall {                id: "mcp-1".to_string(),                server: "search service".to_string(),                tool: "web_run".to_string(),                status: McpToolCallStatus::Completed,                arguments: json!({"search_query": [{"q": "OpenAI Codex CLI documentation"}]}),                mcp_app_resource_uri: None,                plugin_id: None,                result: Some(Box::new(codex_app_server_protocol::McpToolCallResult {                    content: vec![json!({"type": "text", "text": "search result"})],                    structured_content: None,                    meta: Some(json!({"raw_messages": [{"ref_id": "turn0search0"}]})),                })),                error: None,                duration_ms: Some(42),            },            thread_id: "thread-1".to_string(),            turn_id: "turn-1".to_string(),            completed_at_ms: 0,        },    ));    assert_eq!(collected.status, CodexStatus::Running);    assert_eq!(collected.events.len(), 1);    let ThreadEvent::ItemCompleted(ItemCompletedEvent { item }) = &collected.events[0] else {        panic!("expected item.completed event");    };    let ThreadItemDetails::McpToolCall(item) = &item.details else {        panic!("expected MCP tool call item");    };    let result = item.result.as_ref().expect("expected MCP tool result");    assert_eq!(        result.meta,        Some(json!({"raw_messages": [{"ref_id": "turn0search0"}]}))    );    let serialized = serde_json::to_value(&collected.events[0]).expect("serialize event");    assert_eq!(        serialized["item"]["result"]["_meta"],        json!({"raw_messages": [{"ref_id": "turn0search0"}]})    );    assert!(serialized["item"]["result"].get("meta").is_none());}