feat: InvalidParams variant + #[non_exhaustive] TtySessionError (R5)

Review #002 R5 — the open_via_channels fail-fast parse (a params value
that fails the local NegotiateRequest parse before a channel is
allocated) surfaced as NegotiationSerialize, whose name and doc
describe serializing the negotiation frame, not parsing open-op params.

- add TtySessionError::InvalidParams(String); the fail-fast path maps
  to it (the serde_json::Error's From impl stays for
  NegotiationSerialize's real users — the direct-path serialize)
- mark TtySessionError #[non_exhaustive] — the same two-way-door
  pattern as TtyError (backend.rs) and alkcall's consumer-facing
  AdapterError; the policy rationale is in the enum's doc
- NegotiationError / RawError stay exhaustive (deliberate — they
  mirror fixed wire semantics; in-crate matchers keep exhaustiveness
  checking)
- the two fail-fast tests assert InvalidParams(_) now
- review #002: R5 resolved; the superseded deferral rationale is
  recorded (circular trigger — "first channels-path consumer exists"
  fires after the change becomes expensive; misapplied citation —
  ADR-009's version-skew note governs wire skew, not error enums;
  the "unreachable" framing belonged to R4's arm, not R5's — the
  fail-fast path is live today). The #[non_exhaustive] policy is
  decided on principle, pre-publish, while the variant addition is
  additive by construction

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:
2026-09-05 08:45:58 +00:00
parent 8ee9216a07
commit 918af406dd
2 changed files with 68 additions and 19 deletions
+28 -10
View File
@@ -67,7 +67,16 @@ use crate::negotiation::{NegotiateRequest, NegotiationError, NegotiationWriter};
use crate::wire::{Chunk, ChunkReader, ChunkWriter, RawError, STREAM_CTRL_IN, STREAM_CTRL_OUT};
/// Errors from the typed consumer client.
///
/// `#[non_exhaustive]` so new variants are additive (the same
/// two-way-door pattern as [`crate::backend::TtyError`], and the same
/// justification alkcall gives its consumer-facing `AdapterError`):
/// session drivers accrue failure modes (the channels-path fail-fast
/// variant was added pre-1.0), and an exhaustive match on this enum in
/// a downstream consumer would turn every addition into a breaking
/// change.
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum TtySessionError {
/// The underlying transport I/O failed (not a clean EOF).
#[error("io: {0}")]
@@ -86,6 +95,14 @@ pub enum TtySessionError {
/// `channel_id` missing from the response).
#[error("channels open: {0}")]
ChannelsOpen(String),
/// The channels open op's `params` failed the consumer's local
/// `NegotiateRequest` parse — the fail-fast check in
/// [`TtySession::open_via_channels`] before a channel is allocated.
/// (R5: the parse previously surfaced as `NegotiationSerialize`,
/// a variant whose name and doc describe serializing the
/// negotiation frame, not parsing open-op params.)
#[error("invalid open params: {0}")]
InvalidParams(String),
/// The server sent a negotiation error frame (the first frame on
/// the stream is a length-prefixed JSON `{"error":"..."}` rather
/// than a raw chunk).
@@ -195,20 +212,21 @@ impl TtySession {
/// and the producer parses the same value from the open op.
///
/// Failures before the channel opens (ACL denial, unknown op,
/// channel cap, invalid params) surface as
/// [`TtySessionError::ChannelsOpen`]. Post-open failures (a
/// `NegotiateRequest` parse failure of a schema-valid-but-unparseable
/// params value, unknown backend, allocate failure, ownership
/// denial) arrive as a negotiation error frame on the channel
/// stream — the session surfaces those as
/// [`TtySessionError::NegotiationRejected`] via the same `0x00`
/// channel cap) surface as [`TtySessionError::ChannelsOpen`]; a
/// params value that fails the local `NegotiateRequest` parse
/// surfaces as [`TtySessionError::InvalidParams`]. Post-open
/// failures (a `NegotiateRequest` parse failure of a
/// schema-valid-but-unparseable params value, unknown backend,
/// allocate failure, ownership denial) arrive as a negotiation
/// error frame on the channel stream — the session surfaces those
/// as [`TtySessionError::NegotiationRejected`] via the same `0x00`
/// disambiguation read the direct path uses.
pub async fn open_via_channels(
client: &ChannelClient,
params: serde_json::Value,
) -> Result<Self, TtySessionError> {
let _: NegotiateRequest = serde_json::from_value(params.clone())
.map_err(TtySessionError::NegotiationSerialize)?;
.map_err(|e| TtySessionError::InvalidParams(e.to_string()))?;
let (channel_id, send, recv) = client
.open_channel(
crate::channels::OP_TTY_OPEN,
@@ -1083,7 +1101,7 @@ mod tests {
.await
.expect("open_via_channels timed out");
assert!(
matches!(result, Err(TtySessionError::NegotiationSerialize(_))),
matches!(result, Err(TtySessionError::InvalidParams(_))),
"schema-invalid params fail at the local NegotiateRequest parse (fail-fast, pre-open)"
);
}
@@ -1106,7 +1124,7 @@ mod tests {
.await
.expect("open_via_channels timed out");
assert!(
matches!(result, Err(TtySessionError::NegotiationSerialize(_))),
matches!(result, Err(TtySessionError::InvalidParams(_))),
"unparseable params must fail before the open op"
);
}