From 98afb092ce97fc2a95200b9d182c1aa6dabbb48b Mon Sep 17 00:00:00 2001 From: "glm-5.3-flash" Date: Sun, 20 Sep 2026 07:39:52 +0000 Subject: [PATCH] test: G-4 unix accept loop + G-5 unnamed fail-loud guards MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - G-4: drive UnixListenerHalves::accept_loop — bind_unix + temp path, the loop exits on the queue-closed handler error after one unix client accept (the unix parity twin of the TCP accept-loop test; closes the largest single uncovered block, local/mod.rs 591-609). - G-5: pin ADR-008 §3's two fail-loud sentences — bind_unix("") is the kernel autobind (no filesystem path) so local_addr fails loud (578-583), and a socket2 autobind client against a NAMED listener is refused by accept_pair's unnamed-peer guard (626-633; Linux — abstract addresses/autobind are Linux-shaped, mio maps any abstract-namespace peer to unnamed). Verification: cargo test --features local green (162 tests); clippy --all-targets -D warnings clean (native + wasm32-unknown- unknown); cargo fmt --check clean. --- tasks/tunnels/review-002-coverage-gaps.md | 48 ++++++++++++++- tests/local_bindfirst.rs | 72 +++++++++++++++++++++++ tests/producer_listen.rs | 38 ++++++++++++ 3 files changed, 156 insertions(+), 2 deletions(-) diff --git a/tasks/tunnels/review-002-coverage-gaps.md b/tasks/tunnels/review-002-coverage-gaps.md index ef952cc..4292b75 100644 --- a/tasks/tunnels/review-002-coverage-gaps.md +++ b/tasks/tunnels/review-002-coverage-gaps.md @@ -1,7 +1,7 @@ --- id: tunnels/review-002-coverage-gaps name: "G-4 + G-5 — close the remaining cheap coverage gaps (unix accept-loop test, unnamed unix fail-loud guards)" -status: pending +status: completed depends_on: [] scope: narrow risk: low @@ -87,4 +87,48 @@ promise. ## Notes -> Agent fills during implementation. \ No newline at end of file +> Agent fills during implementation. + +### Implementation (2026-09-20) + +**G-4** — `unix_accept_loop_stops_feeding_after_close_and_maps_the_drop` +in `tests/producer_listen.rs` (mirroring the TCP twin above it): +`bind_unix` + a temp path under the test's own pid-scoped dir, the +listener's `accept_loop` spawned against a pre-closed `AcceptQueue`, +one unix client connect, and the loop's `handler_error` exit asserted +within a 5s bound. Covers the full 591-609 range (the unix +accept-first loop was previously never driven). + +**G-5** — `unix_unnamed_bind_and_unnamed_peer_fail_loud` in +`tests/local_bindfirst.rs` (`#[cfg(target_os = "linux")]` — abstract +addresses + autobind are Linux-shaped). Both fail-loud arms pinned via +the crate's own public API (no private-constructor seam needed): + +- **Unnamed bind**: `bind_unix("")` — tokio passes the empty path + through to `bind(2)`, which the kernel autobinds (addlen == + `offsetof(sun_path)`, an abstract name, no filesystem path) — + `local_addr` maps it to the "no filesystem path" handler error + (the 578-583 arm). Probed on this host: the bound addr reports + `as_pathname() == None`. +- **Unnamed peer**: a socket2 client bound to the empty abstract + sockaddr (the autobind shape — verified `Ok` on this kernel) + connects to a NAMED `bind_unix` listener; `accept_pair` refuses + with the unnamed-peer handler error (the 626-633 arm). A plain + unbound client behaves identically: mio's UDS accept maps ANY + abstract-namespace peer (`sun_path[0] == 0`, which includes the + autobind assignment) to an unnamed addr, so the fail-loud guard + catches both client shapes through tokio's accept. + +`bind_unix_first` itself stays untested for the unnamed-bind arm at +the establisher level (it composes `bind_unix` + `local_addr` — the +arm is the same code path the test drives directly); the local_addr +assertion covers the composition's error leg. + +**Verification** (all green): + +- `cargo test` (135) + `cargo test --features local` (162 passed, + 0 failed) +- `cargo clippy --all-targets -- -D warnings` (default + `local`) +- `cargo fmt --check` +- `cargo check` + `cargo clippy -D warnings --target + wasm32-unknown-unknown` (the default crate stays wasm-clean) \ No newline at end of file diff --git a/tests/local_bindfirst.rs b/tests/local_bindfirst.rs index 3a0465d..4966611 100644 --- a/tests/local_bindfirst.rs +++ b/tests/local_bindfirst.rs @@ -400,3 +400,75 @@ async fn accept_pair_loop_downcast_miss_fails_loud() { "the downcast miss is handler_error-class: {err:?}" ); } + +/// The unnamed-peer/unnamed-bind fail-loud arms (G-5 — ADR-008 §3's +/// two pinned sentences: a producer that cannot determine the peer +/// does not serve a bind-first listener over that substrate): +/// +/// - **Unnamed bind** — an empty-path `bind_unix` is the kernel's +/// autobind (the kernel assigns an abstract name, addlen == +/// offsetof(sun_path)); the bound addr has no filesystem path, so +/// `local_addr` (and thus `bind_unix_first`) fails loud with the +/// "no filesystem path" handler error instead of serving an +/// unrepresentable `bound`. +/// - **Unnamed peer** — an addressless client (socket2 autobind +/// here; a plain unbound client behaves identically through tokio +/// accept — mio maps any abstract-namespace peer to unnamed) is +/// accepted by a NAMED listener; `accept_pair` fails loud because +/// the forwarded op's `peer` param is required on the wire. +#[cfg(target_os = "linux")] +#[tokio::test] +async fn unix_unnamed_bind_and_unnamed_peer_fail_loud() { + // Arm 1: the autobind listener — `bind_unix("")`. tokio passes the + // empty path through to `bind(2)` with addlen == offsetof(sun_path): + // the kernel autobinds an abstract name, no filesystem path exists. + let halves = alktunnels::local::bind_unix("") + .expect("autobind bind succeeds (the kernel assigns the abstract name)"); + let err = halves + .local_addr() + .expect_err("an autobound listener has no filesystem path"); + assert!( + matches!( + err, + alktunnels::producer::TunnelEstablishError::HandlerError(ref m) + if m.contains("no filesystem path"), + ), + "unnamed bind maps to the handler error: {err:?}" + ); + + // Arm 2: the unnamed peer — a socket2 autobind client connects to a + // NAMED listener; `accept_pair` must refuse (the forwarded `peer` + // param requires an address). + let dir = std::env::temp_dir().join(format!("alktunnels-g5-{}", std::process::id())); + let _ = std::fs::create_dir_all(&dir); + let listen_path = dir.join("named.sock"); + let _ = std::fs::remove_file(&listen_path); + + let named = + alktunnels::local::bind_unix(&listen_path.to_string_lossy()).expect("bind named listener"); + assert!(named.local_addr().is_ok(), "the named listener has a path"); + + let client = socket2::Socket::new(socket2::Domain::UNIX, socket2::Type::STREAM, None) + .expect("socket2 client"); + client + .bind(&socket2::SockAddr::unix("").expect("empty sockaddr (the autobind shape)")) + .expect("autobind client bind"); + client + .connect(&socket2::SockAddr::unix(&listen_path).expect("listen sockaddr")) + .expect("autobind client dials"); + + let err = named + .accept_pair() + .await + .expect_err("an unnamed peer cannot be represented in the forwarded op"); + assert!( + matches!( + err, + alktunnels::producer::TunnelEstablishError::HandlerError(ref m) + if m.contains("unnamed peer"), + ), + "unnamed peer maps to the handler error: {err:?}" + ); + + let _ = std::fs::remove_file(&listen_path); +} diff --git a/tests/producer_listen.rs b/tests/producer_listen.rs index 978d920..4d3a6c1 100644 --- a/tests/producer_listen.rs +++ b/tests/producer_listen.rs @@ -384,6 +384,44 @@ async fn accept_loop_stops_feeding_after_close_and_maps_the_drop() { ); } +/// The unix accept loop's queue-closed mapping (G-4 — the unix parity +/// twin of the TCP test above): the same loop shape over +/// `UnixListenerHalves` — a `push` after close errors the loop with +/// the handler error, the loop exits on the mapped error, and the +/// unaccepted far end is dropped. +#[tokio::test] +#[cfg(feature = "local")] +async fn unix_accept_loop_stops_feeding_after_close_and_maps_the_drop() { + let queue = AcceptQueue::new(); + queue.close().await; + + let dir = std::env::temp_dir().join(format!("alktunnels-unixaccept-{}", std::process::id())); + let _ = std::fs::create_dir_all(&dir); + let path = dir.join("accept-loop.sock"); + let _ = std::fs::remove_file(&path); + let listen_str = path.to_string_lossy().into_owned(); + + let listener = alktunnels::local::bind_unix(&listen_str).expect("bind"); + let loop_queue = queue.clone(); + let outcome = tokio::spawn(async move { listener.accept_loop(loop_queue).await }); + let _ = tokio::net::UnixStream::connect(&path) + .await + .expect("connect"); + let err = tokio::time::timeout(std::time::Duration::from_secs(5), outcome) + .await + .expect("accept loop timed out (did it keep feeding a closed queue?)") + .expect("accept loop task") + .expect_err("closed queue must end the accept loop"); + assert!( + matches!( + err, + alktunnels::producer::TunnelEstablishError::HandlerError(_) + ), + "queue-closed push maps to the handler error, got {err:?}" + ); + let _ = std::fs::remove_file(&path); +} + /// The lost-wakeup regression (review 001 C-1): a `push` landing in /// the wall-clock window between a parked `pop`'s state check and its /// `Notified` registration must still wake the pop. The window only