Update tests for default-enabled update_plan (#41630)

## What changed

- Cover the default, explicitly enabled, and explicitly disabled states of
  `tools.update_plan.enabled`.
- Verify prompt tool lists remain consistent across requests, including when
  custom base and developer instructions are configured.
- Explicitly enable `update_plan` in existing fixtures that depend on the tool.

GitOrigin-RevId: 3be279cb5195dfc5892c2c8ff6a1461fac879309
This commit is contained in:
rka-oai
2026-08-30 07:40:36 +00:00
committed by copyberry
parent dde85b435b
commit 28327355b8
20 changed files with 111 additions and 24 deletions
@@ -1350,7 +1350,10 @@ async fn turn_profile_tracks_blocking_tool_and_follow_up_sampling() -> Result<()
let codex_home = TempDir::new()?;
MockResponsesConfig::new(&server.uri())
.enable_feature(Feature::Goals)
.with_root_config(&format!("chatgpt_base_url = \"{}\"", server.uri()))
.with_root_config(&format!(
"chatgpt_base_url = \"{}\"\ntools.update_plan.enabled = true",
server.uri()
))
.write(codex_home.path())?;
mount_analytics_capture(&server, codex_home.path()).await?;
@@ -4044,7 +4047,10 @@ async fn turn_start_emits_spawn_agent_item_with_model_metadata_v2() -> Result<()
let codex_home = TempDir::new()?;
MockResponsesConfig::new(&server.uri())
.enable_feature(Feature::Collab)
.with_root_config(&format!("chatgpt_base_url = \"{}\"", server.uri()))
.with_root_config(&format!(
"chatgpt_base_url = \"{}\"\ntools.update_plan.enabled = true",
server.uri()
))
.write(codex_home.path())?;
mount_analytics_capture(&server, codex_home.path()).await?;
+17 -14
View File
@@ -565,21 +565,24 @@ async fn load_config_resolves_non_prefixed_mcp_tool_servers() -> std::io::Result
#[tokio::test]
async fn load_config_resolves_update_plan_enabled() -> std::io::Result<()> {
let codex_home = tempdir()?;
let config_toml = toml::from_str(
r#"
[tools.update_plan]
enabled = false
"#,
)
.expect("TOML deserialization should succeed");
let config = Config::load_from_base_config_with_overrides(
config_toml,
ConfigOverrides::default(),
codex_home.abs(),
)
.await?;
for (config_toml, expected_enabled) in [
("", true),
("[tools.update_plan]", true),
("[tools.update_plan]\nenabled = false", false),
("[tools.update_plan]\nenabled = true", true),
] {
let config = Config::load_from_base_config_with_overrides(
toml::from_str(config_toml).expect("TOML deserialization should succeed"),
ConfigOverrides::default(),
codex_home.abs(),
)
.await?;
assert!(!config.update_plan_enabled);
assert_eq!(
config.update_plan_enabled, expected_enabled,
"{config_toml}"
);
}
Ok(())
}
+1
View File
@@ -3594,6 +3594,7 @@ async fn fork_startup_context_then_first_turn_diff_snapshot() -> anyhow::Result<
.await;
let mut builder = test_codex().with_config(|config| {
config.update_plan_enabled = true;
config.permissions.approval_policy =
codex_config::Constrained::allow_any(AskForApproval::OnRequest);
});
@@ -1917,6 +1917,7 @@ async fn strict_tool_collisions_reject_external_and_synthetic_duplicates() {
let (_session, mut turn) = make_session_and_context().await;
update_config(&mut turn, |config| {
config.tool_registry.error_on_tool_collisions = true;
config.update_plan_enabled = true;
});
if code_mode_enabled {
set_feature(&mut turn, Feature::CodeMode, /*enabled*/ true);
@@ -2587,6 +2588,7 @@ async fn code_mode_only_exposes_default_namespace_tools_directly() {
let plan = probe(|turn| {
set_features(turn, &[Feature::CodeMode, Feature::CodeModeOnly]);
update_config(turn, |config| {
config.update_plan_enabled = true;
config.code_mode.direct_only_tool_namespaces = vec!["functions".to_string()];
});
})
@@ -2644,6 +2646,7 @@ async fn code_mode_excludes_default_namespace_tools() {
let plan = probe(|turn| {
set_feature(turn, Feature::CodeMode, /*enabled*/ true);
update_config(turn, |config| {
config.update_plan_enabled = true;
config.code_mode.excluded_tool_namespaces = vec!["functions".to_string()];
});
})
@@ -31,7 +31,10 @@ async fn additional_context_is_model_visible_but_not_a_user_message_item() -> Re
)
.await;
let test = test_codex()
.with_config(|config| config.include_environment_context = false)
.with_config(|config| {
config.update_plan_enabled = true;
config.include_environment_context = false;
})
.build(&server)
.await?;
+4 -1
View File
@@ -1599,7 +1599,7 @@ async fn code_mode_update_plan_nested_tool_result_is_empty_object() -> Result<()
skip_if_no_network!(Ok(()));
let server = responses::start_mock_server().await;
let (_test, second_mock) = run_code_mode_turn(
let (_test, second_mock) = run_code_mode_turn_with_config(
&server,
"use exec to run update_plan",
r#"
@@ -1608,6 +1608,7 @@ const result = await tools.update_plan({
});
text(JSON.stringify(result));
"#,
|config| config.update_plan_enabled = true,
)
.await?;
@@ -5805,6 +5806,7 @@ async fn code_mode_excludes_configured_nested_tool_namespaces() -> Result<()> {
let server = responses::start_mock_server().await;
let mut builder = test_codex().with_config(|config| {
config.update_plan_enabled = true;
let _ = config.features.enable(Feature::CodeMode);
config.code_mode.excluded_tool_namespaces = vec!["excluded".to_string()];
});
@@ -5907,6 +5909,7 @@ async fn code_mode_omits_configured_mcp_server_tools() -> Result<()> {
model.supports_search_tool = false;
})
.with_config(move |config| {
config.update_plan_enabled = true;
let _ = config.features.enable(Feature::CodeMode);
let mut servers = config.mcp_servers.get().clone();
servers.insert(
+7
View File
@@ -2137,6 +2137,7 @@ async fn pre_sampling_compact_runs_on_switch_to_smaller_context_model() {
.with_auth(CodexAuth::create_dummy_chatgpt_auth_for_testing())
.with_model(previous_model)
.with_config(move |config| {
config.update_plan_enabled = true;
config.model_provider = model_provider;
set_test_compact_prompt(config);
});
@@ -3909,6 +3910,7 @@ async fn manual_compact_twice_preserves_latest_user_messages() {
let model_provider = non_openai_model_provider(&server);
let mut builder = test_codex().with_config(move |config| {
config.update_plan_enabled = true;
config.model_provider = model_provider;
set_test_compact_prompt(config);
});
@@ -4232,6 +4234,7 @@ async fn snapshot_request_shape_mid_turn_continuation_compaction() {
let model_provider = non_openai_model_provider(&server);
let mut builder = test_codex().with_config(move |config| {
config.update_plan_enabled = true;
config.model_provider = model_provider;
set_test_compact_prompt(config);
config.model_context_window = Some(context_window);
@@ -4831,6 +4834,7 @@ async fn snapshot_request_shape_pre_turn_compaction_including_incoming_user_mess
let model_provider = non_openai_model_provider(&server);
let codex = test_codex()
.with_config(move |config| {
config.update_plan_enabled = true;
config.model_provider = model_provider;
set_test_compact_prompt(config);
config.model_auto_compact_token_limit = Some(200);
@@ -4946,6 +4950,7 @@ async fn snapshot_request_shape_pre_turn_compaction_strips_incoming_model_switch
.with_auth(CodexAuth::create_dummy_chatgpt_auth_for_testing())
.with_model(previous_model)
.with_config(move |config| {
config.update_plan_enabled = true;
config.model_provider = model_provider;
set_test_compact_prompt(config);
let _ = config.features.enable(Feature::RemoteModels);
@@ -5043,6 +5048,7 @@ async fn snapshot_request_shape_pre_turn_compaction_context_window_exceeded() {
model_provider.stream_max_retries = Some(0);
let codex = test_codex()
.with_config(move |config| {
config.update_plan_enabled = true;
config.model_provider = model_provider;
set_test_compact_prompt(config);
config.model_auto_compact_token_limit = Some(200);
@@ -5117,6 +5123,7 @@ async fn snapshot_request_shape_manual_compact_without_previous_user_messages()
let model_provider = non_openai_model_provider(&server);
let codex = test_codex()
.with_config(move |config| {
config.update_plan_enabled = true;
config.model_provider = model_provider;
set_test_compact_prompt(config);
})
@@ -221,6 +221,7 @@ fn compacted_summary_only_output(summary: &str) -> Vec<ResponseItem> {
fn test_codex() -> TestCodexBuilder {
base_test_codex().with_config(|config| {
config.update_plan_enabled = true;
let _ = config.features.disable(Feature::RemoteCompactionV2);
})
}
@@ -846,6 +846,7 @@ async fn start_test_conversation(
let base_url = format!("{}/v1", server.uri());
let model = model.map(str::to_string);
let mut builder = test_codex().with_config(move |config| {
config.update_plan_enabled = true;
config.model_provider.name = "Non-OpenAI Model provider".to_string();
config.model_provider.base_url = Some(base_url);
config.compact_prompt = Some(SUMMARIZATION_PROMPT.to_string());
@@ -39,6 +39,7 @@ async fn first_request_item_types_roles_and_content_annotations() -> Result<()>
model_info.input_modalities.push(InputModality::Audio);
})
.with_config(|config| {
config.update_plan_enabled = true;
config.developer_instructions = Some("Keep world-state annotations aligned.".into());
config.model_context_window = Some(128_000);
config.current_time_reminder = Some(CurrentTimeReminderConfig::default());
@@ -569,6 +569,7 @@ async fn cached_mcp_startup_is_eager_for_root_and_lazy_for_subagents() -> anyhow
let fixture = test_codex()
.with_model_info_override("gpt-5.4", |model| model.supports_search_tool = false)
.with_config(move |config| {
config.update_plan_enabled = true;
config.permissions.approval_policy = Constrained::allow_any(AskForApproval::Never);
config
.permissions
@@ -211,6 +211,7 @@ fn format_labeled_requests_snapshot(
}
fn enable_deferred_tool_world_state_without_agents(config: &mut Config) {
config.update_plan_enabled = true;
config.agents_enabled = false;
config
.features
@@ -1148,6 +1149,7 @@ async fn apps_guidance_and_deferred_namespace_appear_after_recovery_within_a_tur
let mut builder = search_capable_apps_builder(apps_server.chatgpt_base_url.clone())
.with_extensions(Arc::new(extensions.build()))
.with_config(|config| {
config.update_plan_enabled = true;
config
.features
.enable(Feature::DefaultModeRequestUserInput)
@@ -285,6 +285,7 @@ async fn snapshot_model_visible_layout_turn_overrides() -> Result<()> {
.with_extensions(skills_extensions())
.with_model("gpt-5.4")
.with_config(|config| {
config.update_plan_enabled = true;
config
.features
.enable(Feature::Personality)
@@ -399,6 +400,7 @@ async fn snapshot_model_visible_layout_cwd_change_refreshes_agents() -> Result<(
.await;
let mut builder = test_codex()
.with_config(|config| config.update_plan_enabled = true)
.with_extensions(skills_extensions())
.with_model("gpt-5.4");
let test = builder.build(&server).await?;
@@ -511,6 +513,7 @@ async fn snapshot_model_visible_layout_resume_with_personality_change() -> Resul
let mut initial_builder = test_codex()
.with_extensions(skills_extensions())
.with_config(|config| {
config.update_plan_enabled = true;
config.model = Some("gpt-5.2".to_string());
});
let initial = initial_builder.build(&server).await?;
@@ -547,6 +550,7 @@ async fn snapshot_model_visible_layout_resume_with_personality_change() -> Resul
let mut resume_builder = test_codex()
.with_extensions(skills_extensions())
.with_config(|config| {
config.update_plan_enabled = true;
config.model = Some("gpt-5.4".to_string());
config
.features
@@ -615,6 +619,7 @@ async fn snapshot_model_visible_layout_resume_override_matches_rollout_model() -
let mut initial_builder = test_codex()
.with_extensions(skills_extensions())
.with_config(|config| {
config.update_plan_enabled = true;
config.model = Some("gpt-5.2".to_string());
});
let initial = initial_builder.build(&server).await?;
@@ -651,6 +656,7 @@ async fn snapshot_model_visible_layout_resume_override_matches_rollout_model() -
let mut resume_builder = test_codex()
.with_extensions(skills_extensions())
.with_config(|config| {
config.update_plan_enabled = true;
config.model = Some("gpt-5.4".to_string());
});
let resumed = resume_builder.restart(&server, &initial).await?;
@@ -316,6 +316,7 @@ fn response_completed_chunks(response_id: &str) -> Vec<StreamingSseChunk> {
async fn build_codex(server: &StreamingSseServer) -> Arc<CodexThread> {
test_codex()
.with_config(|config| config.update_plan_enabled = true)
.with_model("gpt-5.4")
.build_with_streaming_server(server)
.await
+44 -4
View File
@@ -35,6 +35,7 @@ use core_test_support::test_codex::turn_permission_fields;
use core_test_support::wait_for_event;
use pretty_assertions::assert_eq;
use tempfile::TempDir;
use test_case::test_case;
fn write_global_instructions(home: &Path) {
fs::write(home.join("AGENTS.md"), "be consistent and helpful")
@@ -116,11 +117,24 @@ fn normalize_newlines(text: &str) -> String {
text.replace("\r\n", "\n")
}
#[test_case(None, true, false; "default with model instructions")]
#[test_case(Some(true), true, false; "enabled with model instructions")]
#[test_case(Some(false), false, false; "disabled with model instructions")]
#[test_case(None, true, true; "default with custom instructions")]
#[test_case(Some(true), true, true; "enabled with custom instructions")]
#[test_case(Some(false), false, true; "disabled with custom instructions")]
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
async fn prompt_tools_are_consistent_across_requests() -> anyhow::Result<()> {
async fn prompt_tools_are_consistent_across_requests(
update_plan_enabled: Option<bool>,
expected_update_plan_enabled: bool,
custom_instructions: bool,
) -> anyhow::Result<()> {
skip_if_no_network!(Ok(()));
use pretty_assertions::assert_eq;
const CUSTOM_BASE_INSTRUCTIONS: &str =
"Custom base.\r\n## Plan tool\r\nPreserve this custom workflow.\r\n";
let server = start_mock_server().await;
let req1 = mount_sse_once(
&server,
@@ -140,8 +154,16 @@ async fn prompt_tools_are_consistent_across_requests() -> anyhow::Result<()> {
..
} = test_codex()
.with_pre_build_hook(write_global_instructions)
.with_config(|config| {
.with_config(move |config| {
config.model = Some("gpt-5.2".to_string());
if let Some(update_plan_enabled) = update_plan_enabled {
config.update_plan_enabled = update_plan_enabled;
}
if custom_instructions {
config.base_instructions = Some(CUSTOM_BASE_INSTRUCTIONS.to_string());
config.developer_instructions =
Some("## `update_plan`\nNever deploy without explicit approval.\n".to_string());
}
// Keep tool expectations stable when the default web_search mode changes.
config
.web_search_mode
@@ -164,7 +186,11 @@ async fn prompt_tools_are_consistent_across_requests() -> anyhow::Result<()> {
&config.to_models_manager_config(),
)
.await;
let base_instructions = model_info.get_model_instructions(config.personality);
let base_instructions = if custom_instructions {
CUSTOM_BASE_INSTRUCTIONS.to_string()
} else {
model_info.get_model_instructions(config.personality)
};
codex
.start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text {
@@ -183,8 +209,10 @@ async fn prompt_tools_are_consistent_across_requests() -> anyhow::Result<()> {
wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await;
let mut expected_tools_names = vec!["exec_command", "write_stdin"];
if expected_update_plan_enabled {
expected_tools_names.push("update_plan");
}
expected_tools_names.extend([
"update_plan",
"request_user_input",
"apply_patch",
"view_image",
@@ -200,6 +228,18 @@ async fn prompt_tools_are_consistent_across_requests() -> anyhow::Result<()> {
assert_eq!(body1["instructions"], serde_json::json!(base_instructions),);
assert_tool_names(&body1, &expected_tools_names);
for request in [&req1, &req2] {
if let Some(instructions) = &config.developer_instructions {
assert!(
request
.single_request()
.message_input_texts("developer")
.iter()
.any(|text| text == instructions)
);
}
}
Ok(())
}
@@ -1287,6 +1287,7 @@ async fn grandchild_full_fork_preserves_context_baseline(
config.agent_default_subagent_model = Some(V2_DEFAULT_MODEL.to_string());
config.developer_instructions = Some(INSTRUCTIONS.to_string());
if compact_parent {
config.update_plan_enabled = true;
// Use local compaction so the test controls the replacement history.
config.model_provider.name = "test-provider".to_string();
config
@@ -249,6 +249,7 @@ async fn token_budget_guidance_precedes_standalone_context_window() -> Result<()
let guidance_message = "Preserve important state before compaction.";
let test = test_codex()
.with_config(move |config| {
config.update_plan_enabled = true;
config.model_context_window = Some(CONFIGURED_CONTEXT_WINDOW);
config.token_budget = Some(TokenBudgetConfig {
guidance_message: Some(guidance_message.to_string()),
@@ -1468,6 +1469,7 @@ async fn new_context_tool_skips_auto_compact_fallback() -> Result<()> {
let test = test_codex()
.with_extensions(Arc::new(extensions.build()))
.with_config(|config| {
config.update_plan_enabled = true;
config.model_context_window = Some(10_000);
config.token_budget = Some(TokenBudgetConfig {
auto_compact_fallback_prompt: Some(AUTO_COMPACT_FALLBACK_PROMPT.to_string()),
+2 -2
View File
@@ -141,7 +141,7 @@ async fn update_plan_tool_emits_plan_update_event() -> anyhow::Result<()> {
let server = start_mock_server().await;
let mut builder = test_codex();
let mut builder = test_codex().with_config(|config| config.update_plan_enabled = true);
let TestCodex {
codex,
cwd,
@@ -233,7 +233,7 @@ async fn update_plan_tool_rejects_malformed_payload() -> anyhow::Result<()> {
let server = start_mock_server().await;
let mut builder = test_codex();
let mut builder = test_codex().with_config(|config| config.update_plan_enabled = true);
let TestCodex {
codex,
cwd,
@@ -160,6 +160,7 @@ async fn tool_start_receives_conversation_history() -> Result<()> {
extensions.tool_lifecycle_contributor(recorder.clone());
let test = test_codex()
.with_extensions(Arc::new(extensions.build()))
.with_config(|config| config.update_plan_enabled = true)
.build_with_auto_env(&server)
.await?;
@@ -600,6 +601,7 @@ async fn tool_start_receives_rewritten_payload_and_post_hook_history() -> Result
.expect("write pre-tool hook fixture");
})
.with_config(trust_discovered_hooks)
.with_config(|config| config.update_plan_enabled = true)
.build_with_auto_env(&server)
.await?;
@@ -690,6 +692,7 @@ async fn tool_start_is_not_called_when_pre_tool_hook_prevents_execution() -> Res
.expect("write pre-tool hook fixture");
})
.with_config(trust_discovered_hooks)
.with_config(|config| config.update_plan_enabled = true)
.build_with_auto_env(&server)
.await?;
+2
View File
@@ -80,6 +80,7 @@ async fn strict_tool_collisions_fail_the_turn_before_sampling(
let server = start_mock_server().await;
let mut builder = test_codex().with_config(move |config| {
config.tool_registry.error_on_tool_collisions = true;
config.update_plan_enabled = true;
if pre_compact {
config.model_auto_compact_token_limit = Some(0);
}
@@ -231,6 +232,7 @@ async fn empty_turn_environments_omits_environment_backed_tools() -> Result<()>
.await;
let mut builder = test_codex().with_config(|config| {
config.update_plan_enabled = true;
config
.features
.enable(Feature::UnifiedExec)