//! 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 { 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 { 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::WriteHalf, 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, ¶ms("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, ¶ms("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, ¶ms("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, echo_dial(), None, None, Arc::new(alkcall::core::auth::NoopIdentityProvider), ) .await; let err = call_open_params(&topo, ¶ms("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, ¶ms("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, echo_dial(), None, None, Arc::new(harness::TestIdProvider), ) .await; let out = call_open_params(&topo, ¶ms("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, ¶ms("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; }