fix: channels parse-failure path writes the negotiation error frame (R4)
Review #002 R4 — a NegotiateRequest parse failure of the open op's schema-validated input died silently (log + return, channel teardown, consumer observed NoExitChunk — indistinguishable from a crashed producer), while the other post-open failure classes (unknown backend, allocate_failed, ownership denial) wrote the 0x00-prefixed error frame. - make_tty_open_handler now accepts the channel's BiStream and writes a malformed_negotiation frame via the shared crate::adapter::send_negotiation_error (now pub(crate)) before returning; the consumer's M1 peek surfaces NegotiationRejected unchanged - the frame type and layout are unchanged (ADR-001 wire-stable contract); no new frame type, no wire change - tests: make_tty_open_handler seam test with a hand-built schema-bypassing input (cwd: 42) + a real-registry end-to-end test via ChannelClient::open_channel (bypasses open_via_channels's local fail-fast parse — R5's path — so it exercises the producer handler) - docs: ADR-009 amended (Parse-failure error frame section); tty-adapter.md malformed_negotiation row covers both paths; session.rs post-open failure lists updated; review #002 R4 resolved Note: the review's "unreachable end-to-end" premise was refined — open_via_channels parses params locally (fail-fast) so a TtySession consumer never hits the producer-side parse failure, but direct ChannelClient callers do; the schema is deliberately partial so a schema-valid value (cwd typed as a number) reaches the handler. Verification: cargo test 95 lib (default) / 138 (--all-features); clippy -D warnings native + wasm clean; fmt clean; doc 0 warnings.
This commit is contained in:
@@ -206,4 +206,64 @@ mod tests {
|
||||
assert_eq!(first[0], crate::wire::STREAM_STDOUT);
|
||||
drop(send);
|
||||
}
|
||||
|
||||
/// End-to-end (R4): `input` that passes the registry's (partial)
|
||||
/// schema but fails the handler's full `NegotiateRequest` parse —
|
||||
/// `cwd` typed as a number. The open op succeeds (the schema is
|
||||
/// deliberately partial), the producer-side handler writes a
|
||||
/// `0x00`-prefixed `malformed_negotiation` error frame on the
|
||||
/// channel stream, and the consumer-side post-open read sequence
|
||||
/// (`from_halves_raw`'s peek + error-frame parse) observes it — the
|
||||
/// same failure class as the post-open semantic failures, not a
|
||||
/// silent teardown. Uses `ChannelClient::open_channel` directly
|
||||
/// (the consumer's local fail-fast parse in `open_via_channels`
|
||||
/// would reject these params before the open op runs — R5's
|
||||
/// fail-fast path).
|
||||
#[tokio::test]
|
||||
async fn schema_valid_but_unparseable_input_gets_malformed_negotiation_frame() {
|
||||
use tokio::io::AsyncReadExt;
|
||||
|
||||
let mut backends: HashMap<String, Arc<dyn TtyBackend>> = HashMap::new();
|
||||
backends.insert("mock".to_string(), Arc::new(MockBackend::with_exit_code(0)));
|
||||
let client =
|
||||
wire_client_and_server(Arc::new(backends), None, Some(tty_identity("alice"))).await;
|
||||
|
||||
let (_channel_id, _send, mut recv) = tokio::time::timeout(
|
||||
std::time::Duration::from_secs(5),
|
||||
client.open_channel(
|
||||
OP_TTY_OPEN,
|
||||
serde_json::json!({
|
||||
"carriage": "raw",
|
||||
"backend": "mock",
|
||||
"cmd": ["true"],
|
||||
"cwd": 42
|
||||
}),
|
||||
crate::channels::TTY_ALPN,
|
||||
),
|
||||
)
|
||||
.await
|
||||
.expect("open_channel timed out")
|
||||
.expect("open_channel — the partial schema accepts cwd: 42");
|
||||
|
||||
// The consumer-side post-open sequence (`from_halves_raw`):
|
||||
// peek the first byte; `0x00` = negotiation error frame.
|
||||
let mut first = [0u8; 1];
|
||||
tokio::time::timeout(
|
||||
std::time::Duration::from_secs(5),
|
||||
recv.read_exact(&mut first),
|
||||
)
|
||||
.await
|
||||
.expect("no response byte from producer")
|
||||
.expect("read first byte");
|
||||
assert_eq!(first[0], 0x00, "error frame length prefix starts with 0x00");
|
||||
|
||||
let mut len_rest = [0u8; 3];
|
||||
recv.read_exact(&mut len_rest).await.expect("read len rest");
|
||||
let len = u32::from_be_bytes([first[0], len_rest[0], len_rest[1], len_rest[2]]) as usize;
|
||||
let mut body = vec![0u8; len];
|
||||
recv.read_exact(&mut body).await.expect("read error body");
|
||||
let v: serde_json::Value = serde_json::from_slice(&body).expect("parse error frame");
|
||||
assert_eq!(v["error"], "malformed_negotiation");
|
||||
assert!(v["message"].as_str().is_some_and(|m| !m.is_empty()));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user