test: G-4 unix accept loop + G-5 unnamed fail-loud guards

- 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.
This commit is contained in:
glm-5.3-flash committed 2026-09-20 07:39:52 +00:00
1 parent 7991bf6ad0
commit 98afb092ce
3 files changed
+156 -2

No files matched your search

+46 -2
View File
@@ -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.
> 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)
+72
View File
@@ -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);
}
+38
View File
@@ -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