use std::collections::VecDeque;
use std::sync::{Arc, Mutex};
use std::time::Duration;
use futures_util::{SinkExt, StreamExt};
use serde_json::{Value, json};
use tokio::net::{TcpListener, TcpStream};
use tokio::task::JoinHandle;
use tokio_tungstenite::tungstenite::handshake::server::{
Request as WsHandshakeRequest, Response as WsHandshakeResponse,
};
use tokio_tungstenite::tungstenite::protocol::Message as WsMessage;
use tokio_tungstenite::{WebSocketStream, accept_hdr_async};
pub fn assistant_item(message_id: &str, text: &str) -> Value {
json!({
"type": "message",
"id": message_id,
"role": "assistant",
"status": "completed",
"phase": "final_answer",
"content": [{"type": "output_text", "text": text, "annotations": []}]
})
}
pub fn function_call_item(call_id: &str, tool_name: &str, arguments: &str) -> Value {
json!({
"type": "function_call",
"id": format!("fc_{call_id}"),
"call_id": call_id,
"name": tool_name,
"arguments": arguments,
"status": "completed"
})
}
#[derive(Clone, Debug)]
pub enum ScriptedWsAction {
Complete {
response_id: &'static str,
message_id: &'static str,
text: &'static str,
},
CompleteAndClose {
response_id: &'static str,
message_id: &'static str,
text: &'static str,
},
ToolCall {
response_id: &'static str,
call_id: &'static str,
tool_name: &'static str,
arguments: &'static str,
},
Incomplete {
response_id: &'static str,
message_id: &'static str,
text: &'static str,
},
Error { message: &'static str },
MidStreamError {
message_id: &'static str,
text: &'static str,
message: &'static str,
},
CloseAfterStart {
response_id: &'static str,
message_id: &'static str,
text: &'static str,
},
IdleBeforeStart,
IdleAfterStart {
message_id: &'static str,
text: &'static str,
},
}
pub type CapturedHandshakes = Arc<Mutex<Vec<Vec<(String, String)>>>>;
pub struct ScriptedWsServer {
pub url: String,
captured: Arc<Mutex<Vec<Value>>>,
captured_raw: Arc<Mutex<Vec<Vec<u8>>>>,
handshakes: CapturedHandshakes,
close_frames: Arc<Mutex<u32>>,
task: JoinHandle<()>,
}
impl ScriptedWsServer {
pub fn captured(&self) -> Vec<Value> {
self.captured
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.clone()
}
pub fn captured_raw(&self) -> Vec<Vec<u8>> {
self.captured_raw
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.clone()
}
pub fn handshakes(&self) -> Vec<Vec<(String, String)>> {
self.handshakes
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.clone()
}
pub fn close_frame_count(&self) -> u32 {
*self
.close_frames
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
}
}
impl Drop for ScriptedWsServer {
fn drop(&mut self) {
self.task.abort();
}
}
pub async fn spawn_scripted_websocket(actions: Vec<ScriptedWsAction>) -> ScriptedWsServer {
let listener = TcpListener::bind(("127.0.0.1", 0)).await.expect("bind ws");
let addr = listener.local_addr().expect("ws addr");
let actions = Arc::new(Mutex::new(VecDeque::from(actions)));
let captured = Arc::new(Mutex::new(Vec::new()));
let captured_raw = Arc::new(Mutex::new(Vec::new()));
let handshakes = Arc::new(Mutex::new(Vec::new()));
let close_frames = Arc::new(Mutex::new(0u32));
let task_actions = Arc::clone(&actions);
let task_captured = Arc::clone(&captured);
let task_captured_raw = Arc::clone(&captured_raw);
let task_handshakes = Arc::clone(&handshakes);
let task_close_frames = Arc::clone(&close_frames);
let task = tokio::spawn(async move {
loop {
let Ok((stream, _)) = listener.accept().await else {
break;
};
let actions = Arc::clone(&task_actions);
let captured = Arc::clone(&task_captured);
let captured_raw = Arc::clone(&task_captured_raw);
let handshakes = Arc::clone(&task_handshakes);
let close_frames = Arc::clone(&task_close_frames);
tokio::spawn(async move {
#[expect(
clippy::result_large_err,
reason = "tungstenite fixes the handshake callback error to an HTTP response"
)]
let callback = move |request: &WsHandshakeRequest,
response: WsHandshakeResponse| {
let headers = request
.headers()
.iter()
.filter_map(|(name, value)| {
value
.to_str()
.ok()
.map(|value| (name.as_str().to_string(), value.to_string()))
})
.collect::<Vec<_>>();
handshakes
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.push(headers);
Ok(response)
};
let Ok(mut ws) = accept_hdr_async(stream, callback).await else {
return;
};
while let Some(Ok(message)) = ws.next().await {
let text = match message {
WsMessage::Text(text) => text.to_string(),
WsMessage::Binary(bytes) => {
String::from_utf8(bytes.to_vec()).unwrap_or_default()
}
WsMessage::Close(_) => {
*close_frames
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner) += 1;
break;
}
WsMessage::Ping(_) | WsMessage::Pong(_) | WsMessage::Frame(_) => {
continue;
}
};
captured_raw
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.push(text.as_bytes().to_vec());
let request: Value = serde_json::from_str(&text).expect("ws request json");
captured
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.push(request);
let action = actions
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.pop_front()
.expect("scripted ws action");
match action {
ScriptedWsAction::Complete {
response_id,
message_id,
text,
} => {
send_completed_ws_response(&mut ws, response_id, message_id, text)
.await;
}
ScriptedWsAction::CompleteAndClose {
response_id,
message_id,
text,
} => {
send_completed_ws_response(&mut ws, response_id, message_id, text)
.await;
let _ = ws.close(None).await;
break;
}
ScriptedWsAction::ToolCall {
response_id,
call_id,
tool_name,
arguments,
} => {
send_tool_call_ws_response(
&mut ws,
response_id,
call_id,
tool_name,
arguments,
)
.await;
}
ScriptedWsAction::Incomplete {
response_id,
message_id,
text,
} => {
send_incomplete_ws_response(&mut ws, response_id, message_id, text)
.await;
}
ScriptedWsAction::Error { message } => {
send_ws_json(
&mut ws,
json!({"type":"error","error":{"message": message}}),
)
.await;
}
ScriptedWsAction::MidStreamError {
message_id,
text,
message,
} => {
send_ws_json(
&mut ws,
json!({"type":"response.output_item.added","output_index":0,"item":{"type":"message","id":message_id,"status":"in_progress","phase":"final_answer","content":[]}}),
)
.await;
send_ws_json(
&mut ws,
json!({"type":"response.output_text.delta","output_index":0,"item_id":message_id,"delta":text}),
)
.await;
send_ws_json(
&mut ws,
json!({"type":"error","error":{"message": message}}),
)
.await;
}
ScriptedWsAction::CloseAfterStart {
response_id,
message_id,
text,
} => {
send_ws_json(
&mut ws,
json!({"type":"response.created","response":{"id":response_id,"status":"in_progress","usage":{"input_tokens":4,"output_tokens":1,"total_tokens":5}}}),
)
.await;
send_ws_json(
&mut ws,
json!({"type":"response.output_item.added","output_index":0,"item":{"type":"message","id":message_id,"status":"in_progress","phase":"final_answer","content":[]}}),
)
.await;
send_ws_json(
&mut ws,
json!({"type":"response.output_text.delta","output_index":0,"item_id":message_id,"delta":text}),
)
.await;
let _ = ws.close(None).await;
break;
}
ScriptedWsAction::IdleBeforeStart => {
tokio::time::sleep(Duration::from_secs(60)).await;
}
ScriptedWsAction::IdleAfterStart { message_id, text } => {
send_ws_json(
&mut ws,
json!({"type":"response.output_item.added","output_index":0,"item":{"type":"message","id":message_id,"status":"in_progress","phase":"final_answer","content":[]}}),
)
.await;
send_ws_json(
&mut ws,
json!({"type":"response.output_text.delta","output_index":0,"item_id":message_id,"delta":text}),
)
.await;
tokio::time::sleep(Duration::from_secs(60)).await;
}
}
}
});
}
});
ScriptedWsServer {
url: format!("ws://{addr}/codex/responses"),
captured,
captured_raw,
handshakes,
close_frames,
task,
}
}
async fn send_ws_json(ws: &mut WebSocketStream<TcpStream>, value: Value) {
ws.send(WsMessage::Text(value.to_string().into()))
.await
.expect("send ws event");
}
async fn send_completed_ws_response(
ws: &mut WebSocketStream<TcpStream>,
response_id: &str,
message_id: &str,
text: &str,
) {
let item = assistant_item(message_id, text);
send_ws_json(
ws,
json!({"type":"response.output_item.added","output_index":0,"item":{"type":"message","id":message_id,"status":"in_progress","phase":"final_answer","content":[]}}),
)
.await;
send_ws_json(
ws,
json!({"type":"response.output_text.delta","output_index":0,"item_id":message_id,"delta":text}),
)
.await;
send_ws_json(
ws,
json!({"type":"response.output_item.done","output_index":0,"item":item}),
)
.await;
send_ws_json(
ws,
json!({"type":"response.completed","response":{"id":response_id,"status":"completed","output":[assistant_item(message_id, text)],"usage":{"input_tokens":1,"output_tokens":1,"total_tokens":2}}}),
)
.await;
}
async fn send_tool_call_ws_response(
ws: &mut WebSocketStream<TcpStream>,
response_id: &str,
call_id: &str,
tool_name: &str,
arguments: &str,
) {
let item = function_call_item(call_id, tool_name, arguments);
send_ws_json(
ws,
json!({"type":"response.output_item.added","output_index":0,"item":item.clone()}),
)
.await;
send_ws_json(
ws,
json!({"type":"response.output_item.done","output_index":0,"item":item.clone()}),
)
.await;
send_ws_json(
ws,
json!({"type":"response.completed","response":{"id":response_id,"status":"completed","output":[item],"usage":{"input_tokens":1,"output_tokens":1,"total_tokens":2}}}),
)
.await;
}
async fn send_incomplete_ws_response(
ws: &mut WebSocketStream<TcpStream>,
response_id: &str,
message_id: &str,
text: &str,
) {
let item = assistant_item(message_id, text);
send_ws_json(
ws,
json!({"type":"response.output_item.done","output_index":0,"item":item}),
)
.await;
send_ws_json(
ws,
json!({"type":"response.completed","response":{"id":response_id,"status":"incomplete","incomplete_details":{"reason":"max_output_tokens"},"output":[assistant_item(message_id, text)],"usage":{"input_tokens":1,"output_tokens":1,"total_tokens":2}}}),
)
.await;
}