Files
alktunnels/tests/producer_open_op.rs
T
glm-5.3-flash 09d32d5aa6 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.
2026-09-08 09:37:22 +00:00

373 lines
13 KiB
Rust

//! Integration tests for the producer half — the open-op path
//! (establisher + pump handler + registration) over the duplex
//! harness, ported from the reverse POC's suite (16 tests rode the
//! topology) with the dial closure standing in for real sockets.
//!
//! Covered: establishment success + the typed-error table
//! (`unknown_resource` / `dial_failed` / `FORBIDDEN`), the plan flow
//! under same-resource concurrency (R-01 — no handoff race), the
//! per-call opener identity (CF-006 witness), late-registration
//! visibility (W2), pump round-trip through `pump_bidi` (R-02 — the
//! wrapper-managed pump, no spawn-and-forget), and out-of-band
//! `channel/close`.
mod harness;
use std::sync::Arc;
use alkcall::protocol::wire::CallError;
use alktunnels::params::{Substrate, TUNNEL_OPEN_SCOPE};
use alktunnels::producer::ResourceRegistry;
use harness::{echo_dial, failing_dial, wire, wire_with, Topology, TEST_AUTH_TOKEN};
fn params(resource: &str) -> alktunnels::params::TunnelParams {
alktunnels::params::TunnelParams {
resource: resource.to_string(),
substrate: Substrate::Tcp,
}
}
/// Call the producer's open op on channel 0 (the consumer's surface),
/// returning the wire `CallError` on failure.
async fn call_open(
topo: &Topology,
input: serde_json::Value,
auth_token: Option<&str>,
) -> Result<serde_json::Value, CallError> {
let mut payload = serde_json::json!({
"operationId": "channels/tunnel/sub",
"input": input,
});
if let Some(token) = auth_token {
payload["auth_token"] = serde_json::Value::String(token.to_string());
}
let response = topo.consumer_call.call_with_payload(payload).await;
response.result
}
async fn call_open_params(
topo: &Topology,
p: &alktunnels::params::TunnelParams,
auth_token: Option<&str>,
) -> Result<serde_json::Value, CallError> {
call_open(
topo,
serde_json::json!({
"resource": p.resource,
"substrate": match p.substrate {
Substrate::Tcp => "tcp",
Substrate::Udp => "udp",
Substrate::Unix => "unix",
},
}),
auth_token,
)
.await
}
/// Adopt the producer-allocated channel and pump it against in-process
/// halves (the consumer-side data plane — the session's take_halves
/// shape, driven manually until the consumer session task lands).
async fn adopt_and_pump(
topo: &Topology,
channel_id: u32,
) -> (
tokio::io::ReadHalf<tokio::io::DuplexStream>,
tokio::io::WriteHalf<tokio::io::DuplexStream>,
tokio::task::JoinHandle<(u64, u64)>,
) {
let (send, recv) = topo
.consumer_manager
.adopt_channel(channel_id, "alk/tunnel", None)
.await
.expect("adopt producer-allocated channel");
let bidi = alkcall::core::types::BiStream::from_joined(recv, send);
let (channel_end, local_end) = tokio::io::duplex(64 * 1024);
let (c_read, c_write) = tokio::io::split(channel_end);
let pump = tokio::spawn(alkcall::channels::pump::pump_bidi(bidi, c_read, c_write));
let (l_read, l_write) = tokio::io::split(local_end);
(l_read, l_write, pump)
}
/// Only channel 0 (the pre-negotiated call channel) may remain — a
/// failed open leaves no data channel on either side (the
/// phantom-channel property).
fn no_data_channels(topo: &Topology) -> bool {
let consumer = topo.consumer_manager.channel_ids();
let producer = topo.producer.manager().channel_ids();
consumer.iter().all(|&id| id == 0) && producer.iter().all(|&id| id == 0)
}
#[tokio::test]
async fn open_round_trips_through_pump_bidi() {
let registry = ResourceRegistry::new();
registry
.register("echo", Substrate::Tcp, "in-process")
.await;
let topo = wire(registry, echo_dial()).await;
let out = call_open_params(&topo, &params("echo"), None)
.await
.expect("open must succeed");
let channel_id = out["channel_id"].as_u64().expect("channel_id") as u32;
assert!(topo.producer.manager().has_channel(channel_id));
let (mut read, mut write, pump) = adopt_and_pump(&topo, channel_id).await;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
write.write_all(b"ping").await.expect("write");
write.flush().await.expect("flush");
let mut buf = vec![0u8; 4];
tokio::time::timeout(std::time::Duration::from_secs(5), read.read_exact(&mut buf))
.await
.expect("round trip timed out")
.expect("read");
assert_eq!(&buf, b"ping");
// EOF propagates: closing the local write half shuts the loop
// down through both pumps (the two-pump contract), and the
// wrapper reaps the producer-side channel on completion (R-02).
drop(write);
drop(read);
let (c2p, p2c) = tokio::time::timeout(std::time::Duration::from_secs(5), pump)
.await
.expect("pump completion timed out")
.expect("pump task");
assert!(c2p > 0 && p2c > 0, "both pumps moved bytes: {c2p}, {p2c}");
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
assert!(
!topo.producer.manager().has_channel(channel_id),
"wrapper reaped the producer-side channel after pump completion"
);
}
#[tokio::test]
async fn unknown_resource_is_typed_and_leaves_no_channel() {
let registry = ResourceRegistry::new();
registry
.register("echo", Substrate::Tcp, "in-process")
.await;
let topo = wire(registry, echo_dial()).await;
let err = call_open_params(&topo, &params("ghost"), None)
.await
.expect_err("unknown resource must fail the open");
assert_eq!(err.code, "channel:open_failed");
assert_eq!(establishment_reason_of(&err), Some("unknown_resource"));
assert!(no_data_channels(&topo));
}
fn establishment_reason_of(err: &CallError) -> Option<&str> {
err.details
.as_ref()
.and_then(|d| d.get("reason"))
.and_then(|r| r.as_str())
}
#[tokio::test]
async fn dial_failure_is_typed_and_leaves_no_channel() {
let registry = ResourceRegistry::new();
registry
.register("dead", Substrate::Tcp, "unreachable")
.await;
let topo = wire(registry, failing_dial("no route to target")).await;
let err = call_open_params(&topo, &params("dead"), None)
.await
.expect_err("failed dial must fail the open");
assert_eq!(err.code, "channel:open_failed");
assert_eq!(establishment_reason_of(&err), Some("dial_failed"));
assert!(no_data_channels(&topo));
}
#[tokio::test]
async fn open_denied_without_any_identity() {
let registry = ResourceRegistry::new();
registry
.register("echo", Substrate::Tcp, "in-process")
.await;
let topo = wire_with(
registry,
harness::RegistrationMode::Dial(echo_dial()),
None,
None,
Arc::new(alkcall::core::auth::NoopIdentityProvider),
)
.await;
let err = call_open_params(&topo, &params("echo"), None)
.await
.expect_err("identity-less open must be denied");
assert_eq!(err.code, "FORBIDDEN");
assert!(no_data_channels(&topo));
}
#[tokio::test]
async fn open_on_transport_identity_alone() {
// CF-005 (b): the transport identity (set before dialing)
// propagates to the serving dispatch and authorizes the scope
// gate. No token payload at all.
let registry = ResourceRegistry::new();
registry
.register("echo", Substrate::Tcp, "in-process")
.await;
let topo = wire(registry, echo_dial()).await;
let out = call_open_params(&topo, &params("echo"), None)
.await
.expect("transport-identity open must be authorized");
assert!(out["channel_id"].is_u64());
// CF-006: the establisher's per-call auth carried the END CALLER's
// identity (the consumer), not a synthetic install-time value.
let witness = topo.identity_witness.lock().await.clone();
assert_eq!(
witness.as_deref(),
Some("consumer"),
"establisher saw the per-call opener identity (CF-006)"
);
}
#[tokio::test]
async fn open_token_overrides_transport_identity() {
// CF-005 precedence: the payload token wins. The provider resolves
// the scoped token to the consumer identity — the witness proves
// the establisher saw it end-to-end.
let registry = ResourceRegistry::new();
registry
.register("echo", Substrate::Tcp, "in-process")
.await;
let topo = wire_with(
registry,
harness::RegistrationMode::Dial(echo_dial()),
None,
None,
Arc::new(harness::TestIdProvider),
)
.await;
let out = call_open_params(&topo, &params("echo"), Some(TEST_AUTH_TOKEN))
.await
.expect("token open authorized");
assert!(out["channel_id"].is_u64());
let witness = topo.identity_witness.lock().await.clone();
assert_eq!(witness.as_deref(), Some("consumer"));
}
#[tokio::test]
async fn concurrent_opens_same_resource_no_plan_race() {
// The R-01 plan flow under concurrency: two opens of the SAME
// resource — the establisher returns each dialed handle via ITS
// plan; no handoff map to race. Each round-trips its own marker.
let registry = ResourceRegistry::new();
registry
.register("echo", Substrate::Tcp, "in-process")
.await;
let topo = wire(registry, echo_dial()).await;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
let mut sessions = Vec::new();
for i in 0..2 {
let out = call_open_params(&topo, &params("echo"), None)
.await
.expect("open must succeed");
let channel_id = out["channel_id"].as_u64().unwrap() as u32;
let (mut read, mut write, _pump) = adopt_and_pump(&topo, channel_id).await;
let msg = format!("marker {i}");
write.write_all(msg.as_bytes()).await.expect("write");
write.flush().await.expect("flush");
let mut buf = vec![0u8; msg.len()];
tokio::time::timeout(std::time::Duration::from_secs(5), read.read_exact(&mut buf))
.await
.expect("round trip")
.expect("read");
assert_eq!(buf, msg.as_bytes());
sessions.push(channel_id);
}
assert_ne!(sessions[0], sessions[1]);
}
#[tokio::test]
async fn late_registration_is_visible() {
// W2: the harness registers the openable AFTER
// from_connection_with_serving. If the dispatcher cached
// registrations, opens would fail with unknown-operation — the
// successful opens elsewhere prove it; assert registry state too.
let registry = ResourceRegistry::new();
registry
.register("late", Substrate::Tcp, "in-process")
.await;
let topo = wire(registry, echo_dial()).await;
assert!(topo
.producer_registry
.registration("channels/tunnel/sub")
.is_some());
}
#[tokio::test]
async fn schema_rejects_unknown_substrate_loudly() {
// The registry schema-validates input before the establisher
// (ADR-001's loud posture): an unknown substrate value is a schema
// failure, not a silent pass-through.
let registry = ResourceRegistry::new();
registry
.register("echo", Substrate::Tcp, "in-process")
.await;
let topo = wire(registry, echo_dial()).await;
let err = call_open(
&topo,
serde_json::json!({"resource": "echo", "substrate": "sctp"}),
None,
)
.await
.expect_err("unknown substrate must fail the open");
assert_eq!(err.code, "INVALID_INPUT");
}
#[tokio::test]
async fn producer_outbound_calls_still_resolve_while_serving() {
// ADR-022 §2 both-sides sanity: the producer is the connect side
// AND the serving side; its outbound calling still resolves while
// it serves tunnel opens.
let registry = ResourceRegistry::new();
registry
.register("echo", Substrate::Tcp, "in-process")
.await;
let topo = wire(registry, echo_dial()).await;
let response = topo
.producer
.call_open_op("consumer/serves/nothing", serde_json::json!({}))
.await;
assert!(
response.result.is_err(),
"consumer serves nothing; the point is the reply RESOLVED"
);
}
#[tokio::test]
async fn registry_lookup_respects_substrate_key() {
// The registry is keyed by (resource, substrate): a resource
// produced for TCP is unknown for UDP — the same name on a
// different substrate is a different resource (ADR-001).
let registry = ResourceRegistry::new();
registry.register("svc", Substrate::Tcp, "in-process").await;
let topo = wire(registry, failing_dial("unreachable")).await;
let mut p = params("svc");
p.substrate = Substrate::Udp;
let err = call_open_params(&topo, &p, None)
.await
.expect_err("udp lookup of a tcp resource must miss");
assert_eq!(establishment_reason_of(&err), Some("unknown_resource"));
}
/// Type-shape assertions kept for harness parity.
#[allow(dead_code)]
fn type_assertions() {
let _dial = echo_dial();
let _scope: &str = TUNNEL_OPEN_SCOPE;
}