use std::collections::HashSet;
use std::pin::Pin;
use std::sync::Arc;
use futures::stream::Stream;
use serde_json::{json, Value};
use crate::client::AdapterError;
use crate::core::types::Capabilities;
use crate::protocol::connection::CallConnection;
use crate::protocol::wire::ResponseEnvelope;
use crate::registry::context::OperationContext;
use crate::registry::registration::{
Handler, HandlerKind, HandlerRegistration, OperationProvenance, SinkHandler, StreamingHandler,
};
use crate::registry::spec::{
AccessControl, ChannelOpenSpec, ErrorDefinition, OperationSpec, OperationType, Visibility,
};
#[derive(Debug, Clone, Default)]
pub struct FromCallConfig {
pub namespace_prefix: Option<String>,
pub operation_filter: Option<HashSet<String>>,
}
impl FromCallConfig {
pub fn new() -> Self {
Self::default()
}
pub fn with_namespace_prefix(mut self, prefix: impl Into<String>) -> Self {
self.namespace_prefix = Some(prefix.into());
self
}
pub fn with_operation_filter(mut self, filter: HashSet<String>) -> Self {
self.operation_filter = Some(filter);
self
}
}
pub async fn from_call(
connection: &CallConnection,
config: FromCallConfig,
) -> Result<Vec<HandlerRegistration>, AdapterError> {
let discovered = discover_operations(connection).await?;
build_bundles(
discovered,
&config.namespace_prefix,
&config.operation_filter,
)
}
fn build_bundles(
discovered: Vec<OpSummary>,
namespace_prefix: &Option<String>,
operation_filter: &Option<HashSet<String>>,
) -> Result<Vec<HandlerRegistration>, AdapterError> {
let mut bundles = Vec::with_capacity(discovered.len());
let mut seen_names = HashSet::new();
for op_summary in discovered {
let remote_name = op_summary.name;
if let Some(filter) = operation_filter {
if !filter.contains(&remote_name) {
continue;
}
}
let spec = rebuild_spec_for(&op_summary.schema, &remote_name, namespace_prefix)?;
if !seen_names.insert(spec.name.clone()) {
return Err(AdapterError::SamePeerCollision {
message: format!(
"same-peer collision on import: {} (peer exposes two ops with the same name after prefix)",
spec.name
),
});
}
let kind = match spec.op_type {
OperationType::Sub => HandlerKind::Stream(make_streaming_forwarding_handler(
Arc::new(op_summary.connection.clone()),
remote_name,
)),
OperationType::Pub => HandlerKind::Sink(make_sink_forwarding_handler(
Arc::new(op_summary.connection.clone()),
remote_name,
)),
OperationType::Query | OperationType::Mutation => HandlerKind::Once(
make_forwarding_handler(Arc::new(op_summary.connection.clone()), remote_name),
),
};
bundles.push(HandlerRegistration::new(
spec,
kind,
OperationProvenance::FromCall,
None,
None,
Capabilities::new(),
));
}
Ok(bundles)
}
#[derive(Clone)]
struct OpSummary {
name: String,
schema: Value,
connection: CallConnection,
}
async fn discover_operations(connection: &CallConnection) -> Result<Vec<OpSummary>, AdapterError> {
let response = connection.call("services/list", json!({})).await;
let output = response.result.map_err(|e| AdapterError::DiscoveryFailed {
message: format!("services/list failed: {} ({})", e.code, e.message),
})?;
let ops = output
.get("operations")
.and_then(|v| v.as_array())
.ok_or_else(|| AdapterError::SchemaParse {
message: "services/list response missing 'operations' array".to_string(),
})?;
let mut summaries = Vec::with_capacity(ops.len());
for op in ops {
let name =
op.get("name")
.and_then(|v| v.as_str())
.ok_or_else(|| AdapterError::SchemaParse {
message: "services/list entry missing 'name'".to_string(),
})?;
let schema = fetch_schema(connection, name).await?;
summaries.push(OpSummary {
name: name.to_string(),
schema,
connection: connection.clone(),
});
}
Ok(summaries)
}
async fn fetch_schema(connection: &CallConnection, name: &str) -> Result<Value, AdapterError> {
let response = connection
.call("services/schema", json!({ "name": name }))
.await;
response.result.map_err(|e| AdapterError::DiscoveryFailed {
message: format!(
"services/schema for {name} failed: {} ({})",
e.code, e.message
),
})
}
pub(crate) fn rebuild_spec_for(
schema_json: &Value,
remote_name: &str,
namespace_prefix: &Option<String>,
) -> Result<OperationSpec, AdapterError> {
let op_type = parse_op_type(
schema_json
.get("op_type")
.and_then(|v| v.as_str())
.ok_or_else(|| AdapterError::SchemaParse {
message: format!("schema for {remote_name} missing op_type"),
})?,
)?;
let visibility = parse_visibility(
schema_json
.get("visibility")
.and_then(|v| v.as_str())
.unwrap_or("external"),
);
let input_schema = schema_json
.get("input_schema")
.cloned()
.unwrap_or(Value::Null);
let output_schema = schema_json
.get("output_schema")
.cloned()
.unwrap_or(Value::Null);
let error_schemas = schema_json
.get("error_schemas")
.and_then(|v| v.as_array())
.map(|arr| arr.iter().filter_map(parse_error_definition).collect())
.unwrap_or_default();
let access_control = schema_json
.get("access_control")
.map(parse_access_control)
.unwrap_or_default();
let name = match namespace_prefix {
Some(prefix) if !prefix.is_empty() => format!("{prefix}/{remote_name}"),
_ => remote_name.to_string(),
};
let mut spec = OperationSpec::new(
name,
op_type,
visibility,
input_schema,
output_schema,
error_schemas,
access_control,
schema_json
.get("resource_id_path")
.and_then(|v| v.as_str())
.map(String::from),
);
let explicit_alpn = schema_json
.get("channel_open_alpn")
.and_then(|v| v.as_str())
.map(str::trim)
.filter(|s| !s.is_empty());
if schema_json
.get("channel_open")
.and_then(|v| v.as_bool())
.unwrap_or(false)
{
let alpn = explicit_alpn
.map(String::from)
.or_else(|| derive_alpn_from_op_name(remote_name));
if let Some(alpn) = alpn {
spec = spec.with_channel_open(ChannelOpenSpec::new(alpn));
}
} else if let Some(_alpn) = explicit_alpn {
tracing::debug!(
op = remote_name,
"rebuild_spec_for: channel_open_alpn present without the channel_open \
boolean marker — ignored (the boolean is the dispatch hint)"
);
}
if let Some(publish_schema) = schema_json.get("publish_schema") {
if !publish_schema.is_null() {
spec = spec.with_publish_schema(publish_schema.clone());
}
}
if let Some(description) = schema_json.get("description").and_then(|v| v.as_str()) {
spec = spec.with_description(description);
}
Ok(spec)
}
fn derive_alpn_from_op_name(op_name: &str) -> Option<String> {
let rest = op_name.strip_prefix("channels/")?;
let (segment, _flavor) = rest.rsplit_once('/')?;
if segment.is_empty() {
return None;
}
if segment.starts_with("alk/") || segment == "alk" || segment.contains('/') {
Some(segment.to_string())
} else {
Some(format!("alk/{segment}"))
}
}
fn parse_op_type(s: &str) -> Result<OperationType, AdapterError> {
match s {
"query" => Ok(OperationType::Query),
"mutation" => Ok(OperationType::Mutation),
"sub" => Ok(OperationType::Sub),
"pub" => Ok(OperationType::Pub),
other => Err(AdapterError::SchemaParse {
message: format!("unknown op_type: {other}"),
}),
}
}
fn parse_visibility(s: &str) -> Visibility {
match s {
"internal" => Visibility::Internal,
_ => Visibility::External,
}
}
fn parse_error_definition(v: &Value) -> Option<ErrorDefinition> {
Some(ErrorDefinition {
code: v.get("code")?.as_str()?.to_string(),
description: v
.get("description")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string(),
schema: v.get("schema").cloned().unwrap_or(Value::Null),
http_status: v
.get("http_status")
.and_then(|v| v.as_u64())
.map(|n| n as u16),
})
}
fn parse_access_control(v: &Value) -> AccessControl {
AccessControl {
required_scopes: v
.get("required_scopes")
.and_then(|v| v.as_array())
.map(|arr| {
arr.iter()
.filter_map(|s| s.as_str().map(String::from))
.collect()
})
.unwrap_or_default(),
required_scopes_any: v
.get("required_scopes_any")
.and_then(|v| v.as_array())
.map(|arr| {
arr.iter()
.filter_map(|s| s.as_str().map(String::from))
.collect()
}),
resource_type: v
.get("resource_type")
.and_then(|v| v.as_str())
.map(String::from),
resource_action: v
.get("resource_action")
.and_then(|v| v.as_str())
.map(String::from),
}
}
pub(crate) fn make_forwarding_handler(
connection: Arc<CallConnection>,
remote_name: String,
) -> Handler {
use crate::registry::registration::make_handler;
make_handler(move |input, context| {
let connection = Arc::clone(&connection);
let remote_name = remote_name.clone();
async move {
let payload = build_forwarded_payload(&remote_name, input, &context);
let response = connection.call_with_payload(payload).await;
ResponseEnvelope {
request_id: context.request_id,
result: response.result,
}
}
})
}
pub(crate) fn make_streaming_forwarding_handler(
connection: Arc<CallConnection>,
remote_name: String,
) -> StreamingHandler {
use crate::registry::registration::make_streaming_handler;
use futures::stream::{once, StreamExt};
make_streaming_handler(move |input, context| {
let connection = Arc::clone(&connection);
let remote_name = remote_name.clone();
once(async move {
let payload = build_forwarded_payload(&remote_name, input, &context);
connection.subscribe_with_payload(payload).await
})
.flatten()
})
}
pub(crate) fn make_sink_forwarding_handler(
connection: Arc<CallConnection>,
remote_name: String,
) -> SinkHandler {
use crate::registry::registration::make_sink_handler;
use futures::stream::StreamExt;
make_sink_handler(move |input, context, publish_stream| {
let connection = Arc::clone(&connection);
let remote_name = remote_name.clone();
async move {
let payload = build_forwarded_payload(&remote_name, input, &context);
let value_stream: Pin<Box<dyn Stream<Item = Value> + Send>> = Box::pin(
publish_stream
.take_while(|item| futures::future::ready(item.is_ok()))
.filter_map(|item| futures::future::ready(item.ok())),
);
connection.publish_with_payload(payload, value_stream).await
}
})
}
pub(crate) fn build_forwarded_payload(
operation_id: &str,
input: Value,
context: &OperationContext,
) -> Value {
let mut payload = serde_json::Map::new();
payload.insert(
"operationId".to_string(),
Value::String(operation_id.to_string()),
);
payload.insert("input".to_string(), input);
if let Some(originator) = &context.identity {
if let Ok(value) = serde_json::to_value(originator) {
payload.insert("forwarded_for".to_string(), value);
}
}
Value::Object(payload)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::auth::Identity;
use crate::core::types::Capabilities;
use crate::protocol::connection::CallConnection;
use crate::registry::discovery::spec_to_json;
use crate::registry::registration::{make_handler, make_streaming_handler, PublishStream};
use crate::registry::spec::OperationType;
use futures::StreamExt;
use std::collections::HashMap;
use std::sync::Mutex as StdMutex;
use crate::protocol::sink_empty_connection as stub_connection;
fn sample_schema_json(name: &str, op_type: &str) -> Value {
json!({
"name": name,
"namespace": name.split('/').next().unwrap_or(""),
"op_type": op_type,
"visibility": "external",
"input_schema": {"type": "object"},
"output_schema": {"type": "string"},
"error_schemas": [],
"access_control": {"required_scopes": []},
})
}
#[test]
fn rebuild_spec_no_prefix_preserves_name() {
let schema = sample_schema_json("fs/readFile", "query");
let spec = rebuild_spec_for(&schema, "fs/readFile", &None).expect("rebuild");
assert_eq!(spec.name, "fs/readFile");
assert_eq!(spec.op_type, OperationType::Query);
assert_eq!(spec.visibility, Visibility::External);
}
#[test]
fn rebuild_spec_with_prefix_applies_prefix() {
let schema = sample_schema_json("fs/readFile", "query");
let spec =
rebuild_spec_for(&schema, "fs/readFile", &Some("worker".to_string())).expect("rebuild");
assert_eq!(spec.name, "worker/fs/readFile");
}
#[test]
fn rebuild_spec_unknown_op_type_returns_schema_parse() {
let schema = sample_schema_json("fs/readFile", "weird");
match rebuild_spec_for(&schema, "fs/readFile", &None) {
Err(AdapterError::SchemaParse { .. }) => {}
other => panic!("expected SchemaParse, got {other:?}"),
}
}
#[test]
fn rebuild_spec_missing_op_type_returns_schema_parse() {
let schema = json!({"name": "fs/readFile"});
match rebuild_spec_for(&schema, "fs/readFile", &None) {
Err(AdapterError::SchemaParse { .. }) => {}
other => panic!("expected SchemaParse, got {other:?}"),
}
}
#[test]
fn rebuild_spec_parses_error_schemas_and_acl() {
let schema = json!({
"name": "fs/readFileErr",
"namespace": "fs",
"op_type": "query",
"visibility": "external",
"input_schema": {},
"output_schema": {},
"error_schemas": [{
"code": "FILE_NOT_FOUND",
"description": "file not found",
"schema": {"type": "object"},
"http_status": 404,
}],
"access_control": {
"required_scopes": ["fs:read"],
"required_scopes_any": null,
"resource_type": "fs",
"resource_action": "read",
},
});
let spec = rebuild_spec_for(&schema, "fs/readFileErr", &None).expect("rebuild");
assert_eq!(spec.error_schemas.len(), 1);
assert_eq!(spec.error_schemas[0].code, "FILE_NOT_FOUND");
assert_eq!(spec.error_schemas[0].http_status, Some(404));
assert_eq!(
spec.access_control.required_scopes,
vec!["fs:read".to_string()]
);
assert_eq!(spec.access_control.resource_type.as_deref(), Some("fs"));
}
#[test]
fn spec_round_trips_resource_id_path() {
use crate::registry::discovery::spec_to_json_pub;
let spec = OperationSpec::new(
"fs/readFile",
OperationType::Query,
Visibility::External,
json!({}),
json!({}),
vec![],
crate::registry::spec::AccessControl::default(),
Some("/path".to_string()),
);
let wire = spec_to_json_pub(&spec);
assert_eq!(
wire.get("resource_id_path").and_then(|v| v.as_str()),
Some("/path"),
"resource_id_path serialized"
);
let rebuilt = rebuild_spec_for(&wire, "fs/readFile", &None).expect("rebuild");
assert_eq!(
rebuilt.resource_id_path.as_deref(),
Some("/path"),
"resource_id_path survives the round-trip"
);
}
#[test]
fn spec_without_resource_id_path_stays_absent_through_round_trip() {
use crate::registry::discovery::spec_to_json_pub;
let spec = OperationSpec::new(
"fs/readFile",
OperationType::Query,
Visibility::External,
json!({}),
json!({}),
vec![],
crate::registry::spec::AccessControl::default(),
None,
);
let wire = spec_to_json_pub(&spec);
assert!(wire.get("resource_id_path").is_none());
let rebuilt = rebuild_spec_for(&wire, "fs/readFile", &None).expect("rebuild");
assert_eq!(rebuilt.resource_id_path, None);
}
#[test]
fn rebuild_spec_channel_open_marker_set_for_channels_alpn_op() {
let mut schema = sample_schema_json("channels/tty/sub", "sub");
schema["channel_open"] = json!(true);
let spec = rebuild_spec_for(&schema, "channels/tty/sub", &None).expect("rebuild");
let marker = spec.channel_open.expect("channel_open marker parsed");
assert_eq!(marker.alpn, "alk/tty");
}
#[test]
fn rebuild_spec_channel_open_marker_absent_for_plain_op() {
let schema = sample_schema_json("fs/readFile", "query");
let spec = rebuild_spec_for(&schema, "fs/readFile", &None).expect("rebuild");
assert!(
spec.channel_open.is_none(),
"plain op must not get a channel_open marker"
);
}
#[test]
fn rebuild_spec_channel_open_marker_false_is_absent() {
let mut schema = sample_schema_json("channels/tty/sub", "sub");
schema["channel_open"] = json!(false);
let spec = rebuild_spec_for(&schema, "channels/tty/sub", &None).expect("rebuild");
assert!(
spec.channel_open.is_none(),
"channel_open: false must be treated as absent"
);
}
#[test]
fn rebuild_spec_channel_open_marker_ignored_for_non_channels_op_name() {
let mut schema = sample_schema_json("fs/readFile", "query");
schema["channel_open"] = json!(true);
let spec = rebuild_spec_for(&schema, "fs/readFile", &None).expect("rebuild");
assert!(
spec.channel_open.is_none(),
"marker on non-channels op name is ignored"
);
}
#[test]
fn rebuild_spec_flavor_form_reconstructs_marker_via_explicit_alpn() {
use crate::registry::discovery::spec_to_json_pub;
use crate::registry::spec::ChannelOpenSpec;
let spec = OperationSpec::new(
"channels/tunnel/direct",
OperationType::Sub,
Visibility::External,
json!({}),
json!({}),
vec![],
crate::registry::spec::AccessControl::default(),
None,
)
.with_channel_open(ChannelOpenSpec::new("alk/tunnel"));
let wire = spec_to_json_pub(&spec);
assert_eq!(wire["channel_open"], json!(true));
assert_eq!(
wire["channel_open_alpn"],
json!("alk/tunnel"),
"non-standard shape emits the explicit ALPN"
);
let rebuilt = rebuild_spec_for(&wire, "channels/tunnel/direct", &None).expect("rebuild");
let marker = rebuilt.channel_open.expect("marker reconstructed");
assert_eq!(marker.alpn, "alk/tunnel");
let mut old_wire = wire.clone();
old_wire
.as_object_mut()
.expect("object")
.remove("channel_open_alpn");
let rebuilt =
rebuild_spec_for(&old_wire, "channels/tunnel/direct", &None).expect("rebuild");
let marker = rebuilt
.channel_open
.expect("marker reconstructed from the boolean alone (skew case)");
assert_eq!(marker.alpn, "alk/tunnel");
}
#[test]
fn rebuild_spec_explicit_alpn_overrides_derivation() {
let mut schema = sample_schema_json("channels/x/direct", "sub");
schema["channel_open"] = json!(true);
schema["channel_open_alpn"] = json!("alk/x/direct");
let spec = rebuild_spec_for(&schema, "channels/x/direct", &None).expect("rebuild");
let marker = spec.channel_open.expect("marker");
assert_eq!(
marker.alpn, "alk/x/direct",
"the explicit string wins over the strip-last derivation (alk/x)"
);
}
#[test]
fn rebuild_spec_explicit_alpn_without_boolean_is_ignored() {
let mut schema = sample_schema_json("fs/readFile", "query");
schema["channel_open_alpn"] = json!("alk/tty");
let spec = rebuild_spec_for(&schema, "fs/readFile", &None).expect("rebuild");
assert!(
spec.channel_open.is_none(),
"channel_open_alpn without the boolean never marks an op"
);
}
#[test]
fn rebuild_spec_empty_explicit_alpn_falls_back_to_derivation() {
for bad in ["", " "] {
let mut schema = sample_schema_json("channels/tunnel/direct", "sub");
schema["channel_open"] = json!(true);
schema["channel_open_alpn"] = json!(bad);
let spec = rebuild_spec_for(&schema, "channels/tunnel/direct", &None).expect("rebuild");
let marker = spec
.channel_open
.expect("marker reconstructed via the derivation fallback");
assert_eq!(
marker.alpn, "alk/tunnel",
"empty explicit ALPN `{bad}` must not override the derivation"
);
}
}
#[test]
fn spec_empty_segment_name_is_not_treated_as_standard_shape() {
use crate::registry::discovery::{
op_name_is_standard_channel_open_shape, spec_to_json_pub,
};
use crate::registry::spec::ChannelOpenSpec;
assert!(!op_name_is_standard_channel_open_shape("channels//sub"));
let spec = OperationSpec::new(
"channels//sub",
OperationType::Sub,
Visibility::External,
json!({}),
json!({}),
vec![],
crate::registry::spec::AccessControl::default(),
None,
)
.with_channel_open(ChannelOpenSpec::new("alk/tty"));
let wire = spec_to_json_pub(&spec);
assert_eq!(
wire["channel_open_alpn"],
json!("alk/tty"),
"the empty-segment name is non-derivable — the explicit string must ride"
);
let rebuilt = rebuild_spec_for(&wire, "channels//sub", &None).expect("rebuild");
let marker = rebuilt.channel_open.expect("marker reconstructs");
assert_eq!(marker.alpn, "alk/tty");
}
#[test]
fn spec_standard_shape_channel_open_stays_boolean_only() {
use crate::registry::discovery::spec_to_json_pub;
use crate::registry::spec::ChannelOpenSpec;
for name in ["channels/tty/sub", "channels/custom/proto/sub"] {
let spec = OperationSpec::new(
name,
OperationType::Sub,
Visibility::External,
json!({}),
json!({}),
vec![],
crate::registry::spec::AccessControl::default(),
None,
)
.with_channel_open(ChannelOpenSpec::new(if name == "channels/tty/sub" {
"alk/tty"
} else {
"custom/proto"
}));
let wire = spec_to_json_pub(&spec);
assert_eq!(wire["channel_open"], json!(true), "{name}");
assert!(
wire.get("channel_open_alpn").is_none(),
"{name}: standard shape must not emit the explicit key (byte-stable)"
);
let rebuilt = rebuild_spec_for(&wire, name, &None).expect("rebuild");
let marker = rebuilt.channel_open.expect("marker");
assert_eq!(
marker.alpn.as_ref(),
wire["channel_open_alpn"]
.as_str()
.unwrap_or(if name == "channels/tty/sub" {
"alk/tty"
} else {
"custom/proto"
}),
"{name} round-trips"
);
}
}
#[test]
fn spec_standard_shape_wire_payload_golden_pin() {
use crate::registry::discovery::spec_to_json_pub;
use crate::registry::spec::ChannelOpenSpec;
let spec = OperationSpec::new(
"channels/tty/sub",
OperationType::Sub,
Visibility::External,
json!({}),
json!({}),
vec![],
crate::registry::spec::AccessControl::default(),
None,
)
.with_channel_open(ChannelOpenSpec::new("alk/tty"));
let wire = spec_to_json_pub(&spec);
assert_eq!(
wire,
json!({
"name": "channels/tty/sub",
"namespace": "channels",
"op_type": "sub",
"visibility": "external",
"input_schema": {},
"output_schema": {},
"error_schemas": [],
"access_control": {
"required_scopes": [],
"required_scopes_any": null,
"resource_type": null,
"resource_action": null,
},
"channel_open": true,
}),
"the standard-shape payload is exactly this literal — no \
channel_open_alpn key, nothing else added"
);
}
#[test]
fn spec_round_trips_flavor_form_marker() {
use crate::registry::discovery::spec_to_json_pub;
use crate::registry::spec::ChannelOpenSpec;
let spec = OperationSpec::new(
"channels/tunnel/direct",
OperationType::Sub,
Visibility::External,
json!({}),
json!({}),
vec![],
crate::registry::spec::AccessControl::default(),
None,
)
.with_channel_open(ChannelOpenSpec::new("alk/tunnel"))
.with_description("dynamic-target egress (alktunnels ADR-007)");
let wire = spec_to_json_pub(&spec);
let rebuilt = rebuild_spec_for(&wire, "channels/tunnel/direct", &None).expect("rebuild");
let marker = rebuilt
.channel_open
.expect("marker survives the round trip");
assert_eq!(marker.alpn, "alk/tunnel");
assert_eq!(
rebuilt.description.as_deref(),
Some("dynamic-target egress (alktunnels ADR-007)")
);
}
#[test]
fn rebuild_spec_publish_schema_set_when_present() {
let publish_schema = json!({
"type": "object",
"properties": { "bytes": { "type": "string" } },
"required": ["bytes"]
});
let mut schema = sample_schema_json("fs/upload", "pub");
schema["publish_schema"] = publish_schema.clone();
let spec = rebuild_spec_for(&schema, "fs/upload", &None).expect("rebuild");
assert_eq!(spec.publish_schema.as_ref(), Some(&publish_schema));
}
#[test]
fn rebuild_spec_publish_schema_absent_when_omitted() {
let schema = sample_schema_json("fs/upload", "pub");
let spec = rebuild_spec_for(&schema, "fs/upload", &None).expect("rebuild");
assert!(
spec.publish_schema.is_none(),
"publish_schema must be absent when the discovered schema omits it"
);
}
#[test]
fn rebuild_spec_publish_schema_absent_when_null() {
let mut schema = sample_schema_json("fs/upload", "pub");
schema["publish_schema"] = Value::Null;
let spec = rebuild_spec_for(&schema, "fs/upload", &None).expect("rebuild");
assert!(
spec.publish_schema.is_none(),
"publish_schema: null must be treated as absent"
);
}
#[test]
fn rebuild_spec_publish_schema_round_trips_with_spec_to_json() {
let publish_schema = json!({
"type": "object",
"properties": { "n": { "type": "integer" } },
"required": ["n"]
});
let spec = OperationSpec::new(
"fs/upload",
OperationType::Pub,
Visibility::External,
json!({}),
json!({}),
vec![],
AccessControl::default(),
None,
)
.with_publish_schema(publish_schema.clone());
let serialized = spec_to_json(&spec);
let rebuilt = rebuild_spec_for(&serialized, "fs/upload", &None).expect("rebuild");
assert_eq!(rebuilt.publish_schema.as_ref(), Some(&publish_schema));
}
#[test]
fn spec_round_trips_description() {
use crate::registry::discovery::spec_to_json_pub;
let spec = OperationSpec::new(
"channels/tty/sub",
OperationType::Sub,
Visibility::External,
json!({}),
json!({}),
vec![],
crate::registry::spec::AccessControl::default(),
None,
)
.with_description("Interactive TTY sessions");
let wire = spec_to_json_pub(&spec);
assert_eq!(
wire.get("description").and_then(|v| v.as_str()),
Some("Interactive TTY sessions"),
"description serialized"
);
let rebuilt = rebuild_spec_for(&wire, "channels/tty/sub", &None).expect("rebuild");
assert_eq!(
rebuilt.description.as_deref(),
Some("Interactive TTY sessions"),
"description survives the round-trip"
);
}
#[test]
fn spec_without_description_stays_absent_through_round_trip() {
use crate::registry::discovery::spec_to_json_pub;
let spec = OperationSpec::new(
"fs/readFile",
OperationType::Query,
Visibility::External,
json!({}),
json!({}),
vec![],
crate::registry::spec::AccessControl::default(),
None,
);
let wire = spec_to_json_pub(&spec);
assert!(wire.get("description").is_none());
let rebuilt = rebuild_spec_for(&wire, "fs/readFile", &None).expect("rebuild");
assert_eq!(rebuilt.description, None);
}
#[test]
fn derive_alpn_from_op_name_strips_channels_prefix() {
assert_eq!(
derive_alpn_from_op_name("channels/tty/sub"),
Some("alk/tty".to_string())
);
assert_eq!(
derive_alpn_from_op_name("channels/tunnel/pub"),
Some("alk/tunnel".to_string())
);
}
#[test]
fn derive_alpn_from_op_name_returns_none_for_non_channels_op() {
assert_eq!(derive_alpn_from_op_name("fs/readFile"), None);
assert_eq!(derive_alpn_from_op_name("channel/open"), None);
assert_eq!(derive_alpn_from_op_name("channels/"), None);
}
#[test]
fn derive_alpn_from_op_name_multi_segment_non_alknet_alpn_survives() {
assert_eq!(
derive_alpn_from_op_name("channels/custom/proto/sub"),
Some("custom/proto".to_string()),
"multi-segment non-alk/* ALPN uses full ALPN as the path segment"
);
assert_eq!(
derive_alpn_from_op_name("channels/vendor/service/run/pub"),
Some("vendor/service/run".to_string())
);
}
#[test]
fn derive_alpn_from_op_name_explicit_alknet_prefix_returned_as_is() {
assert_eq!(
derive_alpn_from_op_name("channels/alk/tty/sub"),
Some("alk/tty".to_string())
);
}
#[test]
fn derive_alpn_from_op_name_strips_pub_suffix() {
assert_eq!(
derive_alpn_from_op_name("channels/tty/pub"),
Some("alk/tty".to_string())
);
}
#[test]
fn derive_alpn_from_op_name_flavor_form() {
assert_eq!(
derive_alpn_from_op_name("channels/tunnel/direct"),
Some("alk/tunnel".to_string()),
"alktunnels ADR-007's direct op (gate 1)"
);
assert_eq!(
derive_alpn_from_op_name("channels/tunnel/forwarded"),
Some("alk/tunnel".to_string()),
"alktunnels ADR-008's forwarded op"
);
}
#[test]
fn derive_alpn_from_op_name_non_op_type_suffix_is_not_special_cased() {
assert_eq!(
derive_alpn_from_op_name("channels/tty/query"),
Some("alk/tty".to_string()),
"the derivation is shape-blind; the marker is the gate"
);
assert_eq!(derive_alpn_from_op_name("channels/tty"), None);
assert_eq!(derive_alpn_from_op_name("channels/"), None);
}
#[test]
fn derive_alpn_from_op_name_edge_shapes() {
assert_eq!(
derive_alpn_from_op_name("channels//sub"),
None,
"the empty segment is not a derivable ALPN"
);
assert_eq!(
derive_alpn_from_op_name("channels//direct"),
None,
"the guard is flavor-blind"
);
assert_eq!(
derive_alpn_from_op_name("channels"),
None,
"no path segment to strip"
);
assert_eq!(
derive_alpn_from_op_name("channels/x/sub/extra"),
Some("x/sub".to_string()),
"the last segment is stripped unconditionally — `x/sub` is \
treated as a full (multi-segment) ALPN, the same rule the \
5-segment case pins; pre-amendment this returned None \
(only /sub//pub were stripped)"
);
assert_eq!(
derive_alpn_from_op_name("channels/alk/tty/sub"),
Some("alk/tty".to_string()),
"the verbatim alk/* segment rides as-is"
);
}
#[test]
fn from_call_config_builder_methods() {
let config = FromCallConfig::new()
.with_namespace_prefix("worker")
.with_operation_filter(HashSet::from(["fs/readFile".to_string()]));
assert_eq!(config.namespace_prefix.as_deref(), Some("worker"));
assert!(config.operation_filter.unwrap().contains("fs/readFile"));
}
#[tokio::test]
async fn from_call_against_mock_connection_returns_discovery_failed() {
let conn = CallConnection::new(stub_connection());
let result = from_call(&conn, FromCallConfig::new()).await;
match result {
Err(AdapterError::DiscoveryFailed { .. }) => {}
Err(other) => panic!("expected DiscoveryFailed, got another error variant: {other}"),
Ok(_) => panic!("expected DiscoveryFailed on mock connection, got Ok"),
}
}
#[test]
fn from_call_provenance_is_from_call_and_leaf_fields() {
let spec = OperationSpec::new(
"worker/echo",
OperationType::Query,
Visibility::External,
json!({}),
json!({}),
vec![],
AccessControl::default(),
None,
);
let handler = make_forwarding_handler(
Arc::new(CallConnection::new(stub_connection())),
"worker/echo".to_string(),
);
let reg = HandlerRegistration::new(
spec,
HandlerKind::Once(handler),
OperationProvenance::FromCall,
None,
None,
Capabilities::new(),
);
assert_eq!(reg.provenance, OperationProvenance::FromCall);
assert!(reg.composition_authority.is_none());
assert!(reg.scoped_env.is_none());
}
struct NoopEnv;
#[async_trait::async_trait]
impl crate::registry::env::OperationEnv for NoopEnv {
async fn invoke_with_policy(
&self,
_ns: &str,
_op: &str,
_input: Value,
parent: &OperationContext,
_policy: crate::registry::context::AbortPolicy,
) -> ResponseEnvelope {
ResponseEnvelope::ok(parent.request_id.clone(), Value::Null)
}
fn contains(&self, _name: &str) -> bool {
false
}
}
fn test_context(identity: Option<Identity>) -> OperationContext {
use crate::registry::context::{AbortPolicy, ScopedPeerEnv};
use std::collections::HashMap;
use std::time::{Duration, Instant};
OperationContext {
request_id: "req-test".to_string(),
parent_request_id: None,
identity,
handler_identity: None,
forwarded_for: None,
capabilities: Capabilities::new(),
metadata: HashMap::new(),
scoped_env: ScopedPeerEnv::empty(),
env: Arc::new(NoopEnv),
abort_policy: AbortPolicy::default(),
deadline: Some(Instant::now() + Duration::from_secs(30)),
internal: false,
ownership: None,
}
}
fn alice_identity() -> Identity {
Identity {
id: "alice".to_string(),
scopes: vec!["fs:read".to_string()],
resources: HashMap::new(),
}
}
#[test]
fn build_forwarded_payload_populates_forwarded_for_from_context_identity() {
let ctx = test_context(Some(alice_identity()));
let payload = build_forwarded_payload("fs/readFile", json!({"p": 1}), &ctx);
assert_eq!(payload["operationId"], "fs/readFile");
assert_eq!(payload["input"], json!({"p": 1}));
let forwarded_for = payload.get("forwarded_for").expect("forwarded_for present");
assert_eq!(forwarded_for["id"], "alice");
assert_eq!(forwarded_for["scopes"][0], "fs:read");
}
#[test]
fn build_forwarded_payload_omits_forwarded_for_when_context_identity_is_none() {
let ctx = test_context(None);
let payload = build_forwarded_payload("fs/readFile", json!({}), &ctx);
assert!(payload.get("forwarded_for").is_none());
assert_eq!(payload["operationId"], "fs/readFile");
}
#[tokio::test]
async fn forwarding_handler_populates_forwarded_for_from_context_identity() {
let conn = Arc::new(CallConnection::new(stub_connection()));
let captured_payload = Arc::new(StdMutex::new(None::<Value>));
let captured = Arc::clone(&captured_payload);
let handler: Handler = {
let conn = Arc::clone(&conn);
make_handler(move |input, context| {
let conn = Arc::clone(&conn);
let captured = Arc::clone(&captured);
let remote_name = "fs/readFile".to_string();
async move {
let payload = build_forwarded_payload(&remote_name, input, &context);
*captured.lock().unwrap() = Some(payload.clone());
let response = conn.call_with_payload(payload).await;
ResponseEnvelope {
request_id: context.request_id,
result: response.result,
}
}
})
};
let ctx = test_context(Some(alice_identity()));
let _ = handler(json!({}), ctx).await;
let payload = captured_payload.lock().unwrap().clone().expect("captured");
assert_eq!(payload["forwarded_for"]["id"], "alice");
assert_eq!(payload["operationId"], "fs/readFile");
}
#[tokio::test]
async fn forwarding_handler_omits_forwarded_for_when_context_identity_is_none() {
let conn = Arc::new(CallConnection::new(stub_connection()));
let captured_payload = Arc::new(StdMutex::new(None::<Value>));
let captured = Arc::clone(&captured_payload);
let handler: Handler = {
let conn = Arc::clone(&conn);
make_handler(move |input, context| {
let conn = Arc::clone(&conn);
let captured = Arc::clone(&captured);
let remote_name = "fs/readFile".to_string();
async move {
let payload = build_forwarded_payload(&remote_name, input, &context);
*captured.lock().unwrap() = Some(payload.clone());
let response = conn.call_with_payload(payload).await;
ResponseEnvelope {
request_id: context.request_id,
result: response.result,
}
}
})
};
let ctx = test_context(None);
let _ = handler(json!({}), ctx).await;
let payload = captured_payload.lock().unwrap().clone().expect("captured");
assert!(
payload.get("forwarded_for").is_none(),
"forwarded_for must be omitted when context.identity is None"
);
}
fn op_summary(name: &str, conn: &CallConnection) -> OpSummary {
OpSummary {
name: name.to_string(),
schema: sample_schema_json(name, "query"),
connection: conn.clone(),
}
}
fn op_summary_typed(name: &str, op_type: &str, conn: &CallConnection) -> OpSummary {
OpSummary {
name: name.to_string(),
schema: sample_schema_json(name, op_type),
connection: conn.clone(),
}
}
#[test]
fn build_bundles_same_peer_collision_returns_same_peer_collision_error() {
let conn = CallConnection::new(stub_connection());
let discovered = vec![
op_summary("worker/exec", &conn),
op_summary("worker/exec", &conn),
];
match build_bundles(discovered, &None, &None) {
Err(AdapterError::SamePeerCollision { message }) => {
assert!(message.contains("worker/exec"));
}
Err(other) => panic!("expected SamePeerCollision, got another error: {other}"),
Ok(_) => panic!("expected SamePeerCollision, got Ok"),
}
}
#[test]
fn build_bundles_same_peer_collision_after_prefix_returns_error() {
let conn = CallConnection::new(stub_connection());
let discovered = vec![
op_summary("fs/readFile", &conn),
op_summary("fs/readFile", &conn),
];
match build_bundles(discovered, &Some("worker".to_string()), &None) {
Err(AdapterError::SamePeerCollision { message }) => {
assert!(message.contains("worker/fs/readFile"));
}
Err(other) => panic!("expected SamePeerCollision, got another error: {other}"),
Ok(_) => panic!("expected SamePeerCollision, got Ok"),
}
}
#[test]
fn build_bundles_cross_peer_same_name_does_not_collide() {
let conn_a = CallConnection::new(stub_connection());
let conn_b = CallConnection::new(stub_connection());
let bundles_a = build_bundles(vec![op_summary("container/exec", &conn_a)], &None, &None)
.expect("peer a bundles");
let bundles_b = build_bundles(vec![op_summary("container/exec", &conn_b)], &None, &None)
.expect("peer b bundles");
assert_eq!(bundles_a.len(), 1);
assert_eq!(bundles_b.len(), 1);
assert_eq!(bundles_a[0].spec.name, "container/exec");
assert_eq!(bundles_b[0].spec.name, "container/exec");
}
#[test]
fn build_bundles_distinct_names_in_same_peer_do_not_collide() {
let conn = CallConnection::new(stub_connection());
let discovered = vec![
op_summary("worker/exec", &conn),
op_summary("worker/status", &conn),
op_summary("fs/readFile", &conn),
];
let bundles = build_bundles(discovered, &None, &None).expect("distinct names ok");
assert_eq!(bundles.len(), 3);
for b in &bundles {
assert_eq!(b.provenance, OperationProvenance::FromCall);
}
}
#[test]
fn build_bundles_applies_namespace_prefix_without_collision() {
let conn = CallConnection::new(stub_connection());
let discovered = vec![op_summary("exec", &conn), op_summary("status", &conn)];
let bundles =
build_bundles(discovered, &Some("worker".to_string()), &None).expect("prefixed ok");
assert_eq!(bundles[0].spec.name, "worker/exec");
assert_eq!(bundles[1].spec.name, "worker/status");
}
#[test]
fn build_bundles_respects_operation_filter() {
let conn = CallConnection::new(stub_connection());
let discovered = vec![
op_summary("worker/exec", &conn),
op_summary("worker/status", &conn),
op_summary("fs/readFile", &conn),
];
let filter: HashSet<String> = HashSet::from(["worker/exec".to_string()]);
let bundles = build_bundles(discovered, &None, &Some(filter)).expect("filtered ok");
assert_eq!(bundles.len(), 1);
assert_eq!(bundles[0].spec.name, "worker/exec");
}
#[test]
fn build_bundles_subscription_op_produces_stream_kind() {
let conn = CallConnection::new(stub_connection());
let discovered = vec![op_summary_typed("events/stream", "sub", &conn)];
let bundles = build_bundles(discovered, &None, &None).expect("bundles");
assert_eq!(bundles.len(), 1);
assert_eq!(bundles[0].spec.op_type, OperationType::Sub);
assert!(
matches!(bundles[0].handler, HandlerKind::Stream(_)),
"Sub op must register HandlerKind::Stream"
);
assert_eq!(bundles[0].provenance, OperationProvenance::FromCall);
assert!(bundles[0].composition_authority.is_none());
assert!(bundles[0].scoped_env.is_none());
}
#[test]
fn build_bundles_query_op_produces_once_kind() {
let conn = CallConnection::new(stub_connection());
let discovered = vec![op_summary_typed("fs/readFile", "query", &conn)];
let bundles = build_bundles(discovered, &None, &None).expect("bundles");
assert_eq!(bundles.len(), 1);
assert_eq!(bundles[0].spec.op_type, OperationType::Query);
assert!(
matches!(bundles[0].handler, HandlerKind::Once(_)),
"Query op must register HandlerKind::Once"
);
}
#[test]
fn build_bundles_mutation_op_produces_once_kind() {
let conn = CallConnection::new(stub_connection());
let discovered = vec![op_summary_typed("fs/writeFile", "mutation", &conn)];
let bundles = build_bundles(discovered, &None, &None).expect("bundles");
assert_eq!(bundles.len(), 1);
assert_eq!(bundles[0].spec.op_type, OperationType::Mutation);
assert!(
matches!(bundles[0].handler, HandlerKind::Once(_)),
"Mutation op must register HandlerKind::Once"
);
}
#[test]
fn build_bundles_mixed_op_types_route_to_correct_kind() {
let conn = CallConnection::new(stub_connection());
let discovered = vec![
op_summary_typed("fs/readFile", "query", &conn),
op_summary_typed("fs/writeFile", "mutation", &conn),
op_summary_typed("events/stream", "sub", &conn),
];
let bundles = build_bundles(discovered, &None, &None).expect("bundles");
assert_eq!(bundles.len(), 3);
let by_name: std::collections::HashMap<&str, &HandlerKind> = bundles
.iter()
.map(|b| (b.spec.name.as_str(), &b.handler))
.collect();
assert!(matches!(by_name["fs/readFile"], HandlerKind::Once(_)));
assert!(matches!(by_name["fs/writeFile"], HandlerKind::Once(_)));
assert!(matches!(by_name["events/stream"], HandlerKind::Stream(_)));
}
#[tokio::test]
async fn streaming_forwarding_handler_populates_forwarded_for_and_streams() {
use futures::stream::StreamExt;
let conn = Arc::new(CallConnection::new(stub_connection()));
let captured_payload = Arc::new(StdMutex::new(None::<Value>));
let captured = Arc::clone(&captured_payload);
let handler: StreamingHandler = {
let conn = Arc::clone(&conn);
make_streaming_handler(move |input, context| {
let conn = Arc::clone(&conn);
let captured = Arc::clone(&captured);
let remote_name = "events/stream".to_string();
use futures::stream::{once, StreamExt};
once(async move {
let payload = build_forwarded_payload(&remote_name, input, &context);
*captured.lock().unwrap() = Some(payload.clone());
conn.subscribe_with_payload(payload).await
})
.flatten()
})
};
let ctx = test_context(Some(alice_identity()));
let mut stream = handler(json!({}), ctx);
let first = stream.next().await;
assert!(
first.is_some(),
"streaming forwarding handler must produce at least one envelope"
);
if let Some(env) = first {
assert!(
env.result.is_err(),
"mock connection has no transport, so the stream yields an error envelope"
);
}
let second = stream.next().await;
assert!(
second.is_none(),
"stream must terminate after the error (no truncation, no hang)"
);
let payload = captured_payload.lock().unwrap().clone().expect("captured");
assert_eq!(payload["operationId"], "events/stream");
assert_eq!(payload["forwarded_for"]["id"], "alice");
}
#[tokio::test]
async fn streaming_forwarding_handler_omits_forwarded_for_when_identity_none() {
use futures::stream::StreamExt;
let conn = Arc::new(CallConnection::new(stub_connection()));
let captured_payload = Arc::new(StdMutex::new(None::<Value>));
let captured = Arc::clone(&captured_payload);
let handler: StreamingHandler = {
let conn = Arc::clone(&conn);
make_streaming_handler(move |input, context| {
let conn = Arc::clone(&conn);
let captured = Arc::clone(&captured);
let remote_name = "events/stream".to_string();
use futures::stream::{once, StreamExt};
once(async move {
let payload = build_forwarded_payload(&remote_name, input, &context);
*captured.lock().unwrap() = Some(payload.clone());
conn.subscribe_with_payload(payload).await
})
.flatten()
})
};
let ctx = test_context(None);
let mut stream = handler(json!({}), ctx);
let _ = stream.next().await;
let payload = captured_payload.lock().unwrap().clone().expect("captured");
assert!(
payload.get("forwarded_for").is_none(),
"forwarded_for must be omitted when context.identity is None"
);
assert_eq!(payload["operationId"], "events/stream");
}
#[test]
fn make_streaming_forwarding_handler_returns_streaming_handler() {
let handler = make_streaming_forwarding_handler(
Arc::new(CallConnection::new(stub_connection())),
"events/stream".to_string(),
);
let reg = HandlerRegistration::new(
OperationSpec::new(
"events/stream",
OperationType::Sub,
Visibility::External,
json!({}),
json!({}),
vec![],
AccessControl::default(),
None,
),
HandlerKind::Stream(handler),
OperationProvenance::FromCall,
None,
None,
Capabilities::new(),
);
assert!(matches!(reg.handler, HandlerKind::Stream(_)));
assert_eq!(reg.provenance, OperationProvenance::FromCall);
assert!(reg.composition_authority.is_none());
assert!(reg.scoped_env.is_none());
}
#[test]
fn build_bundles_pub_op_produces_sink_kind() {
let conn = CallConnection::new(stub_connection());
let discovered = vec![op_summary_typed("fs/upload", "pub", &conn)];
let bundles = build_bundles(discovered, &None, &None).expect("bundles");
assert_eq!(bundles.len(), 1);
assert_eq!(bundles[0].spec.op_type, OperationType::Pub);
assert!(
matches!(bundles[0].handler, HandlerKind::Sink(_)),
"Pub op must register HandlerKind::Sink"
);
assert_eq!(bundles[0].provenance, OperationProvenance::FromCall);
assert!(bundles[0].composition_authority.is_none());
assert!(bundles[0].scoped_env.is_none());
}
#[test]
fn build_bundles_mixed_with_pub_routes_to_correct_kind() {
let conn = CallConnection::new(stub_connection());
let discovered = vec![
op_summary_typed("fs/readFile", "query", &conn),
op_summary_typed("fs/upload", "pub", &conn),
op_summary_typed("events/stream", "sub", &conn),
];
let bundles = build_bundles(discovered, &None, &None).expect("bundles");
assert_eq!(bundles.len(), 3);
let by_name: std::collections::HashMap<&str, &HandlerKind> = bundles
.iter()
.map(|b| (b.spec.name.as_str(), &b.handler))
.collect();
assert!(matches!(by_name["fs/readFile"], HandlerKind::Once(_)));
assert!(matches!(by_name["fs/upload"], HandlerKind::Sink(_)));
assert!(matches!(by_name["events/stream"], HandlerKind::Stream(_)));
}
#[tokio::test]
async fn sink_forwarding_handler_populates_forwarded_for() {
use crate::registry::registration::make_sink_handler;
use futures::stream;
let conn = Arc::new(CallConnection::new(stub_connection()));
let captured_payload = Arc::new(StdMutex::new(None::<Value>));
let captured = Arc::clone(&captured_payload);
let handler: SinkHandler = {
let conn = Arc::clone(&conn);
make_sink_handler(move |input, context, publish_stream| {
let conn = Arc::clone(&conn);
let captured = Arc::clone(&captured);
let remote_name = "fs/upload".to_string();
async move {
let payload = build_forwarded_payload(&remote_name, input, &context);
*captured.lock().unwrap() = Some(payload.clone());
let value_stream: Pin<Box<dyn Stream<Item = Value> + Send>> = Box::pin(
publish_stream
.take_while(|item| futures::future::ready(item.is_ok()))
.filter_map(|item| futures::future::ready(item.ok())),
);
conn.publish_with_payload(payload, value_stream).await
}
})
};
let ctx = test_context(Some(alice_identity()));
let chunks: Vec<Result<Value, crate::protocol::wire::CallError>> =
vec![Ok(json!({"chunk": 1})), Ok(json!({"chunk": 2}))];
let publish_stream: PublishStream = Box::pin(stream::iter(chunks));
let response = handler(json!({}), ctx, publish_stream).await;
assert!(
response.result.is_err(),
"mock connection has no transport, so the handler yields an error envelope"
);
let payload = captured_payload.lock().unwrap().clone().expect("captured");
assert_eq!(payload["operationId"], "fs/upload");
assert_eq!(payload["forwarded_for"]["id"], "alice");
}
#[tokio::test]
async fn sink_forwarding_handler_omits_forwarded_for_when_identity_none() {
use crate::registry::registration::make_sink_handler;
use futures::stream;
let conn = Arc::new(CallConnection::new(stub_connection()));
let captured_payload = Arc::new(StdMutex::new(None::<Value>));
let captured = Arc::clone(&captured_payload);
let handler: SinkHandler = {
let conn = Arc::clone(&conn);
make_sink_handler(move |input, context, publish_stream| {
let conn = Arc::clone(&conn);
let captured = Arc::clone(&captured);
let remote_name = "fs/upload".to_string();
async move {
let payload = build_forwarded_payload(&remote_name, input, &context);
*captured.lock().unwrap() = Some(payload.clone());
let value_stream: Pin<Box<dyn Stream<Item = Value> + Send>> = Box::pin(
publish_stream
.take_while(|item| futures::future::ready(item.is_ok()))
.filter_map(|item| futures::future::ready(item.ok())),
);
conn.publish_with_payload(payload, value_stream).await
}
})
};
let ctx = test_context(None);
let chunks: Vec<Result<Value, crate::protocol::wire::CallError>> =
vec![Ok(json!({"c": 1}))];
let publish_stream: PublishStream = Box::pin(stream::iter(chunks));
let _ = handler(json!({}), ctx, publish_stream).await;
let payload = captured_payload.lock().unwrap().clone().expect("captured");
assert!(
payload.get("forwarded_for").is_none(),
"forwarded_for must be omitted when context.identity is None"
);
assert_eq!(payload["operationId"], "fs/upload");
}
#[test]
fn make_sink_forwarding_handler_returns_sink_handler() {
let handler = make_sink_forwarding_handler(
Arc::new(CallConnection::new(stub_connection())),
"fs/upload".to_string(),
);
let reg = HandlerRegistration::new(
OperationSpec::new(
"fs/upload",
OperationType::Pub,
Visibility::External,
json!({}),
json!({}),
vec![],
AccessControl::default(),
None,
),
HandlerKind::Sink(handler),
OperationProvenance::FromCall,
None,
None,
Capabilities::new(),
);
assert!(matches!(reg.handler, HandlerKind::Sink(_)));
assert_eq!(reg.provenance, OperationProvenance::FromCall);
assert!(reg.composition_authority.is_none());
assert!(reg.scoped_env.is_none());
}
}