11use anyhow:: Result ;
22use app_test_support:: TestAppServer ;
33use app_test_support:: to_response;
4+ use codex_app_server_protocol:: CurrentTimeReadResponse ;
45use codex_app_server_protocol:: ItemCompletedNotification ;
56use codex_app_server_protocol:: ItemStartedNotification ;
6- use codex_app_server_protocol:: JSONRPCMessage ;
77use codex_app_server_protocol:: JSONRPCResponse ;
88use codex_app_server_protocol:: RequestId ;
9+ use codex_app_server_protocol:: ServerRequest ;
910use codex_app_server_protocol:: ThreadItem ;
1011use codex_app_server_protocol:: ThreadStartParams ;
1112use codex_app_server_protocol:: ThreadStartResponse ;
@@ -19,12 +20,16 @@ use std::time::Duration;
1920use tempfile:: TempDir ;
2021use tokio:: time:: timeout;
2122
23+ #[ cfg( windows) ]
24+ const DEFAULT_READ_TIMEOUT : Duration = Duration :: from_secs ( 25 ) ;
25+ #[ cfg( not( windows) ) ]
2226const DEFAULT_READ_TIMEOUT : Duration = Duration :: from_secs ( 10 ) ;
27+ const CURRENT_TIME_AT : i64 = 1_781_717_655 ;
2328
2429#[ tokio:: test( flavor = "multi_thread" , worker_threads = 2 ) ]
25- async fn sleep_emits_started_and_completed_items ( ) -> Result < ( ) > {
30+ async fn external_sleep_polls_current_time_and_emits_items ( ) -> Result < ( ) > {
2631 const CALL_ID : & str = "sleep-1" ;
27- const DURATION_MS : u64 = 1 ;
32+ const DURATION_MS : u64 = 2_000 ;
2833
2934 let server = responses:: start_mock_server ( ) . await ;
3035 responses:: mount_sse_sequence (
@@ -85,38 +90,19 @@ async fn sleep_emits_started_and_completed_items() -> Result<()> {
8590 . await ??;
8691 let TurnStartResponse { turn, .. } = to_response ( turn_start_response) ?;
8792
88- let ( started, completed) = timeout ( DEFAULT_READ_TIMEOUT , async {
89- let mut started = None ;
90- let mut completed = None ;
91- while started. is_none ( ) || completed. is_none ( ) {
92- let JSONRPCMessage :: Notification ( notification) = mcp. read_next_message ( ) . await ? else {
93- continue ;
94- } ;
95- match notification. method . as_str ( ) {
96- "item/started" => {
97- let payload: ItemStartedNotification =
98- serde_json:: from_value ( notification. params . expect ( "item/started params" ) ) ?;
99- if matches ! ( & payload. item, ThreadItem :: Sleep { .. } ) {
100- started = Some ( payload) ;
101- }
102- }
103- "item/completed" => {
104- let payload: ItemCompletedNotification = serde_json:: from_value (
105- notification. params . expect ( "item/completed params" ) ,
106- ) ?;
107- if matches ! ( & payload. item, ThreadItem :: Sleep { .. } ) {
108- completed = Some ( payload) ;
109- }
110- }
111- _ => { }
112- }
113- }
114- Ok :: < _ , anyhow:: Error > ( (
115- started. expect ( "sleep started" ) ,
116- completed. expect ( "sleep completed" ) ,
117- ) )
118- } )
119- . await ??;
93+ // Read once for the initial reminder, then once to establish the sleep deadline.
94+ respond_to_current_time_read ( & mut mcp, & thread. id , CURRENT_TIME_AT ) . await ?;
95+ let started = wait_for_sleep_started ( & mut mcp, CALL_ID ) . await ?;
96+ respond_to_current_time_read ( & mut mcp, & thread. id , CURRENT_TIME_AT ) . await ?;
97+
98+ // The first poll remains below the deadline, so the provider must request time again.
99+ respond_to_current_time_read ( & mut mcp, & thread. id , CURRENT_TIME_AT + 1 ) . await ?;
100+ respond_to_current_time_read ( & mut mcp, & thread. id , CURRENT_TIME_AT + 2 ) . await ?;
101+
102+ let completed = wait_for_sleep_completed ( & mut mcp, CALL_ID ) . await ?;
103+
104+ // The next inference boundary reads the same external clock after the sleep completes.
105+ respond_to_current_time_read ( & mut mcp, & thread. id , CURRENT_TIME_AT + 2 ) . await ?;
120106 timeout (
121107 DEFAULT_READ_TIMEOUT ,
122108 mcp. read_stream_until_notification_message ( "turn/completed" ) ,
@@ -150,6 +136,64 @@ async fn sleep_emits_started_and_completed_items() -> Result<()> {
150136 Ok ( ( ) )
151137}
152138
139+ async fn wait_for_sleep_started (
140+ mcp : & mut TestAppServer ,
141+ call_id : & str ,
142+ ) -> Result < ItemStartedNotification > {
143+ loop {
144+ let notification = timeout (
145+ DEFAULT_READ_TIMEOUT ,
146+ mcp. read_stream_until_notification_message ( "item/started" ) ,
147+ )
148+ . await ??;
149+ let started: ItemStartedNotification =
150+ serde_json:: from_value ( notification. params . expect ( "item/started params" ) ) ?;
151+ if matches ! ( & started. item, ThreadItem :: Sleep { id, .. } if id == call_id) {
152+ return Ok ( started) ;
153+ }
154+ }
155+ }
156+
157+ async fn wait_for_sleep_completed (
158+ mcp : & mut TestAppServer ,
159+ call_id : & str ,
160+ ) -> Result < ItemCompletedNotification > {
161+ loop {
162+ let notification = timeout (
163+ DEFAULT_READ_TIMEOUT ,
164+ mcp. read_stream_until_notification_message ( "item/completed" ) ,
165+ )
166+ . await ??;
167+ let completed: ItemCompletedNotification =
168+ serde_json:: from_value ( notification. params . expect ( "item/completed params" ) ) ?;
169+ if matches ! ( & completed. item, ThreadItem :: Sleep { id, .. } if id == call_id) {
170+ return Ok ( completed) ;
171+ }
172+ }
173+ }
174+
175+ async fn respond_to_current_time_read (
176+ mcp : & mut TestAppServer ,
177+ thread_id : & str ,
178+ current_time_at : i64 ,
179+ ) -> Result < ( ) > {
180+ let request = timeout (
181+ DEFAULT_READ_TIMEOUT ,
182+ mcp. read_stream_until_request_message ( ) ,
183+ )
184+ . await ??;
185+ let ServerRequest :: CurrentTimeRead { request_id, params } = request else {
186+ panic ! ( "expected CurrentTimeRead request, got: {request:?}" ) ;
187+ } ;
188+ assert_eq ! ( params. thread_id, thread_id) ;
189+ mcp. send_response (
190+ request_id,
191+ serde_json:: to_value ( CurrentTimeReadResponse { current_time_at } ) ?,
192+ )
193+ . await ?;
194+ Ok ( ( ) )
195+ }
196+
153197fn create_config_toml ( codex_home : & Path , server_uri : & str ) -> std:: io:: Result < ( ) > {
154198 std:: fs:: write (
155199 codex_home. join ( "config.toml" ) ,
@@ -170,6 +214,7 @@ stream_max_retries = 0
170214[features.current_time_reminder]
171215enabled = true
172216sleep_tool = true
217+ clock_source = "external"
173218"#
174219 ) ,
175220 )
0 commit comments