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
This commit is contained in:
Alec Barber
2026-09-08 17:51:40 +00:00
committed by copyberry
parent 44ab72674e
commit c1f1467f30
3 changed files with 55 additions and 3 deletions
@@ -297,6 +297,11 @@ pub(super) fn v8_value_to_json(
scope: &mut v8::PinScope<'_, '_>,
value: v8::Local<'_, v8::Value>,
) -> Result<Option<JsonValue>, 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 {
@@ -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();