use super::*;use crate::ModelsManagerConfig;use chrono::Utc;use codex_app_server_protocol::AuthMode;use codex_login::AuthCredentialsStoreMode;use codex_login::AuthKeyringBackendKind;use codex_login::AuthManager;use codex_login::CodexAuth;use codex_login::ExternalAuth;use codex_login::ExternalAuthRefreshContext;use codex_login::ExternalAuthTokens;use codex_login::TokenData;use codex_protocol::openai_models::ModelsResponse;use pretty_assertions::assert_eq;use serde_json::json;use std::collections::VecDeque;use std::path::Path;use std::sync::Arc;use std::sync::Mutex;use std::sync::atomic::AtomicUsize;use std::sync::atomic::Ordering;use tempfile::tempdir;#[path = "model_info_overrides_tests.rs"]mod model_info_overrides_tests;fn remote_model(slug: &str, display: &str, priority: i32) -> ModelInfo { remote_model_with_visibility(slug, display, priority, "list")}fn remote_model_with_visibility( slug: &str, display: &str, priority: i32, visibility: &str,) -> ModelInfo { serde_json::from_value(json!({ "slug": slug, "display_name": display, "description": format!("{display} desc"), "default_reasoning_level": "medium", "supported_reasoning_levels": [{"effort": "low", "description": "low"}, {"effort": "medium", "description": "medium"}], "shell_type": "shell_command", "visibility": visibility, "minimal_client_version": [0, 1, 0], "supported_in_api": true, "priority": priority, "upgrade": null, "base_instructions": "base instructions", "supports_reasoning_summaries": false, "support_verbosity": false, "default_verbosity": null, "apply_patch_tool_type": null, "truncation_policy": {"mode": "bytes", "limit": 10_000}, "supports_parallel_tool_calls": false, "supports_image_detail_original": false, "context_window": 272_000, "max_context_window": 272_000, "experimental_supported_tools": [], })) .expect("valid model")}fn assert_models_contain(actual: &[ModelInfo], expected: &[ModelInfo]) { for model in expected { assert!( actual.iter().any(|candidate| candidate.slug == model.slug), "expected model {} in cached list", model.slug ); }}#[derive(Debug)]struct TestModelsEndpoint { has_command_auth: bool, uses_codex_backend: bool, responses: Mutex<VecDeque<Vec<ModelInfo>>>, fetch_count: AtomicUsize,}impl TestModelsEndpoint { fn new(responses: Vec<Vec<ModelInfo>>) -> Arc<Self> { Arc::new(Self { has_command_auth: false, uses_codex_backend: true, responses: Mutex::new(responses.into()), fetch_count: AtomicUsize::new(0), }) } fn without_refresh(responses: Vec<Vec<ModelInfo>>) -> Arc<Self> { Arc::new(Self { has_command_auth: false, uses_codex_backend: false, responses: Mutex::new(responses.into()), fetch_count: AtomicUsize::new(0), }) } fn fetch_count(&self) -> usize { self.fetch_count.load(Ordering::SeqCst) } async fn list_models(&self) -> CoreResult<(Vec<ModelInfo>, Option<String>)> { self.fetch_count.fetch_add(1, Ordering::SeqCst); let models = self .responses .lock() .expect("responses lock should not be poisoned") .pop_front() .unwrap_or_default(); Ok((models, None)) }}#[derive(Debug)]struct TestExternalApiKeyAuth;impl ExternalAuth for TestExternalApiKeyAuth { fn auth_mode(&self) -> AuthMode { AuthMode::ApiKey } fn resolve(&self) -> codex_login::ExternalAuthFuture<'_, Option<ExternalAuthTokens>> { Box::pin(async { Ok(Some(ExternalAuthTokens::access_token_only( "test-external-api-key", ))) }) } fn refresh( &self, _context: ExternalAuthRefreshContext, ) -> codex_login::ExternalAuthFuture<'_, ExternalAuthTokens> { Box::pin(async { Ok(ExternalAuthTokens::access_token_only( "test-external-api-key", )) }) }}#[derive(Debug)]struct TestUnresolvedExternalApiKeyAuth;impl ExternalAuth for TestUnresolvedExternalApiKeyAuth { fn auth_mode(&self) -> AuthMode { AuthMode::ApiKey } fn refresh( &self, _context: ExternalAuthRefreshContext, ) -> codex_login::ExternalAuthFuture<'_, ExternalAuthTokens> { Box::pin(async { Err(std::io::Error::other("unresolved test auth")) }) }}impl ModelsEndpointClient for TestModelsEndpoint { fn has_command_auth(&self) -> bool { self.has_command_auth } fn uses_codex_backend(&self) -> ModelsEndpointFuture<'_, bool> { Box::pin(async { self.uses_codex_backend }) } fn list_models<'a>( &'a self, _client_version: &'a str, ) -> ModelsEndpointFuture<'a, CoreResult<(Vec<ModelInfo>, Option<String>)>> { Box::pin(TestModelsEndpoint::list_models(self)) }}fn openai_manager_for_tests( codex_home: std::path::PathBuf, endpoint_client: Arc<dyn ModelsEndpointClient>,) -> OpenAiModelsManager { openai_manager_for_tests_with_auth( codex_home, endpoint_client, Some(AuthManager::from_auth_for_testing( CodexAuth::create_dummy_chatgpt_auth_for_testing(), )), )}fn openai_manager_for_tests_with_auth( codex_home: std::path::PathBuf, endpoint_client: Arc<dyn ModelsEndpointClient>, auth_manager: Option<Arc<AuthManager>>,) -> OpenAiModelsManager { OpenAiModelsManager::new(codex_home, endpoint_client, auth_manager)}fn static_manager_for_tests(model_catalog: ModelsResponse) -> StaticModelsManager { StaticModelsManager::new(/*auth_manager*/ None, model_catalog)}async fn chatgpt_auth_tokens_for_tests(codex_home: &Path) -> CodexAuth { let auth_dot_json = codex_login::AuthDotJson { auth_mode: Some(AuthMode::ChatgptAuthTokens), openai_api_key: None, tokens: Some(TokenData { id_token: codex_login::token_data::parse_chatgpt_jwt_claims( "eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.\eyJlbWFpbCI6InVzZXJAZXhhbXBsZS5jb20iLCJodHRwczovL2FwaS5vcGVuYWkuY29tL2F1dGgiOnsiY2hhdGdwdF9wbGFuX3R5cGUiOiJwcm8iLCJjaGF0Z3B0X3VzZXJfaWQiOiJ1c2VyLWlkIiwiY2hhdGdwdF9hY2NvdW50X2lkIjoiYWNjb3VudC1pZCJ9fQ.\c2ln", ) .expect("fake id token should parse"), access_token: "Access Token".to_string(), refresh_token: "test".to_string(), account_id: Some("account_id".to_string()), }), last_refresh: Some(Utc::now()), agent_identity: None, personal_access_token: None, bedrock_api_key: None, }; std::fs::create_dir_all(codex_home).expect("codex home should be created"); std::fs::write( codex_home.join("auth.json"), serde_json::to_string(&auth_dot_json).expect("auth should serialize"), ) .expect("auth.json should be written"); CodexAuth::from_auth_storage( codex_home, AuthCredentialsStoreMode::File, /*chatgpt_base_url*/ None, AuthKeyringBackendKind::default(), ) .await .expect("auth should load") .expect("auth should be present")}#[tokio::test]async fn get_model_info_tracks_fallback_usage() { let codex_home = tempdir().expect("temp dir"); let config = ModelsManagerConfig::default(); let manager = openai_manager_for_tests( codex_home.path().to_path_buf(), TestModelsEndpoint::new(Vec::new()), ); let known_slug = manager .get_remote_models() .await .first() .expect("bundled models should include at least one model") .slug .clone(); let known = manager.get_model_info(known_slug.as_str(), &config).await; assert!(!known.used_fallback_model_metadata); assert_eq!(known.slug, known_slug); let unknown = manager .get_model_info("model-that-does-not-exist", &config) .await; assert!(unknown.used_fallback_model_metadata); assert_eq!(unknown.slug, "model-that-does-not-exist");}#[tokio::test]async fn get_model_info_uses_custom_catalog() { let config = ModelsManagerConfig::default(); let mut overlay = remote_model("gpt-overlay", "Overlay", /*priority*/ 0); overlay.supports_image_detail_original = true; let manager = static_manager_for_tests(ModelsResponse { models: vec![overlay], }); let model_info = manager .get_model_info("gpt-overlay-experiment", &config) .await; assert_eq!(model_info.slug, "gpt-overlay-experiment"); assert_eq!(model_info.display_name, "Overlay"); assert_eq!(model_info.context_window, Some(272_000)); assert!(model_info.supports_image_detail_original); assert!(!model_info.supports_parallel_tool_calls); assert!(!model_info.used_fallback_model_metadata);}#[tokio::test]async fn get_model_info_matches_namespaced_suffix() { let config = ModelsManagerConfig::default(); let mut remote = remote_model("gpt-image", "Image", /*priority*/ 0); remote.supports_image_detail_original = true; let manager = static_manager_for_tests(ModelsResponse { models: vec![remote], }); let namespaced_model = "custom/gpt-image".to_string(); let model_info = manager.get_model_info(&namespaced_model, &config).await; assert_eq!(model_info.slug, namespaced_model); assert!(model_info.supports_image_detail_original); assert!(!model_info.used_fallback_model_metadata);}#[tokio::test]async fn get_model_info_matches_hyphenated_provider_namespace_suffix() { let config = ModelsManagerConfig::default(); let remote = remote_model("gpt-image", "Image", /*priority*/ 0); let manager = static_manager_for_tests(ModelsResponse { models: vec![remote], }); let namespaced_model = "openai-codex/gpt-image".to_string(); let model_info = manager.get_model_info(&namespaced_model, &config).await; assert_eq!(model_info.slug, namespaced_model); assert!(!model_info.used_fallback_model_metadata);}#[tokio::test]async fn get_model_info_rejects_multi_segment_namespace_suffix_matching() { let codex_home = tempdir().expect("temp dir"); let config = ModelsManagerConfig::default(); let manager = openai_manager_for_tests( codex_home.path().to_path_buf(), TestModelsEndpoint::new(Vec::new()), ); let known_slug = manager .get_remote_models() .await .first() .expect("bundled models should include at least one model") .slug .clone(); let namespaced_model = format!("ns1/ns2/{known_slug}"); let model_info = manager.get_model_info(&namespaced_model, &config).await; assert_eq!(model_info.slug, namespaced_model); assert!(model_info.used_fallback_model_metadata);}#[tokio::test]async fn refresh_available_models_sorts_by_priority() { let remote_models = vec![ remote_model("priority-low", "Low", /*priority*/ 1), remote_model("priority-high", "High", /*priority*/ 0), ]; let codex_home = tempdir().expect("temp dir"); let endpoint = TestModelsEndpoint::new(vec![remote_models.clone()]); let manager = openai_manager_for_tests(codex_home.path().to_path_buf(), endpoint.clone()); manager .refresh_available_models(RefreshStrategy::OnlineIfUncached) .await .expect("refresh succeeds"); let cached_remote = manager.get_remote_models().await; assert_models_contain(&cached_remote, &remote_models); let available = manager.list_models(RefreshStrategy::OnlineIfUncached).await; let high_idx = available .iter() .position(|model| model.model == "priority-high") .expect("priority-high should be listed"); let low_idx = available .iter() .position(|model| model.model == "priority-low") .expect("priority-low should be listed"); assert!( high_idx < low_idx, "higher priority should be listed before lower priority" ); assert_eq!(endpoint.fetch_count(), 1, "expected a single model fetch");}#[tokio::test]async fn refresh_available_models_uses_remote_only_catalog_for_chatgpt_auth() { let remote_models = vec![remote_model( "chatgpt-visible-source-of-truth", "ChatGPT Visible", /*priority*/ 0, )]; let codex_home = tempdir().expect("temp dir"); let endpoint = TestModelsEndpoint::new(vec![remote_models.clone()]); let manager = openai_manager_for_tests(codex_home.path().to_path_buf(), endpoint.clone()); manager .refresh_available_models(RefreshStrategy::OnlineIfUncached) .await .expect("refresh succeeds"); assert_eq!(manager.get_remote_models().await, remote_models); assert_eq!(endpoint.fetch_count(), 1, "expected a single model fetch");}#[tokio::test]async fn refresh_available_models_uses_cached_remote_only_catalog_for_chatgpt_auth() { let remote_models = vec![remote_model( "chatgpt-cached-source-of-truth", "ChatGPT Cached", /*priority*/ 0, )]; let codex_home = tempdir().expect("temp dir"); let fetch_endpoint = TestModelsEndpoint::new(vec![remote_models.clone()]); let fetch_manager = openai_manager_for_tests(codex_home.path().to_path_buf(), fetch_endpoint.clone()); fetch_manager .refresh_available_models(RefreshStrategy::OnlineIfUncached) .await .expect("initial refresh succeeds"); let cache_endpoint = TestModelsEndpoint::new(Vec::new()); let cache_manager = openai_manager_for_tests(codex_home.path().to_path_buf(), cache_endpoint.clone()); cache_manager .refresh_available_models(RefreshStrategy::OnlineIfUncached) .await .expect("cached refresh succeeds"); assert_eq!(cache_manager.get_remote_models().await, remote_models); assert_eq!( cache_endpoint.fetch_count(), 0, "fresh cache should avoid a model fetch" );}#[tokio::test]async fn get_model_info_uses_fallback_for_bundled_models_when_chatgpt_remote_is_authoritative() { let remote_models = vec![remote_model( "chatgpt-authoritative-model-info", "ChatGPT Model Info", /*priority*/ 0, )]; let codex_home = tempdir().expect("temp dir"); let endpoint = TestModelsEndpoint::new(vec![remote_models]); let manager = openai_manager_for_tests(codex_home.path().to_path_buf(), endpoint); let bundled_slug = load_remote_models_from_file() .expect("bundled models should parse") .first() .expect("bundled models should contain at least one model") .slug .clone(); manager .refresh_available_models(RefreshStrategy::OnlineIfUncached) .await .expect("refresh succeeds"); let model_info = manager .get_model_info(&bundled_slug, &ModelsManagerConfig::default()) .await; assert_eq!(model_info.slug, bundled_slug); assert!(model_info.used_fallback_model_metadata);}#[tokio::test]async fn refresh_available_models_preserves_bundled_catalog_for_empty_chatgpt_remote() { let codex_home = tempdir().expect("temp dir"); let endpoint = TestModelsEndpoint::new(vec![Vec::new()]); let manager = openai_manager_for_tests(codex_home.path().to_path_buf(), endpoint); let expected = load_remote_models_from_file().expect("bundled models should parse"); manager .refresh_available_models(RefreshStrategy::OnlineIfUncached) .await .expect("refresh succeeds"); assert_eq!(manager.get_remote_models().await, expected);}#[tokio::test]async fn refresh_available_models_merges_hidden_only_chatgpt_remote_with_bundled_catalog() { let hidden_remote = remote_model_with_visibility( "chatgpt-hidden-only", "ChatGPT Hidden", /*priority*/ 0, "hide", ); let codex_home = tempdir().expect("temp dir"); let endpoint = TestModelsEndpoint::new(vec![vec![hidden_remote.clone()]]); let manager = openai_manager_for_tests(codex_home.path().to_path_buf(), endpoint); let mut expected = load_remote_models_from_file().expect("bundled models should parse"); expected.push(hidden_remote); manager .refresh_available_models(RefreshStrategy::OnlineIfUncached) .await .expect("refresh succeeds"); assert_eq!(manager.get_remote_models().await, expected);}#[tokio::test]async fn refresh_available_models_keeps_merging_for_api_auth() { let remote_models = vec![remote_model( "api-auth-visible-remote", "API Auth Visible", /*priority*/ 0, )]; let codex_home = tempdir().expect("temp dir"); let endpoint = Arc::new(TestModelsEndpoint { has_command_auth: true, uses_codex_backend: false, responses: Mutex::new(vec![remote_models.clone()].into()), fetch_count: AtomicUsize::new(0), }); let manager = openai_manager_for_tests_with_auth( codex_home.path().to_path_buf(), endpoint.clone(), Some(AuthManager::from_auth_for_testing(CodexAuth::from_api_key( "test-api-key", ))), ); let mut expected = load_remote_models_from_file().expect("bundled models should parse"); expected.extend(remote_models); manager .refresh_available_models(RefreshStrategy::OnlineIfUncached) .await .expect("refresh succeeds"); assert_eq!(manager.get_remote_models().await, expected); assert_eq!(endpoint.fetch_count(), 1, "expected a single model fetch");}#[tokio::test]async fn refresh_available_models_uses_cache_when_fresh() { let remote_models = vec![remote_model("cached", "Cached", /*priority*/ 5)]; let codex_home = tempdir().expect("temp dir"); let endpoint = TestModelsEndpoint::new(vec![remote_models.clone()]); let manager = openai_manager_for_tests(codex_home.path().to_path_buf(), endpoint.clone()); manager .refresh_available_models(RefreshStrategy::OnlineIfUncached) .await .expect("first refresh succeeds"); assert_models_contain(&manager.get_remote_models().await, &remote_models); // Second call should read from cache and avoid the network. manager .refresh_available_models(RefreshStrategy::OnlineIfUncached) .await .expect("cached refresh succeeds"); assert_models_contain(&manager.get_remote_models().await, &remote_models); assert_eq!( endpoint.fetch_count(), 1, "cache hit should avoid a second model fetch" );}#[tokio::test]async fn refresh_available_models_refetches_when_cache_stale() { let initial_models = vec![remote_model("stale", "Stale", /*priority*/ 1)]; let codex_home = tempdir().expect("temp dir"); let updated_models = vec![remote_model("fresh", "Fresh", /*priority*/ 9)]; let endpoint = TestModelsEndpoint::new(vec![initial_models.clone(), updated_models.clone()]); let manager = openai_manager_for_tests(codex_home.path().to_path_buf(), endpoint.clone()); manager .refresh_available_models(RefreshStrategy::OnlineIfUncached) .await .expect("initial refresh succeeds"); // Rewrite cache with an old timestamp so it is treated as stale. manager .cache_manager .manipulate_cache_for_test(|fetched_at| { *fetched_at = Utc::now() - chrono::Duration::hours(1); }) .await .expect("cache manipulation succeeds"); manager .refresh_available_models(RefreshStrategy::OnlineIfUncached) .await .expect("second refresh succeeds"); assert_models_contain(&manager.get_remote_models().await, &updated_models); assert_eq!( endpoint.fetch_count(), 2, "stale cache refresh should fetch models again" );}#[tokio::test]async fn refresh_available_models_refetches_when_version_mismatch() { let initial_models = vec![remote_model("old", "Old", /*priority*/ 1)]; let codex_home = tempdir().expect("temp dir"); let updated_models = vec![remote_model("new", "New", /*priority*/ 2)]; let endpoint = TestModelsEndpoint::new(vec![initial_models.clone(), updated_models.clone()]); let manager = openai_manager_for_tests(codex_home.path().to_path_buf(), endpoint.clone()); manager .refresh_available_models(RefreshStrategy::OnlineIfUncached) .await .expect("initial refresh succeeds"); manager .cache_manager .mutate_cache_for_test(|cache| { let client_version = crate::client_version_to_whole(); cache.client_version = Some(format!("{client_version}-mismatch")); }) .await .expect("cache mutation succeeds"); manager .refresh_available_models(RefreshStrategy::OnlineIfUncached) .await .expect("second refresh succeeds"); assert_models_contain(&manager.get_remote_models().await, &updated_models); assert_eq!( endpoint.fetch_count(), 2, "version mismatch should fetch models again" );}#[tokio::test]async fn refresh_available_models_drops_removed_remote_models() { let initial_models = vec![remote_model( "remote-old", "Remote Old", /*priority*/ 1, )]; let codex_home = tempdir().expect("temp dir"); let refreshed_models = vec![remote_model( "remote-new", "Remote New", /*priority*/ 1, )]; let endpoint = TestModelsEndpoint::new(vec![initial_models, refreshed_models]); let mut manager = openai_manager_for_tests(codex_home.path().to_path_buf(), endpoint.clone()); manager.cache_manager.set_ttl(Duration::ZERO); manager .refresh_available_models(RefreshStrategy::OnlineIfUncached) .await .expect("initial refresh succeeds"); manager .refresh_available_models(RefreshStrategy::OnlineIfUncached) .await .expect("second refresh succeeds"); let available = manager .try_list_models() .expect("models should be available"); assert!( available.iter().any(|preset| preset.model == "remote-new"), "new remote model should be listed" ); assert!( !available.iter().any(|preset| preset.model == "remote-old"), "removed remote model should not be listed" ); assert_eq!( endpoint.fetch_count(), 2, "second refresh should fetch models again" );}#[tokio::test]async fn refresh_available_models_skips_network_without_chatgpt_auth() { let dynamic_slug = "dynamic-model-only-for-test-noauth"; let codex_home = tempdir().expect("temp dir"); let endpoint = TestModelsEndpoint::without_refresh(vec![vec![remote_model( dynamic_slug, "No Auth", /*priority*/ 1, )]]); let manager = openai_manager_for_tests_with_auth( codex_home.path().to_path_buf(), endpoint.clone(), /*auth_manager*/ None, ); manager .refresh_available_models(RefreshStrategy::Online) .await .expect("refresh should no-op without chatgpt auth"); let cached_remote = manager.get_remote_models().await; assert!( !cached_remote .iter() .any(|candidate| candidate.slug == dynamic_slug), "remote refresh should be skipped without chatgpt auth" ); assert_eq!( endpoint.fetch_count(), 0, "endpoint that cannot refresh should avoid model fetches" );}#[derive(Debug)]struct TestAuthAwareModelsEndpoint { auth_manager: Option<Arc<AuthManager>>, responses: Mutex<VecDeque<Vec<ModelInfo>>>, fetch_count: AtomicUsize,}impl TestAuthAwareModelsEndpoint { fn new(auth_manager: Option<Arc<AuthManager>>, responses: Vec<Vec<ModelInfo>>) -> Arc<Self> { Arc::new(Self { auth_manager, responses: Mutex::new(responses.into()), fetch_count: AtomicUsize::new(0), }) } fn fetch_count(&self) -> usize { self.fetch_count.load(Ordering::SeqCst) } async fn uses_codex_backend(&self) -> bool { match self.auth_manager.as_ref() { Some(auth_manager) => auth_manager .auth() .await .as_ref() .is_some_and(CodexAuth::uses_codex_backend), None => false, } } async fn list_models(&self) -> CoreResult<(Vec<ModelInfo>, Option<String>)> { self.fetch_count.fetch_add(1, Ordering::SeqCst); let models = self .responses .lock() .expect("responses lock should not be poisoned") .pop_front() .unwrap_or_default(); Ok((models, None)) }}impl ModelsEndpointClient for TestAuthAwareModelsEndpoint { fn has_command_auth(&self) -> bool { false } fn uses_codex_backend(&self) -> ModelsEndpointFuture<'_, bool> { Box::pin(TestAuthAwareModelsEndpoint::uses_codex_backend(self)) } fn list_models<'a>( &'a self, _client_version: &'a str, ) -> ModelsEndpointFuture<'a, CoreResult<(Vec<ModelInfo>, Option<String>)>> { Box::pin(TestAuthAwareModelsEndpoint::list_models(self)) }}#[tokio::test]async fn refresh_available_models_skips_network_when_external_api_key_overrides_chatgpt_auth() { let dynamic_slug = "dynamic-model-only-for-test-external-api-key"; let codex_home = tempdir().expect("temp dir"); let auth_manager = AuthManager::from_auth_for_testing(CodexAuth::create_dummy_chatgpt_auth_for_testing()); auth_manager.set_external_auth(Arc::new(TestExternalApiKeyAuth)); let endpoint = TestAuthAwareModelsEndpoint::new( Some(Arc::clone(&auth_manager)), vec![vec![remote_model( dynamic_slug, "External API Key", /*priority*/ 1, )]], ); let manager = openai_manager_for_tests_with_auth( codex_home.path().to_path_buf(), endpoint.clone(), Some(auth_manager), ); manager .refresh_available_models(RefreshStrategy::Online) .await .expect("refresh should no-op with API key auth"); let cached_remote = manager.get_remote_models().await; assert!( !cached_remote .iter() .any(|candidate| candidate.slug == dynamic_slug), "remote refresh should be skipped when external API key auth is active" ); assert_eq!( endpoint.fetch_count(), 0, "endpoint should avoid model fetches when external API key auth is active" );}#[tokio::test]async fn refresh_available_models_uses_cached_chatgpt_when_external_api_key_is_unresolved() { let dynamic_slug = "dynamic-model-only-for-test-unresolved-external-api-key"; let codex_home = tempdir().expect("temp dir"); let auth_manager = AuthManager::from_auth_for_testing(CodexAuth::create_dummy_chatgpt_auth_for_testing()); auth_manager.set_external_auth(Arc::new(TestUnresolvedExternalApiKeyAuth)); let endpoint = TestAuthAwareModelsEndpoint::new( Some(Arc::clone(&auth_manager)), vec![vec![remote_model( dynamic_slug, "Unresolved External API Key", /*priority*/ 1, )]], ); let manager = openai_manager_for_tests_with_auth( codex_home.path().to_path_buf(), endpoint.clone(), Some(auth_manager), ); manager .refresh_available_models(RefreshStrategy::Online) .await .expect("refresh should fall back to cached ChatGPT auth"); assert!( manager .get_remote_models() .await .iter() .any(|candidate| candidate.slug == dynamic_slug), "remote refresh should include models fetched with cached ChatGPT auth" ); assert_eq!( endpoint.fetch_count(), 1, "endpoint should fetch models when unresolved external API key falls back to ChatGPT auth" );}#[tokio::test]async fn refresh_available_models_fetches_with_chatgpt_auth_tokens() { let dynamic_slug = "dynamic-model-only-for-test-chatgpt-auth-tokens"; let codex_home = tempdir().expect("temp dir"); let endpoint = TestModelsEndpoint::new(vec![vec![remote_model( dynamic_slug, "ChatGPT Auth Tokens", /*priority*/ 1, )]]); let auth = chatgpt_auth_tokens_for_tests(codex_home.path()).await; let manager = openai_manager_for_tests_with_auth( codex_home.path().to_path_buf(), endpoint.clone(), Some(AuthManager::from_auth_for_testing(auth)), ); manager .refresh_available_models(RefreshStrategy::Online) .await .expect("refresh should fetch with ChatGPT auth tokens"); assert!( manager .get_remote_models() .await .iter() .any(|candidate| candidate.slug == dynamic_slug), "remote refresh should include models fetched with ChatGPT auth tokens" ); assert_eq!( endpoint.fetch_count(), 1, "endpoint should fetch models with ChatGPT auth tokens" );}#[test]fn build_available_models_picks_default_after_hiding_hidden_models() { let manager = static_manager_for_tests(ModelsResponse { models: Vec::new() }); let hidden_model = remote_model_with_visibility("hidden", "Hidden", /*priority*/ 0, "hide"); let visible_model = remote_model_with_visibility("visible", "Visible", /*priority*/ 1, "list"); let expected_hidden = ModelPreset::from(hidden_model.clone()); let mut expected_visible = ModelPreset::from(visible_model.clone()); expected_visible.is_default = true; let available = manager.build_available_models(vec![hidden_model, visible_model]); assert_eq!(available, vec![expected_hidden, expected_visible]);}#[tokio::test]async fn static_manager_reads_latest_auth_mode() { let auth_manager = AuthManager::from_auth_for_testing(CodexAuth::create_dummy_chatgpt_auth_for_testing()); let chatgpt_only_model = { let mut model = remote_model("chatgpt-only", "ChatGPT Only", /*priority*/ 0); model.supported_in_api = false; model }; let api_model = remote_model("api-model", "API Model", /*priority*/ 1); let manager = StaticModelsManager::new( Some(Arc::clone(&auth_manager)), ModelsResponse { models: vec![chatgpt_only_model, api_model], }, ); let chatgpt_models = manager.list_models(RefreshStrategy::Online).await; assert_eq!( chatgpt_models .iter() .map(|model| model.model.as_str()) .collect::<Vec<_>>(), vec!["chatgpt-only", "api-model"] ); auth_manager.set_external_auth(Arc::new(TestExternalApiKeyAuth)); let api_models = manager.list_models(RefreshStrategy::Online).await; assert_eq!( api_models .iter() .map(|model| model.model.as_str()) .collect::<Vec<_>>(), vec!["api-model"] );}#[test]fn bundled_models_json_roundtrips() { let response = crate::bundled_models_response() .unwrap_or_else(|err| panic!("bundled models.json should parse: {err}")); let serialized = serde_json::to_string(&response).expect("bundled models.json should serialize"); let roundtripped: ModelsResponse = serde_json::from_str(&serialized).expect("serialized models.json should deserialize"); assert_eq!( response, roundtripped, "bundled models.json should round trip through serde" ); assert!( !response.models.is_empty(), "bundled models.json should contain at least one model" );}