tests: the review-002 coverage residue — stage exits, splits, spent arms (N-8)

Eighteen new tests, zero production-code changes; every line the
review attributed in §N-8 is now covered (verified by a
`cargo llvm-cov --show-missing-lines` before/after diff).

- producer: the datagram stage's failure exits pinned as direct unit
  tests on `run_datagram_stage` (the serve layer discards StageExit,
  so the unit shape is the only place the exact matrix is assertable)
  — `ReplyFrameRejected` (a joint-bound-violating reply), `EgressEnded`
  via `next_reply`'s Err, `Io` via a failing write and a failing read;
  the `send`-failure arm got the serve-layer drop-witness treatment
  (full ASSOCIATE conversation, EOF after the refusal, the drop flag
  flips — teardown-at-birth on the failure path)
- producer: the quiet wait's `Err(_)` arm (a read *error* ends the
  wait `Ok(())`, the accept future drops with its owned state — the
  client-gone contract's error-shaped twin; driven directly on
  `bind_quiet_wait_and_serve`) and the BIND reply#2 Domain-peer
  fallback (`TargetAddr::Domain(_, port)` → `0.0.0.0:port`, the data
  plane live end to end)
- producer: the establisher's `None`-witness arm (the registration's
  optional witness seam) added to the existing unit test
- consumer: the two front-door-bridge `split`s tested on live
  `Socks5Stream<BiStream>` streams via `build_skip_auth_bi_stream`
  (two duplexes joined the `open_and_negotiate` shape) — raw framed
  bytes through both halves; the BIND split leaves the take-guard
  untouched (`await_inbound` still reads the reply#2 phase)
- consumer: the single-arm residue — spent `send_datagram`'s `Io` arm
  (+ the spent-`recv` `Ok(None)` shape asserted alongside), the
  mid-batch later-frame `Err` dropping the pending `Ok`s (the
  fail-loud-drops-pending semantics), ASSOCIATE `into_inner`'s `Ok`
  pairing, `BindDataPlane`'s `poll_flush`/`poll_shutdown`
  passthroughs (the BIND door's half-close), `read_reply2`'s
  version pin (a foreign producer's non-5 reply#2), the
  ASSOCIATE/BIND session `Debug` bodies, the spent-`close` no-op
- egress/error: the three seam-handle `Debug` renders
  (TargetHandle / BindTargetHandle / BindAccept) and
  `SocksEstablishmentReason`'s `Display` (each variant renders as
  its canonical wire code)

Coverage: 96.31% regions / 97.22% lines; no new accepted-uncovered
entries (the residual uncovered lines are the pre-existing
out-of-scope set + producer.rs 188-189's untestable `accept_bi`
substrate-failure log arm). One test-only select-shape discovery
recorded in the task Notes: `tokio::select!`'s random branch order
can drop a not-yet-polled accept future before its owned state
exists — the quiet-wait test parks once before erroring so both
branches are guaranteed started.

Verification: `cargo test` + `--all-features` green (375 tests),
clippy `-D warnings` clean (native + wasm), fmt clean, `cargo doc`
clean, both wasm checks clean.
This commit is contained in:
glm-5.3-flash committed 2026-09-24 20:33:15 +00:00
1 parent 986bd13ce7
commit 8dbe73d49b
5 files changed
+1009 -8

No files matched your search

+337
View File
@@ -1158,6 +1158,343 @@ mod tests {
));
}
#[tokio::test]
async fn send_datagram_on_a_spent_stream_is_the_io_arm() {
// The spent-stream `Io` arm (review 002 N-8): a session whose
// stream is gone errors loud through the typed I/O surface —
// the mirror of the covered spent-`recv` arm.
let mut session = AssociateSession {
channel_id: 9,
manager: None,
inner: None,
reader: DatagramReader::new(),
pending: Default::default(),
relay_addr: None,
};
let err = session
.send_datagram(TargetAddr::Ip("10.0.0.1:53".parse().unwrap()), b"payload")
.await
.expect_err("a spent stream must not send");
assert!(
matches!(err, SocksSessionError::Io(_)),
"the spent-stream send is the Io arm: {err:?}"
);
// The spent-session read shape rides the same posture: the
// `recv_datagram` spent arm is `Ok(None)` (the EOF-shape, the
// family's read-side posture).
assert!(
session.recv_datagram().await.expect("spent recv").is_none(),
"a spent stream reads the spent shape, never an error"
);
// The spent-session teardown surfaces: `shutdown` on a spent
// session is a no-op reap (no manager — nothing reaped).
let shutdown = session.shutdown().await.expect("spent shutdown");
assert!(!shutdown, "no manager — nothing reaped");
}
#[tokio::test]
async fn spent_associate_close_reaps_a_no_op() {
// The `close`-on-spent arm: the graceful teardown path is a
// no-op reap on a spent session (the session family's Drop
// posture, on the async path).
let session = AssociateSession {
channel_id: 9,
manager: None,
inner: None,
reader: DatagramReader::new(),
pending: Default::default(),
relay_addr: None,
};
assert!(!session.close().await, "no manager — nothing reaped");
}
#[tokio::test]
async fn recv_datagram_mid_batch_later_frame_err_drops_the_pending_oks() {
use tokio::io::AsyncWriteExt as _;
// The mid-batch later-frame `Err` arm (review 002 N-8): the
// first frame of a batch decodes `Ok`, a later one is garbage
// — the batch is discarded with the session (fail-loud, never
// a partial batch delivered while the session stays open).
let (mut peer_writer, session_reader) = tokio::io::duplex(1024);
let addr = TargetAddr::Ip("10.0.0.1:53".parse().unwrap());
let mut wire = crate::wire::frame(&addr, b"first").unwrap().to_vec();
wire.extend_from_slice(&[0x00, 0x04, 0x7f, 1, 2, 3]);
peer_writer.write_all(&wire).await.unwrap();
drop(peer_writer);
let (write_peer, session_writer) = tokio::io::duplex(64);
let _ = write_peer;
let mut session = AssociateSession {
channel_id: 1,
manager: None,
inner: Some(build_skip_auth_bi_stream(session_reader, session_writer)),
reader: DatagramReader::new(),
pending: Default::default(),
relay_addr: None,
};
let err = session
.recv_datagram()
.await
.expect_err("the later frame's garbage fails loud");
assert!(
matches!(err, SocksSessionError::Rfc(SocksError::UdpHeaderError(_))),
"the decode garbage rides the UDP-header class: {err:?}"
);
// The pending `Ok` was dropped with the session — the queue
// stays empty and the stream is spent for further reads (the
// error ended the caller's loop, the fail-loud posture).
assert!(session.pending.is_empty());
}
#[tokio::test]
async fn associate_into_inner_pairs_the_stream_with_the_teardown_owner() {
// The `Ok` arm of `AssociateSession::into_inner` (review 002
// N-8): the taken stream pairs with the session that stays the
// teardown owner (the `into_inner_pairs_the_pipe_with_the_
// teardown_owner` shape's ASSOCIATE mirror) — and a second
// take is the spent shape.
let (_write_peer, session_writer) = tokio::io::duplex(64);
let (read_peer, session_reader) = tokio::io::duplex(64);
let _ = read_peer;
let session = AssociateSession {
channel_id: 1,
manager: None,
inner: Some(build_skip_auth_bi_stream(session_reader, session_writer)),
reader: DatagramReader::new(),
pending: Default::default(),
relay_addr: Some(TargetAddr::Ip("127.0.0.1:0".parse().unwrap())),
};
let taken = session.into_inner().expect("the stream is live");
// The pair carries both halves: the teardown owner (the relay
// address still observable) and the owned stream.
assert!(taken.session.relay_addr().is_some(), "the owner");
assert!(taken.session.channel_id() == 1);
drop(taken);
}
#[tokio::test]
async fn associate_debug_renders_channel_liveness_and_pending() {
// The Debug impl body (review 002 N-8, egress.rs class): the
// associate session renders its channel id, liveness, the
// relay address, and the pending count.
let mut session = AssociateSession {
channel_id: 7,
manager: None,
inner: Some(build_skip_auth_bi_stream(
tokio::io::duplex(16).0,
tokio::io::duplex(16).0,
)),
reader: DatagramReader::new(),
pending: Default::default(),
relay_addr: Some(TargetAddr::Ip("127.0.0.1:0".parse().unwrap())),
};
session.pending.push_back((
TargetAddr::Ip("10.0.0.1:53".parse().unwrap()),
Bytes::from_static(b"x"),
));
let rendered = format!("{session:?}");
assert!(rendered.contains("channel_id: 7"), "{rendered}");
assert!(rendered.contains("associated: true"), "{rendered}");
assert!(rendered.contains("relay_addr"), "{rendered}");
assert!(rendered.contains("pending_datagrams: 1"), "{rendered}");
drop(session);
let bind = BindSession {
channel_id: 8,
manager: None,
inner: Some(build_skip_auth_bi_stream(
tokio::io::duplex(16).0,
tokio::io::duplex(16).0,
)),
bind_addr: Some(TargetAddr::Domain("bind.test".into(), 2121)),
};
let rendered = format!("{bind:?}");
assert!(rendered.contains("channel_id: 8"), "{rendered}");
assert!(rendered.contains("awaiting_inbound: true"), "{rendered}");
assert!(rendered.contains("bind_addr"), "{rendered}");
}
#[tokio::test]
async fn associate_split_yields_live_halves_and_survives_the_round_trip() {
use crate::wire::frame as wire_frame;
use tokio::io::AsyncWriteExt as _;
// The front-door-bridge surface (consumer.md) on a *live*
// stream: the halves borrow the session and the raw framed
// bytes pass through both directions (0 B framing — the door's
// RFC header codec would own the frames at the boundary).
let addr = TargetAddr::Ip("10.0.0.1:53".parse().unwrap());
let framed = wire_frame(&addr, b"split").unwrap().to_vec();
let (mut peer_writer, session_reader) = tokio::io::duplex(1024);
peer_writer.write_all(&framed).await.unwrap();
drop(peer_writer);
let (mut peer_reader, session_writer) = tokio::io::duplex(1024);
let mut session = AssociateSession {
channel_id: 1,
manager: None,
inner: Some(build_skip_auth_bi_stream(session_reader, session_writer)),
reader: DatagramReader::new(),
pending: Default::default(),
relay_addr: None,
};
let (mut read, mut write) = session.split().expect("live halves");
// Read the raw frame through the borrowed read half.
let mut header = [0u8; 2];
read.read_exact(&mut header)
.await
.expect("the frame header");
let len = u16::from_be_bytes(header) as usize;
let mut rest = vec![0u8; len];
read.read_exact(&mut rest).await.expect("the frame body");
assert_eq!(len, 7 + 5, "one v4-address datagram frame");
drop(read);
// The write half passes through (the bridge seam's shape) —
// the peer receives the exact bytes.
write.write_all(&framed).await.unwrap();
write.flush().await.unwrap();
drop(write);
let mut got = vec![0u8; framed.len()];
peer_reader
.read_exact(&mut got)
.await
.expect("the peer read");
assert_eq!(got, framed, "the write half rode the raw pipe");
}
/// An inner `Socks5Stream` over a true `BiStream` (the session/
/// plane field type) for the BIND-side surfaces: reads come from
/// `session_end` (the peer's writes), writes go out via the duplex
/// whose other end the test holds. Two joined halves, the same
/// shape `open_and_negotiate` assembles.
fn build_skip_auth_bi_stream(
session_end: tokio::io::DuplexStream,
writer_end: tokio::io::DuplexStream,
) -> Socks5Stream<BiStream> {
let mut config = Config::default();
config.set_skip_auth(true);
Socks5Stream::use_stream(BiStream::from_joined(session_end, writer_end), None, config)
.now_or_never()
.expect("skip_auth's use_stream is instantly ready")
.expect("skip_auth's use_stream succeeds")
}
#[tokio::test]
async fn bind_split_yields_live_halves_pre_reply2() {
use tokio::io::AsyncWriteExt as _;
// The BIND front-door-bridge surface (consumer.md): the split
// halves in the reply#1 phase — pre-reply#2 bytes ride the
// same channel (the doc's claim, exercised on a live stream).
let (mut peer_writer, session_reader) = tokio::io::duplex(1024);
peer_writer.write_all(b"raw").await.unwrap();
drop(peer_writer);
let (mut peer_reader, session_writer) = tokio::io::duplex(1024);
let mut session = BindSession {
channel_id: 1,
manager: None,
inner: Some(build_skip_auth_bi_stream(session_reader, session_writer)),
bind_addr: Some(TargetAddr::Ip("127.0.0.1:2121".parse().unwrap())),
};
{
let (mut read, mut write) = session.split().expect("live pre-reply2 halves");
write.write_all(b"ack").await.unwrap();
write.flush().await.expect("flush");
let mut buf = [0u8; 3];
read.read_exact(&mut buf).await.expect("the raw bytes");
assert_eq!(&buf, b"raw");
}
// The write half reached the peer (the bridge seam's shape).
let mut buf = [0u8; 3];
peer_reader.read_exact(&mut buf).await.expect("the ack");
assert_eq!(&buf, b"ack");
drop(peer_reader);
// The take-guard is untouched: the reader still reads the
// reply#2 phase (the split halves were a view, not a
// consumer) — the dropped peer EOFs it loud, never the spent
// shape.
let outcome = session.await_inbound().await;
assert!(
!matches!(outcome, Ok(None)),
"the reply#2 phase was not consumed by the split"
);
session.close().await;
}
#[tokio::test]
async fn bind_data_plane_poll_flush_and_poll_shutdown_pass_through() {
use tokio::io::AsyncWriteExt as _;
// The BIND door's half-close rides the plane's poll_shutdown
// passthrough (and poll_flush rides the write path): write →
// flush → shutdown — the bytes reach the peer and the EOF
// sentinel follows.
let (_session_writer_unused, session_reader) = tokio::io::duplex(64);
let (mut peer_reader, session_writer) = tokio::io::duplex(64);
let mut plane = BindDataPlane {
inner: build_skip_auth_bi_stream(session_reader, session_writer),
peer: TargetAddr::Ip("10.0.0.7:44444".parse().unwrap()),
};
plane.write_all(b"half").await.expect("write");
plane.flush().await.expect("the poll_flush passthrough");
plane
.shutdown()
.await
.expect("the poll_shutdown passthrough");
drop(plane);
let mut seen = Vec::new();
let mut buf = [0u8; 4];
loop {
match peer_reader.read(&mut buf).await {
Ok(0) | Err(_) => break,
Ok(n) => seen.extend_from_slice(&buf[..n]),
}
}
assert_eq!(&seen, b"half", "the write rode through, then EOF");
}
#[tokio::test]
async fn read_reply2_rejects_a_foreign_producers_version() {
use tokio::io::AsyncWriteExt as _;
// A foreign producer's reply#2 with a non-5 version: the
// version pin is loud (never an aliased peer).
let (mut peer_writer, session_reader) = tokio::io::duplex(64);
peer_writer
.write_all(&[0x04, 0x00, 0x00, 0x01, 10, 0, 0, 1, 0, 80])
.await
.unwrap();
drop(peer_writer);
let (_write_peer_unused, session_writer) = tokio::io::duplex(64);
let mut inner = build_skip_auth_bi_stream(session_reader, session_writer);
let err = read_reply2(&mut inner)
.await
.expect_err("a foreign version is refused");
assert!(
matches!(
err,
SocksSessionError::Rfc(SocksError::UnsupportedSocksVersion(0x04))
),
"the version pin is loud: {err:?}"
);
}
/// A session over a raw duplex (the reader internals' test
/// harness — the handshake-free path: `skip_auth` bypasses the
/// greeting, no reply is expected, the datagram stage rides
+37
View File
@@ -592,4 +592,41 @@ mod tests {
assert_send_sync::<BindAcceptError>();
assert_send_sync::<BindAcceptFactoryError>();
}
#[test]
fn debug_impls_render_the_handle_surfaces() {
// The three Debug impl bodies (review 002 N-8): the dialed
// handle renders non-exhaustive; the accept handle carries the
// peer; the accept source carries the bind address.
let dialed = TargetHandle {
read: Box::new(tokio::io::empty()),
write: Box::new(tokio::io::sink()),
};
let rendered = format!("{dialed:?}");
assert!(rendered.contains("TargetHandle"), "{rendered}");
assert!(rendered.contains(".."), "{rendered}");
let accepted = BindTargetHandle {
peer: TargetAddr::Ip(SocketAddr::from(([10, 0, 0, 7], 44444))),
read: Box::new(tokio::io::empty()),
write: Box::new(tokio::io::sink()),
};
let rendered = format!("{accepted:?}");
assert!(rendered.contains("BindTargetHandle"), "{rendered}");
assert!(
rendered.contains("10.0.0.7:44444"),
"the peer is named: {rendered}"
);
let accept = BindAccept::new(
SocketAddr::from(([127, 0, 0, 1], 2121)),
std::future::pending::<Result<BindTargetHandle, BindAcceptError>>(),
);
let rendered = format!("{accept:?}");
assert!(rendered.contains("BindAccept"), "{rendered}");
assert!(
rendered.contains("127.0.0.1:2121"),
"the bind address is named: {rendered}"
);
}
}
+21
View File
@@ -227,4 +227,25 @@ mod tests {
));
assert!(matches!(io, SocksSessionError::Io(_)));
}
#[test]
fn establishment_reason_display_renders_the_wire_code() {
// The Display impl body (review 002 N-8): the typed reason
// renders as its canonical wire code (the `as_str` inverse).
for (typed, wire) in [
(SocksEstablishmentReason::DialFailed, "dial_failed"),
(
SocksEstablishmentReason::UnknownResource,
"unknown_resource",
),
(
SocksEstablishmentReason::ResourceShortage,
"resource_shortage",
),
(SocksEstablishmentReason::HandlerError, "handler_error"),
(SocksEstablishmentReason::Timeout, "timeout"),
] {
assert_eq!(typed.to_string(), wire);
}
}
}
+477
View File
@@ -753,6 +753,8 @@ mod tests {
use super::*;
use crate::egress::{BindAcceptError, BindAcceptFactoryError, SocksDialError, TargetHandle};
use bytes::Bytes;
use std::pin::Pin;
use std::task::{Context, Poll};
use tokio::io::{AsyncReadExt, AsyncWriteExt, DuplexStream};
/// The test dial closure (the harness's echo shape, inline): one
@@ -1626,6 +1628,296 @@ mod tests {
);
}
// -- the datagram stage's failure exits (review 002 N-8) -----------
// The stage's teardown surface, driven directly: each failure exit
// returns its exact `StageExit` (the serve layer discards it, so
// the unit shape is the only place the matrix is pinnable).
/// A stream half that fails its reads or writes on demand (the
/// stage's `Io`-exit triggers).
struct FailStream {
fail_read: bool,
fail_write: bool,
}
impl tokio::io::AsyncRead for FailStream {
fn poll_read(
self: Pin<&mut Self>,
_cx: &mut Context<'_>,
_buf: &mut tokio::io::ReadBuf<'_>,
) -> Poll<std::io::Result<()>> {
if self.fail_read {
Poll::Ready(Err(std::io::Error::other("stage read failed")))
} else {
Poll::Pending
}
}
}
impl tokio::io::AsyncWrite for FailStream {
fn poll_write(
self: Pin<&mut Self>,
_cx: &mut Context<'_>,
buf: &[u8],
) -> Poll<std::io::Result<usize>> {
if self.fail_write {
Poll::Ready(Err(std::io::Error::other("stage write failed")))
} else {
Poll::Ready(Ok(buf.len()))
}
}
fn poll_flush(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<std::io::Result<()>> {
Poll::Ready(Ok(()))
}
fn poll_shutdown(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<std::io::Result<()>> {
Poll::Ready(Ok(()))
}
}
/// An egress yielding one pre-set reply then parking forever (the
/// reply branch wins the select once; the stage never advances
/// past it).
struct OneReplyEgress {
reply: Option<Bytes>,
}
impl crate::egress::Egress for OneReplyEgress {
fn send(
&mut self,
_addr: TargetAddr,
_payload: Bytes,
) -> futures::future::BoxFuture<'_, Result<(), EgressError>> {
Box::pin(async move { Err(EgressError::General("send refused".into())) })
}
fn next_reply(
&mut self,
) -> futures::future::BoxFuture<'_, Result<Option<Bytes>, EgressError>> {
let reply = self.reply.take();
Box::pin(async move {
match reply {
Some(inner) => Ok(Some(inner)),
None => Ok(std::future::pending::<Option<Bytes>>().await),
}
})
}
}
/// An egress whose relay already ended (`next_reply` errs) — the
/// `EgressEnded`-via-`next_reply` trigger.
struct RelayEndedEgress;
impl crate::egress::Egress for RelayEndedEgress {
fn send(
&mut self,
_addr: TargetAddr,
_payload: Bytes,
) -> futures::future::BoxFuture<'_, Result<(), EgressError>> {
Box::pin(async move { Err(EgressError::General("send refused".into())) })
}
fn next_reply(
&mut self,
) -> futures::future::BoxFuture<'_, Result<Option<Bytes>, EgressError>> {
Box::pin(async move { Err(EgressError::General("relay ended".into())) })
}
}
/// An egress that parks forever on `next_reply` (the read branch's
/// exits are then deterministic — the parked-accept shape's egress
/// mirror).
struct ParkedEgress;
impl crate::egress::Egress for ParkedEgress {
fn send(
&mut self,
_addr: TargetAddr,
_payload: Bytes,
) -> futures::future::BoxFuture<'_, Result<(), EgressError>> {
Box::pin(async move { Err(EgressError::General("send refused".into())) })
}
fn next_reply(
&mut self,
) -> futures::future::BoxFuture<'_, Result<Option<Bytes>, EgressError>> {
Box::pin(async move { Ok(std::future::pending::<Option<Bytes>>().await) })
}
}
/// The `ReplyFrameRejected` exit: an egress reply whose inner
/// bytes violate the u16 joint bound — the wire codec refuses it
/// at `frame_inner`, the stage tears the association down (never a
/// wire overflow).
#[tokio::test]
async fn datagram_stage_reply_frame_rejected_exit() {
let mut egress = Box::new(OneReplyEgress {
reply: Some(Bytes::from(vec![0u8; crate::wire::MAX_INNER_LEN + 1])),
}) as Box<dyn Egress + Send>;
let exit = run_datagram_stage(
FailStream {
fail_read: false,
fail_write: true,
},
egress.as_mut(),
)
.await;
assert_eq!(exit, StageExit::ReplyFrameRejected);
}
/// The `EgressEnded` exit via `next_reply`'s `Err` — the egress
/// relay ended; the association ends with it (the lossy-datagram
/// posture).
#[tokio::test]
async fn datagram_stage_egress_relay_ended_exit_via_next_reply() {
let mut egress = Box::new(RelayEndedEgress) as Box<dyn Egress + Send>;
let exit = run_datagram_stage(
FailStream {
fail_read: false,
fail_write: false,
},
egress.as_mut(),
)
.await;
assert_eq!(exit, StageExit::EgressEnded);
}
/// The `Io` exit via a failing write: a valid reply frame arrives
/// but the channel's write half fails — the stage tears the
/// association down.
#[tokio::test]
async fn datagram_stage_write_failure_is_the_io_exit() {
let inner = {
let mut v = TargetAddr::Ip(SocketAddr::from(([10, 0, 0, 1], 53)))
.to_be_bytes()
.unwrap()
.to_vec();
v.extend_from_slice(b"payload");
Bytes::from(v)
};
let mut egress = Box::new(OneReplyEgress { reply: Some(inner) }) as Box<dyn Egress + Send>;
let exit = run_datagram_stage(
FailStream {
fail_read: false,
fail_write: true,
},
egress.as_mut(),
)
.await;
assert_eq!(exit, StageExit::Io);
}
/// The `Io` exit via a failing channel read — the stage tears the
/// association down (a stream error is never a silent end).
#[tokio::test]
async fn datagram_stage_read_failure_is_the_io_exit() {
let mut egress = Box::new(ParkedEgress) as Box<dyn Egress + Send>;
let exit = run_datagram_stage(
FailStream {
fail_read: true,
fail_write: false,
},
egress.as_mut(),
)
.await;
assert_eq!(exit, StageExit::Io);
}
/// The `EgressEnded` exit via a failing `egress.send`, at the
/// serve layer (the drop-witness treatment
/// `egress_factory_called_once_and_dropped_with_the_stage` gives
/// the happy path): one datagram in, the egress refuses it, the
/// stage exits — EOF reaches the client and the egress drops with
/// the stage (teardown-at-birth on the failure path).
#[tokio::test]
async fn udp_egress_send_failure_ends_the_association_and_drops_the_egress() {
use std::sync::atomic::{AtomicBool, Ordering};
let dropped = Arc::new(AtomicBool::new(false));
let dropped_for_factory = Arc::clone(&dropped);
let factory: EgressFactory = Arc::new(move || {
let dropped = Arc::clone(&dropped_for_factory);
Box::pin(async move {
struct FailingSendWitness {
flag: Arc<AtomicBool>,
}
impl crate::egress::Egress for FailingSendWitness {
fn send(
&mut self,
_addr: TargetAddr,
_payload: Bytes,
) -> futures::future::BoxFuture<'_, Result<(), EgressError>>
{
Box::pin(
async move { Err(EgressError::General("egress send failed".into())) },
)
}
fn next_reply(
&mut self,
) -> futures::future::BoxFuture<'_, Result<Option<Bytes>, EgressError>>
{
Box::pin(async move { Ok(std::future::pending::<Option<Bytes>>().await) })
}
}
impl Drop for FailingSendWitness {
fn drop(&mut self) {
self.flag.store(true, Ordering::SeqCst);
}
}
Ok(Box::new(FailingSendWitness { flag: dropped }) as Box<dyn Egress + Send>)
})
});
let (client_end, server_end) = tokio::io::duplex(64 * 1024);
let handle = tokio::spawn(serve_socks5_over(
server_end,
None,
Some(factory),
None,
false,
));
let mut client = client_end;
greet(&mut client).await;
write_request(
&mut client,
&Socks5Command::UDPAssociate,
&TargetAddr::Domain("send.fail".into(), 1080),
)
.await;
let (code, _addr) = read_reply(&mut client).await;
assert_eq!(code, 0x00, "the associate reply succeeds");
// One datagram in: the egress refuses it → the stage exits.
let target = TargetAddr::Ip(SocketAddr::from(([10, 0, 0, 4], 53)));
client
.write_all(&datagram_frame(&target, b"doomed"))
.await
.unwrap();
// The association ends: EOF (no reply datagram follows).
let mut buf = [0u8; 16];
let outcome =
tokio::time::timeout(std::time::Duration::from_secs(5), client.read(&mut buf))
.await
.expect("read resolves");
match outcome {
Ok(0) | Err(_) => {}
Ok(n) => panic!("EOF expected after the egress send failure — got {n} bytes"),
}
drop(client);
tokio::time::timeout(std::time::Duration::from_secs(5), handle)
.await
.expect("session ends")
.expect("join")
.expect("the stage exited");
assert!(
dropped.load(Ordering::SeqCst),
"the stage exit drops the egress on the failure path too"
);
}
/// CONNECT and ASSOCIATE coexist on one `serve_socks5_over` (the
/// regression guard, extended): both commands serve their data
/// planes over separate duplexes on the same seam set.
@@ -2163,6 +2455,182 @@ mod tests {
.expect("join");
}
/// The quiet wait's `Err(_)` arm (review 002 N-8): a channel read
/// that *errors* (I/O error, not EOF) during the quiet wait ends
/// the wait with a clean `Ok(())` — the accept future drops with
/// the session (the client-gone contract's error-shaped twin).
/// Driven directly on `bind_quiet_wait_and_serve` (a read error is
/// not producible over a duplex).
#[tokio::test]
async fn bind_quiet_wait_read_error_ends_the_wait_cleanly() {
use std::cell::Cell;
use std::sync::atomic::{AtomicBool, Ordering};
let dropped = Arc::new(AtomicBool::new(false));
struct FailReadStream {
polled: Cell<bool>,
}
impl tokio::io::AsyncRead for FailReadStream {
fn poll_read(
self: Pin<&mut Self>,
cx: &mut Context<'_>,
_buf: &mut tokio::io::ReadBuf<'_>,
) -> Poll<std::io::Result<()>> {
// Park once (both branches get polled — the accept
// future starts, its owned state exists), then error
// on the wake: the read branch's error resolves after
// the accept branch has started.
if self.polled.get() {
Poll::Ready(Err(std::io::Error::other("quiet-wait read failed")))
} else {
self.polled.set(true);
cx.waker().wake_by_ref();
Poll::Pending
}
}
}
impl tokio::io::AsyncWrite for FailReadStream {
fn poll_write(
self: Pin<&mut Self>,
_cx: &mut Context<'_>,
buf: &[u8],
) -> Poll<std::io::Result<usize>> {
Poll::Ready(Ok(buf.len()))
}
fn poll_flush(
self: Pin<&mut Self>,
_cx: &mut Context<'_>,
) -> Poll<std::io::Result<()>> {
Poll::Ready(Ok(()))
}
fn poll_shutdown(
self: Pin<&mut Self>,
_cx: &mut Context<'_>,
) -> Poll<std::io::Result<()>> {
Poll::Ready(Ok(()))
}
}
struct OwnedAccept(Arc<AtomicBool>);
impl Drop for OwnedAccept {
fn drop(&mut self) {
self.0.store(true, Ordering::SeqCst);
}
}
let accept = {
let dropped = Arc::clone(&dropped);
BindAccept::new(SocketAddr::from(([127, 0, 0, 1], 2121)), async move {
let owner = OwnedAccept(dropped);
std::future::pending::<()>().await;
drop(owner);
unreachable!("pending never resolves")
})
};
let outcome = bind_quiet_wait_and_serve(
FailReadStream {
polled: Cell::new(false),
},
accept,
)
.await;
outcome.expect("an error-shaped quiet wait is not an error exit");
assert!(
dropped.load(Ordering::SeqCst),
"the accept future dropped with the wait (the client-gone contract, error arm)"
);
}
/// The BIND reply#2 Domain-peer fallback (review 002 N-8): a
/// foreign accept handle carrying a `TargetAddr::Domain(_, port)`
/// peer — the producer writes reply#2 with the unspecified
/// address on the peer's port (`0.0.0.0:port`) and serves the
/// data plane.
#[tokio::test]
async fn bind_domain_peer_falls_back_to_the_unspecified_address() {
let factory: BindAcceptFactory = Arc::new(move |_target: TargetAddr| {
Box::pin(async move {
Ok(BindAccept::new(
SocketAddr::from(([127, 0, 0, 1], 2121)),
async {
let (mut echo_end, target_end) = tokio::io::duplex(64);
tokio::spawn(async move {
let mut buf = [0u8; 64];
loop {
match echo_end.read(&mut buf).await {
Ok(0) | Err(_) => return,
Ok(n) => {
if echo_end.write_all(&buf[..n]).await.is_err() {
return;
}
}
}
}
});
let (read_half, write_half) = tokio::io::split(target_end);
Ok(BindTargetHandle {
peer: TargetAddr::Domain("peer.example".into(), 44444),
read: Box::new(read_half) as Box<dyn AsyncRead + Send + Sync + Unpin>,
write: Box::new(write_half)
as Box<dyn AsyncWrite + Send + Sync + Unpin>,
})
},
))
})
});
let (client_end, server_end) = tokio::io::duplex(64 * 1024);
let handle = tokio::spawn(serve_socks5_over(
server_end,
None,
None,
Some(factory),
true,
));
let mut client = client_end;
greet(&mut client).await;
write_request(
&mut client,
&Socks5Command::TCPBind,
&TargetAddr::Domain("domain.peer".into(), 2121),
)
.await;
// Reply#1 succeeds; reply#2 carries the Domain peer's fallback
// (the unspecified address, the peer's port).
let (code1, _addr) = read_reply(&mut client).await;
assert_eq!(code1, 0x00, "reply#1 succeeded");
let (code2, addr) = read_reply(&mut client).await;
assert_eq!(code2, 0x00, "reply#2 succeeded");
assert_eq!(
SocketAddr::from((
[addr[0], addr[1], addr[2], addr[3]],
u16::from_be_bytes([addr[4], addr[5]])
)),
SocketAddr::from(([0, 0, 0, 0], 44444)),
"the Domain peer fell back to 0.0.0.0:port"
);
// The data plane is live (the accept resolved with in-process
// halves — the parked probe half stays alive by forget).
client.write_all(b"ping").await.unwrap();
let mut buf = [0u8; 4];
tokio::time::timeout(
std::time::Duration::from_secs(5),
client.read_exact(&mut buf),
)
.await
.expect("the data plane echoes")
.expect("read the echo");
assert_eq!(&buf, b"ping");
drop(client);
tokio::time::timeout(std::time::Duration::from_secs(5), handle)
.await
.expect("session ends")
.expect("join")
.expect("clean");
}
/// The pump handler's shape: `accept_bi` → the same conversation,
/// with the handler's `JoinHandle` tracking the data plane (R-02
/// — the spawn shape is inline-await, not spawn-and-forget).
@@ -2232,6 +2700,15 @@ mod tests {
Some("opener"),
"the per-call opener identity is recorded"
);
// The None-witness arm (the registration's optional witness
// seam): the session gate passes without recording anything.
let establisher = socks5_session_establisher(None);
let auth = AuthContext::anonymous(b"alk/socks5");
let establishment = establisher(serde_json::json!({}), auth)
.await
.expect("the session gate passes without a witness");
assert!(establishment.plan.is_none(), "no plan payload");
}
/// The command gate's resolved boolean rides the open-op
+137 -8
View File
@@ -1,7 +1,7 @@
---
id: implementation/fix-coverage-residue
name: "Tests — the review-002 coverage residue (N-8: stage exits, splits, spent arms)"
status: pending
status: completed
depends_on: [implementation/fix-bind-reply2-reentry, implementation/fix-producer-gate-policy, implementation/fix-rfc1928-posture]
scope: moderate
risk: low
@@ -77,19 +77,19 @@ through an artificial shape.
## Acceptance Criteria
- [ ] The stage's failure exits tested (`ReplyFrameRejected`,
- [x] The stage's failure exits tested (`ReplyFrameRejected`,
`EgressEnded` both arms, `Io`) — the `send`-failure arm with
the drop-witness assertion
- [ ] `AssociateSession::split` + `BindSession::split` tested on a
- [x] `AssociateSession::split` + `BindSession::split` tested on a
live stream (or the batch-2-door note recorded per arm)
- [ ] The single-arm residue covered: spent-`send`, mid-batch
- [x] The single-arm residue covered: spent-`send`, mid-batch
later-frame `Err`, ASSOCIATE `into_inner`'s `Ok`,
`BindDataPlane` shutdown, `read_reply2` version mismatch, the
quiet-wait `Err(_)` arm, the Domain-peer fallback, the
`Debug`/`Display` renders
- [ ] `cargo llvm-cov` re-run: the named lines covered or recorded
- [x] `cargo llvm-cov` re-run: the named lines covered or recorded
as accepted-uncovered with a reason in the task Notes
- [ ] `cargo test` green; clippy/fmt clean; both wasm checks clean
- [x] `cargo test` green; clippy/fmt clean; both wasm checks clean
## References
@@ -103,8 +103,137 @@ through an artificial shape.
## Notes
> Agent fills this during implementation.
> Filled by the implementation agent (2026-09-24):
- **All N-8-named lines covered; no accepted-uncovered entries
added.** The `cargo llvm-cov --show-missing-lines` diff before →
after: every line the review attributed (consumer.rs 356-359/
419/437/453-456/636-642/771-774/843 pre-fix numbering; producer.rs
374-386/405-414/560/568; egress.rs 72-74/226-230/275-279;
error.rs 113-115) is now covered. The suite grew 357 → 375 tests
(145 lib unit / 38 associate / 41 bind / 40 connect / 41
integration / 30 harness / 37 differential / 3 doc-tests);
TOTAL 96.31% regions / 97.22% lines.
- **The stage's failure exits were pinned as direct unit tests on
`run_datagram_stage`** (the serve layer discards `StageExit`, so
the unit shape is the only place the exact exit matrix is
assertable): `datagram_stage_reply_frame_rejected_exit` (an
egress reply whose inner bytes violate the u16 joint bound →
`ReplyFrameRejected`), `datagram_stage_egress_relay_ended_exit_
via_next_reply` (`next_reply`'s `Err` → `EgressEnded`),
`datagram_stage_write_failure_is_the_io_exit` + `datagram_stage_
read_failure_is_the_io_exit` (a failing channel write/read →
`Io`). The `send`-failure arm got the serve-layer drop-witness
treatment: `udp_egress_send_failure_ends_the_association_and_
drops_the_egress` (a full ASSOCIATE conversation, one datagram in,
the egress refuses it, EOF reaches the client, the stage exits,
and the drop flag flips — teardown-at-birth on the failure path).
The tests ride `FailStream` (a poll-failing stream) and
`OneReplyEgress`/`RelayEndedEgress`/`ParkedEgress` (stage-shaped
egress fakes), all inside the producer test module.
- **The two splits tested on live streams.** `associate_split_
yields_live_halves_and_survives_the_round_trip` and `bind_split_
yields_live_halves_pre_reply2` exercise `tokio::io::split` on a
live `Socks5Stream<BiStream>` built by a new test helper
(`build_skip_auth_bi_stream` — two duplexes joined via
`BiStream::from_joined`, the same shape `open_and_negotiate`
assembles): raw framed bytes pass through both directions on the
borrowed halves, and (BIND) the split leaves the take-guard
untouched — `await_inbound` still reads the reply#2 phase after
the halves are dropped. No "batch-2 door exercises it" note
needed; the tests stand alone.
- **The single-arm residue, test by test:**
`send_datagram_on_a_spent_stream_is_the_io_arm` (the spent-stream
`Io` arm — and the spent-`recv` shape asserted in the same test:
`Ok(None)`, the family's read-side posture),
`recv_datagram_mid_batch_later_frame_err_drops_the_pending_oks`
(first frame `Ok`, a later one garbage → the pending `Ok`s dropped
with the session, the fail-loud-drops-pending semantics pinned),
`associate_into_inner_pairs_the_stream_with_the_teardown_owner`
(the `TakenAssociateInner` Ok arm — the CONNECT
`into_inner_pairs_the_pipe_with_the_teardown_owner` shape's
ASSOCIATE mirror),
`bind_data_plane_poll_flush_and_poll_shutdown_pass_through`
(write → `poll_flush` → `poll_shutdown` — the bytes reach the peer
and the EOF sentinel follows),
`read_reply2_rejects_a_foreign_producers_version` (a non-5
version reply#2 → `UnsupportedSocksVersion(0x04)`, loud),
`bind_quiet_wait_read_error_ends_the_wait_cleanly` (the quiet
wait's `Err(_)` arm driven directly on `bind_quiet_wait_and_serve`
— a read *error* ends the wait `Ok(())` and the accept future
drops with its owned state, the client-gone contract's
error-shaped twin), `bind_domain_peer_falls_back_to_the_
unspecified_address` (a foreign accept handle carrying
`TargetAddr::Domain(_, port)` peer → reply#2 carries
`0.0.0.0:port`, data plane live),
`debug_impls_render_the_handle_surfaces` (the three egress.rs
`Debug` bodies — TargetHandle non-exhaustive, BindTargetHandle
names the peer, BindAccept names the bind address),
`establishment_reason_display_renders_the_wire_code` (the
`SocksEstablishmentReason` `Display` — each variant renders as its
canonical wire code), `associate_debug_renders_channel_liveness_
and_pending` (the ASSOCIATE + BIND session `Debug` bodies),
`spent_associate_close_reaps_a_no_op` (the `close`-on-spent arm),
and `establisher_records_identity_and_defaults_the_establishment`
extended with the `None`-witness arm.
- **A quiet-wait select-shape discovery (test-only note):**
`tokio::select!` polls branches in random order — a read branch
that errors on its *first* poll can win before the accept branch
is ever polled, dropping the accept future at zero polls (the
owned state never constructed; a drop flag would never fire — the
first cut of the test caught this flake). The test's `FailReadStream`
parks once (with a wake) then errors on the second poll so both
branches are guaranteed started; the assertion then holds
deterministically. Production semantics unaffected (either exit
drops the accept future; the contract is exit-shaped, not
poll-count-shaped).
- **One review-item line numbers shifted post-fix-tasks**: the
review's `consumer.rs:396-398` (the covered spent-`recv` arm) is
now `418` (the `inner.as_mut()` else arm after the C-1/D-1
changes); it is covered by the same test that pins the spent-send
arm. `consumer.rs:937` (the spent-bind test's `err().map()`
diagnostic closure) stays uncovered — it only evaluates on `Err`,
and the spent shape is `Ok(None)` by design (the C-1 posture);
cosmetic, not chased.
- **Reachability check for the arm-became-unreachable clause**: no
N-8 arm was made unreachable by the three code-fix tasks — every
named arm still exists at its (shifted) location and is now
tested. The explicit out-of-scope items stayed out of scope:
`rfc1928/client.rs:217-219` remains accepted-uncovered (with its
translated-error note), as do the fork's other accepted-uncovered
blocks.
- **Residual uncovered lines after this task (none new)**: producer
.rs 188-189 (the pump handler's `accept_bi` substrate-failure log
arm — pre-existing, not in N-8's list; untestable without a
broken `Connection` outside alkcall's test surface),
rfc1928/client.rs 217-219 + the fork internals (accepted-
uncovered per review-001/002), wire.rs/mod.rs/target_addr.rs
internals (pre-existing, not in N-8's list), and the test modules'
own panic-guard/unreachable lines (coverage-of-tests noise).
## Summary
> Agent fills this on completion.
Closed review 002's N-8 coverage residue with 18 new tests and zero
production-code changes: the datagram stage's three failure exits
pinned as direct unit tests on `run_datagram_stage`
(`ReplyFrameRejected`, `EgressEnded` via `next_reply`, `Io` via a
failing write and a failing read) plus the serve-layer drop-witness
test for the `send`-failure arm (the egress drops with the stage on
the failure path too); the two front-door-bridge `split`s tested on
live `Socks5Stream<BiStream>` streams (raw bytes through both
halves; the BIND split leaves the take-guard untouched); and the
single-arm residue (spent `send_datagram`'s `Io` arm + the spent
`recv` shape, the mid-batch later-frame `Err` dropping pending `Ok`s,
ASSOCIATE `into_inner`'s `Ok` pairing, `BindDataPlane`'s
`poll_flush`/`poll_shutdown` passthroughs, `read_reply2`'s version
pin, the quiet wait's `Err(_)` arm, the BIND reply#2 Domain-peer
fallback `0.0.0.0:port`, the three egress `Debug` renders, the
`SocksEstablishmentReason` `Display`, the ASSOCIATE/BIND session
`Debug` bodies, the spent-`close` no-op, and the establisher's
`None`-witness arm). `cargo llvm-cov` re-run: every N-8-named line
covered, no new accepted-uncovered entries (the residual uncovered
lines are the pre-existing out-of-scope set + producer.rs 188-189's
untestable `accept_bi` log arm). Verification: `cargo test` +
`--all-features` green (375 tests), clippy `-D warnings` clean
(native + wasm), fmt clean, `cargo doc` clean, both wasm checks
clean; coverage 96.31% regions / 97.22% lines.