feat: consumer-session — TunnelSession (open/adopt, data planes, teardown)

- TunnelSession (src/consumer.rs): open (ChannelClient::open_channel)
  + adopt (ChannelManager::adopt_channel) construction, substrate-
  shaped data planes (raw halves for stream; ADR-003 codec + pending
  frame queue for UDP), pump_against (session-owned pump_bidi handle),
  and teardown ownership per ADR-005: close (abort + ungraceful reap),
  join (await + reap + copy counts, pump-less (0,0,reaped)), Drop
  (abort + sync reap, never leaks). No Clone (compile_fail doc-test).
- open_reverse_channel on the CallConnection (the reverse path's
  two-step: call then adopt — the POC's honest shape).
- Error surfaces (src/error.rs): TunnelOpenError (typed ADR-049 §4
  surface via open_ref/establishment_reason), TunnelIoError (wrong-
  substrate, TruncatedDatagram fail-loud, ChannelTaken), ReverseOpenError.
- Tests: tests/consumer_session.rs (12) over two harness topologies —
  forward (wire_forward/ForwardTopology: consumer connect-side pure
  client, producer serving via adapter) and reverse (the POC shape);
  teardown matrix, W4 half-close, out-of-band close + self-reap (W3),
  F-2 empty datagram, AdoptFailed-on-collision (bogus-id adopt parks
  by design — documented), concurrent sessions. Harness: all_substrate_dial.
- Adopt takes substrate (task-note deviation: the data plane shape
  rode the open call; cannot be inferred from the ID).

Verified: cargo test green (38), clippy -D warnings (native + wasm32),
fmt clean, cargo check --target wasm32-unknown-unknown passes.
This commit is contained in:
2026-09-08 09:33:12 +00:00
parent 2b35da4395
commit fb2389bd62
8 changed files with 1431 additions and 18 deletions
+76 -8
View File
@@ -1,13 +1,13 @@
//! Tunnel error surfaces: the crate's `TunnelError` plus the typed
//! open-error helpers consumers branch on (ADR-049 §4 surface).
//!
//! Skeleton module — filled by `tunnels/params` (codec + establishment
//! error mapping) and `tunnels/consumer-session` (session errors).
//! Tunnel error surfaces: the crate-level [`TunnelError`], the typed
//! session-open error ([`TunnelOpenError`], ADR-049 §4 surface), and
//! the session data-plane error ([`TunnelIoError`]).
use alkcall::channels::client::ChannelOpenError;
use alkcall::protocol::wire::CallError;
use crate::wire::DatagramCodecError;
/// The crate-level error type (thiserror; AGENTS.md convention 2).
///
/// Skeleton: the codec and session variants land with
/// `tunnels/wire-codec` and `tunnels/consumer-session`.
#[derive(Debug, thiserror::Error)]
pub enum TunnelError {
/// The data-plane codec rejected an operation (UDP framing).
@@ -21,3 +21,71 @@ pub enum TunnelError {
#[error("io: {0}")]
Io(#[from] std::io::Error),
}
/// A failed tunnel open or adopt. A failure never yields a session
/// (no phantom session, mirroring the no-phantom-channel property —
/// ADR-005). Branch on [`ChannelOpenError::establishment_reason`]
/// through [`TunnelOpenError::open_ref`] for `channel:open_failed`'s
/// reason (`dial_failed` / `unknown_resource` / `resource_shortage` /
/// `handler_error` / `timeout`), or the pre-establishment codes
/// (`FORBIDDEN`, `channel:too_many_channels`).
#[derive(Debug, thiserror::Error)]
pub enum TunnelOpenError {
/// The open op failed, or the reply/adopt failed — the typed
/// [`ChannelOpenError`] carries the wire [`CallError`] verbatim
/// (adopt failures surface as `AdoptFailed`).
#[error("tunnel open failed: {0}")]
Open(#[from] ChannelOpenError),
}
impl TunnelOpenError {
/// The underlying [`ChannelOpenError`] (the ADR-049 typed surface).
pub fn open_ref(&self) -> &ChannelOpenError {
match self {
TunnelOpenError::Open(e) => e,
}
}
}
/// An I/O failure on the session's data plane, or a substrate-shaped
/// operation attempted on the wrong plane (the POC's shape).
#[derive(Debug, thiserror::Error)]
pub enum TunnelIoError {
/// The underlying stream failed.
#[error("io: {0}")]
Io(#[from] std::io::Error),
/// The datagram codec rejected the operation (e.g. `Oversize` —
/// a >65535-byte send, rejected at frame time per ADR-003).
#[error("codec: {0}")]
Codec(#[from] DatagramCodecError),
/// The stream ended mid-datagram — declared length not satisfied
/// (a clean peer never does this; the fail-loud posture, OQ-TN-13
/// / ADR-003: never a silent partial datagram).
#[error("stream ended mid-datagram (truncated)")]
TruncatedDatagram,
/// A substrate-shaped operation was attempted on the wrong data
/// plane (e.g. `send_datagram` on a stream tunnel).
#[error("wrong substrate for this operation")]
WrongSubstrate,
/// The session's channel halves were already taken — by
/// `take_halves` (caller-driven) or `pump_against` (pump-owned);
/// no data plane remains on the session.
#[error("channel halves already taken from this session")]
ChannelTaken,
}
/// The reverse-path open call failed (`open_reverse_channel` — the
/// assembly layer calls it before [`crate::consumer::TunnelSession::
/// adopt`]; the hub's call surface is a `CallConnection`, so the
/// two-step is the honest API).
#[derive(Debug, thiserror::Error)]
pub enum ReverseOpenError {
/// The open op failed — the wire [`CallError`]; branch on
/// `details.reason` of `channel:open_failed` (ADR-049 §4).
#[error("open call failed: {0:?}")]
Call(CallError),
/// The open op succeeded but the reply carried no `channel_id`
/// (malformed responder).
#[error("open reply missing channel_id")]
NoChannelId,
}