From 6fb55e52aab06b3db850c6d86ea5799d1813b194 Mon Sep 17 00:00:00 2001 From: Yang Liu Date: Sun, 12 Jul 2026 11:06:02 +1200 Subject: [PATCH 1/2] test(compat): add v0.144.0 regression guards for paste-corrupted session entries MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Codex v0.144.0 (PR #31494) fixed a TUI-level bug where pasting terminal control sequences corrupted resumed conversation history in JSONL session files. Sessions captured before the fix may contain two corruption forms: - Raw ESC bytes in a line (invalid JSON) — already silently skipped by serde_json::from_str(...).ok()? in RawEntry::parse() - Unicode-escaped control sequences () in string values (valid JSON, garbled content) — already parsed without panicking Add four tests that explicitly document and lock in this behavior: - entry.rs: invalid JSON line is silently skipped - entry.rs: response_item with \u001b-encoded sequences parses correctly - entry.rs: all four standard v0.144.0 entry types round-trip correctly - turn.rs: turn building completes without panic when history contains garbled response_item content No functional code changes — the parser already handles both cases. --- src-tauri/src/parser/entry.rs | 57 +++++++++++++++++++++++++++++++++++ src-tauri/src/parser/turn.rs | 26 ++++++++++++++++ 2 files changed, 83 insertions(+) diff --git a/src-tauri/src/parser/entry.rs b/src-tauri/src/parser/entry.rs index e13f0ca..f0377f7 100644 --- a/src-tauri/src/parser/entry.rs +++ b/src-tauri/src/parser/entry.rs @@ -1494,4 +1494,61 @@ 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 #31494): paste-triggered corruption of resumed conversation + // history fixed. Sessions captured before v0.144.0 may contain corrupted/garbled + // entries where pasted terminal control sequences broke JSONL line integrity. + // The parser must gracefully handle both corruption forms without crashing. + + #[test] + fn v0144_paste_corrupted_invalid_json_line_is_silently_skipped() { + // Raw ESC byte (0x1b) embedded in a JSON line breaks JSON structure, + // making the line invalid JSON. serde_json::from_str(...).ok()? returns + // None and the line is silently dropped — pre-v0.144.0 sessions with + // this corruption must not cause panics or parse failures. + let corrupted_line = "{\"type\":\"response_item\",\"payload\":{\"type\":\"message\",\"content\":\"run \x1b[1mmake test\x1b[0m\"}}"; + assert!(RawEntry::parse(corrupted_line).is_none()); + } + + #[test] + fn v0144_response_item_with_unicode_escaped_control_sequences_does_not_panic() { + // A response_item where the content field contains Unicode-escaped ANSI + // sequences (\u001b[1m) is technically valid JSON. The parser must + // accept it without panicking -- the garbled display content is passed + // through as-is. This models pre-v0.144.0 resumed history entries where + // pasted control sequences were JSON-encoded rather than written as raw bytes. + let line = "{\"timestamp\":\"2026-07-01T10:00:00Z\",\"type\":\"response_item\",\"payload\":{\"type\":\"message\",\"role\":\"user\",\"content\":\"run \\u001b[1mmake test\\u001b[0m now\"}}"; + let e = RawEntry::parse(line); + assert!(e.is_some()); + let e = e.unwrap(); + assert_eq!(e.entry_type, "response_item"); + } + + #[test] + fn v0144_all_standard_entry_types_parse_correctly() { + // Regression guard: all four standard JSONL entry types from a v0.144.0 + // session must parse correctly. The paste-corruption fix (PR #31494) is + // a TUI-level change — the JSONL session format is unchanged. + 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":"response_item","payload":{"type":"message","role":"assistant","content":"Hello"}}"#, + r#"{"timestamp":"2026-07-01T10:00:03Z","type":"turn_context","payload":{"model":"gpt-5","cwd":"/project"}}"#, + r#"{"timestamp":"2026-07-01T10:00:04Z","type":"event_msg","payload":{"type":"task_complete","turn_id":"turn-1","completed_at":1751367604.0}}"#, + ]; + let expected_types = [ + "session_meta", + "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"); + } } diff --git a/src-tauri/src/parser/turn.rs b/src-tauri/src/parser/turn.rs index 69a4e3c..3929425 100644 --- a/src-tauri/src/parser/turn.rs +++ b/src-tauri/src/parser/turn.rs @@ -3990,4 +3990,30 @@ 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 #31494): paste-triggered corruption of resumed conversation + // history. Pre-v0.144.0 sessions may include corrupted response_items in their + // resumed history. Turn building must complete without panicking even when history + // entries contain control sequence characters in string content. + + #[test] + fn v0144_paste_corrupted_resumed_history_response_item_does_not_crash_turn_building() { + // A resumed session from before v0.144.0 may contain response_item entries + // where the content field has embedded Unicode-escaped ANSI sequences. + // Turn building must complete without panicking and process subsequent turns. + let lines = [ + r#"{"timestamp":"2026-07-01T10:00:00Z","type":"session_meta","payload":{"id":"v0144-resume","timestamp":"2026-07-01T10:00:00Z","cwd":"/project","cli_version":"0.143.0"}}"#, + r#"{"timestamp":"2026-07-01T09:50:00Z","type":"response_item","payload":{"type":"message","role":"user","content":"run [1mmake test[0m now"}}"#, + 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":"response_item","payload":{"type":"message","role":"assistant","content":"Running tests now."}}"#, + r#"{"timestamp":"2026-07-01T10:00:03Z","type":"event_msg","payload":{"type":"task_complete","turn_id":"turn-1","completed_at":1751367603.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); + } } From 7894069f62ee301096ca4420534f7f04d26a93dc Mon Sep 17 00:00:00 2001 From: Yang Liu Date: Sun, 12 Jul 2026 11:07:39 +1200 Subject: [PATCH 2/2] test(compat): add v0.144.0 regression guards for paste-triggered JSONL corruption Codex v0.144.0 (PR #31494) fixed paste-triggered corruption of conversation history where terminal control sequences (ESC = U+001B) could be embedded unescaped inside JSON strings, producing syntactically invalid JSON lines in session JSONL files. The parser already handled this gracefully: - entry.rs: RawEntry::parse uses serde_json::from_str(...).ok()? which returns None for any invalid JSON line - session.rs: parse_session_inner uses filter_map(RawEntry::parse) which silently skips None results - discover.rs: scan_session_file uses Err(_) => continue to skip unparseable lines during the discovery scan Add regression tests to document and lock in this behavior: - entry.rs: verify corrupted line returns None; adjacent valid line still parses correctly - session.rs: verify a full session with a corrupted line produces the correct turns from surrounding valid entries - discover.rs: verify discover_sessions still returns the correct turn_count when a JSONL file contains a corrupted line Fixes #179 --- src-tauri/src/parser/discover.rs | 42 ++++++++++++++++++++++++++++++++ src-tauri/src/parser/entry.rs | 37 ++++++++++++++++++++++++++++ src-tauri/src/parser/session.rs | 42 ++++++++++++++++++++++++++++++++ 3 files changed, 121 insertions(+) diff --git a/src-tauri/src/parser/discover.rs b/src-tauri/src/parser/discover.rs index ae6ea9a..71b2020 100644 --- a/src-tauri/src/parser/discover.rs +++ b/src-tauri/src/parser/discover.rs @@ -1385,4 +1385,46 @@ mod tests { assert_eq!(session.cwd.as_deref(), Some("/project")); assert!(!session.is_ongoing, "completed session must not be ongoing"); } + + #[test] + fn v0144_discover_session_with_corrupted_line_reports_correct_turn_count() { + // Codex v0.144.0 (PR #31494): paste-triggered TUI corruption fixed. + // Sessions captured before v0.144.0 may contain JSONL lines where pasted terminal + // control sequences (e.g. ESC = U+001B) were embedded unescaped in JSON strings, + // producing syntactically invalid JSON. scan_session_file must skip those lines via + // the `Err(_) => continue` branch in the JSON-parse loop and still return the correct + // turn count and session metadata from the surrounding valid entries. + let tmp = tempdir().unwrap(); + let day_dir = tmp.path().join("2026/07/01"); + std::fs::create_dir_all(&day_dir).unwrap(); + let path = day_dir.join("rollout-2026-07-01T10-00-00-v0144corrupt.jsonl"); + let esc = '\x1b'; + let corrupted_user_msg = format!( + r#"{{"timestamp":"2026-07-01T10:00:02Z","type":"event_msg","payload":{{"type":"user_message","content":"pasted: {}[31m red text"}}}}"#, + esc + ); + std::fs::write( + &path, + [ + r#"{"timestamp":"2026-07-01T10:00:00Z","type":"session_meta","payload":{"id":"v0144-disc-session","timestamp":"2026-07-01T10:00:00Z","cwd":"/project","cli_version":"0.143.0","model_provider":"openai"}}"#, + r#"{"timestamp":"2026-07-01T10:00:01Z","type":"event_msg","payload":{"type":"task_started","turn_id":"turn-1"}}"#, + corrupted_user_msg.as_str(), + r#"{"timestamp":"2026-07-01T10:00:03Z","type":"response_item","payload":{"type":"message","role":"assistant","content":"Done"}}"#, + r#"{"timestamp":"2026-07-01T10:00:04Z","type":"event_msg","payload":{"type":"task_complete","turn_id":"turn-1","completed_at":1751414404.0}}"#, + ] + .join("\n"), + ) + .unwrap(); + let sessions = discover_sessions(tmp.path()).unwrap(); + let session = sessions + .iter() + .find(|s| s.id == "v0144-disc-session") + .expect("session with corrupted line must still be discovered"); + assert_eq!(session.cli_version.as_deref(), Some("0.143.0")); + assert_eq!( + session.turn_count, 1, + "corrupted line must not affect turn count — valid task_started/task_complete pair counts as 1 turn" + ); + assert!(!session.is_ongoing, "completed session must not be ongoing"); + } } diff --git a/src-tauri/src/parser/entry.rs b/src-tauri/src/parser/entry.rs index f0377f7..2871c4c 100644 --- a/src-tauri/src/parser/entry.rs +++ b/src-tauri/src/parser/entry.rs @@ -1551,4 +1551,41 @@ mod tests { assert_eq!(meta.payload["cli_version"], "0.144.0"); assert_eq!(meta.payload["id"], "v0144-session"); } + + // Codex v0.144.0 (PR #31494): paste-triggered TUI corruption fixed. + // Sessions captured before v0.144.0 may contain JSONL lines where pasted terminal + // control sequences (e.g. ESC = U+001B) were embedded unescaped inside JSON strings, + // producing syntactically invalid JSON. RawEntry::parse must return None for such lines + // so callers using filter_map can skip them without panicking. + + #[test] + fn v0144_corrupted_line_with_unescaped_control_sequence_returns_none() { + // Simulate a line corrupted by a pasted ESC sequence before Codex v0.144.0 fixed + // the paste handling. The literal ESC byte (U+001B) inside a JSON string makes + // the line invalid JSON — serde_json rejects it, so parse returns None. + let esc = '\x1b'; + let corrupted = format!( + r#"{{"timestamp":"2026-07-01T10:00:00Z","type":"event_msg","payload":{{"type":"user_message","content":"pasted: {}[31m red"}}}}"#, + esc + ); + assert!( + RawEntry::parse(&corrupted).is_none(), + "line with unescaped control sequence must return None" + ); + } + + #[test] + fn v0144_well_formed_line_adjacent_to_corrupted_line_still_parses() { + // Verify that a well-formed entry in the same pre-v0.144.0 session parses + // correctly even when a neighboring line is corrupted. The caller (filter_map) + // skips None results, so only the corrupted entry is lost. + let well_formed = r#"{"timestamp":"2026-07-01T10:00:01Z","type":"event_msg","payload":{"type":"task_complete","turn_id":"turn-1","completed_at":1751414401.0}}"#; + let entry = RawEntry::parse(well_formed) + .expect("well-formed line adjacent to corrupted entry must still parse"); + assert_eq!(entry.entry_type, "event_msg"); + assert_eq!( + entry.payload.get("type").and_then(|t| t.as_str()), + Some("task_complete") + ); + } } diff --git a/src-tauri/src/parser/session.rs b/src-tauri/src/parser/session.rs index db6a596..46b5d9a 100644 --- a/src-tauri/src/parser/session.rs +++ b/src-tauri/src/parser/session.rs @@ -1630,6 +1630,48 @@ mod tests { assert!(!session.is_ongoing); } + // Codex v0.144.0 (PR #31494): paste-triggered TUI corruption fixed. + // Sessions captured before v0.144.0 may contain JSONL lines where pasted terminal + // control sequences were embedded unescaped in JSON strings, producing invalid JSON. + // parse_session must skip those lines via filter_map(RawEntry::parse) and successfully + // return the turns built from the remaining valid entries. + + #[test] + fn v0144_session_with_corrupted_line_skips_bad_entry_and_returns_valid_turns() { + let tmp = tempdir().unwrap(); + let path = tmp + .path() + .join("rollout-2026-07-01T10-00-00-v0144corrupt.jsonl"); + // Embed a literal ESC byte (U+001B) inside a JSON string to simulate the + // pre-v0.144.0 paste corruption. The surrounding valid lines must still produce + // a complete turn. + let esc = '\x1b'; + let corrupted_user_msg = format!( + r#"{{"timestamp":"2026-07-01T10:00:02Z","type":"event_msg","payload":{{"type":"user_message","content":"pasted: {}[31m red text"}}}}"#, + esc + ); + let lines = [ + r#"{"timestamp":"2026-07-01T10:00:00Z","type":"session_meta","payload":{"id":"v0144-corrupt-session","timestamp":"2026-07-01T10:00:00Z","cwd":"/project","cli_version":"0.143.0","model_provider":"openai"}}"#, + r#"{"timestamp":"2026-07-01T10:00:01Z","type":"event_msg","payload":{"type":"task_started","turn_id":"turn-1"}}"#, + corrupted_user_msg.as_str(), + r#"{"timestamp":"2026-07-01T10:00:03Z","type":"response_item","payload":{"type":"message","role":"assistant","content":"Done"}}"#, + r#"{"timestamp":"2026-07-01T10:00:04Z","type":"event_msg","payload":{"type":"task_complete","turn_id":"turn-1","completed_at":1751414404.0}}"#, + ]; + std::fs::write(&path, lines.join("\n")).unwrap(); + + let session = parse_session(&path).unwrap(); + assert_eq!(session.id, "v0144-corrupt-session"); + // Corrupted user_message line is silently skipped; the turn is still captured + // from task_started / task_complete boundaries. + assert_eq!(session.turns.len(), 1); + assert_eq!( + session.turns[0].final_answer.as_deref(), + Some("Done"), + "final_answer must be populated from the valid response_item after the corrupted line" + ); + assert!(!session.is_ongoing); + } + // Codex v0.142.0 (PR #28968): `metadata` on chat message response_items renamed to // `internal_chat_message_metadata_passthrough`. Sessions written by v0.142.0+ must parse // correctly; final_answer must still be populated from `content`, not the metadata field.