From 36162aefaf194568cadaa100358dcd42e81170fa Mon Sep 17 00:00:00 2001 From: zyycn Date: Mon, 15 Jun 2026 13:22:43 +0800 Subject: [PATCH] codex: add websocket golden Co-authored-by: Codex --- docs/OPENAI_CHAIN_PARITY_REAUDIT.md | 6 ++-- tests/codex_gateway/websocket.rs | 29 ++++++++++--------- .../responses/golden/websocket_completed.sse | 2 ++ 3 files changed, 22 insertions(+), 15 deletions(-) create mode 100644 tests/fixtures/responses/golden/websocket_completed.sse diff --git a/docs/OPENAI_CHAIN_PARITY_REAUDIT.md b/docs/OPENAI_CHAIN_PARITY_REAUDIT.md index 27f4b8b87..be22e7cef 100644 --- a/docs/OPENAI_CHAIN_PARITY_REAUDIT.md +++ b/docs/OPENAI_CHAIN_PARITY_REAUDIT.md @@ -379,6 +379,7 @@ Rust 证据: - 本轮新增 `tests/fixtures/responses/websocket/previous_response_not_found.json`、`tests/fixtures/responses/websocket/unanswered_function_call.json`,并用 `tests/codex_serving/responses_websocket.rs` 覆盖 non-streaming 与 streaming `previous_response_not_found`、non-streaming unanswered function call 的同账号 strip-and-retry。 - `tests/codex_gateway/usage_events.rs` 覆盖 multiline data、非标准 JSON 续行、`[DONE]` 忽略和非 SSE JSON body 转 `non_sse_response`。 - `tests/codex_gateway/websocket.rs:376-760` 覆盖 WS 内部 rate-limit event 不转发、首个 rotatable error、mid-stream close error 和 fallback。 +- 本轮新增 `tests/fixtures/responses/golden/websocket_completed.sse`,并在 `tests/codex_gateway/websocket.rs::ordinary_response_should_use_websocket_transport_by_default` 中对 WebSocket `response.completed` JSON frame 转下游 SSE chunk 做 exact golden 断言,锁定 `event:`、`data:` 和 SSE 终止分隔符格式。 - 本轮修正后,HTTP SSE streaming 在上游正常 EOF 但缺少 `response.completed`/`response.failed`/`error` 时,会给客户端追加原版格式的 `response.failed`,错误码为 `stream_disconnected`;`tests/codex_serving/upstream_errors.rs::v1_responses_stream_should_synthesize_response_failed_when_http_sse_closes_before_terminal` 覆盖该行为。 - 本轮新增 `tests/fixtures/responses/golden/stream_premature_close.sse`,并在 `tests/codex_serving/upstream_errors.rs::v1_responses_stream_should_synthesize_response_failed_when_http_sse_closes_before_terminal` 中对 HTTP SSE premature close 的完整下游 SSE 输出做 exact golden 断言;动态 `resp_proxy_*` id 在测试侧 redaction 后比较。 - 本轮修正后,WebSocket streaming 在首个可见 frame 已发送后遇到 `ClosedBeforeTerminal` 或其它未见 terminal 的 body stream 错误时,也会追加同样的 `response.failed/stream_disconnected`,不再把 body stream error 直接暴露给客户端;`tests/codex_serving/responses_websocket.rs::v1_responses_websocket_stream_should_synthesize_response_failed_when_closed_before_terminal` 覆盖该行为。 @@ -388,6 +389,7 @@ Rust 证据: 已对齐: - 标准 SSE 事件解析、WS JSON frame 到 SSE chunk 的基本转换、terminal event 判断、`codex.rate_limits` 内部事件过滤、heartbeat 15 秒节奏、usage 提取、function call ids 提取、空响应重试等主路径已经具备。 +- WebSocket `response.completed` frame 到 SSE chunk 已有 exact golden 覆盖,确认 raw WS JSON 按原版对下游表现为 `event: response.completed` + 单个 `data:` JSON frame。 - SSE parser 已对齐原版的三个容错点:漂亮打印 JSON 续行、`[DONE]` 忽略、非 SSE body 合成 `error/non_sse_response`。 - streaming 路径对客户端保持 SSE 格式,HTTP SSE 与 WebSocket 两种上游传输都能以 SSE 返回给下游。 - `response.failed`/`error` 在 streaming 路径会被透传并写入审计;non-streaming 路径也能识别为上游失败,并已对 429/402/403 进入账号状态机和 fallback。 @@ -401,10 +403,10 @@ Rust 证据: - Responses pooled path 的非流式最终错误外壳已按原版 `PASSTHROUGH_FORMAT` 对齐:普通上游错误为 `codex_api_error`,429 为 `rate_limit_exceeded`,无账号为 `no_available_accounts`;compact 路径复用同一 Responses 外壳。 未完全对齐: -- response/SSE/WS 转换链路仍缺完整 golden fixture 矩阵。主恢复集合已覆盖 429/402/403、401 token invalid、model unsupported、`previous_response_not_found`/unanswered function call、premature close 和最终错误外壳;本轮已先补 HTTP SSE premature close exact golden,但还需要更系统的 golden tests 防止格式细节回退。 +- response/SSE/WS 转换链路仍缺完整 golden fixture 矩阵。主恢复集合已覆盖 429/402/403、401 token invalid、model unsupported、`previous_response_not_found`/unanswered function call、premature close、WS completed frame 和最终错误外壳;本轮已补 HTTP SSE premature close 与 WS completed exact golden,但还需要更系统的 golden tests 防止格式细节回退。 缺口/后续动作: -- 增加 golden tests:标准 SSE、漂亮打印 JSON 续行、非 SSE body、`response.failed` auth recovery、reasoning replay、WS frame to SSE chunk。 +- 增加 golden tests:标准 SSE、漂亮打印 JSON 续行、非 SSE body、`response.failed` auth recovery、reasoning replay。 ## 8. fallback、retry、错误分类 diff --git a/tests/codex_gateway/websocket.rs b/tests/codex_gateway/websocket.rs index 1e2a2af22..09557b536 100644 --- a/tests/codex_gateway/websocket.rs +++ b/tests/codex_gateway/websocket.rs @@ -31,6 +31,9 @@ use codex_proxy_rs::codex::gateway::transport::{ mod pool; +const WS_COMPLETED_SSE_GOLDEN: &str = + include_str!("../fixtures/responses/golden/websocket_completed.sse"); + #[test] fn transport_for_request_should_default_to_websocket_without_history() { let request = base_request(); @@ -209,19 +212,7 @@ async fn ordinary_response_should_use_websocket_transport_by_default() { *request_for_task.lock().await = Some(serde_json::from_str::(&text).unwrap()); websocket .send(Message::Text( - json!({ - "type": "response.completed", - "response": { - "id": "resp_ws_default", - "object": "response", - "usage": { - "input_tokens": 4, - "output_tokens": 2 - } - } - }) - .to_string() - .into(), + websocket_completed_response("resp_ws_default", 4, 2).into(), )) .await .unwrap(); @@ -262,6 +253,10 @@ async fn ordinary_response_should_use_websocket_transport_by_default() { assert_eq!(request["type"], "response.create"); assert_eq!(request["model"], "gpt-5.5"); assert!(request.get("previous_response_id").is_none()); + assert_eq!( + response.body, + with_sse_terminal_separator(WS_COMPLETED_SSE_GOLDEN) + ); assert!(response.body.contains("event: response.completed")); assert!(response.body.contains("\"id\":\"resp_ws_default\"")); assert_eq!(response.usage.unwrap().input_tokens, 4); @@ -1067,6 +1062,14 @@ fn websocket_completed_response( .to_string() } +fn with_sse_terminal_separator(body: &str) -> String { + if body.ends_with("\n\n") { + body.to_string() + } else { + format!("{body}\n") + } +} + async fn read_http_upgrade_request(stream: &mut tokio::net::TcpStream) -> String { let mut request = Vec::new(); let mut buffer = [0u8; 1024]; diff --git a/tests/fixtures/responses/golden/websocket_completed.sse b/tests/fixtures/responses/golden/websocket_completed.sse new file mode 100644 index 000000000..ea9142e22 --- /dev/null +++ b/tests/fixtures/responses/golden/websocket_completed.sse @@ -0,0 +1,2 @@ +event: response.completed +data: {"response":{"id":"resp_ws_default","object":"response","usage":{"input_tokens":4,"output_tokens":2}},"type":"response.completed"}