mirror of
https://github.com/openai/codex.git
synced 2026-10-29 19:08:43 +00:00
## Why Code Mode sessions always used client-side output handling, leaving server truncation settings and observation budgets unused. ## What changed - Honor each session's `output_truncation` setting in the runtime and both host transports, and advertise the bounded-output capability. - Apply `max_output_tokens` independently to execute, wait, and terminate responses in server mode. Keep client mode as the default when the setting is omitted. - Use bounded output collection and rendering in server mode, preserving head and tail output with omission notices. Include script errors in the output budget while retaining failure status and a separate diagnostic capped at 64 KiB. - Estimate audio tokens without the Tokio-dependent cache when rendering bounded output. ## Testing Add runtime and host regression tests for session mode selection, per-request budgets, output overflow, oversized images, zero-token audio output, diagnostic preservation, and termination under output backpressure. GitOrigin-RevId: 3d9661edcdd2dde31526bb92eb83fa0614f0fa88
2160 lines
63 KiB
Rust
2160 lines
63 KiB
Rust
use codex_code_mode_protocol::NoopCodeModeSessionDelegate;
|
|
use std::sync::Arc;
|
|
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::CodeModeSession;
|
|
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::sync::oneshot;
|
|
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 interrupt_yields_execute_and_wait_without_stopping_the_cell() {
|
|
let _time_guard = hold_virtual_time();
|
|
let delegate = Arc::new(ReleasableToolDelegate::default());
|
|
let service: Arc<dyn CodeModeSession> = Arc::new(InProcessCodeModeSession::new());
|
|
let execute_signal = CancellationToken::new();
|
|
let started = service
|
|
.execute(
|
|
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(),
|
|
Some(execute_signal.clone()),
|
|
)
|
|
.await
|
|
.unwrap();
|
|
let response = tokio::spawn(started.initial_response());
|
|
wait_until_tool_started(&delegate).await.unwrap();
|
|
execute_signal.cancel();
|
|
assert_eq!(
|
|
response.await.unwrap().unwrap(),
|
|
RuntimeResponse::Yielded {
|
|
code_mode_host_duration: None,
|
|
cell_id: cell_id("1"),
|
|
content_items: Vec::new(),
|
|
}
|
|
);
|
|
|
|
let wait_signal = CancellationToken::new();
|
|
let response = tokio::spawn({
|
|
let service = Arc::clone(&service);
|
|
let wait_signal = wait_signal.clone();
|
|
async move {
|
|
service
|
|
.wait(
|
|
WaitRequest {
|
|
cell_id: cell_id("1"),
|
|
yield_time_ms: 60_000,
|
|
max_output_tokens: None,
|
|
},
|
|
Some(wait_signal),
|
|
)
|
|
.await
|
|
}
|
|
});
|
|
tokio::task::yield_now().await;
|
|
assert!(!response.is_finished());
|
|
wait_signal.cancel();
|
|
assert_eq!(
|
|
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 response = tokio::spawn({
|
|
let service = Arc::clone(&service);
|
|
async move {
|
|
service
|
|
.wait(
|
|
WaitRequest {
|
|
cell_id: cell_id("1"),
|
|
yield_time_ms: 60_000,
|
|
max_output_tokens: None,
|
|
},
|
|
/*preempt*/ None,
|
|
)
|
|
.await
|
|
}
|
|
});
|
|
assert_eq!(
|
|
response.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(),
|
|
}],
|
|
result_buffered_for: Duration::ZERO,
|
|
error_text: None,
|
|
})
|
|
);
|
|
}
|
|
|
|
#[tokio::test(start_paused = true)]
|
|
async fn execute_waits_for_nested_tool_during_yield_grace() {
|
|
let _time_guard = hold_virtual_time();
|
|
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(), /*preempt*/ None)
|
|
.await
|
|
.unwrap();
|
|
let response = tokio::spawn(started.initial_response());
|
|
wait_until_tool_started(&delegate).await.unwrap();
|
|
tokio::time::advance(Duration::from_millis(10_500)).await;
|
|
delegate.release_tool();
|
|
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(),
|
|
}],
|
|
result_buffered_for: Duration::ZERO,
|
|
error_text: None,
|
|
}
|
|
);
|
|
}
|
|
|
|
#[tokio::test(start_paused = true)]
|
|
async fn execute_and_wait_clamp_yield_grace_without_stopping_the_cell() {
|
|
let _time_guard = hold_virtual_time();
|
|
let delegate = Arc::new(ReleasableToolDelegate::default());
|
|
let service = InProcessCodeModeSession::with_limits(CodeModeSessionCellExecutionLimits {
|
|
max_yield_time_ms: Some(/*value*/ 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(),
|
|
/*preempt*/ None,
|
|
)
|
|
.await
|
|
.unwrap();
|
|
let initial_response = tokio::spawn(started.initial_response());
|
|
wait_until_tool_started(&delegate).await.unwrap();
|
|
|
|
tokio::time::advance(Duration::from_millis(9_999)).await;
|
|
assert!(!initial_response.is_finished());
|
|
tokio::time::advance(Duration::from_millis(/*millis*/ 1)).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,
|
|
max_output_tokens: None,
|
|
},
|
|
/*preempt*/ None,
|
|
)
|
|
.await;
|
|
let wait_response = tokio::spawn(wait_response);
|
|
tokio::task::yield_now().await;
|
|
tokio::time::advance(Duration::from_secs(/*secs*/ 10)).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,
|
|
max_output_tokens: None,
|
|
},
|
|
/*preempt*/ None,
|
|
)
|
|
.await;
|
|
let completion = tokio::spawn(completion);
|
|
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(),
|
|
}],
|
|
result_buffered_for: Duration::ZERO,
|
|
error_text: None,
|
|
})
|
|
);
|
|
}
|
|
|
|
#[tokio::test(start_paused = true)]
|
|
async fn wait_waits_for_nested_tool_during_yield_grace() {
|
|
let _time_guard = hold_virtual_time();
|
|
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,
|
|
max_output_tokens: None,
|
|
},
|
|
/*preempt*/ None,
|
|
)
|
|
.await;
|
|
let response = tokio::spawn(response);
|
|
tokio::task::yield_now().await;
|
|
tokio::time::advance(Duration::from_millis(10_500)).await;
|
|
delegate.release_tool();
|
|
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(),
|
|
}],
|
|
result_buffered_for: Duration::ZERO,
|
|
error_text: None,
|
|
})
|
|
);
|
|
}
|
|
|
|
#[tokio::test(start_paused = true)]
|
|
async fn zero_yield_limit_is_immediate_and_scoped_to_its_session() {
|
|
let _time_guard = hold_virtual_time();
|
|
let zero_delegate = Arc::new(ReleasableToolDelegate::default());
|
|
let zero_session = InProcessCodeModeSession::with_limits(CodeModeSessionCellExecutionLimits {
|
|
max_yield_time_ms: Some(/*value*/ 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(/*value*/ 10),
|
|
max_heap_size_bytes: None,
|
|
});
|
|
let request = ExecuteRequest {
|
|
enabled_tools: vec![echo_tool()],
|
|
source: "await tools.echo({});".to_string(),
|
|
yield_time_ms: Some(/*value*/ 60_000),
|
|
..execute_request("")
|
|
};
|
|
let zero_started = zero_session
|
|
.execute(
|
|
request.clone(),
|
|
zero_delegate.clone(),
|
|
/*preempt*/ None,
|
|
)
|
|
.await
|
|
.unwrap();
|
|
let limited_started = limited_session
|
|
.execute(request, limited_delegate.clone(), /*preempt*/ None)
|
|
.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.unwrap();
|
|
wait_until_tool_started(&limited_delegate).await.unwrap();
|
|
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,
|
|
max_output_tokens: None,
|
|
},
|
|
/*preempt*/ None,
|
|
)
|
|
.await;
|
|
let zero_wait = tokio::spawn(zero_wait);
|
|
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(/*millis*/ 10)).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();
|
|
}
|
|
|
|
#[tokio::test(start_paused = true)]
|
|
async fn tool_start_acknowledgement_reports_error_before_invocation() {
|
|
let _time_guard = hold_virtual_time();
|
|
let delegate = Arc::new(ReleasableToolDelegate::default());
|
|
let service = InProcessCodeModeSession::new();
|
|
let started = service
|
|
.execute(
|
|
execute_request("throw 'startup failed';"),
|
|
delegate.clone(),
|
|
/*preempt*/ None,
|
|
)
|
|
.await
|
|
.unwrap();
|
|
|
|
let (startup, response) = tokio::join!(
|
|
wait_until_tool_started(&delegate),
|
|
started.initial_response(),
|
|
);
|
|
assert_eq!(startup, Err("cell closed before nested tool started"));
|
|
assert_eq!(
|
|
response.unwrap(),
|
|
RuntimeResponse::Result {
|
|
code_mode_host_duration: None,
|
|
cell_id: cell_id("1"),
|
|
content_items: Vec::new(),
|
|
result_buffered_for: Duration::ZERO,
|
|
error_text: Some("startup failed".to_string()),
|
|
}
|
|
);
|
|
}
|
|
|
|
#[tokio::test(start_paused = true)]
|
|
async fn tool_start_acknowledgement_reports_termination_before_invocation() {
|
|
let _time_guard = hold_virtual_time();
|
|
let delegate = Arc::new(ReleasableToolDelegate::default());
|
|
let service = InProcessCodeModeSession::new();
|
|
let started = service
|
|
.execute(
|
|
execute_request("await new Promise(() => {});"),
|
|
delegate.clone(),
|
|
/*preempt*/ None,
|
|
)
|
|
.await
|
|
.unwrap();
|
|
|
|
let (startup, terminated) = tokio::join!(
|
|
wait_until_tool_started(&delegate),
|
|
service.terminate(cell_id("1").into()),
|
|
);
|
|
assert_eq!(startup, Err("cell closed before nested tool started"));
|
|
let response = RuntimeResponse::Terminated {
|
|
code_mode_host_duration: None,
|
|
cell_id: cell_id("1"),
|
|
content_items: Vec::new(),
|
|
};
|
|
assert_eq!(terminated.unwrap(), WaitOutcome::LiveCell(response.clone()));
|
|
assert_eq!(started.initial_response().await.unwrap(), response);
|
|
}
|
|
|
|
fn hold_virtual_time() -> oneshot::Sender<()> {
|
|
// A live blocking task prevents Tokio's paused clock from auto-advancing while
|
|
// we await the V8 thread. Tests advance time explicitly; dropping the guard
|
|
// releases the task even if the test panics or is cancelled.
|
|
let (guard, released) = oneshot::channel();
|
|
tokio::task::spawn_blocking(move || {
|
|
let _ = released.blocking_recv();
|
|
});
|
|
guard
|
|
}
|
|
|
|
async fn wait_until_tool_started(delegate: &ReleasableToolDelegate) -> Result<(), &'static str> {
|
|
tokio::select! {
|
|
biased;
|
|
_ = delegate.tool_started.notified() => Ok(()),
|
|
_ = delegate.cell_closed.cancelled() => Err("cell closed before nested tool started"),
|
|
}
|
|
}
|
|
|
|
#[derive(Default)]
|
|
struct ReleasableToolDelegate {
|
|
tool_release: Notify,
|
|
tool_started: Notify,
|
|
cell_closed: CancellationToken,
|
|
}
|
|
|
|
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.notify_one();
|
|
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) {
|
|
self.cell_closed.cancel();
|
|
}
|
|
}
|
|
|
|
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,
|
|
input_schema_max_bytes: None,
|
|
output_schema: None,
|
|
}
|
|
}
|
|
|
|
async fn execute(service: &InProcessCodeModeSession, request: ExecuteRequest) -> RuntimeResponse {
|
|
service
|
|
.execute(
|
|
request,
|
|
Arc::new(NoopCodeModeSessionDelegate),
|
|
/*preempt*/ None,
|
|
)
|
|
.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(),
|
|
}],
|
|
result_buffered_for: Duration::ZERO,
|
|
error_text: None,
|
|
}
|
|
);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn exit_stops_the_whole_cell_and_preserves_only_prior_writes() {
|
|
for (label, source) in [
|
|
("exit inside infinite loop", r#"for (;;) { exit(); }"#),
|
|
("infinite loop after exit", r#"exit(); for (;;) {}"#),
|
|
(
|
|
"invalid helper after exit",
|
|
r#"try { exit(); image(null); } catch { for (;;) {} }"#,
|
|
),
|
|
(
|
|
"catch and finally",
|
|
r#"try { exit(); } catch { text("caught"); } finally { store("phase", "finally"); text("finally"); }"#,
|
|
),
|
|
(
|
|
"queued microtask",
|
|
r#"Promise.resolve().then(() => { store("phase", "microtask"); text("microtask"); }); exit();"#,
|
|
),
|
|
(
|
|
"unawaited promise",
|
|
r#"Promise.resolve().then(() => exit()).catch(() => text("caught")); await new Promise(() => {});"#,
|
|
),
|
|
(
|
|
"async module",
|
|
r#"await Promise.resolve(); try { exit(); } finally { text("finally"); }"#,
|
|
),
|
|
(
|
|
"parallel promises",
|
|
r#"await Promise.all([Promise.resolve().then(() => exit()), Promise.resolve().then(() => { store("phase", "sibling"); text("sibling"); })]);"#,
|
|
),
|
|
(
|
|
"timer callback",
|
|
r#"setTimeout(() => { try { exit(); } catch { text("caught"); } finally { text("finally"); } }, 0); await new Promise(() => {});"#,
|
|
),
|
|
(
|
|
"timer microtask",
|
|
r#"setTimeout(() => { Promise.resolve().then(() => exit()); }, 0); await new Promise(() => {});"#,
|
|
),
|
|
(
|
|
"after awaited timer",
|
|
r#"await new Promise((resolve) => setTimeout(resolve, 0)); try { exit(); } catch { text("caught"); }"#,
|
|
),
|
|
(
|
|
"text toJSON",
|
|
r#"try { text({ toJSON() { exit(); } }); } catch { text("caught"); } finally { text("finally"); }"#,
|
|
),
|
|
(
|
|
"store toJSON",
|
|
r#"try { store("phase", { toJSON() { exit(); } }); } catch { text("caught"); } finally { text("finally"); }"#,
|
|
),
|
|
(
|
|
"store key coercion",
|
|
r#"try { store({ toString() { exit(); } }, "late"); } catch { text("caught"); } finally { text("finally"); }"#,
|
|
),
|
|
(
|
|
"image getter",
|
|
r#"try { image({ get image_url() { exit(); } }); } catch { text("caught"); } finally { text("finally"); }"#,
|
|
),
|
|
(
|
|
"exception stack getter",
|
|
r#"store("phase", "before formatting");
|
|
throw {
|
|
get stack() { store("phase", "before"); exit(); for (;;) {} },
|
|
toString() { for (;;) {} }
|
|
};"#,
|
|
),
|
|
] {
|
|
let service = InProcessCodeModeSession::new();
|
|
let response = tokio::time::timeout(
|
|
Duration::from_secs(5),
|
|
execute(
|
|
&service,
|
|
ExecuteRequest {
|
|
source: format!(
|
|
r#"store("phase", "before"); text("before"); {source} store("phase", "after"); text("after");"#
|
|
),
|
|
yield_time_ms: Some(30_000),
|
|
..execute_request("")
|
|
},
|
|
),
|
|
)
|
|
.await
|
|
.unwrap_or_else(|_| panic!("exit did not stop {label}"));
|
|
assert_eq!(
|
|
response,
|
|
RuntimeResponse::Result {
|
|
code_mode_host_duration: None,
|
|
cell_id: cell_id("1"),
|
|
content_items: vec![FunctionCallOutputContentItem::InputText {
|
|
text: "before".to_string(),
|
|
}],
|
|
result_buffered_for: Duration::ZERO,
|
|
error_text: None,
|
|
},
|
|
"{label}"
|
|
);
|
|
|
|
let response = execute(
|
|
&service,
|
|
ExecuteRequest {
|
|
yield_time_ms: None,
|
|
..execute_request(r#"text(load("phase"));"#)
|
|
},
|
|
)
|
|
.await;
|
|
assert_eq!(
|
|
response,
|
|
RuntimeResponse::Result {
|
|
code_mode_host_duration: None,
|
|
cell_id: cell_id("2"),
|
|
content_items: vec![FunctionCallOutputContentItem::InputText {
|
|
text: "before".to_string(),
|
|
}],
|
|
result_buffered_for: Duration::ZERO,
|
|
error_text: None,
|
|
},
|
|
"{label}: next cell"
|
|
);
|
|
}
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn exit_after_yield_and_tool_response_finishes_successfully() {
|
|
let delegate = Arc::new(ReleasableToolDelegate::default());
|
|
let service = InProcessCodeModeSession::new();
|
|
let yield_signal = CancellationToken::new();
|
|
let started = service
|
|
.execute(
|
|
ExecuteRequest {
|
|
enabled_tools: vec![echo_tool()],
|
|
source: r#"
|
|
text("before yield");
|
|
await tools.echo({});
|
|
store("phase", "after tool");
|
|
text("after tool");
|
|
try { exit(); } catch { text("caught"); } finally { text("finally"); }
|
|
text("after exit");
|
|
"#
|
|
.to_string(),
|
|
yield_time_ms: Some(30_000),
|
|
..execute_request("")
|
|
},
|
|
delegate.clone(),
|
|
Some(yield_signal.clone()),
|
|
)
|
|
.await
|
|
.unwrap();
|
|
let response = tokio::spawn(started.initial_response());
|
|
wait_until_tool_started(&delegate).await.unwrap();
|
|
yield_signal.cancel();
|
|
assert_eq!(
|
|
response.await.unwrap().unwrap(),
|
|
RuntimeResponse::Yielded {
|
|
code_mode_host_duration: None,
|
|
cell_id: cell_id("1"),
|
|
content_items: vec![FunctionCallOutputContentItem::InputText {
|
|
text: "before yield".to_string(),
|
|
}],
|
|
}
|
|
);
|
|
delegate.release_tool();
|
|
let actual = service
|
|
.wait(
|
|
WaitRequest {
|
|
max_output_tokens: None,
|
|
cell_id: cell_id("1"),
|
|
yield_time_ms: 30_000,
|
|
},
|
|
/*preempt*/ None,
|
|
)
|
|
.await
|
|
.unwrap();
|
|
assert_eq!(
|
|
actual,
|
|
WaitOutcome::LiveCell(RuntimeResponse::Result {
|
|
code_mode_host_duration: None,
|
|
cell_id: cell_id("1"),
|
|
content_items: vec![FunctionCallOutputContentItem::InputText {
|
|
text: "after tool".to_string(),
|
|
}],
|
|
result_buffered_for: actual.result_buffered_for().unwrap(),
|
|
error_text: None,
|
|
})
|
|
);
|
|
assert_eq!(
|
|
execute(
|
|
&service,
|
|
ExecuteRequest {
|
|
yield_time_ms: None,
|
|
..execute_request(r#"text(load("phase"));"#)
|
|
},
|
|
)
|
|
.await,
|
|
RuntimeResponse::Result {
|
|
code_mode_host_duration: None,
|
|
cell_id: cell_id("2"),
|
|
content_items: vec![FunctionCallOutputContentItem::InputText {
|
|
text: "after tool".to_string(),
|
|
}],
|
|
result_buffered_for: Duration::ZERO,
|
|
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(),
|
|
result_buffered_for: Duration::ZERO,
|
|
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(),
|
|
}],
|
|
result_buffered_for: Duration::ZERO,
|
|
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(),
|
|
}],
|
|
result_buffered_for: Duration::ZERO,
|
|
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(),
|
|
},
|
|
],
|
|
result_buffered_for: Duration::ZERO,
|
|
error_text: None,
|
|
}
|
|
);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn json_conversion_rejects_serde_markers_without_poisoning_sessions() {
|
|
let first = InProcessCodeModeSession::new();
|
|
let second = InProcessCodeModeSession::new();
|
|
let source = r#"
|
|
store("healthy", 42);
|
|
function rejects(value, message) {
|
|
let error;
|
|
try { store("healthy", value); } catch (e) { error = String(e); }
|
|
if (!message.test(error ?? "")) throw new Error(`expected ${message}; got ${error ?? "no error"}`);
|
|
}
|
|
const keys = ["$serde_json::private::RawValue", "$serde_json::private::Number"];
|
|
for (const key of keys) {
|
|
for (const value of [
|
|
{[key]: "123"},
|
|
{first: 0, [key]: "null"},
|
|
[{[key]: "123"}],
|
|
JSON.parse('{"\\u0024' + key.slice(1) + '":"123"}'),
|
|
]) rejects(value, /reserved JSON object key/);
|
|
|
|
// Marker text in strings and longer keys must remain valid.
|
|
const literal = ['{"' + key + '":"123"}', {['prefix"' + key]: key}];
|
|
store("literal", literal);
|
|
if (JSON.stringify(load("literal")) !== JSON.stringify(literal)) throw "changed literal";
|
|
}
|
|
for (const segments of [2, 5]) {
|
|
let encoded = "0";
|
|
for (let i = 0; i < segments; i++) {
|
|
encoded = "[".repeat(80) + encoded + "]".repeat(80);
|
|
if (i < segments - 1) encoded = JSON.stringify({[keys[0]]: encoded});
|
|
}
|
|
// Exercise Serde's depth reset without first requiring a deep V8 object.
|
|
rejects({[keys[0]]: encoded}, /reserved JSON object key/);
|
|
}
|
|
let value = 0;
|
|
for (let depth = 1; depth <= 160; depth++) {
|
|
value = [value];
|
|
if (depth === 127) {
|
|
try {
|
|
store("boundary", value);
|
|
if (JSON.stringify(load("boundary")) !== JSON.stringify(value)) throw "changed boundary";
|
|
} catch (e) {
|
|
// V8 can reach its stack limit before Serde's depth limit on Windows.
|
|
if (!String(e).includes("Maximum call stack size exceeded")) throw e;
|
|
}
|
|
}
|
|
if (depth === 128 || depth === 160) rejects(value, /recursion limit exceeded|Maximum call stack size exceeded/);
|
|
}
|
|
const normal = {toJSON() { return [undefined, NaN, Infinity, 1.25, 1e100]; }};
|
|
store("normal", normal);
|
|
if (JSON.stringify(load("normal")) !== JSON.stringify(normal)) throw "changed normal JSON";
|
|
text("ok");
|
|
store("healthy", {[keys[0]]: "null"});
|
|
"#;
|
|
|
|
for (session, source, expected_text, expected_error) in [
|
|
(
|
|
&second,
|
|
r#"store("healthy", 42); text(load("healthy"));"#,
|
|
"42",
|
|
None,
|
|
),
|
|
(
|
|
&first,
|
|
source,
|
|
"ok",
|
|
Some("failed to serialize JavaScript value: reserved JSON object key"),
|
|
),
|
|
(&first, r#"text(load("healthy"));"#, "42", None),
|
|
(&second, r#"text(load("healthy"));"#, "42", None),
|
|
] {
|
|
let response = execute(
|
|
session,
|
|
ExecuteRequest {
|
|
yield_time_ms: None,
|
|
..execute_request(source)
|
|
},
|
|
)
|
|
.await;
|
|
let RuntimeResponse::Result {
|
|
content_items,
|
|
error_text,
|
|
..
|
|
} = response
|
|
else {
|
|
panic!("unexpected response: {response:?}");
|
|
};
|
|
assert_eq!(
|
|
(content_items, error_text),
|
|
(
|
|
vec![FunctionCallOutputContentItem::InputText {
|
|
text: expected_text.to_string()
|
|
}],
|
|
expected_error.map(str::to_string),
|
|
)
|
|
);
|
|
}
|
|
}
|
|
|
|
#[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),
|
|
/*preempt*/ None,
|
|
)
|
|
.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),
|
|
/*preempt*/ None,
|
|
)
|
|
.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(),
|
|
}],
|
|
result_buffered_for: Duration::ZERO,
|
|
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").into()).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").into()).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").into()).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").into()).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(),
|
|
}],
|
|
result_buffered_for: Duration::ZERO,
|
|
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",
|
|
"as_settled",
|
|
"audio",
|
|
"decodeURI",
|
|
"decodeURIComponent",
|
|
"encodeURI",
|
|
"encodeURIComponent",
|
|
"escape",
|
|
"exit",
|
|
"eval",
|
|
"generatedImage",
|
|
"globalThis",
|
|
"image",
|
|
"isFinite",
|
|
"isNaN",
|
|
"load",
|
|
"notify",
|
|
"parseFloat",
|
|
"parseInt",
|
|
"setTimeout",
|
|
"store",
|
|
"stream_settled",
|
|
"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(),
|
|
}],
|
|
result_buffered_for: Duration::ZERO,
|
|
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(),
|
|
}],
|
|
result_buffered_for: Duration::ZERO,
|
|
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(),
|
|
}],
|
|
result_buffered_for: Duration::ZERO,
|
|
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(),
|
|
},
|
|
],
|
|
result_buffered_for: Duration::ZERO,
|
|
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(),
|
|
}],
|
|
result_buffered_for: Duration::ZERO,
|
|
error_text: None,
|
|
}
|
|
);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn output_helpers_surface_serialization_errors() {
|
|
for (source, expected_error) in [
|
|
(
|
|
"const circular = {}; circular.self = circular; text(circular);",
|
|
"Converting circular structure to JSON",
|
|
),
|
|
(
|
|
"image({get image_url() { throw new Error('image getter failed'); }});",
|
|
"image getter failed",
|
|
),
|
|
(
|
|
"image({type: 'image', get data() { throw new Error('MCP getter failed'); }});",
|
|
"MCP getter failed",
|
|
),
|
|
] {
|
|
let service = InProcessCodeModeSession::new();
|
|
let response = execute(
|
|
&service,
|
|
ExecuteRequest {
|
|
yield_time_ms: None,
|
|
..execute_request(source)
|
|
},
|
|
)
|
|
.await;
|
|
|
|
let RuntimeResponse::Result {
|
|
error_text: Some(error_text),
|
|
..
|
|
} = &response
|
|
else {
|
|
panic!("serialization unexpectedly succeeded: {response:?}");
|
|
};
|
|
assert!(
|
|
error_text.contains(expected_error),
|
|
"unexpected serialization 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(),
|
|
result_buffered_for: Duration::ZERO,
|
|
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(),
|
|
},
|
|
],
|
|
result_buffered_for: Duration::ZERO,
|
|
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(),
|
|
result_buffered_for: Duration::ZERO,
|
|
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),
|
|
}],
|
|
result_buffered_for: Duration::ZERO,
|
|
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(),
|
|
},
|
|
],
|
|
result_buffered_for: Duration::ZERO,
|
|
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),
|
|
}],
|
|
result_buffered_for: Duration::ZERO,
|
|
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),
|
|
}],
|
|
result_buffered_for: Duration::ZERO,
|
|
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),
|
|
}],
|
|
result_buffered_for: Duration::ZERO,
|
|
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(),
|
|
result_buffered_for: Duration::ZERO,
|
|
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(),
|
|
result_buffered_for: Duration::ZERO,
|
|
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(),
|
|
result_buffered_for: Duration::ZERO,
|
|
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(),
|
|
result_buffered_for: Duration::ZERO,
|
|
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,
|
|
max_output_tokens: None,
|
|
},
|
|
/*preempt*/ None,
|
|
)
|
|
.await
|
|
.unwrap();
|
|
|
|
assert_eq!(
|
|
response,
|
|
WaitOutcome::MissingCell(RuntimeResponse::Result {
|
|
code_mode_host_duration: None,
|
|
cell_id: cell_id("missing"),
|
|
content_items: Vec::new(),
|
|
result_buffered_for: Duration::ZERO,
|
|
error_text: Some("exec cell missing not found".to_string()),
|
|
})
|
|
);
|
|
}
|