diff --git a/src-tauri/src/parser/discover.rs b/src-tauri/src/parser/discover.rs index 67130d1..6d89f91 100644 --- a/src-tauri/src/parser/discover.rs +++ b/src-tauri/src/parser/discover.rs @@ -922,11 +922,16 @@ mod tests { // the active profile; `instructions_file` is gone from config so instructions arrive // via `base_instructions.text` or are absent entirely. The discover scanner must handle // both cases without panicking. + // + // Note: As of Codex v0.134.0 (PRs #23883, #24051, #24055, #24059), --profile-v2 was + // renamed to --profile and all legacy profile v1 support was removed. See the v0134_* + // tests below for the corresponding v0.134.0 verification. #[test] fn discover_sessions_v0131_profile_v2_session_discovered_correctly() { - // session_meta with profile-v2 `profile` field: discover scanner must not panic - // and must populate cli_version correctly from the payload. + // session_meta with profile-v2 `profile` field (--profile-v2 renamed to --profile + // in v0.134.0): discover scanner must not panic and must populate cli_version + // correctly from the payload. let tmp = tempdir().unwrap(); let day_dir = tmp.path().join("2026/05/18"); std::fs::create_dir_all(&day_dir).unwrap(); @@ -980,4 +985,71 @@ mod tests { assert_eq!(session.turn_count, 1); assert!(!session.is_ongoing); } + + // Codex v0.134.0 (PRs #23883, #24051, #24055, #24059): --profile-v2 renamed to --profile; + // legacy profile v1 support removed entirely. + // + // codex-trace reads JSONL session files only — it never invokes `codex` or reads Codex + // TOML config. Sessions from v0.134.0+ carry the same `profile` field in session_meta + // as v0.131.0+ sessions. The discover scanner is unaffected; these tests confirm + // v0.134.0 sessions are discovered and indexed correctly. + + #[test] + fn discover_sessions_v0134_profile_session_discovered_correctly() { + // session_meta with --profile active (flag renamed from --profile-v2 in v0.134.0): + // discover scanner must not panic and must populate cli_version correctly. + let tmp = tempdir().unwrap(); + let day_dir = tmp.path().join("2026/05/26"); + std::fs::create_dir_all(&day_dir).unwrap(); + let path = day_dir.join("rollout-2026-05-26T10-00-00-profile.jsonl"); + std::fs::write( + &path, + [ + r#"{"timestamp":"2026-05-26T10:00:00Z","type":"session_meta","payload":{"id":"v0134-disc-profile","timestamp":"2026-05-26T10:00:00Z","cwd":"/home/user","cli_version":"0.134.0","model_provider":"openai","profile":"work","base_instructions":{"text":"You are helpful."}}}"#, + r#"{"timestamp":"2026-05-26T10:00:01Z","type":"event_msg","payload":{"type":"task_started","turn_id":"turn-1"}}"#, + r#"{"timestamp":"2026-05-26T10:00:02Z","type":"event_msg","payload":{"type":"task_complete","turn_id":"turn-1","completed_at":1748254802.0}}"#, + ] + .join("\n"), + ) + .unwrap(); + + let sessions = discover_sessions(tmp.path()).unwrap(); + let session = sessions + .iter() + .find(|s| s.id == "v0134-disc-profile") + .unwrap(); + assert_eq!(session.cli_version.as_deref(), Some("0.134.0")); + assert_eq!(session.turn_count, 1); + assert!(!session.is_ongoing); + } + + #[test] + fn discover_sessions_v0134_no_profile_discovered_correctly() { + // v0.134.0 session without --profile: no `profile` field in session_meta. + // The discover scanner must index the session normally — the absent field has no + // effect on discovery (legacy profile v1 removal is transparent to codex-trace). + let tmp = tempdir().unwrap(); + let day_dir = tmp.path().join("2026/05/26"); + std::fs::create_dir_all(&day_dir).unwrap(); + let path = day_dir.join("rollout-2026-05-26T10-01-00-noprofile.jsonl"); + std::fs::write( + &path, + [ + r#"{"timestamp":"2026-05-26T10:01:00Z","type":"session_meta","payload":{"id":"v0134-disc-noprofile","timestamp":"2026-05-26T10:01:00Z","cwd":"/home/user","cli_version":"0.134.0","model_provider":"openai"}}"#, + r#"{"timestamp":"2026-05-26T10:01:01Z","type":"event_msg","payload":{"type":"task_started","turn_id":"turn-1"}}"#, + r#"{"timestamp":"2026-05-26T10:01:02Z","type":"event_msg","payload":{"type":"task_complete","turn_id":"turn-1","completed_at":1748254862.0}}"#, + ] + .join("\n"), + ) + .unwrap(); + + let sessions = discover_sessions(tmp.path()).unwrap(); + let session = sessions + .iter() + .find(|s| s.id == "v0134-disc-noprofile") + .unwrap(); + assert_eq!(session.cli_version.as_deref(), Some("0.134.0")); + assert_eq!(session.turn_count, 1); + assert!(!session.is_ongoing); + } } diff --git a/src-tauri/src/parser/entry.rs b/src-tauri/src/parser/entry.rs index 23ffe37..09c348d 100644 --- a/src-tauri/src/parser/entry.rs +++ b/src-tauri/src/parser/entry.rs @@ -363,6 +363,10 @@ mod tests { // PR #22647 made Codex reject the legacy [profiles] TOML section when profile-v2 is active. // PR #22724 removed the experimental `instructions_file` config key entirely. // + // Note: As of Codex v0.134.0 (PRs #23883, #24051, #24055, #24059), --profile-v2 was + // renamed to --profile and all legacy profile v1 support was removed. See the v0134_* + // tests below for the corresponding v0.134.0 verification. + // // codex-trace does NOT read Codex CLI config files (TOML profiles). It reads only the // JSONL session files at ~/.codex/sessions/. The profile-v2 changes affect what Codex // writes into session_meta entries: @@ -377,8 +381,9 @@ mod tests { #[test] fn v0131_session_meta_with_profile_v2_field_does_not_panic() { - // session_meta from a v0.131.0 session started with --profile-v2 active. - // The `profile` field names the active profile; codex-trace ignores it gracefully. + // session_meta from a v0.131.0 session started with --profile-v2 active (renamed to + // --profile in v0.134.0). The `profile` field names the active profile; codex-trace + // ignores it gracefully. let line = r#"{"timestamp":"2026-05-18T10:00:00Z","type":"session_meta","payload":{"id":"v0131-profile-v2","timestamp":"2026-05-18T10:00:00Z","cwd":"/tmp","cli_version":"0.131.0","profile":"work","base_instructions":{"text":"You are a helpful assistant."}}}"#; let e = RawEntry::parse(line).expect("session_meta with profile-v2 fields must parse"); assert_eq!(e.entry_type, "session_meta"); @@ -537,4 +542,69 @@ mod tests { assert_eq!(meta.payload["cli_version"], "0.134.0"); assert_eq!(meta.payload["id"], "v0134-session"); } + + // Codex v0.134.0 (PRs #23883, #24051, #24055, #24059): --profile-v2 renamed to --profile; + // legacy profile v1 support removed entirely. + // + // PRs #23883, #24051, #24055, #24059 promoted --profile to the primary profile selector + // and removed all legacy profile v1 resolution and write paths. Passing a legacy profile + // selector now returns an error instead of silently falling back. This is a CLI-level + // change: codex-trace does NOT invoke `codex` at runtime and does NOT read Codex TOML + // config files. Sessions from v0.134.0+ carry the same `profile` field in session_meta + // as v0.131.0+ sessions — the only observable difference for codex-trace is the + // cli_version bump. The parser is unaffected; these tests confirm v0.134.0 sessions + // parse correctly. + + #[test] + fn v0134_session_meta_with_profile_field_does_not_panic() { + // session_meta from v0.134.0 with --profile active (flag renamed from --profile-v2). + // The `profile` field names the active profile; codex-trace reads it gracefully. + let line = r#"{"timestamp":"2026-05-26T10:00:00Z","type":"session_meta","payload":{"id":"v0134-profile","timestamp":"2026-05-26T10:00:00Z","cwd":"/tmp","cli_version":"0.134.0","profile":"work","base_instructions":{"text":"You are a helpful assistant."}}}"#; + let e = RawEntry::parse(line).expect("session_meta with profile field must parse"); + assert_eq!(e.entry_type, "session_meta"); + assert_eq!(e.payload["id"], "v0134-profile"); + assert_eq!(e.payload["cli_version"], "0.134.0"); + assert_eq!(e.payload["profile"], "work"); + assert_eq!( + e.payload["base_instructions"]["text"], + "You are a helpful assistant." + ); + } + + #[test] + fn v0134_session_meta_without_profile_does_not_panic() { + // v0.134.0 session started without --profile: no `profile` field in session_meta. + // The parser must handle the absent field gracefully (opt_str returns None). + let line = r#"{"timestamp":"2026-05-26T10:01:00Z","type":"session_meta","payload":{"id":"v0134-no-profile","timestamp":"2026-05-26T10:01:00Z","cwd":"/home/user","cli_version":"0.134.0","model_provider":"openai"}}"#; + let e = RawEntry::parse(line).expect("session_meta without profile must parse"); + assert_eq!(e.entry_type, "session_meta"); + assert_eq!(e.payload["id"], "v0134-no-profile"); + assert!(e.payload.get("profile").is_none()); + } + + #[test] + fn v0134_all_standard_entry_types_parse_correctly_with_profile() { + // Regression guard: all four standard JSONL entry types must parse under v0.134.0. + let lines = [ + r#"{"timestamp":"2026-05-26T10:02:00Z","type":"session_meta","payload":{"id":"v0134-session-profile","timestamp":"2026-05-26T10:02:00Z","cwd":"/tmp","cli_version":"0.134.0","profile":"default","model_provider":"openai"}}"#, + r#"{"timestamp":"2026-05-26T10:02:01Z","type":"event_msg","payload":{"type":"task_started","turn_id":"turn-1"}}"#, + r#"{"timestamp":"2026-05-26T10:02:02Z","type":"response_item","payload":{"type":"message","role":"assistant","content":"Hello"}}"#, + r#"{"timestamp":"2026-05-26T10:02:03Z","type":"turn_context","payload":{"model":"gpt-5","cwd":"/tmp"}}"#, + r#"{"timestamp":"2026-05-26T10:02:04Z","type":"event_msg","payload":{"type":"task_complete","turn_id":"turn-1","completed_at":1748254924.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.134.0"); + assert_eq!(meta.payload["profile"], "default"); + } } diff --git a/src-tauri/src/parser/session.rs b/src-tauri/src/parser/session.rs index 5389dad..80a1ba8 100644 --- a/src-tauri/src/parser/session.rs +++ b/src-tauri/src/parser/session.rs @@ -682,11 +682,16 @@ mod tests { // the active profile, and instructions now come via `base_instructions.text` from the // profile's system_prompt (the `instructions_file` config key is gone from Codex config). // All cases below must parse without panics and produce correct field values. + // + // Note: As of Codex v0.134.0 (PRs #23883, #24051, #24055, #24059), --profile-v2 was + // renamed to --profile and all legacy profile v1 support was removed. See the v0134_* + // tests below for the corresponding v0.134.0 verification. #[test] fn v0131_profile_v2_session_parses_correctly() { - // session_meta from v0.131.0 with --profile-v2 active: carries `profile` field and - // instructions sourced from the profile's system_prompt via base_instructions.text. + // session_meta from v0.131.0 with --profile-v2 active (renamed to --profile in + // v0.134.0): carries `profile` field and instructions sourced from the profile's + // system_prompt via base_instructions.text. let tmp = tempdir().unwrap(); let path = tmp .path() @@ -818,4 +823,92 @@ mod tests { .expect("spawn_agent tool call should embed worker session"); assert_eq!(worker.id, "worker-v131"); } + + // Codex v0.134.0 (PRs #23883, #24051, #24055, #24059): --profile-v2 renamed to --profile; + // legacy profile v1 support removed entirely. + // + // codex-trace reads JSONL session files only — it never invokes `codex` or reads Codex + // TOML config. Sessions from v0.134.0+ carry the same `profile` field in session_meta + // as v0.131.0+ sessions. The parser is unaffected; these tests confirm v0.134.0 + // sessions parse correctly and produce the expected field values. + + #[test] + fn v0134_profile_session_parses_correctly() { + // session_meta from v0.134.0 with --profile active (flag renamed from --profile-v2). + let tmp = tempdir().unwrap(); + let path = tmp.path().join("rollout-2026-05-26T10-00-00-profile.jsonl"); + std::fs::write( + &path, + [ + r#"{"timestamp":"2026-05-26T10:00:00Z","type":"session_meta","payload":{"id":"v0134-profile","timestamp":"2026-05-26T10:00:00Z","cwd":"/home/user","cli_version":"0.134.0","model_provider":"openai","profile":"work","base_instructions":{"text":"You are a helpful assistant."}}}"#, + r#"{"timestamp":"2026-05-26T10:00:01Z","type":"event_msg","payload":{"type":"task_started","turn_id":"turn-1"}}"#, + r#"{"timestamp":"2026-05-26T10:00:02Z","type":"event_msg","payload":{"type":"task_complete","turn_id":"turn-1","completed_at":1748254802.0}}"#, + ] + .join("\n"), + ) + .unwrap(); + + let session = parse_session(&path).unwrap(); + assert_eq!(session.id, "v0134-profile"); + assert_eq!(session.cli_version.as_deref(), Some("0.134.0")); + assert_eq!( + session.instructions.as_deref(), + Some("You are a helpful assistant.") + ); + assert_eq!(session.turns.len(), 1); + assert!(!session.is_ongoing); + } + + #[test] + fn v0134_session_without_profile_parses_correctly() { + // v0.134.0 session started without --profile: no `profile` field in session_meta. + // parse_session must return None for instructions, not panic. + let tmp = tempdir().unwrap(); + let path = tmp + .path() + .join("rollout-2026-05-26T10-01-00-noprofile.jsonl"); + std::fs::write( + &path, + [ + r#"{"timestamp":"2026-05-26T10:01:00Z","type":"session_meta","payload":{"id":"v0134-no-profile","timestamp":"2026-05-26T10:01:00Z","cwd":"/home/user","cli_version":"0.134.0","model_provider":"openai"}}"#, + r#"{"timestamp":"2026-05-26T10:01:01Z","type":"event_msg","payload":{"type":"task_started","turn_id":"turn-1"}}"#, + r#"{"timestamp":"2026-05-26T10:01:02Z","type":"event_msg","payload":{"type":"task_complete","turn_id":"turn-1","completed_at":1748254862.0}}"#, + ] + .join("\n"), + ) + .unwrap(); + + let session = parse_session(&path).unwrap(); + assert_eq!(session.id, "v0134-no-profile"); + assert_eq!(session.cli_version.as_deref(), Some("0.134.0")); + assert!(session.instructions.is_none()); + assert_eq!(session.turns.len(), 1); + } + + #[test] + fn v0134_legacy_profile_v1_absent_does_not_affect_session_parsing() { + // v0.134.0 removed legacy profile v1 support entirely. Since codex-trace reads only + // JSONL session files (never Codex TOML config), the removal has no effect on + // parsing. Standard v0.134.0 sessions must parse correctly regardless. + let tmp = tempdir().unwrap(); + let path = tmp.path().join("rollout-2026-05-26T10-02-00-v0134.jsonl"); + std::fs::write( + &path, + [ + r#"{"timestamp":"2026-05-26T10:02:00Z","type":"session_meta","payload":{"id":"v0134-standard","timestamp":"2026-05-26T10:02:00Z","cwd":"/workspace","cli_version":"0.134.0","model_provider":"openai","profile":"default"}}"#, + r#"{"timestamp":"2026-05-26T10:02:01Z","type":"event_msg","payload":{"type":"task_started","turn_id":"turn-1"}}"#, + r#"{"timestamp":"2026-05-26T10:02:02Z","type":"response_item","payload":{"type":"message","role":"assistant","content":"Hello"}}"#, + r#"{"timestamp":"2026-05-26T10:02:03Z","type":"turn_context","payload":{"model":"gpt-5","cwd":"/workspace"}}"#, + r#"{"timestamp":"2026-05-26T10:02:04Z","type":"event_msg","payload":{"type":"task_complete","turn_id":"turn-1","completed_at":1748254924.0}}"#, + ] + .join("\n"), + ) + .unwrap(); + + let session = parse_session(&path).unwrap(); + assert_eq!(session.id, "v0134-standard"); + assert_eq!(session.cli_version.as_deref(), Some("0.134.0")); + assert_eq!(session.turns.len(), 1); + assert!(!session.is_ongoing); + } }