msquic_h3/error.rs
1//! Error vocabulary for the msquic <-> h3 adapter.
2//!
3//! This module introduces the shared, adapter-owned error types, the scoped
4//! terminal enums that record *why* a connection, receive half, or send half
5//! ended, and the pure conversion helpers that mint fresh `h3` error values at
6//! the polling boundary. It also defines the outgoing application-code clamp.
7//!
8//! Most of the terminal enums and conversion helpers are wired into the FFI
9//! callbacks and polling paths in later phases (Phase 3 onwards); they are
10//! introduced here so the vocabulary exists in one place. The `#[allow(dead_code)]`
11//! attributes below are scoped to individual not-yet-wired items and each notes
12//! the phase that consumes it.
13
14use std::fmt;
15
16use h3::quic::{ConnectionErrorIncoming, StreamErrorIncoming};
17
18use crate::msquic::{Status, StatusCode};
19
20/// Maximum value representable by a QUIC 62-bit variable-length integer.
21///
22/// Outgoing application error codes must never exceed this or msquic rejects the
23/// encode. See [`clamp_application_code`].
24pub const MAX_QUIC_VARINT: u64 = (1 << 62) - 1;
25
26/// Default maximum single `send_data` payload the adapter will accept before
27/// rejecting with [`OversizedSend`] (16 MiB). Configurable via [`H3Config`](crate::H3Config).
28pub const MAX_ADAPTER_SEND: u64 = 16 * 1024 * 1024;
29
30/// Clamp an outgoing application error code to the QUIC 62-bit varint maximum.
31///
32/// `(1 << 62) - 1` passes unchanged; `1 << 62` and above clamp down to the
33/// maximum. Applied at every site where an application code crosses the FFI
34/// boundary (connection close, `stop_sending`, and — in Phase 7 — `reset`).
35pub(crate) fn clamp_application_code(code: u64) -> u64 {
36 code.min(MAX_QUIC_VARINT)
37}
38
39// ---------------------------------------------------------------------------
40// Adapter-owned error types.
41//
42// Each is `Debug + Display + Error + Send + Sync` so it can be `Arc`'d into
43// `ConnectionErrorIncoming::Undefined(Arc<dyn Error + Send + Sync>)` or boxed
44// into `StreamErrorIncoming::Unknown(Box<dyn Error + Send + Sync>)`.
45// ---------------------------------------------------------------------------
46
47/// A `send_data` payload above `MAX_ADAPTER_SEND`. One-shot; never stored in
48/// the shared send-terminal slot. Constructed at the `send_data` rejection site
49/// and boxed into `StreamErrorIncoming::Unknown`.
50#[derive(Debug)]
51pub struct OversizedSend {
52 /// Requested payload length in bytes (the `remaining()` that was rejected).
53 pub len: usize,
54 /// The connection's configured send-size ceiling that was exceeded.
55 pub max_bytes: u64,
56}
57
58impl fmt::Display for OversizedSend {
59 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
60 write!(
61 f,
62 "send_data payload of {} bytes exceeds configured ceiling ({} bytes)",
63 self.len, self.max_bytes
64 )
65 }
66}
67
68impl std::error::Error for OversizedSend {}
69
70/// A non-application transport shutdown. Both the QUIC status and the wire
71/// transport error code are retained for diagnostics; the transport code is a
72/// QUIC transport code, not an HTTP/3 application code.
73#[derive(Debug)]
74pub struct MsQuicTransportError {
75 /// The QUIC status that accompanied the transport shutdown.
76 pub status: Status,
77 /// The wire transport error code (diagnostic only).
78 pub error_code: u64,
79}
80
81impl fmt::Display for MsQuicTransportError {
82 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
83 write!(
84 f,
85 "QUIC transport shutdown: status {} (transport error_code {})",
86 self.status, self.error_code
87 )
88 }
89}
90
91impl std::error::Error for MsQuicTransportError {}
92
93/// A local (application-initiated) connection close. Carries no peer code: the
94/// required mapping is explicit that we must not invent one.
95#[derive(Debug)]
96pub struct LocalConnectionClose;
97
98impl fmt::Display for LocalConnectionClose {
99 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
100 f.write_str("connection closed locally by the application")
101 }
102}
103
104impl std::error::Error for LocalConnectionClose {}
105
106/// A local reset via `SendStream::reset(code)`. Distinct from a peer
107/// `StreamTerminated`: h3 should not poll a reset send stream, and if it does we
108/// surface this rather than pretending the peer terminated the stream.
109#[derive(Debug)]
110pub struct LocalStreamReset {
111 /// The (already-clamped) local reset code.
112 pub code: u64,
113}
114
115impl fmt::Display for LocalStreamReset {
116 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
117 write!(f, "send stream reset locally with code {}", self.code)
118 }
119}
120
121impl std::error::Error for LocalStreamReset {}
122
123// ---------------------------------------------------------------------------
124// Scoped terminal enums.
125//
126// Small, adapter-owned reasons recorded in callback-facing state. `Clone` where
127// multiple consumers need the reason. Converted to fresh h3 error values only at
128// the polling boundary via the `convert_*` helpers below.
129// ---------------------------------------------------------------------------
130
131/// Why a connection terminated. The first writer wins for the connection scope,
132/// except a *provisional* cause may be refined to a more specific peer/transport
133/// cause before the terminal is externally observed (see
134/// [`ConnectionTerminal::is_provisional`]).
135#[derive(Clone, Debug)]
136pub(crate) enum ConnectionTerminal {
137 /// Peer closed with an HTTP/3 application error code.
138 PeerApplication(u64),
139 /// Idle or handshake timeout.
140 Timeout,
141 /// Non-application transport shutdown.
142 Transport {
143 /// The QUIC status that accompanied the shutdown.
144 status: Status,
145 /// The wire transport error code (diagnostic only).
146 error_code: u64,
147 },
148 /// Local application-initiated close.
149 LocalClose,
150 /// Internal adapter failure.
151 #[allow(dead_code)] // constructed by the accepted-stream fail-fast path in Phase 5
152 Internal(&'static str),
153}
154
155impl ConnectionTerminal {
156 /// Whether this cause is *provisional* and may still be refined.
157 ///
158 /// A provisional cause (a local close, or — in later phases — a generic
159 /// transport fallback derived without a specific reason) may be replaced by
160 /// a subsequently-published, more-specific peer/transport cause until the
161 /// terminal has been externally observed. Specific peer/transport causes and
162 /// internal failures are authoritative and never refine to a provisional
163 /// value. See "Terminal-cause refinement" in `docs/error-model.md`.
164 pub(crate) fn is_provisional(&self) -> bool {
165 matches!(self, ConnectionTerminal::LocalClose)
166 }
167}
168
169/// Why a receive half terminated. The first writer wins for the receive scope.
170#[derive(Clone, Debug)]
171pub(crate) enum ReceiveTerminal {
172 /// Clean end of stream (peer FIN / send-shutdown).
173 Fin,
174 /// Peer reset the stream with the given code.
175 Reset(u64),
176 /// The whole connection terminated.
177 Connection(ConnectionTerminal),
178 /// Internal adapter failure.
179 Internal(&'static str),
180}
181
182/// Why a send half terminated. The first writer wins for the send scope, except
183/// the distinct *provisional* cancellation marker ([`SendTerminal::ProvisionalAbort`],
184/// synthesized for a cancelled `SendComplete` with no yet-known cause, MF-2)
185/// which may be refined to a more-specific peer/connection cause — or finalized
186/// to an authoritative [`SendTerminal::Failed`] abort at the closure point —
187/// while it is still unobserved.
188#[derive(Clone, Debug)]
189pub(crate) enum SendTerminal {
190 /// Peer sent `STOP_SENDING` with the given code.
191 Stopped(u64),
192 /// The whole connection terminated.
193 Connection(ConnectionTerminal),
194 /// Local `reset(code)` (already clamped). Not a peer termination.
195 LocalReset(u64),
196 /// A native send/start failure carrying its status. Authoritative: even a
197 /// native `QUIC_STATUS_ABORTED` here is a *real* failure, never refinable.
198 Failed(Status),
199 /// A *provisional*, still-refinable cancellation synthesized for a cancelled
200 /// `SendComplete` that arrived with no published peer/connection cause (MF-2).
201 /// It is deliberately distinct from a real [`SendTerminal::Failed`]: only this
202 /// marker is refinable, it is never surfaced to the caller (it stays
203 /// unobserved), and it is finalized to `Failed(QUIC_STATUS_ABORTED)` at the
204 /// defined closure point when no richer cause has appeared.
205 ProvisionalAbort,
206 /// Internal adapter failure.
207 Internal(&'static str),
208}
209
210impl SendTerminal {
211 /// Whether this cause is the *provisional* send cancellation marker that may
212 /// still be refined to a more-specific cause (MF-2).
213 ///
214 /// Only [`SendTerminal::ProvisionalAbort`] is provisional. A specific peer
215 /// `Stopped`, a `Connection` reason, a `LocalReset`, an internal failure, or
216 /// **any** `Failed` status (including a real native `QUIC_STATUS_ABORTED`) is
217 /// authoritative and never refined. See "Terminal-cause refinement" in `docs/error-model.md`.
218 pub(crate) fn is_provisional(&self) -> bool {
219 matches!(self, SendTerminal::ProvisionalAbort)
220 }
221}
222
223/// The synthesized status for an unclassified send cancellation.
224pub(crate) fn aborted_status() -> Status {
225 Status::new(StatusCode::QUIC_STATUS_ABORTED)
226}
227
228// ---------------------------------------------------------------------------
229// Conversion helpers.
230//
231// These are the *only* places that mint h3 error values from adapter terminals.
232// Pure functions; wired into the polling boundary in later phases.
233// ---------------------------------------------------------------------------
234
235/// Convert a [`ConnectionTerminal`] into the h3 connection error it represents.
236pub(crate) fn convert_conn(t: ConnectionTerminal) -> ConnectionErrorIncoming {
237 use ConnectionTerminal as C;
238 match t {
239 C::PeerApplication(code) => ConnectionErrorIncoming::ApplicationClose { error_code: code },
240 C::Timeout => ConnectionErrorIncoming::Timeout,
241 C::Transport { status, error_code } => {
242 ConnectionErrorIncoming::Undefined(std::sync::Arc::new(MsQuicTransportError {
243 status,
244 error_code,
245 }))
246 }
247 C::LocalClose => {
248 ConnectionErrorIncoming::Undefined(std::sync::Arc::new(LocalConnectionClose))
249 }
250 C::Internal(msg) => ConnectionErrorIncoming::InternalError(msg.to_string()),
251 }
252}
253
254/// Convert a [`SendTerminal`] into the h3 stream error it represents.
255pub(crate) fn convert_send(t: SendTerminal) -> StreamErrorIncoming {
256 use SendTerminal as S;
257 match t {
258 S::Stopped(code) => StreamErrorIncoming::StreamTerminated { error_code: code },
259 S::Connection(reason) => StreamErrorIncoming::ConnectionErrorIncoming {
260 connection_error: convert_conn(reason),
261 },
262 S::LocalReset(code) => StreamErrorIncoming::Unknown(Box::new(LocalStreamReset { code })),
263 S::Failed(status) => StreamErrorIncoming::Unknown(Box::new(status)),
264 // A provisional marker is never surfaced to the caller while unobserved;
265 // if it is ever converted (a defensive, non-panicking fallback) it maps to
266 // the same authoritative aborted status its closure-point finalization uses.
267 S::ProvisionalAbort => StreamErrorIncoming::Unknown(Box::new(aborted_status())),
268 S::Internal(msg) => StreamErrorIncoming::ConnectionErrorIncoming {
269 connection_error: ConnectionErrorIncoming::InternalError(msg.to_string()),
270 },
271 }
272}
273
274/// Convert a [`ReceiveTerminal`] into the h3 receive outcome it represents.
275///
276/// A clean FIN maps to `Ok(None)`; every other terminal maps to an error.
277pub(crate) fn convert_recv(
278 t: ReceiveTerminal,
279) -> Result<Option<bytes::Bytes>, StreamErrorIncoming> {
280 use ReceiveTerminal as R;
281 match t {
282 R::Fin => Ok(None),
283 R::Reset(code) => Err(StreamErrorIncoming::StreamTerminated { error_code: code }),
284 R::Connection(reason) => Err(StreamErrorIncoming::ConnectionErrorIncoming {
285 connection_error: convert_conn(reason),
286 }),
287 R::Internal(msg) => Err(StreamErrorIncoming::ConnectionErrorIncoming {
288 connection_error: ConnectionErrorIncoming::InternalError(msg.to_string()),
289 }),
290 }
291}
292
293// ---------------------------------------------------------------------------
294// Send-side reducer (Phase 7).
295//
296// A pure, native-free state machine for the send half. It mutates only
297// [`SendState`] and emits one [`SendCommand`] per input; the frontend executor
298// loops (in `stream.rs`) run the returned command against MsQuic through the
299// [`crate::SendExec`] seam and feed results straight back. Keeping the reducer
300// pure makes every transition exhaustively table-testable with no native handle.
301// See "Send-side transitions" / the reducer in `docs/error-model.md`.
302// ---------------------------------------------------------------------------
303
304/// One order-sensitive send event. All send events (data completion, terminal
305/// wake, finish completion) ride a **single** mpsc so chronological finish-vs-
306/// terminal order is preserved by construction (MF-1).
307#[derive(Clone, Copy, Debug, PartialEq, Eq)]
308pub(crate) enum SendEvent {
309 /// A native `SendComplete` for an outstanding data send; `cancelled` is the
310 /// native cancel flag.
311 Complete { cancelled: bool },
312 /// A terminal wake: a peer `STOP_SENDING` or connection shutdown published a
313 /// sticky [`SendTerminal`] into the shared slot and woke the send half.
314 TerminalWake,
315 /// A `SendShutdownComplete`: the graceful (or aborted) finish completed.
316 FinishComplete { graceful: bool },
317}
318
319/// Reducer bookkeeping for the send half. `*_submitting` are transaction
320/// reservations set before a native call so a reentrant/repeated input cannot
321/// emit a second submission; each clears when its matching `*Submitted` arrives.
322#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
323pub(crate) struct SendState {
324 /// A data `SubmitSend` was issued; awaiting `SendSubmitted`.
325 pub(crate) send_submitting: bool,
326 /// A data send is committed and awaiting its `SendComplete`.
327 pub(crate) send_inprogress: bool,
328 /// A `SubmitGraceful` was issued; awaiting `GracefulSubmitted`.
329 pub(crate) finish_submitting: bool,
330 /// Graceful shutdown was submitted (committed on `GracefulSubmitted(Ok)`).
331 pub(crate) finish_started: bool,
332 /// The graceful finish completed (absorbing success).
333 pub(crate) finish_complete: bool,
334 /// A `SubmitReset` was issued; awaiting `ResetSubmitted`.
335 pub(crate) reset_submitting: bool,
336}
337
338impl SendState {
339 /// A fresh, all-false state.
340 pub(crate) fn new() -> Self {
341 SendState::default()
342 }
343
344 /// A native submission is reserved but its `*Submitted` result has not arrived.
345 pub(crate) fn submission_reserved(&self) -> bool {
346 self.send_submitting || self.finish_submitting || self.reset_submitting
347 }
348}
349
350/// Which frontend method initiated a `PublishTerminal`, so the reducer can pick
351/// the correct continuation once the first-writer winner is known.
352#[derive(Clone, Copy, Debug, PartialEq, Eq)]
353pub(crate) enum TerminalContinuation {
354 SendData,
355 PollReady,
356 PollFinish,
357 Reset,
358}
359
360/// Outcome of polling the single send-event channel. `Closed` (a channel closed
361/// with no terminal item) is an adapter-internal fault, distinct from `Pending`.
362#[derive(Clone, Copy, Debug, PartialEq, Eq)]
363pub(crate) enum SendPoll {
364 Pending,
365 Event(SendEvent),
366 Closed,
367}
368
369/// `send_data` payload classification, computed from `remaining()` before any
370/// owned bytes are materialized. The `NonEmpty`/`Oversized` split is against
371/// [`MAX_ADAPTER_SEND`], not the raw native `u32::MAX`.
372#[derive(Clone, Copy, Debug, PartialEq, Eq)]
373pub(crate) enum SendPayload {
374 Empty,
375 NonEmpty { len: u32 },
376 Oversized { len: usize, ceiling: u64 },
377}
378
379/// One-shot, non-sticky adapter error for `send_data`. Never stored in the
380/// shared slot; a later `poll_ready` is unaffected.
381#[derive(Clone, Copy, Debug, PartialEq, Eq)]
382pub(crate) enum SendOperationError {
383 OversizedSend { len: usize, ceiling: u64 },
384 Misuse(&'static str),
385}
386
387/// Inputs to [`transition`]. Each `terminal` field is the current shared winner
388/// the caller loaded from the send-terminal slot (poison-safe).
389#[derive(Clone, Debug)]
390pub(crate) enum SendInput {
391 /// A `send_data` request; `payload` is classified before allocation.
392 SendRequested {
393 payload: SendPayload,
394 terminal: Option<SendTerminal>,
395 },
396 /// Immediate result of the `SubmitSend` native call.
397 SendSubmitted {
398 result: Result<(), Status>,
399 terminal: Option<SendTerminal>,
400 },
401 /// A `poll_ready` request, carrying the send-channel poll outcome.
402 PollReady {
403 poll: SendPoll,
404 terminal: Option<SendTerminal>,
405 },
406 /// A `poll_finish` request, carrying the send-channel poll outcome.
407 PollFinish {
408 poll: SendPoll,
409 terminal: Option<SendTerminal>,
410 },
411 /// A `reset(code)` request (code already clamped).
412 Reset {
413 code: u64,
414 terminal: Option<SendTerminal>,
415 },
416 /// Immediate result of a `SubmitGraceful` native call.
417 GracefulSubmitted {
418 result: Result<(), Status>,
419 terminal: Option<SendTerminal>,
420 },
421 /// Immediate result of a `SubmitReset` native call.
422 ResetSubmitted {
423 code: u64,
424 result: Result<(), Status>,
425 terminal: Option<SendTerminal>,
426 },
427 /// The actual winner returned by the first-writer publish helper, fed back
428 /// after a `PublishTerminal` command, tagged with its initiating method.
429 TerminalPublished {
430 winner: SendTerminal,
431 continuation: TerminalContinuation,
432 },
433}
434
435/// Commands emitted by [`transition`]. The frontend executor loop runs each and
436/// feeds the result back as a further [`SendInput`].
437#[derive(Clone, Debug)]
438pub(crate) enum SendCommand {
439 /// `Poll::Pending`; the caller has already registered the waker.
440 Pending,
441 /// Infallible no-op (e.g. `reset` after a terminal).
442 NoOp,
443 /// `send_data` success / no-op (empty payload); no bytes submitted.
444 ReturnSent,
445 /// Construct the allocation and call `submit_send`; result -> `SendSubmitted`.
446 SubmitSend,
447 /// Call `submit_graceful`; result -> `GracefulSubmitted`.
448 SubmitGraceful,
449 /// Call `submit_reset(code)`; result -> `ResetSubmitted`.
450 SubmitReset(u64),
451 /// Re-poll the send channel and feed a fresh `PollFinish` in the SAME call,
452 /// so a synchronously-queued `FinishComplete` is drained (or the waker is
453 /// re-armed) before `Pending`.
454 RepollFinish,
455 /// Re-poll the send channel and feed a fresh `PollReady` in the SAME call.
456 /// Emitted after retaining an unobserved provisional cancellation (MF-2): it
457 /// drains a synchronously-queued paired terminal / channel close (the closure
458 /// point) or re-arms the waker before `Pending`, so the provisional value is
459 /// never returned to the caller.
460 RepollReady,
461 /// Publish a local terminal candidate via the first-writer helper; the
462 /// resolved winner is fed back as `TerminalPublished { winner, continuation }`.
463 PublishTerminal {
464 candidate: SendTerminal,
465 continuation: TerminalContinuation,
466 },
467 /// Return a one-shot, non-sticky `send_data` error (oversized / misuse).
468 ReturnImmediateError(SendOperationError),
469 /// `poll_ready` success (`Poll::Ready(Ok(()))`).
470 ReturnReady,
471 /// `poll_finish` success (`Poll::Ready(Ok(()))`).
472 ReturnFinished,
473 /// A terminal winner surfaced to the caller as an error.
474 ReturnError(SendTerminal),
475}
476
477/// Convert a one-shot [`SendOperationError`] into the h3 error `send_data`
478/// returns. Never stored in the shared slot.
479pub(crate) fn convert_send_op(e: SendOperationError) -> StreamErrorIncoming {
480 match e {
481 SendOperationError::OversizedSend { len, ceiling } => {
482 StreamErrorIncoming::Unknown(Box::new(OversizedSend {
483 len,
484 max_bytes: ceiling,
485 }))
486 }
487 SendOperationError::Misuse(msg) => StreamErrorIncoming::ConnectionErrorIncoming {
488 connection_error: ConnectionErrorIncoming::InternalError(msg.to_string()),
489 },
490 }
491}
492
493/// The winner only if it is an *authoritative* (non-provisional) cause. A
494/// provisional cancellation marker ([`SendTerminal::ProvisionalAbort`], MF-2) is
495/// treated as "no definitive winner yet": it is never surfaced to the caller and
496/// never freezes the slot, so an authoritative local candidate (a specific cause,
497/// a `LocalReset`, or the closure-point abort) refines it instead.
498fn definitive(terminal: &Option<SendTerminal>) -> Option<SendTerminal> {
499 terminal.clone().filter(|w| !w.is_provisional())
500}
501
502/// Whether an unobserved provisional cancellation marker is currently retained in
503/// the shared slot (MF-2). Drives the closure-point decisions in the event arms.
504fn provisional_pending(terminal: &Option<SendTerminal>) -> bool {
505 terminal.as_ref().is_some_and(SendTerminal::is_provisional)
506}
507
508/// Pure, native-free send transition. Mutates only `state`; emits one command.
509///
510/// One exhaustive `match` in the normative precedence order: `*Submitted`
511/// bookkeeping first (always clears its reservation before any winner), then
512/// absorbing/queued finish, then method-specific terminal handling, then event
513/// rows. Every impossible pairing is an explicit `Internal` publish, never a panic.
514///
515/// The `terminal` field of every input is a **non-freezing snapshot** of the
516/// resolved shared-slot winner (the frontend's `resolve_terminal`); the reducer
517/// consults it only through [`definitive`] / [`provisional_pending`], so a
518/// provisional MF-2 cancellation marker is never observed by the caller — it is
519/// refined by a paired terminal callback or finalized to an authoritative abort at
520/// the closure point (channel close, finish/reset completion). The reducer never
521/// freezes the shared connection slot: when it emits a [`SendCommand::ReturnError`]
522/// carrying a connection cause, the frontend commits (freezes) that connection
523/// identity via `commit_send_winner` at the exact delivery point, so the freeze
524/// happens only when the cause is actually returned to h3 (commit-on-delivery).
525pub(crate) fn transition(state: &mut SendState, input: SendInput) -> SendCommand {
526 use SendCommand::*;
527 use SendInput::*;
528 use TerminalContinuation as K;
529 let aborted = || SendTerminal::Failed(aborted_status());
530
531 match input {
532 // ── Precedence 1: *Submitted bookkeeping. Clears its reservation before
533 // any winner is applied; a missing reservation is internal.
534 SendSubmitted { result, terminal } => {
535 if !state.send_submitting {
536 return PublishTerminal {
537 candidate: SendTerminal::Internal("SendSubmitted without reservation"),
538 continuation: K::SendData,
539 };
540 }
541 state.send_submitting = false;
542 match result {
543 Ok(()) => {
544 state.send_inprogress = true;
545 match definitive(&terminal) {
546 Some(w) => ReturnError(w), // a specific winner raced in
547 None => ReturnSent, // send_data's Ok(())
548 }
549 }
550 // Buffer already reclaimed by the caller on the Err path. A real
551 // native failure is authoritative; it refines a provisional marker.
552 Err(status) => PublishTerminal {
553 candidate: definitive(&terminal).unwrap_or(SendTerminal::Failed(status)),
554 continuation: K::SendData,
555 },
556 }
557 }
558 GracefulSubmitted { result, terminal } => {
559 if !state.finish_submitting {
560 return PublishTerminal {
561 candidate: SendTerminal::Internal("GracefulSubmitted without reservation"),
562 continuation: K::PollFinish,
563 };
564 }
565 state.finish_submitting = false;
566 match result {
567 Ok(()) => {
568 state.finish_started = true;
569 RepollFinish // never Pending directly (drain a queued FinishComplete)
570 }
571 Err(status) => PublishTerminal {
572 candidate: definitive(&terminal).unwrap_or(SendTerminal::Failed(status)),
573 continuation: K::PollFinish,
574 },
575 }
576 }
577 ResetSubmitted {
578 code,
579 result,
580 terminal,
581 } => {
582 if !state.reset_submitting {
583 return PublishTerminal {
584 candidate: SendTerminal::Internal("ResetSubmitted without reservation"),
585 continuation: K::Reset,
586 };
587 }
588 state.reset_submitting = false;
589 // Reset completion is a closure point: an authoritative `LocalReset`
590 // (or native failure) refines any retained provisional cancellation.
591 let candidate = match result {
592 Ok(()) => definitive(&terminal).unwrap_or(SendTerminal::LocalReset(code)),
593 Err(status) => definitive(&terminal).unwrap_or(SendTerminal::Failed(status)),
594 };
595 PublishTerminal {
596 candidate,
597 continuation: K::Reset,
598 }
599 }
600
601 // ── Consume the resolved winner (no state mutation).
602 TerminalPublished {
603 winner,
604 continuation,
605 } => {
606 if winner.is_provisional() {
607 // MF-2: an unobserved provisional cancellation is never returned to
608 // the caller. Re-poll to drain a synchronously-queued paired terminal
609 // / channel close (the closure point) or re-arm the waker.
610 match continuation {
611 K::PollReady => RepollReady,
612 K::PollFinish => RepollFinish,
613 // Provisional markers never arise on the send_data / reset paths.
614 K::SendData | K::Reset => NoOp,
615 }
616 } else {
617 match continuation {
618 K::SendData | K::PollReady | K::PollFinish => ReturnError(winner),
619 K::Reset => NoOp, // infallible method; winner stored for a later poll
620 }
621 }
622 }
623
624 // ── send_data request. Terminal wins; else misuse if busy/finishing.
625 SendRequested { payload, terminal } => {
626 if let Some(w) = definitive(&terminal) {
627 return ReturnError(w);
628 }
629 if state.send_inprogress
630 || state.send_submitting
631 || state.finish_submitting
632 || state.finish_started
633 || state.finish_complete
634 {
635 return ReturnImmediateError(SendOperationError::Misuse(
636 "send_data called while a send is already in progress",
637 ));
638 }
639 match payload {
640 // idle, no winner
641 SendPayload::Empty => ReturnSent, // no-op, no reservation
642 SendPayload::Oversized { len, ceiling } => {
643 ReturnImmediateError(SendOperationError::OversizedSend { len, ceiling }) // state unchanged
644 }
645 SendPayload::NonEmpty { .. } => {
646 state.send_submitting = true;
647 SubmitSend
648 }
649 }
650 }
651
652 // ── reset request (infallible; at most one native SubmitReset).
653 Reset { code, terminal } => {
654 // A provisional cancellation does NOT block the reset: reset is a
655 // closure point whose authoritative `LocalReset` refines it.
656 if state.finish_complete || definitive(&terminal).is_some() {
657 return NoOp; // already terminal; nothing to submit
658 }
659 if state.submission_reserved() {
660 return PublishTerminal {
661 // impossible under serialized callers; never a 2nd native op
662 candidate: SendTerminal::Internal("reset during submission reservation"),
663 continuation: K::Reset,
664 };
665 }
666 state.reset_submitting = true; // otherwise, incl. incomplete finish
667 SubmitReset(code)
668 }
669
670 // ── poll_ready request. A present shared terminal is surfaced first.
671 PollReady { poll, terminal } => {
672 if state.submission_reserved() {
673 return PublishTerminal {
674 candidate: SendTerminal::Internal("poll_ready during submission reservation"),
675 continuation: K::PollReady,
676 };
677 }
678 // Method-terminal step: an authoritative winner already in the shared
679 // slot is returned before ordinary event handling. A provisional
680 // cancellation marker is deliberately NOT surfaced here (MF-2).
681 if let Some(w) = definitive(&terminal) {
682 state.send_inprogress = false;
683 return ReturnError(w);
684 }
685 if state.finish_started {
686 // h3 misuse; no native call, non-sticky (reproduced each call).
687 return ReturnError(SendTerminal::Internal("poll_ready after finish"));
688 }
689 // A retained, unobserved provisional cancellation (MF-2) is pending.
690 let provisional = provisional_pending(&terminal);
691 match poll {
692 // Channel close is a closure point: finalize the provisional to an
693 // authoritative abort; otherwise it is an adapter-internal fault.
694 SendPoll::Closed if provisional => PublishTerminal {
695 candidate: aborted(),
696 continuation: K::PollReady,
697 },
698 SendPoll::Closed => PublishTerminal {
699 candidate: SendTerminal::Internal("send channel closed without a terminal"),
700 continuation: K::PollReady,
701 },
702 SendPoll::Event(SendEvent::Complete { cancelled: false }) => {
703 if !state.send_inprogress {
704 return PublishTerminal {
705 candidate: SendTerminal::Internal(
706 "SendComplete without an outstanding send",
707 ),
708 continuation: K::PollReady,
709 };
710 }
711 state.send_inprogress = false;
712 ReturnReady
713 }
714 SendPoll::Event(SendEvent::Complete { cancelled: true }) => {
715 if !state.send_inprogress {
716 return PublishTerminal {
717 candidate: SendTerminal::Internal(
718 "cancelled SendComplete without an outstanding send",
719 ),
720 continuation: K::PollReady,
721 };
722 }
723 state.send_inprogress = false;
724 // No authoritative winner (handled above): retain an UNOBSERVED
725 // provisional cancellation marker in the shared slot (MF-2). It is
726 // never returned to the caller — the `PublishTerminal` winner is
727 // fed back as a provisional `TerminalPublished`, which re-polls.
728 PublishTerminal {
729 candidate: SendTerminal::ProvisionalAbort,
730 continuation: K::PollReady,
731 }
732 }
733 // A lone terminal wake normally means a specific cause was published
734 // (surfaced above). With a provisional pending and no specific cause,
735 // treat it as the closure point and finalize to an authoritative abort.
736 SendPoll::Event(SendEvent::TerminalWake) if provisional => PublishTerminal {
737 candidate: aborted(),
738 continuation: K::PollReady,
739 },
740 SendPoll::Event(SendEvent::TerminalWake) => PublishTerminal {
741 candidate: SendTerminal::Internal("terminal wake without a terminal"),
742 continuation: K::PollReady,
743 },
744 SendPoll::Event(SendEvent::FinishComplete { .. }) => PublishTerminal {
745 candidate: SendTerminal::Internal("finish event observed on poll_ready"),
746 continuation: K::PollReady,
747 },
748 SendPoll::Pending => {
749 if state.send_inprogress {
750 Pending
751 } else if provisional {
752 // Retained provisional cancellation, no closure event yet:
753 // wait (the repoll registered the waker) — never ReturnReady.
754 Pending
755 } else {
756 ReturnReady
757 }
758 }
759 }
760 }
761
762 // ── poll_finish request. Absorbing finish and a just-completed graceful
763 // finish both precede the method-terminal step and event handling.
764 PollFinish { poll, terminal } => {
765 if state.finish_complete {
766 return ReturnFinished; // absorbing
767 }
768 if state.submission_reserved() {
769 return PublishTerminal {
770 candidate: SendTerminal::Internal("poll_finish during submission reservation"),
771 continuation: K::PollFinish,
772 };
773 }
774 // Documented successful-finish exception: a graceful finish that
775 // actually completed wins even over a present terminal.
776 if let SendPoll::Event(SendEvent::FinishComplete { graceful }) = poll {
777 if !state.finish_started {
778 return PublishTerminal {
779 candidate: SendTerminal::Internal(
780 "finish complete without a started finish",
781 ),
782 continuation: K::PollFinish,
783 };
784 }
785 if graceful {
786 state.finish_complete = true;
787 return ReturnFinished;
788 }
789 // graceful == false while finishing is a closure point: an
790 // authoritative winner wins, else finalize to an abort.
791 return PublishTerminal {
792 candidate: definitive(&terminal).unwrap_or_else(aborted),
793 continuation: K::PollFinish,
794 };
795 }
796 // Method-terminal step: after the finish exceptions, an authoritative
797 // winner is surfaced before any Pending/SubmitGraceful. A provisional
798 // cancellation marker is deliberately NOT surfaced here (MF-2).
799 if let Some(w) = definitive(&terminal) {
800 state.send_inprogress = false;
801 return ReturnError(w);
802 }
803 let provisional = provisional_pending(&terminal);
804 match poll {
805 // Channel close is a closure point for a retained provisional.
806 SendPoll::Closed if provisional => PublishTerminal {
807 candidate: aborted(),
808 continuation: K::PollFinish,
809 },
810 SendPoll::Closed => PublishTerminal {
811 candidate: SendTerminal::Internal("send channel closed without a terminal"),
812 continuation: K::PollFinish,
813 },
814 SendPoll::Event(SendEvent::Complete { cancelled: false })
815 if !state.finish_started =>
816 {
817 if !state.send_inprogress {
818 return PublishTerminal {
819 candidate: SendTerminal::Internal(
820 "SendComplete without an outstanding send",
821 ),
822 continuation: K::PollFinish,
823 };
824 }
825 state.send_inprogress = false;
826 state.finish_submitting = true;
827 SubmitGraceful
828 }
829 SendPoll::Event(SendEvent::Complete { cancelled: true })
830 if !state.finish_started =>
831 {
832 if !state.send_inprogress {
833 return PublishTerminal {
834 candidate: SendTerminal::Internal(
835 "cancelled SendComplete without an outstanding send",
836 ),
837 continuation: K::PollFinish,
838 };
839 }
840 state.send_inprogress = false;
841 // Retain an UNOBSERVED provisional cancellation marker (MF-2); the
842 // provisional `TerminalPublished` re-polls rather than returning it.
843 PublishTerminal {
844 candidate: SendTerminal::ProvisionalAbort,
845 continuation: K::PollFinish,
846 }
847 }
848 // A lone terminal wake with a provisional pending is the closure point.
849 SendPoll::Event(SendEvent::TerminalWake) if provisional => PublishTerminal {
850 candidate: aborted(),
851 continuation: K::PollFinish,
852 },
853 SendPoll::Event(SendEvent::TerminalWake) => PublishTerminal {
854 candidate: SendTerminal::Internal("terminal wake without a terminal"),
855 continuation: K::PollFinish,
856 },
857 SendPoll::Pending if state.finish_started => Pending,
858 SendPoll::Pending if state.send_inprogress => Pending, // wait for in-flight send
859 // Retained provisional cancellation, no closure event yet: wait for
860 // the closure point rather than submitting a graceful finish on a
861 // cancelled stream (the repoll already registered the waker).
862 SendPoll::Pending if provisional => Pending,
863 SendPoll::Pending => {
864 state.finish_submitting = true;
865 SubmitGraceful // idle, not finishing
866 }
867 // Any remaining event/state pair (e.g. a data completion while
868 // finishing) is adapter-internal, never a panic.
869 SendPoll::Event(_) => PublishTerminal {
870 candidate: SendTerminal::Internal("impossible poll_finish event/state"),
871 continuation: K::PollFinish,
872 },
873 }
874 }
875 }
876}
877
878#[cfg(test)]
879mod tests {
880 use super::*;
881
882 #[test]
883 fn clamp_application_code_table() {
884 // (name, input, expected)
885 let cases: &[(&str, u64, u64)] = &[
886 ("small value unchanged", 42, 42),
887 ("zero unchanged", 0, 0),
888 ("max varint unchanged", (1u64 << 62) - 1, (1u64 << 62) - 1),
889 ("one over max clamps", 1u64 << 62, (1u64 << 62) - 1),
890 ("u64::MAX clamps", u64::MAX, (1u64 << 62) - 1),
891 ];
892 for (name, input, expected) in cases {
893 assert_eq!(clamp_application_code(*input), *expected, "case: {name}");
894 }
895 }
896
897 #[test]
898 fn max_quic_varint_value() {
899 assert_eq!(MAX_QUIC_VARINT, (1u64 << 62) - 1);
900 }
901
902 /// Error-mapping matrix (Phases 3–7): every adapter terminal → h3 result row
903 /// from the design's "Required mapping" conversion table, asserted through the
904 /// authoritative `convert_conn` / `convert_recv` / `convert_send` helpers.
905 /// These are the ONLY sites that mint h3 error values, so proving the table
906 /// here proves the whole propagation surface's terminal-to-h3 contract.
907 #[test]
908 fn convert_conn_matrix() {
909 // PeerApplication(code) → ApplicationClose { code }
910 match convert_conn(ConnectionTerminal::PeerApplication(0x42)) {
911 ConnectionErrorIncoming::ApplicationClose { error_code } => {
912 assert_eq!(error_code, 0x42)
913 }
914 other => panic!("PeerApplication → {other:?}"),
915 }
916 // Timeout → Timeout
917 assert!(matches!(
918 convert_conn(ConnectionTerminal::Timeout),
919 ConnectionErrorIncoming::Timeout
920 ));
921 // Transport { status, error_code } → Undefined(MsQuicTransportError)
922 match convert_conn(ConnectionTerminal::Transport {
923 status: Status::new(StatusCode::QUIC_STATUS_TLS_ERROR),
924 error_code: 7,
925 }) {
926 ConnectionErrorIncoming::Undefined(e) => {
927 let t = e
928 .downcast_ref::<MsQuicTransportError>()
929 .expect("Transport → MsQuicTransportError");
930 // Both the wire transport error code AND the accompanying QUIC
931 // status must be preserved verbatim through the conversion.
932 assert_eq!(t.error_code, 7, "transport error code preserved");
933 assert_eq!(
934 t.status
935 .try_as_status_code()
936 .expect("known transport status"),
937 StatusCode::QUIC_STATUS_TLS_ERROR,
938 "transport status preserved verbatim"
939 );
940 }
941 other => panic!("Transport → {other:?}"),
942 }
943 // LocalClose → Undefined(LocalConnectionClose)
944 match convert_conn(ConnectionTerminal::LocalClose) {
945 ConnectionErrorIncoming::Undefined(e) => {
946 assert!(e.downcast_ref::<LocalConnectionClose>().is_some());
947 }
948 other => panic!("LocalClose → {other:?}"),
949 }
950 // Internal(msg) → InternalError(msg)
951 match convert_conn(ConnectionTerminal::Internal("boom")) {
952 ConnectionErrorIncoming::InternalError(m) => assert_eq!(m, "boom"),
953 other => panic!("Internal → {other:?}"),
954 }
955 }
956
957 #[test]
958 fn convert_recv_matrix() {
959 // Fin → Ok(None)
960 assert!(matches!(convert_recv(ReceiveTerminal::Fin), Ok(None)));
961 // Reset(code) → StreamTerminated { code }
962 match convert_recv(ReceiveTerminal::Reset(0x55)) {
963 Err(StreamErrorIncoming::StreamTerminated { error_code }) => {
964 assert_eq!(error_code, 0x55)
965 }
966 other => panic!("Reset → {other:?}"),
967 }
968 // Connection(reason) → ConnectionErrorIncoming using the conn conversion
969 match convert_recv(ReceiveTerminal::Connection(ConnectionTerminal::Timeout)) {
970 Err(StreamErrorIncoming::ConnectionErrorIncoming { connection_error }) => {
971 assert!(matches!(connection_error, ConnectionErrorIncoming::Timeout));
972 }
973 other => panic!("Connection → {other:?}"),
974 }
975 // Internal(msg) → ConnectionErrorIncoming(InternalError)
976 match convert_recv(ReceiveTerminal::Internal("recv-boom")) {
977 Err(StreamErrorIncoming::ConnectionErrorIncoming {
978 connection_error: ConnectionErrorIncoming::InternalError(m),
979 }) => assert_eq!(m, "recv-boom"),
980 other => panic!("Internal → {other:?}"),
981 }
982 }
983
984 #[test]
985 fn convert_send_matrix() {
986 // Stopped(code) → StreamTerminated { code }
987 match convert_send(SendTerminal::Stopped(0x66)) {
988 StreamErrorIncoming::StreamTerminated { error_code } => assert_eq!(error_code, 0x66),
989 other => panic!("Stopped → {other:?}"),
990 }
991 // Connection(reason) → ConnectionErrorIncoming using the conn conversion
992 match convert_send(SendTerminal::Connection(
993 ConnectionTerminal::PeerApplication(9),
994 )) {
995 StreamErrorIncoming::ConnectionErrorIncoming {
996 connection_error: ConnectionErrorIncoming::ApplicationClose { error_code },
997 } => assert_eq!(error_code, 9),
998 other => panic!("Connection → {other:?}"),
999 }
1000 // LocalReset(code) → Unknown(LocalStreamReset)
1001 match convert_send(SendTerminal::LocalReset(0x77)) {
1002 StreamErrorIncoming::Unknown(e) => {
1003 let r = e
1004 .downcast_ref::<LocalStreamReset>()
1005 .expect("LocalReset → LocalStreamReset");
1006 assert_eq!(r.code, 0x77);
1007 }
1008 other => panic!("LocalReset → {other:?}"),
1009 }
1010 // Failed(status) → Unknown(status), preserving the exact QUIC status.
1011 match convert_send(SendTerminal::Failed(Status::new(
1012 StatusCode::QUIC_STATUS_ABORTED,
1013 ))) {
1014 StreamErrorIncoming::Unknown(e) => {
1015 let s = e.downcast_ref::<Status>().expect("Failed → Status");
1016 assert_eq!(
1017 s.try_as_status_code().expect("known failed status"),
1018 StatusCode::QUIC_STATUS_ABORTED,
1019 "failed send status preserved verbatim"
1020 );
1021 }
1022 other => panic!("Failed → {other:?}"),
1023 }
1024 // ProvisionalAbort → Unknown(aborted status) (defensive fallback); the
1025 // synthesized status is the authoritative QUIC_STATUS_ABORTED.
1026 match convert_send(SendTerminal::ProvisionalAbort) {
1027 StreamErrorIncoming::Unknown(e) => {
1028 let s = e
1029 .downcast_ref::<Status>()
1030 .expect("ProvisionalAbort → Status");
1031 assert_eq!(
1032 s.try_as_status_code().expect("known provisional status"),
1033 StatusCode::QUIC_STATUS_ABORTED,
1034 "provisional abort maps to the aborted status"
1035 );
1036 }
1037 other => panic!("ProvisionalAbort → {other:?}"),
1038 }
1039 // Internal(msg) → ConnectionErrorIncoming(InternalError)
1040 match convert_send(SendTerminal::Internal("send-boom")) {
1041 StreamErrorIncoming::ConnectionErrorIncoming {
1042 connection_error: ConnectionErrorIncoming::InternalError(m),
1043 } => assert_eq!(m, "send-boom"),
1044 other => panic!("Internal → {other:?}"),
1045 }
1046 }
1047}
1048
1049/// Table-driven tests for the pure send-side reducer (Phase 7).
1050///
1051/// Every case drives [`transition`] directly against a [`SendState`], so the
1052/// full send state machine is exercised with NO native handle. The reducer is
1053/// pure (mutates only `state`, emits one [`SendCommand`]); the frontend executor
1054/// loops and the shared-slot first-writer refinement (`publish_send`) live in
1055/// `stream.rs`/`terminal.rs` and are covered by the `send_seam` tests
1056/// (`send_seam.rs`).
1057#[cfg(test)]
1058mod reducer_tests {
1059 use super::*;
1060
1061 // `SendCommand`/`SendTerminal` embed `Status`, which is not `PartialEq`, so
1062 // these helpers pattern-match rather than compare by value.
1063
1064 /// A `Connection(LocalClose)` terminal, a convenient concrete winner.
1065 fn conn_terminal() -> SendTerminal {
1066 SendTerminal::Connection(ConnectionTerminal::LocalClose)
1067 }
1068
1069 /// Drive the (state, input) -> command reducer once.
1070 fn step(state: &mut SendState, input: SendInput) -> SendCommand {
1071 transition(state, input)
1072 }
1073
1074 // ── MF-1: chronological finish-vs-terminal ordering via the single source ──
1075
1076 #[test]
1077 fn mf1_finish_first_completes_cleanly_then_absorbs_terminal() {
1078 // A finish was submitted; its completion is dequeued BEFORE any terminal
1079 // wake. Success is absorbing: the later terminal cannot overwrite it.
1080 let mut st = SendState {
1081 finish_started: true,
1082 ..SendState::new()
1083 };
1084 let cmd = step(
1085 &mut st,
1086 SendInput::PollFinish {
1087 poll: SendPoll::Event(SendEvent::FinishComplete { graceful: true }),
1088 terminal: None,
1089 },
1090 );
1091 assert!(matches!(cmd, SendCommand::ReturnFinished));
1092 assert!(st.finish_complete, "graceful completion marks finish done");
1093
1094 // A terminal wake dequeued afterwards is absorbed: still a clean finish.
1095 let cmd = step(
1096 &mut st,
1097 SendInput::PollFinish {
1098 poll: SendPoll::Event(SendEvent::TerminalWake),
1099 terminal: Some(conn_terminal()),
1100 },
1101 );
1102 assert!(
1103 matches!(cmd, SendCommand::ReturnFinished),
1104 "a later terminal must not overwrite a successful finish"
1105 );
1106 }
1107
1108 #[test]
1109 fn mf1_terminal_first_wins_over_later_finish() {
1110 // The terminal wake is dequeued FIRST (its cause already published). The
1111 // finish never completes: the terminal is surfaced as the error.
1112 let mut st = SendState {
1113 finish_started: true,
1114 ..SendState::new()
1115 };
1116 let cmd = step(
1117 &mut st,
1118 SendInput::PollFinish {
1119 poll: SendPoll::Event(SendEvent::TerminalWake),
1120 terminal: Some(SendTerminal::Stopped(7)),
1121 },
1122 );
1123 match cmd {
1124 SendCommand::ReturnError(SendTerminal::Stopped(7)) => {}
1125 other => panic!("expected ReturnError(Stopped(7)), got {other:?}"),
1126 }
1127 assert!(
1128 !st.finish_complete,
1129 "a terminal-first order must not mark finish complete"
1130 );
1131 }
1132
1133 #[test]
1134 fn finish_event_not_overwritten_by_later_terminal() {
1135 // Absorbing success precedes the method-terminal step even when a winner
1136 // is present in the slot.
1137 let mut st = SendState {
1138 finish_complete: true,
1139 ..SendState::new()
1140 };
1141 let cmd = step(
1142 &mut st,
1143 SendInput::PollFinish {
1144 poll: SendPoll::Pending,
1145 terminal: Some(conn_terminal()),
1146 },
1147 );
1148 assert!(matches!(cmd, SendCommand::ReturnFinished));
1149 }
1150
1151 // ── SF-2: poll_ready-after-finish is a non-sticky internal error ──
1152
1153 #[test]
1154 fn poll_ready_after_finish_is_nonsticky_internal() {
1155 // The reducer arm returns an Internal error without mutating state (the
1156 // frontend guard additionally avoids consuming the channel; see the
1157 // liveness test in `send_seam.rs`).
1158 let mut st = SendState {
1159 finish_started: true,
1160 ..SendState::new()
1161 };
1162 let before = st;
1163 let cmd = step(
1164 &mut st,
1165 SendInput::PollReady {
1166 poll: SendPoll::Pending,
1167 terminal: None,
1168 },
1169 );
1170 match cmd {
1171 SendCommand::ReturnError(SendTerminal::Internal(m)) => {
1172 assert_eq!(m, "poll_ready after finish")
1173 }
1174 other => panic!("expected ReturnError(Internal), got {other:?}"),
1175 }
1176 assert_eq!(st, before, "poll_ready-after-finish must not mutate state");
1177 }
1178
1179 // ── reset: infallible, at most one clamped native RESET_STREAM ──
1180
1181 #[test]
1182 fn reset_emits_single_submit_reset_then_publishes_local_reset() {
1183 let mut st = SendState::new();
1184 let code = (1u64 << 62) - 1;
1185 let cmd = step(
1186 &mut st,
1187 SendInput::Reset {
1188 code,
1189 terminal: None,
1190 },
1191 );
1192 match cmd {
1193 SendCommand::SubmitReset(c) => assert_eq!(c, code),
1194 other => panic!("expected SubmitReset, got {other:?}"),
1195 }
1196 assert!(st.reset_submitting, "reset reserves its submission");
1197
1198 let cmd = step(
1199 &mut st,
1200 SendInput::ResetSubmitted {
1201 code,
1202 result: Ok(()),
1203 terminal: None,
1204 },
1205 );
1206 match cmd {
1207 SendCommand::PublishTerminal {
1208 candidate: SendTerminal::LocalReset(c),
1209 continuation: TerminalContinuation::Reset,
1210 } => assert_eq!(c, code),
1211 other => panic!("expected PublishTerminal(LocalReset), got {other:?}"),
1212 }
1213 assert!(!st.reset_submitting, "the reservation is cleared");
1214
1215 // The resolved winner is fed back: reset is infallible, so NoOp.
1216 let cmd = step(
1217 &mut st,
1218 SendInput::TerminalPublished {
1219 winner: SendTerminal::LocalReset(code),
1220 continuation: TerminalContinuation::Reset,
1221 },
1222 );
1223 assert!(matches!(cmd, SendCommand::NoOp));
1224 }
1225
1226 #[test]
1227 fn reset_after_terminal_is_noop() {
1228 let mut st = SendState::new();
1229 let cmd = step(
1230 &mut st,
1231 SendInput::Reset {
1232 code: 5,
1233 terminal: Some(SendTerminal::Stopped(9)),
1234 },
1235 );
1236 assert!(matches!(cmd, SendCommand::NoOp));
1237 assert!(!st.reset_submitting, "no native reset after a terminal");
1238 }
1239
1240 #[test]
1241 fn reset_after_finish_complete_is_noop() {
1242 let mut st = SendState {
1243 finish_complete: true,
1244 ..SendState::new()
1245 };
1246 let cmd = step(
1247 &mut st,
1248 SendInput::Reset {
1249 code: 5,
1250 terminal: None,
1251 },
1252 );
1253 assert!(matches!(cmd, SendCommand::NoOp));
1254 }
1255
1256 #[test]
1257 fn local_reset_loses_to_published_peer_terminal() {
1258 // Local reset raced a peer STOP_SENDING that landed before ResetSubmitted:
1259 // the callback-published terminal wins, no second native op.
1260 let mut st = SendState::new();
1261 let _ = step(
1262 &mut st,
1263 SendInput::Reset {
1264 code: 3,
1265 terminal: None,
1266 },
1267 );
1268 let cmd = step(
1269 &mut st,
1270 SendInput::ResetSubmitted {
1271 code: 3,
1272 result: Ok(()),
1273 terminal: Some(SendTerminal::Stopped(9)),
1274 },
1275 );
1276 match cmd {
1277 SendCommand::PublishTerminal {
1278 candidate: SendTerminal::Stopped(9),
1279 continuation: TerminalContinuation::Reset,
1280 } => {}
1281 other => panic!("expected PublishTerminal(Stopped(9)), got {other:?}"),
1282 }
1283 assert!(!st.reset_submitting, "reservation cleared on the race");
1284 }
1285
1286 #[test]
1287 fn local_reset_loses_to_published_connection_terminal() {
1288 // Same race against a connection shutdown.
1289 let mut st = SendState::new();
1290 let _ = step(
1291 &mut st,
1292 SendInput::Reset {
1293 code: 3,
1294 terminal: None,
1295 },
1296 );
1297 let cmd = step(
1298 &mut st,
1299 SendInput::ResetSubmitted {
1300 code: 3,
1301 result: Ok(()),
1302 terminal: Some(conn_terminal()),
1303 },
1304 );
1305 assert!(matches!(
1306 cmd,
1307 SendCommand::PublishTerminal {
1308 candidate: SendTerminal::Connection(_),
1309 continuation: TerminalContinuation::Reset,
1310 }
1311 ));
1312 }
1313
1314 // ── idempotent finish: exactly one graceful shutdown ──
1315
1316 #[test]
1317 fn idempotent_finish_submits_graceful_once() {
1318 let mut st = SendState::new();
1319 // First poll_finish on an idle stream submits the graceful shutdown.
1320 let cmd = step(
1321 &mut st,
1322 SendInput::PollFinish {
1323 poll: SendPoll::Pending,
1324 terminal: None,
1325 },
1326 );
1327 assert!(matches!(cmd, SendCommand::SubmitGraceful));
1328 assert!(st.finish_submitting);
1329
1330 // The submit succeeds: reservation clears, finish is started, re-poll.
1331 let cmd = step(
1332 &mut st,
1333 SendInput::GracefulSubmitted {
1334 result: Ok(()),
1335 terminal: None,
1336 },
1337 );
1338 assert!(matches!(cmd, SendCommand::RepollFinish));
1339 assert!(st.finish_started && !st.finish_submitting);
1340
1341 // A subsequent poll_finish while awaiting completion does NOT re-submit.
1342 let cmd = step(
1343 &mut st,
1344 SendInput::PollFinish {
1345 poll: SendPoll::Pending,
1346 terminal: None,
1347 },
1348 );
1349 assert!(
1350 matches!(cmd, SendCommand::Pending),
1351 "no second graceful shutdown while finishing"
1352 );
1353 }
1354
1355 // ── *Submitted without a reservation is internal, never a panic ──
1356
1357 #[test]
1358 fn submitted_without_reservation_is_internal() {
1359 for input in [
1360 SendInput::SendSubmitted {
1361 result: Ok(()),
1362 terminal: None,
1363 },
1364 SendInput::GracefulSubmitted {
1365 result: Ok(()),
1366 terminal: None,
1367 },
1368 SendInput::ResetSubmitted {
1369 code: 1,
1370 result: Ok(()),
1371 terminal: None,
1372 },
1373 ] {
1374 let mut st = SendState::new();
1375 let cmd = step(&mut st, input);
1376 assert!(
1377 matches!(
1378 cmd,
1379 SendCommand::PublishTerminal {
1380 candidate: SendTerminal::Internal(_),
1381 ..
1382 }
1383 ),
1384 "a *Submitted without a reservation must publish Internal"
1385 );
1386 }
1387 }
1388
1389 // ── send_data request precedence ──
1390
1391 #[test]
1392 fn send_requested_terminal_wins() {
1393 let mut st = SendState::new();
1394 let cmd = step(
1395 &mut st,
1396 SendInput::SendRequested {
1397 payload: SendPayload::NonEmpty { len: 4 },
1398 terminal: Some(SendTerminal::Stopped(1)),
1399 },
1400 );
1401 match cmd {
1402 SendCommand::ReturnError(SendTerminal::Stopped(1)) => {}
1403 other => panic!("expected ReturnError(Stopped(1)), got {other:?}"),
1404 }
1405 assert!(!st.send_submitting, "no submission behind a terminal");
1406 }
1407
1408 #[test]
1409 fn send_requested_while_busy_is_misuse() {
1410 let mut st = SendState {
1411 send_inprogress: true,
1412 ..SendState::new()
1413 };
1414 let cmd = step(
1415 &mut st,
1416 SendInput::SendRequested {
1417 payload: SendPayload::NonEmpty { len: 4 },
1418 terminal: None,
1419 },
1420 );
1421 assert!(matches!(
1422 cmd,
1423 SendCommand::ReturnImmediateError(SendOperationError::Misuse(_))
1424 ));
1425 }
1426
1427 #[test]
1428 fn send_empty_is_noop_and_oversized_is_immediate_error() {
1429 let mut st = SendState::new();
1430 let cmd = step(
1431 &mut st,
1432 SendInput::SendRequested {
1433 payload: SendPayload::Empty,
1434 terminal: None,
1435 },
1436 );
1437 assert!(matches!(cmd, SendCommand::ReturnSent));
1438 assert!(!st.send_submitting, "empty send reserves nothing");
1439
1440 let mut st = SendState::new();
1441 let cmd = step(
1442 &mut st,
1443 SendInput::SendRequested {
1444 payload: SendPayload::Oversized {
1445 len: 999,
1446 ceiling: 4096,
1447 },
1448 terminal: None,
1449 },
1450 );
1451 assert!(matches!(
1452 cmd,
1453 SendCommand::ReturnImmediateError(SendOperationError::OversizedSend {
1454 len: 999,
1455 ceiling: 4096
1456 })
1457 ));
1458 assert_eq!(
1459 st,
1460 SendState::new(),
1461 "oversized rejection leaves state clean"
1462 );
1463 }
1464
1465 #[test]
1466 fn send_requested_nonempty_reserves_and_submits() {
1467 let mut st = SendState::new();
1468 let cmd = step(
1469 &mut st,
1470 SendInput::SendRequested {
1471 payload: SendPayload::NonEmpty { len: 8 },
1472 terminal: None,
1473 },
1474 );
1475 assert!(matches!(cmd, SendCommand::SubmitSend));
1476 assert!(st.send_submitting);
1477
1478 let cmd = step(
1479 &mut st,
1480 SendInput::SendSubmitted {
1481 result: Ok(()),
1482 terminal: None,
1483 },
1484 );
1485 assert!(matches!(cmd, SendCommand::ReturnSent));
1486 assert!(st.send_inprogress && !st.send_submitting);
1487 }
1488
1489 // ── SC-011 / MF-2: send-cancellation retained-provisional / closure point ──
1490
1491 #[test]
1492 fn sc011_cancelled_complete_no_cause_retains_unobserved_provisional() {
1493 // A cancelled SendComplete with NO published cause retains the DISTINCT
1494 // `ProvisionalAbort` marker in the shared slot (MF-2). It is provisional
1495 // (refinable) and, crucially, is fed back as a provisional `TerminalPublished`
1496 // that RE-POLLS rather than returning — the caller never observes it.
1497 let mut st = SendState {
1498 send_inprogress: true,
1499 ..SendState::new()
1500 };
1501 let cmd = step(
1502 &mut st,
1503 SendInput::PollReady {
1504 poll: SendPoll::Event(SendEvent::Complete { cancelled: true }),
1505 terminal: None,
1506 },
1507 );
1508 match cmd {
1509 SendCommand::PublishTerminal {
1510 candidate,
1511 continuation: TerminalContinuation::PollReady,
1512 } => {
1513 assert!(candidate.is_provisional(), "retains a provisional marker");
1514 assert!(
1515 matches!(candidate, SendTerminal::ProvisionalAbort),
1516 "the marker is the distinct ProvisionalAbort, not a real Failed"
1517 );
1518 }
1519 other => panic!("expected PublishTerminal(ProvisionalAbort), got {other:?}"),
1520 }
1521 assert!(!st.send_inprogress);
1522
1523 // The provisional winner fed back must NOT be returned to the caller; it
1524 // re-polls to reach the closure point / drain a paired terminal.
1525 let cmd = step(
1526 &mut st,
1527 SendInput::TerminalPublished {
1528 winner: SendTerminal::ProvisionalAbort,
1529 continuation: TerminalContinuation::PollReady,
1530 },
1531 );
1532 assert!(
1533 matches!(cmd, SendCommand::RepollReady),
1534 "a provisional winner re-polls (unobserved), never ReturnError"
1535 );
1536 }
1537
1538 #[test]
1539 fn mf2_provisional_refines_to_specific_before_observation() {
1540 // Cancellation-FIRST order: after the provisional is retained, a paired peer
1541 // cause published into the slot is surfaced on the re-poll — the caller's
1542 // FIRST observation is the refined specific cause, never the abort.
1543 let mut st = SendState {
1544 send_inprogress: false, // cleared by the prior cancelled completion
1545 ..SendState::new()
1546 };
1547 let cmd = step(
1548 &mut st,
1549 SendInput::PollReady {
1550 poll: SendPoll::Pending,
1551 terminal: Some(SendTerminal::Stopped(9)), // refined in the slot
1552 },
1553 );
1554 match cmd {
1555 SendCommand::ReturnError(SendTerminal::Stopped(9)) => {}
1556 other => panic!("expected refined ReturnError(Stopped(9)), got {other:?}"),
1557 }
1558 }
1559
1560 #[test]
1561 fn mf2_provisional_channel_close_is_closure_point_to_authoritative_abort() {
1562 // Channel close with a retained provisional and no richer cause is the
1563 // closure point: it finalizes to an AUTHORITATIVE (non-provisional) abort.
1564 let mut st = SendState::new();
1565 let cmd = step(
1566 &mut st,
1567 SendInput::PollReady {
1568 poll: SendPoll::Closed,
1569 terminal: Some(SendTerminal::ProvisionalAbort),
1570 },
1571 );
1572 match cmd {
1573 SendCommand::PublishTerminal {
1574 candidate,
1575 continuation: TerminalContinuation::PollReady,
1576 } => {
1577 assert!(
1578 !candidate.is_provisional(),
1579 "closure abort is authoritative"
1580 );
1581 assert!(matches!(candidate, SendTerminal::Failed(_)));
1582 }
1583 other => panic!("expected authoritative Failed at closure, got {other:?}"),
1584 }
1585 }
1586
1587 #[test]
1588 fn mf2_provisional_pending_channel_pending_waits_not_ready() {
1589 // While a provisional is retained and no closure event has arrived, an idle
1590 // Pending poll must WAIT (never spuriously report ReturnReady/ready).
1591 let mut st = SendState::new();
1592 let cmd = step(
1593 &mut st,
1594 SendInput::PollReady {
1595 poll: SendPoll::Pending,
1596 terminal: Some(SendTerminal::ProvisionalAbort),
1597 },
1598 );
1599 assert!(
1600 matches!(cmd, SendCommand::Pending),
1601 "a retained provisional must wait, not report ready"
1602 );
1603 }
1604
1605 #[test]
1606 fn mf2_provisional_finish_abort_is_closure_point() {
1607 // poll_finish observing a graceful==false finish while a provisional is
1608 // retained finalizes to an authoritative abort (closure point).
1609 let mut st = SendState {
1610 finish_started: true,
1611 ..SendState::new()
1612 };
1613 let cmd = step(
1614 &mut st,
1615 SendInput::PollFinish {
1616 poll: SendPoll::Event(SendEvent::FinishComplete { graceful: false }),
1617 terminal: Some(SendTerminal::ProvisionalAbort),
1618 },
1619 );
1620 match cmd {
1621 SendCommand::PublishTerminal { candidate, .. } => {
1622 assert!(!candidate.is_provisional());
1623 assert!(matches!(candidate, SendTerminal::Failed(_)));
1624 }
1625 other => panic!("expected authoritative Failed at finish closure, got {other:?}"),
1626 }
1627 }
1628
1629 #[test]
1630 fn sc011_cancelled_complete_with_peer_stop_surfaces_peer_cause() {
1631 // Peer STOP_SENDING already published: the method-terminal step surfaces
1632 // the specific peer cause ahead of the cancelled-completion synthesis.
1633 let mut st = SendState {
1634 send_inprogress: true,
1635 ..SendState::new()
1636 };
1637 let cmd = step(
1638 &mut st,
1639 SendInput::PollReady {
1640 poll: SendPoll::Event(SendEvent::Complete { cancelled: true }),
1641 terminal: Some(SendTerminal::Stopped(5)),
1642 },
1643 );
1644 match cmd {
1645 SendCommand::ReturnError(SendTerminal::Stopped(5)) => {}
1646 other => panic!("expected ReturnError(Stopped(5)), got {other:?}"),
1647 }
1648 }
1649
1650 #[test]
1651 fn sc011_cancelled_complete_with_connection_surfaces_connection_cause() {
1652 // Connection shutdown published with an outstanding send: the connection
1653 // cause wins over the provisional abort.
1654 let mut st = SendState {
1655 send_inprogress: true,
1656 ..SendState::new()
1657 };
1658 let cmd = step(
1659 &mut st,
1660 SendInput::PollFinish {
1661 poll: SendPoll::Event(SendEvent::Complete { cancelled: true }),
1662 terminal: Some(conn_terminal()),
1663 },
1664 );
1665 assert!(matches!(
1666 cmd,
1667 SendCommand::ReturnError(SendTerminal::Connection(_))
1668 ));
1669 }
1670
1671 // ── STOP_SENDING observable with no send in flight ──
1672
1673 #[test]
1674 fn stop_sending_without_send_observable_at_poll_ready_and_finish() {
1675 // No outstanding send; a sticky peer terminal is surfaced before any
1676 // early Ok on both poll_ready and poll_finish.
1677 let mut st = SendState::new();
1678 let cmd = step(
1679 &mut st,
1680 SendInput::PollReady {
1681 poll: SendPoll::Pending,
1682 terminal: Some(SendTerminal::Stopped(3)),
1683 },
1684 );
1685 match cmd {
1686 SendCommand::ReturnError(SendTerminal::Stopped(3)) => {}
1687 other => panic!("poll_ready: expected Stopped(3), got {other:?}"),
1688 }
1689
1690 let mut st = SendState::new();
1691 let cmd = step(
1692 &mut st,
1693 SendInput::PollFinish {
1694 poll: SendPoll::Pending,
1695 terminal: Some(SendTerminal::Stopped(3)),
1696 },
1697 );
1698 match cmd {
1699 SendCommand::ReturnError(SendTerminal::Stopped(3)) => {}
1700 other => panic!("poll_finish: expected Stopped(3), got {other:?}"),
1701 }
1702 }
1703
1704 #[test]
1705 fn terminal_wake_without_terminal_is_internal() {
1706 // A wake with no published cause is an adapter fault, never a panic.
1707 let mut st = SendState::new();
1708 let cmd = step(
1709 &mut st,
1710 SendInput::PollReady {
1711 poll: SendPoll::Event(SendEvent::TerminalWake),
1712 terminal: None,
1713 },
1714 );
1715 assert!(matches!(
1716 cmd,
1717 SendCommand::PublishTerminal {
1718 candidate: SendTerminal::Internal(_),
1719 ..
1720 }
1721 ));
1722 }
1723
1724 #[test]
1725 fn provisional_abort_is_refinable_but_specific_is_not() {
1726 // is_provisional gates the shared-slot refinement (publish_send in terminal.rs).
1727 // MF-2 (Item 2): ONLY the distinct synthesized `ProvisionalAbort` marker is
1728 // provisional/refinable; a real native `Failed` — even `QUIC_STATUS_ABORTED`
1729 // — is authoritative and NEVER refinable.
1730 assert!(SendTerminal::ProvisionalAbort.is_provisional());
1731 assert!(
1732 !SendTerminal::Failed(aborted_status()).is_provisional(),
1733 "a REAL native Failed(ABORTED) is authoritative, not provisional"
1734 );
1735 assert!(!SendTerminal::Stopped(1).is_provisional());
1736 assert!(!conn_terminal().is_provisional());
1737 assert!(!SendTerminal::LocalReset(1).is_provisional());
1738 assert!(!SendTerminal::Internal("x").is_provisional());
1739 }
1740}