diff --git a/codex-rs/tui/src/lib.rs b/codex-rs/tui/src/lib.rs index 85aeca9bc..55c190a66 100644 --- a/codex-rs/tui/src/lib.rs +++ b/codex-rs/tui/src/lib.rs @@ -1278,16 +1278,12 @@ async fn run_ratatui_app( } } } else if cli.fork_last { - let filter_cwd = if remote_mode { - latest_session_cwd_filter( - remote_mode, - remote_cwd_override.as_deref(), - &config, - cli.fork_show_all, - ) - } else { - None - }; + let filter_cwd = latest_session_cwd_filter( + remote_mode, + remote_cwd_override.as_deref(), + &config, + cli.fork_show_all, + ); let Some(app_server) = app_server.as_mut() else { unreachable!("app server should be initialized for --fork --last"); }; @@ -1885,6 +1881,181 @@ mod tests { Ok(()) } + #[tokio::test] + async fn latest_session_cwd_filter_respects_scope_options() -> std::io::Result<()> { + let temp_dir = TempDir::new()?; + let config = build_config(&temp_dir).await?; + let remote_cwd = Path::new("repo/on/server"); + + let local_filter = latest_session_cwd_filter( + /*remote_mode*/ false, /*remote_cwd_override*/ None, &config, + /*show_all*/ false, + ); + let show_all_filter = latest_session_cwd_filter( + /*remote_mode*/ false, /*remote_cwd_override*/ None, &config, + /*show_all*/ true, + ); + let remote_filter = latest_session_cwd_filter( + /*remote_mode*/ true, + Some(remote_cwd), + &config, + /*show_all*/ false, + ); + + assert_eq!(local_filter, Some(config.cwd.as_path())); + assert_eq!(show_all_filter, None); + assert_eq!(remote_filter, Some(remote_cwd)); + Ok(()) + } + + #[tokio::test] + async fn fork_last_filters_latest_session_by_cwd_unless_show_all() -> color_eyre::Result<()> { + fn write_session_rollout( + codex_home: &Path, + filename_ts: &str, + meta_rfc3339: &str, + preview: &str, + model_provider: &str, + cwd: &Path, + ) -> color_eyre::Result { + let uuid = Uuid::new_v4(); + let uuid_str = uuid.to_string(); + let thread_id = ThreadId::from_string(&uuid_str)?; + let year = &filename_ts[0..4]; + let month = &filename_ts[5..7]; + let day = &filename_ts[8..10]; + let rollout_path = codex_home + .join("sessions") + .join(year) + .join(month) + .join(day) + .join(format!("rollout-{filename_ts}-{uuid_str}.jsonl")); + let parent = rollout_path.parent().ok_or_else(|| { + color_eyre::eyre::eyre!("rollout path is missing a parent directory") + })?; + std::fs::create_dir_all(parent)?; + + let session_meta = codex_protocol::protocol::SessionMeta { + id: thread_id, + timestamp: meta_rfc3339.to_string(), + cwd: cwd.to_path_buf(), + originator: "codex".to_string(), + cli_version: "0.0.0".to_string(), + source: codex_protocol::protocol::SessionSource::Cli, + model_provider: Some(model_provider.to_string()), + ..Default::default() + }; + let session_meta = serde_json::to_value(codex_protocol::protocol::SessionMetaLine { + meta: session_meta, + git: None, + })?; + let lines = [ + serde_json::json!({ + "timestamp": meta_rfc3339, + "type": "session_meta", + "payload": session_meta, + }) + .to_string(), + serde_json::json!({ + "timestamp": meta_rfc3339, + "type": "response_item", + "payload": { + "type": "message", + "role": "user", + "content": [{"type": "input_text", "text": preview}], + }, + }) + .to_string(), + serde_json::json!({ + "timestamp": meta_rfc3339, + "type": "event_msg", + "payload": { + "type": "user_message", + "message": preview, + "kind": "plain", + }, + }) + .to_string(), + ]; + std::fs::write(&rollout_path, lines.join("\n") + "\n")?; + let updated_at = + chrono::DateTime::parse_from_rfc3339(meta_rfc3339)?.with_timezone(&chrono::Utc); + let times = std::fs::FileTimes::new().set_modified(updated_at.into()); + OpenOptions::new() + .append(true) + .open(rollout_path)? + .set_times(times)?; + + Ok(thread_id) + } + + let temp_dir = TempDir::new()?; + let project_cwd = temp_dir.path().join("project"); + let other_cwd = temp_dir.path().join("other-project"); + std::fs::create_dir_all(&project_cwd)?; + std::fs::create_dir_all(&other_cwd)?; + + let config = ConfigBuilder::default() + .codex_home(temp_dir.path().to_path_buf()) + .harness_overrides(ConfigOverrides { + cwd: Some(project_cwd.clone()), + ..Default::default() + }) + .build() + .await?; + let model_provider = config.model_provider_id.as_str(); + let project_thread_id = write_session_rollout( + temp_dir.path(), + "2025-01-02T10-00-00", + "2025-01-02T10:00:00Z", + "older project session", + model_provider, + &project_cwd, + )?; + let other_thread_id = write_session_rollout( + temp_dir.path(), + "2025-01-02T12-00-00", + "2025-01-02T12:00:00Z", + "newer other project session", + model_provider, + &other_cwd, + )?; + + let mut app_server = + AppServerSession::new(codex_app_server_client::AppServerClient::InProcess( + start_test_embedded_app_server(config.clone()).await?, + )); + let filter_cwd = latest_session_cwd_filter( + /*remote_mode*/ false, /*remote_cwd_override*/ None, &config, + /*show_all*/ false, + ); + let scoped_target = lookup_latest_session_target_with_app_server( + &mut app_server, + &config, + filter_cwd, + /*include_non_interactive*/ false, + ) + .await? + .expect("expected project-scoped fork --last target"); + let show_all_filter_cwd = latest_session_cwd_filter( + /*remote_mode*/ false, /*remote_cwd_override*/ None, &config, + /*show_all*/ true, + ); + let show_all_target = lookup_latest_session_target_with_app_server( + &mut app_server, + &config, + show_all_filter_cwd, + /*include_non_interactive*/ false, + ) + .await? + .expect("expected global fork --last target"); + app_server.shutdown().await?; + + assert_eq!(scoped_target.thread_id, project_thread_id); + assert_eq!(show_all_target.thread_id, other_thread_id); + Ok(()) + } + #[tokio::test] async fn config_cwd_for_app_server_target_omits_cwd_for_remote_sessions() -> std::io::Result<()> {