Files
alktunnels/tests/harness.rs
T
glm-5.3-flash fb2389bd62 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.
2026-09-08 09:33:12 +00:00

472 lines
20 KiB
Rust

//! Test harness — the producer↔consumer wiring over a
//! `tokio::io::duplex` carrying the channels 8-byte chunk format.
#![allow(dead_code)]
//!
//! Topology (the reverse POC's shape, mirrored for both directions):
//! - **producer** (connect side of the channels connection — it runs
//! `ChannelClient::from_connection_with_serving`): dials the
//! duplex, serves the tunnel open op + generic channel ops on its
//! serving registry. Its manager allocates the tunnel channel IDs
//! (`ChannelSide::Connect` — ADR-047 §5: the serving side
//! allocates; the initiator adopts).
//! - **consumer** (accept side): the `ChannelsAdapter` + capturing
//! install hook. The hook captures the consumer's channel-0
//! `CallConnection` (for calling the producer's open op) and the
//! consumer's `ChannelManager` (for adopting the producer-
//! allocated IDs and tearing channels down). The consumer serves
//! no ops.
//!
//! Identity on the serving path follows alkcall 0.7.0's CF-005
//! precedence: payload `auth_token` → `ServingConfig.identity` → the
//! transport connection's identity (`Connection::set_identity` before
//! dialing). CF-006 (the per-call opener identity on the establisher)
//! is probed via the `identity_witness` the establisher records.
use std::sync::Arc;
use alkcall::channels::adapter::{ChannelsAdapter, InstallChannelZero};
use alkcall::channels::client::{ChannelClient, ServingConfig};
use alkcall::channels::manager::ChannelManager;
use alkcall::channels::operations::ChannelOperations;
use alkcall::core::auth::{AuthContext, AuthToken, Identity, IdentityProvider};
use alkcall::core::types::Connection as CoreConnection;
use alkcall::protocol::connection::{split_single_stream, CallConnection};
use alkcall::protocol::dispatch::Dispatcher;
use alkcall::registry::registration::OperationRegistry;
use alktunnels::params::{Substrate, TUNNEL_OPEN_SCOPE};
use alktunnels::producer::{register_tunnel_openable, ResourceRegistry};
/// The consumer identity the transport carries (the mTLS/QUIC
/// analogue — key-based identity resolved out-of-band, attached to
/// the dialing connection before `from_connection_with_serving`).
pub fn consumer_identity() -> Identity {
Identity {
id: "consumer".to_string(),
scopes: vec![TUNNEL_OPEN_SCOPE.to_string()],
resources: std::collections::HashMap::new(),
}
}
/// The scoped token (the hub-forwarding path) — resolves to
/// [`consumer_identity`] via [`TestIdProvider`].
pub const TEST_AUTH_TOKEN: &str = "test-token-with-tunnel-open-scope";
/// Resolve the scoped token to the consumer identity; anything else
/// resolves to none (falling through to the transport identity per
/// the 0.7.0 precedence order).
pub struct TestIdProvider;
impl IdentityProvider for TestIdProvider {
fn resolve_from_fingerprint(&self, _: &str) -> Option<Identity> {
None
}
fn resolve_from_token(&self, token: &AuthToken) -> Option<Identity> {
if token.raw == TEST_AUTH_TOKEN.as_bytes() {
Some(consumer_identity())
} else {
None
}
}
}
/// The full producer↔consumer topology over one duplex transport.
pub struct Topology {
/// The producer's client (connect side, serving). Its manager
/// allocated the tunnel channels; its pumps are wrapper-managed.
pub producer: Arc<ChannelClient>,
/// The consumer's manager (accept side). `adopt` installs the
/// producer-allocated IDs here; reaping calls `teardown_channel`.
pub consumer_manager: ChannelManager,
/// The consumer's channel-0 call connection — its surface for
/// calling the producer's open op (and channel/close).
pub consumer_call: Arc<CallConnection>,
/// The producer's serving registry (probes).
pub producer_registry: Arc<OperationRegistry>,
/// What the establisher's per-call auth carried as the opener
/// identity id (the CF-006 probe — `None` until an open runs).
pub identity_witness: Arc<tokio::sync::Mutex<Option<String>>>,
}
/// Wire the topology. `transport_identity` is the key-based identity
/// attached to the dialing connection (CF-005 remediation (b));
/// `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).
pub async fn wire_with(
registry: ResourceRegistry,
dial: alktunnels::producer::DialFn,
transport_identity: Option<Identity>,
serving_identity: Option<Identity>,
provider: Arc<dyn IdentityProvider>,
) -> Topology {
let producer_op_registry = Arc::new(OperationRegistry::new());
let (consumer_call_tx, mut consumer_call_rx) =
tokio::sync::mpsc::channel::<Arc<CallConnection>>(1);
let (consumer_manager_tx, mut consumer_manager_rx) =
tokio::sync::mpsc::channel::<ChannelManager>(1);
// --- consumer (accept side): ChannelsAdapter + capturing hook ------
let install_hook: InstallChannelZero = Arc::new(move |manager, channel0_conn, _auth| {
let call_tx = consumer_call_tx.clone();
let manager_tx = consumer_manager_tx.clone();
tokio::spawn(async move {
let channel0_bidi = match channel0_conn.accept_bi().await {
Ok(s) => s,
Err(_) => return,
};
let (writer, reader) = split_single_stream(channel0_bidi);
let call_connection = Arc::new(CallConnection::new_single_stream(
channel0_conn,
Arc::clone(&writer),
));
let _ = call_tx.send(Arc::clone(&call_connection)).await;
let _ = manager_tx.send(manager).await;
// The consumer serves no ops — an empty registry. The
// dispatcher keeps the call loop alive (its read loop
// resolves the producer's responses to the consumer's
// pending entries).
let dp = Dispatcher::new(
Arc::new(OperationRegistry::new()),
Arc::new(alkcall::core::auth::NoopIdentityProvider),
);
dp.serve_single_stream(call_connection, reader, writer)
.await;
})
});
// --- transport ------------------------------------------------------
let (producer_end, consumer_end) = tokio::io::duplex(64 * 1024);
let producer_conn = CoreConnection::from_bidi(producer_end, b"alk/channels".to_vec(), None);
let consumer_conn = CoreConnection::from_bidi(consumer_end, b"alk/channels".to_vec(), None);
let adapter = ChannelsAdapter::new(install_hook, Arc::new(alkcall::channels::policy::NoCap));
let consumer_transport_auth = AuthContext::anonymous(b"alk/channels");
let _adapter_task = tokio::spawn(async move {
let _ = alkcall::core::types::ProtocolHandler::handle(
&adapter,
consumer_conn,
&consumer_transport_auth,
)
.await;
});
// The transport identity attaches to the dialing connection
// BEFORE from_connection_with_serving (the mTLS/QUIC posture).
if let Some(id) = transport_identity {
producer_conn
.set_identity(id)
.expect("transport identity set once");
}
// --- producer (connect side, serving) --------------------------------
let producer_client = ChannelClient::from_connection_with_serving(
producer_conn,
Some(ServingConfig {
registry: Arc::clone(&producer_op_registry),
identity_provider: provider,
identity: serving_identity,
}),
)
.await
.expect("producer channel client init");
// The generic channel ops ride the producer's serving registry
// (channel/close tears a channel down out-of-band — the review
// 007 recipe).
let ops = ChannelOperations::with_default_policy(producer_client.manager().clone());
ops.register_on(&producer_op_registry)
.expect("register channel ops on producer");
// Post-hoc openable registration on the shared registry (W2). The
// identity witness captures what the establisher's per-call auth
// carried (the CF-006 probe).
let identity_witness = Arc::new(tokio::sync::Mutex::new(None::<String>));
let core = alkcall::channels::operations::ChannelCore::new(
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");
let producer_client = Arc::new(producer_client);
// --- consumer pieces (captured from the hook) -------------------------
let consumer_call = consumer_call_rx.recv().await.expect("consumer call conn");
let consumer_manager = consumer_manager_rx.recv().await.expect("consumer manager");
Topology {
producer: producer_client,
consumer_manager,
consumer_call,
producer_registry: producer_op_registry,
identity_witness,
}
}
/// The default topology: the consumer's transport identity is the
/// primary caller-identity path (no token). The provider is
/// noop-shaped — only the identity chain authorizes.
pub async fn wire(registry: ResourceRegistry, dial: alktunnels::producer::DialFn) -> Topology {
wire_with(
registry,
dial,
Some(consumer_identity()),
None,
Arc::new(alkcall::core::auth::NoopIdentityProvider),
)
.await
}
/// The forward topology (the `-L` direction): the consumer dials the
/// transport (connect side — `ChannelClient::from_connection`, a pure
/// consumer serving nothing) and the producer accepts (the adapter +
/// an install hook that registers the channel ops + the tunnel
/// openable on the producer's serving registry). This is the topology
/// `TunnelSession::open(&channel_client, params)` is normative for:
/// the consumer's own client calls the open op on the producer's
/// channel 0.
///
/// Identity on the accept-side serving path (the mTLS posture): the
/// transport authenticated the dialer; the hook attaches the resolved
/// identity to channel 0 so the serving dispatch sees the caller
/// (the accept-side analogue of CF-005 (b)'s propagation).
pub struct ForwardTopology {
/// The consumer's own client (connect side, pure consumer). Its
/// manager adopted the producer-allocated channel IDs; the
/// session reaps via this manager.
pub consumer: Arc<ChannelClient>,
/// The producer's manager (accept side, serving) — for asserting
/// wrapper-managed reaping.
pub producer_manager: ChannelManager,
/// What the establisher's per-call auth carried (the CF-006 probe).
pub identity_witness: Arc<tokio::sync::Mutex<Option<String>>>,
}
pub async fn wire_forward(
registry: ResourceRegistry,
dial: alktunnels::producer::DialFn,
) -> ForwardTopology {
let producer_op_registry = Arc::new(OperationRegistry::new());
let (producer_manager_tx, mut producer_manager_rx) =
tokio::sync::mpsc::channel::<ChannelManager>(1);
let identity_witness = Arc::new(tokio::sync::Mutex::new(None::<String>));
let dial_witness = Arc::clone(&identity_witness);
// --- producer (accept side, serving): adapter + install hook ------
let install_hook: InstallChannelZero = Arc::new(move |manager, channel0_conn, _auth| {
let registry_arc = Arc::clone(&producer_op_registry);
let resource_registry = registry.clone();
let dial = Arc::clone(&dial);
let witness = Arc::clone(&dial_witness);
let manager_tx = producer_manager_tx.clone();
tokio::spawn(async move {
let channel0_bidi = match channel0_conn.accept_bi().await {
Ok(s) => s,
Err(_) => return,
};
let (writer, reader) = split_single_stream(channel0_bidi);
// The accept-side transport-identity posture: the transport
// authenticated the dialer; attach the resolved identity to
// channel 0 (the dispatch reads it as the caller identity).
let _ = channel0_conn.set_identity(consumer_identity());
let call_connection = Arc::new(CallConnection::new_single_stream(
channel0_conn,
Arc::clone(&writer),
));
// Register the generic channel ops + the tunnel openable
// (the producer.md recipe) before the dispatch loop runs.
let core = alkcall::channels::operations::ChannelCore::new(
manager.clone(),
alkcall::channels::policy::default_policy(),
);
let ops = ChannelOperations::with_default_policy(manager.clone());
ops.register_on(&registry_arc)
.expect("register channel ops on producer");
register_tunnel_openable(
&core,
&resource_registry,
&registry_arc,
AuthContext::anonymous(b"alk/tunnel"),
dial,
Some(witness),
)
.expect("register tunnel openable");
let _ = manager_tx.send(manager).await;
let dp = Dispatcher::new(
registry_arc,
Arc::new(alkcall::core::auth::NoopIdentityProvider),
);
dp.serve_single_stream(call_connection, reader, writer)
.await;
})
});
// --- transport ------------------------------------------------------
let (consumer_end, producer_end) = tokio::io::duplex(64 * 1024);
let consumer_conn = CoreConnection::from_bidi(consumer_end, b"alk/channels".to_vec(), None);
let producer_conn = CoreConnection::from_bidi(producer_end, b"alk/channels".to_vec(), None);
// The dialer's transport identity (the mTLS/QUIC posture) — set
// before from_connection. The accept side resolves it out-of-band
// (modeled in the hook above).
consumer_conn
.set_identity(consumer_identity())
.expect("transport identity set once");
let adapter = ChannelsAdapter::new(install_hook, Arc::new(alkcall::channels::policy::NoCap));
let producer_transport_auth = AuthContext::anonymous(b"alk/channels");
let _adapter_task = tokio::spawn(async move {
let _ = alkcall::core::types::ProtocolHandler::handle(
&adapter,
producer_conn,
&producer_transport_auth,
)
.await;
});
// --- consumer (connect side): pure client ---------------------------
let consumer_client = ChannelClient::from_connection(consumer_conn)
.await
.expect("consumer channel client init");
let consumer_client = Arc::new(consumer_client);
let producer_manager = producer_manager_rx
.recv()
.await
.expect("producer manager captured");
ForwardTopology {
consumer: consumer_client,
producer_manager,
identity_witness,
}
}
/// An echo dial closure (the ADR-004 injection point under test): an
/// in-process pipe per dial — the "target" echoes bytes until EOF.
/// The consumer side gets split `DuplexStream` halves; a spawned task
/// echoes the other end. No `local` feature, no sockets.
pub fn echo_dial() -> alktunnels::producer::DialFn {
Arc::new(move |substrate: Substrate, backing: &str| {
let backing = backing.to_string();
Box::pin(async move {
match substrate {
Substrate::Tcp | Substrate::Unix => {
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 _ = &backing;
let (c_read, c_write) = tokio::io::split(consumer_side);
Ok(alktunnels::producer::TargetHandle {
read: Box::new(c_read),
write: Box::new(c_write),
})
}
Substrate::Udp => Err(alktunnels::producer::TunnelEstablishError::DialFailed(
format!("udp dial not wired in this harness: {backing}"),
)),
}
})
})
}
/// A both-substrates dial: stream substrates echo via pipes, UDP
/// echoes via the framed codec (the datagram tests' single dial).
pub fn all_substrate_dial() -> alktunnels::producer::DialFn {
let echo = echo_dial();
let udp = framed_udp_echo_dial();
Arc::new(move |substrate: Substrate, backing: &str| match substrate {
Substrate::Udp => udp(substrate, backing),
Substrate::Tcp | Substrate::Unix => echo(substrate, backing),
})
}
/// A failing dial closure (the `dial_failed` stand-in).
pub fn failing_dial(message: &'static str) -> alktunnels::producer::DialFn {
Arc::new(move |_substrate: Substrate, _backing: &str| {
Box::pin(async move {
Err(alktunnels::producer::TunnelEstablishError::DialFailed(
message.to_string(),
))
})
})
}
/// A framed UDP echo dial (the ADR-003 codec at the target boundary —
/// the in-process stand-in for the real `local` UDP adapter): the
/// dial's halves carry the `[len: u16 BE]` wire framing; a spawned
/// echo task decodes frames from the channel side and re-frames the
/// echoed payloads back. The producer pump copies raw bytes; the
/// codec lives at this boundary (the pump never sees UDP specifics).
pub fn framed_udp_echo_dial() -> alktunnels::producer::DialFn {
Arc::new(move |substrate: Substrate, backing: &str| {
let backing = backing.to_string();
Box::pin(async move {
match substrate {
Substrate::Udp => {
let (consumer_side, target_side) = tokio::io::duplex(64 * 1024);
tokio::spawn(async move {
use alktunnels::wire::DatagramReader;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
let (mut t_read, mut t_write) = tokio::io::split(target_side);
let mut reader = DatagramReader::new();
let mut buf = vec![0u8; 16 * 1024];
loop {
let n = match t_read.read(&mut buf).await {
Ok(0) | Err(_) => return,
Ok(n) => n,
};
let dgs = match reader.feed(&buf[..n]) {
Ok(dgs) => dgs,
Err(_) => return,
};
for dg in dgs {
let framed = match alktunnels::wire::frame_datagram(&dg) {
Ok(f) => f,
Err(_) => return,
};
if t_write.write_all(&framed).await.is_err() {
return;
}
let _ = t_write.flush().await;
}
}
});
let _ = &backing;
let (c_read, c_write) = tokio::io::split(consumer_side);
Ok(alktunnels::producer::TargetHandle {
read: Box::new(c_read),
write: Box::new(c_write),
})
}
Substrate::Tcp | Substrate::Unix => {
Err(alktunnels::producer::TunnelEstablishError::DialFailed(
format!("stream dial not wired in the udp harness: {backing}"),
))
}
}
})
})
}