feat: per-session fork registry, connect-side serving loop, op/register (review 004 Units 1-3)

Remediates all six findings of review 004 (per-connection dispatch
resolution and client-side op serving). All claims re-verified in
source before remediation; F-02's member list gains ScopedPeerEnv
(also Clone — fork surface simpler than estimated).

- OperationRegistry: interior mutability (parking_lot RwLock on both
  maps); register takes &self; registration/list_operations return
  owned clones; fork() deep-copies registrations + cached publish-schema
  validators (F-02/F-03); OperationRegistryBuilder::from_registry.
- install_bootstrap_discovery: services/list, services/list-peers,
  services/schema registered closed over the fork itself, so
  per-session openables are discoverable and services/schema answers
  from the fork (F-06).
- Dispatcher::serve_single_stream: full-duplex single-stream loop —
  call.requested dispatches inbound; responded/completed/error resolve
  outbound pendings; aborted tries both tables (in-flight sink aborts
  + pending cascade); published routes inbound sinks (F-04).
- ChannelClient::from_connection_with_serving(connection,
  Option<ServingConfig>): opt-in serving; from_connection keeps the
  pure-consumer default.
- registry::op_register: OpRegisterRequest wire DTO (spec in
  services/schema JSON + replace flag), op_register_spec,
  op_register_handler (rebuild -> forwarding stub -> register_imported,
  forced Internal/FromCall), announce_op; CallError::already_exists;
  spec_to_json_pub; from_call's rebuild_spec_for + forwarding-handler
  constructors crate-shared (F-05).
- ADR-047 §4 amendment #2: per-session fork is the dispatch-registry
  mechanism; overlay stays nested-invocation/peer-announced landing
  zone (F-01/F-02).
- ADR-022 amendment 2026-09-03: bootstrap-op set (services/list,
  services/schema, op/register), opt-in connect-side serving, op/register
  wire shape (F-04/F-05).
- alkhttp ADR-048 reconciliation note + OQ-05 re-pointed at the alkcall
  ADRs (Unit 1b).
- Review 004 status -> remediated; remediation log with gates.

Verification:
- cargo test: 581 passed, 0 failed (565 baseline + 16 new)
- cargo test --all-features: 598 passed, 0 failed
- cargo clippy --all-targets -- -D warnings: clean
- cargo clippy --all-features --all-targets -- -D warnings: clean
- cargo clippy --target wasm32-unknown-unknown -- -D warnings: clean
- cargo fmt --check: clean
- cargo doc --no-deps: clean

Gates: fork_registry_open_op_resolves_and_is_discoverable (open op via
fork + services/list shows openable + services/schema validates),
serving_loop_hub_to_consumer_call_resolves (hub->consumer call through
consumer's serving loop, consumer->hub still resolves),
op_register_announce_then_hub_call_routes_back_to_consumer (announce ->
overlay -> hub call -> forwarding stub -> consumer serves).
This commit is contained in:
glm-5.3-flash committed 2026-09-03 17:32:45 +00:00
1 parent c0dbf82518
commit f84d214173
18 files changed
+2024 -138

No files matched your search

+568 -23
View File
@@ -26,6 +26,7 @@ use tokio::sync::Mutex;
use crate::core::types::{Connection, StreamError};
use crate::protocol::connection::CallConnection;
use crate::protocol::wire::ResponseEnvelope;
use crate::registry::registration::OperationRegistry;
use super::manager::{ChannelManager, ChannelSide};
use super::mux::MuxRunner;
@@ -43,7 +44,20 @@ use super::reassembly::{MpscRecvStream, MpscSendStream};
/// (`channels/<alpn>/sub`, `channels/<alpn>/pub`) on channel 0.
pub struct ChannelClient {
manager: ChannelManager,
call_connection: Arc<Mutex<Option<CallConnection>>>,
call_connection: Arc<Mutex<Option<Arc<CallConnection>>>>,
}
/// Opt-in serving configuration for a `ChannelClient` (review 004
/// F-04). A pure consumer has no registry to serve — `from_connection`
/// keeps the resolution-only read pump. A consumer that also serves
/// ops passes its registry (and identity provider) via
/// [`ServingConfig`]; the read pump becomes the full-duplex serving
/// loop (`Dispatcher::serve_single_stream`), so inbound
/// `call.requested` frames from the peer dispatch and resolve instead
/// of being dropped.
pub struct ServingConfig {
pub registry: Arc<OperationRegistry>,
pub identity_provider: Arc<dyn crate::core::auth::IdentityProvider>,
}
impl ChannelClient {
@@ -60,7 +74,21 @@ impl ChannelClient {
/// the `PendingRequestMap` via `dispatch_envelope`, resolving
/// pending calls. The `CallConnection` holds the framed write
/// half so `call_open_op` can write `call.requested` frames.
pub async fn from_connection(connection: Connection) -> Result<Self, StreamError> {
///
/// Serving (review 004 F-04): with `serving: None` (the default)
/// the read pump resolves responses only — inbound
/// `call.requested` frames are dropped (a pure consumer has no
/// registry to serve). With `Some(ServingConfig { .. })` the read
/// pump is the full-duplex serving loop
/// (`Dispatcher::serve_single_stream`): inbound requests dispatch
/// against the configured registry and resolve back to the peer,
/// and outbound pendings still resolve. This is the opt-in that
/// makes the connect side a serving half on channel 0 (ADR-022 §2
/// — both sides can be both).
pub async fn from_connection_with_serving(
connection: Connection,
serving: Option<ServingConfig>,
) -> Result<Self, StreamError> {
let remote_addr = connection.remote_addr();
let bidi = connection.accept_bi().await?;
let (reader, writer) = tokio::io::split(bidi);
@@ -98,17 +126,39 @@ impl ChannelClient {
let (single_stream_writer, single_stream_reader) =
crate::protocol::connection::split_single_stream(channel0_bidi);
let call_connection =
CallConnection::new_single_stream(channel0_conn, single_stream_writer);
let call_connection = Arc::new(CallConnection::new_single_stream(
channel0_conn,
Arc::clone(&single_stream_writer),
));
let pending_map = Arc::clone(call_connection.pending());
let _read_pump = tokio::spawn(async move {
crate::protocol::connection::read_single_stream_until_closed(
single_stream_reader,
&pending_map,
)
.await;
});
match serving {
None => {
let _read_pump = tokio::spawn(async move {
crate::protocol::connection::read_single_stream_until_closed(
single_stream_reader,
&pending_map,
)
.await;
});
}
Some(config) => {
let dispatcher = crate::protocol::dispatch::Dispatcher::new(
config.registry,
config.identity_provider,
);
let call_conn_for_loop = Arc::clone(&call_connection);
let _serve_loop = tokio::spawn(async move {
dispatcher
.serve_single_stream(
call_conn_for_loop,
single_stream_reader,
single_stream_writer,
)
.await;
});
}
}
// The demux loop ends on transport EOF, clearing the channel
// map (REQ-CH-02). The connect side passes `policy: None` (it
@@ -129,6 +179,13 @@ impl ChannelClient {
})
}
/// Construct from an established `Connection` with serving
/// disabled (the pure-consumer default). See
/// [`ChannelClient::from_connection_with_serving`].
pub async fn from_connection(connection: Connection) -> Result<Self, StreamError> {
Self::from_connection_with_serving(connection, None).await
}
/// The `ChannelManager` — for relay logic and tests.
pub fn manager(&self) -> &ChannelManager {
&self.manager
@@ -193,8 +250,11 @@ impl ChannelClient {
/// Take the `CallConnection` — used by the consumer to register
/// imported ops (`from_call`) on the connection's overlay. After
/// this, `call_open_op` returns an error (the connection is owned
/// by the consumer).
pub async fn take_call_connection(&self) -> Option<CallConnection> {
/// by the consumer). The `Arc` is shared with the serving loop when
/// serving is enabled; taking it detaches the client's own calling
/// surface, not the serving loop's dispatch (the loop holds its own
/// `Arc` clone).
pub async fn take_call_connection(&self) -> Option<Arc<CallConnection>> {
self.call_connection.lock().await.take()
}
}
@@ -329,7 +389,7 @@ mod tests {
/// on the accept side).
#[tokio::test]
async fn channel_0_end_to_end_call_round_trip() {
let mut registry = crate::registry::registration::OperationRegistry::new();
let registry = crate::registry::registration::OperationRegistry::new();
registry
.register(HandlerRegistration::new(
external_query_spec("echo/run"),
@@ -428,7 +488,7 @@ mod tests {
async fn channel_0_end_to_end_publish_delivers_chunks() {
use futures::stream::StreamExt;
let mut registry = crate::registry::registration::OperationRegistry::new();
let registry = crate::registry::registration::OperationRegistry::new();
let counting_sink = crate::registry::registration::make_sink_handler(
|_input, ctx, mut stream| async move {
let mut count = 0u32;
@@ -559,7 +619,7 @@ mod tests {
ResponseEnvelope::ok(ctx.request_id, serde_json::json!({ "ok": oks, "err": err }))
},
);
let mut registry = crate::registry::registration::OperationRegistry::new();
let registry = crate::registry::registration::OperationRegistry::new();
registry
.register(HandlerRegistration::new(
OperationSpec::new(
@@ -667,7 +727,7 @@ mod tests {
ResponseEnvelope::ok(ctx.request_id, serde_json::json!({ "ok": oks, "err": err }))
},
);
let mut registry = crate::registry::registration::OperationRegistry::new();
let registry = crate::registry::registration::OperationRegistry::new();
registry
.register(HandlerRegistration::new(
OperationSpec::new(
@@ -797,7 +857,7 @@ mod tests {
let (writer, reader) = split_single_stream(channel0_bidi);
let core = ChannelCore::new(manager, policy);
let mut registry = crate::registry::registration::OperationRegistry::new();
let registry = crate::registry::registration::OperationRegistry::new();
let spec = OperationSpec::new(
"channels/tty/sub",
OperationType::Sub,
@@ -823,7 +883,7 @@ mod tests {
core.register_openable(
spec,
Arc::clone(&open_handler),
&mut registry,
&registry,
auth.clone(),
)
.expect("register_openable");
@@ -1043,8 +1103,8 @@ mod tests {
});
let manager = ChannelManager::with_defaults(handle, None);
let ops = ChannelOperations::new(manager, Arc::new(NoCap));
let mut registry = crate::registry::registration::OperationRegistry::new();
ops.register_on(&mut registry).expect("register");
let registry = crate::registry::registration::OperationRegistry::new();
ops.register_on(&registry).expect("register");
let handler = registry
.registration("channel/close")
@@ -1117,7 +1177,7 @@ mod tests {
};
let (writer, reader) = split_single_stream(channel0_bidi);
let core = ChannelCore::new(manager, Arc::new(NoCap));
let mut registry = crate::registry::registration::OperationRegistry::new();
let registry = crate::registry::registration::OperationRegistry::new();
let spec = OperationSpec::new(
"channels/tty/sub",
OperationType::Sub,
@@ -1139,7 +1199,7 @@ mod tests {
core.register_openable(
spec,
Arc::clone(&open_handler),
&mut registry,
&registry,
auth.clone(),
)
.expect("register_openable");
@@ -1195,4 +1255,489 @@ mod tests {
"handler received ping through adopted channel"
);
}
// --- review 004 Unit 3 acceptance gates (F-04 serving half) -----------
/// F-04 gate 1: hub→consumer call over an existing `ChannelClient`
/// session resolves. The consumer (connect side) dials with
/// `Some(ServingConfig { .. })`; the accept side's dispatcher later
/// calls an op the consumer serves (by opening a fresh stream from
/// its side is not available on channel 0 — the accept side writes
/// a `call.requested` through its channel-0 writer, and the
/// consumer's serving loop dispatches it and writes the response,
/// which resolves on the accept side's pending).
///
/// Mechanically: the accept side's `install_channel_zero` hook
/// returns a clone of the channel-0 `CallConnection` back to the
/// test via a channel; the test then calls
/// `call_open_op("consumer/echo")` — wait, that calls *through* the
/// accept side's registry. The consumer-serving direction needs the
/// accept side to *initiate*: it writes `call.requested` for
/// `consumer/echo` on channel 0. The client's serving loop (with
/// the `consumer/echo` op in its registry) dispatches it and writes
/// the response, resolving the accept side's pending.
#[tokio::test]
async fn serving_loop_hub_to_consumer_call_resolves() {
// Consumer-side (connect side) registry: serves `consumer/echo`.
let consumer_registry = crate::registry::registration::OperationRegistry::new();
consumer_registry
.register(HandlerRegistration::new(
external_query_spec("consumer/echo"),
HandlerKind::Once(make_handler(|input, ctx| async move {
ResponseEnvelope::ok(ctx.request_id, input)
})),
OperationProvenance::Local,
None,
None,
crate::core::types::Capabilities::new(),
))
.unwrap();
let consumer_registry = Arc::new(consumer_registry);
// Accept side: a plain echo registry plus a handle back to the
// accept side's channel-0 `CallConnection` so the test can
// initiate a call *from* the accept side.
let (accept_conn_tx, mut accept_conn_rx) =
tokio::sync::mpsc::channel::<Arc<CallConnection>>(1);
let accept_registry = crate::registry::registration::OperationRegistry::new();
accept_registry
.register(HandlerRegistration::new(
external_query_spec("accept/echo"),
HandlerKind::Once(make_handler(|input, ctx| async move {
ResponseEnvelope::ok(ctx.request_id, input)
})),
OperationProvenance::Local,
None,
None,
crate::core::types::Capabilities::new(),
))
.unwrap();
let accept_registry = Arc::new(accept_registry);
let install_hook: crate::channels::adapter::InstallChannelZero =
Arc::new(move |manager, channel0_conn, _auth| {
let accept_registry = Arc::clone(&accept_registry);
let accept_conn_tx = accept_conn_tx.clone();
tokio::spawn(async move {
let _ = manager;
let channel0_bidi = match channel0_conn.accept_bi().await {
Ok(s) => s,
Err(_) => return,
};
let (writer, reader) = split_single_stream(channel0_bidi);
let call_connection = Arc::new(CallConnection::new_single_stream(
channel0_conn,
Arc::clone(&writer),
));
let _ = accept_conn_tx.send(Arc::clone(&call_connection)).await;
let dp = Dispatcher::new(
accept_registry,
Arc::new(NoopIdProvider) as Arc<dyn IdentityProvider>,
);
// The serving loop on the accept side too: the
// consumer may call back over the same session.
dp.serve_single_stream(call_connection, reader, writer)
.await;
})
});
let (client_end, server_end) = tokio::io::duplex(64 * 1024);
let client_conn =
Connection::from_bidi(client_end, b"alk/channels".to_vec(), Some(TEST_ADDR));
let server_conn =
Connection::from_bidi(server_end, b"alk/channels".to_vec(), Some(TEST_ADDR));
let adapter = ChannelsAdapter::new(install_hook, Arc::new(NoCap));
let auth = AuthContext::anonymous(b"alk/channels");
let _server_handle = tokio::spawn(async move {
let _ = crate::core::types::ProtocolHandler::handle(&adapter, server_conn, &auth).await;
});
let client = ChannelClient::from_connection_with_serving(
client_conn,
Some(ServingConfig {
registry: Arc::clone(&consumer_registry),
identity_provider: Arc::new(NoopIdProvider),
}),
)
.await
.expect("channel client init");
let accept_conn = accept_conn_rx
.recv()
.await
.expect("accept side channel-0 connection handle");
// Accept side (hub) initiates a call to an op the consumer
// serves — the exact direction F-04 says was dropped.
let response = tokio::time::timeout(
std::time::Duration::from_secs(5),
accept_conn.call("consumer/echo", serde_json::json!({ "from": "hub" })),
)
.await
.expect("hub→consumer call timed out");
assert!(
response.result.is_ok(),
"hub→consumer call resolves through the consumer's serving loop, got {:?}",
response.result
);
assert_eq!(
response.result.unwrap(),
serde_json::json!({ "from": "hub" })
);
// The consumer's own calling half still works (outbound pendings
// resolve in the same loop).
let outbound = client
.call_open_op("accept/echo", serde_json::json!({ "to": "hub" }))
.await;
assert!(
outbound.result.is_ok(),
"consumer→hub call still resolves, got {:?}",
outbound.result
);
}
// --- review 004 Unit 3 acceptance gates (F-05 op/register) ------------
/// F-05 gate: the consumer announces an op over channel 0
/// (`op/register` served on the accept side's fork), the hub
/// registers it in the connection overlay, and the hub then calls
/// it back — the forwarding stub issues a nested `call.requested`
/// over channel 0 to the consumer, whose serving loop dispatches
/// the real handler. Peer-announced op, discoverable and callable,
/// end-to-end.
#[tokio::test]
async fn op_register_announce_then_hub_call_routes_back_to_consumer() {
// Consumer side: serves `consumer/exec` locally; announces it.
let consumer_registry = crate::registry::registration::OperationRegistry::new();
consumer_registry
.register(HandlerRegistration::new(
external_query_spec("consumer/exec"),
HandlerKind::Once(make_handler(|input, ctx| async move {
ResponseEnvelope::ok(
ctx.request_id,
serde_json::json!({ "ran_on": "consumer", "input": input }),
)
})),
OperationProvenance::Local,
None,
None,
crate::core::types::Capabilities::new(),
))
.unwrap();
let consumer_registry = Arc::new(consumer_registry);
// Accept side: serves the `op/register` bootstrap op composed on
// a fork (the F-02(a) shape — the fork is fresh here, so the
// bootstrap set is just `op/register`).
let (accept_conn_tx, mut accept_conn_rx) =
tokio::sync::mpsc::channel::<Arc<CallConnection>>(1);
let accept_registry_for_hook =
Arc::new(crate::registry::registration::OperationRegistry::new());
let accept_tx_for_hook = accept_conn_tx.clone();
let install_hook: crate::channels::adapter::InstallChannelZero =
Arc::new(move |_manager, channel0_conn, _auth| {
let accept_registry = Arc::clone(&accept_registry_for_hook);
let accept_conn_tx = accept_tx_for_hook.clone();
tokio::spawn(async move {
let channel0_bidi = match channel0_conn.accept_bi().await {
Ok(s) => s,
Err(_) => return,
};
let (writer, reader) = split_single_stream(channel0_bidi);
let call_connection = Arc::new(CallConnection::new_single_stream(
channel0_conn,
Arc::clone(&writer),
));
let _ = accept_conn_tx.send(Arc::clone(&call_connection)).await;
let reg = accept_registry.fork();
reg.register(HandlerRegistration::new(
crate::registry::op_register::op_register_spec(AccessControl::default()),
HandlerKind::Once(crate::registry::op_register::op_register_handler(
Arc::clone(&call_connection),
)),
OperationProvenance::Local,
None,
None,
crate::core::types::Capabilities::new(),
))
.unwrap();
let dp = Dispatcher::new(
Arc::new(reg),
Arc::new(NoopIdProvider) as Arc<dyn IdentityProvider>,
);
dp.serve_single_stream(call_connection, reader, writer)
.await;
})
});
let (client_end, server_end) = tokio::io::duplex(64 * 1024);
let client_conn =
Connection::from_bidi(client_end, b"alk/channels".to_vec(), Some(TEST_ADDR));
let server_conn =
Connection::from_bidi(server_end, b"alk/channels".to_vec(), Some(TEST_ADDR));
let adapter = ChannelsAdapter::new(install_hook, Arc::new(NoCap));
let auth = AuthContext::anonymous(b"alk/channels");
let _server_handle = tokio::spawn(async move {
let _ = crate::core::types::ProtocolHandler::handle(&adapter, server_conn, &auth).await;
});
let client = ChannelClient::from_connection_with_serving(
client_conn,
Some(ServingConfig {
registry: Arc::clone(&consumer_registry),
identity_provider: Arc::new(NoopIdProvider),
}),
)
.await
.expect("channel client init");
let accept_conn = accept_conn_rx
.recv()
.await
.expect("accept side channel-0 connection handle");
// Consumer announces `consumer/exec` over channel 0.
let announce = tokio::time::timeout(
std::time::Duration::from_secs(10),
client.call_open_op(
crate::registry::op_register::OP_REGISTER_NAME,
crate::registry::op_register::OpRegisterRequest {
spec: external_query_spec("consumer/exec"),
replace: false,
}
.to_json(),
),
)
.await
.expect("op/register announce timed out");
assert!(
announce.result.is_ok(),
"announce ok, got {:?}",
announce.result
);
// The hub's overlay now holds the announced op; the hub calls
// it — the forwarding stub routes the nested call back over
// channel 0 to the consumer's serving loop.
let response = tokio::time::timeout(
std::time::Duration::from_secs(10),
accept_conn.call("consumer/exec", serde_json::json!({ "task": "greet" })),
)
.await
.expect("hub call to peer-announced op timed out");
assert!(
response.result.is_ok(),
"hub→peer-announced-op call resolves, got {:?}",
response.result
);
assert_eq!(
response.result.unwrap(),
serde_json::json!({ "ran_on": "consumer", "input": { "task": "greet" } })
);
}
// --- review 004 Unit 2 acceptance gate (F-02/F-06 fork) ----------------
/// F-02/F-06 gate: the open op is registered on a **fork** of the
/// base registry (base carries a placeholder op; the fork adds the
/// openable + bootstrap discovery via
/// `install_bootstrap_discovery`), the session dispatches over the
/// fork, and — after the open op resolves over the live channels
/// connection — the per-session openable is discoverable through
/// `services/list` **on the fork** (the F-06 self-referential
/// closure) and `services/schema` on the fork still validates.
#[tokio::test]
async fn fork_registry_open_op_resolves_and_is_discoverable() {
use crate::channels::operations::{ChannelCore, OpenHandler};
use crate::channels::policy::PerIdentityChannelPolicy;
use crate::registry::spec::ChannelOpenSpec;
// Base registry: a plain op + bootstrap discovery closed over
// the *base* (which will NOT see per-session openables — that
// is the F-06 finding; the fork's own discovery does).
let base = crate::registry::registration::OperationRegistry::new();
base.register(HandlerRegistration::new(
external_query_spec("base/ping"),
HandlerKind::Once(make_handler(|input, ctx| async move {
ResponseEnvelope::ok(ctx.request_id, input)
})),
OperationProvenance::Local,
None,
None,
crate::core::types::Capabilities::new(),
))
.unwrap();
let policy: Arc<PerIdentityChannelPolicy> = Arc::new(PerIdentityChannelPolicy::new(256));
let policy_for_hook: Arc<dyn ChannelLifecyclePolicy> =
Arc::clone(&policy) as Arc<dyn ChannelLifecyclePolicy>;
let open_handler: OpenHandler = Arc::new(|_input, _channel_conn, _auth| {
tokio::spawn(async move {
tokio::time::sleep(std::time::Duration::from_millis(50)).await;
})
});
let install_hook: crate::channels::adapter::InstallChannelZero =
Arc::new(move |manager, channel0_conn, auth| {
let open_handler = Arc::clone(&open_handler);
let policy = Arc::clone(&policy_for_hook);
let base = Arc::new(crate::registry::registration::OperationRegistry::new());
base.register(HandlerRegistration::new(
external_query_spec("base/ping"),
HandlerKind::Once(make_handler(|input, ctx| async move {
ResponseEnvelope::ok(ctx.request_id, input)
})),
OperationProvenance::Local,
None,
None,
crate::core::types::Capabilities::new(),
))
.unwrap();
let base = Arc::clone(&base);
tokio::spawn(async move {
let channel0_bidi = match channel0_conn.accept_bi().await {
Ok(s) => s,
Err(_) => return,
};
let (writer, reader) = split_single_stream(channel0_bidi);
let core = ChannelCore::new(manager, policy);
// The F-02(a) composition: fork the base, register
// the openable on the fork, install bootstrap
// discovery on the fork (F-06 — closed over the
// fork), dispatch over the fork.
let fork = base.fork();
core.register_openable(
OperationSpec::new(
"channels/tty/sub",
OperationType::Sub,
Visibility::External,
serde_json::json!({
"type": "object",
"properties": {
"container": { "type": "string" }
},
"required": ["container"]
}),
serde_json::json!({
"type": "object",
"properties": {
"channel_id": { "type": "integer" }
}
}),
vec![],
AccessControl::default(),
None,
)
.with_channel_open(ChannelOpenSpec::new("alk/tty")),
Arc::clone(&open_handler),
&fork,
auth.clone(),
)
.expect("register_openable on fork");
let fork = Arc::new(fork);
crate::registry::discovery::install_bootstrap_discovery(&fork)
.expect("bootstrap discovery on fork");
let provider: Arc<dyn IdentityProvider> = Arc::new(NoopIdProvider);
let call_connection = Arc::new(CallConnection::new_single_stream(
channel0_conn,
Arc::clone(&writer),
));
let dp = Dispatcher::new(fork, provider);
dp.run_loop_single_stream(call_connection, reader, writer)
.await;
})
});
let (client_end, server_end) = tokio::io::duplex(64 * 1024);
let client_conn =
Connection::from_bidi(client_end, b"alk/channels".to_vec(), Some(TEST_ADDR));
let server_conn =
Connection::from_bidi(server_end, b"alk/channels".to_vec(), Some(TEST_ADDR));
let adapter = ChannelsAdapter::new(install_hook, Arc::new(NoCap));
let auth = AuthContext::anonymous(b"alk/channels");
let _server_handle = tokio::spawn(async move {
let _ = crate::core::types::ProtocolHandler::handle(&adapter, server_conn, &auth).await;
});
let client = ChannelClient::from_connection(client_conn)
.await
.expect("channel client init");
// 1. The open op resolves over the live connection through the fork.
let response = tokio::time::timeout(
std::time::Duration::from_secs(10),
client.call_open_op(
"channels/tty/sub",
serde_json::json!({ "container": "abc" }),
),
)
.await
.expect("open op timed out");
assert!(
response.result.is_ok(),
"open op resolves through the fork, got {:?}",
response.result
);
let channel_id = response
.result
.unwrap()
.get("channel_id")
.and_then(|v| v.as_u64())
.expect("channel_id in response");
assert!(channel_id > 0);
// 2. The per-session openable is discoverable via services/list
// (F-06) — the fork's own discovery closure sees the fork's ops.
let listing = tokio::time::timeout(
std::time::Duration::from_secs(10),
client.call_open_op("services/list", serde_json::json!({})),
)
.await
.expect("services/list timed out");
assert!(
listing.result.is_ok(),
"services/list resolves, got {:?}",
listing.result
);
let names: Vec<String> = listing
.result
.unwrap()
.get("operations")
.and_then(|v| v.as_array())
.expect("operations array")
.iter()
.filter_map(|o| o.get("name").and_then(|n| n.as_str().map(String::from)))
.collect();
assert!(
names.contains(&"channels/tty/sub".to_string()),
"per-session openable discoverable through the fork's discovery: {names:?}"
);
// 3. services/schema on the fork still validates input (the
// bootstrap op is on the fork and answers).
let schema = tokio::time::timeout(
std::time::Duration::from_secs(10),
client.call_open_op(
"services/schema",
serde_json::json!({ "name": "channels/tty/sub" }),
),
)
.await
.expect("services/schema timed out");
assert!(
schema.result.is_ok(),
"services/schema resolves on the fork, got {:?}",
schema.result
);
}
}
+14 -14
View File
@@ -62,7 +62,7 @@ impl ChannelOperations {
/// Register the three generic ops on the call `OperationRegistry`.
/// The per-ALPN open ops are registered separately by the ALPN
/// crates via [`ChannelCore::register_openable`] (ADR-047 §3).
pub fn register_on(&self, registry: &mut OperationRegistry) -> Result<(), String> {
pub fn register_on(&self, registry: &OperationRegistry) -> Result<(), String> {
let manager = self.manager.clone();
let policy = Arc::clone(&self.policy);
registry.register(HandlerRegistration::new(
@@ -400,7 +400,7 @@ impl ChannelCore {
&self,
spec: OperationSpec,
open_handler: OpenHandler,
registry: &mut OperationRegistry,
registry: &OperationRegistry,
auth: AuthContext,
) -> Result<(), String> {
let op_type = spec.op_type;
@@ -765,8 +765,8 @@ mod tests {
async fn register_on_registers_three_ops() {
let manager = make_manager().await;
let ops = ChannelOperations::with_default_policy(manager);
let mut registry = OperationRegistry::new();
ops.register_on(&mut registry).expect("register");
let registry = OperationRegistry::new();
ops.register_on(&registry).expect("register");
assert!(registry.registration(OP_CHANNEL_CLOSE).is_some());
assert!(registry.registration(OP_CHANNEL_CONTROL).is_some());
assert!(registry
@@ -899,8 +899,8 @@ mod tests {
let manager = make_manager().await;
let policy = super::super::policy::default_policy();
let ops = ChannelOperations::new(manager, policy);
let mut registry = OperationRegistry::new();
ops.register_on(&mut registry).expect("register");
let registry = OperationRegistry::new();
ops.register_on(&registry).expect("register");
assert!(registry.registration(OP_CHANNEL_CLOSE).is_some());
}
@@ -1129,11 +1129,11 @@ mod tests {
spawned_clone.store(true, Ordering::SeqCst);
tokio::spawn(async {})
});
let mut registry = OperationRegistry::new();
let registry = OperationRegistry::new();
core.register_openable(
spec,
open_handler,
&mut registry,
&registry,
AuthContext::anonymous(b"alk/call"),
)
.expect("register");
@@ -1172,11 +1172,11 @@ mod tests {
spawned_clone.store(true, Ordering::SeqCst);
tokio::spawn(async {})
});
let mut registry = OperationRegistry::new();
let registry = OperationRegistry::new();
core.register_openable(
spec,
open_handler,
&mut registry,
&registry,
AuthContext::anonymous(b"alk/call"),
)
.expect("register");
@@ -1209,11 +1209,11 @@ mod tests {
)
.with_channel_open(ChannelOpenSpec::new("alk/tty"));
let open_handler: OpenHandler = Arc::new(|_input, _conn, _auth| tokio::spawn(async {}));
let mut registry = OperationRegistry::new();
let registry = OperationRegistry::new();
core.register_openable(
spec,
open_handler,
&mut registry,
&registry,
AuthContext::anonymous(b"alk/call"),
)
.expect("register");
@@ -1239,11 +1239,11 @@ mod tests {
None,
);
let open_handler: OpenHandler = Arc::new(|_input, _conn, _auth| tokio::spawn(async {}));
let mut registry = OperationRegistry::new();
let registry = OperationRegistry::new();
let result = core.register_openable(
spec,
open_handler,
&mut registry,
&registry,
AuthContext::anonymous(b"alk/call"),
);
assert!(result.is_err());