--- id: ops-call-harness name: the in-process alkcall harness — the serving assembly and the client side over one duplex pair status: completed depends_on: [ops-foundation] scope: narrow risk: low impact: component level: implementation tags: [wave-7, ops, testing, harness, alkcall] --- ## Description The test harness every ops handler task exercises its handlers through: an in-process alkcall **serving side + client side over one `tokio::io::duplex` pair** — the embedder's "dials and hands an established `Connection` in" shape (ops-surface.md §Why alkcall; alkcall is a pure protocol crate), realized in-process so the ops surface is testable server-less. - **The serving assembly** (the embedder-assembly pattern realized in test): `OperationRegistryBuilder` receiving the ops family's spec/handler pairs → the dispatch serving stack (`CallAdapter` / `GatewayDispatch`, plus `ChannelsAdapter` for the binary ops' `channel_open` path) → the dispatch run loop over a `Connection::from_bidi` half (alkcall ADR-009's public stream constructor). The assembly's shape is the module's documented embedder entry — the "module exports spec/handler pairs; the embedder assembles" placement (ops-surface.md's placement note, the alkgit `git/repo/*` pattern). - **The client side**: `CallClient` + `spawn_dispatch` over the duplex pair's other half; `ChannelClient` (`from_connection`, `open_channel`/`call_open_op`) for the channel ops. - **The identity/ACL wiring**: a test `IdentityProvider` + `OwnershipProvider` — the embedder-registry stand-in (namespace → identities → granted actions) — so ACL behavior is exercisable from the first handler task onward, including the closed-by-default denial path (OQ-09). - **Proven with a stub op**: a trivial stub op registered through the same path round-trips (call → response envelope), and the channel path opens with bytes flowing — the pipe proven before any real op rides it. The stub lives in the test tree; it registers nothing into the shipped module surface. ## Acceptance Criteria - [x] The harness lives in the ops module's test tree (`#[cfg(test)]` under feature `ops`), parameterized over the assembled registry (ops register; the harness serves) - [x] Server + client halves over one duplex pair; the dispatch run loop serves; the client calls and receives envelopes - [x] The identity/ownership wiring exercisable: a stub op carrying an `AccessControl` denies an ungranted identity and admits a granted one (the ACL path proven end-to-end pre-handler) - [x] The channel path proven: a stub `channel_open` op establishes (the `Establishment` phase) and bytes flow both directions - [x] `cargo test --features ops` green; the default build unaffected (the harness is feature-gated test code); `cargo clippy --all-targets -- -D warnings` and `cargo fmt --check` green on both shapes ## References - docs/architecture/ops-surface.md (§Why alkcall — the Connection hand-in; the placement note) - docs/architecture/decisions/001-substrate-posture-and-ops-placement.md §2 (the machinery inventory this harness assembles) - alkcall (upstream checkout `/workspace/@alkdev/alkcall`): `src/registry/registration.rs` (`OperationRegistryBuilder`, the handler kinds), `src/protocol/adapter.rs` (`CallAdapter`), `src/protocol/connection.rs` (`Connection::from_bidi`), `src/channels/adapter.rs` (`ChannelsAdapter`), `src/channels/client.rs` (`ChannelClient`), ADR-047/049 (the channel_open marker + establishment) - tasks/ops-foundation.md (the context the harness's assembled handlers capture) ## Notes > Landed as a `#[cfg(test)] mod harness` appended to > `src/ops.rs` (the ops module's test tree — feature-gated by > construction, the default build compiles it out; the shipped module > surface is untouched). All wiring is alkcall's public API: > > - **The loopback connection source** (`LoopbackSource` + `Connection::from_source`, > the ADR-008 extension point): the QUIC-shaped in-memory stand-in the > top-level `alk/call` shape needs — `open_bi` hands out pre-wired > stream pairs (one `tokio::io::duplex` pair per stream, joined via > the public `BiStream::from_joined`), `open_bi` at exhaustion answers > `StreamClosed`, `accept_bi` at idle parks (an open connection with > no inbound streams), `close` drains. This was the one place the > task text needed a ruling: **`spawn_dispatch` + stream-per-request > cannot complete a call over a single `from_bidi` duplex end** > (the accept loop consumes the one stream and `call()`'s `open_bi` > then fails; alkcall's own e2e shape is the same pre-wired > `BidiStreamSource` stand-in, `pub(crate)` there — reimplemented > here as the documented downstream seam, N streams wide so the > client can call repeatedly). The channel shape uses > `Connection::from_bidi` halves directly (the channels protocol > multiplexes internally). > - **The serving assembly**: `OperationRegistryBuilder` receiving the > stub ops family's spec/handler pairs (`with_local`) → > `CallAdapter::new(...).with_ownership_provider(...)` → > `ProtocolHandler::handle` (the dispatch run loop) over its > `from_source` half; client side > `CallClient::new(...).spawn_dispatch(...)` over the other. > `GatewayDispatch` (alkcall's `gateway`-gated transport-neutral > spine) was not wired — no gateway front-end rides the duplex; the > wire-serving stack is the `CallAdapter` half. > - **The channel assembly**: `ChannelsAdapter::new(install_hook,\ NoCap)` > — the install hook (the ADR-036 amendment shape): sets the > transport-authenticated identity on channel 0's `Connection` > (`Connection::set_identity`, the CF-005 (b) path), forks the > assembled registry (ADR-047 §4's per-connection registration), and > registers the stub openables through > `ChannelCore::register_openable_with_establisher` — the awaited > **Establishment phase** (establisher records its invocation, > admits `stub-ns` only, returns `Establishment::default()`; refusal > → `channel:open_failed`/`unknown_resource`) — then > `Dispatcher::run_loop_single_stream` over > `CallConnection::new_single_stream`. Client side: > `ChannelClient::from_connection`, `open_channel` for the binary > data plane, `call_open_op` for the refusal arms. > - **The identity/ACL wiring** (`TestIdentityProvider` — token → > identity — and `TestOwnership` — namespace → identities → granted > actions, closed by default): the call shape exercises the > `auth_token` → `IdentityProvider` route (payload `auth_token` via > `call_with_payload`, alkcall ADR-017 §7's precedence); the channel > shape exercises the transport-identity route (identity set on > channel 0's connection in the install hook). Both resolve through > the dispatch's `AccessControl::check` with the ownership provider > in context — the ops surface's namespace-as-resource mapping > (`blob-namespace`, action `read`, `resource_id_path: "$.namespace"`), > never a re-implemented check (convention 6). > - **The stubs**: `blobs/stub-echo` (no ACL, echo), `blobs/stub-ns-probe` > (the ACL op above), `channels/stub/sub` (the channel_open op, over > the `alk/stub` data-plane ALPN, whose pump reads one chunk, records > it, and echoes `b"pong"` — the R-02 duty in one act). All live in > the test tree; nothing registers into the shipped surface. > > One dev-dependency entered: `async-trait = "0.1"` (the > `BidiStreamSource` impl's annotation; cargo dedupes to alkcall's > version). The lib dependency tree is unchanged. ## Summary > Completed 2026-10-13 (wave 7). The in-process alkcall harness landed in > `src/ops.rs`'s test tree: two wire shapes over in-memory connections — > the call shape (stub JSON ops registered through > `OperationRegistryBuilder`; serving side `CallAdapter` with the > identity + ownership providers wired; client side `CallClient` + > `spawn_dispatch` over the loopback pair's stream-per-request source) > and the channel shape (stub openable registered per-connection through > `ChannelCore::register_openable_with_establisher` — the awaited > Establishment phase; serving side `ChannelsAdapter`'s install hook > forking the assembled registry and running `run_loop_single_stream`; > client side `ChannelClient::from_connection` + `open_channel`). > Proven by the stub ops: the JSON stub round-trips call → response > envelope; the ACL stub (namespace = `blob-namespace` resource) denies > anonymous and ungranted identities (closed-by-default, OQ-09) and > admits the granted one; the channel stub establishes and bytes flow > both directions (`ping`/`pong`); the refusals arms prove the ACL gate > runs before the establisher and that an establisher refusal answers > `channel:open_failed`/`unknown_resource` through the typed > `ChannelOpenError`. Four harness tests green (lib 88 with `--features > ops`, 73 default; all conformance/interop columns unchanged). Gates > green: `cargo test --features ops`, `cargo test` (default, harness > compiled out), `cargo clippy --all-targets -- -D warnings` on both > shapes, `cargo fmt --check`, `cargo doc --no-deps` clean. The > handler tasks (ops-json-ops, ops-fetch, ops-put) now register their > spec/handler pairs through `OpsHarness` instead of inventing wiring.