From ff3646ec8390f64e00b508b59ca2a0339f0d0bae Mon Sep 17 00:00:00 2001 From: codex-layer-stabilizer Date: Thu, 15 Oct 2026 23:36:31 +0800 Subject: [PATCH] =?UTF-8?q?test(proxy):=20=E8=A1=A5=E5=8E=8B=E7=BC=A9?= =?UTF-8?q?=E8=BD=AE=E7=B4=AF=E8=AE=A1=E4=B8=8A=E9=99=90=E7=9A=84=E4=B8=A4?= =?UTF-8?q?=E6=9D=A1=E7=AB=AF=E5=88=B0=E7=AB=AF=E9=AA=8C=E6=94=B6=E2=80=94?= =?UTF-8?q?=E2=80=94=E9=9D=9E=E6=B5=81=E5=BC=8F=E4=B8=8E=E6=B5=81=E5=BC=8F?= =?UTF-8?q?=E5=90=84=E4=B8=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 上限判据(上限 > 上游自报失败 > 项数)此前只有纯函数层的对拍,缺「正文里确实有 恰好一个原生项、但体积超限」这一组合的端到端证据——而那正是最容易失手的形态: 项数过关会让透传路径以为可以放行,客户端于是拿到一份状态被截断的响应却毫不知情。 - 非流式:超限响应必须按失败收口(原因 compaction_over_limit),opaque 一个字 都不下发,也不冒充本地摘要。 - 流式:累计到上限后停止转发,并以显式失败帧收口;转发量必须被上限截住。 上限值刻意在测试里写死并与实现侧同值:有人放宽上限时,这条测试必须显式改动, padding 也要同步加大,否则悄悄退回「测不到超限」。 两条测试的有效性反向验过:把实现侧 over_limit 判据改成恒 false 后,非流式那条立刻失败。 --- tests/native_compaction_wiring.rs | 83 ++++++++++++++++++++++++++++++- 1 file changed, 82 insertions(+), 1 deletion(-) diff --git a/tests/native_compaction_wiring.rs b/tests/native_compaction_wiring.rs index 5279056..e87ea87 100644 --- a/tests/native_compaction_wiring.rs +++ b/tests/native_compaction_wiring.rs @@ -6,7 +6,8 @@ //! 3) 4xx 但判假(无关错误)→ 错误原样透传,不降级(反判据); //! 4) 截断/零项的流 → 显式 response.failed,无静默半截流; //! 5) 上游自报失败的流 → 不补重复帧; -//! 6) 非流式原生项 → 透传上游 JSON,不被本地摘要冒充。 +//! 6) 非流式原生项 → 透传上游 JSON,不被本地摘要冒充; +//! 7) 响应体超过累计上限 → 按失败收口,且残缺正文一个字都不下发。 use serde_json::{Value, json}; use std::io::Read; @@ -437,6 +438,86 @@ async fn truncated_compaction_stream_still_gets_failure_frame() { ); } +/// 累计上限与实现侧常量同值;刻意写死而不是导入,是为了让「有人抬上限」时这条 +/// 测试必须显式改动,而不是跟着悄悄放宽(padding 也要同步加大,否则测不到超限)。 +const MAX_COMPACTION_RESPONSE_BYTES: usize = 32 * 1024 * 1024; +const OVER_LIMIT_PADDING_BYTES: usize = MAX_COMPACTION_RESPONSE_BYTES + 1024 * 1024; + +/// 恰好一个原生项、但正文体积超过上限的填充:padding 挂在 message 项的文本里, +/// 使正文既有原生项、又必然超限——两种判据同时在场时上限必须赢。 +fn oversized_text() -> String { + "x".repeat(OVER_LIMIT_PADDING_BYTES) +} + +/// 非流式超限:即使正文里确有恰好一个原生 compaction 项,也不得透传。 +/// 残缺正文建立在不可信文本上,据它宣布成功会让客户端拿到「看起来压缩完了、 +/// 其实状态被截断」的响应;必须按失败收口,且 opaque 一个字都不下发。 +#[tokio::test] +async fn oversized_non_stream_body_fails_instead_of_passing_through() { + let body = json!({ + "id": "r5", + "status": "completed", + "output": [ + {"id": "msg_1", "type": "message", "content": [ + {"type": "output_text", "text": oversized_text()} + ]}, + {"id": "cmp_1", "type": "compaction", "encrypted_content": OPAQUE}, + ], + }) + .to_string(); + let (port, _guard, _dir, _log, _capture) = + harness(vec![raw_response("200 OK", "application/json", &body)]).await; + let key = format!("over-limit-nonstream-{}", std::process::id()); + + let mut request = trigger_request(&key); + request["stream"] = json!(false); + let seen = post(port, &request).await; + assert!( + seen.contains("response.failed"), + "超限必须显式失败收口, got 前 200 字={}", + &seen[..seen.len().min(200)] + ); + assert!( + seen.contains("compaction_over_limit"), + "失败原因必须点明是体积超限, got 前 400 字={}", + &seen[..seen.len().min(400)] + ); + assert!(!seen.contains(OPAQUE), "超限响应不得把残缺正文下发给客户端"); + assert!(!seen.contains("codexlayer:summary"), "超限不得冒充本地摘要"); +} + +/// 流式超限:超限时**停止转发**(继续下发只会让客户端拿到一段注定按失败收口的 +/// 残缺流),并以显式失败帧收口。 +#[tokio::test] +async fn oversized_stream_stops_forwarding_and_fails() { + let upstream_body = format!( + "{}event: response.output_item.done\ndata: {{\"type\":\"response.output_item.done\",\"output_index\":0,\"item\":{{\"id\":\"msg_1\",\"type\":\"message\",\"content\":[{{\"type\":\"output_text\",\"text\":\"{}\"}}]}}}}\n\n{}", + created_event(), + oversized_text(), + compaction_item_event() + ); + let (port, _guard, _dir, _log, _capture) = harness(vec![sse_response(&upstream_body)]).await; + let key = format!("over-limit-stream-{}", std::process::id()); + + let seen = post(port, &trigger_request(&key)).await; + assert!( + seen.contains("response.failed"), + "超限流必须显式失败收口,不得静默半截流" + ); + assert!( + seen.contains("compaction_over_limit"), + "失败原因必须点明是体积超限" + ); + assert!(!seen.contains(OPAQUE), "超限后不得继续转发后续事件"); + // 实现口径是「累计到上限后停止转发」,所以上限以内的前缀允许先下发; + // 这条断言钉的是「不会把整段超限正文倾泻给客户端」——即下发量必须被上限截住。 + assert!( + seen.len() < MAX_COMPACTION_RESPONSE_BYTES, + "转发量必须被累计上限截住,不该接近完整正文, 实际下发 {} 字节", + seen.len() + ); +} + fn read_log_lines(path: &std::path::Path) -> Vec { let mut text = String::new(); std::fs::File::open(path)