| use codex_code_mode_protocol::NoopCodeModeSessionDelegate; |
| use std::sync::Arc; |
| use std::sync::atomic::AtomicBool; |
| use std::sync::atomic::Ordering; |
| use std::time::Duration; |
|
|
| use super::CellId; |
| use super::CodeModeNestedToolCall; |
| use super::CodeModeSessionDelegate; |
| use super::InProcessCodeModeSession; |
| use super::RuntimeResponse; |
| use super::WaitOutcome; |
| use super::WaitRequest; |
| use super::WaitToPendingOutcome; |
| use super::WaitToPendingRequest; |
| use crate::CodeModeToolKind; |
| use crate::ExecuteRequest; |
| use crate::ExecuteToPendingOutcome; |
| use crate::FunctionCallOutputContentItem; |
| use crate::ToolDefinition; |
| use codex_code_mode_protocol::CodeModeSessionCellExecutionLimits; |
| use codex_code_mode_protocol::NotificationFuture; |
| use codex_code_mode_protocol::ToolInvocationFuture; |
| use codex_protocol::ToolName; |
| use pretty_assertions::assert_eq; |
| use serde_json::Value as JsonValue; |
| use tokio::sync::Notify; |
| use tokio_util::sync::CancellationToken; |
|
|
| #[test] |
| fn resolve_yield_timeout_applies_grace_before_session_limits() { |
| for (max_yield_time_ms, requested_yield_time_ms, expected_timeout) in [ |
| (None, 0, Duration::ZERO), |
| (None, 9_999, Duration::from_millis(9_999)), |
| (None, 10_000, Duration::from_secs(11)), |
| (None, 10_001, Duration::from_millis(11_001)), |
| (Some(0), 0, Duration::ZERO), |
| (Some(0), 10_000, Duration::ZERO), |
| (Some(5_000), 9_999, Duration::from_secs(5)), |
| (Some(10_000), 10_000, Duration::from_secs(10)), |
| (Some(10_500), 10_000, Duration::from_millis(10_500)), |
| (Some(11_000), 10_000, Duration::from_secs(11)), |
| (Some(12_000), 10_000, Duration::from_secs(11)), |
| (Some(10_500), 5_000, Duration::from_secs(5)), |
| (Some(u64::MAX), u64::MAX, Duration::from_millis(u64::MAX)), |
| ] { |
| let session = InProcessCodeModeSession::with_limits(CodeModeSessionCellExecutionLimits { |
| max_yield_time_ms, |
| max_heap_size_bytes: None, |
| }); |
|
|
| assert_eq!( |
| session.resolve_yield_timeout(requested_yield_time_ms), |
| expected_timeout, |
| "requested {requested_yield_time_ms} ms with limit {max_yield_time_ms:?}" |
| ); |
| } |
| } |
|
|
| #[tokio::test(start_paused = true)] |
| async fn execute_waits_for_nested_tool_during_yield_grace() { |
| let delegate = Arc::new(ReleasableToolDelegate::default()); |
| let service = InProcessCodeModeSession::new(); |
| let request = ExecuteRequest { |
| enabled_tools: vec![echo_tool()], |
| source: r#"await tools.echo({}); text("done");"#.to_string(), |
| yield_time_ms: Some(10_000), |
| ..execute_request("") |
| }; |
| let started = service.execute(request, delegate.clone()).await.unwrap(); |
| let response = tokio::spawn(started.initial_response()); |
| wait_until_tool_started(&delegate).await; |
| tokio::time::advance(Duration::from_millis(10_500)).await; |
| delegate.release_tool(); |
| wait_until_finished(&response).await; |
| let response = response.await.unwrap().unwrap(); |
|
|
| assert_eq!( |
| response, |
| RuntimeResponse::Result { |
| code_mode_host_duration: None, |
| cell_id: cell_id("1"), |
| content_items: vec![FunctionCallOutputContentItem::InputText { |
| text: "done".to_string(), |
| }], |
| error_text: None, |
| } |
| ); |
| } |
|
|
| #[tokio::test(start_paused = true)] |
| async fn execute_and_wait_clamp_yield_grace_without_stopping_the_cell() { |
| let delegate = Arc::new(ReleasableToolDelegate::default()); |
| let service = InProcessCodeModeSession::with_limits(CodeModeSessionCellExecutionLimits { |
| max_yield_time_ms: Some( 10_000), |
| max_heap_size_bytes: None, |
| }); |
| let started = service |
| .execute( |
| ExecuteRequest { |
| enabled_tools: vec![echo_tool()], |
| source: r#"await tools.echo({}); text("done");"#.to_string(), |
| yield_time_ms: None, |
| ..execute_request("") |
| }, |
| delegate.clone(), |
| ) |
| .await |
| .unwrap(); |
| let initial_response = tokio::spawn(started.initial_response()); |
| wait_until_tool_started(&delegate).await; |
|
|
| tokio::time::advance(Duration::from_millis(9_999)).await; |
| assert!(!initial_response.is_finished()); |
| tokio::time::advance(Duration::from_millis( 1)).await; |
| wait_until_finished(&initial_response).await; |
| assert_eq!( |
| initial_response.await.unwrap().unwrap(), |
| RuntimeResponse::Yielded { |
| code_mode_host_duration: None, |
| cell_id: cell_id("1"), |
| content_items: Vec::new(), |
| } |
| ); |
|
|
| let wait_response = service |
| .begin_wait(WaitRequest { |
| cell_id: cell_id("1"), |
| yield_time_ms: 10_000, |
| }) |
| .await; |
| let wait_response = tokio::spawn(wait_response); |
| tokio::task::yield_now().await; |
| tokio::time::advance(Duration::from_secs( 10)).await; |
| wait_until_finished(&wait_response).await; |
| assert_eq!( |
| wait_response.await.unwrap().unwrap(), |
| WaitOutcome::LiveCell(RuntimeResponse::Yielded { |
| code_mode_host_duration: None, |
| cell_id: cell_id("1"), |
| content_items: Vec::new(), |
| }) |
| ); |
|
|
| delegate.release_tool(); |
| let completion = service |
| .begin_wait(WaitRequest { |
| cell_id: cell_id("1"), |
| yield_time_ms: 10_000, |
| }) |
| .await; |
| let completion = tokio::spawn(completion); |
| wait_until_finished(&completion).await; |
| assert_eq!( |
| completion.await.unwrap().unwrap(), |
| WaitOutcome::LiveCell(RuntimeResponse::Result { |
| code_mode_host_duration: None, |
| cell_id: cell_id("1"), |
| content_items: vec![FunctionCallOutputContentItem::InputText { |
| text: "done".to_string(), |
| }], |
| error_text: None, |
| }) |
| ); |
| } |
|
|
| #[tokio::test(start_paused = true)] |
| async fn wait_waits_for_nested_tool_during_yield_grace() { |
| let delegate = Arc::new(ReleasableToolDelegate::default()); |
| let service = InProcessCodeModeSession::new(); |
| let initial_response = service |
| .execute_to_pending( |
| ExecuteRequest { |
| enabled_tools: vec![echo_tool()], |
| source: r#"await tools.echo({}); text("done");"#.to_string(), |
| ..execute_request("") |
| }, |
| delegate.clone(), |
| ) |
| .await |
| .unwrap(); |
| assert_eq!( |
| initial_response, |
| ExecuteToPendingOutcome::Pending { |
| cell_id: cell_id("1"), |
| content_items: Vec::new(), |
| pending_tool_call_ids: vec!["tool-1".to_string()], |
| } |
| ); |
| let response = service |
| .begin_wait(WaitRequest { |
| cell_id: cell_id("1"), |
| yield_time_ms: 10_000, |
| }) |
| .await; |
| let response = tokio::spawn(response); |
| tokio::task::yield_now().await; |
| tokio::time::advance(Duration::from_millis(10_500)).await; |
| delegate.release_tool(); |
| wait_until_finished(&response).await; |
| let response = response.await.unwrap(); |
|
|
| assert_eq!( |
| response.unwrap(), |
| WaitOutcome::LiveCell(RuntimeResponse::Result { |
| code_mode_host_duration: None, |
| cell_id: cell_id("1"), |
| content_items: vec![FunctionCallOutputContentItem::InputText { |
| text: "done".to_string(), |
| }], |
| error_text: None, |
| }) |
| ); |
| } |
|
|
| #[tokio::test(start_paused = true)] |
| async fn zero_yield_limit_is_immediate_and_scoped_to_its_session() { |
| let zero_delegate = Arc::new(ReleasableToolDelegate::default()); |
| let zero_session = InProcessCodeModeSession::with_limits(CodeModeSessionCellExecutionLimits { |
| max_yield_time_ms: Some( 0), |
| max_heap_size_bytes: None, |
| }); |
| let limited_delegate = Arc::new(ReleasableToolDelegate::default()); |
| let limited_session = |
| InProcessCodeModeSession::with_limits(CodeModeSessionCellExecutionLimits { |
| max_yield_time_ms: Some( 10), |
| max_heap_size_bytes: None, |
| }); |
| let request = ExecuteRequest { |
| enabled_tools: vec![echo_tool()], |
| source: "await tools.echo({});".to_string(), |
| yield_time_ms: Some( 60_000), |
| ..execute_request("") |
| }; |
| let zero_started = zero_session |
| .execute(request.clone(), zero_delegate.clone()) |
| .await |
| .unwrap(); |
| let limited_started = limited_session |
| .execute(request, limited_delegate.clone()) |
| .await |
| .unwrap(); |
| let zero_response = tokio::spawn(zero_started.initial_response()); |
| let limited_response = tokio::spawn(limited_started.initial_response()); |
| wait_until_tool_started(&zero_delegate).await; |
| wait_until_tool_started(&limited_delegate).await; |
| wait_until_finished(&zero_response).await; |
| assert!(!limited_response.is_finished()); |
| assert_eq!( |
| zero_response.await.unwrap().unwrap(), |
| RuntimeResponse::Yielded { |
| code_mode_host_duration: None, |
| cell_id: cell_id("1"), |
| content_items: Vec::new(), |
| } |
| ); |
|
|
| let zero_wait = zero_session |
| .begin_wait(WaitRequest { |
| cell_id: cell_id("1"), |
| yield_time_ms: 60_000, |
| }) |
| .await; |
| let zero_wait = tokio::spawn(zero_wait); |
| wait_until_finished(&zero_wait).await; |
| assert_eq!( |
| zero_wait.await.unwrap().unwrap(), |
| WaitOutcome::LiveCell(RuntimeResponse::Yielded { |
| code_mode_host_duration: None, |
| cell_id: cell_id("1"), |
| content_items: Vec::new(), |
| }) |
| ); |
|
|
| tokio::time::advance(Duration::from_millis( 10)).await; |
| wait_until_finished(&limited_response).await; |
| assert_eq!( |
| limited_response.await.unwrap().unwrap(), |
| RuntimeResponse::Yielded { |
| code_mode_host_duration: None, |
| cell_id: cell_id("1"), |
| content_items: Vec::new(), |
| } |
| ); |
|
|
| zero_session.shutdown().await.unwrap(); |
| limited_session.shutdown().await.unwrap(); |
| } |
|
|
| async fn wait_until_finished<T>(task: &tokio::task::JoinHandle<T>) { |
| for _ in 0..10_000 { |
| if task.is_finished() { |
| return; |
| } |
| tokio::task::yield_now().await; |
| } |
| panic!("code-mode response did not finish while virtual time was held in the grace period"); |
| } |
|
|
| async fn wait_until_tool_started(delegate: &ReleasableToolDelegate) { |
| for _ in 0..10_000 { |
| if delegate.tool_started.load(Ordering::Acquire) { |
| return; |
| } |
| tokio::task::yield_now().await; |
| } |
| panic!("nested code-mode tool did not start"); |
| } |
|
|
| #[derive(Default)] |
| struct ReleasableToolDelegate { |
| tool_release: Notify, |
| tool_started: AtomicBool, |
| } |
|
|
| impl ReleasableToolDelegate { |
| fn release_tool(&self) { |
| self.tool_release.notify_one(); |
| } |
| } |
|
|
| impl CodeModeSessionDelegate for ReleasableToolDelegate { |
| fn invoke_tool<'a>( |
| &'a self, |
| _invocation: CodeModeNestedToolCall, |
| cancellation_token: CancellationToken, |
| ) -> ToolInvocationFuture<'a> { |
| self.tool_started.store(true, Ordering::Release); |
| Box::pin(async move { |
| tokio::select! { |
| _ = self.tool_release.notified() => Ok(JsonValue::Null), |
| _ = cancellation_token.cancelled() => Err("cancelled".to_string()), |
| } |
| }) |
| } |
|
|
| fn notify<'a>( |
| &'a self, |
| _call_id: String, |
| _cell_id: CellId, |
| _text: String, |
| _cancellation_token: CancellationToken, |
| ) -> NotificationFuture<'a> { |
| Box::pin(async { Ok(()) }) |
| } |
|
|
| fn cell_closed(&self, _cell_id: &CellId) {} |
| } |
|
|
| fn execute_request(source: &str) -> ExecuteRequest { |
| ExecuteRequest { |
| tool_call_id: "call_1".to_string(), |
| enabled_tools: Vec::new(), |
| source: source.to_string(), |
| yield_time_ms: Some(1), |
| max_output_tokens: None, |
| } |
| } |
|
|
| fn cell_id(value: &str) -> CellId { |
| CellId::new(value.to_string()) |
| } |
|
|
| fn echo_tool() -> ToolDefinition { |
| ToolDefinition { |
| name: "echo".to_string(), |
| tool_name: ToolName::plain("echo"), |
| description: String::new(), |
| kind: CodeModeToolKind::Function, |
| input_schema: None, |
| output_schema: None, |
| } |
| } |
|
|
| async fn execute(service: &InProcessCodeModeSession, request: ExecuteRequest) -> RuntimeResponse { |
| service |
| .execute(request, Arc::new(NoopCodeModeSessionDelegate)) |
| .await |
| .unwrap() |
| .initial_response() |
| .await |
| .unwrap() |
| } |
|
|
| #[tokio::test] |
| async fn synchronous_exit_returns_successfully() { |
| let service = InProcessCodeModeSession::new(); |
|
|
| let response = execute( |
| &service, |
| ExecuteRequest { |
| source: r#"text("before"); exit(); text("after");"#.to_string(), |
| yield_time_ms: None, |
| ..execute_request("") |
| }, |
| ) |
| .await; |
|
|
| assert_eq!( |
| response, |
| RuntimeResponse::Result { |
| code_mode_host_duration: None, |
| cell_id: cell_id("1"), |
| content_items: vec![FunctionCallOutputContentItem::InputText { |
| text: "before".to_string(), |
| }], |
| error_text: None, |
| } |
| ); |
| } |
|
|
| #[tokio::test] |
| async fn stored_values_are_shared_between_cells_but_not_sessions() { |
| let first_session = InProcessCodeModeSession::new(); |
| let second_session = InProcessCodeModeSession::new(); |
|
|
| let write_response = execute( |
| &first_session, |
| ExecuteRequest { |
| source: r#"store("key", "visible");"#.to_string(), |
| yield_time_ms: None, |
| ..execute_request("") |
| }, |
| ) |
| .await; |
|
|
| let same_session = execute( |
| &first_session, |
| ExecuteRequest { |
| source: r#"text(String(load("key")));"#.to_string(), |
| yield_time_ms: None, |
| ..execute_request("") |
| }, |
| ) |
| .await; |
| let other_session = execute( |
| &second_session, |
| ExecuteRequest { |
| source: r#"text(String(load("key")));"#.to_string(), |
| yield_time_ms: None, |
| ..execute_request("") |
| }, |
| ) |
| .await; |
|
|
| assert_eq!( |
| write_response, |
| RuntimeResponse::Result { |
| code_mode_host_duration: None, |
| cell_id: cell_id("1"), |
| content_items: Vec::new(), |
| error_text: None, |
| } |
| ); |
| assert_eq!( |
| same_session, |
| RuntimeResponse::Result { |
| code_mode_host_duration: None, |
| cell_id: cell_id("2"), |
| content_items: vec![FunctionCallOutputContentItem::InputText { |
| text: "visible".to_string(), |
| }], |
| error_text: None, |
| } |
| ); |
| assert_eq!( |
| other_session, |
| RuntimeResponse::Result { |
| code_mode_host_duration: None, |
| cell_id: cell_id("1"), |
| content_items: vec![FunctionCallOutputContentItem::InputText { |
| text: "undefined".to_string(), |
| }], |
| error_text: None, |
| } |
| ); |
| } |
|
|
| #[tokio::test] |
| async fn storing_undefined_preserves_the_previous_value() { |
| let service = InProcessCodeModeSession::new(); |
|
|
| let response = execute( |
| &service, |
| ExecuteRequest { |
| source: r#" |
| store("key", null); |
| try { |
| store("key", undefined); |
| } catch (error) { |
| text(String(error)); |
| } |
| text(load("key")); |
| "# |
| .to_string(), |
| yield_time_ms: None, |
| ..execute_request("") |
| }, |
| ) |
| .await; |
|
|
| assert_eq!( |
| response, |
| RuntimeResponse::Result { |
| code_mode_host_duration: None, |
| cell_id: cell_id("1"), |
| content_items: vec![ |
| FunctionCallOutputContentItem::InputText { |
| text: "Unable to store \"key\". Only plain serializable objects can be stored." |
| .to_string(), |
| }, |
| FunctionCallOutputContentItem::InputText { |
| text: "null".to_string(), |
| }, |
| ], |
| error_text: None, |
| } |
| ); |
| } |
|
|
| #[tokio::test] |
| async fn shutdown_interrupts_cpu_bound_cells() { |
| let service = InProcessCodeModeSession::new(); |
|
|
| let cell = service |
| .execute( |
| ExecuteRequest { |
| source: "while (true) {}".to_string(), |
| ..execute_request("") |
| }, |
| Arc::new(NoopCodeModeSessionDelegate), |
| ) |
| .await |
| .unwrap(); |
| assert_eq!( |
| cell.initial_response().await.unwrap(), |
| RuntimeResponse::Yielded { |
| code_mode_host_duration: None, |
| cell_id: cell_id("1"), |
| content_items: Vec::new(), |
| } |
| ); |
|
|
| tokio::time::timeout(Duration::from_secs(1), service.shutdown()) |
| .await |
| .unwrap() |
| .unwrap(); |
| } |
|
|
| #[tokio::test] |
| async fn start_cell_rejects_new_cell_after_shutdown_begins() { |
| let service = InProcessCodeModeSession::new(); |
| service.shutdown().await.unwrap(); |
|
|
| let error = service |
| .execute( |
| execute_request("text('late');"), |
| Arc::new(NoopCodeModeSessionDelegate), |
| ) |
| .await |
| .err() |
| .unwrap(); |
|
|
| assert_eq!(error, "code mode session is shutting down".to_string()); |
| } |
|
|
| #[tokio::test] |
| async fn execute_to_pending_returns_completed_for_synchronous_results() { |
| let service = InProcessCodeModeSession::new(); |
|
|
| let response = service |
| .execute_to_pending( |
| ExecuteRequest { |
| source: r#"text("done");"#.to_string(), |
| yield_time_ms: Some(60_000), |
| ..execute_request("") |
| }, |
| Arc::new(NoopCodeModeSessionDelegate), |
| ) |
| .await |
| .unwrap(); |
|
|
| assert_eq!( |
| response, |
| ExecuteToPendingOutcome::Completed(RuntimeResponse::Result { |
| code_mode_host_duration: None, |
| cell_id: cell_id("1"), |
| content_items: vec![FunctionCallOutputContentItem::InputText { |
| text: "done".to_string(), |
| }], |
| error_text: None, |
| }) |
| ); |
| } |
|
|
| #[tokio::test] |
| async fn execute_to_pending_returns_once_the_runtime_is_quiescent() { |
| let service = InProcessCodeModeSession::new(); |
|
|
| let response = tokio::time::timeout( |
| Duration::from_secs(1), |
| service.execute_to_pending( |
| ExecuteRequest { |
| source: r#"text("before"); await new Promise(() => {});"#.to_string(), |
| yield_time_ms: Some(60_000), |
| ..execute_request("") |
| }, |
| Arc::new(NoopCodeModeSessionDelegate), |
| ), |
| ) |
| .await |
| .unwrap() |
| .unwrap(); |
|
|
| assert_eq!( |
| response, |
| ExecuteToPendingOutcome::Pending { |
| cell_id: cell_id("1"), |
| content_items: vec![FunctionCallOutputContentItem::InputText { |
| text: "before".to_string(), |
| }], |
| pending_tool_call_ids: Vec::new(), |
| } |
| ); |
|
|
| let termination = service.terminate(cell_id("1")).await.unwrap(); |
|
|
| assert_eq!( |
| termination, |
| WaitOutcome::LiveCell(RuntimeResponse::Terminated { |
| code_mode_host_duration: None, |
| cell_id: cell_id("1"), |
| content_items: Vec::new(), |
| }) |
| ); |
| } |
|
|
| #[tokio::test] |
| async fn execute_to_pending_identifies_tool_calls_in_paused_frontier() { |
| let service = InProcessCodeModeSession::new(); |
|
|
| let response = service |
| .execute_to_pending( |
| ExecuteRequest { |
| enabled_tools: vec![echo_tool()], |
| source: r#" |
| await Promise.all([ |
| tools.echo({ value: "first" }), |
| tools.echo({ value: "second" }), |
| ]); |
| "# |
| .to_string(), |
| yield_time_ms: Some(60_000), |
| ..execute_request("") |
| }, |
| Arc::new(NoopCodeModeSessionDelegate), |
| ) |
| .await |
| .unwrap(); |
|
|
| assert_eq!( |
| response, |
| ExecuteToPendingOutcome::Pending { |
| cell_id: cell_id("1"), |
| content_items: Vec::new(), |
| pending_tool_call_ids: vec!["tool-1".to_string(), "tool-2".to_string()], |
| } |
| ); |
|
|
| let termination = service.terminate(cell_id("1")).await.unwrap(); |
|
|
| assert_eq!( |
| termination, |
| WaitOutcome::LiveCell(RuntimeResponse::Terminated { |
| code_mode_host_duration: None, |
| cell_id: cell_id("1"), |
| content_items: Vec::new(), |
| }) |
| ); |
| } |
|
|
| #[tokio::test] |
| async fn execute_to_pending_excludes_delayed_timeout_tool_calls_until_wait() { |
| let service = InProcessCodeModeSession::new(); |
|
|
| let initial_response = service |
| .execute_to_pending( |
| ExecuteRequest { |
| enabled_tools: vec![echo_tool()], |
| source: r#" |
| setTimeout(() => { |
| tools.echo({ value: "delayed" }); |
| }, 1000); |
| await Promise.all([ |
| tools.echo({ value: "second" }), |
| tools.echo({ value: "third" }), |
| ]); |
| "# |
| .to_string(), |
| yield_time_ms: Some(60_000), |
| ..execute_request("") |
| }, |
| Arc::new(NoopCodeModeSessionDelegate), |
| ) |
| .await |
| .unwrap(); |
|
|
| assert_eq!( |
| initial_response, |
| ExecuteToPendingOutcome::Pending { |
| cell_id: cell_id("1"), |
| content_items: Vec::new(), |
| pending_tool_call_ids: vec!["tool-1".to_string(), "tool-2".to_string()], |
| } |
| ); |
|
|
| tokio::time::sleep(Duration::from_secs(2)).await; |
|
|
| let resumed_response = tokio::time::timeout( |
| Duration::from_secs(1), |
| service.wait_to_pending(WaitToPendingRequest { |
| cell_id: cell_id("1"), |
| }), |
| ) |
| .await |
| .unwrap() |
| .unwrap(); |
|
|
| assert_eq!( |
| resumed_response, |
| WaitToPendingOutcome::LiveCell(ExecuteToPendingOutcome::Pending { |
| cell_id: cell_id("1"), |
| content_items: Vec::new(), |
| pending_tool_call_ids: vec!["tool-3".to_string()], |
| }) |
| ); |
|
|
| let termination = service.terminate(cell_id("1")).await.unwrap(); |
|
|
| assert_eq!( |
| termination, |
| WaitOutcome::LiveCell(RuntimeResponse::Terminated { |
| code_mode_host_duration: None, |
| cell_id: cell_id("1"), |
| content_items: Vec::new(), |
| }) |
| ); |
| } |
|
|
| #[tokio::test] |
| async fn wait_to_pending_returns_after_resumed_runtime_becomes_quiescent_again() { |
| let delegate = Arc::new(ReleasableToolDelegate::default()); |
| let service = InProcessCodeModeSession::new(); |
|
|
| let initial_response = service |
| .execute_to_pending( |
| ExecuteRequest { |
| enabled_tools: vec![echo_tool()], |
| source: r#" |
| await tools.echo({}); |
| text("after"); |
| await new Promise(() => {}); |
| "# |
| .to_string(), |
| yield_time_ms: Some(60_000), |
| ..execute_request("") |
| }, |
| delegate.clone(), |
| ) |
| .await |
| .unwrap(); |
|
|
| assert_eq!( |
| initial_response, |
| ExecuteToPendingOutcome::Pending { |
| cell_id: cell_id("1"), |
| content_items: Vec::new(), |
| pending_tool_call_ids: vec!["tool-1".to_string()], |
| } |
| ); |
|
|
| delegate.release_tool(); |
|
|
| let resumed_response = tokio::time::timeout( |
| Duration::from_secs(1), |
| service.wait_to_pending(WaitToPendingRequest { |
| cell_id: cell_id("1"), |
| }), |
| ) |
| .await |
| .unwrap() |
| .unwrap(); |
|
|
| assert_eq!( |
| resumed_response, |
| WaitToPendingOutcome::LiveCell(ExecuteToPendingOutcome::Pending { |
| cell_id: cell_id("1"), |
| content_items: vec![FunctionCallOutputContentItem::InputText { |
| text: "after".to_string(), |
| }], |
| pending_tool_call_ids: Vec::new(), |
| }) |
| ); |
|
|
| let termination = service.terminate(cell_id("1")).await.unwrap(); |
|
|
| assert_eq!( |
| termination, |
| WaitOutcome::LiveCell(RuntimeResponse::Terminated { |
| code_mode_host_duration: None, |
| cell_id: cell_id("1"), |
| content_items: Vec::new(), |
| }) |
| ); |
| } |
|
|
| #[tokio::test] |
| async fn wait_to_pending_returns_completed_after_resumed_runtime_finishes() { |
| let delegate = Arc::new(ReleasableToolDelegate::default()); |
| let service = InProcessCodeModeSession::new(); |
|
|
| let initial_response = service |
| .execute_to_pending( |
| ExecuteRequest { |
| enabled_tools: vec![echo_tool()], |
| source: r#" |
| await tools.echo({}); |
| text("done"); |
| "# |
| .to_string(), |
| yield_time_ms: Some(60_000), |
| ..execute_request("") |
| }, |
| delegate.clone(), |
| ) |
| .await |
| .unwrap(); |
|
|
| assert_eq!( |
| initial_response, |
| ExecuteToPendingOutcome::Pending { |
| cell_id: cell_id("1"), |
| content_items: Vec::new(), |
| pending_tool_call_ids: vec!["tool-1".to_string()], |
| } |
| ); |
|
|
| delegate.release_tool(); |
|
|
| let resumed_response = tokio::time::timeout( |
| Duration::from_secs(1), |
| service.wait_to_pending(WaitToPendingRequest { |
| cell_id: cell_id("1"), |
| }), |
| ) |
| .await |
| .unwrap() |
| .unwrap(); |
|
|
| assert_eq!( |
| resumed_response, |
| WaitToPendingOutcome::LiveCell(ExecuteToPendingOutcome::Completed( |
| RuntimeResponse::Result { |
| code_mode_host_duration: None, |
| cell_id: cell_id("1"), |
| content_items: vec![FunctionCallOutputContentItem::InputText { |
| text: "done".to_string(), |
| }], |
| error_text: None, |
| } |
| )) |
| ); |
| } |
|
|
| #[tokio::test] |
| async fn global_scope_contains_only_allowed_items() { |
| let service = InProcessCodeModeSession::new(); |
|
|
| let response = execute( |
| &service, |
| ExecuteRequest { |
| enabled_tools: vec![echo_tool()], |
| source: "text(JSON.stringify(Object.getOwnPropertyNames(globalThis).sort()));" |
| .to_string(), |
| yield_time_ms: None, |
| ..execute_request("") |
| }, |
| ) |
| .await; |
|
|
| let RuntimeResponse::Result { |
| content_items, |
| error_text: None, |
| .. |
| } = response |
| else { |
| panic!("global scope inspection failed unexpectedly: {response:?}"); |
| }; |
| let [FunctionCallOutputContentItem::InputText { text }] = content_items.as_slice() else { |
| panic!("global scope inspection returned unexpected output: {content_items:?}"); |
| }; |
| let globals = serde_json::from_str::<Vec<String>>(text) |
| .expect("global scope inspection should return a JSON array"); |
| let expected = [ |
| "AggregateError", |
| "ALL_TOOLS", |
| "Array", |
| "ArrayBuffer", |
| "AsyncDisposableStack", |
| "BigInt", |
| "BigInt64Array", |
| "BigUint64Array", |
| "Boolean", |
| "clearTimeout", |
| "DataView", |
| "Date", |
| "DisposableStack", |
| "Error", |
| "EvalError", |
| "FinalizationRegistry", |
| "Float16Array", |
| "Float32Array", |
| "Float64Array", |
| "Function", |
| "Infinity", |
| "Int16Array", |
| "Int32Array", |
| "Int8Array", |
| "Intl", |
| "Iterator", |
| "JSON", |
| "Map", |
| "Math", |
| "NaN", |
| "Number", |
| "Object", |
| "Promise", |
| "Proxy", |
| "RangeError", |
| "ReferenceError", |
| "Reflect", |
| "RegExp", |
| "Set", |
| "String", |
| "SuppressedError", |
| "Symbol", |
| "SyntaxError", |
| "Temporal", |
| "TypeError", |
| "URIError", |
| "Uint16Array", |
| "Uint32Array", |
| "Uint8Array", |
| "Uint8ClampedArray", |
| "WeakMap", |
| "WeakRef", |
| "WeakSet", |
| "__codexContentItems", |
| "add_content", |
| "audio", |
| "decodeURI", |
| "decodeURIComponent", |
| "encodeURI", |
| "encodeURIComponent", |
| "escape", |
| "exit", |
| "eval", |
| "generatedImage", |
| "globalThis", |
| "image", |
| "isFinite", |
| "isNaN", |
| "load", |
| "notify", |
| "parseFloat", |
| "parseInt", |
| "setTimeout", |
| "store", |
| "text", |
| "tools", |
| "undefined", |
| "unescape", |
| "yield_control", |
| ]; |
| for global in &globals { |
| assert!( |
| expected.contains(&global.as_str()), |
| "unexpected global {global} in {globals:?}" |
| ); |
| } |
| } |
|
|
| #[tokio::test] |
| async fn v8_console_is_not_exposed_on_global_this() { |
| let service = InProcessCodeModeSession::new(); |
|
|
| let response = execute( |
| &service, |
| ExecuteRequest { |
| source: r#"text(String(Object.hasOwn(globalThis, "console")));"#.to_string(), |
| yield_time_ms: None, |
| ..execute_request("") |
| }, |
| ) |
| .await; |
|
|
| assert_eq!( |
| response, |
| RuntimeResponse::Result { |
| code_mode_host_duration: None, |
| cell_id: cell_id("1"), |
| content_items: vec![FunctionCallOutputContentItem::InputText { |
| text: "false".to_string(), |
| }], |
| error_text: None, |
| } |
| ); |
| } |
|
|
| #[tokio::test] |
| async fn date_locale_string_formats_with_icu_data() { |
| let service = InProcessCodeModeSession::new(); |
|
|
| let response = execute( |
| &service, |
| ExecuteRequest { |
| source: r#" |
| const value = new Date("2025-01-02T03:04:05Z") |
| .toLocaleString("fr-FR", { |
| weekday: "long", |
| month: "long", |
| day: "numeric", |
| hour: "2-digit", |
| minute: "2-digit", |
| second: "2-digit", |
| hour12: false, |
| timeZone: "UTC", |
| }); |
| text(value); |
| "# |
| .to_string(), |
| yield_time_ms: None, |
| ..execute_request("") |
| }, |
| ) |
| .await; |
|
|
| assert_eq!( |
| response, |
| RuntimeResponse::Result { |
| code_mode_host_duration: None, |
| cell_id: cell_id("1"), |
| content_items: vec![FunctionCallOutputContentItem::InputText { |
| text: "jeudi 2 janvier \u{e0} 03:04:05".to_string(), |
| }], |
| error_text: None, |
| } |
| ); |
| } |
|
|
| #[tokio::test] |
| async fn intl_date_time_format_formats_with_icu_data() { |
| let service = InProcessCodeModeSession::new(); |
|
|
| let response = execute( |
| &service, |
| ExecuteRequest { |
| source: r#" |
| const formatter = new Intl.DateTimeFormat("fr-FR", { |
| weekday: "long", |
| month: "long", |
| day: "numeric", |
| hour: "2-digit", |
| minute: "2-digit", |
| second: "2-digit", |
| hour12: false, |
| timeZone: "UTC", |
| }); |
| text(formatter.format(new Date("2025-01-02T03:04:05Z"))); |
| "# |
| .to_string(), |
| yield_time_ms: None, |
| ..execute_request("") |
| }, |
| ) |
| .await; |
|
|
| assert_eq!( |
| response, |
| RuntimeResponse::Result { |
| code_mode_host_duration: None, |
| cell_id: cell_id("1"), |
| content_items: vec![FunctionCallOutputContentItem::InputText { |
| text: "jeudi 2 janvier \u{e0} 03:04:05".to_string(), |
| }], |
| error_text: None, |
| } |
| ); |
| } |
|
|
| #[tokio::test] |
| async fn output_helpers_return_undefined() { |
| let service = InProcessCodeModeSession::new(); |
|
|
| let response = execute( |
| &service, |
| ExecuteRequest { |
| source: r#" |
| const returnsUndefined = [ |
| text("first"), |
| image("data:image/png;base64,AAA"), |
| audio("data:audio/wav;base64,YXVkaW8="), |
| notify("ping"), |
| ].map((value) => value === undefined); |
| text(JSON.stringify(returnsUndefined)); |
| "# |
| .to_string(), |
| yield_time_ms: None, |
| ..execute_request("") |
| }, |
| ) |
| .await; |
|
|
| assert_eq!( |
| response, |
| RuntimeResponse::Result { |
| code_mode_host_duration: None, |
| cell_id: cell_id("1"), |
| content_items: vec![ |
| FunctionCallOutputContentItem::InputText { |
| text: "first".to_string(), |
| }, |
| FunctionCallOutputContentItem::InputImage { |
| image_url: "data:image/png;base64,AAA".to_string(), |
| detail: Some(crate::DEFAULT_IMAGE_DETAIL), |
| }, |
| FunctionCallOutputContentItem::InputAudio { |
| audio_url: "data:audio/wav;base64,YXVkaW8=".to_string(), |
| }, |
| FunctionCallOutputContentItem::InputText { |
| text: "[true,true,true,true]".to_string(), |
| }, |
| ], |
| error_text: None, |
| } |
| ); |
| } |
|
|
| #[tokio::test] |
| async fn text_helper_serializes_objects() { |
| let service = InProcessCodeModeSession::new(); |
|
|
| let response = execute( |
| &service, |
| ExecuteRequest { |
| source: "text({ json: true });".to_string(), |
| yield_time_ms: None, |
| ..execute_request("") |
| }, |
| ) |
| .await; |
|
|
| assert_eq!( |
| response, |
| RuntimeResponse::Result { |
| code_mode_host_duration: None, |
| cell_id: cell_id("1"), |
| content_items: vec![FunctionCallOutputContentItem::InputText { |
| text: r#"{"json":true}"#.to_string(), |
| }], |
| error_text: None, |
| } |
| ); |
| } |
|
|
| #[tokio::test] |
| async fn text_helper_surfaces_stringify_errors() { |
| let service = InProcessCodeModeSession::new(); |
|
|
| let response = execute( |
| &service, |
| ExecuteRequest { |
| source: r#" |
| const circular = {}; |
| circular.self = circular; |
| text(circular); |
| "# |
| .to_string(), |
| yield_time_ms: None, |
| ..execute_request("") |
| }, |
| ) |
| .await; |
|
|
| let RuntimeResponse::Result { |
| error_text: Some(error_text), |
| .. |
| } = &response |
| else { |
| panic!("circular stringify unexpectedly succeeded: {response:?}"); |
| }; |
| assert!( |
| error_text.contains("Converting circular structure to JSON"), |
| "unexpected circular stringify error: {error_text}" |
| ); |
| let error_text = error_text.clone(); |
| assert_eq!( |
| response, |
| RuntimeResponse::Result { |
| code_mode_host_duration: None, |
| cell_id: cell_id("1"), |
| content_items: Vec::new(), |
| error_text: Some(error_text), |
| } |
| ); |
| } |
|
|
| #[tokio::test] |
| async fn audio_helper_accepts_audio_url_object_and_raw_mcp_audio_block() { |
| let service = InProcessCodeModeSession::new(); |
|
|
| let response = execute( |
| &service, |
| ExecuteRequest { |
| source: r#" |
| audio({ |
| audio_url: "data:audio/mpeg;base64,YXVkaW8=", |
| }); |
| audio({ |
| type: "audio", |
| data: "YXVkaW8=", |
| mimeType: "audio/wav", |
| }); |
| "# |
| .to_string(), |
| yield_time_ms: None, |
| ..execute_request("") |
| }, |
| ) |
| .await; |
|
|
| assert_eq!( |
| response, |
| RuntimeResponse::Result { |
| code_mode_host_duration: None, |
| cell_id: cell_id("1"), |
| content_items: vec![ |
| FunctionCallOutputContentItem::InputAudio { |
| audio_url: "data:audio/mpeg;base64,YXVkaW8=".to_string(), |
| }, |
| FunctionCallOutputContentItem::InputAudio { |
| audio_url: "data:audio/wav;base64,YXVkaW8=".to_string(), |
| }, |
| ], |
| error_text: None, |
| } |
| ); |
| } |
|
|
| #[path = "service_audio_tests.rs"] |
| mod audio_tests; |
|
|
| #[tokio::test] |
| async fn audio_helper_rejects_non_data_urls() { |
| for source in [ |
| r#"audio("https://example.com/audio.wav");"#, |
| r#"audio({ audio_url: "file:///tmp/audio.wav" });"#, |
| ] { |
| let service = InProcessCodeModeSession::new(); |
|
|
| let response = execute( |
| &service, |
| ExecuteRequest { |
| source: source.to_string(), |
| yield_time_ms: None, |
| ..execute_request("") |
| }, |
| ) |
| .await; |
|
|
| assert_eq!( |
| response, |
| RuntimeResponse::Result { |
| code_mode_host_duration: None, |
| cell_id: cell_id("1"), |
| content_items: Vec::new(), |
| error_text: Some( |
| "Tool call failed: invalid audio output. Pass a base64 data URI instead" |
| .to_string(), |
| ), |
| } |
| ); |
| } |
| } |
|
|
| #[tokio::test] |
| async fn image_helper_accepts_raw_mcp_image_block_with_original_detail() { |
| let service = InProcessCodeModeSession::new(); |
|
|
| let response = execute(&service, ExecuteRequest { |
| source: r#" |
| image({ |
| type: "image", |
| data: "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR4nGP4z8DwHwAFAAH/iZk9HQAAAABJRU5ErkJggg==", |
| mimeType: "image/png", |
| _meta: { "codex/imageDetail": "original" }, |
| }); |
| "# |
| .to_string(), |
| yield_time_ms: None, |
| ..execute_request("") |
| }) |
| .await; |
|
|
| assert_eq!( |
| response, |
| RuntimeResponse::Result { |
| code_mode_host_duration: None, |
| cell_id: cell_id("1"), |
| content_items: vec![FunctionCallOutputContentItem::InputImage { |
| image_url: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR4nGP4z8DwHwAFAAH/iZk9HQAAAABJRU5ErkJggg==".to_string(), |
| detail: Some(crate::ImageDetail::Original), |
| }], |
| error_text: None, |
| } |
| ); |
| } |
|
|
| #[tokio::test] |
| async fn generated_image_helper_appends_image_and_output_hint() { |
| let service = InProcessCodeModeSession::new(); |
|
|
| let response = execute( |
| &service, |
| ExecuteRequest { |
| source: r#" |
| generatedImage({ |
| image_url: "data:image/png;base64,AAA", |
| output_hint: "generated image save hint", |
| }); |
| "# |
| .to_string(), |
| yield_time_ms: None, |
| ..execute_request("") |
| }, |
| ) |
| .await; |
|
|
| assert_eq!( |
| response, |
| RuntimeResponse::Result { |
| code_mode_host_duration: None, |
| cell_id: cell_id("1"), |
| content_items: vec![ |
| FunctionCallOutputContentItem::InputImage { |
| image_url: "data:image/png;base64,AAA".to_string(), |
| detail: Some(crate::DEFAULT_IMAGE_DETAIL), |
| }, |
| FunctionCallOutputContentItem::InputText { |
| text: "generated image save hint".to_string(), |
| }, |
| ], |
| error_text: None, |
| } |
| ); |
| } |
|
|
| #[tokio::test] |
| async fn image_helper_second_arg_overrides_explicit_object_detail() { |
| let service = InProcessCodeModeSession::new(); |
|
|
| let response = execute( |
| &service, |
| ExecuteRequest { |
| source: r#" |
| image( |
| { |
| image_url: "data:image/png;base64,AAA", |
| detail: "high", |
| }, |
| "original", |
| ); |
| "# |
| .to_string(), |
| yield_time_ms: None, |
| ..execute_request("") |
| }, |
| ) |
| .await; |
|
|
| assert_eq!( |
| response, |
| RuntimeResponse::Result { |
| code_mode_host_duration: None, |
| cell_id: cell_id("1"), |
| content_items: vec![FunctionCallOutputContentItem::InputImage { |
| image_url: "data:image/png;base64,AAA".to_string(), |
| detail: Some(crate::ImageDetail::Original), |
| }], |
| error_text: None, |
| } |
| ); |
| } |
|
|
| #[tokio::test] |
| async fn image_helper_second_arg_overrides_raw_mcp_image_detail() { |
| let service = InProcessCodeModeSession::new(); |
|
|
| let response = execute(&service, ExecuteRequest { |
| source: r#" |
| image( |
| { |
| type: "image", |
| data: "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR4nGP4z8DwHwAFAAH/iZk9HQAAAABJRU5ErkJggg==", |
| mimeType: "image/png", |
| _meta: { "codex/imageDetail": "original" }, |
| }, |
| "high", |
| ); |
| "# |
| .to_string(), |
| yield_time_ms: None, |
| ..execute_request("") |
| }) |
| .await; |
|
|
| assert_eq!( |
| response, |
| RuntimeResponse::Result { |
| code_mode_host_duration: None, |
| cell_id: cell_id("1"), |
| content_items: vec![FunctionCallOutputContentItem::InputImage { |
| image_url: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR4nGP4z8DwHwAFAAH/iZk9HQAAAABJRU5ErkJggg==".to_string(), |
| detail: Some(crate::ImageDetail::High), |
| }], |
| error_text: None, |
| } |
| ); |
| } |
|
|
| #[tokio::test] |
| async fn image_helper_accepts_low_detail() { |
| let service = InProcessCodeModeSession::new(); |
|
|
| let response = execute( |
| &service, |
| ExecuteRequest { |
| source: r#" |
| image({ |
| image_url: "data:image/png;base64,AAA", |
| detail: "low", |
| }); |
| "# |
| .to_string(), |
| yield_time_ms: None, |
| ..execute_request("") |
| }, |
| ) |
| .await; |
|
|
| assert_eq!( |
| response, |
| RuntimeResponse::Result { |
| code_mode_host_duration: None, |
| cell_id: cell_id("1"), |
| content_items: vec![FunctionCallOutputContentItem::InputImage { |
| image_url: "data:image/png;base64,AAA".to_string(), |
| detail: Some(crate::ImageDetail::Low), |
| }], |
| error_text: None, |
| } |
| ); |
| } |
|
|
| #[tokio::test] |
| async fn image_helpers_reject_remote_urls() { |
| for image_url in [ |
| "http://example.com/image.jpg", |
| "https://example.com/image.jpg", |
| ] { |
| for source in [ |
| format!("image({image_url:?});"), |
| format!("generatedImage({{ image_url: {image_url:?} }});"), |
| ] { |
| let service = InProcessCodeModeSession::new(); |
|
|
| let response = execute( |
| &service, |
| ExecuteRequest { |
| source, |
| yield_time_ms: None, |
| ..execute_request("") |
| }, |
| ) |
| .await; |
|
|
| assert_eq!( |
| response, |
| RuntimeResponse::Result { |
| code_mode_host_duration: None, |
| cell_id: cell_id("1"), |
| content_items: Vec::new(), |
| error_text: Some( |
| "Tool call failed: remote image URLs are not supported in tool outputs. Pass a base64 data URI instead".to_string(), |
| ), |
| } |
| ); |
| } |
| } |
| } |
|
|
| #[tokio::test] |
| async fn image_helpers_reject_invalid_image_outputs() { |
| let image_url = |
| "Error executing tool exec: Expected at least one message to convert to CallToolResult"; |
| for source in [ |
| format!("image({image_url:?}, \"original\");"), |
| format!("generatedImage({{ image_url: {image_url:?} }});"), |
| ] { |
| let service = InProcessCodeModeSession::new(); |
|
|
| let response = execute( |
| &service, |
| ExecuteRequest { |
| source, |
| yield_time_ms: None, |
| ..execute_request("") |
| }, |
| ) |
| .await; |
|
|
| assert_eq!( |
| response, |
| RuntimeResponse::Result { |
| code_mode_host_duration: None, |
| cell_id: cell_id("1"), |
| content_items: Vec::new(), |
| error_text: Some( |
| "Tool call failed: invalid image output. Pass a base64 data URI instead" |
| .to_string(), |
| ), |
| } |
| ); |
| } |
| } |
|
|
| #[tokio::test] |
| async fn image_helper_rejects_unsupported_detail() { |
| let service = InProcessCodeModeSession::new(); |
|
|
| let response = execute( |
| &service, |
| ExecuteRequest { |
| source: r#" |
| image({ |
| image_url: "data:image/png;base64,AAA", |
| detail: "medium", |
| }); |
| "# |
| .to_string(), |
| yield_time_ms: None, |
| ..execute_request("") |
| }, |
| ) |
| .await; |
|
|
| assert_eq!( |
| response, |
| RuntimeResponse::Result { |
| code_mode_host_duration: None, |
| cell_id: cell_id("1"), |
| content_items: Vec::new(), |
| error_text: Some("image detail must be one of: auto, low, high, original".to_string()), |
| } |
| ); |
| } |
|
|
| #[tokio::test] |
| async fn image_helper_rejects_raw_mcp_result_container() { |
| let service = InProcessCodeModeSession::new(); |
|
|
| let response = execute(&service, ExecuteRequest { |
| source: r#" |
| image({ |
| content: [ |
| { |
| type: "image", |
| data: "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR4nGP4z8DwHwAFAAH/iZk9HQAAAABJRU5ErkJggg==", |
| mimeType: "image/png", |
| _meta: { "codex/imageDetail": "original" }, |
| }, |
| ], |
| isError: false, |
| }); |
| "# |
| .to_string(), |
| yield_time_ms: None, |
| ..execute_request("") |
| }) |
| .await; |
|
|
| assert_eq!( |
| response, |
| RuntimeResponse::Result { |
| code_mode_host_duration: None, |
| cell_id: cell_id("1"), |
| content_items: Vec::new(), |
| error_text: Some( |
| "image expects a non-empty image URL string, an object with image_url and optional detail, or a raw MCP image block".to_string(), |
| ), |
| } |
| ); |
| } |
|
|
| #[tokio::test] |
| async fn wait_reports_missing_cell_separately_from_runtime_results() { |
| let service = InProcessCodeModeSession::new(); |
|
|
| let response = service |
| .wait(WaitRequest { |
| cell_id: cell_id("missing"), |
| yield_time_ms: 1, |
| }) |
| .await |
| .unwrap(); |
|
|
| assert_eq!( |
| response, |
| WaitOutcome::MissingCell(RuntimeResponse::Result { |
| code_mode_host_duration: None, |
| cell_id: cell_id("missing"), |
| content_items: Vec::new(), |
| error_text: Some("exec cell missing not found".to_string()), |
| }) |
| ); |
| } |
|
|