Skip to main content

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}