Remove test-support feature from codex-core and replace it with explicit test toggles (#11405)

## Why

`codex-core` was being built in multiple feature-resolved permutations
because test-only behavior was modeled as crate features. For a large
crate, those permutations increase compile cost and reduce cache reuse.

## Net Change

- Removed the `test-support` crate feature and related feature wiring so
`codex-core` no longer needs separate feature shapes for test consumers.
- Standardized cross-crate test-only access behind
`codex_core::test_support`.
- External test code now imports helpers from
`codex_core::test_support`.
- Underlying implementation hooks are kept internal (`pub(crate)`)
instead of broadly public.

## Outcome

- Fewer `codex-core` build permutations.
- Better incremental cache reuse across test targets.
- No intended production behavior change.
This commit is contained in:
Michael Bolin
2026-02-10 22:44:02 -08:00
committed by GitHub
parent f6dd9e37e7
commit 476c1a7160
36 changed files with 393 additions and 266 deletions

View File

@@ -2624,12 +2624,9 @@ mod tests {
use crate::history_cell::HistoryCell;
use crate::history_cell::UserHistoryCell;
use crate::history_cell::new_session_info;
use codex_core::AuthManager;
use codex_core::CodexAuth;
use codex_core::ThreadManager;
use codex_core::config::ConfigBuilder;
use codex_core::config::ConfigOverrides;
use codex_core::models_manager::manager::ModelsManager;
use codex_core::protocol::AskForApproval;
use codex_core::protocol::Event;
use codex_core::protocol::EventMsg;
@@ -2726,14 +2723,17 @@ mod tests {
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 server = Arc::new(ThreadManager::with_models_provider(
let server = Arc::new(
codex_core::test_support::thread_manager_with_models_provider(
CodexAuth::from_api_key("Test API Key"),
config.model_provider.clone(),
),
);
let auth_manager = codex_core::test_support::auth_manager_from_auth(
CodexAuth::from_api_key("Test API Key"),
config.model_provider.clone(),
));
let auth_manager =
AuthManager::from_auth_for_testing(CodexAuth::from_api_key("Test API Key"));
);
let file_search = FileSearchManager::new(config.cwd.clone(), app_event_tx.clone());
let model = ModelsManager::get_model_offline(config.model.as_deref());
let model = codex_core::test_support::get_model_offline(config.model.as_deref());
let otel_manager = test_otel_manager(&config, model.as_str());
App {
@@ -2779,14 +2779,17 @@ mod tests {
) {
let (chat_widget, app_event_tx, rx, op_rx) = make_chatwidget_manual_with_sender().await;
let config = chat_widget.config_ref().clone();
let server = Arc::new(ThreadManager::with_models_provider(
let server = Arc::new(
codex_core::test_support::thread_manager_with_models_provider(
CodexAuth::from_api_key("Test API Key"),
config.model_provider.clone(),
),
);
let auth_manager = codex_core::test_support::auth_manager_from_auth(
CodexAuth::from_api_key("Test API Key"),
config.model_provider.clone(),
));
let auth_manager =
AuthManager::from_auth_for_testing(CodexAuth::from_api_key("Test API Key"));
);
let file_search = FileSearchManager::new(config.cwd.clone(), app_event_tx.clone());
let model = ModelsManager::get_model_offline(config.model.as_deref());
let model = codex_core::test_support::get_model_offline(config.model.as_deref());
let otel_manager = test_otel_manager(&config, model.as_str());
(
@@ -2830,7 +2833,7 @@ mod tests {
}
fn test_otel_manager(config: &Config, model: &str) -> OtelManager {
let model_info = ModelsManager::construct_model_info_offline(model, config);
let model_info = codex_core::test_support::construct_model_info_offline(model, config);
OtelManager::new(
ThreadId::new(),
model,
@@ -2846,7 +2849,7 @@ mod tests {
}
fn all_model_presets() -> Vec<ModelPreset> {
codex_core::models_manager::model_presets::all_model_presets().clone()
codex_core::test_support::all_model_presets().clone()
}
fn model_migration_copy_to_plain_text(

View File

@@ -16,7 +16,6 @@ use crate::test_backend::VT100Backend;
use crate::tui::FrameRequester;
use assert_matches::assert_matches;
use codex_common::approval_presets::builtin_approval_presets;
use codex_core::AuthManager;
use codex_core::CodexAuth;
use codex_core::config::Config;
use codex_core::config::ConfigBuilder;
@@ -965,13 +964,16 @@ async fn helpers_are_available_and_do_not_panic() {
let (tx_raw, _rx) = unbounded_channel::<AppEvent>();
let tx = AppEventSender::new(tx_raw);
let cfg = test_config().await;
let resolved_model = ModelsManager::get_model_offline(cfg.model.as_deref());
let resolved_model = codex_core::test_support::get_model_offline(cfg.model.as_deref());
let otel_manager = test_otel_manager(&cfg, resolved_model.as_str());
let thread_manager = Arc::new(ThreadManager::with_models_provider(
CodexAuth::from_api_key("test"),
cfg.model_provider.clone(),
));
let auth_manager = AuthManager::from_auth_for_testing(CodexAuth::from_api_key("test"));
let thread_manager = Arc::new(
codex_core::test_support::thread_manager_with_models_provider(
CodexAuth::from_api_key("test"),
cfg.model_provider.clone(),
),
);
let auth_manager =
codex_core::test_support::auth_manager_from_auth(CodexAuth::from_api_key("test"));
let init = ChatWidgetInit {
config: cfg,
frame_requester: FrameRequester::test_dummy(),
@@ -993,7 +995,7 @@ async fn helpers_are_available_and_do_not_panic() {
}
fn test_otel_manager(config: &Config, model: &str) -> OtelManager {
let model_info = ModelsManager::construct_model_info_offline(model, config);
let model_info = codex_core::test_support::construct_model_info_offline(model, config);
OtelManager::new(
ThreadId::new(),
model,
@@ -1022,7 +1024,7 @@ async fn make_chatwidget_manual(
let mut cfg = test_config().await;
let resolved_model = model_override
.map(str::to_owned)
.unwrap_or_else(|| ModelsManager::get_model_offline(cfg.model.as_deref()));
.unwrap_or_else(|| codex_core::test_support::get_model_offline(cfg.model.as_deref()));
if let Some(model) = model_override {
cfg.model = Some(model.to_string());
}
@@ -1039,7 +1041,8 @@ async fn make_chatwidget_manual(
});
bottom.set_steer_enabled(true);
bottom.set_collaboration_modes_enabled(cfg.features.enabled(Feature::CollaborationModes));
let auth_manager = AuthManager::from_auth_for_testing(CodexAuth::from_api_key("test"));
let auth_manager =
codex_core::test_support::auth_manager_from_auth(CodexAuth::from_api_key("test"));
let codex_home = cfg.codex_home.clone();
let models_manager = Arc::new(ModelsManager::new(codex_home, auth_manager.clone()));
let reasoning_effort = None;
@@ -1144,8 +1147,9 @@ fn next_submit_op(op_rx: &mut tokio::sync::mpsc::UnboundedReceiver<Op>) -> Op {
}
fn set_chatgpt_auth(chat: &mut ChatWidget) {
chat.auth_manager =
AuthManager::from_auth_for_testing(CodexAuth::create_dummy_chatgpt_auth_for_testing());
chat.auth_manager = codex_core::test_support::auth_manager_from_auth(
CodexAuth::create_dummy_chatgpt_auth_for_testing(),
);
chat.models_manager = Arc::new(ModelsManager::new(
chat.config.codex_home.clone(),
chat.auth_manager.clone(),
@@ -1448,8 +1452,9 @@ async fn rate_limit_snapshots_keep_separate_entries_per_limit_id() {
#[tokio::test]
async fn rate_limit_switch_prompt_skips_when_on_lower_cost_model() {
let (mut chat, _, _) = make_chatwidget_manual(Some(NUDGE_MODEL_SLUG)).await;
chat.auth_manager =
AuthManager::from_auth_for_testing(CodexAuth::create_dummy_chatgpt_auth_for_testing());
chat.auth_manager = codex_core::test_support::auth_manager_from_auth(
CodexAuth::create_dummy_chatgpt_auth_for_testing(),
);
chat.on_rate_limit_snapshot(Some(snapshot(95.0)));
@@ -1463,7 +1468,7 @@ async fn rate_limit_switch_prompt_skips_when_on_lower_cost_model() {
async fn rate_limit_switch_prompt_skips_non_codex_limit() {
let auth = CodexAuth::create_dummy_chatgpt_auth_for_testing();
let (mut chat, _, _) = make_chatwidget_manual(Some("gpt-5")).await;
chat.auth_manager = AuthManager::from_auth_for_testing(auth);
chat.auth_manager = codex_core::test_support::auth_manager_from_auth(auth);
chat.on_rate_limit_snapshot(Some(RateLimitSnapshot {
limit_id: Some("codex_other".to_string()),
@@ -1488,7 +1493,7 @@ async fn rate_limit_switch_prompt_skips_non_codex_limit() {
async fn rate_limit_switch_prompt_shows_once_per_session() {
let auth = CodexAuth::create_dummy_chatgpt_auth_for_testing();
let (mut chat, _, _) = make_chatwidget_manual(Some("gpt-5")).await;
chat.auth_manager = AuthManager::from_auth_for_testing(auth);
chat.auth_manager = codex_core::test_support::auth_manager_from_auth(auth);
chat.on_rate_limit_snapshot(Some(snapshot(90.0)));
assert!(
@@ -1512,7 +1517,7 @@ async fn rate_limit_switch_prompt_shows_once_per_session() {
async fn rate_limit_switch_prompt_respects_hidden_notice() {
let auth = CodexAuth::create_dummy_chatgpt_auth_for_testing();
let (mut chat, _, _) = make_chatwidget_manual(Some("gpt-5")).await;
chat.auth_manager = AuthManager::from_auth_for_testing(auth);
chat.auth_manager = codex_core::test_support::auth_manager_from_auth(auth);
chat.config.notices.hide_rate_limit_model_nudge = Some(true);
chat.on_rate_limit_snapshot(Some(snapshot(95.0)));
@@ -1527,7 +1532,7 @@ async fn rate_limit_switch_prompt_respects_hidden_notice() {
async fn rate_limit_switch_prompt_defers_until_task_complete() {
let auth = CodexAuth::create_dummy_chatgpt_auth_for_testing();
let (mut chat, _, _) = make_chatwidget_manual(Some("gpt-5")).await;
chat.auth_manager = AuthManager::from_auth_for_testing(auth);
chat.auth_manager = codex_core::test_support::auth_manager_from_auth(auth);
chat.bottom_pane.set_task_running(true);
chat.on_rate_limit_snapshot(Some(snapshot(90.0)));
@@ -1547,8 +1552,9 @@ async fn rate_limit_switch_prompt_defers_until_task_complete() {
#[tokio::test]
async fn rate_limit_switch_prompt_popup_snapshot() {
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(Some("gpt-5")).await;
chat.auth_manager =
AuthManager::from_auth_for_testing(CodexAuth::create_dummy_chatgpt_auth_for_testing());
chat.auth_manager = codex_core::test_support::auth_manager_from_auth(
CodexAuth::create_dummy_chatgpt_auth_for_testing(),
);
chat.on_rate_limit_snapshot(Some(snapshot(92.0)));
chat.maybe_show_pending_rate_limit_prompt();
@@ -1881,8 +1887,9 @@ async fn plan_implementation_popup_shows_after_proposed_plan_output() {
#[tokio::test]
async fn plan_implementation_popup_skips_when_rate_limit_prompt_pending() {
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(Some("gpt-5")).await;
chat.auth_manager =
AuthManager::from_auth_for_testing(CodexAuth::create_dummy_chatgpt_auth_for_testing());
chat.auth_manager = codex_core::test_support::auth_manager_from_auth(
CodexAuth::create_dummy_chatgpt_auth_for_testing(),
);
chat.set_feature_enabled(Feature::CollaborationModes, true);
let plan_mask =
collaboration_modes::mask_for_kind(chat.models_manager.as_ref(), ModeKind::Plan)
@@ -3143,13 +3150,16 @@ async fn collaboration_modes_defaults_to_code_on_startup() {
.build()
.await
.expect("config");
let resolved_model = ModelsManager::get_model_offline(cfg.model.as_deref());
let resolved_model = codex_core::test_support::get_model_offline(cfg.model.as_deref());
let otel_manager = test_otel_manager(&cfg, resolved_model.as_str());
let thread_manager = Arc::new(ThreadManager::with_models_provider(
CodexAuth::from_api_key("test"),
cfg.model_provider.clone(),
));
let auth_manager = AuthManager::from_auth_for_testing(CodexAuth::from_api_key("test"));
let thread_manager = Arc::new(
codex_core::test_support::thread_manager_with_models_provider(
CodexAuth::from_api_key("test"),
cfg.model_provider.clone(),
),
);
let auth_manager =
codex_core::test_support::auth_manager_from_auth(CodexAuth::from_api_key("test"));
let init = ChatWidgetInit {
config: cfg,
frame_requester: FrameRequester::test_dummy(),
@@ -3189,13 +3199,16 @@ async fn experimental_mode_plan_applies_on_startup() {
.build()
.await
.expect("config");
let resolved_model = ModelsManager::get_model_offline(cfg.model.as_deref());
let resolved_model = codex_core::test_support::get_model_offline(cfg.model.as_deref());
let otel_manager = test_otel_manager(&cfg, resolved_model.as_str());
let thread_manager = Arc::new(ThreadManager::with_models_provider(
CodexAuth::from_api_key("test"),
cfg.model_provider.clone(),
));
let auth_manager = AuthManager::from_auth_for_testing(CodexAuth::from_api_key("test"));
let thread_manager = Arc::new(
codex_core::test_support::thread_manager_with_models_provider(
CodexAuth::from_api_key("test"),
cfg.model_provider.clone(),
),
);
let auth_manager =
codex_core::test_support::auth_manager_from_auth(CodexAuth::from_api_key("test"));
let init = ChatWidgetInit {
config: cfg,
frame_requester: FrameRequester::test_dummy(),

View File

@@ -7,7 +7,6 @@ use chrono::Utc;
use codex_core::AuthManager;
use codex_core::config::Config;
use codex_core::config::ConfigBuilder;
use codex_core::models_manager::manager::ModelsManager;
use codex_core::protocol::CreditsSnapshot;
use codex_core::protocol::RateLimitSnapshot;
use codex_core::protocol::RateLimitWindow;
@@ -40,7 +39,7 @@ fn test_auth_manager(config: &Config) -> AuthManager {
fn token_info_for(model_slug: &str, config: &Config, usage: &TokenUsage) -> TokenUsageInfo {
let context_window =
ModelsManager::construct_model_info_offline(model_slug, config).context_window;
codex_core::test_support::construct_model_info_offline(model_slug, config).context_window;
TokenUsageInfo {
total_token_usage: usage.clone(),
last_token_usage: usage.clone(),
@@ -139,7 +138,7 @@ async fn status_snapshot_includes_reasoning_details() {
};
let rate_display = rate_limit_snapshot_display(&snapshot, captured_at);
let model_slug = ModelsManager::get_model_offline(config.model.as_deref());
let model_slug = codex_core::test_support::get_model_offline(config.model.as_deref());
let token_info = token_info_for(&model_slug, &config, &usage);
let reasoning_effort_override = Some(Some(ReasoningEffort::High));
@@ -190,7 +189,7 @@ async fn status_snapshot_includes_forked_from() {
.single()
.expect("valid time");
let model_slug = ModelsManager::get_model_offline(config.model.as_deref());
let model_slug = codex_core::test_support::get_model_offline(config.model.as_deref());
let token_info = token_info_for(&model_slug, &config, &usage);
let session_id =
ThreadId::from_string("0f0f3c13-6cf9-4aa4-8b80-7d49c2f1be2e").expect("session id");
@@ -257,7 +256,7 @@ async fn status_snapshot_includes_monthly_limit() {
};
let rate_display = rate_limit_snapshot_display(&snapshot, captured_at);
let model_slug = ModelsManager::get_model_offline(config.model.as_deref());
let model_slug = codex_core::test_support::get_model_offline(config.model.as_deref());
let token_info = token_info_for(&model_slug, &config, &usage);
let composite = new_status_output(
&config,
@@ -307,7 +306,7 @@ async fn status_snapshot_shows_unlimited_credits() {
plan_type: None,
};
let rate_display = rate_limit_snapshot_display(&snapshot, captured_at);
let model_slug = ModelsManager::get_model_offline(config.model.as_deref());
let model_slug = codex_core::test_support::get_model_offline(config.model.as_deref());
let token_info = token_info_for(&model_slug, &config, &usage);
let composite = new_status_output(
&config,
@@ -356,7 +355,7 @@ async fn status_snapshot_shows_positive_credits() {
plan_type: None,
};
let rate_display = rate_limit_snapshot_display(&snapshot, captured_at);
let model_slug = ModelsManager::get_model_offline(config.model.as_deref());
let model_slug = codex_core::test_support::get_model_offline(config.model.as_deref());
let token_info = token_info_for(&model_slug, &config, &usage);
let composite = new_status_output(
&config,
@@ -405,7 +404,7 @@ async fn status_snapshot_hides_zero_credits() {
plan_type: None,
};
let rate_display = rate_limit_snapshot_display(&snapshot, captured_at);
let model_slug = ModelsManager::get_model_offline(config.model.as_deref());
let model_slug = codex_core::test_support::get_model_offline(config.model.as_deref());
let token_info = token_info_for(&model_slug, &config, &usage);
let composite = new_status_output(
&config,
@@ -452,7 +451,7 @@ async fn status_snapshot_hides_when_has_no_credits_flag() {
plan_type: None,
};
let rate_display = rate_limit_snapshot_display(&snapshot, captured_at);
let model_slug = ModelsManager::get_model_offline(config.model.as_deref());
let model_slug = codex_core::test_support::get_model_offline(config.model.as_deref());
let token_info = token_info_for(&model_slug, &config, &usage);
let composite = new_status_output(
&config,
@@ -497,7 +496,7 @@ async fn status_card_token_usage_excludes_cached_tokens() {
.single()
.expect("timestamp");
let model_slug = ModelsManager::get_model_offline(config.model.as_deref());
let model_slug = codex_core::test_support::get_model_offline(config.model.as_deref());
let token_info = token_info_for(&model_slug, &config, &usage);
let composite = new_status_output(
&config,
@@ -558,7 +557,7 @@ async fn status_snapshot_truncates_in_narrow_terminal() {
};
let rate_display = rate_limit_snapshot_display(&snapshot, captured_at);
let model_slug = ModelsManager::get_model_offline(config.model.as_deref());
let model_slug = codex_core::test_support::get_model_offline(config.model.as_deref());
let token_info = token_info_for(&model_slug, &config, &usage);
let reasoning_effort_override = Some(Some(ReasoningEffort::High));
let composite = new_status_output(
@@ -608,7 +607,7 @@ async fn status_snapshot_shows_missing_limits_message() {
.single()
.expect("timestamp");
let model_slug = ModelsManager::get_model_offline(config.model.as_deref());
let model_slug = codex_core::test_support::get_model_offline(config.model.as_deref());
let token_info = token_info_for(&model_slug, &config, &usage);
let composite = new_status_output(
&config,
@@ -677,7 +676,7 @@ async fn status_snapshot_includes_credits_and_limits() {
};
let rate_display = rate_limit_snapshot_display(&snapshot, captured_at);
let model_slug = ModelsManager::get_model_offline(config.model.as_deref());
let model_slug = codex_core::test_support::get_model_offline(config.model.as_deref());
let token_info = token_info_for(&model_slug, &config, &usage);
let composite = new_status_output(
&config,
@@ -734,7 +733,7 @@ async fn status_snapshot_shows_empty_limits_message() {
.expect("timestamp");
let rate_display = rate_limit_snapshot_display(&snapshot, captured_at);
let model_slug = ModelsManager::get_model_offline(config.model.as_deref());
let model_slug = codex_core::test_support::get_model_offline(config.model.as_deref());
let token_info = token_info_for(&model_slug, &config, &usage);
let composite = new_status_output(
&config,
@@ -800,7 +799,7 @@ async fn status_snapshot_shows_stale_limits_message() {
let rate_display = rate_limit_snapshot_display(&snapshot, captured_at);
let now = captured_at + ChronoDuration::minutes(20);
let model_slug = ModelsManager::get_model_offline(config.model.as_deref());
let model_slug = codex_core::test_support::get_model_offline(config.model.as_deref());
let token_info = token_info_for(&model_slug, &config, &usage);
let composite = new_status_output(
&config,
@@ -870,7 +869,7 @@ async fn status_snapshot_cached_limits_hide_credits_without_flag() {
let rate_display = rate_limit_snapshot_display(&snapshot, captured_at);
let now = captured_at + ChronoDuration::minutes(20);
let model_slug = ModelsManager::get_model_offline(config.model.as_deref());
let model_slug = codex_core::test_support::get_model_offline(config.model.as_deref());
let token_info = token_info_for(&model_slug, &config, &usage);
let composite = new_status_output(
&config,
@@ -924,7 +923,7 @@ async fn status_context_window_uses_last_usage() {
.single()
.expect("timestamp");
let model_slug = ModelsManager::get_model_offline(config.model.as_deref());
let model_slug = codex_core::test_support::get_model_offline(config.model.as_deref());
let token_info = TokenUsageInfo {
total_token_usage: total_usage.clone(),
last_token_usage: last_usage,