Files
alktunnels/tests/producer_open_op.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

437 lines
15 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 schema_gate_rejects_missing_substrate_before_the_establisher() {
// The registry's input-schema gate runs before the establisher:
// a missing substrate is `INVALID_INPUT` with no reason code, and
// the establisher's own `handler_error` parse branch is
// unreachable past this gate (the schema pins both fields).
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"}), None)
.await
.expect_err("missing substrate must fail the schema gate");
assert_eq!(err.code, "INVALID_INPUT");
assert!(establishment_reason_of(&err).is_none());
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, which
// 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"));
}
#[tokio::test]
async fn tunnel_establisher_no_witness_wrapper_opens() {
// The no-witness wrapper (`tunnel_establisher`, the public
// default shape, producer.rs's witness-less establisher body):
// a full open through the wrapper round-trips. The wrapper
// differs from `tunnel_establisher_with_witness` ONLY in the
// witness (the CF-006 probe seam), so the default-registration
// shape is exercised end to end here.
let registry = ResourceRegistry::new();
registry
.register("echo", Substrate::Tcp, "in-process")
.await;
let topo = wire_with(
registry,
harness::RegistrationMode::DialNoWitness(echo_dial()),
Some(harness::consumer_identity()),
None,
Arc::new(alkcall::core::auth::NoopIdentityProvider),
)
.await;
let out = call_open_params(&topo, &params("echo"), None)
.await
.expect("the wrapper establisher opens");
let channel_id = out["channel_id"].as_u64().expect("channel_id") as u32;
let (mut read, mut write, pump) = adopt_and_pump(&topo, channel_id).await;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
write.write_all(b"no-witness").await.expect("write");
write.flush().await.expect("flush");
let mut buf = vec![0u8; 10];
tokio::time::timeout(std::time::Duration::from_secs(5), read.read_exact(&mut buf))
.await
.expect("round trip")
.expect("read");
assert_eq!(&buf, b"no-witness");
drop(write);
drop(read);
tokio::time::timeout(std::time::Duration::from_secs(5), pump)
.await
.expect("pump completion")
.expect("pump task");
}
/// Type-shape assertions kept for harness parity.
#[allow(dead_code)]
fn type_assertions() {
let _dial = echo_dial();
let _scope: &str = TUNNEL_OPEN_SCOPE;
}