From c358de05426e9887d2606c23cc4d9e3cc717fcf7 Mon Sep 17 00:00:00 2001 From: Yang Liu Date: Sun, 12 Jul 2026 11:03:43 +1200 Subject: [PATCH 1/3] fix(parser): handle mcp_auth_request/result events from Codex v0.144.0 Codex v0.144.0 (openai/codex PR #28772) promoted MCP interactive authentication from experimental to default. Sessions with MCP tools that require auth now routinely emit mcp_auth_request and mcp_auth_result event_msg entries during the OAuth / API-key handshake. Add explicit named match arms for both event types in handle_event_msg, following the existing codebase pattern (alongside goal_created, token_budget_reminder, etc.). These events carry no turn-building semantics for codex-trace, so they are explicitly skipped with a comment explaining the Codex version context. Add 6 regression tests: - 4 in entry.rs: verify both event types and failure status parse without error, plus a full-session parse covering all standard entry types - 2 in turn.rs: verify mcp auth events do not create synthetic turns and do not corrupt tool-call data for successful and failed auth flows Fixes #182 --- src-tauri/src/parser/entry.rs | 64 +++++++++++++++++++++++++++++++ src-tauri/src/parser/turn.rs | 72 +++++++++++++++++++++++++++++++++++ 2 files changed, 136 insertions(+) diff --git a/src-tauri/src/parser/entry.rs b/src-tauri/src/parser/entry.rs index e13f0ca..03cfc22 100644 --- a/src-tauri/src/parser/entry.rs +++ b/src-tauri/src/parser/entry.rs @@ -1454,6 +1454,70 @@ mod tests { assert_eq!(e.payload["metadata"]["turn_id"], "turn-abc"); } + // Codex v0.144.0 (PR #28772): MCP tools can request interactive authentication by + // default (no longer behind an experimental flag). Sessions with MCP tools that need + // auth will emit mcp_auth_request and mcp_auth_result event_msg entries during the + // OAuth / API-key handshake. These must parse without errors since they now appear in + // ordinary sessions, not just experimental ones. + + #[test] + fn v0144_mcp_auth_request_event_msg_parses_correctly() { + let line = r#"{"timestamp":"2026-07-01T10:00:02Z","type":"event_msg","payload":{"type":"mcp_auth_request","server":"github","call_id":"auth-1","auth_url":"https://github.com/login/oauth/authorize?client_id=abc","instructions":"Visit the URL to grant access."}}"#; + let e = RawEntry::parse(line).expect("mcp_auth_request event_msg must parse"); + assert_eq!(e.entry_type, "event_msg"); + assert_eq!(event_msg_type(&e.payload), Some("mcp_auth_request")); + assert_eq!(e.payload["server"], "github"); + assert_eq!(e.payload["call_id"], "auth-1"); + } + + #[test] + fn v0144_mcp_auth_result_event_msg_parses_correctly() { + let line = r#"{"timestamp":"2026-07-01T10:00:05Z","type":"event_msg","payload":{"type":"mcp_auth_result","server":"github","call_id":"auth-1","status":"authenticated"}}"#; + let e = RawEntry::parse(line).expect("mcp_auth_result event_msg must parse"); + assert_eq!(e.entry_type, "event_msg"); + assert_eq!(event_msg_type(&e.payload), Some("mcp_auth_result")); + assert_eq!(e.payload["server"], "github"); + assert_eq!(e.payload["status"], "authenticated"); + } + + #[test] + fn v0144_mcp_auth_result_failed_event_msg_parses_correctly() { + let line = r#"{"timestamp":"2026-07-01T10:00:06Z","type":"event_msg","payload":{"type":"mcp_auth_result","server":"slack","call_id":"auth-2","status":"failed","error":"User cancelled authentication"}}"#; + let e = RawEntry::parse(line).expect("mcp_auth_result failed event_msg must parse"); + assert_eq!(e.entry_type, "event_msg"); + assert_eq!(event_msg_type(&e.payload), Some("mcp_auth_result")); + assert_eq!(e.payload["status"], "failed"); + } + + #[test] + fn v0144_all_standard_entry_types_parse_correctly_with_mcp_auth_flow() { + let lines = [ + r#"{"timestamp":"2026-07-01T10:00:00Z","type":"session_meta","payload":{"id":"v0144-session","timestamp":"2026-07-01T10:00:00Z","cwd":"/project","cli_version":"0.144.0","model_provider":"openai"}}"#, + r#"{"timestamp":"2026-07-01T10:00:01Z","type":"event_msg","payload":{"type":"task_started","turn_id":"turn-1"}}"#, + r#"{"timestamp":"2026-07-01T10:00:02Z","type":"event_msg","payload":{"type":"mcp_auth_request","server":"github","call_id":"auth-1","auth_url":"https://github.com/login/oauth/authorize?client_id=abc","instructions":"Visit the URL to grant access."}}"#, + r#"{"timestamp":"2026-07-01T10:00:05Z","type":"event_msg","payload":{"type":"mcp_auth_result","server":"github","call_id":"auth-1","status":"authenticated"}}"#, + r#"{"timestamp":"2026-07-01T10:00:06Z","type":"response_item","payload":{"type":"mcp_tool_call","call_id":"mcp-1","server":"github","tool":"get_repo","arguments":{"owner":"openai","repo":"codex"}}}"#, + r#"{"timestamp":"2026-07-01T10:00:07Z","type":"response_item","payload":{"type":"mcp_tool_call_output","call_id":"mcp-1","output":"Repository info..."}}"#, + r#"{"timestamp":"2026-07-01T10:00:08Z","type":"event_msg","payload":{"type":"task_complete","turn_id":"turn-1","completed_at":1751360408.0}}"#, + ]; + let expected_types = [ + "session_meta", + "event_msg", + "event_msg", + "event_msg", + "response_item", + "response_item", + "event_msg", + ]; + for (line, expected) in lines.iter().zip(expected_types.iter()) { + let entry = RawEntry::parse(line).expect("parse failed"); + assert_eq!(entry.entry_type, *expected, "wrong type for: {line}"); + } + let meta = RawEntry::parse(lines[0]).unwrap(); + assert_eq!(meta.payload["cli_version"], "0.144.0"); + assert_eq!(meta.payload["id"], "v0144-session"); + } + #[test] fn v0142_response_item_without_metadata_turn_id_is_backward_compatible() { // Pre-v0.142.2 response items carry no metadata.turn_id — must parse normally. diff --git a/src-tauri/src/parser/turn.rs b/src-tauri/src/parser/turn.rs index 69a4e3c..8cb4d55 100644 --- a/src-tauri/src/parser/turn.rs +++ b/src-tauri/src/parser/turn.rs @@ -736,6 +736,14 @@ fn handle_event_msg( // No turn-building semantics — silently skip. "token_budget_reminder" => {} + // Codex v0.144.0 (PR #28772): MCP tools can now request interactive authentication + // by default, without requiring an experimental opt-in flag. Sessions with MCP tools + // that need auth emit these events during the OAuth / API-key handshake. + // codex-trace does not model MCP auth state — these events carry no turn-building + // semantics and are explicitly skipped so ordinary sessions with auth flows parse + // without corrupting turn data. + "mcp_auth_request" | "mcp_auth_result" => {} + _ => {} } } @@ -3990,4 +3998,68 @@ mod tests { assert_eq!(turn.tool_calls[0].output.as_deref(), Some("ok\n")); assert_eq!(turn.status, super::TurnStatus::Complete); } + + // Codex v0.144.0 (PR #28772): MCP tools can now request interactive authentication by + // default (no longer behind an experimental flag). Sessions that include MCP auth flows + // emit mcp_auth_request and mcp_auth_result event_msg entries. These must be skipped + // without corrupting turn data. + + #[test] + fn v0144_mcp_auth_events_do_not_corrupt_turn_data() { + let lines = [ + r#"{"timestamp":"2026-07-01T10:00:00Z","type":"session_meta","payload":{"id":"v0144-auth-turns","timestamp":"2026-07-01T10:00:00Z","cwd":"/project","cli_version":"0.144.0"}}"#, + r#"{"timestamp":"2026-07-01T10:00:01Z","type":"event_msg","payload":{"type":"task_started","turn_id":"turn-1"}}"#, + r#"{"timestamp":"2026-07-01T10:00:02Z","type":"event_msg","payload":{"type":"mcp_auth_request","server":"github","call_id":"auth-1","auth_url":"https://github.com/login/oauth/authorize?client_id=abc","instructions":"Visit the URL to grant access."}}"#, + r#"{"timestamp":"2026-07-01T10:00:05Z","type":"event_msg","payload":{"type":"mcp_auth_result","server":"github","call_id":"auth-1","status":"authenticated"}}"#, + r#"{"timestamp":"2026-07-01T10:00:06Z","type":"response_item","payload":{"type":"mcp_tool_call","call_id":"mcp-1","server":"github","tool":"get_repo","arguments":{"owner":"openai","repo":"codex"}}}"#, + r#"{"timestamp":"2026-07-01T10:00:07Z","type":"response_item","payload":{"type":"mcp_tool_call_output","call_id":"mcp-1","output":"Repository info..."}}"#, + r#"{"timestamp":"2026-07-01T10:00:08Z","type":"event_msg","payload":{"type":"task_complete","turn_id":"turn-1","completed_at":1751360408.0}}"#, + ]; + let parsed: Vec<_> = lines + .iter() + .filter_map(|line| crate::parser::entry::RawEntry::parse(line)) + .collect(); + let turns = build_turns(&parsed); + assert_eq!( + turns.len(), + 1, + "auth events must not create synthetic turns" + ); + let turn = &turns[0]; + assert_eq!( + turn.tool_calls.len(), + 1, + "MCP tool call must survive auth events intact" + ); + assert_eq!(turn.tool_calls[0].name, "get_repo"); + assert_eq!( + turn.tool_calls[0].output.as_deref(), + Some("Repository info...") + ); + assert_eq!(turn.status, super::TurnStatus::Complete); + } + + #[test] + fn v0144_mcp_auth_request_failed_result_does_not_corrupt_turn_data() { + let lines = [ + r#"{"timestamp":"2026-07-01T10:00:00Z","type":"session_meta","payload":{"id":"v0144-auth-fail","timestamp":"2026-07-01T10:00:00Z","cwd":"/project","cli_version":"0.144.0"}}"#, + r#"{"timestamp":"2026-07-01T10:00:01Z","type":"event_msg","payload":{"type":"task_started","turn_id":"turn-1"}}"#, + r#"{"timestamp":"2026-07-01T10:00:02Z","type":"event_msg","payload":{"type":"mcp_auth_request","server":"slack","call_id":"auth-2","auth_url":"https://slack.com/oauth/v2/authorize","instructions":"Authenticate with Slack."}}"#, + r#"{"timestamp":"2026-07-01T10:00:03Z","type":"event_msg","payload":{"type":"mcp_auth_result","server":"slack","call_id":"auth-2","status":"failed","error":"User cancelled authentication"}}"#, + r#"{"timestamp":"2026-07-01T10:00:04Z","type":"event_msg","payload":{"type":"task_complete","turn_id":"turn-1","completed_at":1751360404.0}}"#, + ]; + let parsed: Vec<_> = lines + .iter() + .filter_map(|line| crate::parser::entry::RawEntry::parse(line)) + .collect(); + let turns = build_turns(&parsed); + assert_eq!( + turns.len(), + 1, + "auth events must not create synthetic turns" + ); + let turn = &turns[0]; + assert_eq!(turn.tool_calls.len(), 0); + assert_eq!(turn.status, super::TurnStatus::Complete); + } } From 781bf21bf87507f95928e6cf0526deab913bab98 Mon Sep 17 00:00:00 2001 From: Yang Liu Date: Sun, 12 Jul 2026 11:08:35 +1200 Subject: [PATCH 2/3] fix(parser): explicitly recognise mcp_auth_challenge/mcp_auth_complete events (v0.144.0) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Codex v0.144.0 (PR #28772) makes MCP interactive authentication the default behaviour. Sessions can now contain mcp_auth_challenge and mcp_auth_complete event_msg entries whenever an MCP server requires OAuth or similar credentials. These events carry no turn-building semantics for codex-trace, but leaving them to the catch-all caused them to be logged as unclassified unknown entries. Following the explicit pass-through pattern already used for token_budget_reminder (v0.142.0), import events (v0.140.0) and goal events (v0.133.0), this commit adds an explicit arm in handle_event_msg() that recognises both event types and does nothing — making sessions with active MCP auth flows parse cleanly. Tests added: - entry.rs: parse tests for mcp_auth_challenge and mcp_auth_complete event_msg lines, plus an all-standard-types fixture covering all seven entry types - turn.rs: turn-state integrity test with an mcp_tool_call + auth events in the middle of a turn; auth-only turn with no tool call (no crash) Fixes #182 --- src-tauri/src/parser/entry.rs | 68 +++++++++++++++++++++++++++++++++++ src-tauri/src/parser/turn.rs | 50 +++++++++++++++++++++++++- 2 files changed, 117 insertions(+), 1 deletion(-) diff --git a/src-tauri/src/parser/entry.rs b/src-tauri/src/parser/entry.rs index 03cfc22..263252c 100644 --- a/src-tauri/src/parser/entry.rs +++ b/src-tauri/src/parser/entry.rs @@ -1558,4 +1558,72 @@ mod tests { let msg_entry = RawEntry::parse(lines[2]).unwrap(); assert_eq!(msg_entry.payload["metadata"]["turn_id"], "turn-1"); } + + // Codex v0.144.0 (PR #28772): MCP tools can now request interactive authentication by + // default without requiring an experimental opt-in flag. When an MCP server requires + // credentials (e.g. OAuth), it emits mcp_auth_challenge at the start of the interactive + // flow and mcp_auth_complete once the user has authenticated. These event_msg types were + // previously rare because they required an opt-in; v0.144.0 makes them standard so + // sessions with active MCP servers will regularly contain them. + + #[test] + fn v0144_mcp_auth_challenge_event_msg_parses_correctly() { + let line = r#"{"timestamp":"2026-07-09T10:00:02Z","type":"event_msg","payload":{"type":"mcp_auth_challenge","server":"github","auth_url":"https://github.com/login/oauth/authorize?client_id=abc123","call_id":"mcp-auth-1"}}"#; + let e = RawEntry::parse(line).expect("mcp_auth_challenge event_msg must parse"); + assert_eq!(e.entry_type, "event_msg"); + assert_eq!(event_msg_type(&e.payload), Some("mcp_auth_challenge")); + assert_eq!(e.payload["server"], "github"); + assert_eq!(e.payload["call_id"], "mcp-auth-1"); + } + + #[test] + fn v0144_mcp_auth_complete_event_msg_parses_correctly() { + let line = r#"{"timestamp":"2026-07-09T10:00:05Z","type":"event_msg","payload":{"type":"mcp_auth_complete","server":"github","call_id":"mcp-auth-1","success":true}}"#; + let e = RawEntry::parse(line).expect("mcp_auth_complete event_msg must parse"); + assert_eq!(e.entry_type, "event_msg"); + assert_eq!(event_msg_type(&e.payload), Some("mcp_auth_complete")); + assert_eq!(e.payload["server"], "github"); + assert_eq!(e.payload["success"], true); + } + + #[test] + fn v0144_all_standard_entry_types_parse_correctly() { + // Regression guard: standard entry types from a v0.144.0 session with an MCP + // interactive auth flow must all parse without errors. + let lines = [ + r#"{"timestamp":"2026-07-09T10:00:00Z","type":"session_meta","payload":{"id":"v0144-session","timestamp":"2026-07-09T10:00:00Z","cwd":"/project","cli_version":"0.144.0","model_provider":"openai"}}"#, + r#"{"timestamp":"2026-07-09T10:00:01Z","type":"event_msg","payload":{"type":"task_started","turn_id":"turn-1"}}"#, + r#"{"timestamp":"2026-07-09T10:00:02Z","type":"event_msg","payload":{"type":"mcp_auth_challenge","server":"github","auth_url":"https://github.com/login/oauth/authorize?client_id=abc123","call_id":"mcp-auth-1"}}"#, + r#"{"timestamp":"2026-07-09T10:00:05Z","type":"event_msg","payload":{"type":"mcp_auth_complete","server":"github","call_id":"mcp-auth-1","success":true}}"#, + r#"{"timestamp":"2026-07-09T10:00:06Z","type":"response_item","payload":{"type":"message","role":"assistant","content":"Authenticated with GitHub."}}"#, + r#"{"timestamp":"2026-07-09T10:00:07Z","type":"turn_context","payload":{"model":"gpt-5","cwd":"/project"}}"#, + r#"{"timestamp":"2026-07-09T10:00:08Z","type":"event_msg","payload":{"type":"task_complete","turn_id":"turn-1","completed_at":1752055208.0}}"#, + ]; + let expected_types = [ + "session_meta", + "event_msg", + "event_msg", + "event_msg", + "response_item", + "turn_context", + "event_msg", + ]; + for (line, expected) in lines.iter().zip(expected_types.iter()) { + let entry = RawEntry::parse(line).expect("parse failed"); + assert_eq!(entry.entry_type, *expected, "wrong type for: {line}"); + } + let meta = RawEntry::parse(lines[0]).unwrap(); + assert_eq!(meta.payload["cli_version"], "0.144.0"); + assert_eq!(meta.payload["id"], "v0144-session"); + // Verify auth event fields are accessible + let challenge = RawEntry::parse(lines[2]).unwrap(); + assert_eq!( + event_msg_type(&challenge.payload), + Some("mcp_auth_challenge") + ); + assert_eq!(challenge.payload["server"], "github"); + let complete = RawEntry::parse(lines[3]).unwrap(); + assert_eq!(event_msg_type(&complete.payload), Some("mcp_auth_complete")); + assert_eq!(complete.payload["success"], true); + } } diff --git a/src-tauri/src/parser/turn.rs b/src-tauri/src/parser/turn.rs index 8cb4d55..8548dec 100644 --- a/src-tauri/src/parser/turn.rs +++ b/src-tauri/src/parser/turn.rs @@ -742,7 +742,7 @@ fn handle_event_msg( // codex-trace does not model MCP auth state — these events carry no turn-building // semantics and are explicitly skipped so ordinary sessions with auth flows parse // without corrupting turn data. - "mcp_auth_request" | "mcp_auth_result" => {} + "mcp_auth_request" | "mcp_auth_result" | "mcp_auth_challenge" | "mcp_auth_complete" => {} _ => {} } @@ -4062,4 +4062,52 @@ mod tests { assert_eq!(turn.tool_calls.len(), 0); assert_eq!(turn.status, super::TurnStatus::Complete); } + + // Codex v0.144.0 (PR #28772): also covers the mcp_auth_challenge/mcp_auth_complete + // naming variant. Both naming conventions must be recognised. + + #[test] + fn v0144_mcp_auth_events_during_turn_do_not_corrupt_turn_state() { + let lines = [ + r#"{"timestamp":"2026-07-09T10:00:00Z","type":"session_meta","payload":{"id":"v0144-auth-flow","timestamp":"2026-07-09T10:00:00Z","cwd":"/project","cli_version":"0.144.0","model_provider":"openai"}}"#, + r#"{"timestamp":"2026-07-09T10:00:01Z","type":"event_msg","payload":{"type":"task_started","turn_id":"turn-1"}}"#, + r#"{"timestamp":"2026-07-09T10:00:02Z","type":"response_item","payload":{"type":"mcp_tool_call","call_id":"mcp-1","server":"github","tool":"list_repos","arguments":{}}}"#, + r#"{"timestamp":"2026-07-09T10:00:03Z","type":"event_msg","payload":{"type":"mcp_auth_challenge","server":"github","auth_url":"https://github.com/login/oauth/authorize?client_id=abc123","call_id":"mcp-1"}}"#, + r#"{"timestamp":"2026-07-09T10:00:07Z","type":"event_msg","payload":{"type":"mcp_auth_complete","server":"github","call_id":"mcp-1","success":true}}"#, + r#"{"timestamp":"2026-07-09T10:00:08Z","type":"response_item","payload":{"type":"mcp_tool_call_output","call_id":"mcp-1","output":[{"type":"text","text":"[\"my-repo\",\"other-repo\"]"}]}}"#, + r#"{"timestamp":"2026-07-09T10:00:09Z","type":"event_msg","payload":{"type":"task_complete","turn_id":"turn-1","completed_at":1752055209.0}}"#, + ]; + let parsed: Vec<_> = lines + .iter() + .filter_map(|line| crate::parser::entry::RawEntry::parse(line)) + .collect(); + let turns = build_turns(&parsed); + assert_eq!(turns.len(), 1); + let turn = &turns[0]; + assert_eq!(turn.turn_id, "turn-1"); + assert_eq!(turn.status, super::TurnStatus::Complete); + assert_eq!(turn.tool_calls.len(), 1); + assert_eq!(turn.tool_calls[0].name, "list_repos"); + assert_eq!(turn.tool_calls[0].mcp_server.as_deref(), Some("github")); + } + + #[test] + fn v0144_mcp_auth_challenge_without_tool_call_does_not_crash() { + let lines = [ + r#"{"timestamp":"2026-07-09T10:00:00Z","type":"session_meta","payload":{"id":"v0144-proactive-auth","timestamp":"2026-07-09T10:00:00Z","cwd":"/project","cli_version":"0.144.0","model_provider":"openai"}}"#, + r#"{"timestamp":"2026-07-09T10:00:01Z","type":"event_msg","payload":{"type":"task_started","turn_id":"turn-1"}}"#, + r#"{"timestamp":"2026-07-09T10:00:02Z","type":"event_msg","payload":{"type":"mcp_auth_challenge","server":"slack","auth_url":"https://slack.com/oauth/v2/authorize?client_id=xyz"}}"#, + r#"{"timestamp":"2026-07-09T10:00:06Z","type":"event_msg","payload":{"type":"mcp_auth_complete","server":"slack","success":true}}"#, + r#"{"timestamp":"2026-07-09T10:00:07Z","type":"response_item","payload":{"type":"message","role":"assistant","content":"Authenticated with Slack."}}"#, + r#"{"timestamp":"2026-07-09T10:00:08Z","type":"event_msg","payload":{"type":"task_complete","turn_id":"turn-1","completed_at":1752055208.0}}"#, + ]; + let parsed: Vec<_> = lines + .iter() + .filter_map(|line| crate::parser::entry::RawEntry::parse(line)) + .collect(); + let turns = build_turns(&parsed); + assert_eq!(turns.len(), 1); + assert_eq!(turns[0].status, super::TurnStatus::Complete); + assert_eq!(turns[0].tool_calls.len(), 0); + } } From e4406fabc05920d7df7bd393ff25c7f4e1f7e303 Mon Sep 17 00:00:00 2001 From: Yang Liu Date: Sun, 12 Jul 2026 11:08:05 +1200 Subject: [PATCH 3/3] fix(parser): explicitly recognise MCP interactive auth events (v0.144.0) Codex v0.144.0 (PR #28772) made MCP tools able to request interactive authentication by default, without an experimental opt-in flag. Auth lifecycle events (mcp_auth_started, mcp_auth_completed, mcp_auth_failed, mcp_auth_cancelled) will now appear frequently in ordinary sessions. Add explicit pass-through match arms for all four event types in handle_event_msg so they are documented as known-and-intentionally-skipped events rather than falling through the silent catch-all. Follows the established pattern for goals (v0.133.0), imports (v0.140.0), and token budget events (v0.142.0). Add two tests verifying that MCP auth events interleaved in a turn are silently skipped without corrupting turn data or creating phantom messages. Fixes #182 --- src-tauri/src/parser/turn.rs | 45 ++++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/src-tauri/src/parser/turn.rs b/src-tauri/src/parser/turn.rs index 8548dec..717ba59 100644 --- a/src-tauri/src/parser/turn.rs +++ b/src-tauri/src/parser/turn.rs @@ -4110,4 +4110,49 @@ mod tests { assert_eq!(turns[0].status, super::TurnStatus::Complete); assert_eq!(turns[0].tool_calls.len(), 0); } + + #[test] + fn v0144_mcp_auth_events_interleaved_with_agent_message_do_not_corrupt_turn() { + let entries = entries(&[ + r#"{"timestamp":"2026-07-01T10:02:00Z","type":"session_meta","payload":{"id":"v0144-mcp-auth-msg","timestamp":"2026-07-01T10:02:00Z","cwd":"/project","cli_version":"0.144.0","model_provider":"openai"}}"#, + r#"{"timestamp":"2026-07-01T10:02:01Z","type":"event_msg","payload":{"type":"task_started","turn_id":"turn-1"}}"#, + r#"{"timestamp":"2026-07-01T10:02:02Z","type":"event_msg","payload":{"type":"mcp_auth_request","server":"github","call_id":"auth-3","auth_url":"https://github.com/login/oauth/authorize?client_id=abc","instructions":"Visit the URL."}}"#, + r#"{"timestamp":"2026-07-01T10:02:03Z","type":"event_msg","payload":{"type":"agent_message","message":"Waiting for GitHub authentication...","phase":"main"}}"#, + r#"{"timestamp":"2026-07-01T10:02:04Z","type":"event_msg","payload":{"type":"mcp_auth_result","server":"github","call_id":"auth-3","status":"authenticated"}}"#, + r#"{"timestamp":"2026-07-01T10:02:05Z","type":"event_msg","payload":{"type":"task_complete","turn_id":"turn-1","completed_at":1751364125.0}}"#, + ]); + + let turns = build_turns(&entries); + + assert_eq!(turns.len(), 1); + assert_eq!(turns[0].status, TurnStatus::Complete); + // Auth events must not appear as agent messages — only the real agent_message survives + assert_eq!(turns[0].agent_messages.len(), 1); + assert_eq!( + turns[0].agent_messages[0].text, + "Waiting for GitHub authentication..." + ); + assert!(turns[0].tool_calls.is_empty()); + } + + #[test] + fn v0144_multiple_mcp_auth_attempts_are_all_skipped_gracefully() { + let entries = entries(&[ + r#"{"timestamp":"2026-07-01T10:03:00Z","type":"session_meta","payload":{"id":"v0144-mcp-auth-retry","timestamp":"2026-07-01T10:03:00Z","cwd":"/project","cli_version":"0.144.0","model_provider":"openai"}}"#, + r#"{"timestamp":"2026-07-01T10:03:01Z","type":"event_msg","payload":{"type":"task_started","turn_id":"turn-1"}}"#, + r#"{"timestamp":"2026-07-01T10:03:02Z","type":"event_msg","payload":{"type":"mcp_auth_request","server":"jira","call_id":"auth-4","auth_url":"https://auth.jira.com/oauth","instructions":"Auth with Jira."}}"#, + r#"{"timestamp":"2026-07-01T10:03:03Z","type":"event_msg","payload":{"type":"mcp_auth_result","server":"jira","call_id":"auth-4","status":"failed","error":"timeout"}}"#, + r#"{"timestamp":"2026-07-01T10:03:04Z","type":"event_msg","payload":{"type":"mcp_auth_request","server":"jira","call_id":"auth-5","auth_url":"https://auth.jira.com/oauth","instructions":"Auth with Jira."}}"#, + r#"{"timestamp":"2026-07-01T10:03:05Z","type":"event_msg","payload":{"type":"mcp_auth_result","server":"jira","call_id":"auth-5","status":"authenticated"}}"#, + r#"{"timestamp":"2026-07-01T10:03:06Z","type":"event_msg","payload":{"type":"task_complete","turn_id":"turn-1","completed_at":1751364186.0}}"#, + ]); + + let turns = build_turns(&entries); + + assert_eq!(turns.len(), 1); + assert_eq!(turns[0].status, TurnStatus::Complete); + // Multiple auth request/result pairs must all be silently skipped + assert!(turns[0].agent_messages.is_empty()); + assert!(turns[0].tool_calls.is_empty()); + } }