Files
alktunnels/tests/producer_forwarded_gate.rs
T
glm-5.3-flash 0ceecf582b docs: remove em dashes from repo prose and forbid them going forward
- Rewrote the remaining em dashes outside docs/reviews/ (preserved as
  historical record, matching the alktty precedent): tests/ (313),
  fuzz/ (34 plus one shell comment), tasks/ (671 plus 12 kept in
  fences), docs/research + sdd_process + plans (525 plus 25 in
  fences), docs/architecture/overview.md fence labels (13), README,
  CHANGELOG, AGENTS.md, fuzz/README.md (176), and the
  .opencode/agents prompts (110)
- Rewrote fenced content too: ASCII tree and module-map label
  separators, Rust snippet comments, and template placeholders; wire
  data, JSON/BAST examples, and transcripts survive verbatim
- Rewrote 6 assert/expect message literals in tests/ and 2 format
  strings in fuzz/shared/src; no test data changed
- Added Project Convention 18: no em dashes in prose (doc comments,
  markdown including docs/ and tasks/, agent prompts, commit
  messages); fence content rule, en-dash clause rule, and the
  no-spaced-hyphen prohibition follow alktty's convention 15

Follows 6b20924 (src doc comments) and cd0afe0 (docs/architecture).

Verification: cargo test (141 pass); cargo clippy --all-targets -D
warnings; cargo fmt --check; cargo doc --no-deps; wasm check and
clippy for wasm32-unknown-unknown with -D warnings; em dash grep over
the repo returns matches only in docs/reviews/
2026-11-03 14:12:46 +00:00

587 lines
23 KiB
Rust

//! Integration tests for the forwarded op's establisher variant
//! (ADR-008 Amendment 2:
//! `register_tunnel_forwarded_openable_with_establisher` + the
//! injected [`ForwardedGate`]): the four pins plan 003 §W1's
//! "Tests:" bullet asks for, mirroring `tests/producer_forwarded.rs`
//! (the base serving shape is that file's; these probes cover the
//! establisher-registered delta).
//!
//! Covered:
//! - the gate MISS: a `listen` token with no waiting session → the
//! open fails as a typed `channel:open_failed` with
//! `details.reason == "unknown_resource"`; the transiently
//! allocated channel is torn down by the wrapper (ADR-049 §5; the
//! reply is the typed error, not a phantom channel); the pump
//! handler NEVER runs.
//! - the gate PASS: the existing serving flow is unchanged; the
//! injected handler receives the parsed params + the `BiStream`,
//! the byte round-trip works end to end.
//! - the residual race: gate passes, route gone at handler time →
//! the handler's error path drops the stream → teardown-at-birth
//! reaps the forwarded channel (re-pinned under the establisher
//! variant; the establisher-pass→handler-run window is the residual
//! `correlation-miss` class).
//! - the ordering: an identity WITHOUT `tunnel:forwarded` → FORBIDDEN
//! and the gate closure is never invoked (the registry runs the ACL
//! before the open op's establishment phase (schema → ACL →
//! allocate → establisher → handler).
//! - the establisher-variant malformed-params delta: an unknown
//! top-level field (passes the schema's additive posture, fails the
//! establisher's serde parse) → a typed `channel:open_failed` with
//! `details.reason == "handler_error"`, NOT the base shape's
//! success-then-birth-teardown; the parse precedes the gate (the
//! gate never sees malformed input).
mod harness;
use std::collections::HashSet;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, Mutex};
use alkcall::channels::manager::ChannelManager;
use alkcall::core::auth::Identity;
use alkcall::protocol::wire::CallError;
use alktunnels::params::{TUNNEL_FORWARDED_SCOPE, TUNNEL_OPEN_SCOPE};
use alktunnels::producer::{
register_tunnel_forwarded_openable_with_establisher, ForwardedGate, TargetHandle,
TunnelEstablishError,
};
use harness::{wire_with, RegistrationMode};
/// The opener-side identity holding ONLY `tunnel:forwarded` (the
/// ingress grant; `tests/producer_forwarded.rs`'s shape).
fn forwarded_only_identity() -> Identity {
Identity {
id: "opener-forwarded-only".to_string(),
scopes: vec![TUNNEL_FORWARDED_SCOPE.to_string()],
resources: std::collections::HashMap::new(),
}
}
/// A plain dial-egress identity (`tunnel:open` only, no
/// `tunnel:forwarded`; the ordering probe's identity).
fn open_only_identity() -> Identity {
Identity {
id: "opener-open-only".to_string(),
scopes: vec![TUNNEL_OPEN_SCOPE.to_string()],
resources: std::collections::HashMap::new(),
}
}
/// What the assembly's test handler received per forwarded open (the
/// invocation probe; unmoved = the handler never ran).
#[derive(Default)]
struct Received {
params: Option<alktunnels::params::TunnelForwardedParams>,
}
type ReceivedWitness = Arc<tokio::sync::Mutex<Received>>;
/// The gate-invocation witness: the gate sets it on entry; an
/// ordering assertion (`assert !moved`) pins "the gate never saw the
/// open" (the scope-gate precedence pin, task AC 4).
type GateWitness = Arc<AtomicBool>;
/// The waiting-session map: `listen` token → present. The gate's
/// sync in-memory lookup surface (convention 11's "the gate is an
/// in-memory map lookup"): a token present at gate time = a waiting
/// session; absent = the miss (`TunnelEstablishError::UnknownResource`,
/// the canonical stale-token reason ADR-008 Amendment 2 pins).
type WaitingTokens = Arc<Mutex<HashSet<u32>>>;
/// The gate closure under test: set the witness on entry (even for
/// opens it will reject, because the ordering probe needs the untouched
/// signal), then the token lookup.
fn gate_with_tokens(waiting: WaitingTokens, invoked: GateWitness) -> ForwardedGate {
Arc::new(move |params: &alktunnels::params::TunnelForwardedParams| {
invoked.store(true, Ordering::SeqCst);
if waiting
.lock()
.unwrap_or_else(|e| e.into_inner())
.contains(&params.listen)
{
Ok(())
} else {
Err(TunnelEstablishError::UnknownResource(format!(
"no waiting session for listen token {}",
params.listen
)))
}
})
}
/// Serve the forwarded op on the opener side through the establisher
/// variant: the same injected handler shape as
/// `tests/producer_forwarded.rs`'s `serve_forwarded_on` (record the
/// parsed params, pop the correlated halves for the token, `pump_bidi`
/// against them; a handler-time miss drops the stream and returns,
/// teardown-at-birth) with the gate establisher wrapped around it.
fn serve_forwarded_establisher_on(
consumer_registry: &Arc<alkcall::registry::registration::OperationRegistry>,
manager: ChannelManager,
received: ReceivedWitness,
local_halves: Arc<tokio::sync::Mutex<Vec<TargetHandle>>>,
gate: ForwardedGate,
) {
let handler: alktunnels::producer::ForwardedHandler = Arc::new(move |forwarded, bidi| {
let received = Arc::clone(&received);
let local_halves = Arc::clone(&local_halves);
tokio::spawn(async move {
*received.lock().await = Received {
params: Some(forwarded),
};
let target = local_halves.lock().await.pop();
if let Some(target) = target {
alkcall::channels::pump::pump_bidi(bidi, target.read, target.write).await;
}
})
});
let core = alkcall::channels::operations::ChannelCore::new(
manager,
alkcall::channels::policy::default_policy(),
);
register_tunnel_forwarded_openable_with_establisher(
&core,
consumer_registry,
alkcall::core::auth::AuthContext::anonymous(b"alk/tunnel"),
gate,
handler,
)
.expect("register forwarded openable with establisher");
}
/// A topology for the establisher-variant probes (the
/// `wire_forwarded_serving` shape): the producer leg is the connect
/// side (the base op fails-dial; not what's under test); the opener
/// leg is the accept side whose serving registry carries the
/// establisher-registered forwarded op. The test plays the producer,
/// calling the forwarded op on the producer's call plane; the
/// opener-allocated `channel_id` in the reply is what the producer
/// adopts.
async fn wire_forwarded_establisher(identity: Identity) -> harness::Topology {
wire_with(
alktunnels::producer::ResourceRegistry::new(),
RegistrationMode::Dial(harness::failing_dial(
"base op unused in forwarded-gate probes",
)),
Some(identity),
None,
Arc::new(alkcall::core::auth::NoopIdentityProvider),
)
.await
}
fn forwarded_input(listen: u32, peer: (u32, u16)) -> serde_json::Value {
serde_json::json!({
"listen": listen,
"substrate": "tcp",
"peer": {"host": peer.0.to_string(), "port": peer.1},
})
}
/// Call the forwarded op on the producer's per-accept call plane.
async fn call_forwarded(
producer: &alkcall::channels::client::ChannelClient,
input: serde_json::Value,
) -> Result<serde_json::Value, CallError> {
producer
.call_open_op(alktunnels::params::OP_TUNNEL_FORWARDED, input)
.await
.result
}
/// The accepted-side echo pair (the producer-side pump target).
fn driving_halves() -> TargetHandle {
let (drive_end, target_end) = tokio::io::duplex(64 * 1024);
let (mut t_read, mut t_write) = tokio::io::split(target_end);
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 (d_read, d_write) = tokio::io::split(drive_end);
TargetHandle {
read: Box::new(d_read),
write: Box::new(d_write),
}
}
/// The "local BIND client" pair (the opener-side pump target).
fn bind_client_pair() -> (
tokio::io::ReadHalf<tokio::io::DuplexStream>,
tokio::io::WriteHalf<tokio::io::DuplexStream>,
TargetHandle,
) {
let (a, b) = tokio::io::duplex(64 * 1024);
let (a_read, a_write) = tokio::io::split(a);
let (b_read, b_write) = tokio::io::split(b);
(
b_read,
b_write,
TargetHandle {
read: Box::new(a_read),
write: Box::new(a_write),
},
)
}
/// The gate MISS: a `listen` token with no waiting session → the open
/// fails as a typed `channel:open_failed` with
/// `details.reason == "unknown_resource"`; the transiently allocated
/// channel is torn down by the wrapper (no phantom channel, ADR-049
/// §5); the pump handler NEVER runs. The gate DID run (its witness is
/// set; the miss is AT the gate, not before it).
#[tokio::test]
async fn gate_miss_fails_typed_no_phantom_no_handler() {
let topo = wire_forwarded_establisher(forwarded_only_identity()).await;
let received: ReceivedWitness = Arc::new(tokio::sync::Mutex::new(Received::default()));
let local_halves: Arc<tokio::sync::Mutex<Vec<TargetHandle>>> =
Arc::new(tokio::sync::Mutex::new(Vec::new()));
let waiting: WaitingTokens = Arc::new(Mutex::new(HashSet::new()));
let gate_invoked: GateWitness = Arc::new(AtomicBool::new(false));
serve_forwarded_establisher_on(
&topo.consumer_registry,
topo.consumer_manager.clone(),
Arc::clone(&received),
local_halves,
gate_with_tokens(waiting, Arc::clone(&gate_invoked)),
);
// The open with a token the gate rejects: typed failure, not a
// success + post-open decline.
let err = call_forwarded(&topo.producer, forwarded_input(42113, (198, 55231)))
.await
.expect_err("a gate miss must fail the open");
assert_eq!(
err.code, "channel:open_failed",
"the establisher's typed failure shape"
);
let details = err.details.expect("typed error details");
assert_eq!(
details["reason"], "unknown_resource",
"a stale/missing token maps through the existing table"
);
assert!(
details["message"]
.as_str()
.is_some_and(|m| m.contains("42113")),
"the gate's message names the missed token: {}",
details["message"]
);
// No phantom channel: the wrapper's transient allocation is torn
// down with the establisher's failure (ADR-049 §5).
tokio::time::sleep(std::time::Duration::from_millis(300)).await;
assert!(
topo.consumer_manager
.channel_ids()
.iter()
.all(|&id| id == 0),
"a gate-miss open leaves no channel on the opener side"
);
// The gate ran (its witness is set); the pump handler NEVER did.
assert!(
gate_invoked.load(Ordering::SeqCst),
"the miss happened at the gate"
);
assert!(
received.lock().await.params.is_none(),
"a gate-miss open must not reach the pump handler"
);
}
/// The gate PASS: the registration serves exactly like the base shape
/// (`tests/producer_forwarded.rs`'s e2e): the injected handler
/// receives the parsed params + the `BiStream`, and the byte
/// round-trip works end to end through the establisher-registered
/// variant.
#[tokio::test]
async fn gate_pass_serves_the_unchanged_serving_flow() {
let topo = wire_forwarded_establisher(forwarded_only_identity()).await;
let received: ReceivedWitness = Arc::new(tokio::sync::Mutex::new(Received::default()));
let local_halves: Arc<tokio::sync::Mutex<Vec<TargetHandle>>> =
Arc::new(tokio::sync::Mutex::new(Vec::new()));
let waiting: WaitingTokens = Arc::new(Mutex::new(HashSet::new()));
serve_forwarded_establisher_on(
&topo.consumer_registry,
topo.consumer_manager.clone(),
Arc::clone(&received),
Arc::clone(&local_halves),
gate_with_tokens(Arc::clone(&waiting), Arc::new(AtomicBool::new(false))),
);
// The waiting session's bookkeeping: token in the gate map + the
// correlated halves pre-registered (the correlate-first posture).
const TOKEN: u32 = 918273;
waiting
.lock()
.unwrap_or_else(|e| e.into_inner())
.insert(TOKEN);
let (mut b_read, mut b_write, local) = bind_client_pair();
local_halves.lock().await.push(local);
// The gate-pass open resolves with the wrapper's channel_id.
let out = call_forwarded(&topo.producer, forwarded_input(TOKEN, (198, 55231)))
.await
.expect("the gate-pass open");
let channel_id = out["channel_id"].as_u64().expect("channel_id") as u32;
assert!(channel_id > 0);
tokio::time::sleep(std::time::Duration::from_millis(150)).await;
assert!(
topo.consumer_manager.has_channel(channel_id),
"the forwarded channel is live on the opener side"
);
// The injected handler saw the parsed params verbatim.
let got = received.lock().await.params.clone().expect("invoked");
assert_eq!(got.listen, TOKEN, "the token rides `listen`");
assert_eq!(got.substrate, alktunnels::params::Substrate::Tcp);
assert_eq!(
got.peer,
alktunnels::params::SubstrateAddr::HostPort {
host: "198".to_string(),
port: 55231,
}
);
// The producer adopts the opener-allocated ID and drives the
// accepted side's echo.
let (send, recv) = topo
.producer
.manager()
.adopt_channel(channel_id, alktunnels::TUNNEL_ALPN, None)
.await
.expect("producer adopts the opener-allocated ID");
let driving = driving_halves();
let _producer_pump = tokio::spawn(alkcall::channels::pump::pump_bidi(
alkcall::core::types::BiStream::from_joined(recv, send),
driving.read,
driving.write,
));
// THE round-trip: the test writes on the BIND client's end → the
// assembly's pump → the forwarded channel → the producer's pump →
// the driving echo → back.
use tokio::io::{AsyncReadExt, AsyncWriteExt};
b_write.write_all(b"gate-pass-ping").await.expect("write");
b_write.flush().await.expect("flush");
let mut buf = vec![0u8; 14];
tokio::time::timeout(
std::time::Duration::from_secs(5),
b_read.read_exact(&mut buf),
)
.await
.expect("round trip")
.expect("read");
assert_eq!(&buf, b"gate-pass-ping");
}
/// The residual race (the base shape's pinned semantics, re-pinned
/// under the establisher variant): the gate passes (waiting token
/// present) but the route is GONE at handler time (no correlated
/// halves) → the injected handler's error path drops the stream and
/// returns → teardown-at-birth reaps the forwarded channel. The
/// establisher-pass→handler-run window is the residual
/// `correlation-miss` class; the handler-side fallback is what reaps
/// it, under both shapes.
#[tokio::test]
async fn residual_race_still_reaps_cleanly() {
let topo = wire_forwarded_establisher(forwarded_only_identity()).await;
let received: ReceivedWitness = Arc::new(tokio::sync::Mutex::new(Received::default()));
let local_halves: Arc<tokio::sync::Mutex<Vec<TargetHandle>>> =
Arc::new(tokio::sync::Mutex::new(Vec::new()));
let waiting: WaitingTokens = Arc::new(Mutex::new(HashSet::new()));
serve_forwarded_establisher_on(
&topo.consumer_registry,
topo.consumer_manager.clone(),
Arc::clone(&received),
local_halves,
gate_with_tokens(Arc::clone(&waiting), Arc::new(AtomicBool::new(false))),
);
// The gate passes (the waiting token is present at establisher
// time), but nothing was correlated for the handler: the race.
const TOKEN: u32 = 42114;
waiting
.lock()
.unwrap_or_else(|e| e.into_inner())
.insert(TOKEN);
// The open itself STILL RESOLVES (the gate passed; the call-plane
// success is what makes this a residual, post-open race).
let out = call_forwarded(&topo.producer, forwarded_input(TOKEN, (198, 55232)))
.await
.expect("the gate-pass open");
let channel_id = out["channel_id"].as_u64().expect("channel_id") as u32;
// The handler ran (the invocation was witnessed; the rejection is
// the handler's and the stream was dropped), and its miss reaps
// the channel (teardown-at-birth).
tokio::time::sleep(std::time::Duration::from_millis(300)).await;
let witnessed = received.lock().await.params.clone();
assert_eq!(witnessed.expect("invoked").listen, TOKEN);
assert!(
!topo.consumer_manager.has_channel(channel_id),
"a residual-race handler-miss reaps the forwarded channel"
);
}
/// The ordering: an identity WITHOUT `tunnel:forwarded` → FORBIDDEN
/// and the gate closure is NEVER invoked (a witness the gate sets on
/// entry stays unmoved): the registry runs the ACL before the open
/// op's establishment phase, so the gate never sees an unauthorized
/// open.
#[tokio::test]
async fn scope_gate_precedes_the_establisher() {
let topo = wire_forwarded_establisher(open_only_identity()).await;
let received: ReceivedWitness = Arc::new(tokio::sync::Mutex::new(Received::default()));
let local_halves: Arc<tokio::sync::Mutex<Vec<TargetHandle>>> =
Arc::new(tokio::sync::Mutex::new(Vec::new()));
let waiting: WaitingTokens = Arc::new(Mutex::new(HashSet::new()));
waiting.lock().unwrap_or_else(|e| e.into_inner()).insert(77);
let gate_invoked: GateWitness = Arc::new(AtomicBool::new(false));
serve_forwarded_establisher_on(
&topo.consumer_registry,
topo.consumer_manager.clone(),
Arc::clone(&received),
local_halves,
gate_with_tokens(waiting, Arc::clone(&gate_invoked)),
);
// The scope-less open is FORBIDDEN (even though the token WOULD
// have passed if the gate had run first; the tie-break is the
// order, not the gate's answer).
match call_forwarded(&topo.producer, forwarded_input(77, (10, 1))).await {
Ok(_) => panic!("a scope-less opener must not reach forwarded establishes"),
Err(e) => {
assert_eq!(
e.code, "FORBIDDEN",
"missing `tunnel:forwarded` → FORBIDDEN"
);
assert!(
e.message.contains("tunnel:forwarded"),
"the denial names the missing scope: {}",
e.message
);
}
}
tokio::time::sleep(std::time::Duration::from_millis(150)).await;
// The establishment phase never ran: no gate invocation, no
// channel allocation, no handler execution.
assert!(
!gate_invoked.load(Ordering::SeqCst),
"the ACL precedes the establisher; the gate never saw the open"
);
assert!(
topo.consumer_manager
.channel_ids()
.iter()
.all(|&id| id == 0),
"a FORBIDDEN open leaves no channel on the opener side"
);
assert!(received.lock().await.params.is_none());
}
/// The establisher-variant malformed-params delta (the base shape's
/// `forwarded_malformed_params_tear_down_loud` covers the base
/// posture; this pins the delta): the schema gates (`listen: 0`,
/// missing `peer`) fire first exactly as before (`INVALID_INPUT`),
/// but an unknown top-level field (passes the schema's additive
/// posture, fails the establisher's serde parse) becomes a TYPED
/// `channel:open_failed` with `details.reason == "handler_error"`.
/// The parse precedes the gate (the gate's witness is never set), the
/// wrapper tears the transient allocation down, and the pump handler
/// never runs.
#[tokio::test]
async fn malformed_params_fail_typed_at_the_establisher() {
let topo = wire_forwarded_establisher(forwarded_only_identity()).await;
let received: ReceivedWitness = Arc::new(tokio::sync::Mutex::new(Received::default()));
let local_halves: Arc<tokio::sync::Mutex<Vec<TargetHandle>>> =
Arc::new(tokio::sync::Mutex::new(Vec::new()));
let gate_invoked: GateWitness = Arc::new(AtomicBool::new(false));
serve_forwarded_establisher_on(
&topo.consumer_registry,
topo.consumer_manager.clone(),
Arc::clone(&received),
local_halves,
gate_with_tokens(
Arc::new(Mutex::new(HashSet::new())),
Arc::clone(&gate_invoked),
),
);
// The schema gates are unchanged by the establisher (they precede
// it, the wrapper order's schema step).
match call_forwarded(
&topo.producer,
serde_json::json!({
"listen": 0,
"substrate": "tcp",
"peer": {"host": "10", "port": 53},
}),
)
.await
{
Ok(_) => panic!("`listen: 0` must fail the open"),
Err(e) => assert_eq!(e.code, "INVALID_INPUT", "schema gate unchanged"),
}
match call_forwarded(
&topo.producer,
serde_json::json!({"listen": 7, "substrate": "tcp"}),
)
.await
{
Ok(_) => panic!("missing `peer` must fail the open"),
Err(e) => assert_eq!(e.code, "INVALID_INPUT", "schema gate unchanged"),
}
// THE DELTA: an unknown top-level field fails the establisher's
// serde parse → a typed open failure (vs the base shape's
// success-with-channel_id + birth-teardown; the typed posture is
// the improvement ADR-008 Amendment 2 pins).
let err = call_forwarded(
&topo.producer,
serde_json::json!({
"listen": 918273,
"substrate": "tcp",
"peer": {"host": "198", "port": 55233},
"sneaky": true
}),
)
.await
.expect_err("the malformed parse must fail the open as a typed error");
assert_eq!(err.code, "channel:open_failed");
let details = err.details.expect("typed error details");
assert_eq!(
details["reason"], "handler_error",
"an establisher parse failure maps through the existing table"
);
// The establisher's parse precedes the gate; the wrapper tears
// the transient allocation down; the pump handler never runs.
tokio::time::sleep(std::time::Duration::from_millis(300)).await;
assert!(
!gate_invoked.load(Ordering::SeqCst),
"the establisher parses BEFORE the gate; the gate never ran"
);
assert!(
topo.consumer_manager
.channel_ids()
.iter()
.all(|&id| id == 0),
"a parse-failed open leaves no channel on the opener side"
);
assert!(received.lock().await.params.is_none());
}