test(infra): cover review-002 stream/PEM/cap error arms + drop dead WsTimeouts Default

- forward_stream build-error arm (forward.rs): wire-level test asserts
  one INVALID_INPUT envelope then stream end with zero upstream
  contact (a panicking responder counts as the contact guard), plus
  the from_jsonschema integration mirror (undeclared key + non-scalar
  placeholder, each naming its rejection source)
- PEM read-failure arms (http_client.rs): nonexistent CA path →
  CaBundleRead (sync new), nonexistent client-cert path → ClientCertRead
  (async reload, prior generation retained)
- Over-cap poll_write rejection leg (byte_adapter.rs): cap+1 write →
  InvalidData naming the cap; stream stays usable for an at-cap write
  afterwards
- SSE parser edges: CRLF split across feed chunks frames one line;
  invalid-UTF8 data lines drop without killing the frame stream
- from_value structural rejects: non-object doc, missing `info`,
  missing `paths`, non-object `paths` each name the member
- Connection-failure arms: accept-path ConnectionClosed →
  HandlerError::ConnectionClosed via stream_error_to_handler; read-pump
  demux-gone break ends the pump when the byte-stream side is dropped
- Delete the caller-less `impl Default for WsTimeouts` (the extension
  is constructed explicitly)

cargo llvm-cov --all-features: all named arms covered; TOTAL regions
94.18% (was 93.86%), lines 96.04% (was 95.77%); http_client.rs
86.56% lines (was 81.72%).

docs(tasks): mark review-002-fu-stream-error-coverage done
This commit is contained in:
2026-08-31 06:47:29 +00:00
parent 4b6507c452
commit 7294d19fc7
8 changed files with 371 additions and 10 deletions
+92
View File
@@ -2361,6 +2361,54 @@ mod tests {
assert_eq!(envelopes[3].result.clone().unwrap(), json!({"n": 1}));
}
/// SSE framing edge: a CRLF split across TCP chunks (chunk 1 ends
/// with the `\r`, chunk 2 opens with the `\n`) still frames as one
/// line — the reassembly window waits for the `\n` before parsing,
/// not treating the `\r` as a terminator (so the bare `\r` holds
/// until the `\n` arrives, then frames a single line).
#[test]
fn sse_parser_crlf_split_across_chunks_frames_one_line() {
let mut parser = SseParser::new();
let first = parser.feed(b"data: {\"n\":1}\r", false).expect("chunk 1");
assert!(first.is_empty(), "the bare \\r does not dispatch");
let second = parser
.feed(b"\ndata: {\"n\":2}\r\n\r\n", false)
.expect("chunk 2");
assert_eq!(second.len(), 1, "the joined CRLF framed exactly one event");
assert_eq!(
second[0].data, "{\"n\":1}\n{\"n\":2}",
"the \\r held as line content until the arriving \\n framed it, \
joining both data lines per WHATWG accumulation"
);
let rest = parser.feed(b"data: {\"n\":2}\n\n", true).expect("tail");
assert_eq!(rest.len(), 1);
assert_eq!(rest[0].data, "{\"n\":2}", "the second frame is unaffected");
}
/// A line that is not valid UTF-8 is dropped (WHATWG decode-failure
/// semantics for this parser): no event, no panic, and the framing
/// continues — the following valid frame dispatches normally.
#[test]
fn sse_parser_drops_invalid_utf8_lines_and_keeps_framing() {
let mut parser = SseParser::new();
let events = parser
.feed(b"data: \xff\xfe\xfd\n\ndata: {\"ok\":true}\n\n", false)
.expect("ascii framing is valid");
assert_eq!(events.len(), 1, "the invalid-UTF8 data line was dropped");
assert_eq!(events[0].data, "{\"ok\":true}");
assert_eq!(events[0].event, None);
let mut parser = SseParser::new();
let events = parser
.feed(b"data: good\r\ndata: \xf0\x28\x8c\x28\r\n\r\n", false)
.expect("CRLF framing is valid");
assert_eq!(events.len(), 1);
assert_eq!(
events[0].data, "good",
"the invalid line contributes nothing to the joined data"
);
}
/// FWD-17: an upstream `event:` field on a non-JSON payload is
/// carried in the wrapper; JSON payloads surface as themselves even
/// under a named event, and the name is reset after dispatch (the
@@ -2983,6 +3031,50 @@ mod tests {
}
}
/// The streaming analog of the Once-path build-error envelope: a
/// `forward_stream` invocation with an invalid input yields exactly
/// one `INVALID_INPUT` envelope and the stream ends, before any
/// upstream contact — the responder counts requests and the test
/// asserts the count stays zero (a regression returning an empty
/// stream, silently swallowing the error, would be the worst
/// failure shape for a subscriptions consumer).
#[tokio::test]
async fn stream_build_error_yields_one_invalid_input_envelope_with_zero_upstream_contact() {
let base = spawn_responder(TestArc::new(|_parts| {
panic!("the rejected invocation must never reach the upstream");
}))
.await;
for input in [
json!({"debug": true}),
json!({"id": {"deeply": {"nested": "object"}}}),
] {
let stream = forward_stream(
&minimal_client(),
&base,
"/x/{id}",
"GET",
&None,
&TestHashMap::new(),
"svc",
&serde_json::json!({
"type": "object",
"properties": {"id": {"type": "string"}}
}),
&[],
input,
noop_context(),
);
let envelopes = collect_stream(stream).await;
assert_eq!(envelopes.len(), 1, "one envelope, then the stream ends");
match &envelopes[0].result {
Err(err) => {
assert_eq!(err.code, "INVALID_INPUT", "message was: {}", err.message);
}
other => panic!("expected one INVALID_INPUT envelope, got {other:?}"),
}
}
}
/// COV-10 (b): the responder emits one complete `data:` frame, lets
/// the client consume it, then kills the connection before the
/// declared body length is met — a genuine mid-stream transport
+57
View File
@@ -591,6 +591,63 @@ mod tests {
assert_eq!(collected[1].request_id, "req-13");
}
/// The streaming analog of the Once-path build-error envelope: a
/// Sub op invoked with an invalid input (undeclared key, or a
/// non-scalar value bound to a path placeholder) yields exactly one
/// `INVALID_INPUT` envelope, then the stream ends — pre-send, so
/// the upstream receives zero requests. An empty stream (a silently
/// swallowed error) would be the worst failure shape for a
/// subscriptions consumer; the non-empty, single-error shape is the
/// assertion.
#[tokio::test]
async fn integration_sse_subscription_invalid_input_yields_one_error_envelope_and_ends() {
for (input, expected_fragment) in [
(
serde_json::json!({"debug": true}),
"not declared by the operation's input schema",
),
(
serde_json::json!({"id": {"nested": "object"}}),
"must be a scalar",
),
] {
let base = spawn_echo_server(200, "data: {\"n\":1}\n\n", "text/event-stream").await;
let adapter = FromJsonSchema::new(
test_spec("svc/stream", OperationType::Sub),
test_config("svc", &base),
"/stream/{id}".to_string(),
"POST".to_string(),
test_http_client(),
)
.unwrap();
let bundles = adapter.import().await.unwrap();
let registration = &bundles[0];
let ctx = noop_context("req-13b", Capabilities::new());
let stream = match &registration.handler {
HandlerKind::Stream(h) => h(input.clone(), ctx),
_ => panic!("expected Stream handler"),
};
let collected: Vec<ResponseEnvelope> = stream.collect().await;
assert_eq!(
collected.len(),
1,
"exactly one error envelope, then the stream ends (never an empty-200-subscribe)"
);
match &collected[0].result {
Err(e) => {
assert_eq!(e.code, "INVALID_INPUT", "message was: {}", e.message);
assert!(
e.message.contains(expected_fragment),
"message `{}` lacks `{expected_fragment}`",
e.message
);
}
other => panic!("expected INVALID_INPUT, got {other:?}"),
}
assert_eq!(collected[0].request_id, "req-13b");
}
}
#[test]
fn no_env_vars_read_in_build_request() {
std::env::set_var("OPENAI_API_KEY", "should-not-be-used");
+35
View File
@@ -1204,6 +1204,41 @@ mod tests {
assert!(props.get("name").is_some());
}
/// The structural-reject arms at the very top of `from_value`
/// (before any path walking): a non-object document, a
/// missing-`info` object, and a non-object `paths` each fail with
/// `SchemaParse` naming the missing/mistyped member.
#[test]
fn from_value_structural_rejects_name_the_missing_member() {
for (raw, expected_fragment) in [
(json!("just a string"), "must be a JSON object"),
(json!([1, 2, 3]), "must be a JSON object"),
(json!({"paths": {}}), "missing `info`"),
(
json!({"info": {"title": "T", "version": "1"}}),
"missing `paths`",
),
(
json!({
"info": {"title": "T", "version": "1"},
"paths": ["/x"]
}),
"`paths` must be a JSON object",
),
] {
match OpenAPISpec::from_value(raw) {
Err(AdapterError::SchemaParse { message }) => {
assert!(
message.contains(expected_fragment),
"message `{message}` lacks `{expected_fragment}`"
);
}
Ok(_) => panic!("the malformed document must be rejected"),
other => panic!("expected SchemaParse, got {other:?}"),
}
}
}
#[test]
fn request_body_self_ref_fails_import_not_silent_bodyless_op() {
let doc = r##"{