mirror of
https://github.com/openai/codex.git
synced 2026-10-18 17:18:52 +00:00
## What changed Expose two JavaScript globals for consuming results as inputs settle: - `as_settled(promises)` returns an async iterator of fulfilled and rejected records in settlement order, preserving queued results while the consumer is busy. - `stream_settled(promises, emit)` delivers those records to a callback, awaiting each invocation and propagating callback errors. Records include an `index`, a `status`, and either a `value` or a `reason`. Iterable inputs use positional indices; `Map` inputs retain their keys. Both helpers accept promises, thenables, and plain values. ## Testing Add runtime coverage for ordering, input types, object identity, callback behavior, early iteration exit, and termination. Add stdio and gRPC host tests for partial results across waits, plus a scenario snapshot verifying fulfilled and rejected records reach the model. GitOrigin-RevId: cddc9f3679ae04f2421e154caa0417afb15f550d
258 lines
7.7 KiB
Rust
258 lines
7.7 KiB
Rust
//! Exercises settlement ordering and callback behavior through V8 cell execution.
|
|
|
|
use std::sync::Arc;
|
|
use std::time::Duration;
|
|
|
|
use codex_code_mode_runtime::ExecuteRequest;
|
|
use codex_code_mode_runtime::ExecuteToPendingOutcome;
|
|
use codex_code_mode_runtime::FunctionCallOutputContentItem;
|
|
use codex_code_mode_runtime::InProcessCodeModeSession;
|
|
use codex_code_mode_runtime::NoopCodeModeSessionDelegate;
|
|
use codex_code_mode_runtime::RuntimeResponse;
|
|
use codex_code_mode_runtime::WaitOutcome;
|
|
use pretty_assertions::assert_eq;
|
|
use serde_json::Value;
|
|
use serde_json::json;
|
|
|
|
#[allow(clippy::expect_used)]
|
|
async fn assert_output(source: &str, expected: Value) {
|
|
let session = InProcessCodeModeSession::new();
|
|
let started = session
|
|
.execute(
|
|
ExecuteRequest {
|
|
tool_call_id: "settled".to_string(),
|
|
enabled_tools: Vec::new(),
|
|
source: source.to_string(),
|
|
yield_time_ms: Some(/*value*/ 5_000),
|
|
max_output_tokens: None,
|
|
},
|
|
Arc::new(NoopCodeModeSessionDelegate),
|
|
/*preempt*/ None,
|
|
)
|
|
.await
|
|
.expect("start cell");
|
|
let response = started.initial_response().await.expect("execute cell");
|
|
let RuntimeResponse::Result {
|
|
content_items,
|
|
error_text: None,
|
|
..
|
|
} = response
|
|
else {
|
|
panic!("unexpected cell response: {response:?}");
|
|
};
|
|
let actual: Vec<Value> = content_items
|
|
.into_iter()
|
|
.map(|item| {
|
|
let FunctionCallOutputContentItem::InputText { text } = item else {
|
|
panic!("unexpected output: {item:?}");
|
|
};
|
|
serde_json::from_str(&text).expect("JSON output")
|
|
})
|
|
.collect();
|
|
assert_eq!(actual, vec![expected]);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn preserves_settlement_order_before_and_during_iteration() {
|
|
assert_output(
|
|
r#"
|
|
const resolve = [];
|
|
const results = as_settled(Array.from({length: 4}, (_, index) =>
|
|
new Promise(done => { resolve[index] = done; })));
|
|
resolve[2]("two");
|
|
await Promise.resolve();
|
|
resolve[0]("zero");
|
|
await Promise.resolve();
|
|
const seen = [];
|
|
for await (const result of results) {
|
|
seen.push(result);
|
|
if (seen.length === 1) {
|
|
resolve[3]("three");
|
|
resolve[1]("one");
|
|
await Promise.resolve();
|
|
}
|
|
}
|
|
text(seen);
|
|
"#,
|
|
json!([
|
|
{"index": 2, "status": "fulfilled", "value": "two"},
|
|
{"index": 0, "status": "fulfilled", "value": "zero"},
|
|
{"index": 3, "status": "fulfilled", "value": "three"},
|
|
{"index": 1, "status": "fulfilled", "value": "one"},
|
|
]),
|
|
)
|
|
.await;
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn accepts_iterables_values_thenables_and_duplicate_promises() {
|
|
assert_output(
|
|
r#"
|
|
const shared = Promise.resolve("shared");
|
|
function* inputs() {
|
|
yield 42;
|
|
yield {then(resolve) { resolve("thenable"); }};
|
|
yield shared;
|
|
yield shared;
|
|
yield Promise.reject({message: "failed"});
|
|
yield {get then() { throw "getter failed"; }};
|
|
}
|
|
const seen = [];
|
|
for await (const result of as_settled(inputs())) seen.push(result);
|
|
text(seen.sort((a, b) => a.index - b.index));
|
|
"#,
|
|
json!([
|
|
{"index": 0, "status": "fulfilled", "value": 42},
|
|
{"index": 1, "status": "fulfilled", "value": "thenable"},
|
|
{"index": 2, "status": "fulfilled", "value": "shared"},
|
|
{"index": 3, "status": "fulfilled", "value": "shared"},
|
|
{"index": 4, "status": "rejected", "reason": {"message": "failed"}},
|
|
{"index": 5, "status": "rejected", "reason": "getter failed"},
|
|
]),
|
|
)
|
|
.await;
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn preserves_map_keys_and_completes_empty_inputs() {
|
|
assert_output(
|
|
r#"
|
|
const seen = [];
|
|
await stream_settled([], result => seen.push(result));
|
|
for await (const result of as_settled(new Map())) seen.push(result);
|
|
await stream_settled(new Map([
|
|
["success", 7],
|
|
["failure", Promise.reject("failed")],
|
|
]), result => seen.push(result));
|
|
text(seen);
|
|
"#,
|
|
json!([
|
|
{"index": "success", "status": "fulfilled", "value": 7},
|
|
{"index": "failure", "status": "rejected", "reason": "failed"},
|
|
]),
|
|
)
|
|
.await;
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn awaits_each_callback_including_rejected_inputs() {
|
|
assert_output(
|
|
r#"
|
|
const seen = [];
|
|
await stream_settled([Promise.reject("failed"), 2], async result => {
|
|
seen.push(result);
|
|
await new Promise(resolve => setTimeout(resolve, 1));
|
|
seen.push(["callback finished", result.index]);
|
|
});
|
|
seen.push("stream finished");
|
|
text(seen);
|
|
"#,
|
|
json!([
|
|
{"index": 0, "status": "rejected", "reason": "failed"},
|
|
["callback finished", 0],
|
|
{"index": 1, "status": "fulfilled", "value": 2},
|
|
["callback finished", 1],
|
|
"stream finished",
|
|
]),
|
|
)
|
|
.await;
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn propagates_callback_errors_and_closes_early_iterations() {
|
|
assert_output(
|
|
r#"
|
|
const seen = [];
|
|
const failure = new Error("callback failed");
|
|
try {
|
|
await stream_settled([1, 2], async result => {
|
|
seen.push(result);
|
|
await new Promise(resolve => setTimeout(resolve, 1));
|
|
throw failure;
|
|
});
|
|
} catch (error) {
|
|
seen.push(error === failure);
|
|
}
|
|
const results = as_settled([3, new Promise(() => {}), Promise.reject("unconsumed")]);
|
|
for await (const result of results) {
|
|
seen.push(result);
|
|
break;
|
|
}
|
|
seen.push(await results.next());
|
|
text(seen);
|
|
"#,
|
|
json!([
|
|
{"index": 0, "status": "fulfilled", "value": 1},
|
|
true,
|
|
{"index": 0, "status": "fulfilled", "value": 3},
|
|
{"done": true},
|
|
]),
|
|
)
|
|
.await;
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn preserves_identity_of_keys_values_and_rejections() {
|
|
assert_output(
|
|
r#"
|
|
const key = {};
|
|
const value = {method() { return 7; }};
|
|
value.self = value;
|
|
const failure = new Error("failed");
|
|
const seen = [];
|
|
await stream_settled(new Map([
|
|
[key, Promise.resolve(value)],
|
|
[failure, Promise.reject(failure)],
|
|
]), result => {
|
|
seen.push(result.status === "fulfilled"
|
|
? result.index === key && result.value === value && result.value.self === value
|
|
&& result.value.method() === 7
|
|
: result.index === failure && result.reason === failure);
|
|
});
|
|
text(seen);
|
|
"#,
|
|
json!([true, true]),
|
|
)
|
|
.await;
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn terminates_helpers_waiting_for_unsettled_inputs() {
|
|
let session = InProcessCodeModeSession::new();
|
|
for source in [
|
|
"for await (const result of as_settled([new Promise(() => {})])) text(result);",
|
|
"await stream_settled([new Promise(() => {})], text);",
|
|
] {
|
|
let response = session
|
|
.execute_to_pending(
|
|
ExecuteRequest {
|
|
tool_call_id: "settled".to_string(),
|
|
enabled_tools: Vec::new(),
|
|
source: source.to_string(),
|
|
yield_time_ms: None,
|
|
max_output_tokens: None,
|
|
},
|
|
Arc::new(NoopCodeModeSessionDelegate),
|
|
)
|
|
.await
|
|
.expect("execute to pending");
|
|
let ExecuteToPendingOutcome::Pending { cell_id, .. } = response else {
|
|
panic!("expected pending helper: {response:?}");
|
|
};
|
|
assert_eq!(
|
|
tokio::time::timeout(
|
|
Duration::from_secs(/*secs*/ 5),
|
|
session.terminate(cell_id.clone())
|
|
)
|
|
.await
|
|
.expect("termination timed out")
|
|
.expect("terminate cell"),
|
|
WaitOutcome::LiveCell(RuntimeResponse::Terminated {
|
|
cell_id,
|
|
content_items: Vec::new(),
|
|
code_mode_host_duration: None,
|
|
}),
|
|
);
|
|
}
|
|
}
|