Files
codex/codex-rs/code-mode-host/tests/host.rs
2026-06-21 07:43:09 +00:00

685 lines
20 KiB
Rust

#![allow(clippy::expect_used)]
use std::process::Stdio;
use std::time::Duration;
use codex_code_mode_protocol::wire::*;
use pretty_assertions::assert_eq;
use serde_json::json;
use tokio::process::Child;
use tokio::process::ChildStdin;
use tokio::process::Command;
use tokio::sync::mpsc;
struct HostHarness {
child: Child,
stdin: Option<ChildStdin>,
messages_rx: mpsc::UnboundedReceiver<Result<HostMessage, String>>,
}
impl HostHarness {
fn spawn() -> Self {
let mut child = Command::new(
codex_utils_cargo_bin::cargo_bin("codex-code-mode-host").expect("resolve host binary"),
)
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::inherit())
.kill_on_drop(true)
.spawn()
.expect("spawn host");
let stdin = child.stdin.take().expect("host stdin");
let mut stdout = child.stdout.take().expect("host stdout");
let (messages_tx, messages_rx) = mpsc::unbounded_channel();
tokio::spawn(async move {
loop {
match read_frame(&mut stdout).await {
Ok(Some(message)) => {
if messages_tx.send(Ok(message)).is_err() {
break;
}
}
Ok(None) => break,
Err(err) => {
let _ = messages_tx.send(Err(err.to_string()));
break;
}
}
}
});
Self {
child,
stdin: Some(stdin),
messages_rx,
}
}
async fn send(&mut self, message: ClientMessage) {
write_frame(self.stdin.as_mut().expect("host stdin open"), &message)
.await
.expect("write host message");
}
async fn recv(&mut self) -> HostMessage {
tokio::time::timeout(Duration::from_secs(5), self.messages_rx.recv())
.await
.expect("host message timeout")
.expect("host output closed")
.expect("read host message")
}
async fn assert_no_message(&mut self, duration: Duration) {
assert!(
tokio::time::timeout(duration, self.messages_rx.recv())
.await
.is_err(),
"received an unexpected host message"
);
}
async fn create_session(&mut self) -> SessionId {
self.send(ClientMessage::Request {
id: 1,
request: HostRequest::CreateSession,
})
.await;
match self.recv().await {
HostMessage::Response {
id: 1,
result:
WireResult::Ok {
value: HostResponse::SessionCreated { session_id },
},
} => session_id,
message => panic!("unexpected create-session response: {message:?}"),
}
}
async fn shutdown_session(&mut self, session_id: SessionId, request_id: RequestId) {
self.send(ClientMessage::Request {
id: request_id,
request: HostRequest::ShutdownSession { session_id },
})
.await;
assert_eq!(
self.recv().await,
HostMessage::Response {
id: request_id,
result: WireResult::Ok {
value: HostResponse::SessionShutdown,
},
}
);
}
async fn finish(mut self) {
self.stdin.take();
let status = tokio::time::timeout(Duration::from_secs(5), self.child.wait())
.await
.expect("host exit timeout")
.expect("wait for host");
assert!(status.success(), "host exited with {status}");
}
}
fn create_cell_request(source: &str) -> CreateCellRequest {
CreateCellRequest {
tool_call_id: "call-1".to_string(),
enabled_tools: Vec::new(),
source: source.to_string(),
}
}
async fn create_cell(
host: &mut HostHarness,
session_id: SessionId,
request_id: RequestId,
request: CreateCellRequest,
) -> CellId {
host.send(ClientMessage::Request {
id: request_id,
request: HostRequest::CreateCell {
session_id,
request,
},
})
.await;
match host.recv().await {
HostMessage::Response {
id,
result:
WireResult::Ok {
value: HostResponse::CellCreated { cell_id },
},
} if id == request_id => cell_id,
message => panic!("unexpected create-cell response: {message:?}"),
}
}
async fn observe_cell(
host: &mut HostHarness,
session_id: SessionId,
request_id: RequestId,
cell_id: CellId,
mode: ObserveMode,
) {
host.send(ClientMessage::Request {
id: request_id,
request: HostRequest::Observe {
session_id,
cell_id,
mode,
},
})
.await;
}
async fn recv_observation(host: &mut HostHarness, request_id: RequestId) -> CellEvent {
match host.recv().await {
HostMessage::Response {
id,
result:
WireResult::Ok {
value: HostResponse::Observed { event },
},
} if id == request_id => event,
message => panic!("unexpected observation response: {message:?}"),
}
}
async fn recv_terminal_observation(
host: &mut HostHarness,
session_id: SessionId,
request_id: RequestId,
cell_id: &CellId,
) -> CellEvent {
let mut event = None;
let mut cell_closed = false;
for _ in 0..2 {
match host.recv().await {
HostMessage::Response {
id,
result:
WireResult::Ok {
value: HostResponse::Observed { event: response },
},
} if id == request_id => event = Some(response),
HostMessage::CellClosed {
session_id: closed_session_id,
cell_id: closed_cell_id,
} => {
assert_eq!(closed_session_id, session_id);
assert_eq!(&closed_cell_id, cell_id);
cell_closed = true;
}
message => panic!("unexpected terminal observation message: {message:?}"),
}
}
assert!(cell_closed);
event.expect("terminal observation response")
}
#[tokio::test]
async fn yields_resumes_and_closes_over_stdio() {
let mut host = HostHarness::spawn();
let session_id = host.create_session().await;
let cell_id = create_cell(
&mut host,
session_id,
2,
create_cell_request(concat!(
"await new Promise(resolve => setTimeout(resolve, 100));",
r#"text("before"); yield_control(); text("after");"#,
)),
)
.await;
observe_cell(
&mut host,
session_id,
3,
cell_id.clone(),
ObserveMode::YieldAfter {
duration_ms: 60_000,
},
)
.await;
let mut yielded = None;
let mut saw_cell_closed = false;
for _ in 0..2 {
match host.recv().await {
HostMessage::Response {
id: 3,
result:
WireResult::Ok {
value: HostResponse::Observed { event },
},
} => yielded = Some(event),
HostMessage::CellClosed {
session_id: closed_session_id,
cell_id: closed_cell_id,
} => {
assert_eq!(closed_session_id, session_id);
assert_eq!(closed_cell_id, cell_id);
saw_cell_closed = true;
}
message => panic!("unexpected initial observation message: {message:?}"),
}
}
assert_eq!(
yielded,
Some(CellEvent::Yielded {
content_items: vec![OutputItem::Text {
text: "before".to_string(),
}],
})
);
assert!(saw_cell_closed);
observe_cell(
&mut host,
session_id,
4,
cell_id.clone(),
ObserveMode::YieldAfter { duration_ms: 1 },
)
.await;
assert_eq!(
recv_observation(&mut host, 4).await,
CellEvent::Completed {
content_items: vec![OutputItem::Text {
text: "after".to_string(),
}],
error_text: None,
}
);
host.shutdown_session(session_id, 5).await;
host.finish().await;
}
#[tokio::test]
async fn termination_resolves_an_active_observer() {
let mut host = HostHarness::spawn();
let session_id = host.create_session().await;
let cell_id = create_cell(
&mut host,
session_id,
2,
create_cell_request("await new Promise(() => {});"),
)
.await;
observe_cell(
&mut host,
session_id,
3,
cell_id.clone(),
ObserveMode::YieldAfter {
duration_ms: 60_000,
},
)
.await;
host.assert_no_message(Duration::from_millis(100)).await;
host.send(ClientMessage::Request {
id: 4,
request: HostRequest::Terminate {
session_id,
cell_id: cell_id.clone(),
},
})
.await;
let mut saw_observation_response = false;
let mut saw_termination_response = false;
let mut saw_cell_closed = false;
for _ in 0..3 {
match host.recv().await {
HostMessage::Response {
id: 3,
result:
WireResult::Ok {
value:
HostResponse::Observed {
event: CellEvent::Terminated { content_items },
},
},
} => {
assert!(content_items.is_empty());
saw_observation_response = true;
}
HostMessage::Response {
id: 4,
result:
WireResult::Ok {
value:
HostResponse::Observed {
event: CellEvent::Terminated { content_items },
},
},
} => {
assert!(content_items.is_empty());
saw_termination_response = true;
}
HostMessage::CellClosed {
session_id: closed_session_id,
cell_id: closed_cell_id,
} => {
assert_eq!(closed_session_id, session_id);
assert_eq!(closed_cell_id, cell_id);
saw_cell_closed = true;
}
message => panic!("unexpected termination message: {message:?}"),
}
}
assert!(saw_observation_response);
assert!(saw_termination_response);
assert!(saw_cell_closed);
host.shutdown_session(session_id, 5).await;
host.finish().await;
}
#[tokio::test]
async fn forwards_tool_and_notification_callbacks() {
let mut host = HostHarness::spawn();
let session_id = host.create_session().await;
let mut request = create_cell_request(
r#"
notify("note");
const value = await tools.echo({ value: "input" });
text(value.value);
"#,
);
request.enabled_tools.push(ToolDefinition {
name: "echo".to_string(),
tool_name: ToolName {
name: "echo".to_string(),
namespace: None,
},
description: String::new(),
kind: ToolKind::Function,
});
let cell_id = create_cell(&mut host, session_id, 2, request).await;
observe_cell(
&mut host,
session_id,
3,
cell_id.clone(),
ObserveMode::YieldAfter {
duration_ms: 60_000,
},
)
.await;
for _ in 0..2 {
match host.recv().await {
HostMessage::CallbackRequest {
id,
session_id: callback_session_id,
request: CallbackRequest::Notify { text, .. },
} => {
assert_eq!(callback_session_id, session_id);
assert_eq!(text, "note");
host.send(ClientMessage::CallbackResponse {
id,
response: CallbackResponse::NotificationDelivered,
})
.await;
}
HostMessage::CallbackRequest {
id,
session_id: callback_session_id,
request: CallbackRequest::InvokeTool { invocation },
} => {
assert_eq!(callback_session_id, session_id);
assert_eq!(invocation.input, Some(json!({"value": "input"})));
host.send(ClientMessage::CallbackResponse {
id,
response: CallbackResponse::ToolResult {
result: json!({"value": "output"}),
},
})
.await;
}
message => panic!("unexpected callback message: {message:?}"),
}
}
assert_eq!(
recv_terminal_observation(&mut host, session_id, 3, &cell_id).await,
CellEvent::Completed {
content_items: vec![OutputItem::Text {
text: "output".to_string(),
}],
error_text: None,
}
);
host.shutdown_session(session_id, 4).await;
host.finish().await;
}
#[tokio::test]
async fn pending_frontier_rejects_a_second_observer_and_termination_preempts_the_first() {
let mut host = HostHarness::spawn();
let session_id = host.create_session().await;
let cell_id = create_cell(
&mut host,
session_id,
2,
create_cell_request("await new Promise(() => {});"),
)
.await;
observe_cell(
&mut host,
session_id,
3,
cell_id.clone(),
ObserveMode::PendingFrontier,
)
.await;
assert_eq!(
recv_observation(&mut host, 3).await,
CellEvent::Pending {
content_items: Vec::new(),
pending_tool_call_ids: Vec::new(),
}
);
host.send(ClientMessage::Request {
id: 4,
request: HostRequest::Observe {
session_id,
cell_id: cell_id.clone(),
mode: ObserveMode::YieldAfter {
duration_ms: 60_000,
},
},
})
.await;
host.assert_no_message(Duration::from_millis(100)).await;
host.send(ClientMessage::Request {
id: 5,
request: HostRequest::Observe {
session_id,
cell_id: cell_id.clone(),
mode: ObserveMode::YieldAfter {
duration_ms: 60_000,
},
},
})
.await;
assert_eq!(
host.recv().await,
HostMessage::Response {
id: 5,
result: WireResult::Err {
error: Error::BusyObserver {
cell_id: cell_id.clone(),
},
},
}
);
host.send(ClientMessage::Request {
id: 6,
request: HostRequest::Terminate {
session_id,
cell_id: cell_id.clone(),
},
})
.await;
let terminated = HostResponse::Observed {
event: CellEvent::Terminated {
content_items: Vec::new(),
},
};
let mut response_ids = Vec::new();
let mut saw_cell_closed = false;
for _ in 0..3 {
match host.recv().await {
HostMessage::Response {
id,
result: WireResult::Ok { value },
} => {
assert_eq!(value, terminated);
response_ids.push(id);
}
HostMessage::CellClosed {
session_id: closed_session_id,
cell_id: closed_cell_id,
} => {
assert_eq!(closed_session_id, session_id);
assert_eq!(closed_cell_id, cell_id);
saw_cell_closed = true;
}
message => panic!("unexpected termination response: {message:?}"),
}
}
response_ids.sort_unstable();
assert_eq!(response_ids, vec![4, 6]);
assert!(saw_cell_closed);
host.shutdown_session(session_id, 7).await;
host.finish().await;
}
#[tokio::test]
async fn shutdown_cancels_callbacks_before_acknowledging_the_session() {
let mut host = HostHarness::spawn();
let session_id = host.create_session().await;
let mut request = create_cell_request("await tools.echo({});");
request.enabled_tools.push(ToolDefinition {
name: "echo".to_string(),
tool_name: ToolName {
name: "echo".to_string(),
namespace: None,
},
description: String::new(),
kind: ToolKind::Function,
});
let cell_id = create_cell(&mut host, session_id, 2, request).await;
observe_cell(
&mut host,
session_id,
3,
cell_id.clone(),
ObserveMode::YieldAfter {
duration_ms: 60_000,
},
)
.await;
let callback_id = match host.recv().await {
HostMessage::CallbackRequest {
id,
request: CallbackRequest::InvokeTool { .. },
..
} => id,
message => panic!("unexpected callback message: {message:?}"),
};
host.send(ClientMessage::Request {
id: 4,
request: HostRequest::ShutdownSession { session_id },
})
.await;
let mut saw_callback_cancellation = false;
let mut saw_terminated_response = false;
let mut saw_shutdown_response = false;
let mut saw_cell_closed = false;
for _ in 0..4 {
match host.recv().await {
HostMessage::CancelCallback { id } => {
assert_eq!(id, callback_id);
saw_callback_cancellation = true;
}
HostMessage::Response {
id: 3,
result:
WireResult::Ok {
value:
HostResponse::Observed {
event: CellEvent::Terminated { content_items },
},
},
} => {
assert!(content_items.is_empty());
saw_terminated_response = true;
}
HostMessage::Response {
id: 4,
result:
WireResult::Ok {
value: HostResponse::SessionShutdown,
},
} => saw_shutdown_response = true,
HostMessage::CellClosed {
session_id: closed_session_id,
cell_id: closed_cell_id,
} => {
assert_eq!(closed_session_id, session_id);
assert_eq!(closed_cell_id, cell_id);
saw_cell_closed = true;
}
message => panic!("unexpected shutdown message: {message:?}"),
}
}
assert!(saw_callback_cancellation);
assert!(saw_terminated_response);
assert!(saw_shutdown_response);
assert!(saw_cell_closed);
host.finish().await;
}
#[tokio::test]
async fn disconnect_exits_with_a_pending_callback() {
let mut host = HostHarness::spawn();
let session_id = host.create_session().await;
let mut request = create_cell_request("await tools.echo({});");
request.enabled_tools.push(ToolDefinition {
name: "echo".to_string(),
tool_name: ToolName {
name: "echo".to_string(),
namespace: None,
},
description: String::new(),
kind: ToolKind::Function,
});
let cell_id = create_cell(&mut host, session_id, 2, request).await;
observe_cell(
&mut host,
session_id,
3,
cell_id,
ObserveMode::YieldAfter {
duration_ms: 60_000,
},
)
.await;
assert!(matches!(
host.recv().await,
HostMessage::CallbackRequest {
request: CallbackRequest::InvokeTool { .. },
..
}
));
host.finish().await;
}