fix: move inline codex-rs/core unit tests into sibling files (#14444)

## Why
PR #13783 moved the `codex.rs` unit tests into `codex_tests.rs`. This
applies the same extraction pattern across the rest of `codex-rs/core`
so the production modules stay focused on runtime code instead of large
inline test blocks.

Keeping the tests in sibling files also makes follow-up edits easier to
review because product changes no longer have to share a file with
hundreds or thousands of lines of test scaffolding.

## What changed
- replaced each inline `mod tests { ... }` in `codex-rs/core/src/**`
with a path-based module declaration
- moved each extracted unit test module into a sibling `*_tests.rs`
file, using `mod_tests.rs` for `mod.rs` modules
- preserved the existing `cfg(...)` guards and module-local structure so
the refactor remains structural rather than behavioral

## Testing
- `cargo test -p codex-core --lib` (`1653 passed; 0 failed; 5 ignored`)
- `just fix -p codex-core`
- `cargo fmt --check`
- `cargo shear`
This commit is contained in:
Michael Bolin
2026-03-12 08:16:36 -07:00
committed by GitHub
parent 7f2ca502f5
commit 0c8a36676a
252 changed files with 40158 additions and 40383 deletions

View File

@@ -103,57 +103,5 @@ fn asking_questions_guidance_message(default_mode_request_user_input: bool) -> S
}
#[cfg(test)]
mod tests {
use super::*;
use pretty_assertions::assert_eq;
#[test]
fn preset_names_use_mode_display_names() {
assert_eq!(plan_preset().name, ModeKind::Plan.display_name());
assert_eq!(
default_preset(CollaborationModesConfig::default()).name,
ModeKind::Default.display_name()
);
assert_eq!(
plan_preset().reasoning_effort,
Some(Some(ReasoningEffort::Medium))
);
}
#[test]
fn default_mode_instructions_replace_mode_names_placeholder() {
let default_instructions = default_preset(CollaborationModesConfig {
default_mode_request_user_input: true,
})
.developer_instructions
.expect("default preset should include instructions")
.expect("default instructions should be set");
assert!(!default_instructions.contains(KNOWN_MODE_NAMES_PLACEHOLDER));
assert!(!default_instructions.contains(REQUEST_USER_INPUT_AVAILABILITY_PLACEHOLDER));
assert!(!default_instructions.contains(ASKING_QUESTIONS_GUIDANCE_PLACEHOLDER));
let known_mode_names = format_mode_names(&TUI_VISIBLE_COLLABORATION_MODES);
let expected_snippet = format!("Known mode names are {known_mode_names}.");
assert!(default_instructions.contains(&expected_snippet));
let expected_availability_message =
request_user_input_availability_message(ModeKind::Default, true);
assert!(default_instructions.contains(&expected_availability_message));
assert!(default_instructions.contains("prefer using the `request_user_input` tool"));
}
#[test]
fn default_mode_instructions_use_plain_text_questions_when_feature_disabled() {
let default_instructions = default_preset(CollaborationModesConfig::default())
.developer_instructions
.expect("default preset should include instructions")
.expect("default instructions should be set");
assert!(!default_instructions.contains("prefer using the `request_user_input` tool"));
assert!(
default_instructions
.contains("ask the user directly with a concise plain-text question")
);
}
}
#[path = "collaboration_mode_presets_tests.rs"]
mod tests;

View File

@@ -0,0 +1,51 @@
use super::*;
use pretty_assertions::assert_eq;
#[test]
fn preset_names_use_mode_display_names() {
assert_eq!(plan_preset().name, ModeKind::Plan.display_name());
assert_eq!(
default_preset(CollaborationModesConfig::default()).name,
ModeKind::Default.display_name()
);
assert_eq!(
plan_preset().reasoning_effort,
Some(Some(ReasoningEffort::Medium))
);
}
#[test]
fn default_mode_instructions_replace_mode_names_placeholder() {
let default_instructions = default_preset(CollaborationModesConfig {
default_mode_request_user_input: true,
})
.developer_instructions
.expect("default preset should include instructions")
.expect("default instructions should be set");
assert!(!default_instructions.contains(KNOWN_MODE_NAMES_PLACEHOLDER));
assert!(!default_instructions.contains(REQUEST_USER_INPUT_AVAILABILITY_PLACEHOLDER));
assert!(!default_instructions.contains(ASKING_QUESTIONS_GUIDANCE_PLACEHOLDER));
let known_mode_names = format_mode_names(&TUI_VISIBLE_COLLABORATION_MODES);
let expected_snippet = format!("Known mode names are {known_mode_names}.");
assert!(default_instructions.contains(&expected_snippet));
let expected_availability_message =
request_user_input_availability_message(ModeKind::Default, true);
assert!(default_instructions.contains(&expected_availability_message));
assert!(default_instructions.contains("prefer using the `request_user_input` tool"));
}
#[test]
fn default_mode_instructions_use_plain_text_questions_when_feature_disabled() {
let default_instructions = default_preset(CollaborationModesConfig::default())
.developer_instructions
.expect("default preset should include instructions")
.expect("default instructions should be set");
assert!(!default_instructions.contains("prefer using the `request_user_input` tool"));
assert!(
default_instructions.contains("ask the user directly with a concise plain-text question")
);
}

View File

@@ -428,584 +428,5 @@ impl ModelsManager {
}
#[cfg(test)]
mod tests {
use super::*;
use crate::CodexAuth;
use crate::auth::AuthCredentialsStoreMode;
use crate::config::ConfigBuilder;
use crate::model_provider_info::WireApi;
use chrono::Utc;
use codex_protocol::openai_models::ModelsResponse;
use core_test_support::responses::mount_models_once;
use pretty_assertions::assert_eq;
use serde_json::json;
use tempfile::tempdir;
use wiremock::MockServer;
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,
"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
);
}
}
fn provider_for(base_url: String) -> ModelProviderInfo {
ModelProviderInfo {
name: "mock".into(),
base_url: Some(base_url),
env_key: None,
env_key_instructions: None,
experimental_bearer_token: None,
wire_api: WireApi::Responses,
query_params: None,
http_headers: None,
env_http_headers: None,
request_max_retries: Some(0),
stream_max_retries: Some(0),
stream_idle_timeout_ms: Some(5_000),
requires_openai_auth: false,
supports_websockets: false,
}
}
#[tokio::test]
async fn get_model_info_tracks_fallback_usage() {
let codex_home = tempdir().expect("temp dir");
let config = ConfigBuilder::default()
.codex_home(codex_home.path().to_path_buf())
.build()
.await
.expect("load default test config");
let auth_manager =
AuthManager::from_auth_for_testing(CodexAuth::from_api_key("Test API Key"));
let manager = ModelsManager::new(
codex_home.path().to_path_buf(),
auth_manager,
None,
CollaborationModesConfig::default(),
);
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 codex_home = tempdir().expect("temp dir");
let config = ConfigBuilder::default()
.codex_home(codex_home.path().to_path_buf())
.build()
.await
.expect("load default test config");
let mut overlay = remote_model("gpt-overlay", "Overlay", 0);
overlay.supports_image_detail_original = true;
let auth_manager =
AuthManager::from_auth_for_testing(CodexAuth::from_api_key("Test API Key"));
let manager = ModelsManager::new(
codex_home.path().to_path_buf(),
auth_manager,
Some(ModelsResponse {
models: vec![overlay],
}),
CollaborationModesConfig::default(),
);
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 codex_home = tempdir().expect("temp dir");
let config = ConfigBuilder::default()
.codex_home(codex_home.path().to_path_buf())
.build()
.await
.expect("load default test config");
let mut remote = remote_model("gpt-image", "Image", 0);
remote.supports_image_detail_original = true;
let auth_manager =
AuthManager::from_auth_for_testing(CodexAuth::from_api_key("Test API Key"));
let manager = ModelsManager::new(
codex_home.path().to_path_buf(),
auth_manager,
Some(ModelsResponse {
models: vec![remote],
}),
CollaborationModesConfig::default(),
);
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_rejects_multi_segment_namespace_suffix_matching() {
let codex_home = tempdir().expect("temp dir");
let config = ConfigBuilder::default()
.codex_home(codex_home.path().to_path_buf())
.build()
.await
.expect("load default test config");
let auth_manager =
AuthManager::from_auth_for_testing(CodexAuth::from_api_key("Test API Key"));
let manager = ModelsManager::new(
codex_home.path().to_path_buf(),
auth_manager,
None,
CollaborationModesConfig::default(),
);
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 server = MockServer::start().await;
let remote_models = vec![
remote_model("priority-low", "Low", 1),
remote_model("priority-high", "High", 0),
];
let models_mock = mount_models_once(
&server,
ModelsResponse {
models: remote_models.clone(),
},
)
.await;
let codex_home = tempdir().expect("temp dir");
let auth_manager =
AuthManager::from_auth_for_testing(CodexAuth::create_dummy_chatgpt_auth_for_testing());
let provider = provider_for(server.uri());
let manager = ModelsManager::with_provider_for_tests(
codex_home.path().to_path_buf(),
auth_manager,
provider,
);
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!(
models_mock.requests().len(),
1,
"expected a single /models request"
);
}
#[tokio::test]
async fn refresh_available_models_uses_cache_when_fresh() {
let server = MockServer::start().await;
let remote_models = vec![remote_model("cached", "Cached", 5)];
let models_mock = mount_models_once(
&server,
ModelsResponse {
models: remote_models.clone(),
},
)
.await;
let codex_home = tempdir().expect("temp dir");
let auth_manager =
AuthManager::from_auth_for_testing(CodexAuth::create_dummy_chatgpt_auth_for_testing());
let provider = provider_for(server.uri());
let manager = ModelsManager::with_provider_for_tests(
codex_home.path().to_path_buf(),
auth_manager,
provider,
);
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!(
models_mock.requests().len(),
1,
"cache hit should avoid a second /models request"
);
}
#[tokio::test]
async fn refresh_available_models_refetches_when_cache_stale() {
let server = MockServer::start().await;
let initial_models = vec![remote_model("stale", "Stale", 1)];
let initial_mock = mount_models_once(
&server,
ModelsResponse {
models: initial_models.clone(),
},
)
.await;
let codex_home = tempdir().expect("temp dir");
let auth_manager =
AuthManager::from_auth_for_testing(CodexAuth::create_dummy_chatgpt_auth_for_testing());
let provider = provider_for(server.uri());
let manager = ModelsManager::with_provider_for_tests(
codex_home.path().to_path_buf(),
auth_manager,
provider,
);
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");
let updated_models = vec![remote_model("fresh", "Fresh", 9)];
server.reset().await;
let refreshed_mock = mount_models_once(
&server,
ModelsResponse {
models: updated_models.clone(),
},
)
.await;
manager
.refresh_available_models(RefreshStrategy::OnlineIfUncached)
.await
.expect("second refresh succeeds");
assert_models_contain(&manager.get_remote_models().await, &updated_models);
assert_eq!(
initial_mock.requests().len(),
1,
"initial refresh should only hit /models once"
);
assert_eq!(
refreshed_mock.requests().len(),
1,
"stale cache refresh should fetch /models once"
);
}
#[tokio::test]
async fn refresh_available_models_refetches_when_version_mismatch() {
let server = MockServer::start().await;
let initial_models = vec![remote_model("old", "Old", 1)];
let initial_mock = mount_models_once(
&server,
ModelsResponse {
models: initial_models.clone(),
},
)
.await;
let codex_home = tempdir().expect("temp dir");
let auth_manager =
AuthManager::from_auth_for_testing(CodexAuth::create_dummy_chatgpt_auth_for_testing());
let provider = provider_for(server.uri());
let manager = ModelsManager::with_provider_for_tests(
codex_home.path().to_path_buf(),
auth_manager,
provider,
);
manager
.refresh_available_models(RefreshStrategy::OnlineIfUncached)
.await
.expect("initial refresh succeeds");
manager
.cache_manager
.mutate_cache_for_test(|cache| {
let client_version = crate::models_manager::client_version_to_whole();
cache.client_version = Some(format!("{client_version}-mismatch"));
})
.await
.expect("cache mutation succeeds");
let updated_models = vec![remote_model("new", "New", 2)];
server.reset().await;
let refreshed_mock = mount_models_once(
&server,
ModelsResponse {
models: updated_models.clone(),
},
)
.await;
manager
.refresh_available_models(RefreshStrategy::OnlineIfUncached)
.await
.expect("second refresh succeeds");
assert_models_contain(&manager.get_remote_models().await, &updated_models);
assert_eq!(
initial_mock.requests().len(),
1,
"initial refresh should only hit /models once"
);
assert_eq!(
refreshed_mock.requests().len(),
1,
"version mismatch should fetch /models once"
);
}
#[tokio::test]
async fn refresh_available_models_drops_removed_remote_models() {
let server = MockServer::start().await;
let initial_models = vec![remote_model("remote-old", "Remote Old", 1)];
let initial_mock = mount_models_once(
&server,
ModelsResponse {
models: initial_models,
},
)
.await;
let codex_home = tempdir().expect("temp dir");
let auth_manager =
AuthManager::from_auth_for_testing(CodexAuth::create_dummy_chatgpt_auth_for_testing());
let provider = provider_for(server.uri());
let mut manager = ModelsManager::with_provider_for_tests(
codex_home.path().to_path_buf(),
auth_manager,
provider,
);
manager.cache_manager.set_ttl(Duration::ZERO);
manager
.refresh_available_models(RefreshStrategy::OnlineIfUncached)
.await
.expect("initial refresh succeeds");
server.reset().await;
let refreshed_models = vec![remote_model("remote-new", "Remote New", 1)];
let refreshed_mock = mount_models_once(
&server,
ModelsResponse {
models: refreshed_models,
},
)
.await;
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!(
initial_mock.requests().len(),
1,
"initial refresh should only hit /models once"
);
assert_eq!(
refreshed_mock.requests().len(),
1,
"second refresh should only hit /models once"
);
}
#[tokio::test]
async fn refresh_available_models_skips_network_without_chatgpt_auth() {
let server = MockServer::start().await;
let dynamic_slug = "dynamic-model-only-for-test-noauth";
let models_mock = mount_models_once(
&server,
ModelsResponse {
models: vec![remote_model(dynamic_slug, "No Auth", 1)],
},
)
.await;
let codex_home = tempdir().expect("temp dir");
let auth_manager = Arc::new(AuthManager::new(
codex_home.path().to_path_buf(),
false,
AuthCredentialsStoreMode::File,
));
let provider = provider_for(server.uri());
let manager = ModelsManager::with_provider_for_tests(
codex_home.path().to_path_buf(),
auth_manager,
provider,
);
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!(
models_mock.requests().len(),
0,
"no auth should avoid /models requests"
);
}
#[test]
fn build_available_models_picks_default_after_hiding_hidden_models() {
let codex_home = tempdir().expect("temp dir");
let auth_manager =
AuthManager::from_auth_for_testing(CodexAuth::from_api_key("Test API Key"));
let provider = provider_for("http://example.test".to_string());
let manager = ModelsManager::with_provider_for_tests(
codex_home.path().to_path_buf(),
auth_manager,
provider,
);
let hidden_model = remote_model_with_visibility("hidden", "Hidden", 0, "hide");
let visible_model = remote_model_with_visibility("visible", "Visible", 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]);
}
#[test]
fn bundled_models_json_roundtrips() {
let file_contents = include_str!("../../models.json");
let response: ModelsResponse =
serde_json::from_str(file_contents).expect("bundled models.json should deserialize");
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"
);
}
}
#[path = "manager_tests.rs"]
mod tests;

View File

@@ -0,0 +1,574 @@
use super::*;
use crate::CodexAuth;
use crate::auth::AuthCredentialsStoreMode;
use crate::config::ConfigBuilder;
use crate::model_provider_info::WireApi;
use chrono::Utc;
use codex_protocol::openai_models::ModelsResponse;
use core_test_support::responses::mount_models_once;
use pretty_assertions::assert_eq;
use serde_json::json;
use tempfile::tempdir;
use wiremock::MockServer;
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,
"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
);
}
}
fn provider_for(base_url: String) -> ModelProviderInfo {
ModelProviderInfo {
name: "mock".into(),
base_url: Some(base_url),
env_key: None,
env_key_instructions: None,
experimental_bearer_token: None,
wire_api: WireApi::Responses,
query_params: None,
http_headers: None,
env_http_headers: None,
request_max_retries: Some(0),
stream_max_retries: Some(0),
stream_idle_timeout_ms: Some(5_000),
requires_openai_auth: false,
supports_websockets: false,
}
}
#[tokio::test]
async fn get_model_info_tracks_fallback_usage() {
let codex_home = tempdir().expect("temp dir");
let config = ConfigBuilder::default()
.codex_home(codex_home.path().to_path_buf())
.build()
.await
.expect("load default test config");
let auth_manager = AuthManager::from_auth_for_testing(CodexAuth::from_api_key("Test API Key"));
let manager = ModelsManager::new(
codex_home.path().to_path_buf(),
auth_manager,
None,
CollaborationModesConfig::default(),
);
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 codex_home = tempdir().expect("temp dir");
let config = ConfigBuilder::default()
.codex_home(codex_home.path().to_path_buf())
.build()
.await
.expect("load default test config");
let mut overlay = remote_model("gpt-overlay", "Overlay", 0);
overlay.supports_image_detail_original = true;
let auth_manager = AuthManager::from_auth_for_testing(CodexAuth::from_api_key("Test API Key"));
let manager = ModelsManager::new(
codex_home.path().to_path_buf(),
auth_manager,
Some(ModelsResponse {
models: vec![overlay],
}),
CollaborationModesConfig::default(),
);
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 codex_home = tempdir().expect("temp dir");
let config = ConfigBuilder::default()
.codex_home(codex_home.path().to_path_buf())
.build()
.await
.expect("load default test config");
let mut remote = remote_model("gpt-image", "Image", 0);
remote.supports_image_detail_original = true;
let auth_manager = AuthManager::from_auth_for_testing(CodexAuth::from_api_key("Test API Key"));
let manager = ModelsManager::new(
codex_home.path().to_path_buf(),
auth_manager,
Some(ModelsResponse {
models: vec![remote],
}),
CollaborationModesConfig::default(),
);
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_rejects_multi_segment_namespace_suffix_matching() {
let codex_home = tempdir().expect("temp dir");
let config = ConfigBuilder::default()
.codex_home(codex_home.path().to_path_buf())
.build()
.await
.expect("load default test config");
let auth_manager = AuthManager::from_auth_for_testing(CodexAuth::from_api_key("Test API Key"));
let manager = ModelsManager::new(
codex_home.path().to_path_buf(),
auth_manager,
None,
CollaborationModesConfig::default(),
);
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 server = MockServer::start().await;
let remote_models = vec![
remote_model("priority-low", "Low", 1),
remote_model("priority-high", "High", 0),
];
let models_mock = mount_models_once(
&server,
ModelsResponse {
models: remote_models.clone(),
},
)
.await;
let codex_home = tempdir().expect("temp dir");
let auth_manager =
AuthManager::from_auth_for_testing(CodexAuth::create_dummy_chatgpt_auth_for_testing());
let provider = provider_for(server.uri());
let manager = ModelsManager::with_provider_for_tests(
codex_home.path().to_path_buf(),
auth_manager,
provider,
);
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!(
models_mock.requests().len(),
1,
"expected a single /models request"
);
}
#[tokio::test]
async fn refresh_available_models_uses_cache_when_fresh() {
let server = MockServer::start().await;
let remote_models = vec![remote_model("cached", "Cached", 5)];
let models_mock = mount_models_once(
&server,
ModelsResponse {
models: remote_models.clone(),
},
)
.await;
let codex_home = tempdir().expect("temp dir");
let auth_manager =
AuthManager::from_auth_for_testing(CodexAuth::create_dummy_chatgpt_auth_for_testing());
let provider = provider_for(server.uri());
let manager = ModelsManager::with_provider_for_tests(
codex_home.path().to_path_buf(),
auth_manager,
provider,
);
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!(
models_mock.requests().len(),
1,
"cache hit should avoid a second /models request"
);
}
#[tokio::test]
async fn refresh_available_models_refetches_when_cache_stale() {
let server = MockServer::start().await;
let initial_models = vec![remote_model("stale", "Stale", 1)];
let initial_mock = mount_models_once(
&server,
ModelsResponse {
models: initial_models.clone(),
},
)
.await;
let codex_home = tempdir().expect("temp dir");
let auth_manager =
AuthManager::from_auth_for_testing(CodexAuth::create_dummy_chatgpt_auth_for_testing());
let provider = provider_for(server.uri());
let manager = ModelsManager::with_provider_for_tests(
codex_home.path().to_path_buf(),
auth_manager,
provider,
);
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");
let updated_models = vec![remote_model("fresh", "Fresh", 9)];
server.reset().await;
let refreshed_mock = mount_models_once(
&server,
ModelsResponse {
models: updated_models.clone(),
},
)
.await;
manager
.refresh_available_models(RefreshStrategy::OnlineIfUncached)
.await
.expect("second refresh succeeds");
assert_models_contain(&manager.get_remote_models().await, &updated_models);
assert_eq!(
initial_mock.requests().len(),
1,
"initial refresh should only hit /models once"
);
assert_eq!(
refreshed_mock.requests().len(),
1,
"stale cache refresh should fetch /models once"
);
}
#[tokio::test]
async fn refresh_available_models_refetches_when_version_mismatch() {
let server = MockServer::start().await;
let initial_models = vec![remote_model("old", "Old", 1)];
let initial_mock = mount_models_once(
&server,
ModelsResponse {
models: initial_models.clone(),
},
)
.await;
let codex_home = tempdir().expect("temp dir");
let auth_manager =
AuthManager::from_auth_for_testing(CodexAuth::create_dummy_chatgpt_auth_for_testing());
let provider = provider_for(server.uri());
let manager = ModelsManager::with_provider_for_tests(
codex_home.path().to_path_buf(),
auth_manager,
provider,
);
manager
.refresh_available_models(RefreshStrategy::OnlineIfUncached)
.await
.expect("initial refresh succeeds");
manager
.cache_manager
.mutate_cache_for_test(|cache| {
let client_version = crate::models_manager::client_version_to_whole();
cache.client_version = Some(format!("{client_version}-mismatch"));
})
.await
.expect("cache mutation succeeds");
let updated_models = vec![remote_model("new", "New", 2)];
server.reset().await;
let refreshed_mock = mount_models_once(
&server,
ModelsResponse {
models: updated_models.clone(),
},
)
.await;
manager
.refresh_available_models(RefreshStrategy::OnlineIfUncached)
.await
.expect("second refresh succeeds");
assert_models_contain(&manager.get_remote_models().await, &updated_models);
assert_eq!(
initial_mock.requests().len(),
1,
"initial refresh should only hit /models once"
);
assert_eq!(
refreshed_mock.requests().len(),
1,
"version mismatch should fetch /models once"
);
}
#[tokio::test]
async fn refresh_available_models_drops_removed_remote_models() {
let server = MockServer::start().await;
let initial_models = vec![remote_model("remote-old", "Remote Old", 1)];
let initial_mock = mount_models_once(
&server,
ModelsResponse {
models: initial_models,
},
)
.await;
let codex_home = tempdir().expect("temp dir");
let auth_manager =
AuthManager::from_auth_for_testing(CodexAuth::create_dummy_chatgpt_auth_for_testing());
let provider = provider_for(server.uri());
let mut manager = ModelsManager::with_provider_for_tests(
codex_home.path().to_path_buf(),
auth_manager,
provider,
);
manager.cache_manager.set_ttl(Duration::ZERO);
manager
.refresh_available_models(RefreshStrategy::OnlineIfUncached)
.await
.expect("initial refresh succeeds");
server.reset().await;
let refreshed_models = vec![remote_model("remote-new", "Remote New", 1)];
let refreshed_mock = mount_models_once(
&server,
ModelsResponse {
models: refreshed_models,
},
)
.await;
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!(
initial_mock.requests().len(),
1,
"initial refresh should only hit /models once"
);
assert_eq!(
refreshed_mock.requests().len(),
1,
"second refresh should only hit /models once"
);
}
#[tokio::test]
async fn refresh_available_models_skips_network_without_chatgpt_auth() {
let server = MockServer::start().await;
let dynamic_slug = "dynamic-model-only-for-test-noauth";
let models_mock = mount_models_once(
&server,
ModelsResponse {
models: vec![remote_model(dynamic_slug, "No Auth", 1)],
},
)
.await;
let codex_home = tempdir().expect("temp dir");
let auth_manager = Arc::new(AuthManager::new(
codex_home.path().to_path_buf(),
false,
AuthCredentialsStoreMode::File,
));
let provider = provider_for(server.uri());
let manager = ModelsManager::with_provider_for_tests(
codex_home.path().to_path_buf(),
auth_manager,
provider,
);
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!(
models_mock.requests().len(),
0,
"no auth should avoid /models requests"
);
}
#[test]
fn build_available_models_picks_default_after_hiding_hidden_models() {
let codex_home = tempdir().expect("temp dir");
let auth_manager = AuthManager::from_auth_for_testing(CodexAuth::from_api_key("Test API Key"));
let provider = provider_for("http://example.test".to_string());
let manager = ModelsManager::with_provider_for_tests(
codex_home.path().to_path_buf(),
auth_manager,
provider,
);
let hidden_model = remote_model_with_visibility("hidden", "Hidden", 0, "hide");
let visible_model = remote_model_with_visibility("visible", "Visible", 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]);
}
#[test]
fn bundled_models_json_roundtrips() {
let file_contents = include_str!("../../models.json");
let response: ModelsResponse =
serde_json::from_str(file_contents).expect("bundled models.json should deserialize");
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"
);
}

View File

@@ -110,44 +110,5 @@ fn local_personality_messages_for_slug(slug: &str) -> Option<ModelMessages> {
}
#[cfg(test)]
mod tests {
use super::*;
use crate::config::test_config;
use pretty_assertions::assert_eq;
#[test]
fn reasoning_summaries_override_true_enables_support() {
let model = model_info_from_slug("unknown-model");
let mut config = test_config();
config.model_supports_reasoning_summaries = Some(true);
let updated = with_config_overrides(model.clone(), &config);
let mut expected = model;
expected.supports_reasoning_summaries = true;
assert_eq!(updated, expected);
}
#[test]
fn reasoning_summaries_override_false_does_not_disable_support() {
let mut model = model_info_from_slug("unknown-model");
model.supports_reasoning_summaries = true;
let mut config = test_config();
config.model_supports_reasoning_summaries = Some(false);
let updated = with_config_overrides(model.clone(), &config);
assert_eq!(updated, model);
}
#[test]
fn reasoning_summaries_override_false_is_noop_when_model_is_false() {
let model = model_info_from_slug("unknown-model");
let mut config = test_config();
config.model_supports_reasoning_summaries = Some(false);
let updated = with_config_overrides(model.clone(), &config);
assert_eq!(updated, model);
}
}
#[path = "model_info_tests.rs"]
mod tests;

View File

@@ -0,0 +1,39 @@
use super::*;
use crate::config::test_config;
use pretty_assertions::assert_eq;
#[test]
fn reasoning_summaries_override_true_enables_support() {
let model = model_info_from_slug("unknown-model");
let mut config = test_config();
config.model_supports_reasoning_summaries = Some(true);
let updated = with_config_overrides(model.clone(), &config);
let mut expected = model;
expected.supports_reasoning_summaries = true;
assert_eq!(updated, expected);
}
#[test]
fn reasoning_summaries_override_false_does_not_disable_support() {
let mut model = model_info_from_slug("unknown-model");
model.supports_reasoning_summaries = true;
let mut config = test_config();
config.model_supports_reasoning_summaries = Some(false);
let updated = with_config_overrides(model.clone(), &config);
assert_eq!(updated, model);
}
#[test]
fn reasoning_summaries_override_false_is_noop_when_model_is_false() {
let model = model_info_from_slug("unknown-model");
let mut config = test_config();
config.model_supports_reasoning_summaries = Some(false);
let updated = with_config_overrides(model.clone(), &config);
assert_eq!(updated, model);
}