From c1f1467f3028bd433c8f2063ecc28dd5be206df6 Mon Sep 17 00:00:00 2001 From: Alec Barber Date: Tue, 8 Sep 2026 17:38:23 +0000 Subject: [PATCH] Handle undefined values before JSON serialization in code mode (#43873) ## Why Passing `undefined` to a tool could fail JSON parsing because V8 stringifies it as the non-JSON text `undefined`. ## What changed Return no JSON value for JavaScript `undefined`, so explicit `undefined` tool arguments behave like omitted arguments. Attempts to `store` an undefined value report the existing serializability error and preserve the previous stored value. ## Testing Add regression coverage for storing `undefined` over an existing `null` value, and extend the current-time tool test to cover `{}`, omitted arguments, and explicit `undefined`. GitOrigin-RevId: 071050ebb2ecc44c8f08453cf8523b4d21728c7e --- .../code-mode-runtime/src/runtime/value.rs | 5 +++ .../code-mode-runtime/src/service_tests.rs | 42 +++++++++++++++++++ codex-rs/core/tests/suite/code_mode.rs | 11 +++-- 3 files changed, 55 insertions(+), 3 deletions(-) diff --git a/codex-rs/code-mode-runtime/src/runtime/value.rs b/codex-rs/code-mode-runtime/src/runtime/value.rs index f7e63d0429..2e43d883f8 100644 --- a/codex-rs/code-mode-runtime/src/runtime/value.rs +++ b/codex-rs/code-mode-runtime/src/runtime/value.rs @@ -297,6 +297,11 @@ pub(super) fn v8_value_to_json( scope: &mut v8::PinScope<'_, '_>, value: v8::Local<'_, v8::Value>, ) -> Result, String> { + // V8 stringifies undefined as the non-JSON text "undefined". + if value.is_undefined() { + return Ok(None); + } + let tc = std::pin::pin!(v8::TryCatch::new(scope)); let mut tc = tc.init(); let Some(stringified) = v8::json::stringify(&tc, value) else { diff --git a/codex-rs/code-mode-runtime/src/service_tests.rs b/codex-rs/code-mode-runtime/src/service_tests.rs index 2ce272efe4..66b8cf46b6 100644 --- a/codex-rs/code-mode-runtime/src/service_tests.rs +++ b/codex-rs/code-mode-runtime/src/service_tests.rs @@ -474,6 +474,48 @@ async fn stored_values_are_shared_between_cells_but_not_sessions() { ); } +#[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(); diff --git a/codex-rs/core/tests/suite/code_mode.rs b/codex-rs/core/tests/suite/code_mode.rs index 1d5cc70a0e..33fe2062de 100644 --- a/codex-rs/core/tests/suite/code_mode.rs +++ b/codex-rs/core/tests/suite/code_mode.rs @@ -1695,18 +1695,23 @@ text(JSON.stringify(result)); Ok(()) } +#[test_case("{}"; "object")] +#[test_case(""; "omitted")] +#[test_case("undefined"; "undefined")] #[tokio::test(flavor = "multi_thread", worker_threads = 2)] -async fn code_mode_current_time_returns_structured_result() -> Result<()> { +async fn code_mode_current_time_returns_structured_result(argument: &str) -> Result<()> { skip_if_no_network!(Ok(())); let server = responses::start_mock_server().await; let (_test, second_mock) = run_code_mode_turn_with_config( &server, "use exec to get the current time", - r#" -const result = await tools.clock__curr_time({}); + &format!( + r#" +const result = await tools.clock__curr_time({argument}); text(JSON.stringify(result)); "#, + ), |config| { config .features