diff --git a/src/network/iroh_impl.rs b/src/network/iroh_impl.rs index 20ddf04..6c733cb 100644 --- a/src/network/iroh_impl.rs +++ b/src/network/iroh_impl.rs @@ -5,7 +5,7 @@ use bytes::Bytes; use tokio::sync::mpsc; use tokio::sync::mpsc::Receiver; use std::sync::{Arc, Mutex as StdMutex}; -use std::collections::HashMap; +use std::collections::{HashMap, HashSet}; use std::time::Duration; use async_trait::async_trait; @@ -135,6 +135,13 @@ fn is_graceful_leave(err: &ConnectionError) -> bool { matches!(err, ConnectionError::ApplicationClosed(frame) if frame.error_code == VarInt::from_u32(GOODBYE_CODE)) } +/// Pure S8 membership decision: iroh already authenticated `remote` as the +/// connection's endpoint id, so audio admission is exactly live roster membership. +#[allow(dead_code)] // Design-first S8 seam; wiring waits for senior review. +pub(crate) fn audio_sender_admitted(remote: EndpointId, roster: &HashSet) -> bool { + roster.contains(&remote) +} + /// Owns a single peer's connection lifecycle for as long as the peer is in the /// room: obtain a link, run the send/read loops, and on loss obtain a new one — /// with capped backoff on the dialing side. The deterministic-initiator rule @@ -443,3 +450,53 @@ impl NetworkTransport for IrohTransport { .ok_or_else(|| NetError::Other("Connection events already subscribed".to_string())) } } + +#[cfg(test)] +mod tests { + use super::*; + use iroh::SecretKey; + + fn endpoint_id() -> EndpointId { + SecretKey::generate().public() + } + + #[test] + fn audio_sender_admission_accepts_roster_member() { + let member = endpoint_id(); + let roster = HashSet::from([member]); + + assert!(audio_sender_admitted(member, &roster)); + } + + #[test] + fn audio_sender_admission_rejects_unknown_sender() { + let member = endpoint_id(); + let stranger = endpoint_id(); + let roster = HashSet::from([member]); + + assert!(!audio_sender_admitted(stranger, &roster)); + } + + #[test] + fn audio_sender_admission_rejects_former_member_after_roster_removal() { + let former = endpoint_id(); + let mut roster = HashSet::from([former]); + assert!(audio_sender_admitted(former, &roster)); + + roster.remove(&former); + + assert!(!audio_sender_admitted(former, &roster)); + } + + #[test] + fn audio_sender_admission_waits_for_mid_join_announce() { + let joining_peer = endpoint_id(); + let mut roster = HashSet::new(); + + assert!(!audio_sender_admitted(joining_peer, &roster)); + + roster.insert(joining_peer); + + assert!(audio_sender_admitted(joining_peer, &roster)); + } +}