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
@@ -1645,7 +1645,7 @@ impl CodexMessageProcessor {
return;
}
let cwd = cwd.unwrap_or_else(|| self.config.cwd.clone());
let cwd = cwd.unwrap_or_else(|| self.config.cwd.to_path_buf());
let mut env = create_env(
&self.config.permissions.shell_environment_policy,
/*thread_id*/ None,
@@ -5464,7 +5464,7 @@ impl CodexMessageProcessor {
per_cwd_extra_user_roots,
} = params;
let cwds = if cwds.is_empty() {
vec![self.config.cwd.clone()]
vec![self.config.cwd.to_path_buf()]
} else {
cwds
};
@@ -7258,7 +7258,7 @@ impl CodexMessageProcessor {
let command_cwd = params
.cwd
.map(PathBuf::from)
.unwrap_or_else(|| config.cwd.clone());
.unwrap_or_else(|| config.cwd.to_path_buf());
let cli_overrides = self.current_cli_overrides();
let runtime_feature_enablement = self.current_runtime_feature_enablement();
let outgoing = Arc::clone(&self.outgoing);
@@ -7283,7 +7283,7 @@ impl CodexMessageProcessor {
let setup_request = WindowsSandboxSetupRequest {
mode,
policy: config.permissions.sandbox_policy.get().clone(),
policy_cwd: config.cwd.clone(),
policy_cwd: config.cwd.to_path_buf(),
command_cwd,
env_map: std::env::vars().collect(),
codex_home: config.codex_home.clone(),
+2 -2
View File
@@ -260,7 +260,7 @@ async fn run_command_under_sandbox(
PathBuf::from("/usr/bin/sandbox-exec"),
args,
/*arg0*/ None,
cwd,
cwd.to_path_buf(),
network_policy,
env,
|env_map| {
@@ -293,7 +293,7 @@ async fn run_command_under_sandbox(
codex_linux_sandbox_exe,
args,
Some("codex-linux-sandbox"),
cwd,
cwd.to_path_buf(),
network_policy,
env,
|env_map| {
+1 -1
View File
@@ -252,7 +252,7 @@ mod reload {
fn reload_overrides(config: &Config, preserve_current_provider: bool) -> ConfigOverrides {
ConfigOverrides {
cwd: Some(config.cwd.clone()),
cwd: Some(config.cwd.to_path_buf()),
model_provider: preserve_current_provider.then(|| config.model_provider_id.clone()),
codex_linux_sandbox_exe: config.codex_linux_sandbox_exe.clone(),
main_execve_wrapper_exe: config.main_execve_wrapper_exe.clone(),
+40 -37
View File
@@ -33,6 +33,7 @@ use crate::models_manager::manager::ModelsManager;
use crate::models_manager::manager::RefreshStrategy;
use crate::parse_command::parse_command;
use crate::parse_turn_item;
use crate::path_utils::normalize_for_native_workdir;
use crate::realtime_conversation::RealtimeConversationManager;
use crate::realtime_conversation::handle_audio as handle_realtime_conversation_audio;
use crate::realtime_conversation::handle_close as handle_realtime_conversation_close;
@@ -835,10 +836,10 @@ pub(crate) struct TurnContext {
pub(crate) reasoning_summary: ReasoningSummaryConfig,
pub(crate) session_source: SessionSource,
pub(crate) environment: Arc<Environment>,
/// The session's current working directory. All relative paths provided by
/// the model as well as sandbox policies are resolved against this path
/// The session's absolute working directory. All relative paths provided
/// by the model as well as sandbox policies are resolved against this path
/// instead of `std::env::current_dir()`.
pub(crate) cwd: PathBuf,
pub(crate) cwd: AbsolutePathBuf,
pub(crate) current_date: Option<String>,
pub(crate) timezone: Option<String>,
pub(crate) app_server_client_name: Option<String>,
@@ -979,7 +980,7 @@ impl TurnContext {
pub(crate) fn resolve_path(&self, path: Option<String>) -> PathBuf {
path.as_ref()
.map(PathBuf::from)
.map_or_else(|| self.cwd.clone(), |p| self.cwd.join(p))
.map_or_else(|| self.cwd.to_path_buf(), |p| self.cwd.as_path().join(p))
}
pub(crate) fn compact_prompt(&self) -> &str {
@@ -992,7 +993,7 @@ impl TurnContext {
TurnContextItem {
turn_id: Some(self.sub_id.clone()),
trace_id: self.trace_id.clone(),
cwd: self.cwd.clone(),
cwd: self.cwd.to_path_buf(),
current_date: self.current_date.clone(),
timezone: self.timezone.clone(),
approval_policy: self.approval_policy.value(),
@@ -1068,14 +1069,11 @@ pub(crate) struct SessionConfiguration {
network_sandbox_policy: NetworkSandboxPolicy,
windows_sandbox_level: WindowsSandboxLevel,
/// Working directory that should be treated as the *root* of the
/// Absolute working directory that should be treated as the *root* of the
/// session. All relative paths supplied by the model as well as the
/// execution sandbox are resolved against this directory **instead**
/// of the process-wide current working directory. CLI front-ends are
/// expected to expand this to an absolute path before sending the
/// `ConfigureSession` operation so that the business-logic layer can
/// operate deterministically.
cwd: PathBuf,
/// execution sandbox are resolved against this directory **instead** of
/// the process-wide current working directory.
cwd: AbsolutePathBuf,
/// Directory containing all Codex state for this session.
codex_home: PathBuf,
/// Optional user-facing name for the thread, updated during the session.
@@ -1107,7 +1105,7 @@ impl SessionConfiguration {
approval_policy: self.approval_policy.value(),
approvals_reviewer: self.approvals_reviewer,
sandbox_policy: self.sandbox_policy.get().clone(),
cwd: self.cwd.clone(),
cwd: self.cwd.to_path_buf(),
ephemeral: self.original_config_do_not_use.ephemeral,
reasoning_effort: self.collaboration_mode.reasoning_effort(),
personality: self.personality,
@@ -1150,11 +1148,23 @@ impl SessionConfiguration {
if let Some(windows_sandbox_level) = updates.windows_sandbox_level {
next_configuration.windows_sandbox_level = windows_sandbox_level;
}
let mut cwd_changed = false;
if let Some(cwd) = updates.cwd.clone() {
next_configuration.cwd = cwd;
cwd_changed = true;
}
let absolute_cwd = updates
.cwd
.as_ref()
.map(|cwd| {
AbsolutePathBuf::relative_to_current_dir(normalize_for_native_workdir(
cwd.as_path(),
))
.unwrap_or_else(|e| {
warn!("failed to normalize update cwd: {cwd:?}: {e}");
self.cwd.clone()
})
})
.unwrap_or_else(|| self.cwd.clone());
let cwd_changed = absolute_cwd.as_path() != self.cwd.as_path();
next_configuration.cwd = absolute_cwd;
if sandbox_policy_changed || (cwd_changed && file_system_policy_matches_legacy) {
// Preserve richer split policies across cwd-only updates; only
// rederive when the session is already using the legacy bridge.
@@ -1351,8 +1361,6 @@ impl Session {
let auth_manager_for_context = auth_manager;
let provider_for_context = provider;
let session_telemetry_for_context = session_telemetry;
let per_turn_config = Arc::new(per_turn_config);
let tools_config = ToolsConfig::new(&ToolsConfigParams {
model_info: &model_info,
available_models: &models_manager.try_list_models().unwrap_or_default(),
@@ -1372,10 +1380,12 @@ impl Session {
.with_agent_roles(per_turn_config.agent_roles.clone());
let cwd = session_configuration.cwd.clone();
let per_turn_config = Arc::new(per_turn_config);
let turn_metadata_state = Arc::new(TurnMetadataState::new(
conversation_id.to_string(),
sub_id.clone(),
cwd.clone(),
cwd.to_path_buf(),
session_configuration.sandbox_policy.get(),
session_configuration.windows_sandbox_level,
));
@@ -1447,13 +1457,6 @@ impl Session {
session_configuration.collaboration_mode.model(),
session_configuration.provider
);
if !session_configuration.cwd.is_absolute() {
return Err(anyhow::anyhow!(
"cwd is not absolute: {:?}",
session_configuration.cwd
));
}
let forked_from_id = initial_history.forked_from_id();
let (conversation_id, rollout_params) = match &initial_history {
@@ -1723,7 +1726,7 @@ impl Session {
ShellSnapshot::start_snapshotting(
config.codex_home.clone(),
conversation_id,
session_configuration.cwd.clone(),
session_configuration.cwd.to_path_buf(),
&mut default_shell,
session_telemetry.clone(),
)
@@ -1922,7 +1925,7 @@ impl Session {
approval_policy: session_configuration.approval_policy.value(),
approvals_reviewer: session_configuration.approvals_reviewer,
sandbox_policy: session_configuration.sandbox_policy.get().clone(),
cwd: session_configuration.cwd.clone(),
cwd: session_configuration.cwd.to_path_buf(),
reasoning_effort: session_configuration.collaboration_mode.reasoning_effort(),
history_log_id,
history_entry_count,
@@ -1943,7 +1946,7 @@ impl Session {
let sandbox_state = SandboxState {
sandbox_policy: session_configuration.sandbox_policy.get().clone(),
codex_linux_sandbox_exe: config.codex_linux_sandbox_exe.clone(),
sandbox_cwd: session_configuration.cwd.clone(),
sandbox_cwd: session_configuration.cwd.to_path_buf(),
use_legacy_landlock: config.features.use_legacy_landlock(),
};
let mut required_mcp_servers: Vec<String> = mcp_servers
@@ -2407,7 +2410,7 @@ impl Session {
let sandbox_state = SandboxState {
sandbox_policy: per_turn_config.permissions.sandbox_policy.get().clone(),
codex_linux_sandbox_exe: per_turn_config.codex_linux_sandbox_exe.clone(),
sandbox_cwd: per_turn_config.cwd.clone(),
sandbox_cwd: per_turn_config.cwd.to_path_buf(),
use_legacy_landlock: per_turn_config.features.use_legacy_landlock(),
};
if let Err(e) = self
@@ -4138,7 +4141,7 @@ impl Session {
let sandbox_state = SandboxState {
sandbox_policy: turn_context.sandbox_policy.get().clone(),
codex_linux_sandbox_exe: turn_context.codex_linux_sandbox_exe.clone(),
sandbox_cwd: turn_context.cwd.clone(),
sandbox_cwd: turn_context.cwd.to_path_buf(),
use_legacy_landlock: turn_context.features.use_legacy_landlock(),
};
{
@@ -4919,7 +4922,7 @@ mod handlers {
) {
let cwds = if cwds.is_empty() {
let state = sess.state.lock().await;
vec![state.session_configuration.cwd.clone()]
vec![state.session_configuration.cwd.to_path_buf()]
} else {
cwds
};
@@ -5363,7 +5366,7 @@ async fn spawn_review_thread(
let turn_metadata_state = Arc::new(TurnMetadataState::new(
sess.conversation_id.to_string(),
review_turn_id.clone(),
parent_turn_context.cwd.clone(),
parent_turn_context.cwd.to_path_buf(),
parent_turn_context.sandbox_policy.get(),
parent_turn_context.windows_sandbox_level,
));
@@ -5852,7 +5855,7 @@ pub(crate) async fn run_turn(
let stop_request = codex_hooks::StopRequest {
session_id: sess.conversation_id,
turn_id: turn_context.sub_id.clone(),
cwd: turn_context.cwd.clone(),
cwd: turn_context.cwd.to_path_buf(),
transcript_path: sess.hook_transcript_path().await,
model: turn_context.model_info.slug.clone(),
permission_mode: stop_hook_permission_mode,
@@ -5902,7 +5905,7 @@ pub(crate) async fn run_turn(
.hooks()
.dispatch(HookPayload {
session_id: sess.conversation_id,
cwd: turn_context.cwd.clone(),
cwd: turn_context.cwd.to_path_buf(),
client: turn_context.app_server_client_name.clone(),
triggered_at: chrono::Utc::now(),
hook_event: HookEvent::AfterAgent {
@@ -62,7 +62,7 @@ async fn record_initial_history_resumed_bare_turn_context_does_not_hydrate_previ
let previous_context_item = TurnContextItem {
turn_id: Some(turn_context.sub_id.clone()),
trace_id: turn_context.trace_id.clone(),
cwd: turn_context.cwd.clone(),
cwd: turn_context.cwd.to_path_buf(),
current_date: turn_context.current_date.clone(),
timezone: turn_context.timezone.clone(),
approval_policy: turn_context.approval_policy.value(),
@@ -101,7 +101,7 @@ async fn record_initial_history_resumed_hydrates_previous_turn_settings_from_lif
let mut previous_context_item = TurnContextItem {
turn_id: Some(turn_context.sub_id.clone()),
trace_id: turn_context.trace_id.clone(),
cwd: turn_context.cwd.clone(),
cwd: turn_context.cwd.to_path_buf(),
current_date: turn_context.current_date.clone(),
timezone: turn_context.timezone.clone(),
approval_policy: turn_context.approval_policy.value(),
@@ -851,7 +851,7 @@ async fn record_initial_history_resumed_turn_context_after_compaction_reestablis
let previous_context_item = TurnContextItem {
turn_id: Some(turn_context.sub_id.clone()),
trace_id: turn_context.trace_id.clone(),
cwd: turn_context.cwd.clone(),
cwd: turn_context.cwd.to_path_buf(),
current_date: turn_context.current_date.clone(),
timezone: turn_context.timezone.clone(),
approval_policy: turn_context.approval_policy.value(),
@@ -923,7 +923,7 @@ async fn record_initial_history_resumed_turn_context_after_compaction_reestablis
serde_json::to_value(Some(TurnContextItem {
turn_id: Some(turn_context.sub_id.clone()),
trace_id: turn_context.trace_id.clone(),
cwd: turn_context.cwd.clone(),
cwd: turn_context.cwd.to_path_buf(),
current_date: turn_context.current_date.clone(),
timezone: turn_context.timezone.clone(),
approval_policy: turn_context.approval_policy.value(),
@@ -952,7 +952,7 @@ async fn record_initial_history_resumed_aborted_turn_without_id_clears_active_tu
let previous_context_item = TurnContextItem {
turn_id: Some(turn_context.sub_id.clone()),
trace_id: turn_context.trace_id.clone(),
cwd: turn_context.cwd.clone(),
cwd: turn_context.cwd.to_path_buf(),
current_date: turn_context.current_date.clone(),
timezone: turn_context.timezone.clone(),
approval_policy: turn_context.approval_policy.value(),
@@ -1058,7 +1058,7 @@ async fn record_initial_history_resumed_unmatched_abort_preserves_active_turn_fo
let current_context_item = TurnContextItem {
turn_id: Some(current_turn_id.clone()),
trace_id: turn_context.trace_id.clone(),
cwd: turn_context.cwd.clone(),
cwd: turn_context.cwd.to_path_buf(),
current_date: turn_context.current_date.clone(),
timezone: turn_context.timezone.clone(),
approval_policy: turn_context.approval_policy.value(),
@@ -1160,7 +1160,7 @@ async fn record_initial_history_resumed_trailing_incomplete_turn_compaction_clea
let previous_context_item = TurnContextItem {
turn_id: Some(turn_context.sub_id.clone()),
trace_id: turn_context.trace_id.clone(),
cwd: turn_context.cwd.clone(),
cwd: turn_context.cwd.to_path_buf(),
current_date: turn_context.current_date.clone(),
timezone: turn_context.timezone.clone(),
approval_policy: turn_context.approval_policy.value(),
@@ -1304,7 +1304,7 @@ async fn record_initial_history_resumed_replaced_incomplete_compacted_turn_clear
let previous_context_item = TurnContextItem {
turn_id: Some(turn_context.sub_id.clone()),
trace_id: turn_context.trace_id.clone(),
cwd: turn_context.cwd.clone(),
cwd: turn_context.cwd.to_path_buf(),
current_date: turn_context.current_date.clone(),
timezone: turn_context.timezone.clone(),
approval_policy: turn_context.approval_policy.value(),
+2 -3
View File
@@ -22,7 +22,6 @@ use codex_protocol::request_permissions::RequestPermissionsResponse;
use codex_protocol::request_user_input::RequestUserInputArgs;
use codex_protocol::request_user_input::RequestUserInputResponse;
use codex_protocol::user_input::UserInput;
use codex_utils_absolute_path::AbsolutePathBuf;
use serde_json::Value;
use std::time::Duration;
use tokio::sync::Mutex;
@@ -518,7 +517,7 @@ async fn handle_patch_approval(
let change_count = changes.len();
let maybe_files = changes
.keys()
.map(|path| AbsolutePathBuf::from_absolute_path(parent_ctx.cwd.join(path)).ok())
.map(|path| parent_ctx.cwd.join(path).ok())
.collect::<Option<Vec<_>>>();
if let Some(files) = maybe_files {
let review_cancel = cancel_token.child_token();
@@ -554,7 +553,7 @@ async fn handle_patch_approval(
Arc::clone(parent_ctx),
GuardianApprovalRequest::ApplyPatch {
id: approval_id.clone(),
cwd: parent_ctx.cwd.clone(),
cwd: parent_ctx.cwd.to_path_buf(),
files,
change_count,
patch,
+38 -9
View File
@@ -80,6 +80,7 @@ use codex_protocol::protocol::ConversationAudioParams;
use codex_protocol::protocol::RealtimeAudioFrame;
use codex_protocol::protocol::Submission;
use codex_protocol::protocol::W3cTraceContext;
use core_test_support::PathBufExt;
use core_test_support::context_snapshot;
use core_test_support::context_snapshot::ContextSnapshotOptions;
use core_test_support::context_snapshot::ContextSnapshotRenderMode;
@@ -1266,7 +1267,7 @@ async fn record_initial_history_forked_hydrates_previous_turn_settings() {
let previous_context_item = TurnContextItem {
turn_id: Some(turn_context.sub_id.clone()),
trace_id: turn_context.trace_id.clone(),
cwd: turn_context.cwd.clone(),
cwd: turn_context.cwd.to_path_buf(),
current_date: turn_context.current_date.clone(),
timezone: turn_context.timezone.clone(),
approval_policy: turn_context.approval_policy.value(),
@@ -2282,10 +2283,9 @@ async fn session_configuration_apply_preserves_split_file_system_policy_on_cwd_o
let original_cwd = project_root.join("subdir");
let docs_dir = original_cwd.join("docs");
std::fs::create_dir_all(&docs_dir).expect("create docs dir");
let docs_dir =
codex_utils_absolute_path::AbsolutePathBuf::from_absolute_path(&docs_dir).expect("docs");
let docs_dir = docs_dir.abs();
session_configuration.cwd = original_cwd;
session_configuration.cwd = original_cwd.abs();
session_configuration.sandbox_policy =
codex_config::Constrained::allow_any(SandboxPolicy::WorkspaceWrite {
writable_roots: Vec::new(),
@@ -2407,10 +2407,9 @@ async fn session_configuration_apply_rederives_legacy_file_system_policy_on_cwd_
let original_cwd = project_root.join("subdir");
let docs_dir = original_cwd.join("docs");
std::fs::create_dir_all(&docs_dir).expect("create docs dir");
let docs_dir =
codex_utils_absolute_path::AbsolutePathBuf::from_absolute_path(&docs_dir).expect("docs");
let docs_dir = docs_dir.abs();
session_configuration.cwd = original_cwd;
session_configuration.cwd = original_cwd.abs();
session_configuration.sandbox_policy =
codex_config::Constrained::allow_any(SandboxPolicy::WorkspaceWrite {
writable_roots: Vec::new(),
@@ -2444,6 +2443,36 @@ async fn session_configuration_apply_rederives_legacy_file_system_policy_on_cwd_
);
}
#[tokio::test]
async fn session_update_settings_keeps_runtime_cwds_absolute() {
let (session, turn_context) = make_session_and_context().await;
let updated_cwd = turn_context
.cwd
.join("project")
.expect("resolve project dir");
std::fs::create_dir_all(updated_cwd.as_path()).expect("create project dir");
session
.update_settings(SessionSettingsUpdate {
cwd: Some(PathBuf::from("project")),
..Default::default()
})
.await
.expect("cwd update should succeed");
let session_cwd = {
let state = session.state.lock().await;
state.session_configuration.cwd.clone()
};
let config = session.get_config().await;
let next_turn = session.new_default_turn().await;
assert_eq!(session_cwd, updated_cwd);
assert_eq!(config.cwd, turn_context.cwd);
assert_eq!(next_turn.cwd, updated_cwd);
assert_eq!(next_turn.config.cwd, updated_cwd);
}
#[tokio::test]
async fn session_new_fails_when_zsh_fork_enabled_without_zsh_path() {
let codex_home = tempfile::tempdir().expect("create temp dir");
@@ -3058,7 +3087,7 @@ async fn user_turn_updates_approvals_reviewer() {
text: "hello".to_string(),
text_elements: Vec::new(),
}],
cwd: config.cwd.clone(),
cwd: config.cwd.to_path_buf(),
approval_policy: config.permissions.approval_policy.value(),
approvals_reviewer: Some(crate::config::types::ApprovalsReviewer::GuardianSubagent),
sandbox_policy: config.permissions.sandbox_policy.get().clone(),
@@ -5060,7 +5089,7 @@ async fn rejects_escalated_permissions_when_policy_not_on_request() {
"echo hi".to_string(),
]
},
cwd: turn_context.cwd.clone(),
cwd: turn_context.cwd.to_path_buf(),
expiration: timeout_ms.into(),
capture_policy: ExecCapturePolicy::ShellTool,
env: HashMap::new(),
+5 -5
View File
@@ -23,7 +23,8 @@ use codex_protocol::models::ResponseItem;
use codex_protocol::models::function_call_output_content_items_to_text;
use codex_protocol::permissions::FileSystemSandboxPolicy;
use codex_protocol::permissions::NetworkSandboxPolicy;
use codex_utils_absolute_path::AbsolutePathBuf;
use core_test_support::PathExt;
use core_test_support::TempDirExt;
use core_test_support::codex_linux_sandbox_exe_or_skip;
use core_test_support::responses::ev_assistant_message;
use core_test_support::responses::ev_completed;
@@ -123,7 +124,7 @@ async fn guardian_allows_shell_additional_permissions_requests_past_policy_valid
"echo hi".to_string(),
]
},
cwd: turn_context.cwd.clone(),
cwd: turn_context.cwd.to_path_buf(),
expiration: expiration_ms.into(),
capture_policy: ExecCapturePolicy::ShellTool,
env: HashMap::new(),
@@ -388,12 +389,11 @@ async fn guardian_subagent_does_not_inherit_parent_exec_policy_rules() {
.expect("write policy file");
let mut config = build_test_config(codex_home.path()).await;
config.cwd = project_dir.path().to_path_buf();
config.cwd = project_dir.abs();
config.config_layer_stack = ConfigLayerStack::new(
vec![ConfigLayerEntry::new(
ConfigLayerSource::Project {
dot_codex_folder: AbsolutePathBuf::from_absolute_path(project_dir.path())
.expect("absolute project path"),
dot_codex_folder: project_dir.path().abs(),
},
toml::Value::Table(Default::default()),
)],
+43 -21
View File
@@ -27,6 +27,9 @@ use serde::Deserialize;
use tempfile::tempdir;
use super::*;
use core_test_support::PathBufExt;
use core_test_support::PathExt;
use core_test_support::TempDirExt;
use core_test_support::test_absolute_path;
use pretty_assertions::assert_eq;
@@ -77,6 +80,23 @@ fn http_mcp(url: &str) -> McpServerConfig {
}
}
#[test]
fn load_config_normalizes_relative_cwd_override() -> std::io::Result<()> {
let expected_cwd = AbsolutePathBuf::relative_to_current_dir("nested")?;
let codex_home = tempdir()?;
let config = Config::load_from_base_config_with_overrides(
ConfigToml::default(),
ConfigOverrides {
cwd: Some(PathBuf::from("nested")),
..Default::default()
},
codex_home.abs().into_path_buf(),
)?;
assert_eq!(config.cwd, expected_cwd);
Ok(())
}
#[test]
fn test_toml_parsing() {
let history_with_persistence = r#"
@@ -460,7 +480,7 @@ fn default_permissions_profile_populates_runtime_sandbox_policy() -> std::io::Re
codex_home.path().to_path_buf(),
)?;
let memories_root = AbsolutePathBuf::try_from(codex_home.path().join("memories")).unwrap();
let memories_root = codex_home.path().join("memories").abs();
assert_eq!(
config.permissions.file_system_sandbox_policy,
FileSystemSandboxPolicy::restricted(vec![
@@ -496,9 +516,7 @@ fn default_permissions_profile_populates_runtime_sandbox_policy() -> std::io::Re
writable_roots: vec![memories_root],
read_only_access: ReadOnlyAccess::Restricted {
include_platform_defaults: true,
readable_roots: vec![
AbsolutePathBuf::try_from(cwd.path().join("docs")).expect("absolute docs path"),
],
readable_roots: vec![cwd.path().join("docs").abs(),],
},
network_access: false,
exclude_tmpdir_env_var: true,
@@ -1277,7 +1295,7 @@ fn add_dir_override_extends_workspace_writable_roots() -> std::io::Result<()> {
temp_dir.path().to_path_buf(),
)?;
let expected_backend = AbsolutePathBuf::try_from(backend).unwrap();
let expected_backend = backend.abs();
if cfg!(target_os = "windows") {
match config.permissions.sandbox_policy.get() {
SandboxPolicy::ReadOnly { .. } => {}
@@ -1327,7 +1345,7 @@ fn workspace_write_always_includes_memories_root_once() -> std::io::Result<()> {
let config = Config::load_from_base_config_with_overrides(
ConfigToml {
sandbox_workspace_write: Some(SandboxWorkspaceWrite {
writable_roots: vec![AbsolutePathBuf::from_absolute_path(&memories_root)?],
writable_roots: vec![memories_root.abs()],
..Default::default()
}),
..Default::default()
@@ -1350,7 +1368,7 @@ fn workspace_write_always_includes_memories_root_once() -> std::io::Result<()> {
"expected memories root directory to exist at {}",
memories_root.display()
);
let expected_memories_root = AbsolutePathBuf::from_absolute_path(&memories_root)?;
let expected_memories_root = memories_root.abs();
match config.permissions.sandbox_policy.get() {
SandboxPolicy::WorkspaceWrite { writable_roots, .. } => {
assert_eq!(
@@ -1768,7 +1786,7 @@ async fn managed_config_overrides_oauth_store_mode() -> anyhow::Result<()> {
macos_managed_config_requirements_base64: None,
};
let cwd = AbsolutePathBuf::try_from(codex_home.path())?;
let cwd = codex_home.path().abs();
let config_layer_stack = load_config_layers_state(
codex_home.path(),
Some(cwd),
@@ -1897,7 +1915,7 @@ async fn managed_config_wins_over_cli_overrides() -> anyhow::Result<()> {
macos_managed_config_requirements_base64: None,
};
let cwd = AbsolutePathBuf::try_from(codex_home.path())?;
let cwd = codex_home.path().abs();
let config_layer_stack = load_config_layers_state(
codex_home.path(),
Some(cwd),
@@ -2933,7 +2951,11 @@ struct PrecedenceTestFixture {
}
impl PrecedenceTestFixture {
fn cwd(&self) -> PathBuf {
fn cwd(&self) -> AbsolutePathBuf {
self.cwd.abs()
}
fn cwd_path(&self) -> PathBuf {
self.cwd.path().to_path_buf()
}
@@ -2974,7 +2996,7 @@ fn loads_compact_prompt_from_file() -> std::io::Result<()> {
std::fs::write(&prompt_path, " summarize differently ")?;
let cfg = ConfigToml {
experimental_compact_prompt_file: Some(AbsolutePathBuf::from_absolute_path(prompt_path)?),
experimental_compact_prompt_file: Some(prompt_path.abs()),
..Default::default()
};
@@ -3071,7 +3093,7 @@ fn load_config_rejects_missing_agent_role_config_file() -> std::io::Result<()> {
"researcher".to_string(),
AgentRoleToml {
description: Some("Research role".to_string()),
config_file: Some(AbsolutePathBuf::from_absolute_path(missing_path)?),
config_file: Some(missing_path.abs()),
nickname_candidates: None,
},
)]),
@@ -4082,7 +4104,7 @@ fn model_catalog_json_loads_from_path() -> std::io::Result<()> {
)?;
let cfg = ConfigToml {
model_catalog_json: Some(AbsolutePathBuf::from_absolute_path(catalog_path)?),
model_catalog_json: Some(catalog_path.abs()),
..Default::default()
};
@@ -4103,7 +4125,7 @@ fn model_catalog_json_rejects_empty_catalog() -> std::io::Result<()> {
std::fs::write(&catalog_path, r#"{"models":[]}"#)?;
let cfg = ConfigToml {
model_catalog_json: Some(AbsolutePathBuf::from_absolute_path(catalog_path)?),
model_catalog_json: Some(catalog_path.abs()),
..Default::default()
};
@@ -4240,7 +4262,7 @@ fn test_precedence_fixture_with_o3_profile() -> std::io::Result<()> {
let o3_profile_overrides = ConfigOverrides {
config_profile: Some("o3".to_string()),
cwd: Some(fixture.cwd()),
cwd: Some(fixture.cwd_path()),
..Default::default()
};
let o3_profile_config: Config = Config::load_from_base_config_with_overrides(
@@ -4368,7 +4390,7 @@ fn metrics_exporter_defaults_to_statsig_when_missing() -> std::io::Result<()> {
let config = Config::load_from_base_config_with_overrides(
fixture.cfg.clone(),
ConfigOverrides {
cwd: Some(fixture.cwd()),
cwd: Some(fixture.cwd_path()),
..Default::default()
},
fixture.codex_home(),
@@ -4384,7 +4406,7 @@ fn test_precedence_fixture_with_gpt3_profile() -> std::io::Result<()> {
let gpt3_profile_overrides = ConfigOverrides {
config_profile: Some("gpt3".to_string()),
cwd: Some(fixture.cwd()),
cwd: Some(fixture.cwd_path()),
..Default::default()
};
let gpt3_profile_config = Config::load_from_base_config_with_overrides(
@@ -4505,7 +4527,7 @@ fn test_precedence_fixture_with_gpt3_profile() -> std::io::Result<()> {
// Verify that loading without specifying a profile in ConfigOverrides
// uses the default profile from the config file (which is "gpt3").
let default_profile_overrides = ConfigOverrides {
cwd: Some(fixture.cwd()),
cwd: Some(fixture.cwd_path()),
..Default::default()
};
@@ -4525,7 +4547,7 @@ fn test_precedence_fixture_with_zdr_profile() -> std::io::Result<()> {
let zdr_profile_overrides = ConfigOverrides {
config_profile: Some("zdr".to_string()),
cwd: Some(fixture.cwd()),
cwd: Some(fixture.cwd_path()),
..Default::default()
};
let zdr_profile_config = Config::load_from_base_config_with_overrides(
@@ -4652,7 +4674,7 @@ fn test_precedence_fixture_with_gpt5_profile() -> std::io::Result<()> {
let gpt5_profile_overrides = ConfigOverrides {
config_profile: Some("gpt5".to_string()),
cwd: Some(fixture.cwd()),
cwd: Some(fixture.cwd_path()),
..Default::default()
};
let gpt5_profile_config = Config::load_from_base_config_with_overrides(
@@ -4820,7 +4842,7 @@ fn test_requirements_web_search_mode_allowlist_does_not_warn_when_unset() -> any
let config = Config::load_config_with_layer_stack(
fixture.cfg.clone(),
ConfigOverrides {
cwd: Some(fixture.cwd()),
cwd: Some(fixture.cwd_path()),
..Default::default()
},
fixture.codex_home(),
+22 -17
View File
@@ -395,10 +395,10 @@ pub struct Config {
/// Syntax highlighting theme override (kebab-case name).
pub tui_theme: Option<String>,
/// The directory that should be treated as the current working directory
/// for the session. All relative paths inside the business-logic layer are
/// resolved against this path.
pub cwd: PathBuf,
/// The absolute directory that should be treated as the current working
/// directory for the session. All relative paths inside the business-logic
/// layer are resolved against this path.
pub cwd: AbsolutePathBuf,
/// Preferred store for CLI auth credentials.
/// file (default): Use a file in the Codex home directory.
@@ -683,7 +683,7 @@ impl ConfigBuilder {
let loader_overrides = loader_overrides.unwrap_or_default();
let cwd_override = harness_overrides.cwd.as_deref().or(fallback_cwd.as_deref());
let cwd = match cwd_override {
Some(path) => AbsolutePathBuf::try_from(path)?,
Some(path) => AbsolutePathBuf::relative_to_current_dir(path)?,
None => AbsolutePathBuf::current_dir()?,
};
harness_overrides.cwd = Some(cwd.to_path_buf());
@@ -2104,7 +2104,7 @@ impl Config {
let windows_sandbox_mode = resolve_windows_sandbox_mode(&cfg, &config_profile);
let windows_sandbox_private_desktop =
resolve_windows_sandbox_private_desktop(&cfg, &config_profile);
let resolved_cwd = normalize_for_native_workdir({
let resolved_cwd = AbsolutePathBuf::try_from(normalize_for_native_workdir({
use std::env;
match cwd {
@@ -2121,13 +2121,13 @@ impl Config {
current
}
}
});
}))?;
let mut additional_writable_roots: Vec<AbsolutePathBuf> = additional_writable_roots
.into_iter()
.map(|path| AbsolutePathBuf::resolve_path_against_base(path, &resolved_cwd))
.map(|path| AbsolutePathBuf::resolve_path_against_base(path, resolved_cwd.as_path()))
.collect::<Result<Vec<_>, _>>()?;
let active_project = cfg
.get_active_project(&resolved_cwd)
.get_active_project(resolved_cwd.as_path())
.unwrap_or(ProjectConfig { trust_level: None });
let permission_config_syntax = resolve_permission_config_syntax(
&config_layer_stack,
@@ -2200,12 +2200,15 @@ impl Config {
&mut startup_warnings,
)?;
let mut sandbox_policy = file_system_sandbox_policy
.to_legacy_sandbox_policy(network_sandbox_policy, &resolved_cwd)?;
.to_legacy_sandbox_policy(network_sandbox_policy, resolved_cwd.as_path())?;
if matches!(sandbox_policy, SandboxPolicy::WorkspaceWrite { .. }) {
file_system_sandbox_policy = file_system_sandbox_policy
.with_additional_writable_roots(&resolved_cwd, &additional_writable_roots);
.with_additional_writable_roots(
resolved_cwd.as_path(),
&additional_writable_roots,
);
sandbox_policy = file_system_sandbox_policy
.to_legacy_sandbox_policy(network_sandbox_policy, &resolved_cwd)?;
.to_legacy_sandbox_policy(network_sandbox_policy, resolved_cwd.as_path())?;
}
(
configured_network_proxy_config,
@@ -2219,7 +2222,7 @@ impl Config {
sandbox_mode,
config_profile.sandbox_mode,
windows_sandbox_level,
&resolved_cwd,
resolved_cwd.as_path(),
Some(&constrained_sandbox_policy),
);
if let SandboxPolicy::WorkspaceWrite { writable_roots, .. } = &mut sandbox_policy {
@@ -2229,8 +2232,10 @@ impl Config {
}
}
}
let file_system_sandbox_policy =
FileSystemSandboxPolicy::from_legacy_sandbox_policy(&sandbox_policy, &resolved_cwd);
let file_system_sandbox_policy = FileSystemSandboxPolicy::from_legacy_sandbox_policy(
&sandbox_policy,
resolved_cwd.as_path(),
);
let network_sandbox_policy = NetworkSandboxPolicy::from(&sandbox_policy);
(
configured_network_proxy_config,
@@ -2566,11 +2571,11 @@ impl Config {
} else {
FileSystemSandboxPolicy::from_legacy_sandbox_policy(
&effective_sandbox_policy,
&resolved_cwd,
resolved_cwd.as_path(),
)
};
let effective_file_system_sandbox_policy = effective_file_system_sandbox_policy
.with_additional_readable_roots(&resolved_cwd, &helper_readable_roots);
.with_additional_readable_roots(resolved_cwd.as_path(), &helper_readable_roots);
let effective_network_sandbox_policy =
if effective_sandbox_policy == original_sandbox_policy {
network_sandbox_policy
+3 -3
View File
@@ -70,8 +70,8 @@ impl EnvironmentContext {
) -> Self {
let before_network = Self::network_from_turn_context_item(before);
let after_network = Self::network_from_turn_context(after);
let cwd = if before.cwd != after.cwd {
Some(after.cwd.clone())
let cwd = if before.cwd.as_path() != after.cwd.as_path() {
Some(after.cwd.to_path_buf())
} else {
None
};
@@ -94,7 +94,7 @@ impl EnvironmentContext {
pub fn from_turn_context(turn_context: &TurnContext, shell: &Shell) -> Self {
Self::new(
Some(turn_context.cwd.clone()),
Some(turn_context.cwd.to_path_buf()),
shell.clone(),
turn_context.current_date.clone(),
turn_context.timezone.clone(),
+2 -2
View File
@@ -140,7 +140,7 @@ impl GuardianReviewSessionReuseKey {
base_instructions: spawn_config.base_instructions.clone(),
user_instructions: spawn_config.user_instructions.clone(),
compact_prompt: spawn_config.compact_prompt.clone(),
cwd: spawn_config.cwd.clone(),
cwd: spawn_config.cwd.to_path_buf(),
mcp_servers: spawn_config.mcp_servers.clone(),
codex_linux_sandbox_exe: spawn_config.codex_linux_sandbox_exe.clone(),
main_execve_wrapper_exe: spawn_config.main_execve_wrapper_exe.clone(),
@@ -512,7 +512,7 @@ async fn run_review_on_session(
.codex
.submit(Op::UserTurn {
items: params.prompt_items.clone(),
cwd: params.parent_turn.cwd.clone(),
cwd: params.parent_turn.cwd.to_path_buf(),
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::new_read_only_policy(),
+6 -5
View File
@@ -25,7 +25,8 @@ use codex_protocol::protocol::EventMsg;
use codex_protocol::protocol::GuardianAssessmentStatus;
use codex_protocol::protocol::GuardianRiskLevel;
use codex_protocol::protocol::ReviewDecision;
use codex_utils_absolute_path::AbsolutePathBuf;
use core_test_support::PathBufExt;
use core_test_support::TempDirExt;
use core_test_support::context_snapshot;
use core_test_support::context_snapshot::ContextSnapshotOptions;
use core_test_support::responses::ev_assistant_message;
@@ -322,7 +323,7 @@ fn guardian_assessment_action_value_redacts_apply_patch_patch_text() {
("/tmp", "/tmp/guardian.txt")
};
let cwd = PathBuf::from(cwd);
let file = AbsolutePathBuf::try_from(file).expect("absolute path");
let file = PathBuf::from(file).abs();
let action = GuardianApprovalRequest::ApplyPatch {
id: "patch-1".to_string(),
cwd: cwd.clone(),
@@ -356,7 +357,7 @@ fn guardian_request_turn_id_prefers_network_access_owner_turn() {
let apply_patch = GuardianApprovalRequest::ApplyPatch {
id: "patch-1".to_string(),
cwd: PathBuf::from("/tmp"),
files: vec![AbsolutePathBuf::try_from("/tmp/guardian.txt").expect("absolute path")],
files: vec![PathBuf::from("/tmp/guardian.txt").abs()],
change_count: 1usize,
patch: "*** Begin Patch\n*** Update File: guardian.txt\n@@\n+hello\n*** End Patch"
.to_string(),
@@ -384,7 +385,7 @@ async fn cancelled_guardian_review_emits_terminal_abort_without_warning() {
GuardianApprovalRequest::ApplyPatch {
id: "patch-1".to_string(),
cwd: PathBuf::from("/tmp"),
files: vec![AbsolutePathBuf::try_from("/tmp/guardian.txt").expect("absolute path")],
files: vec![PathBuf::from("/tmp/guardian.txt").abs()],
change_count: 1usize,
patch: "*** Begin Patch\n*** Update File: guardian.txt\n@@\n+hello\n*** End Patch"
.to_string(),
@@ -512,7 +513,7 @@ async fn guardian_review_request_layout_matches_model_visible_request_snapshot()
let (mut session, mut turn) = crate::codex::make_session_and_context().await;
let temp_cwd = TempDir::new()?;
let mut config = (*turn.config).clone();
config.cwd = temp_cwd.path().to_path_buf();
config.cwd = temp_cwd.abs();
config.model_provider.base_url = Some(format!("{}/v1", server.uri()));
let config = Arc::new(config);
let models_manager = Arc::new(test_support::models_manager_with_provider(
+3 -3
View File
@@ -92,7 +92,7 @@ pub(crate) async fn run_pending_session_start_hooks(
let request = codex_hooks::SessionStartRequest {
session_id: sess.conversation_id,
cwd: turn_context.cwd.clone(),
cwd: turn_context.cwd.to_path_buf(),
transcript_path: sess.hook_transcript_path().await,
model: turn_context.model_info.slug.clone(),
permission_mode: hook_permission_mode(turn_context),
@@ -120,7 +120,7 @@ pub(crate) async fn run_pre_tool_use_hooks(
let request = PreToolUseRequest {
session_id: sess.conversation_id,
turn_id: turn_context.sub_id.clone(),
cwd: turn_context.cwd.clone(),
cwd: turn_context.cwd.to_path_buf(),
transcript_path: sess.hook_transcript_path().await,
model: turn_context.model_info.slug.clone(),
permission_mode: hook_permission_mode(turn_context),
@@ -149,7 +149,7 @@ pub(crate) async fn run_user_prompt_submit_hooks(
let request = UserPromptSubmitRequest {
session_id: sess.conversation_id,
turn_id: turn_context.sub_id.clone(),
cwd: turn_context.cwd.clone(),
cwd: turn_context.cwd.to_path_buf(),
transcript_path: sess.hook_transcript_path().await,
model: turn_context.model_info.slug.clone(),
permission_mode: hook_permission_mode(turn_context),
+10 -1
View File
@@ -266,7 +266,16 @@ mod agent {
let root = memory_root(&config.codex_home);
let mut agent_config = config.as_ref().clone();
agent_config.cwd = root;
match AbsolutePathBuf::from_absolute_path(root) {
Ok(root) => agent_config.cwd = root,
Err(err) => {
warn!(
"memory phase-2 consolidation could not set cwd from codex_home {}: {err}",
agent_config.codex_home.display()
);
return None;
}
}
// Consolidation threads must never feed back into phase-1 memory generation.
agent_config.memories.generate_memories = false;
// Approval policy
+5 -4
View File
@@ -435,6 +435,7 @@ mod phase2 {
use codex_state::Phase2JobClaimOutcome;
use codex_state::Stage1Output;
use codex_state::ThreadMetadataBuilder;
use core_test_support::PathBufExt;
use std::path::PathBuf;
use std::sync::Arc;
use std::time::Duration;
@@ -469,7 +470,7 @@ mod phase2 {
let codex_home = tempfile::tempdir().expect("create temp codex home");
let mut config = test_config();
config.codex_home = codex_home.path().to_path_buf();
config.cwd = config.codex_home.clone();
config.cwd = config.codex_home.abs();
let config = Arc::new(config);
let state_db = codex_state::StateRuntime::init(
@@ -507,7 +508,7 @@ mod phase2 {
Utc::now(),
SessionSource::Cli,
);
metadata_builder.cwd = self.config.cwd.clone();
metadata_builder.cwd = self.config.cwd.to_path_buf();
metadata_builder.model_provider = Some(self.config.model_provider_id.clone());
let metadata = metadata_builder.build(&self.config.model_provider_id);
@@ -882,7 +883,7 @@ mod phase2 {
let codex_home = tempfile::tempdir().expect("create temp codex home");
let mut config = test_config();
config.codex_home = codex_home.path().to_path_buf();
config.cwd = config.codex_home.clone();
config.cwd = config.codex_home.abs();
let config = Arc::new(config);
let state_db = codex_state::StateRuntime::init(
@@ -904,7 +905,7 @@ mod phase2 {
Utc::now(),
SessionSource::Cli,
);
metadata_builder.cwd = config.cwd.clone();
metadata_builder.cwd = config.cwd.to_path_buf();
metadata_builder.model_provider = Some(config.model_provider_id.clone());
let metadata = metadata_builder.build(&config.model_provider_id);
state_db
+1 -1
View File
@@ -184,7 +184,7 @@ pub async fn read_project_docs(config: &Config) -> std::io::Result<Option<String
/// directory (inclusive). Symlinks are allowed. When `project_doc_max_bytes`
/// is zero, returns an empty list.
pub fn discover_project_doc_paths(config: &Config) -> std::io::Result<Vec<PathBuf>> {
let mut dir = config.cwd.clone();
let mut dir = config.cwd.to_path_buf();
if let Ok(canon) = normalize_path(&dir) {
dir = canon;
}
+8 -6
View File
@@ -1,6 +1,8 @@
use super::*;
use crate::config::ConfigBuilder;
use codex_features::Feature;
use core_test_support::PathBufExt;
use core_test_support::TempDirExt;
use std::fs;
use std::path::PathBuf;
use tempfile::TempDir;
@@ -18,7 +20,7 @@ async fn make_config(root: &TempDir, limit: usize, instructions: Option<&str>) -
.await
.expect("defaults for test should always succeed");
config.cwd = root.path().to_path_buf();
config.cwd = root.abs();
config.project_doc_max_bytes = limit;
config.user_instructions = instructions.map(ToOwned::to_owned);
@@ -62,7 +64,7 @@ async fn make_config_with_project_root_markers(
.await
.expect("defaults for test should always succeed");
config.cwd = root.path().to_path_buf();
config.cwd = root.abs();
config.project_doc_max_bytes = limit;
config.user_instructions = instructions.map(ToOwned::to_owned);
config
@@ -136,7 +138,7 @@ async fn finds_doc_in_repo_root() {
// Build config pointing at the nested dir.
let mut cfg = make_config(&repo, 4096, None).await;
cfg.cwd = nested;
cfg.cwd = nested.abs();
let res = get_user_instructions(&cfg).await.expect("doc expected");
assert_eq!(res, "root level doc");
@@ -261,7 +263,7 @@ async fn concatenates_root_and_cwd_docs() {
fs::write(nested.join("AGENTS.md"), "crate doc").unwrap();
let mut cfg = make_config(&repo, 4096, None).await;
cfg.cwd = nested;
cfg.cwd = nested.abs();
let res = get_user_instructions(&cfg).await.expect("doc expected");
assert_eq!(res, "root doc\n\ncrate doc");
@@ -278,13 +280,13 @@ async fn project_root_markers_are_honored_for_agents_discovery() {
fs::write(nested.join("AGENTS.md"), "child doc").unwrap();
let mut cfg = make_config_with_project_root_markers(&root, 4096, None, &[".codex-root"]).await;
cfg.cwd = nested;
cfg.cwd = nested.abs();
let discovery = discover_project_doc_paths(&cfg).expect("discover paths");
let expected_parent =
dunce::canonicalize(root.path().join("AGENTS.md")).expect("canonical parent doc path");
let expected_child =
dunce::canonicalize(cfg.cwd.join("AGENTS.md")).expect("canonical child doc path");
dunce::canonicalize(cfg.cwd.as_path().join("AGENTS.md")).expect("canonical child doc path");
assert_eq!(discovery.len(), 2);
assert_eq!(discovery[0], expected_parent);
assert_eq!(discovery[1], expected_child);
+5 -5
View File
@@ -145,7 +145,7 @@ pub(crate) async fn execute_user_shell_command(
process_id: None,
turn_id: turn_context.sub_id.clone(),
command: display_command.clone(),
cwd: cwd.clone(),
cwd: cwd.to_path_buf(),
parsed_cmd: parsed_cmd.clone(),
source: ExecCommandSource::UserShell,
interaction_input: None,
@@ -156,7 +156,7 @@ pub(crate) async fn execute_user_shell_command(
let sandbox_policy = SandboxPolicy::DangerFullAccess;
let exec_env = ExecRequest {
command: exec_command.clone(),
cwd: cwd.clone(),
cwd: cwd.to_path_buf(),
env: create_env(
&turn_context.shell_environment_policy,
Some(session.conversation_id),
@@ -221,7 +221,7 @@ pub(crate) async fn execute_user_shell_command(
process_id: None,
turn_id: turn_context.sub_id.clone(),
command: display_command.clone(),
cwd: cwd.clone(),
cwd: cwd.to_path_buf(),
parsed_cmd: parsed_cmd.clone(),
source: ExecCommandSource::UserShell,
interaction_input: None,
@@ -245,7 +245,7 @@ pub(crate) async fn execute_user_shell_command(
process_id: None,
turn_id: turn_context.sub_id.clone(),
command: display_command.clone(),
cwd: cwd.clone(),
cwd: cwd.to_path_buf(),
parsed_cmd: parsed_cmd.clone(),
source: ExecCommandSource::UserShell,
interaction_input: None,
@@ -289,7 +289,7 @@ pub(crate) async fn execute_user_shell_command(
process_id: None,
turn_id: turn_context.sub_id.clone(),
command: display_command,
cwd,
cwd: cwd.to_path_buf(),
parsed_cmd,
source: ExecCommandSource::UserShell,
interaction_input: None,
+6 -5
View File
@@ -12,6 +12,7 @@ use codex_protocol::openai_models::ModelsResponse;
use codex_protocol::protocol::AgentMessageEvent;
use codex_protocol::protocol::TurnStartedEvent;
use codex_protocol::protocol::UserMessageEvent;
use core_test_support::PathExt;
use core_test_support::responses::mount_models_once;
use pretty_assertions::assert_eq;
use std::time::Duration;
@@ -236,7 +237,7 @@ async fn shutdown_all_threads_bounded_submits_shutdown_to_every_thread() {
let temp_dir = tempdir().expect("tempdir");
let mut config = test_config();
config.codex_home = temp_dir.path().join("codex-home");
config.cwd = config.codex_home.clone();
config.cwd = config.codex_home.abs();
std::fs::create_dir_all(&config.codex_home).expect("create codex home");
let manager = ThreadManager::with_models_provider_and_home_for_tests(
@@ -275,7 +276,7 @@ async fn new_uses_configured_openai_provider_for_model_refresh() {
let temp_dir = tempdir().expect("tempdir");
let mut config = test_config();
config.codex_home = temp_dir.path().join("codex-home");
config.cwd = config.codex_home.clone();
config.cwd = config.codex_home.abs();
std::fs::create_dir_all(&config.codex_home).expect("create codex home");
config.model_catalog = None;
config
@@ -408,7 +409,7 @@ async fn interrupted_fork_snapshot_does_not_synthesize_turn_id_for_legacy_histor
let temp_dir = tempdir().expect("tempdir");
let mut config = test_config();
config.codex_home = temp_dir.path().join("codex-home");
config.cwd = config.codex_home.clone();
config.cwd = config.codex_home.abs();
std::fs::create_dir_all(&config.codex_home).expect("create codex home");
let auth_manager =
@@ -505,7 +506,7 @@ async fn interrupted_fork_snapshot_preserves_explicit_turn_id() {
let temp_dir = tempdir().expect("tempdir");
let mut config = test_config();
config.codex_home = temp_dir.path().join("codex-home");
config.cwd = config.codex_home.clone();
config.cwd = config.codex_home.abs();
std::fs::create_dir_all(&config.codex_home).expect("create codex home");
let auth_manager =
@@ -591,7 +592,7 @@ async fn interrupted_fork_snapshot_uses_persisted_mid_turn_history_without_live_
let temp_dir = tempdir().expect("tempdir");
let mut config = test_config();
config.codex_home = temp_dir.path().join("codex-home");
config.cwd = config.codex_home.clone();
config.cwd = config.codex_home.abs();
std::fs::create_dir_all(&config.codex_home).expect("create codex home");
let auth_manager =
@@ -89,7 +89,7 @@ impl ToolHandler for ArtifactsHandler {
let result = client
.execute_build(ArtifactBuildRequest {
source: args.source,
cwd: turn.cwd.clone(),
cwd: turn.cwd.to_path_buf(),
timeout: Some(Duration::from_millis(
args.timeout_ms
.unwrap_or(DEFAULT_EXECUTION_TIMEOUT.as_millis() as u64),
@@ -221,7 +221,7 @@ fn default_runtime_manager(codex_home: std::path::PathBuf) -> ArtifactRuntimeMan
async fn emit_exec_begin(session: &Session, turn: &TurnContext, call_id: &str) {
let emitter = ToolEmitter::shell(
vec![ARTIFACTS_TOOL_NAME.to_string()],
turn.cwd.clone(),
turn.cwd.to_path_buf(),
ExecCommandSource::Agent,
/*freeform*/ true,
);
@@ -247,7 +247,7 @@ async fn emit_exec_end(
};
let emitter = ToolEmitter::shell(
vec![ARTIFACTS_TOOL_NAME.to_string()],
turn.cwd.clone(),
turn.cwd.to_path_buf(),
ExecCommandSource::Agent,
/*freeform*/ true,
);
+2 -2
View File
@@ -61,7 +61,7 @@ async fn emit_js_repl_exec_begin(
) {
let emitter = ToolEmitter::shell(
vec!["js_repl".to_string()],
turn.cwd.clone(),
turn.cwd.to_path_buf(),
ExecCommandSource::Agent,
/*freeform*/ false,
);
@@ -80,7 +80,7 @@ async fn emit_js_repl_exec_end(
let exec_output = build_js_repl_exec_output(output, error, duration);
let emitter = ToolEmitter::shell(
vec!["js_repl".to_string()],
turn.cwd.clone(),
turn.cwd.to_path_buf(),
ExecCommandSource::Agent,
/*freeform*/ false,
);
@@ -77,7 +77,7 @@ async fn emit_js_repl_exec_end_sends_event() {
assert_eq!(event.call_id, "call-1");
assert_eq!(event.turn_id, turn.sub_id);
assert_eq!(event.command, vec!["js_repl".to_string()]);
assert_eq!(event.cwd, turn.cwd);
assert_eq!(event.cwd, turn.cwd.to_path_buf());
assert_eq!(event.source, ExecCommandSource::Agent);
assert_eq!(event.interaction_input, None);
assert_eq!(event.stdout, "hello");
@@ -39,6 +39,7 @@ use codex_protocol::protocol::InitialHistory;
use codex_protocol::protocol::InterAgentCommunication;
use codex_protocol::protocol::RolloutItem;
use codex_protocol::user_input::UserInput;
use core_test_support::TempDirExt;
use pretty_assertions::assert_eq;
use serde::Deserialize;
use serde_json::json;
@@ -2236,7 +2237,7 @@ async fn build_agent_spawn_config_uses_turn_context_values() {
..ShellEnvironmentPolicy::default()
};
let temp_dir = tempfile::tempdir().expect("temp dir");
turn.cwd = temp_dir.path().to_path_buf();
turn.cwd = temp_dir.abs();
turn.codex_linux_sandbox_exe = Some(PathBuf::from("/bin/echo"));
let sandbox_policy = pick_allowed_sandbox_policy(
&turn.config.permissions.sandbox_policy,
+1 -1
View File
@@ -1050,7 +1050,7 @@ impl JsReplManager {
"--experimental-vm-modules".to_string(),
kernel_path.to_string_lossy().to_string(),
],
cwd: turn.cwd.clone(),
cwd: turn.cwd.to_path_buf(),
env,
additional_permissions: None,
};
+20 -18
View File
@@ -14,6 +14,8 @@ use codex_protocol::models::FunctionCallOutputPayload;
use codex_protocol::models::ImageDetail;
use codex_protocol::models::ResponseInputItem;
use codex_protocol::openai_models::InputModality;
use core_test_support::PathBufExt;
use core_test_support::TempDirExt;
use pretty_assertions::assert_eq;
use std::fs;
use std::path::Path;
@@ -739,7 +741,7 @@ async fn interrupt_active_exec_stops_aborted_kernel_before_later_exec() -> anyho
let dir = tempdir()?;
let (session, mut turn) = make_session_and_context().await;
turn.cwd = dir.path().to_path_buf();
turn.cwd = dir.abs();
set_danger_full_access(&mut turn);
let session = Arc::new(session);
let turn = Arc::new(turn);
@@ -1017,7 +1019,7 @@ async fn js_repl_waits_for_unawaited_tool_calls_before_completion() -> anyhow::R
let marker = turn
.cwd
.join(format!("js-repl-unawaited-marker-{}.txt", Uuid::new_v4()));
.join(format!("js-repl-unawaited-marker-{}.txt", Uuid::new_v4()))?;
let marker_json = serde_json::to_string(&marker.to_string_lossy().to_string())?;
let result = manager
.execute(
@@ -1062,10 +1064,10 @@ async fn js_repl_persisted_tool_helpers_work_across_cells() -> anyhow::Result<()
let global_marker = turn
.cwd
.join(format!("js-repl-global-helper-{}.txt", Uuid::new_v4()));
.join(format!("js-repl-global-helper-{}.txt", Uuid::new_v4()))?;
let lexical_marker = turn
.cwd
.join(format!("js-repl-lexical-helper-{}.txt", Uuid::new_v4()));
.join(format!("js-repl-lexical-helper-{}.txt", Uuid::new_v4()))?;
let global_marker_json = serde_json::to_string(&global_marker.to_string_lossy().to_string())?;
let lexical_marker_json = serde_json::to_string(&lexical_marker.to_string_lossy().to_string())?;
@@ -2101,7 +2103,7 @@ async fn js_repl_prefers_env_node_module_dirs_over_config() -> anyhow::Result<()
"CODEX_JS_REPL_NODE_MODULE_DIRS".to_string(),
env_base.path().to_string_lossy().to_string(),
);
turn.cwd = cwd_dir.path().to_path_buf();
turn.cwd = cwd_dir.abs();
turn.js_repl = Arc::new(JsReplHandle::with_node_path(
turn.config.js_repl_node_path.clone(),
vec![config_base.path().to_path_buf()],
@@ -2145,7 +2147,7 @@ async fn js_repl_resolves_from_first_config_dir() -> anyhow::Result<()> {
turn.shell_environment_policy
.r#set
.remove("CODEX_JS_REPL_NODE_MODULE_DIRS");
turn.cwd = cwd_dir.path().to_path_buf();
turn.cwd = cwd_dir.abs();
turn.js_repl = Arc::new(JsReplHandle::with_node_path(
turn.config.js_repl_node_path.clone(),
vec![
@@ -2189,7 +2191,7 @@ async fn js_repl_falls_back_to_cwd_node_modules() -> anyhow::Result<()> {
turn.shell_environment_policy
.r#set
.remove("CODEX_JS_REPL_NODE_MODULE_DIRS");
turn.cwd = cwd_dir.path().to_path_buf();
turn.cwd = cwd_dir.abs();
turn.js_repl = Arc::new(JsReplHandle::with_node_path(
turn.config.js_repl_node_path.clone(),
vec![config_base.path().to_path_buf()],
@@ -2230,7 +2232,7 @@ async fn js_repl_accepts_node_modules_dir_entries() -> anyhow::Result<()> {
turn.shell_environment_policy
.r#set
.remove("CODEX_JS_REPL_NODE_MODULE_DIRS");
turn.cwd = cwd_dir.path().to_path_buf();
turn.cwd = cwd_dir.abs();
turn.js_repl = Arc::new(JsReplHandle::with_node_path(
turn.config.js_repl_node_path.clone(),
vec![base_dir.path().join("node_modules")],
@@ -2284,7 +2286,7 @@ async fn js_repl_supports_relative_file_imports() -> anyhow::Result<()> {
turn.shell_environment_policy
.r#set
.remove("CODEX_JS_REPL_NODE_MODULE_DIRS");
turn.cwd = cwd_dir.path().to_path_buf();
turn.cwd = cwd_dir.abs();
turn.js_repl = Arc::new(JsReplHandle::with_node_path(
turn.config.js_repl_node_path.clone(),
Vec::new(),
@@ -2331,7 +2333,7 @@ async fn js_repl_supports_absolute_file_imports() -> anyhow::Result<()> {
turn.shell_environment_policy
.r#set
.remove("CODEX_JS_REPL_NODE_MODULE_DIRS");
turn.cwd = cwd_dir.path().to_path_buf();
turn.cwd = cwd_dir.abs();
turn.js_repl = Arc::new(JsReplHandle::with_node_path(
turn.config.js_repl_node_path.clone(),
Vec::new(),
@@ -2385,7 +2387,7 @@ async fn js_repl_imported_local_files_can_access_repl_globals() -> anyhow::Resul
turn.shell_environment_policy
.r#set
.remove("CODEX_JS_REPL_NODE_MODULE_DIRS");
turn.cwd = cwd_dir.path().to_path_buf();
turn.cwd = cwd_dir.abs();
turn.js_repl = Arc::new(JsReplHandle::with_node_path(
turn.config.js_repl_node_path.clone(),
Vec::new(),
@@ -2429,7 +2431,7 @@ async fn js_repl_reimports_local_files_after_edit() -> anyhow::Result<()> {
turn.shell_environment_policy
.r#set
.remove("CODEX_JS_REPL_NODE_MODULE_DIRS");
turn.cwd = cwd_dir.path().to_path_buf();
turn.cwd = cwd_dir.abs();
turn.js_repl = Arc::new(JsReplHandle::with_node_path(
turn.config.js_repl_node_path.clone(),
Vec::new(),
@@ -2485,7 +2487,7 @@ async fn js_repl_reimports_local_files_after_fixing_failure() -> anyhow::Result<
turn.shell_environment_policy
.r#set
.remove("CODEX_JS_REPL_NODE_MODULE_DIRS");
turn.cwd = cwd_dir.path().to_path_buf();
turn.cwd = cwd_dir.abs();
turn.js_repl = Arc::new(JsReplHandle::with_node_path(
turn.config.js_repl_node_path.clone(),
Vec::new(),
@@ -2563,7 +2565,7 @@ async fn js_repl_local_files_expose_node_like_import_meta() -> anyhow::Result<()
turn.shell_environment_policy
.r#set
.remove("CODEX_JS_REPL_NODE_MODULE_DIRS");
turn.cwd = cwd_dir.path().to_path_buf();
turn.cwd = cwd_dir.abs();
turn.js_repl = Arc::new(JsReplHandle::with_node_path(
turn.config.js_repl_node_path.clone(),
Vec::new(),
@@ -2648,7 +2650,7 @@ async fn js_repl_local_files_reject_static_bare_imports() -> anyhow::Result<()>
turn.shell_environment_policy
.r#set
.remove("CODEX_JS_REPL_NODE_MODULE_DIRS");
turn.cwd = cwd_dir.path().to_path_buf();
turn.cwd = cwd_dir.abs();
turn.js_repl = Arc::new(JsReplHandle::with_node_path(
turn.config.js_repl_node_path.clone(),
Vec::new(),
@@ -2693,7 +2695,7 @@ async fn js_repl_rejects_unsupported_file_specifiers() -> anyhow::Result<()> {
turn.shell_environment_policy
.r#set
.remove("CODEX_JS_REPL_NODE_MODULE_DIRS");
turn.cwd = cwd_dir.path().to_path_buf();
turn.cwd = cwd_dir.abs();
turn.js_repl = Arc::new(JsReplHandle::with_node_path(
turn.config.js_repl_node_path.clone(),
Vec::new(),
@@ -2795,7 +2797,7 @@ async fn js_repl_blocks_sensitive_builtin_imports_from_local_files() -> anyhow::
turn.shell_environment_policy
.r#set
.remove("CODEX_JS_REPL_NODE_MODULE_DIRS");
turn.cwd = cwd_dir.path().to_path_buf();
turn.cwd = cwd_dir.abs();
turn.js_repl = Arc::new(JsReplHandle::with_node_path(
turn.config.js_repl_node_path.clone(),
Vec::new(),
@@ -2845,7 +2847,7 @@ async fn js_repl_local_files_do_not_escape_node_module_search_roots() -> anyhow:
turn.shell_environment_policy
.r#set
.remove("CODEX_JS_REPL_NODE_MODULE_DIRS");
turn.cwd = cwd_dir.clone();
turn.cwd = cwd_dir.abs();
turn.js_repl = Arc::new(JsReplHandle::with_node_path(
turn.config.js_repl_node_path.clone(),
Vec::new(),
+1 -1
View File
@@ -379,7 +379,7 @@ impl NetworkApprovalService {
approval_id,
/*approval_id*/ None,
prompt_command,
turn_context.cwd.clone(),
turn_context.cwd.to_path_buf(),
Some(prompt_reason),
Some(network_approval_context.clone()),
/*proposed_execpolicy_amendment*/ None,
+1 -1
View File
@@ -530,7 +530,7 @@ async fn dispatch_after_tool_use_hook(
.hooks()
.dispatch(HookPayload {
session_id: session.conversation_id,
cwd: turn.cwd.clone(),
cwd: turn.cwd.to_path_buf(),
client: turn.app_server_client_name.clone(),
triggered_at: chrono::Utc::now(),
hook_event: HookEvent::AfterToolUse {
@@ -159,7 +159,7 @@ pub(super) async fn try_run_zsh_fork(
network: sandbox_network,
windows_sandbox_level,
arg0,
sandbox_policy_cwd: ctx.turn.cwd.clone(),
sandbox_policy_cwd: ctx.turn.cwd.to_path_buf(),
macos_seatbelt_profile_extensions: ctx
.turn
.config
@@ -263,7 +263,7 @@ pub(crate) async fn prepare_unified_exec_zsh_fork(
network: exec_request.network.clone(),
windows_sandbox_level: exec_request.windows_sandbox_level,
arg0: exec_request.arg0.clone(),
sandbox_policy_cwd: ctx.turn.cwd.clone(),
sandbox_policy_cwd: ctx.turn.cwd.to_path_buf(),
macos_seatbelt_profile_extensions: ctx
.turn
.config
@@ -160,7 +160,7 @@ impl UnifiedExecProcessManager {
let cwd = request
.workdir
.clone()
.unwrap_or_else(|| context.turn.cwd.clone());
.unwrap_or_else(|| context.turn.cwd.to_path_buf());
let process = self
.open_session_with_sandbox(&request, cwd.clone(), context)
.await;
+31
View File
@@ -14,6 +14,7 @@ use codex_core::config::ConfigBuilder;
use codex_core::config::ConfigOverrides;
use codex_utils_absolute_path::AbsolutePathBuf;
use regex_lite::Regex;
use std::path::Path;
use std::path::PathBuf;
pub mod apps_test_server;
@@ -104,6 +105,36 @@ pub fn test_absolute_path(unix_path: &str) -> AbsolutePathBuf {
test_absolute_path_with_windows(unix_path, /*windows_path*/ None)
}
pub trait PathExt {
fn abs(&self) -> AbsolutePathBuf;
}
impl PathExt for Path {
fn abs(&self) -> AbsolutePathBuf {
AbsolutePathBuf::try_from(self.to_path_buf()).expect("path should already be absolute")
}
}
pub trait PathBufExt {
fn abs(&self) -> AbsolutePathBuf;
}
impl PathBufExt for PathBuf {
fn abs(&self) -> AbsolutePathBuf {
self.as_path().abs()
}
}
pub trait TempDirExt {
fn abs(&self) -> AbsolutePathBuf;
}
impl TempDirExt for TempDir {
fn abs(&self) -> AbsolutePathBuf {
self.path().abs()
}
}
pub fn test_tmp_path() -> AbsolutePathBuf {
test_absolute_path_with_windows("/tmp", Some(r"C:\Users\codex\AppData\Local\Temp"))
}
+7 -5
View File
@@ -37,7 +37,10 @@ use serde_json::Value;
use tempfile::TempDir;
use wiremock::MockServer;
use crate::PathBufExt;
use crate::PathExt;
use crate::RemoteEnvConfig;
use crate::TempDirExt;
use crate::get_remote_test_env;
use crate::load_default_config_for_test;
use crate::responses::WebSocketTestServer;
@@ -297,8 +300,7 @@ fn docker_command_capture_stdout<const N: usize>(args: [&str; N]) -> Result<Stri
}
fn absolute_path(path: &Path) -> Result<AbsolutePathBuf> {
AbsolutePathBuf::try_from(path.to_path_buf())
.map_err(|err| anyhow!("invalid absolute path {}: {err}", path.display()))
Ok(path.abs())
}
/// A collection of different ways the model can output an apply_patch call
@@ -393,7 +395,7 @@ impl TestCodexBuilder {
let cwd = test_env.cwd.to_path_buf();
self.config_mutators.push(Box::new(move |config| {
config.experimental_exec_server_url = experimental_exec_server_url;
config.cwd = cwd;
config.cwd = cwd.abs();
}));
let mut test = self.build(server).await?;
@@ -556,7 +558,7 @@ impl TestCodexBuilder {
};
let cwd = Arc::new(TempDir::new()?);
let mut config = load_default_config_for_test(home).await;
config.cwd = cwd.path().to_path_buf();
config.cwd = cwd.abs();
config.model_provider = model_provider;
for hook in self.pre_build_hooks.drain(..) {
hook(home.path());
@@ -716,7 +718,7 @@ impl TestCodex {
text_elements: Vec::new(),
}],
final_output_json_schema: None,
cwd: self.config.cwd.clone(),
cwd: self.config.cwd.to_path_buf(),
approval_policy,
approvals_reviewer: None,
sandbox_policy,
+4 -3
View File
@@ -42,6 +42,7 @@ use codex_protocol::protocol::SessionMeta;
use codex_protocol::protocol::SessionMetaLine;
use codex_protocol::protocol::SessionSource;
use codex_protocol::user_input::UserInput;
use core_test_support::PathBufExt;
use core_test_support::apps_test_server::AppsTestServer;
use core_test_support::load_default_config_for_test;
use core_test_support::responses::ev_completed;
@@ -1105,7 +1106,7 @@ async fn skills_append_to_developer_message() {
.with_home(codex_home.clone())
.with_auth(CodexAuth::from_api_key("Test API Key"))
.with_config(move |config| {
config.cwd = codex_home_path;
config.cwd = codex_home_path.abs();
});
let codex = builder
.build(&server)
@@ -1308,7 +1309,7 @@ async fn user_turn_collaboration_mode_overrides_model_and_effort() -> anyhow::Re
text: "hello".into(),
text_elements: Vec::new(),
}],
cwd: config.cwd.clone(),
cwd: config.cwd.to_path_buf(),
approval_policy: config.permissions.approval_policy.value(),
approvals_reviewer: None,
sandbox_policy: config.permissions.sandbox_policy.get().clone(),
@@ -1426,7 +1427,7 @@ async fn user_turn_explicit_reasoning_summary_overrides_model_catalog_default()
text: "hello".into(),
text_elements: Vec::new(),
}],
cwd: config.cwd.clone(),
cwd: config.cwd.to_path_buf(),
approval_policy: config.permissions.approval_policy.value(),
approvals_reviewer: None,
sandbox_policy: config.permissions.sandbox_policy.get().clone(),
@@ -176,7 +176,7 @@ async fn collaboration_instructions_added_on_user_turn() -> Result<()> {
text: "hello".into(),
text_elements: Vec::new(),
}],
cwd: test.config.cwd.clone(),
cwd: test.config.cwd.to_path_buf(),
approval_policy: test.config.permissions.approval_policy.value(),
approvals_reviewer: None,
sandbox_policy: test.config.permissions.sandbox_policy.get().clone(),
@@ -292,7 +292,7 @@ async fn user_turn_overrides_collaboration_instructions_after_override() -> Resu
text: "hello".into(),
text_elements: Vec::new(),
}],
cwd: test.config.cwd.clone(),
cwd: test.config.cwd.to_path_buf(),
approval_policy: test.config.permissions.approval_policy.value(),
approvals_reviewer: None,
sandbox_policy: test.config.permissions.sandbox_policy.get().clone(),
@@ -567,11 +567,11 @@ async fn snapshot_rollback_followup_turn_trims_context_updates() -> Result<()> {
user_turn(&conversation, TURN_ONE_USER).await;
let override_cwd = config.cwd.join(PRETURN_CONTEXT_DIFF_CWD);
let override_cwd = config.cwd.join(PRETURN_CONTEXT_DIFF_CWD)?;
std::fs::create_dir_all(&override_cwd)?;
conversation
.submit(Op::OverrideTurnContext {
cwd: Some(override_cwd),
cwd: Some(override_cwd.to_path_buf()),
approval_policy: None,
approvals_reviewer: None,
sandbox_policy: None,
@@ -23,7 +23,14 @@ async fn hierarchical_agents_appends_to_project_doc_in_user_instructions() {
.features
.enable(Feature::ChildAgentsMd)
.expect("test config should allow feature update");
std::fs::write(config.cwd.join("AGENTS.md"), "be nice").expect("write AGENTS.md");
std::fs::write(
config
.cwd
.join("AGENTS.md")
.expect("absolute AGENTS.md path"),
"be nice",
)
.expect("write AGENTS.md");
});
let test = builder.build(&server).await.expect("build test codex");
+6 -6
View File
@@ -16,7 +16,7 @@ use codex_protocol::protocol::EventMsg;
use codex_protocol::protocol::Op;
use codex_protocol::protocol::SandboxPolicy;
use codex_protocol::user_input::UserInput;
use codex_utils_absolute_path::AbsolutePathBuf;
use core_test_support::TempDirExt;
use core_test_support::responses::ev_completed;
use core_test_support::responses::ev_response_created;
use core_test_support::responses::mount_sse_once;
@@ -689,7 +689,7 @@ async fn per_turn_overrides_keep_cached_prefix_and_key_constant() -> anyhow::Res
let new_cwd = TempDir::new().unwrap();
let writable = TempDir::new().unwrap();
let new_policy = SandboxPolicy::WorkspaceWrite {
writable_roots: vec![AbsolutePathBuf::try_from(writable.path()).unwrap()],
writable_roots: vec![writable.abs()],
read_only_access: Default::default(),
network_access: true,
exclude_tmpdir_env_var: true,
@@ -814,7 +814,7 @@ async fn send_user_turn_with_no_changes_does_not_send_environment_context() -> a
text: "hello 1".into(),
text_elements: Vec::new(),
}],
cwd: default_cwd.clone(),
cwd: default_cwd.to_path_buf(),
approval_policy: default_approval_policy,
approvals_reviewer: None,
sandbox_policy: default_sandbox_policy.clone(),
@@ -835,7 +835,7 @@ async fn send_user_turn_with_no_changes_does_not_send_environment_context() -> a
text: "hello 2".into(),
text_elements: Vec::new(),
}],
cwd: default_cwd.clone(),
cwd: default_cwd.to_path_buf(),
approval_policy: default_approval_policy,
approvals_reviewer: None,
sandbox_policy: default_sandbox_policy.clone(),
@@ -940,7 +940,7 @@ async fn send_user_turn_with_changes_sends_environment_context() -> anyhow::Resu
text: "hello 1".into(),
text_elements: Vec::new(),
}],
cwd: default_cwd.clone(),
cwd: default_cwd.to_path_buf(),
approval_policy: default_approval_policy,
approvals_reviewer: None,
sandbox_policy: default_sandbox_policy.clone(),
@@ -961,7 +961,7 @@ async fn send_user_turn_with_changes_sends_environment_context() -> anyhow::Resu
text: "hello 2".into(),
text_elements: Vec::new(),
}],
cwd: default_cwd.clone(),
cwd: default_cwd.to_path_buf(),
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::DangerFullAccess,
+1 -1
View File
@@ -28,7 +28,7 @@ fn resume_history(
let turn_ctx = TurnContextItem {
turn_id: Some(turn_id.clone()),
trace_id: None,
cwd: config.cwd.clone(),
cwd: config.cwd.to_path_buf(),
current_date: None,
timezone: None,
approval_policy: config.permissions.approval_policy.value(),
+2 -1
View File
@@ -17,6 +17,7 @@ use codex_protocol::protocol::ReviewTarget;
use codex_protocol::protocol::RolloutItem;
use codex_protocol::protocol::RolloutLine;
use codex_protocol::user_input::UserInput;
use core_test_support::PathBufExt;
use core_test_support::load_sse_fixture_with_id_from_str;
use core_test_support::responses::ResponseMock;
use core_test_support::responses::mount_sse_sequence;
@@ -817,7 +818,7 @@ async fn review_uses_overridden_cwd_for_base_branch_merge_base() {
let codex_home = Arc::new(TempDir::new().unwrap());
let initial_cwd_path = initial_cwd.path().to_path_buf();
let codex = new_conversation_for_server(&server, codex_home.clone(), move |config| {
config.cwd = initial_cwd_path;
config.cwd = initial_cwd_path.abs();
})
.await;
+2 -1
View File
@@ -10,6 +10,7 @@ use codex_protocol::protocol::Op;
use codex_protocol::protocol::SandboxPolicy;
use codex_protocol::protocol::TurnAbortReason;
use codex_protocol::user_input::UserInput;
use core_test_support::PathBufExt;
use core_test_support::assert_regex_match;
use core_test_support::responses;
use core_test_support::responses::ev_assistant_message;
@@ -46,7 +47,7 @@ async fn user_shell_cmd_ls_and_cat_in_temp_dir() {
let server = start_mock_server().await;
let cwd_path = cwd.path().to_path_buf();
let mut builder = test_codex().with_config(move |config| {
config.cwd = cwd_path;
config.cwd = cwd_path.abs();
});
let codex = builder
.build(&server)
+22 -36
View File
@@ -41,7 +41,6 @@ use image::load_from_memory;
use pretty_assertions::assert_eq;
use serde_json::Value;
use std::io::Cursor;
use std::path::Path;
use std::path::PathBuf;
use tokio::time::Duration;
use wiremock::BodyPrintLimit;
@@ -78,11 +77,6 @@ fn find_image_message(body: &Value) -> Option<&Value> {
image_messages(body).into_iter().next()
}
fn absolute_path(path: &Path) -> anyhow::Result<codex_utils_absolute_path::AbsolutePathBuf> {
codex_utils_absolute_path::AbsolutePathBuf::try_from(path.to_path_buf())
.map_err(|err| anyhow::anyhow!("invalid absolute path {}: {err}", path.display()))
}
fn png_bytes(width: u32, height: u32, rgba: [u8; 4]) -> anyhow::Result<Vec<u8>> {
let image = ImageBuffer::from_pixel(width, height, Rgba(rgba));
let mut cursor = Cursor::new(Vec::new());
@@ -91,14 +85,11 @@ fn png_bytes(width: u32, height: u32, rgba: [u8; 4]) -> anyhow::Result<Vec<u8>>
}
async fn create_workspace_directory(test: &TestCodex, rel_path: &str) -> anyhow::Result<PathBuf> {
let abs_path = test.config.cwd.join(rel_path);
let abs_path = test.config.cwd.join(rel_path)?;
test.fs()
.create_directory(
&absolute_path(&abs_path)?,
CreateDirectoryOptions { recursive: true },
)
.create_directory(&abs_path, CreateDirectoryOptions { recursive: true })
.await?;
Ok(abs_path)
Ok(abs_path.into_path_buf())
}
async fn write_workspace_file(
@@ -106,19 +97,14 @@ async fn write_workspace_file(
rel_path: &str,
contents: Vec<u8>,
) -> anyhow::Result<PathBuf> {
let abs_path = test.config.cwd.join(rel_path);
let abs_path = test.config.cwd.join(rel_path)?;
if let Some(parent) = abs_path.parent() {
test.fs()
.create_directory(
&absolute_path(parent)?,
CreateDirectoryOptions { recursive: true },
)
.create_directory(&parent, CreateDirectoryOptions { recursive: true })
.await?;
}
test.fs()
.write_file(&absolute_path(&abs_path)?, contents)
.await?;
Ok(abs_path)
test.fs().write_file(&abs_path, contents).await?;
Ok(abs_path.into_path_buf())
}
async fn write_workspace_png(
@@ -168,7 +154,7 @@ async fn user_turn_with_local_image_attaches_image() -> anyhow::Result<()> {
path: abs_path.clone(),
}],
final_output_json_schema: None,
cwd: config.cwd.clone(),
cwd: config.cwd.to_path_buf(),
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::DangerFullAccess,
@@ -240,7 +226,7 @@ async fn view_image_tool_attaches_local_image() -> anyhow::Result<()> {
let cwd = config.cwd.clone();
let rel_path = "assets/example.png";
let abs_path = cwd.join(rel_path);
let abs_path = cwd.join(rel_path)?;
let original_width = 2304;
let original_height = 864;
write_workspace_png(
@@ -277,7 +263,7 @@ async fn view_image_tool_attaches_local_image() -> anyhow::Result<()> {
text_elements: Vec::new(),
}],
final_output_json_schema: None,
cwd: cwd.clone(),
cwd: cwd.to_path_buf(),
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::DangerFullAccess,
@@ -312,7 +298,7 @@ async fn view_image_tool_attaches_local_image() -> anyhow::Result<()> {
_ => unreachable!("stored event must be ViewImageToolCall"),
};
assert_eq!(tool_event.call_id, call_id);
assert_eq!(tool_event.path, abs_path);
assert_eq!(tool_event.path, abs_path.to_path_buf());
let req = mock.single_request();
let body = req.body_json();
@@ -418,7 +404,7 @@ async fn view_image_tool_can_preserve_original_resolution_when_requested_on_gpt5
text_elements: Vec::new(),
}],
final_output_json_schema: None,
cwd: config.cwd.clone(),
cwd: config.cwd.to_path_buf(),
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::DangerFullAccess,
@@ -517,7 +503,7 @@ async fn view_image_tool_errors_clearly_for_unsupported_detail_values() -> anyho
text_elements: Vec::new(),
}],
final_output_json_schema: None,
cwd: config.cwd.clone(),
cwd: config.cwd.to_path_buf(),
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::DangerFullAccess,
@@ -609,7 +595,7 @@ async fn view_image_tool_treats_null_detail_as_omitted() -> anyhow::Result<()> {
text_elements: Vec::new(),
}],
final_output_json_schema: None,
cwd: config.cwd.clone(),
cwd: config.cwd.to_path_buf(),
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::DangerFullAccess,
@@ -709,7 +695,7 @@ async fn view_image_tool_resizes_when_model_lacks_original_detail_support() -> a
text_elements: Vec::new(),
}],
final_output_json_schema: None,
cwd: config.cwd.clone(),
cwd: config.cwd.to_path_buf(),
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::DangerFullAccess,
@@ -820,7 +806,7 @@ async fn view_image_tool_does_not_force_original_resolution_with_capability_feat
text_elements: Vec::new(),
}],
final_output_json_schema: None,
cwd: config.cwd.clone(),
cwd: config.cwd.to_path_buf(),
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::DangerFullAccess,
@@ -1135,7 +1121,7 @@ async fn view_image_tool_errors_when_path_is_directory() -> anyhow::Result<()> {
text_elements: Vec::new(),
}],
final_output_json_schema: None,
cwd: config.cwd.clone(),
cwd: config.cwd.to_path_buf(),
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::DangerFullAccess,
@@ -1211,7 +1197,7 @@ async fn view_image_tool_errors_for_non_image_files() -> anyhow::Result<()> {
text_elements: Vec::new(),
}],
final_output_json_schema: None,
cwd: config.cwd.clone(),
cwd: config.cwd.to_path_buf(),
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::DangerFullAccess,
@@ -1265,7 +1251,7 @@ async fn view_image_tool_errors_when_file_missing() -> anyhow::Result<()> {
} = &test;
let rel_path = "missing/example.png";
let abs_path = config.cwd.join(rel_path);
let abs_path = config.cwd.join(rel_path)?;
let call_id = "view-image-missing";
let arguments = serde_json::json!({ "path": rel_path }).to_string();
@@ -1292,7 +1278,7 @@ async fn view_image_tool_errors_when_file_missing() -> anyhow::Result<()> {
text_elements: Vec::new(),
}],
final_output_json_schema: None,
cwd: config.cwd.clone(),
cwd: config.cwd.to_path_buf(),
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::DangerFullAccess,
@@ -1415,7 +1401,7 @@ async fn view_image_tool_returns_unsupported_message_for_text_only_model() -> an
text_elements: Vec::new(),
}],
final_output_json_schema: None,
cwd: config.cwd.clone(),
cwd: config.cwd.to_path_buf(),
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::DangerFullAccess,
@@ -1490,7 +1476,7 @@ async fn replaces_invalid_local_image_after_bad_request() -> anyhow::Result<()>
path: abs_path.clone(),
}],
final_output_json_schema: None,
cwd: config.cwd.clone(),
cwd: config.cwd.to_path_buf(),
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::DangerFullAccess,
+47 -23
View File
@@ -34,6 +34,8 @@ use crate::pager_overlay::Overlay;
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;
@@ -1029,7 +1031,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;
@@ -1528,7 +1530,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)
@@ -1781,7 +1783,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: ev.changes.clone(),
},
)),
@@ -2545,7 +2547,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);
@@ -2612,7 +2614,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,
);
}
}
@@ -2815,7 +2823,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 = match crate::resolve_cwd_for_resume_or_fork(
tui,
&self.config,
@@ -2865,7 +2873,8 @@ impl App {
self.shutdown_current_thread().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());
let init = self.chatwidget_init_for_forked_or_resumed_thread(
tui,
self.config.clone(),
@@ -3192,7 +3201,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(cwd.clone());
if should_refresh_plugin_detail {
self.fetch_plugin_detail(
@@ -3644,7 +3654,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(cwd);
}
}
@@ -3831,7 +3843,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,
@@ -4981,7 +4993,7 @@ mod tests {
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: None,
history_log_id: 0,
history_entry_count: 0,
@@ -6121,7 +6133,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)?;
@@ -6213,7 +6225,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)?;
@@ -6280,7 +6292,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)?;
@@ -6341,7 +6353,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)?;
@@ -6411,7 +6423,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"
@@ -6499,7 +6511,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)?;
@@ -6833,7 +6845,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));
@@ -6944,21 +6956,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));
@@ -6993,7 +7017,7 @@ guardian_approval = true
let auth_manager = codex_core::test_support::auth_manager_from_auth(
CodexAuth::from_api_key("Test API Key"),
);
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());
@@ -7056,7 +7080,7 @@ guardian_approval = true
let auth_manager = codex_core::test_support::auth_manager_from_auth(
CodexAuth::from_api_key("Test API Key"),
);
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());
@@ -7555,7 +7579,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?;
@@ -7575,7 +7599,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);
+24 -10
View File
@@ -163,6 +163,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;
@@ -1416,7 +1417,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
@@ -3475,13 +3481,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(),
});
}
@@ -3711,7 +3717,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(),
@@ -3914,7 +3920,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 {
@@ -4537,7 +4543,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."
@@ -5268,7 +5282,7 @@ impl ChatWidget {
let service_tier = self.config.service_tier.map(Some);
let op = Op::UserTurn {
items,
cwd: self.config.cwd.clone(),
cwd: self.config.cwd.to_path_buf(),
approval_policy: self.config.permissions.approval_policy.value(),
approvals_reviewer: None,
sandbox_policy: self.config.permissions.sandbox_policy.get().clone(),
@@ -8409,7 +8423,7 @@ impl ChatWidget {
placeholder_style,
/*reasoning_effort*/ None,
/*show_fast_status*/ false,
config.cwd.clone(),
config.cwd.to_path_buf(),
CODEX_CLI_VERSION,
))
}
@@ -9158,7 +9172,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()));
}
@@ -9185,7 +9199,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()));
}
+14 -14
View File
@@ -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;
@@ -326,7 +326,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())
}
/// Resolves the project root associated with `cwd`.
+37 -31
View File
@@ -16,6 +16,8 @@ use crate::bottom_pane::MentionBinding;
use crate::chatwidget::realtime::RealtimeConversationPhase;
use crate::history_cell::UserHistoryCell;
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;
@@ -434,10 +436,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,
@@ -448,7 +450,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,
@@ -5801,7 +5803,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);
@@ -6822,13 +6824,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(),
}),
});
@@ -7118,12 +7124,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(
@@ -7205,7 +7209,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)
}
@@ -7356,10 +7360,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,
@@ -7396,10 +7400,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,
@@ -7486,7 +7490,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!(
@@ -7555,7 +7559,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!(
@@ -8271,8 +8275,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",
)
@@ -8336,8 +8339,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")
@@ -8912,7 +8914,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,
@@ -9716,8 +9718,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(),
@@ -11627,7 +11628,7 @@ async fn default_terminal_title_refreshes_when_spinner_state_changes() {
let cwd = chat
.current_cwd
.clone()
.unwrap_or_else(|| chat.config.cwd.clone());
.unwrap_or_else(|| chat.config.cwd.to_path_buf());
let project = get_git_repo_root(&cwd)
.map(|root| {
root.file_name()
@@ -11866,7 +11867,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(),
@@ -11876,10 +11877,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_surfaces();
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");
@@ -11887,18 +11889,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;
@@ -1137,7 +1139,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)];
@@ -2654,7 +2656,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,
@@ -2768,9 +2770,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",
+2
View File
@@ -251,6 +251,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;
@@ -82,7 +82,7 @@ impl OnboardingScreen {
auth_manager,
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
@@ -256,7 +256,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),
+1 -1
View File
@@ -46,7 +46,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
@@ -1,6 +1,7 @@
use super::new_status_output;
use super::rate_limit_snapshot_display;
use crate::history_cell::HistoryCell;
use crate::test_support::PathBufExt;
use chrono::Duration as ChronoDuration;
use chrono::TimeZone;
use chrono::Utc;
@@ -109,7 +110,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 auth_manager = test_auth_manager(&config);
let usage = TokenUsage {
@@ -193,7 +194,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 auth_manager = test_auth_manager(&config);
let usage = TokenUsage::default();
@@ -242,7 +243,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 auth_manager = test_auth_manager(&config);
let usage = TokenUsage {
@@ -296,7 +297,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 auth_manager = test_auth_manager(&config);
let usage = TokenUsage {
@@ -549,7 +550,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 auth_manager = test_auth_manager(&config);
let usage = TokenUsage {
@@ -597,7 +598,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 auth_manager = test_auth_manager(&config);
let usage = TokenUsage {
@@ -660,7 +661,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 auth_manager = test_auth_manager(&config);
let usage = TokenUsage {
@@ -708,7 +709,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 auth_manager = test_auth_manager(&config);
let usage = TokenUsage {
@@ -777,7 +778,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 auth_manager = test_auth_manager(&config);
let usage = TokenUsage {
@@ -834,7 +835,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 auth_manager = test_auth_manager(&config);
let usage = TokenUsage {
@@ -900,7 +901,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 auth_manager = test_auth_manager(&config);
let usage = TokenUsage {
+28
View File
@@ -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()
}
+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()
}
+25
View File
@@ -63,6 +63,12 @@ impl AbsolutePathBuf {
Self::from_absolute_path(current_dir)
}
/// Construct an absolute path from `path`, resolving relative paths against
/// the process current working directory.
pub fn relative_to_current_dir<P: AsRef<Path>>(path: P) -> std::io::Result<Self> {
Self::resolve_path_against_base(path, std::env::current_dir()?)
}
pub fn join<P: AsRef<Path>>(&self, path: P) -> std::io::Result<Self> {
Self::resolve_path_against_base(path, &self.0)
}
@@ -104,6 +110,14 @@ impl AsRef<Path> for AbsolutePathBuf {
}
}
impl std::ops::Deref for AbsolutePathBuf {
type Target = Path;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl From<AbsolutePathBuf> for PathBuf {
fn from(path: AbsolutePathBuf) -> Self {
path.into_path_buf()
@@ -216,6 +230,17 @@ mod tests {
assert_eq!(abs_path_buf.as_path(), base_dir.join("file.txt").as_path());
}
#[test]
fn relative_to_current_dir_resolves_relative_path() -> std::io::Result<()> {
let current_dir = std::env::current_dir()?;
let abs_path_buf = AbsolutePathBuf::relative_to_current_dir("file.txt")?;
assert_eq!(
abs_path_buf.as_path(),
current_dir.join("file.txt").as_path()
);
Ok(())
}
#[test]
fn guard_used_in_deserialization() {
let temp_dir = tempdir().expect("base dir");