Use AbsolutePathBuf for cwd state (#15710)

Migrate `cwd` and related session/config state to `AbsolutePathBuf` so
downstream consumers consistently see absolute working directories.

Add test-only `.abs()` helpers for `Path`, `PathBuf`, and `TempDir`, and
update branch-local tests to use them instead of
`AbsolutePathBuf::try_from(...)`.

For the remaining TUI/app-server snapshot coverage that renders absolute
cwd values, keep the snapshots unchanged and skip the Windows-only cases
where the platform-specific absolute path layout differs.
This commit is contained in:
pakrym-oai
2026-03-25 09:02:22 -07:00
committed by GitHub
Unverified
parent 178c3b15b4
commit 504aeb0e09
65 changed files with 717 additions and 422 deletions
+47 -23
View File
@@ -42,6 +42,8 @@ use crate::read_session_model;
use crate::render::highlight::highlight_bash_to_lines;
use crate::render::renderable::Renderable;
use crate::resume_picker::SessionSelection;
#[cfg(test)]
use crate::test_support::PathBufExt;
use crate::tui;
use crate::tui::TuiEvent;
use crate::update_action::UpdateAction;
@@ -1039,7 +1041,7 @@ impl App {
async fn refresh_in_memory_config_from_disk(&mut self) -> Result<()> {
let mut config = self
.rebuild_config_for_cwd(self.chat_widget.config_ref().cwd.clone())
.rebuild_config_for_cwd(self.chat_widget.config_ref().cwd.to_path_buf())
.await?;
self.apply_runtime_policy_overrides(&mut config);
self.config = config;
@@ -1406,7 +1408,7 @@ impl App {
self.chat_widget.current_model(),
self.chat_widget.current_service_tier(),
),
self.config.cwd.clone(),
self.config.cwd.to_path_buf(),
version,
)
.display_lines(width)
@@ -1702,7 +1704,7 @@ impl App {
cwd: self
.thread_cwd(thread_id)
.await
.unwrap_or_else(|| self.config.cwd.clone()),
.unwrap_or_else(|| self.config.cwd.to_path_buf()),
changes: HashMap::new(),
}),
),
@@ -3077,7 +3079,7 @@ impl App {
chat_widget
.maybe_prompt_windows_sandbox_enable(should_prompt_windows_sandbox_nux_at_startup);
let file_search = FileSearchManager::new(config.cwd.clone(), app_event_tx.clone());
let file_search = FileSearchManager::new(config.cwd.to_path_buf(), app_event_tx.clone());
#[cfg(not(debug_assertions))]
let upgrade_version = crate::updates::get_upgrade_version(&config);
@@ -3144,7 +3146,13 @@ impl App {
let tx = app.app_event_tx.clone();
let logs_base_dir = app.config.codex_home.clone();
let sandbox_policy = app.config.permissions.sandbox_policy.get().clone();
Self::spawn_world_writable_scan(cwd, env_map, logs_base_dir, sandbox_policy, tx);
Self::spawn_world_writable_scan(
cwd.to_path_buf(),
env_map,
logs_base_dir,
sandbox_policy,
tx,
);
}
}
@@ -3369,7 +3377,7 @@ impl App {
.await?
{
SessionSelection::Resume(target_session) => {
let current_cwd = self.config.cwd.clone();
let current_cwd = self.config.cwd.to_path_buf();
let resume_cwd = if self.remote_app_server_url.is_some() {
current_cwd.clone()
} else {
@@ -3417,7 +3425,8 @@ impl App {
self.shutdown_current_thread(app_server).await;
self.config = resume_config;
tui.set_notification_method(self.config.tui_notification_method);
self.file_search.update_search_dir(self.config.cwd.clone());
self.file_search
.update_search_dir(self.config.cwd.to_path_buf());
match self
.replace_chat_widget_with_app_server_thread(tui, resumed)
.await
@@ -3715,7 +3724,8 @@ impl App {
plugin_display_name,
result,
);
if install_succeeded && self.chat_widget.config_ref().cwd == cwd {
if install_succeeded && self.chat_widget.config_ref().cwd.as_path() == cwd.as_path()
{
self.fetch_plugins_list(app_server, cwd.clone());
if should_refresh_plugin_detail {
self.fetch_plugin_detail(
@@ -4190,7 +4200,9 @@ impl App {
plugin_display_name,
result,
);
if uninstall_succeeded && self.chat_widget.config_ref().cwd == cwd {
if uninstall_succeeded
&& self.chat_widget.config_ref().cwd.as_path() == cwd.as_path()
{
self.fetch_plugins_list(app_server, cwd);
}
}
@@ -4377,7 +4389,7 @@ impl App {
let logs_base_dir = self.config.codex_home.clone();
let sandbox_policy = self.config.permissions.sandbox_policy.get().clone();
Self::spawn_world_writable_scan(
cwd,
cwd.to_path_buf(),
env_map,
logs_base_dir,
sandbox_policy,
@@ -6693,7 +6705,7 @@ mod tests {
let (mut app, mut app_event_rx, mut op_rx) = make_test_app_with_channels().await;
let codex_home = tempdir()?;
app.config.codex_home = codex_home.path().to_path_buf();
let config_toml_path = AbsolutePathBuf::try_from(codex_home.path().join("config.toml"))?;
let config_toml_path = codex_home.path().join("config.toml").abs();
let config_toml = "approvals_reviewer = \"guardian_subagent\"\napproval_policy = \"on-request\"\nsandbox_mode = \"workspace-write\"\n\n[features]\nguardian_approval = true\n";
std::fs::write(config_toml_path.as_path(), config_toml)?;
let user_config = toml::from_str::<TomlValue>(config_toml)?;
@@ -6785,7 +6797,7 @@ mod tests {
let codex_home = tempdir()?;
app.config.codex_home = codex_home.path().to_path_buf();
let guardian_approvals = guardian_approvals_mode();
let config_toml_path = AbsolutePathBuf::try_from(codex_home.path().join("config.toml"))?;
let config_toml_path = codex_home.path().join("config.toml").abs();
let config_toml = "approvals_reviewer = \"user\"\n";
std::fs::write(config_toml_path.as_path(), config_toml)?;
let user_config = toml::from_str::<TomlValue>(config_toml)?;
@@ -6852,7 +6864,7 @@ mod tests {
let (mut app, mut app_event_rx, mut op_rx) = make_test_app_with_channels().await;
let codex_home = tempdir()?;
app.config.codex_home = codex_home.path().to_path_buf();
let config_toml_path = AbsolutePathBuf::try_from(codex_home.path().join("config.toml"))?;
let config_toml_path = codex_home.path().join("config.toml").abs();
let config_toml = "approvals_reviewer = \"user\"\napproval_policy = \"on-request\"\nsandbox_mode = \"workspace-write\"\n\n[features]\nguardian_approval = true\n";
std::fs::write(config_toml_path.as_path(), config_toml)?;
let user_config = toml::from_str::<TomlValue>(config_toml)?;
@@ -6913,7 +6925,7 @@ mod tests {
app.config.codex_home = codex_home.path().to_path_buf();
let guardian_approvals = guardian_approvals_mode();
app.active_profile = Some("guardian".to_string());
let config_toml_path = AbsolutePathBuf::try_from(codex_home.path().join("config.toml"))?;
let config_toml_path = codex_home.path().join("config.toml").abs();
let config_toml = "profile = \"guardian\"\napprovals_reviewer = \"user\"\n";
std::fs::write(config_toml_path.as_path(), config_toml)?;
let user_config = toml::from_str::<TomlValue>(config_toml)?;
@@ -6983,7 +6995,7 @@ mod tests {
let codex_home = tempdir()?;
app.config.codex_home = codex_home.path().to_path_buf();
app.active_profile = Some("guardian".to_string());
let config_toml_path = AbsolutePathBuf::try_from(codex_home.path().join("config.toml"))?;
let config_toml_path = codex_home.path().join("config.toml").abs();
let config_toml = r#"
profile = "guardian"
approvals_reviewer = "user"
@@ -7071,7 +7083,7 @@ guardian_approval = true
let codex_home = tempdir()?;
app.config.codex_home = codex_home.path().to_path_buf();
app.active_profile = Some("guardian".to_string());
let config_toml_path = AbsolutePathBuf::try_from(codex_home.path().join("config.toml"))?;
let config_toml_path = codex_home.path().join("config.toml").abs();
let config_toml = "profile = \"guardian\"\napprovals_reviewer = \"guardian_subagent\"\n\n[features]\nguardian_approval = true\n";
std::fs::write(config_toml_path.as_path(), config_toml)?;
let user_config = toml::from_str::<TomlValue>(config_toml)?;
@@ -7652,7 +7664,7 @@ guardian_approval = true
async fn render_clear_ui_header_after_long_transcript_for_snapshot() -> String {
let mut app = make_test_app().await;
app.config.cwd = PathBuf::from("/tmp/project");
app.config.cwd = PathBuf::from("/tmp/project").abs();
app.chat_widget.set_model("gpt-test");
app.chat_widget
.set_reasoning_effort(Some(ReasoningEffortConfig::High));
@@ -7705,7 +7717,7 @@ guardian_approval = true
approval_policy: AskForApproval::Never,
approvals_reviewer: ApprovalsReviewer::User,
sandbox_policy: SandboxPolicy::new_read_only_policy(),
cwd: PathBuf::from("/tmp/project"),
cwd: PathBuf::from("/tmp/project").abs().to_path_buf(),
reasoning_effort: Some(ReasoningEffortConfig::High),
history_log_id: 0,
history_entry_count: 0,
@@ -7763,21 +7775,33 @@ guardian_approval = true
}
#[tokio::test]
#[cfg_attr(
target_os = "windows",
ignore = "snapshot path rendering differs on Windows"
)]
async fn clear_ui_after_long_transcript_snapshots_fresh_header_only() {
let rendered = render_clear_ui_header_after_long_transcript_for_snapshot().await;
assert_snapshot!("clear_ui_after_long_transcript_fresh_header_only", rendered);
}
#[tokio::test]
#[cfg_attr(
target_os = "windows",
ignore = "snapshot path rendering differs on Windows"
)]
async fn ctrl_l_clear_ui_after_long_transcript_reuses_clear_header_snapshot() {
let rendered = render_clear_ui_header_after_long_transcript_for_snapshot().await;
assert_snapshot!("clear_ui_after_long_transcript_fresh_header_only", rendered);
}
#[tokio::test]
#[cfg_attr(
target_os = "windows",
ignore = "snapshot path rendering differs on Windows"
)]
async fn clear_ui_header_shows_fast_status_only_for_gpt54() {
let mut app = make_test_app().await;
app.config.cwd = PathBuf::from("/tmp/project");
app.config.cwd = PathBuf::from("/tmp/project").abs();
app.chat_widget.set_model("gpt-5.4");
app.chat_widget
.set_reasoning_effort(Some(ReasoningEffortConfig::XHigh));
@@ -7803,7 +7827,7 @@ guardian_approval = true
async fn make_test_app() -> App {
let (chat_widget, app_event_tx, _rx, _op_rx) = make_chatwidget_manual_with_sender().await;
let config = chat_widget.config_ref().clone();
let file_search = FileSearchManager::new(config.cwd.clone(), app_event_tx.clone());
let file_search = FileSearchManager::new(config.cwd.to_path_buf(), app_event_tx.clone());
let model = codex_core::test_support::get_model_offline(config.model.as_deref());
let session_telemetry = test_session_telemetry(&config, model.as_str());
@@ -7853,7 +7877,7 @@ guardian_approval = true
) {
let (chat_widget, app_event_tx, rx, op_rx) = make_chatwidget_manual_with_sender().await;
let config = chat_widget.config_ref().clone();
let file_search = FileSearchManager::new(config.cwd.clone(), app_event_tx.clone());
let file_search = FileSearchManager::new(config.cwd.to_path_buf(), app_event_tx.clone());
let model = codex_core::test_support::get_model_offline(config.model.as_deref());
let session_telemetry = test_session_telemetry(&config, model.as_str());
@@ -8598,7 +8622,7 @@ guardian_approval = true
}),
});
assert_eq!(app.chat_widget.config_ref().cwd, next_cwd);
assert_eq!(app.chat_widget.config_ref().cwd.to_path_buf(), next_cwd);
assert_eq!(app.config.cwd, original_cwd);
app.refresh_in_memory_config_from_disk().await?;
@@ -8618,7 +8642,7 @@ guardian_approval = true
let current_cwd = current_config.cwd.clone();
let resume_config = app
.rebuild_config_for_resume_or_fallback(&current_cwd, current_cwd.clone())
.rebuild_config_for_resume_or_fallback(&current_cwd, current_cwd.to_path_buf())
.await?;
assert_eq!(resume_config, current_config);
+26 -10
View File
@@ -207,6 +207,7 @@ use codex_terminal_detection::Multiplexer;
use codex_terminal_detection::TerminalInfo;
use codex_terminal_detection::TerminalName;
use codex_terminal_detection::terminal_info;
use codex_utils_absolute_path::AbsolutePathBuf;
use codex_utils_sleep_inhibitor::SleepInhibitor;
use crossterm::event::KeyCode;
use crossterm::event::KeyEvent;
@@ -1778,7 +1779,12 @@ impl ChatWidget {
self.forked_from = event.forked_from_id;
self.current_rollout_path = event.rollout_path.clone();
self.current_cwd = Some(event.cwd.clone());
self.config.cwd = event.cwd.clone();
match AbsolutePathBuf::try_from(event.cwd.clone()) {
Ok(cwd) => self.config.cwd = cwd,
Err(err) => {
tracing::warn!(path = %event.cwd.display(), %err, "session cwd should be absolute");
}
}
if let Err(err) = self
.config
.permissions
@@ -4026,13 +4032,13 @@ impl ChatWidget {
id: ev.call_id,
reason: ev.reason,
changes: ev.changes.clone(),
cwd: self.config.cwd.clone(),
cwd: self.config.cwd.to_path_buf(),
};
self.bottom_pane
.push_approval_request(request, &self.config.features);
self.request_redraw();
self.notify(Notification::EditApprovalRequested {
cwd: self.config.cwd.clone(),
cwd: self.config.cwd.to_path_buf(),
changes: ev.changes.keys().cloned().collect(),
});
}
@@ -4274,7 +4280,7 @@ impl ChatWidget {
let active_cell = Some(Self::placeholder_session_header_cell(&config));
let current_cwd = Some(config.cwd.clone());
let current_cwd = Some(config.cwd.to_path_buf());
let queued_message_edit_binding = queued_message_edit_binding_for_terminal(terminal_info());
let mut widget = Self {
app_event_tx: app_event_tx.clone(),
@@ -4693,7 +4699,15 @@ impl ChatWidget {
self.app_event_tx.send(AppEvent::ForkCurrentSession);
}
SlashCommand::Init => {
let init_target = self.config.cwd.join(DEFAULT_PROJECT_DOC_FILENAME);
let init_target = match self.config.cwd.join(DEFAULT_PROJECT_DOC_FILENAME) {
Ok(path) => path,
Err(err) => {
self.add_error_message(format!(
"Failed to prepare {DEFAULT_PROJECT_DOC_FILENAME}: {err}",
));
return;
}
};
if init_target.exists() {
let message = format!(
"{DEFAULT_PROJECT_DOC_FILENAME} already exists here. Skipping /init to avoid overwriting it."
@@ -5415,7 +5429,7 @@ impl ChatWidget {
let service_tier = self.config.service_tier.map(Some);
let op = AppCommand::user_turn(
items,
self.config.cwd.clone(),
self.config.cwd.to_path_buf(),
self.config.permissions.approval_policy.value(),
self.config.permissions.sandbox_policy.get().clone(),
effective_mode.model().to_string(),
@@ -7022,7 +7036,9 @@ impl ChatWidget {
}
fn status_line_cwd(&self) -> &Path {
self.current_cwd.as_ref().unwrap_or(&self.config.cwd)
self.current_cwd
.as_deref()
.unwrap_or(self.config.cwd.as_path())
}
fn status_line_project_root(&self) -> Option<PathBuf> {
@@ -9617,7 +9633,7 @@ impl ChatWidget {
placeholder_style,
/*reasoning_effort*/ None,
/*show_fast_status*/ false,
config.cwd.clone(),
config.cwd.to_path_buf(),
CODEX_CLI_VERSION,
))
}
@@ -10331,7 +10347,7 @@ impl ChatWidget {
name: "Review against a base branch".to_string(),
description: Some("(PR Style)".into()),
actions: vec![Box::new({
let cwd = self.config.cwd.clone();
let cwd = self.config.cwd.to_path_buf();
move |tx| {
tx.send(AppEvent::OpenReviewBranchPicker(cwd.clone()));
}
@@ -10356,7 +10372,7 @@ impl ChatWidget {
items.push(SelectionItem {
name: "Review a commit".to_string(),
actions: vec![Box::new({
let cwd = self.config.cwd.clone();
let cwd = self.config.cwd.to_path_buf();
move |tx| {
tx.send(AppEvent::OpenReviewCommitPicker(cwd.clone()));
}
@@ -159,11 +159,11 @@ impl ChatWidget {
cwd: PathBuf,
result: Result<PluginListResponse, String>,
) {
if self.plugins_fetch_state.in_flight_cwd.as_ref() == Some(&cwd) {
if self.plugins_fetch_state.in_flight_cwd.as_deref() == Some(cwd.as_path()) {
self.plugins_fetch_state.in_flight_cwd = None;
}
if self.config.cwd != cwd {
if self.config.cwd.as_path() != cwd.as_path() {
return;
}
@@ -191,13 +191,13 @@ impl ChatWidget {
}
fn prefetch_plugins(&mut self) {
let cwd = self.config.cwd.clone();
if self.plugins_fetch_state.in_flight_cwd.as_ref() == Some(&cwd) {
let cwd = self.config.cwd.to_path_buf();
if self.plugins_fetch_state.in_flight_cwd.as_deref() == Some(cwd.as_path()) {
return;
}
self.plugins_fetch_state.in_flight_cwd = Some(cwd.clone());
if self.plugins_fetch_state.cache_cwd.as_ref() != Some(&cwd) {
if self.plugins_fetch_state.cache_cwd.as_deref() != Some(cwd.as_path()) {
self.plugins_cache = PluginsCacheState::Loading;
}
@@ -205,7 +205,7 @@ impl ChatWidget {
}
fn plugins_cache_for_current_cwd(&self) -> PluginsCacheState {
if self.plugins_fetch_state.cache_cwd.as_ref() == Some(&self.config.cwd) {
if self.plugins_fetch_state.cache_cwd.as_deref() == Some(self.config.cwd.as_path()) {
self.plugins_cache.clone()
} else {
PluginsCacheState::Uninitialized
@@ -253,7 +253,7 @@ impl ChatWidget {
cwd: PathBuf,
result: Result<PluginReadResponse, String>,
) {
if self.config.cwd != cwd {
if self.config.cwd.as_path() != cwd.as_path() {
return;
}
@@ -288,7 +288,7 @@ impl ChatWidget {
plugin_display_name: String,
result: Result<PluginInstallResponse, String>,
) -> bool {
if self.config.cwd != cwd {
if self.config.cwd.as_path() != cwd.as_path() {
return true;
}
@@ -346,7 +346,7 @@ impl ChatWidget {
plugin_display_name: String,
result: Result<PluginUninstallResponse, String>,
) {
if self.config.cwd != cwd {
if self.config.cwd.as_path() != cwd.as_path() {
return;
}
@@ -676,7 +676,7 @@ impl ChatWidget {
..Default::default()
}];
if let Some(plugins_response) = plugins_response.cloned() {
let cwd = self.config.cwd.clone();
let cwd = self.config.cwd.to_path_buf();
items.push(SelectionItem {
name: "Back to plugins".to_string(),
description: Some("Return to the plugin list.".to_string()),
@@ -770,7 +770,7 @@ impl ChatWidget {
"{display_name} {} {} {}",
plugin.id, plugin.name, marketplace_label
);
let cwd = self.config.cwd.clone();
let cwd = self.config.cwd.to_path_buf();
let plugin_display_name = display_name.clone();
let marketplace_path = marketplace.path.clone();
let plugin_name = plugin.name.clone();
@@ -861,7 +861,7 @@ impl ChatWidget {
header.push(Line::from(description.dim()));
}
let cwd = self.config.cwd.clone();
let cwd = self.config.cwd.to_path_buf();
let plugins_response = plugins_response.clone();
let mut items = vec![SelectionItem {
name: "Back to plugins".to_string(),
@@ -877,7 +877,7 @@ impl ChatWidget {
}];
if plugin.summary.installed {
let uninstall_cwd = self.config.cwd.clone();
let uninstall_cwd = self.config.cwd.to_path_buf();
let plugin_id = plugin.summary.id.clone();
let plugin_display_name = display_name;
items.push(SelectionItem {
@@ -906,7 +906,7 @@ impl ChatWidget {
..Default::default()
});
} else {
let install_cwd = self.config.cwd.clone();
let install_cwd = self.config.cwd.to_path_buf();
let marketplace_path = plugin.marketplace_path.clone();
let plugin_name = plugin.summary.name.clone();
let plugin_display_name = display_name;
+36 -30
View File
@@ -17,6 +17,8 @@ use crate::chatwidget::realtime::RealtimeConversationPhase;
use crate::history_cell::UserHistoryCell;
use crate::model_catalog::ModelCatalog;
use crate::test_backend::VT100Backend;
use crate::test_support::PathBufExt;
use crate::test_support::test_path_display;
use crate::tui::FrameRequester;
use assert_matches::assert_matches;
use codex_app_server_protocol::AppSummary;
@@ -458,10 +460,10 @@ async fn session_configured_syncs_widget_config_permissions_and_cwd() {
.sandbox_policy
.set(SandboxPolicy::new_workspace_write_policy())
.expect("set sandbox policy");
chat.config.cwd = PathBuf::from("/home/user/main");
chat.config.cwd = PathBuf::from("/home/user/main").abs();
let expected_sandbox = SandboxPolicy::new_read_only_policy();
let expected_cwd = PathBuf::from("/home/user/sub-agent");
let expected_cwd = PathBuf::from("/home/user/sub-agent").abs();
let configured = codex_protocol::protocol::SessionConfiguredEvent {
session_id: ThreadId::new(),
forked_from_id: None,
@@ -472,7 +474,7 @@ async fn session_configured_syncs_widget_config_permissions_and_cwd() {
approval_policy: AskForApproval::Never,
approvals_reviewer: ApprovalsReviewer::User,
sandbox_policy: expected_sandbox.clone(),
cwd: expected_cwd.clone(),
cwd: expected_cwd.to_path_buf(),
reasoning_effort: Some(ReasoningEffortConfig::default()),
history_log_id: 0,
history_entry_count: 0,
@@ -6426,7 +6428,7 @@ async fn slash_init_skips_when_project_doc_exists() {
let tempdir = tempdir().unwrap();
let existing_path = tempdir.path().join(DEFAULT_PROJECT_DOC_FILENAME);
std::fs::write(&existing_path, "existing instructions").unwrap();
chat.config.cwd = tempdir.path().to_path_buf();
chat.config.cwd = tempdir.path().to_path_buf().abs();
chat.dispatch_command(SlashCommand::Init);
@@ -7419,13 +7421,17 @@ async fn custom_prompt_enter_empty_does_not_send() {
#[tokio::test]
async fn view_image_tool_call_adds_history_cell() {
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(None).await;
let image_path = chat.config.cwd.join("example.png");
let image_path = chat
.config
.cwd
.join("example.png")
.expect("absolute image path");
chat.handle_codex_event(Event {
id: "sub-image".into(),
msg: EventMsg::ViewImageToolCall(ViewImageToolCallEvent {
call_id: "call-image".into(),
path: image_path,
path: image_path.to_path_buf(),
}),
});
@@ -7715,12 +7721,10 @@ fn strip_osc8_for_snapshot(text: &str) -> String {
}
fn plugins_test_absolute_path(path: &str) -> AbsolutePathBuf {
AbsolutePathBuf::try_from(
std::env::temp_dir()
.join("codex-plugin-menu-tests")
.join(path),
)
.expect("expected absolute test path")
std::env::temp_dir()
.join("codex-plugin-menu-tests")
.join(path)
.abs()
}
fn plugins_test_interface(
@@ -7802,7 +7806,7 @@ fn plugins_test_response(marketplaces: Vec<PluginMarketplaceEntry>) -> PluginLis
fn render_loaded_plugins_popup(chat: &mut ChatWidget, response: PluginListResponse) -> String {
let cwd = chat.config.cwd.clone();
chat.on_plugins_loaded(cwd, Ok(response));
chat.on_plugins_loaded(cwd.to_path_buf(), Ok(response));
chat.add_plugins_output();
render_bottom_popup(chat, 100)
}
@@ -7953,10 +7957,10 @@ async fn plugin_detail_popup_snapshot_shows_install_actions_and_capability_summa
summary.clone(),
])]);
let cwd = chat.config.cwd.clone();
chat.on_plugins_loaded(cwd.clone(), Ok(response));
chat.on_plugins_loaded(cwd.to_path_buf(), Ok(response));
chat.add_plugins_output();
chat.on_plugin_detail_loaded(
cwd,
cwd.to_path_buf(),
Ok(PluginReadResponse {
plugin: plugins_test_detail(
summary,
@@ -7993,10 +7997,10 @@ async fn plugin_detail_popup_hides_disclosure_for_installed_plugins() {
summary.clone(),
])]);
let cwd = chat.config.cwd.clone();
chat.on_plugins_loaded(cwd.clone(), Ok(response));
chat.on_plugins_loaded(cwd.to_path_buf(), Ok(response));
chat.add_plugins_output();
chat.on_plugin_detail_loaded(
cwd,
cwd.to_path_buf(),
Ok(PluginReadResponse {
plugin: plugins_test_detail(
summary,
@@ -8083,7 +8087,7 @@ async fn plugins_popup_refresh_replaces_selection_with_first_row() {
),
])]);
let cwd = chat.config.cwd.clone();
chat.on_plugins_loaded(cwd, Ok(refreshed));
chat.on_plugins_loaded(cwd.to_path_buf(), Ok(refreshed));
let after = render_bottom_popup(&chat, 100);
assert!(
@@ -8152,7 +8156,7 @@ async fn plugins_popup_refreshes_installed_counts_after_install() {
),
])]);
let cwd = chat.config.cwd.clone();
chat.on_plugins_loaded(cwd, Ok(refreshed));
chat.on_plugins_loaded(cwd.to_path_buf(), Ok(refreshed));
let after = render_bottom_popup(&chat, 100);
assert!(
@@ -8824,8 +8828,7 @@ async fn apps_initial_load_applies_enabled_state_from_config() {
chat.bottom_pane.set_connectors_enabled(true);
let temp = tempdir().expect("tempdir");
let config_toml_path =
AbsolutePathBuf::try_from(temp.path().join("config.toml")).expect("absolute config path");
let config_toml_path = temp.path().join("config.toml").abs();
let user_config = toml::from_str::<TomlValue>(
"[apps.connector_1]\nenabled = false\ndisabled_reason = \"user\"\n",
)
@@ -8889,8 +8892,7 @@ async fn apps_initial_load_applies_enabled_state_from_requirements_with_user_ove
..Default::default()
};
let temp = tempdir().expect("tempdir");
let config_toml_path =
AbsolutePathBuf::try_from(temp.path().join("config.toml")).expect("absolute config path");
let config_toml_path = temp.path().join("config.toml").abs();
chat.config.config_layer_stack =
ConfigLayerStack::new(Vec::new(), ConfigRequirements::default(), requirements)
.expect("requirements stack")
@@ -9465,7 +9467,7 @@ async fn preset_matching_accepts_workspace_write_with_extra_roots() {
.find(|p| p.id == "auto")
.expect("auto preset exists");
let current_sandbox = SandboxPolicy::WorkspaceWrite {
writable_roots: vec![AbsolutePathBuf::try_from("C:\\extra").unwrap()],
writable_roots: vec![PathBuf::from("C:\\extra").abs()],
read_only_access: Default::default(),
network_access: false,
exclude_tmpdir_env_var: false,
@@ -10265,8 +10267,7 @@ async fn permissions_selection_marks_guardian_approvals_current_with_custom_work
.features
.set_enabled(Feature::GuardianApproval, true);
let extra_root = AbsolutePathBuf::try_from("/tmp/guardian-approvals-extra")
.expect("absolute extra writable root");
let extra_root = PathBuf::from("/tmp/guardian-approvals-extra").abs();
chat.handle_codex_event(Event {
id: "session-configured-custom-workspace".to_string(),
@@ -12279,7 +12280,7 @@ async fn status_line_fast_mode_footer_snapshot() {
#[tokio::test]
async fn status_line_model_with_reasoning_includes_fast_for_gpt54_only() {
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(Some("gpt-5.4")).await;
chat.config.cwd = PathBuf::from("/tmp/project");
chat.config.cwd = PathBuf::from("/tmp/project").abs();
chat.config.tui_status_line = Some(vec![
"model-with-reasoning".to_string(),
"context-remaining".to_string(),
@@ -12289,10 +12290,11 @@ async fn status_line_model_with_reasoning_includes_fast_for_gpt54_only() {
chat.set_service_tier(Some(ServiceTier::Fast));
set_chatgpt_auth(&mut chat);
chat.refresh_status_line();
let test_cwd = test_path_display("/tmp/project");
assert_eq!(
status_line_text(&chat),
Some("gpt-5.4 xhigh fast · 100% left · /tmp/project".to_string())
Some(format!("gpt-5.4 xhigh fast · 100% left · {test_cwd}"))
);
chat.set_model("gpt-5.3-codex");
@@ -12300,18 +12302,22 @@ async fn status_line_model_with_reasoning_includes_fast_for_gpt54_only() {
assert_eq!(
status_line_text(&chat),
Some("gpt-5.3-codex xhigh · 100% left · /tmp/project".to_string())
Some(format!("gpt-5.3-codex xhigh · 100% left · {test_cwd}"))
);
}
#[tokio::test]
#[cfg_attr(
target_os = "windows",
ignore = "snapshot path rendering differs on Windows"
)]
async fn status_line_model_with_reasoning_fast_footer_snapshot() {
use ratatui::Terminal;
use ratatui::backend::TestBackend;
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(Some("gpt-5.4")).await;
chat.show_welcome_banner = false;
chat.config.cwd = PathBuf::from("/tmp/project");
chat.config.cwd = PathBuf::from("/tmp/project").abs();
chat.config.tui_status_line = Some(vec![
"model-with-reasoning".to_string(),
"context-remaining".to_string(),
+9 -3
View File
@@ -27,6 +27,8 @@ use crate::render::line_utils::push_owned_lines;
use crate::render::renderable::Renderable;
use crate::style::proposed_plan_style;
use crate::style::user_message_style;
#[cfg(test)]
use crate::test_support::PathBufExt;
use crate::text_formatting::format_and_truncate_tool_result;
use crate::text_formatting::truncate_text;
use crate::tooltips;
@@ -1143,7 +1145,7 @@ pub(crate) fn new_session_info(
model.clone(),
reasoning_effort,
show_fast_status,
config.cwd.clone(),
config.cwd.to_path_buf(),
CODEX_CLI_VERSION,
);
let mut parts: Vec<Box<dyn HistoryCell>> = vec![Box::new(header)];
@@ -2883,7 +2885,7 @@ mod tests {
approval_policy: AskForApproval::Never,
approvals_reviewer: codex_protocol::config_types::ApprovalsReviewer::User,
sandbox_policy: SandboxPolicy::new_read_only_policy(),
cwd: PathBuf::from("/tmp/project"),
cwd: PathBuf::from("/tmp/project").abs().to_path_buf(),
reasoning_effort: None,
history_log_id: 0,
history_entry_count: 0,
@@ -2997,9 +2999,13 @@ mod tests {
}
#[tokio::test]
#[cfg_attr(
target_os = "windows",
ignore = "snapshot path rendering differs on Windows"
)]
async fn session_info_availability_nux_tooltip_snapshot() {
let mut config = test_config().await;
config.cwd = PathBuf::from("/tmp/project");
config.cwd = PathBuf::from("/tmp/project").abs();
let cell = new_session_info(
&config,
"gpt-5",
+3 -1
View File
@@ -248,6 +248,8 @@ mod wrapping;
#[cfg(test)]
pub mod test_backend;
#[cfg(test)]
pub(crate) mod test_support;
use crate::onboarding::onboarding_screen::OnboardingScreenArgs;
use crate::onboarding::onboarding_screen::run_onboarding_app;
@@ -1221,7 +1223,7 @@ async fn run_ratatui_app(
let fallback_cwd = match action_and_target_session_if_resume_or_fork {
Some((action, target_session)) => {
if remote_mode {
Some(current_cwd.clone())
Some(current_cwd.to_path_buf())
} else {
match resolve_cwd_for_resume_or_fork(
&mut tui,
@@ -85,7 +85,7 @@ impl OnboardingScreen {
app_server_request_handle,
config,
} = args;
let cwd = config.cwd.clone();
let cwd = config.cwd.to_path_buf();
let forced_chatgpt_workspace_id = config.forced_chatgpt_workspace_id.clone();
let forced_login_method = config.forced_login_method;
let codex_home = config.codex_home.clone();
+1 -1
View File
@@ -255,7 +255,7 @@ impl StatusHistoryCell {
Self {
model_name,
model_details,
directory: config.cwd.clone(),
directory: config.cwd.to_path_buf(),
permissions,
agents_summary,
collaboration_mode: collaboration_mode.map(ToString::to_string),
@@ -42,7 +42,7 @@ pub(crate) fn compose_agents_summary(config: &Config) -> String {
.map(|name| name.to_string_lossy().to_string())
.unwrap_or_else(|| "<unknown>".to_string());
let display = if let Some(parent) = p.parent() {
if parent == config.cwd {
if parent == config.cwd.as_path() {
file_name.clone()
} else {
let mut cur = config.cwd.as_path();
+12 -11
View File
@@ -2,6 +2,7 @@ use super::new_status_output;
use super::rate_limit_snapshot_display;
use crate::history_cell::HistoryCell;
use crate::status::StatusAccountDisplay;
use crate::test_support::PathBufExt;
use chrono::Duration as ChronoDuration;
use chrono::TimeZone;
use chrono::Utc;
@@ -105,7 +106,7 @@ async fn status_snapshot_includes_reasoning_details() {
})
.expect("set sandbox policy");
config.cwd = PathBuf::from("/workspace/tests");
config.cwd = PathBuf::from("/workspace/tests").abs();
let account_display = test_status_account_display();
let usage = TokenUsage {
@@ -189,7 +190,7 @@ async fn status_permissions_non_default_workspace_write_is_custom() {
exclude_slash_tmp: false,
})
.expect("set sandbox policy");
config.cwd = PathBuf::from("/workspace/tests");
config.cwd = PathBuf::from("/workspace/tests").abs();
let account_display = test_status_account_display();
let usage = TokenUsage::default();
@@ -238,7 +239,7 @@ async fn status_snapshot_includes_forked_from() {
let mut config = test_config(&temp_home).await;
config.model = Some("gpt-5.1-codex-max".to_string());
config.model_provider_id = "openai".to_string();
config.cwd = PathBuf::from("/workspace/tests");
config.cwd = PathBuf::from("/workspace/tests").abs();
let account_display = test_status_account_display();
let usage = TokenUsage {
@@ -292,7 +293,7 @@ async fn status_snapshot_includes_monthly_limit() {
let mut config = test_config(&temp_home).await;
config.model = Some("gpt-5.1-codex-max".to_string());
config.model_provider_id = "openai".to_string();
config.cwd = PathBuf::from("/workspace/tests");
config.cwd = PathBuf::from("/workspace/tests").abs();
let account_display = test_status_account_display();
let usage = TokenUsage {
@@ -545,7 +546,7 @@ async fn status_card_token_usage_excludes_cached_tokens() {
let temp_home = TempDir::new().expect("temp home");
let mut config = test_config(&temp_home).await;
config.model = Some("gpt-5.1-codex-max".to_string());
config.cwd = PathBuf::from("/workspace/tests");
config.cwd = PathBuf::from("/workspace/tests").abs();
let account_display = test_status_account_display();
let usage = TokenUsage {
@@ -593,7 +594,7 @@ async fn status_snapshot_truncates_in_narrow_terminal() {
config.model = Some("gpt-5.1-codex-max".to_string());
config.model_provider_id = "openai".to_string();
config.model_reasoning_summary = Some(ReasoningSummary::Detailed);
config.cwd = PathBuf::from("/workspace/tests");
config.cwd = PathBuf::from("/workspace/tests").abs();
let account_display = test_status_account_display();
let usage = TokenUsage {
@@ -656,7 +657,7 @@ async fn status_snapshot_shows_missing_limits_message() {
let temp_home = TempDir::new().expect("temp home");
let mut config = test_config(&temp_home).await;
config.model = Some("gpt-5.1-codex-max".to_string());
config.cwd = PathBuf::from("/workspace/tests");
config.cwd = PathBuf::from("/workspace/tests").abs();
let account_display = test_status_account_display();
let usage = TokenUsage {
@@ -704,7 +705,7 @@ async fn status_snapshot_includes_credits_and_limits() {
let temp_home = TempDir::new().expect("temp home");
let mut config = test_config(&temp_home).await;
config.model = Some("gpt-5.1-codex".to_string());
config.cwd = PathBuf::from("/workspace/tests");
config.cwd = PathBuf::from("/workspace/tests").abs();
let account_display = test_status_account_display();
let usage = TokenUsage {
@@ -773,7 +774,7 @@ async fn status_snapshot_shows_empty_limits_message() {
let temp_home = TempDir::new().expect("temp home");
let mut config = test_config(&temp_home).await;
config.model = Some("gpt-5.1-codex-max".to_string());
config.cwd = PathBuf::from("/workspace/tests");
config.cwd = PathBuf::from("/workspace/tests").abs();
let account_display = test_status_account_display();
let usage = TokenUsage {
@@ -830,7 +831,7 @@ async fn status_snapshot_shows_stale_limits_message() {
let temp_home = TempDir::new().expect("temp home");
let mut config = test_config(&temp_home).await;
config.model = Some("gpt-5.1-codex-max".to_string());
config.cwd = PathBuf::from("/workspace/tests");
config.cwd = PathBuf::from("/workspace/tests").abs();
let account_display = test_status_account_display();
let usage = TokenUsage {
@@ -896,7 +897,7 @@ async fn status_snapshot_cached_limits_hide_credits_without_flag() {
let temp_home = TempDir::new().expect("temp home");
let mut config = test_config(&temp_home).await;
config.model = Some("gpt-5.1-codex".to_string());
config.cwd = PathBuf::from("/workspace/tests");
config.cwd = PathBuf::from("/workspace/tests").abs();
let account_display = test_status_account_display();
let usage = TokenUsage {
@@ -0,0 +1,28 @@
use codex_utils_absolute_path::AbsolutePathBuf;
use std::path::Path;
use std::path::PathBuf;
pub(crate) trait PathExt {
fn abs(&self) -> AbsolutePathBuf;
}
impl PathExt for Path {
fn abs(&self) -> AbsolutePathBuf {
AbsolutePathBuf::try_from(self.to_path_buf())
.unwrap_or_else(|_| panic!("path should already be absolute"))
}
}
pub(crate) trait PathBufExt {
fn abs(&self) -> AbsolutePathBuf;
}
impl PathBufExt for PathBuf {
fn abs(&self) -> AbsolutePathBuf {
self.as_path().abs()
}
}
pub(crate) fn test_path_display(path: &str) -> String {
Path::new(path).abs().display().to_string()
}