use super::*; use async_trait::async_trait; use codex_file_system::CopyOptions; use codex_file_system::CreateDirectoryOptions; use codex_file_system::FileMetadata; use codex_file_system::FileSystemResult; use codex_file_system::FileSystemSandboxContext; use codex_file_system::ReadDirectoryEntry; use codex_file_system::RemoveOptions; use pretty_assertions::assert_eq; use std::path::Path; use tempfile::tempdir; struct TestFileSystem; #[async_trait] impl ExecutorFileSystem for TestFileSystem { async fn canonicalize( &self, path: &AbsolutePathBuf, _sandbox: Option<&FileSystemSandboxContext>, ) -> FileSystemResult { path.canonicalize() } async fn join( &self, base_path: &AbsolutePathBuf, path: &Path, ) -> FileSystemResult { Ok(base_path.join(path)) } async fn parent(&self, path: &AbsolutePathBuf) -> FileSystemResult> { Ok(path.parent()) } async fn read_file( &self, path: &AbsolutePathBuf, _sandbox: Option<&FileSystemSandboxContext>, ) -> FileSystemResult> { tokio::fs::read(path.as_path()).await } async fn write_file( &self, _path: &AbsolutePathBuf, _contents: Vec, _sandbox: Option<&FileSystemSandboxContext>, ) -> FileSystemResult<()> { unimplemented!("test filesystem only supports reads") } async fn create_directory( &self, _path: &AbsolutePathBuf, _create_directory_options: CreateDirectoryOptions, _sandbox: Option<&FileSystemSandboxContext>, ) -> FileSystemResult<()> { unimplemented!("test filesystem only supports reads") } async fn get_metadata( &self, _path: &AbsolutePathBuf, _sandbox: Option<&FileSystemSandboxContext>, ) -> FileSystemResult { unimplemented!("test filesystem only supports reads") } async fn read_directory( &self, _path: &AbsolutePathBuf, _sandbox: Option<&FileSystemSandboxContext>, ) -> FileSystemResult> { unimplemented!("test filesystem only supports reads") } async fn remove( &self, _path: &AbsolutePathBuf, _remove_options: RemoveOptions, _sandbox: Option<&FileSystemSandboxContext>, ) -> FileSystemResult<()> { unimplemented!("test filesystem only supports reads") } async fn copy( &self, _source_path: &AbsolutePathBuf, _destination_path: &AbsolutePathBuf, _copy_options: CopyOptions, _sandbox: Option<&FileSystemSandboxContext>, ) -> FileSystemResult<()> { unimplemented!("test filesystem only supports reads") } } #[tokio::test] async fn profile_v2_rejects_matching_legacy_profile_in_base_user_config() { let tmp = tempdir().expect("tempdir"); let selected_config = tmp.path().join("work.config.toml"); std::fs::write( tmp.path().join(CONFIG_TOML_FILE), r#" model = "gpt-main" [profiles.work] model = "gpt-work" "#, ) .expect("write default user config"); std::fs::write(&selected_config, r#"model = "gpt-work-v2""#) .expect("write selected user config"); let mut overrides = LoaderOverrides::without_managed_config_for_tests(); overrides.user_config_path = Some(AbsolutePathBuf::resolve_path_against_base( "work.config.toml", tmp.path(), )); overrides.user_config_profile = Some("work".parse().expect("profile-v2 name")); let err = load_config_layers_state( &TestFileSystem, tmp.path(), /*cwd*/ None, &[], overrides, &crate::NoopThreadConfigLoader, ) .await .expect_err("profile-v2 should reject a matching legacy profile in base user config"); assert_eq!( err.kind(), io::ErrorKind::InvalidData, "a matching legacy profile should be a hard config error" ); let message = err.to_string(); assert!( message.contains("--profile `work` cannot be used"), "unexpected error message: {message}" ); assert!( message.contains("config.toml"), "unexpected error message: {message}" ); assert!( message.contains("[profiles.work]"), "unexpected error message: {message}" ); assert!( message.contains("https://developers.openai.com/codex/config-advanced#profiles"), "unexpected error message: {message}" ); } #[tokio::test] async fn profile_v2_rejects_matching_legacy_profile_selector_in_base_user_config() { let tmp = tempdir().expect("tempdir"); let selected_config = tmp.path().join("work.config.toml"); std::fs::write( tmp.path().join(CONFIG_TOML_FILE), r#" profile = "work" model = "gpt-main" "#, ) .expect("write default user config"); std::fs::write(&selected_config, r#"model = "gpt-work-v2""#) .expect("write selected user config"); let mut overrides = LoaderOverrides::without_managed_config_for_tests(); overrides.user_config_path = Some(AbsolutePathBuf::resolve_path_against_base( "work.config.toml", tmp.path(), )); overrides.user_config_profile = Some("work".parse().expect("profile-v2 name")); let err = load_config_layers_state( &TestFileSystem, tmp.path(), /*cwd*/ None, &[], overrides, &crate::NoopThreadConfigLoader, ) .await .expect_err("profile-v2 should reject a matching legacy profile selector"); assert_eq!( err.kind(), io::ErrorKind::InvalidData, "a matching legacy profile selector should be a hard config error" ); let message = err.to_string(); assert!( message.contains("--profile `work` cannot be used"), "unexpected error message: {message}" ); assert!( message.contains("profile = \"work\""), "unexpected error message: {message}" ); assert!( message.contains("work.config.toml"), "unexpected error message: {message}" ); } #[tokio::test] async fn profile_v2_allows_unrelated_legacy_profiles_in_base_user_config() { let tmp = tempdir().expect("tempdir"); let selected_config = tmp.path().join("work.config.toml"); std::fs::write( tmp.path().join(CONFIG_TOML_FILE), r#" model = "gpt-main" [profiles.dev] model = "gpt-dev" "#, ) .expect("write default user config"); std::fs::write(&selected_config, r#"model = "gpt-work-v2""#) .expect("write selected user config"); let mut overrides = LoaderOverrides::without_managed_config_for_tests(); overrides.user_config_path = Some(AbsolutePathBuf::resolve_path_against_base( "work.config.toml", tmp.path(), )); overrides.user_config_profile = Some("work".parse().expect("profile-v2 name")); load_config_layers_state( &TestFileSystem, tmp.path(), /*cwd*/ None, &[], overrides, &crate::NoopThreadConfigLoader, ) .await .expect("profile-v2 should allow unrelated legacy profiles in base user config"); }