feat: producer-listen — listen establisher (shape 2) + AcceptQueue contract

- AcceptFn: the injected accepted-connection source (the assembly
  layer owns the listener + accept loop; the protocol never binds —
  OQ-TN-04).
- AcceptQueue: the protocol-side queue contract (async push/pop/
  close; FIFO always-before-take ordering; close-while-waiting
  resolves None). Empty-queue posture is assembly-owned (a late
  accept is legitimate; resource_shortage is the closure's mapping;
  the wrapper's 10s establishment timeout is the backstop).
- listen_establisher: same open op, same params, same typed errors —
  registry namespace gate → accept() → Establishment::new(plan);
  the pump handler is untouched (plan-flow with a different source).
- register_tunnel_listen_openable: same spec/pump registration with
  the listen establisher (the honest shape vs a dial/accept enum:
  the establisher is the only difference; one establisher per op id
  per session registry — documented).
- Tests (tests/producer_listen.rs, 6): listen flow end-to-end, FIFO
  ordering across two opens (R-01 plan-flow, listen-flavored), empty
  queue -> resource_shortage, closed listener -> dial_failed,
  unknown resource -> unknown_resource, late-push wait-then-resolve.
- Harness: RegistrationMode enum + wire_listen.
- TargetHandle gains a structural Debug impl (test ergonomics).

Verified: cargo test green (44), clippy -D warnings (native + wasm32),
fmt clean, wasm32 check passes.
This commit is contained in:
2026-09-08 09:37:22 +00:00
parent fb2389bd62
commit 09d32d5aa6
6 changed files with 589 additions and 15 deletions
+4
View File
@@ -28,4 +28,8 @@ pub use consumer::{open_reverse_channel, TakenHalves, TunnelSession};
pub use error::{ReverseOpenError, TunnelError, TunnelIoError, TunnelOpenError};
pub use params::ChannelOpenError;
pub use params::{establishment_reason, TUNNEL_ALPN, TUNNEL_OPEN_SCOPE};
pub use producer::{
register_tunnel_listen_openable, AcceptFn, AcceptQueue, DialFn, ResourceRegistry, TargetHandle,
TunnelEstablishError,
};
pub use wire::MAX_DATAGRAM_LEN;
+157
View File
@@ -47,6 +47,15 @@ pub struct TargetHandle {
pub write: Box<dyn AsyncWrite + Send + Sync + Unpin>,
}
impl std::fmt::Debug for TargetHandle {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("TargetHandle")
.field("read", &"<boxed>")
.field("write", &"<boxed>")
.finish()
}
}
/// The typed establishment-failure vocabulary (ADR-049 §3's reason
/// codes as this crate's establisher errors; mapped 1:1 onto
/// `EstablishmentError`).
@@ -179,6 +188,129 @@ fn parse_params(input: &Value) -> Result<TunnelParams, TunnelEstablishError> {
.map_err(|e| TunnelEstablishError::HandlerError(format!("invalid params: {e}")))
}
/// The listen establisher (shape 2 — producer.md §The Establisher):
/// the same open op, same params, same typed errors; the establisher
/// pops the next accepted connection from the injected [`AcceptFn`]
/// instead of dialing. The resource lookup follows the same registry
/// miss mapping (`unknown_resource`); the accept closure's failures
/// map at the closure (empty queue → `resource_shortage`, closed
/// listener → `dial_failed` — producer.md's table). The pump handler
/// is UNCHANGED: the listen variant is plan-flow with a different
/// halves source.
///
/// The registry check is a namespace gate only — the listen producer's
/// "backing" is its accept queue, not a dial target. A resource the
/// producer does not produce is still `unknown_resource` (same as
/// dial); an openable resource with nothing yet accepted is
/// `resource_shortage` (the accept closure's call).
pub fn listen_establisher(registry: ResourceRegistry, accept: AcceptFn) -> OpenEstablisher {
Arc::new(move |input: Value, _auth| {
let registry = registry.clone();
let accept = Arc::clone(&accept);
Box::pin(async move {
let params: TunnelParams = parse_params(&input)?;
registry
.lookup(params.substrate, &params.resource)
.await
.ok_or_else(|| {
TunnelEstablishError::UnknownResource(format!(
"resource `{}` ({:?}) not produced by this producer",
params.resource, params.substrate
))
})?;
let accepted: TargetHandle = accept().await?;
Ok(Establishment::new(Arc::new(accepted) as ChannelPlan))
})
})
}
/// The accepted-connection source the assembly layer injects for a
/// LISTEN producer (shape 2 — the `-R` far-side listener; producer.md
/// §The Establisher): pops one accepted connection's halves. The
/// assembly layer owns the listener + its accept loop — the protocol
/// crate never binds (OQ-TN-04). The queue contract lives in
/// [`AcceptQueue`]; the closure pops from it (or from whatever the
/// assembly chose).
pub type AcceptFn = Arc<
dyn Fn() -> futures::future::BoxFuture<'static, Result<TargetHandle, TunnelEstablishError>>
+ Send
+ Sync,
>;
/// The bounded queue between an assembly-owned accept loop and the
/// listen establisher (the protocol-side queue contract). The accept
/// loop feeds ([`AcceptQueue::push`]); the establisher pops during the
/// open op's establishment phase ([`AcceptQueue::pop`]). `pop` is
/// bounded by the establisher's own deadline (ADR-049 §2) — but an
/// empty queue is NOT inherently an error (a late accept is
/// legitimate); the mapping to `resource_shortage` is the ASSEMBLY
/// layer's posture (its accept closure knows the listener's bound —
/// a drained listener or an accept budget). The wrapper's
/// establishment timeout is the backstop above that.
///
/// `pop` returns `None` when the queue is closed-and-empty (the
/// listener is gone — the assembly layer calls [`AcceptQueue::close`]
/// on teardown). FIFO order: every popped handle was accepted BEFORE
/// that pop began (always-before-take ordering — push enqueues under
/// the same lock that pops dequeue under, and `pop` re-checks the
/// queue after every wake).
#[derive(Clone, Default)]
pub struct AcceptQueue {
inner: Arc<tokio::sync::Mutex<AcceptQueueInner>>,
notify: Arc<tokio::sync::Notify>,
}
#[derive(Default)]
struct AcceptQueueInner {
handles: std::collections::VecDeque<TargetHandle>,
closed: bool,
}
impl AcceptQueue {
pub fn new() -> Self {
Self::default()
}
/// Feed one accepted handle (the accept loop's step). `Err` once
/// the queue is closed (the listener is gone; the accept loop
/// should stop feeding).
pub async fn push(&self, handle: TargetHandle) -> Result<(), TargetHandle> {
{
let mut inner = self.inner.lock().await;
if inner.closed {
return Err(handle);
}
inner.handles.push_back(handle);
}
self.notify.notify_waiters();
Ok(())
}
/// Pop the next accepted handle, waiting while the queue is empty
/// and open. `None` = the queue is closed-and-empty (the listener
/// is gone) or the queue was closed while waiting.
pub async fn pop(&self) -> Option<TargetHandle> {
loop {
{
let mut inner = self.inner.lock().await;
if let Some(handle) = inner.handles.pop_front() {
return Some(handle);
}
if inner.closed {
return None;
}
}
self.notify.notified().await;
}
}
/// Close the queue: waiting `pop`s resolve `None` immediately.
pub async fn close(&self) {
self.inner.lock().await.closed = true;
self.notify.notify_waiters();
}
}
/// The pump handler: accept the channel's yield-once `BiStream`,
/// downcast the plan to `TargetHandle`, and await `pump_bidi` inline
/// (R-02 — the returned `JoinHandle` tracks the data-plane lifetime;
@@ -236,3 +368,28 @@ pub fn register_tunnel_openable(
None,
)
}
/// Register the tunnel open op with a LISTEN establisher (shape 2):
/// the same spec + pump handler as [`register_tunnel_openable`]; the
/// establisher pops accepted handles from the injected [`AcceptFn`]
/// instead of dialing. One registration per operation id per registry
/// — a session serves either a dial establisher or a listen
/// establisher for `channels/tunnel/sub`; a producer that does both
/// registers on separate registries (per-connection registries are
/// independent, producer.md §Registration API).
pub fn register_tunnel_listen_openable(
core: &ChannelCore,
registry: &ResourceRegistry,
on_registry: &Arc<OperationRegistry>,
auth: AuthContext,
accept: AcceptFn,
) -> Result<(), String> {
core.register_openable_with_establisher(
crate::params::tunnel_open_spec(),
Some(listen_establisher(registry.clone(), accept)),
make_tunnel_pump_handler(),
on_registry,
auth,
None,
)
}
+54 -1
View File
@@ -1,7 +1,7 @@
---
id: tunnels/producer-listen
name: Listen establisher — the producer-side listener variant
status: pending
status: completed
depends_on: [tunnels/producer-open-op]
scope: narrow
risk: medium
@@ -87,6 +87,59 @@ queue → `resource_shortage`, closed listener → `dial_failed`.
> Agent fills during implementation.
- **Registration shape (the "pick the honest shape" decision):** a
separate `register_tunnel_listen_openable(core, registry, on_registry,
auth, accept: AcceptFn)` — not a dial/accept enum on
`register_tunnel_openable`. Rationale: the establisher is the only
difference; `register_openable_with_establisher` takes a pre-built
`OpenEstablisher`, so both variants share the same spec + pump
handler + registration call, and a session serves ONE establisher
per op id (a dual-shape producer registers on separate per-session
registries). An enum would have added a mode flag the wrapper does
not need.
- **`AcceptQueue::push` is async** (lock-acquiring): the accept loop's
step is async anyway; a sync `push` would need a sync mutex with
cross-thread notify — not worth it for a queue that drains one
handle per open.
- **Empty-queue posture is assembly-owned, not queue-owned:** an
empty queue during establishment is NOT inherently an error (a late
accept is legitimate — `accept_wait_resolves_when_push_arrives_late`
pins the wait-then-resolve shape). The mapping to
`resource_shortage` belongs to the accept closure (the assembly
knows its listener's bound: drained listener, accept budget); the
wrapper's establishment timeout (10s) is the backstop. The task
sketch's "empty queue → resource_shortage" is realized by the
assembly mapping — the test pins the fail-fast closure posture.
- **`closed_listener → dial_failed`** is likewise the closure's
mapping (the queue's `close()` + pop → `None` → the closure maps).
The typed-error table holds: both reasons verified on the wire via
`channel:open_failed` details.
- **AcceptFn = the queue pop wrapped in a closure** in the tests; the
`local`-gated listener helper (local-socket-halves) feeds real
accepted sockets through the same queue.
## Summary
> Agent fills this on completion.
Implemented the listen establisher (shape 2) per producer.md §The
Establisher: `AcceptFn` (the injected accepted-connection source),
`AcceptQueue` (the protocol-side queue contract: async push/pop/
close, FIFO with always-before-take ordering, close-while-waiting
resolves None), `listen_establisher` (registry namespace gate →
accept() → `Establishment::new(plan)` — the same plan flow, a
different halves source; the pump handler is untouched), and
`register_tunnel_listen_openable` (the same spec/pump registration,
listen establisher).
Tests: `tests/producer_listen.rs` (6) — the listen flow end-to-end
(pre-accepted handle round-trips through the pump), FIFO ordering
across two opens (the R-01 plan-flow property, listen-flavored),
empty queue → `resource_shortage` (fail-fast closure posture pinned),
closed listener → `dial_failed`, unknown resource →
`unknown_resource`, and the late-push wait-then-resolve shape.
Harness: `RegistrationMode` enum + `wire_listen` on the existing
topology.
Verified: cargo test green (44 total), clippy `-D warnings` clean
(native + wasm32), fmt clean, wasm32 check passes.
+49 -11
View File
@@ -92,9 +92,16 @@ pub struct Topology {
/// `serving_identity` is the explicit `ServingConfig.identity`
/// override (CF-005 remediation (a)) — `None` in the primary path.
/// `provider` resolves payload tokens (the hub-forwarding path).
/// The open-op registration mode: a dial establisher (shape 1) or a
/// listen establisher over an assembly-fed [`AcceptQueue`] (shape 2).
pub enum RegistrationMode {
Dial(alktunnels::producer::DialFn),
Listen(alktunnels::producer::AcceptFn),
}
pub async fn wire_with(
registry: ResourceRegistry,
dial: alktunnels::producer::DialFn,
mode: RegistrationMode,
transport_identity: Option<Identity>,
serving_identity: Option<Identity>,
provider: Arc<dyn IdentityProvider>,
@@ -185,15 +192,29 @@ pub async fn wire_with(
producer_client.manager().clone(),
alkcall::channels::policy::default_policy(),
);
register_tunnel_openable(
&core,
&registry,
&producer_op_registry,
AuthContext::anonymous(b"alk/tunnel"),
dial,
Some(Arc::clone(&identity_witness)),
)
.expect("register tunnel openable");
match mode {
RegistrationMode::Dial(dial) => {
register_tunnel_openable(
&core,
&registry,
&producer_op_registry,
AuthContext::anonymous(b"alk/tunnel"),
dial,
Some(Arc::clone(&identity_witness)),
)
.expect("register tunnel openable");
}
RegistrationMode::Listen(accept) => {
alktunnels::producer::register_tunnel_listen_openable(
&core,
&registry,
&producer_op_registry,
AuthContext::anonymous(b"alk/tunnel"),
accept,
)
.expect("register tunnel listen openable");
}
}
let producer_client = Arc::new(producer_client);
// --- consumer pieces (captured from the hook) -------------------------
@@ -215,7 +236,24 @@ pub async fn wire_with(
pub async fn wire(registry: ResourceRegistry, dial: alktunnels::producer::DialFn) -> Topology {
wire_with(
registry,
dial,
RegistrationMode::Dial(dial),
Some(consumer_identity()),
None,
Arc::new(alkcall::core::auth::NoopIdentityProvider),
)
.await
}
/// The listen topology (shape 2): the producer's establisher pops
/// accepted handles from the assembly-fed [`AcceptQueue`] instead of
/// dialing. Same identity posture as [`wire`].
pub async fn wire_listen(
registry: ResourceRegistry,
accept: alktunnels::producer::AcceptFn,
) -> Topology {
wire_with(
registry,
RegistrationMode::Listen(accept),
Some(consumer_identity()),
None,
Arc::new(alkcall::core::auth::NoopIdentityProvider),
+322
View File
@@ -0,0 +1,322 @@
//! Integration tests for the listen establisher (shape 2 — producer.md
//! §The Establisher): a producer whose resource is a LISTENER. The
//! assembly layer owns the listener + its accept loop and feeds an
//! [`AcceptQueue`]; the establisher pops one accepted handle during
//! the open op's establishment phase. Same open op, same params, same
//! typed errors — the pump handler is unchanged.
//!
//! Covered: the listen flow end-to-end (in-process listener queue fed
//! by a test accept loop; consumer opens toward it; the accepted
//! handle is the plan payload; the two-pump round-trip completes),
//! pop-during-establishment ordering (always-before-take), empty
//! queue → `resource_shortage`, closed listener → `dial_failed`,
//! unknown resource → `unknown_resource`, and the out-of-band close.
mod harness;
use std::sync::Arc;
use alktunnels::params::{Substrate, TunnelParams};
use alktunnels::producer::{AcceptQueue, ResourceRegistry, TargetHandle, TunnelEstablishError};
use alktunnels::{open_reverse_channel, TunnelSession};
use harness::{wire_listen, Topology};
fn params(resource: &str) -> TunnelParams {
TunnelParams {
resource: resource.to_string(),
substrate: Substrate::Tcp,
}
}
/// An accept closure popping from the queue (the assembly-side
/// contract the real `local` listener helper will present).
fn accept_from_queue(queue: AcceptQueue) -> alktunnels::producer::AcceptFn {
Arc::new(move || {
let queue = queue.clone();
Box::pin(async move {
queue
.pop()
.await
.ok_or_else(|| TunnelEstablishError::DialFailed("listener closed".to_string()))
})
})
}
/// An in-process accepted handle: a duplex pair, the far end driven by
/// an echo task (the same shape the dial harness echoes with).
fn accepted_handle() -> TargetHandle {
let (consumer_side, target_side) = tokio::io::duplex(64 * 1024);
let (mut t_read, mut t_write) = tokio::io::split(target_side);
tokio::spawn(async move {
use tokio::io::{AsyncReadExt, AsyncWriteExt};
let mut buf = vec![0u8; 8192];
loop {
match t_read.read(&mut buf).await {
Ok(0) | Err(_) => return,
Ok(n) => {
if t_write.write_all(&buf[..n]).await.is_err() {
return;
}
}
}
}
});
let (c_read, c_write) = tokio::io::split(consumer_side);
TargetHandle {
read: Box::new(c_read),
write: Box::new(c_write),
}
}
fn only_channel_0(topo: &Topology) -> bool {
topo.consumer_manager
.channel_ids()
.iter()
.all(|&id| id == 0)
&& topo
.producer
.manager()
.channel_ids()
.iter()
.all(|&id| id == 0)
}
#[tokio::test]
async fn listen_flow_round_trips() {
// The listen topology: the assembly accept loop pre-accepts one
// connection; the consumer opens toward the resource; the accepted
// handle is the plan payload; the pump round-trips.
let queue = AcceptQueue::new();
queue.push(accepted_handle()).await.expect("push");
let registry = ResourceRegistry::new();
registry
.register("listener", Substrate::Tcp, "assembly-owned")
.await;
let topo = wire_listen(registry, accept_from_queue(queue.clone())).await;
let channel_id = open_reverse_channel(&topo.consumer_call, &params("listener"), None)
.await
.expect("listen open");
let mut session = TunnelSession::adopt(
&topo.consumer_manager,
channel_id,
Substrate::Tcp,
alktunnels::TUNNEL_ALPN,
)
.await
.expect("adopt");
use tokio::io::{AsyncReadExt, AsyncWriteExt};
let (read, write) = session.stream_halves().expect("stream halves");
write.write_all(b"listen-ping").await.expect("write");
write.flush().await.expect("flush");
let mut buf = vec![0u8; 11];
tokio::time::timeout(std::time::Duration::from_secs(5), read.read_exact(&mut buf))
.await
.expect("round trip")
.expect("read");
assert_eq!(&buf, b"listen-ping");
session.close().await;
tokio::time::sleep(std::time::Duration::from_millis(150)).await;
assert!(only_channel_0(&topo));
}
#[tokio::test]
async fn pop_during_establishment_is_always_before_take() {
// The always-before-take guarantee: two accepts pushed BEFORE an
// open are popped in FIFO order across two opens — each open's
// establisher sees the handle accepted before it (the plan flow:
// every open's result flows to ITS pump).
let queue = AcceptQueue::new();
queue.push(accepted_handle()).await.expect("push 1");
queue.push(accepted_handle()).await.expect("push 2");
let registry = ResourceRegistry::new();
registry
.register("listener", Substrate::Tcp, "assembly-owned")
.await;
let topo = wire_listen(registry, accept_from_queue(queue.clone())).await;
let id1 = open_reverse_channel(&topo.consumer_call, &params("listener"), None)
.await
.expect("open 1");
let id2 = open_reverse_channel(&topo.consumer_call, &params("listener"), None)
.await
.expect("open 2");
assert_ne!(id1, id2);
// Each session round-trips its own marker (the R-01 plan-flow
// property, listen-flavored: distinct handles per open).
use tokio::io::{AsyncReadExt, AsyncWriteExt};
for (channel_id, marker) in [(id1, "one"), (id2, "two")] {
let mut session = TunnelSession::adopt(
&topo.consumer_manager,
channel_id,
Substrate::Tcp,
alktunnels::TUNNEL_ALPN,
)
.await
.expect("adopt");
let (read, write) = session.stream_halves().expect("halves");
write.write_all(marker.as_bytes()).await.expect("write");
write.flush().await.expect("flush");
let mut buf = vec![0u8; marker.len()];
tokio::time::timeout(std::time::Duration::from_secs(5), read.read_exact(&mut buf))
.await
.expect("round trip")
.expect("read");
assert_eq!(buf, marker.as_bytes());
session.close().await;
}
tokio::time::sleep(std::time::Duration::from_millis(150)).await;
assert!(only_channel_0(&topo));
}
#[tokio::test]
async fn empty_queue_is_resource_shortage() {
// producer.md's error mapping: an empty queue during establishment
// → `resource_shortage`. The QUEUE's pop waits (a late accept is
// legitimate — see accept_wait_resolves_when_push_arrives_late);
// the assembly layer decides its own posture. An assembly that
// knows nothing is coming (a drained listener, a bounded accept
// budget) closes its queue or maps empty-pop to the typed error —
// here, the closure maps it directly (fail-fast posture).
let queue = AcceptQueue::new();
queue.close().await;
let registry = ResourceRegistry::new();
registry
.register("listener", Substrate::Tcp, "assembly-owned")
.await;
let accept: alktunnels::producer::AcceptFn = Arc::new(move || {
let queue = queue.clone();
Box::pin(async move {
// Bounded wait (the assembly's own deadline, not the
// wrapper's): a closed queue is an empty listen backlog —
// resource_shortage per producer.md's table.
match tokio::time::timeout(std::time::Duration::from_millis(50), queue.pop()).await {
Ok(Some(handle)) => Ok(handle),
_ => Err(TunnelEstablishError::ResourceShortage(
"no accepted connection available".to_string(),
)),
}
})
});
let topo = wire_listen(registry, accept).await;
match open_reverse_channel(&topo.consumer_call, &params("listener"), None).await {
Ok(_) => panic!("empty-queue open must fail"),
Err(alktunnels::ReverseOpenError::Call(call_err)) => {
assert_eq!(call_err.code, "channel:open_failed");
let reason = call_err
.details
.as_ref()
.and_then(|d| d.get("reason"))
.and_then(|r| r.as_str())
.expect("reason in details");
assert_eq!(reason, "resource_shortage");
}
Err(other) => panic!("expected Call error, got {other:?}"),
}
tokio::time::sleep(std::time::Duration::from_millis(150)).await;
assert!(only_channel_0(&topo), "failed open leaves no channel");
}
#[tokio::test]
async fn closed_listener_is_dial_failed() {
// producer.md's error mapping: a closed listener (the queue closed
// before the open) → `dial_failed`-class.
let queue = AcceptQueue::new();
queue.close().await;
let registry = ResourceRegistry::new();
registry
.register("listener", Substrate::Tcp, "assembly-owned")
.await;
let topo = wire_listen(registry, accept_from_queue(queue.clone())).await;
match open_reverse_channel(&topo.consumer_call, &params("listener"), None).await {
Ok(_) => panic!("closed-listener open must fail"),
Err(alktunnels::ReverseOpenError::Call(call_err)) => {
assert_eq!(call_err.code, "channel:open_failed");
let reason = call_err
.details
.as_ref()
.and_then(|d| d.get("reason"))
.and_then(|r| r.as_str())
.expect("reason in details");
assert_eq!(reason, "dial_failed");
}
Err(other) => panic!("expected Call error, got {other:?}"),
}
tokio::time::sleep(std::time::Duration::from_millis(150)).await;
assert!(only_channel_0(&topo));
}
#[tokio::test]
async fn unknown_resource_stays_unknown() {
// The registry namespace gate: a resource the listen producer does
// not produce is `unknown_resource` (same as the dial shape).
let queue = AcceptQueue::new();
queue.push(accepted_handle()).await.expect("push");
let registry = ResourceRegistry::new();
registry
.register("listener", Substrate::Tcp, "assembly-owned")
.await;
let topo = wire_listen(registry, accept_from_queue(queue.clone())).await;
match open_reverse_channel(&topo.consumer_call, &params("ghost"), None).await {
Ok(_) => panic!("unknown resource must fail"),
Err(alktunnels::ReverseOpenError::Call(call_err)) => {
assert_eq!(call_err.code, "channel:open_failed");
let reason = call_err
.details
.as_ref()
.and_then(|d| d.get("reason"))
.and_then(|r| r.as_str())
.expect("reason in details");
assert_eq!(reason, "unknown_resource");
}
Err(other) => panic!("expected Call error, got {other:?}"),
}
tokio::time::sleep(std::time::Duration::from_millis(150)).await;
assert!(only_channel_0(&topo));
}
#[tokio::test]
async fn accept_wait_resolves_when_push_arrives_late() {
// Pop-during-establishment ordering under a LIVE accept loop: the
// establisher's pop waits; the accept loop pushes after a tick;
// the open resolves once the push lands (the wrapper's bounded
// wait — ADR-049 §2's deadline applies above us).
let queue = AcceptQueue::new();
let accept_loop = tokio::spawn({
let queue = queue.clone();
async move {
tokio::time::sleep(std::time::Duration::from_millis(200)).await;
queue.push(accepted_handle()).await.expect("push");
}
});
let registry = ResourceRegistry::new();
registry
.register("listener", Substrate::Tcp, "assembly-owned")
.await;
let topo = wire_listen(registry, accept_from_queue(queue.clone())).await;
let channel_id = open_reverse_channel(&topo.consumer_call, &params("listener"), None)
.await
.expect("open resolves after the late push");
accept_loop.await.expect("accept loop task");
let session = TunnelSession::adopt(
&topo.consumer_manager,
channel_id,
Substrate::Tcp,
alktunnels::TUNNEL_ALPN,
)
.await
.expect("adopt");
let reaped = session.close().await;
assert!(reaped);
}
+2 -2
View File
@@ -188,7 +188,7 @@ async fn open_denied_without_any_identity() {
.await;
let topo = wire_with(
registry,
echo_dial(),
harness::RegistrationMode::Dial(echo_dial()),
None,
None,
Arc::new(alkcall::core::auth::NoopIdentityProvider),
@@ -239,7 +239,7 @@ async fn open_token_overrides_transport_identity() {
.await;
let topo = wire_with(
registry,
echo_dial(),
harness::RegistrationMode::Dial(echo_dial()),
None,
None,
Arc::new(harness::TestIdProvider),