slice-3 §7.4: external golden JSON fixtures + a new integration-test
file locking the exact on-wire bytes produced by each slice-3 encode_*
function. The inline round-trip tests in protocol.rs use String::contains
(substring) which cannot catch field-order drift, stray fields, or number
formatting changes — all of which the Envelope Serialize impl promises to
keep stable (protocol.rs:115-118: "field order is pinned to make the wire
bytes deterministic"). These fixtures make that promise load-bearing.
5 fixtures cover the 5 new event kinds (spec §3.2-3.3):
speech_started, speech_stopped, function_call,
function_call_output, tools.update.
Each fixture asserts (a) encode_* output equals the fixture bytes exactly
(string equality, not serde_json::Value equality — the latter would
erase the field-order pinning the serializer guarantees), and (b) the
fixture decodes back to the expected DecodedPayload variant with the
expected fields.
Plus a forwards-compat re-assertion: unknown wire type -> DecodedPayload::Unknown
(via FrameKind's #[serde(other)]), not an error — the foundation of the
slice-2 §3.4 "drop + observe" posture that lets a future-protocol brain
not crash an older core.
On first run the golden test caught real drift: serde_json (without
preserve_order) alphabetizes nested object keys, so tools.update's tools
array serializes description-before-name, not input order. Fixture
corrected to lock the actual contract.
11 new tests, all green. cargo fmt + clippy -D warnings clean.
slice-3 §5.2 + §6: the binary's poll task now drains the brain's
function_call proposals from rx_function_call, dispatches through the
per-channel ToolRegistry (HangupTool wired at spawn_tap_engine time),
and writes function_call_output replies back through tx_function_call_output
which run_tap_client forwards as tap WS frames to the brain.
TapClient: handle_brain_frame now forwards function_call events to a
new tx_function_call mpsc side-channel instead of dropping them.
run_tap_client adds a select! arm draining rx_function_call_output +
sending each as a tap frame. Advisory events (speech_started/stopped,
tools.update) still log + count (slice-3 deferred-action posture).
TapEngine: spawn_tap_engine now takes AppState + constructs a per-channel
ToolRegistry (spec §6.2) with HangupTool pre-registered (§6.3). TapConn
gains rx_function_call, tx_function_call_output, tool_registry fields.
session_map: drive_all_sessions calls drain_function_calls in the same
cycle as the slice-2 §5.3 step 4 flush drain (one extra channel, same
cycle); the helper spawns each dispatch as its own task so the 750 ms
hangup teardown bound (AppState::close) can't stall the 10 ms poll
cadence.
files touched: crates/rutster-tap/src/{lib,tap_client}.rs,
crates/rutster/src/{session_map,tap_engine}.rs,
crates/rutster/tests/tap_integration.rs ( AppState arg ),
crates/rutster-brain-realtime/src/translator.rs (clippy needless_borrow ).
NOT touched: loop_driver.rs, rtc_session.rs (seam test §7.5 #6).
gates: cargo fmt --check OK. cargo clippy --all --tests -D warnings OK.
cargo test --all OK. cargo deny check has pre-existing environmental
failure (CVSS 4.0 unsupported in advisory-db; same on main).
Five new event types in slice-2's v1 protocol. Forwards-compatible
per slice-2 §3.4: FrameKind's #[serde(other)] Unknown absorbs the
new types in old brains (they log + count + drop). No wire-format
break, no version bump.
New kid on the dispatch: tools.update — brain declares its catalog
on hello so the core's tool registry can validate function_call
events by name (Task 6). The 'function_call'/'function_call_output'
pair is the FOB-boundary dispatch contract the brain's translator
(Task 4) wires to OpenAI Realtime's
response.function_call_arguments.done / conversation.item.create.
TDD: 6 tests fail-red on missing impl, pass-green after this task.
Slice-2's existing protocol tests stay green — the additions are
purely additive.
Code-review findings from the slice-2 final whole-branch review:
- tap_client.rs: close arm sends session_end (not bye) per §5.2, awaits
brain bye with bounded 500ms timeout, then ws.close(). Promotes the
Task-7-documented Minor to Important's required spec compliance.
- session_map.rs: AppState::close() now clears channel.tap = None before
Closing->Closed, sends close signal, bounded-awaits engine task for the
teardown handshake (500ms cap), aborts as fallback. Per §5.1 step 5.
- pcm.rs + tap_audio_pipe.rs: AudioPipe trait gains clear_playout_ring()
(default no-op for EchoAudioPipe); TapAudioPipe clears the VecDeque.
RtcSession::clear_playout_ring delegates. Per §5.3 step 4 playout-on-
disconnect-flush contract.
- tap_engine.rs: spawn_tap_engine creates + shares a flush_tx mpsc
channel; run_engine_loop signals flush after each failed pump loop
(before backoff); TapConn.flush_rx is drained by drive_all_sessions
to call RtcSession::clear_playout_ring.
- tests/tap_integration.rs: new reconnect-path test asserts Channel
stays Connected, metrics.reconnect_attempts increments, playout
returns None during outage, playout resumes after EchoServer
restart with fresh frames (no stale bleed-through). Per §8.4.
- static/index.html: one static line acknowledging external-brain
routing per §7.2 (defer real-time tap status to step-3 GUI).
Spec ref: 2026-06-28-slice-2-agent-tap-design.md §5.1 step 5, §5.2, §5.3 step 4, §7.2, §8.4.
- run_tap_client: drives a connected WebSocketStream with a tokio::select!
over rx_pcm_in (inbound PCM → audio_in WS frame) and ws.next() (brain
frame → audio_out mpsc or control handling).
- Handshake: send hello, await brain hello (bounded 2s timeout).
- seq gap detection (log + count, never drop on gap — spec §3.1).
- Hot-path errors (encode/decode failures, send/recv failures) are logged
+ counted; TapClientError is returned only on graceful close, hello
timeout, or WS errors. The TapEngine (Task 7) decides reconnect policy.
- Pure-helper unit test (elapsed_ms); full pump behavior is exercised
against the in-process EchoServer in the Task 8 integration test.
Spec ref: 2026-06-28-slice-2-agent-tap-design.md §4.2.
- Add tokio-tungstenite 0.24, futures-util 0.3, url 2, base64 0.22 to
[workspace.dependencies] (spec §8.1).
- Add crates/rutster-tap-echo as 7th workspace member (spec §2).
- Transition crates/rutster-tap/Cargo.toml from stub to real manifest
with the deps Tasks 2-4 will need (rutster-media, tokio, tokio-tungstenite,
serde, serde_json, base64, url, thiserror, tracing).
- Skeleton lib.rs (compile test) + main.rs (placeholder fn main) for
rutster-tap-echo; Task 5 fills in the echo logic.
- Verify cargo deny check passes against the new transitive dep tree.
Spec ref: docs/superpowers/specs/2026-06-28-slice-2-agent-tap-design.md §2, §8.1.
Workspace root, pinned toolchain, and the three stub crates whose only
job in slice 1 is to lock the ADR-0002 boundary shape. Each ships a
lib.rs module doc (what it will hold, why deferred, which spearhead step
fills it) and a crate_compiles test. Spec §2.2.