Skip to main content

whatsapp_rust/client/
voip.rs

1//! Call-control accessor. Reject/terminate are always available since their stanza builders live in
2//! core; the high-level call/accept flows, including their signaling, need the `voip` feature.
3
4#[cfg(feature = "voip-runtime")]
5use std::mem::size_of;
6#[cfg(feature = "voip-runtime")]
7use std::sync::Arc;
8#[cfg(feature = "voip-runtime")]
9use std::time::Duration;
10
11#[cfg(feature = "voip-runtime")]
12use log::warn;
13use wacore::stanza::call::{TerminateParams, build_reject, build_terminate};
14#[cfg(feature = "voip-runtime")]
15use wacore::stanza::group_call::{
16    build_active_group_accept, build_active_group_preaccept, build_call_link_create,
17    build_call_link_join_with_capability, build_call_link_query, build_raise_hand,
18    build_screen_share, build_waiting_room_admit, build_waiting_room_deny,
19    build_waiting_room_heartbeat, build_waiting_room_toggle, parse_call_link_create_ack,
20    parse_call_link_join_ack, parse_call_link_join_call_id, parse_call_link_query_ack,
21    parse_waiting_room_admit_ack, parse_waiting_room_deny_ack, parse_waiting_room_toggle_ack,
22};
23use wacore::types::call::IncomingCall;
24#[cfg(feature = "voip-runtime")]
25use wacore::types::call::{CallAction, VideoState};
26#[cfg(feature = "voip-runtime")]
27use wacore::types::group_call::{
28    CallLink, CallLinkJoin, CallLinkMedia, CallLinkPreview, GroupCallUpdate, ScreenShare,
29    ScreenShareState, WaitingRoom,
30};
31#[cfg(feature = "voip-runtime")]
32use wacore::voip::{AudioFormat, CallEvent, CallPhase, CallSession, VideoControl};
33use wacore_binary::Jid;
34#[cfg(feature = "voip-runtime")]
35use wacore_binary::Node;
36#[cfg(feature = "voip-runtime")]
37use wacore_binary::Server;
38#[cfg(feature = "voip-runtime")]
39use zeroize::Zeroizing;
40
41#[cfg(feature = "voip-runtime")]
42use super::ResponseWaiter;
43use super::{Client, ClientError};
44
45#[cfg(feature = "voip-runtime")]
46const CALL_SERVICE_REQUEST_TIMEOUT: Duration = Duration::from_secs(10);
47#[cfg(feature = "voip-runtime")]
48const WAITING_ROOM_HEARTBEAT_INTERVAL: Duration = Duration::from_secs(10);
49#[cfg(feature = "voip-runtime")]
50const WAITING_ROOM_HEARTBEAT_MAX_CONSECUTIVE_FAILURES: u8 = 3;
51#[cfg(feature = "voip-runtime")]
52const MAX_PENDING_CALL_LINK_TRANSITIONS: usize = 32;
53#[cfg(feature = "voip-runtime")]
54const MAX_PENDING_CALL_LINK_TRANSITION_BYTES: usize = 1024 * 1024;
55#[cfg(feature = "voip-runtime")]
56const MAX_PENDING_CALL_LINK_SATURATION_FINGERPRINTS: usize = 32;
57
58/// Opaque call-control handle obtained via [`Client::voip`]. Borrows the client;
59/// kept as a newtype so the surface can grow without breaking callers.
60pub struct Voip<'a> {
61    client: &'a Client,
62}
63
64#[cfg(feature = "voip-runtime")]
65struct CallLinkRegistrationGuard {
66    client: std::sync::Weak<Client>,
67    registry: Arc<wacore::voip::CallRegistry>,
68    call_id: String,
69    call_creator: Jid,
70    generation: u64,
71    armed: bool,
72}
73
74#[cfg(feature = "voip-runtime")]
75pub(crate) struct CallLinkJoinRegistration {
76    pub(crate) join: CallLinkJoin,
77    pub(crate) generation: u64,
78}
79
80#[cfg(feature = "voip-runtime")]
81#[derive(Clone, Copy)]
82enum WaitingRoomUserAction {
83    Admit,
84    Deny,
85}
86
87#[cfg(feature = "voip-runtime")]
88enum PendingCallLinkTransition {
89    Group(Box<GroupCallUpdate>),
90    WaitingRoom(WaitingRoom),
91    RawEpoch {
92        call_creator: Jid,
93        sender: Jid,
94        transaction_id: u32,
95        raw_epoch: Zeroizing<Vec<u8>>,
96    },
97    Terminated {
98        call_creator: Jid,
99        sender: Jid,
100    },
101    Saturated,
102}
103
104#[cfg(feature = "voip-runtime")]
105#[derive(Clone, Copy, Debug, PartialEq, Eq)]
106pub(crate) enum PendingCallLinkBuffer {
107    NotPending,
108    Buffered,
109    Saturated,
110}
111
112#[cfg(feature = "voip-runtime")]
113impl PendingCallLinkBuffer {
114    pub(crate) fn suppresses_dispatch(self) -> bool {
115        self != Self::NotPending
116    }
117}
118
119#[cfg(feature = "voip-runtime")]
120impl PendingCallLinkTransition {
121    fn group_heap_bytes(update: &GroupCallUpdate) -> usize {
122        use wacore::stats::HeapSize;
123
124        size_of::<GroupCallUpdate>() + update.heap_bytes()
125    }
126
127    fn waiting_room_heap_bytes(room: &WaitingRoom) -> usize {
128        use wacore::stats::HeapSize;
129
130        room.heap_bytes()
131    }
132
133    fn heap_bytes(&self) -> usize {
134        use wacore::stats::HeapSize;
135
136        match self {
137            Self::Group(update) => Self::group_heap_bytes(update),
138            Self::WaitingRoom(room) => Self::waiting_room_heap_bytes(room),
139            Self::RawEpoch {
140                call_creator,
141                sender,
142                raw_epoch,
143                ..
144            } => call_creator
145                .heap_bytes()
146                .saturating_add(sender.heap_bytes())
147                .saturating_add(raw_epoch.capacity()),
148            Self::Terminated {
149                call_creator,
150                sender,
151            } => call_creator
152                .heap_bytes()
153                .saturating_add(sender.heap_bytes()),
154            Self::Saturated => 0,
155        }
156    }
157}
158
159#[cfg(feature = "voip-runtime")]
160#[derive(Default)]
161pub(super) struct PendingCallLinkJoins {
162    active: usize,
163    bound_call_id: Option<String>,
164    transitions: std::collections::HashMap<String, Vec<PendingCallLinkTransition>>,
165    saturation_fingerprints: Vec<u64>,
166    saturation_hash_builder: std::collections::hash_map::RandomState,
167    untracked_saturation: bool,
168}
169
170#[cfg(feature = "voip-runtime")]
171impl PendingCallLinkJoins {
172    fn accepts(&self, call_id: &str) -> bool {
173        self.bound_call_id
174            .as_deref()
175            .is_none_or(|bound| bound == call_id)
176    }
177
178    fn can_buffer_transition(&self, call_id: &str, payload_bytes: usize) -> bool {
179        use wacore::stats::HeapSize;
180
181        let entries = self.transitions.values().map(Vec::len).sum::<usize>();
182        if entries >= MAX_PENDING_CALL_LINK_TRANSITIONS {
183            return false;
184        }
185        let new_key_bytes = if self.transitions.contains_key(call_id) {
186            0
187        } else {
188            size_of::<String>() + call_id.heap_bytes()
189        };
190        let structural_reserve = MAX_PENDING_CALL_LINK_TRANSITIONS
191            .saturating_mul(size_of::<PendingCallLinkTransition>());
192        self.memory_stats()
193            .bytes
194            .saturating_add(payload_bytes.try_into().unwrap_or(u64::MAX))
195            .saturating_add(new_key_bytes.try_into().unwrap_or(u64::MAX))
196            .saturating_add(structural_reserve.try_into().unwrap_or(u64::MAX))
197            <= MAX_PENDING_CALL_LINK_TRANSITION_BYTES as u64
198    }
199
200    fn bind_call_id(&mut self, call_id: &str) {
201        let fingerprint = self.call_id_fingerprint(call_id);
202        self.bound_call_id = Some(call_id.to_string());
203        self.transitions.retain(|retained, _| retained == call_id);
204        self.saturation_fingerprints
205            .retain(|retained| *retained == fingerprint);
206    }
207
208    fn prepare_bound_retry(&mut self, call_id: &str) -> bool {
209        if !self.untracked_saturation || self.bound_call_id.as_deref() != Some(call_id) {
210            return false;
211        }
212        // The first ACK gives the provisional buffer an exact identity. When unrelated traffic
213        // exhausted even the overflow fingerprints, retry the join from that bound state instead
214        // of either failing the valid call or silently ignoring a possibly dropped transition.
215        // The refreshed ACK is the new authoritative floor; controls racing the retry are retained
216        // only for this call id and replayed after it.
217        self.transitions.clear();
218        self.saturation_fingerprints.clear();
219        self.untracked_saturation = false;
220        true
221    }
222
223    fn is_saturated(&self, call_id: &str) -> bool {
224        self.untracked_saturation
225            || self
226                .saturation_fingerprints
227                .contains(&self.call_id_fingerprint(call_id))
228            || self.transitions.get(call_id).is_some_and(|transitions| {
229                transitions
230                    .iter()
231                    .any(|transition| matches!(transition, PendingCallLinkTransition::Saturated))
232            })
233    }
234
235    fn call_id_fingerprint(&self, call_id: &str) -> u64 {
236        use std::hash::BuildHasher;
237
238        self.saturation_hash_builder.hash_one(call_id)
239    }
240
241    fn mark_saturated(&mut self, call_id: &str) {
242        // Saturation belongs to the call whose transition could not be retained. An unrelated
243        // creator-authenticated control must not poison the one unknown call-link join that owns
244        // this bounded buffer.
245        self.transitions.remove(call_id);
246        if self.can_buffer_transition(call_id, 0) {
247            self.transitions.insert(
248                call_id.to_string(),
249                vec![PendingCallLinkTransition::Saturated],
250            );
251            return;
252        }
253        let fingerprint = self.call_id_fingerprint(call_id);
254        if self.saturation_fingerprints.contains(&fingerprint) {
255            return;
256        }
257        if self.saturation_fingerprints.len() < MAX_PENDING_CALL_LINK_SATURATION_FINGERPRINTS {
258            // Keep the failure identity in fixed-size metadata even when unrelated payloads have
259            // consumed every transition slot. Binding the ACK can then fail exactly the join whose
260            // admission state was dropped without letting unrelated saturation poison it.
261            self.saturation_fingerprints.push(fingerprint);
262        } else {
263            // If even the bounded fingerprint reserve is exhausted, remember that exact
264            // membership is ambiguous. Once the ACK binds a call id, the join path refreshes its
265            // authoritative state before registration instead of guessing or failing another id.
266            self.untracked_saturation = true;
267        }
268    }
269
270    pub(super) fn memory_stats(&self) -> wacore::stats::CollectionStats {
271        use wacore::stats::HeapSize;
272
273        let transition_bytes = self
274            .transitions
275            .iter()
276            .map(|(call_id, transitions)| {
277                size_of::<String>()
278                    + call_id.heap_bytes()
279                    + transitions.capacity() * size_of::<PendingCallLinkTransition>()
280                    + transitions
281                        .iter()
282                        .map(PendingCallLinkTransition::heap_bytes)
283                        .sum::<usize>()
284            })
285            .sum::<usize>();
286        let bytes = transition_bytes
287            .saturating_add(self.saturation_fingerprints.capacity() * size_of::<u64>());
288        wacore::stats::CollectionStats::new(
289            self.transitions
290                .values()
291                .map(Vec::len)
292                .sum::<usize>()
293                .saturating_add(self.saturation_fingerprints.len())
294                .saturating_add(usize::from(self.untracked_saturation))
295                .try_into()
296                .unwrap_or(u64::MAX),
297            bytes.try_into().unwrap_or(u64::MAX),
298        )
299    }
300}
301
302#[cfg(feature = "voip-runtime")]
303struct PendingCallLinkJoinGuard {
304    state: Arc<std::sync::Mutex<PendingCallLinkJoins>>,
305}
306
307#[cfg(feature = "voip-runtime")]
308impl Drop for PendingCallLinkJoinGuard {
309    fn drop(&mut self) {
310        let mut state = self
311            .state
312            .lock()
313            .unwrap_or_else(|poisoned| poisoned.into_inner());
314        state.active = state.active.saturating_sub(1);
315        if state.active == 0 {
316            state.bound_call_id = None;
317            state.transitions.clear();
318            state.saturation_fingerprints.clear();
319            state.untracked_saturation = false;
320        }
321    }
322}
323
324#[cfg(feature = "voip-runtime")]
325impl CallLinkRegistrationGuard {
326    fn new(
327        client: &Client,
328        registry: Arc<wacore::voip::CallRegistry>,
329        call_id: &str,
330        call_creator: Jid,
331        generation: u64,
332    ) -> Self {
333        Self {
334            client: client.self_weak.get().cloned().unwrap_or_default(),
335            registry,
336            call_id: call_id.to_string(),
337            call_creator,
338            generation,
339            armed: true,
340        }
341    }
342
343    fn disarm(&mut self) {
344        self.armed = false;
345    }
346}
347
348#[cfg(feature = "voip-runtime")]
349impl Drop for CallLinkRegistrationGuard {
350    fn drop(&mut self) {
351        if !self.armed {
352            return;
353        }
354        let Some(client) = self.client.upgrade() else {
355            self.registry
356                .remove_if_current(&self.call_id, self.generation);
357            return;
358        };
359        let registry = self.registry.clone();
360        let call_id = self.call_id.clone();
361        let call_creator = self.call_creator.clone();
362        let generation = self.generation;
363        let runtime = client.runtime.clone();
364        runtime
365            .spawn(Box::pin(async move {
366                // A cancelled admitted join is still live on the call service. Claim this exact
367                // generation under the replacement lane before deciding whether a wire terminate
368                // is required; waiting-room cancellation remains local-only.
369                let _transition = client.lock_answer_transition(&call_id).await;
370                let Some(phase) = registry.remove_if_current_with_phase(&call_id, generation)
371                else {
372                    return;
373                };
374                if phase == CallPhase::WaitingRoom {
375                    return;
376                }
377                let target = Jid::new(&call_id, Server::Call);
378                crate::voip::facade::send_answer_terminate(
379                    &client,
380                    &call_id,
381                    &target,
382                    &call_creator,
383                )
384                .await;
385            }))
386            .detach();
387    }
388}
389
390impl Client {
391    /// Call control: reject/terminate are always available; media (call/accept)
392    /// needs the `voip` feature.
393    pub fn voip(&self) -> Voip<'_> {
394        Voip { client: self }
395    }
396
397    /// The per-call media registry the `voip` facade registers active calls in. `pub(crate)` so the
398    /// facade and the connection-cleanup teardown share one instance.
399    #[cfg(feature = "voip-runtime")]
400    pub(crate) fn call_registry(&self) -> Arc<wacore::voip::CallRegistry> {
401        self.call_registry.clone()
402    }
403
404    #[cfg(feature = "voip-runtime")]
405    fn begin_call_link_join(&self) -> PendingCallLinkJoinGuard {
406        let mut state = self
407            .pending_call_link_joins
408            .lock()
409            .unwrap_or_else(|poisoned| poisoned.into_inner());
410        if state.active == 0 {
411            state.bound_call_id = None;
412            state.transitions.clear();
413            state.saturation_fingerprints.clear();
414            state.untracked_saturation = false;
415        }
416        state.active = state.active.saturating_add(1);
417        drop(state);
418        PendingCallLinkJoinGuard {
419            state: self.pending_call_link_joins.clone(),
420        }
421    }
422
423    #[cfg(feature = "voip-runtime")]
424    pub(crate) fn pending_call_link_control_candidate(
425        &self,
426        call_id: &str,
427        call_creator: &Jid,
428        sender: &Jid,
429    ) -> bool {
430        let state = self
431            .pending_call_link_joins
432            .lock()
433            .unwrap_or_else(|poisoned| poisoned.into_inner());
434        state.active != 0
435            && !call_id.is_empty()
436            && state.accepts(call_id)
437            && sender.to_non_ad() == call_creator.to_non_ad()
438            && self.call_registry.generation_of(call_id).is_none()
439    }
440
441    /// Bind the one serialized pending link join to the ACK's exact call id before the read loop
442    /// wakes the request task. Controls for unrelated unknown calls can no longer consume its
443    /// retained-entry or byte budget during the ACK/registration race.
444    #[cfg(feature = "voip-runtime")]
445    pub(crate) fn bind_pending_call_link_join_ack(&self, response: &wacore_binary::NodeRef<'_>) {
446        let Ok(call_id) = parse_call_link_join_call_id(response) else {
447            return;
448        };
449        let mut state = self
450            .pending_call_link_joins
451            .lock()
452            .unwrap_or_else(|poisoned| poisoned.into_inner());
453        if state.active != 0 {
454            state.bind_call_id(&call_id);
455        }
456    }
457
458    #[cfg(feature = "voip-runtime")]
459    fn prepare_pending_call_link_join_retry(&self, call_id: &str) -> bool {
460        let mut state = self
461            .pending_call_link_joins
462            .lock()
463            .unwrap_or_else(|poisoned| poisoned.into_inner());
464        state.prepare_bound_retry(call_id)
465    }
466
467    /// Buffer a creator-authenticated admission snapshot while its link-join ACK is being
468    /// registered. The pending-state lock is shared with registration, closing both orderings of
469    /// the ACK/update race without accepting arbitrary unknown calls.
470    #[cfg(feature = "voip-runtime")]
471    pub(crate) fn buffer_pending_call_link_update(
472        &self,
473        update: &GroupCallUpdate,
474        sender: &Jid,
475    ) -> PendingCallLinkBuffer {
476        let mut state = self
477            .pending_call_link_joins
478            .lock()
479            .unwrap_or_else(|poisoned| poisoned.into_inner());
480        if state.active == 0
481            || update.call_id.is_empty()
482            || !state.accepts(&update.call_id)
483            || sender.to_non_ad() != update.call_creator.to_non_ad()
484            || self.call_registry.generation_of(&update.call_id).is_some()
485        {
486            return PendingCallLinkBuffer::NotPending;
487        }
488        if state.is_saturated(&update.call_id) {
489            return PendingCallLinkBuffer::Saturated;
490        }
491        if state
492            .transitions
493            .get(&update.call_id)
494            .into_iter()
495            .flatten()
496            .rev()
497            .find_map(|transition| match transition {
498                PendingCallLinkTransition::Group(update) => Some(update.transaction_id),
499                PendingCallLinkTransition::WaitingRoom(_)
500                | PendingCallLinkTransition::RawEpoch { .. }
501                | PendingCallLinkTransition::Terminated { .. }
502                | PendingCallLinkTransition::Saturated => None,
503            })
504            .is_some_and(|transaction_id| transaction_id >= update.transaction_id)
505        {
506            return PendingCallLinkBuffer::Buffered;
507        }
508        if !state.can_buffer_transition(
509            &update.call_id,
510            PendingCallLinkTransition::group_heap_bytes(update),
511        ) {
512            state.mark_saturated(&update.call_id);
513            return PendingCallLinkBuffer::Saturated;
514        }
515        state
516            .transitions
517            .entry(update.call_id.clone())
518            .or_default()
519            .push(PendingCallLinkTransition::Group(Box::new(update.clone())));
520        PendingCallLinkBuffer::Buffered
521    }
522
523    /// Retain a creator-authenticated epoch that overtook publication of the call-link generation.
524    #[cfg(feature = "voip-runtime")]
525    pub(crate) fn buffer_pending_call_link_epoch(
526        &self,
527        call_id: &str,
528        call_creator: &Jid,
529        sender: &Jid,
530        transaction_id: u32,
531        raw_epoch: &[u8],
532    ) -> PendingCallLinkBuffer {
533        let mut state = self
534            .pending_call_link_joins
535            .lock()
536            .unwrap_or_else(|poisoned| poisoned.into_inner());
537        if state.active == 0
538            || call_id.is_empty()
539            || !state.accepts(call_id)
540            || sender.to_non_ad() != call_creator.to_non_ad()
541            || self.call_registry.generation_of(call_id).is_some()
542        {
543            return PendingCallLinkBuffer::NotPending;
544        }
545        if state.is_saturated(call_id) {
546            return PendingCallLinkBuffer::Saturated;
547        }
548        if state
549            .transitions
550            .get(call_id)
551            .into_iter()
552            .flatten()
553            .rev()
554            .find_map(|transition| match transition {
555                PendingCallLinkTransition::RawEpoch {
556                    call_creator: retained_creator,
557                    sender: retained_sender,
558                    transaction_id,
559                    ..
560                } if retained_creator == call_creator && retained_sender == sender => {
561                    Some(*transaction_id)
562                }
563                _ => None,
564            })
565            .is_some_and(|retained| retained >= transaction_id)
566        {
567            return PendingCallLinkBuffer::Buffered;
568        }
569        if !state.can_buffer_transition(call_id, raw_epoch.len()) {
570            state.mark_saturated(call_id);
571            return PendingCallLinkBuffer::Saturated;
572        }
573        state
574            .transitions
575            .entry(call_id.to_string())
576            .or_default()
577            .push(PendingCallLinkTransition::RawEpoch {
578                call_creator: call_creator.clone(),
579                sender: sender.clone(),
580                transaction_id,
581                raw_epoch: Zeroizing::new(raw_epoch.to_vec()),
582            });
583        PendingCallLinkBuffer::Buffered
584    }
585
586    /// Mark a creator-authenticated call-link generation as ended before its ACK is registered.
587    #[cfg(feature = "voip-runtime")]
588    pub(crate) fn buffer_pending_call_link_terminate(
589        &self,
590        call_id: &str,
591        call_creator: &Jid,
592        sender: &Jid,
593    ) -> PendingCallLinkBuffer {
594        let mut state = self
595            .pending_call_link_joins
596            .lock()
597            .unwrap_or_else(|poisoned| poisoned.into_inner());
598        if state.active == 0
599            || call_id.is_empty()
600            || !state.accepts(call_id)
601            || sender.to_non_ad() != call_creator.to_non_ad()
602            || self.call_registry.generation_of(call_id).is_some()
603        {
604            return PendingCallLinkBuffer::NotPending;
605        }
606        if state.is_saturated(call_id) {
607            return PendingCallLinkBuffer::Saturated;
608        }
609        if state
610            .transitions
611            .get(call_id)
612            .into_iter()
613            .flatten()
614            .any(|transition| {
615                matches!(
616                    transition,
617                    PendingCallLinkTransition::Terminated {
618                        call_creator: retained_creator,
619                        sender: retained_sender,
620                    } if retained_creator == call_creator && retained_sender == sender
621                )
622            })
623        {
624            return PendingCallLinkBuffer::Buffered;
625        }
626        if !state.can_buffer_transition(call_id, 0) {
627            state.mark_saturated(call_id);
628            return PendingCallLinkBuffer::Saturated;
629        }
630        state
631            .transitions
632            .entry(call_id.to_string())
633            .or_default()
634            .push(PendingCallLinkTransition::Terminated {
635                call_creator: call_creator.clone(),
636                sender: sender.clone(),
637            });
638        PendingCallLinkBuffer::Buffered
639    }
640
641    /// Serialize a terminal control with publication of the call-link generation it targets.
642    #[cfg(feature = "voip-runtime")]
643    pub(crate) async fn retain_or_apply_pending_call_link_terminate(
644        &self,
645        call_id: &str,
646        call_creator: &Jid,
647        sender: &Jid,
648    ) -> bool {
649        let _answer_transition = self.lock_answer_transition(call_id).await;
650        let buffered = self.buffer_pending_call_link_terminate(call_id, call_creator, sender);
651        if buffered.suppresses_dispatch() {
652            return true;
653        }
654        let Some(generation) = self.call_registry.generation_of(call_id) else {
655            return false;
656        };
657        if !self.call_registry.group_creator_authorized_if_current(
658            call_id,
659            generation,
660            call_creator,
661            sender,
662        ) {
663            return false;
664        }
665        self.call_registry.remove_if_current(call_id, generation)
666    }
667
668    /// Buffer a creator-authenticated waiting-room snapshot in the same ordered call-link
669    /// transition stream as admission rosters.
670    #[cfg(feature = "voip-runtime")]
671    pub(crate) fn buffer_pending_call_link_waiting_room(
672        &self,
673        room: &WaitingRoom,
674        sender: &Jid,
675    ) -> PendingCallLinkBuffer {
676        let mut state = self
677            .pending_call_link_joins
678            .lock()
679            .unwrap_or_else(|poisoned| poisoned.into_inner());
680        if self.call_registry.generation_of(&room.call_id).is_some()
681            || state.active == 0
682            || room.call_id.is_empty()
683            || !state.accepts(&room.call_id)
684            || room.link_token.is_empty()
685            || sender.to_non_ad() != room.call_creator.to_non_ad()
686        {
687            return PendingCallLinkBuffer::NotPending;
688        }
689        if state.is_saturated(&room.call_id) {
690            return PendingCallLinkBuffer::Saturated;
691        }
692        if !state.can_buffer_transition(
693            &room.call_id,
694            PendingCallLinkTransition::waiting_room_heap_bytes(room),
695        ) {
696            state.mark_saturated(&room.call_id);
697            return PendingCallLinkBuffer::Saturated;
698        }
699        state
700            .transitions
701            .entry(room.call_id.clone())
702            .or_default()
703            .push(PendingCallLinkTransition::WaitingRoom(room.clone()));
704        PendingCallLinkBuffer::Buffered
705    }
706
707    #[cfg(feature = "voip-runtime")]
708    async fn register_call_link_session(
709        &self,
710        session: CallSession,
711        waiting_room: Option<WaitingRoom>,
712        expected_media: CallLinkMedia,
713        expected_token: &str,
714    ) -> Result<u64, wacore::voip::GroupStateApply> {
715        let call_id = session.call_id.clone();
716        let call_creator = session.call_creator.clone();
717        let mut rekey_pending = session
718            .group
719            .as_ref()
720            .is_some_and(|update| update.rekey_requested);
721        // Share the stable call-id lane with every competing call registration. Once this join
722        // inserts its generation, no re-offer can replace it until all staged admission state has
723        // either committed to that generation or caused registration to fail.
724        let _answer_transition = self.lock_answer_transition(&call_id).await;
725        let mut state = self
726            .pending_call_link_joins
727            .lock()
728            .unwrap_or_else(|poisoned| poisoned.into_inner());
729        state.bind_call_id(&call_id);
730        let saturated = state.is_saturated(&call_id);
731        let staged = state.transitions.remove(&call_id).unwrap_or_default();
732        if saturated {
733            return Err(wacore::voip::GroupStateApply::InvalidSnapshot);
734        }
735        let generation = self.call_registry.insert_call_link_checked(session)?;
736        if let Some(room) = waiting_room {
737            let applied = self
738                .call_registry
739                .apply_waiting_room_if_current(room, generation);
740            if applied != wacore::voip::GroupStateApply::Applied {
741                self.call_registry.remove_if_current(&call_id, generation);
742                return Err(applied);
743            }
744        }
745        for transition in staged {
746            match transition {
747                PendingCallLinkTransition::Group(update)
748                    if update.call_creator == call_creator
749                        && update.media == expected_media.as_str() =>
750                {
751                    let mut update = *update;
752                    update.rekey_requested |= rekey_pending;
753                    let staged_rekey = update.rekey_requested;
754                    match self.apply_pending_call_link_update(update, generation) {
755                        wacore::voip::GroupStateApply::Applied => {
756                            rekey_pending = staged_rekey;
757                        }
758                        wacore::voip::GroupStateApply::Stale => {}
759                        rejected => {
760                            self.call_registry.remove_if_current(&call_id, generation);
761                            return Err(rejected);
762                        }
763                    }
764                }
765                PendingCallLinkTransition::WaitingRoom(room)
766                    if room.call_creator == call_creator
767                        && room.media == expected_media
768                        && room.link_token == expected_token =>
769                {
770                    let applied = self
771                        .call_registry
772                        .apply_waiting_room_if_current(room, generation);
773                    if !matches!(
774                        applied,
775                        wacore::voip::GroupStateApply::Applied
776                            | wacore::voip::GroupStateApply::Stale
777                    ) {
778                        self.call_registry.remove_if_current(&call_id, generation);
779                        return Err(applied);
780                    }
781                }
782                PendingCallLinkTransition::RawEpoch {
783                    call_creator: staged_creator,
784                    sender,
785                    transaction_id,
786                    raw_epoch,
787                } => {
788                    if !self.call_registry.group_sender_authorized_if_current(
789                        &call_id,
790                        generation,
791                        &staged_creator,
792                        &sender,
793                    ) {
794                        continue;
795                    }
796                    if !self.call_registry.send_group_epoch_if_current(
797                        &call_id,
798                        generation,
799                        transaction_id,
800                        raw_epoch.to_vec(),
801                    ) {
802                        self.call_registry.remove_if_current(&call_id, generation);
803                        return Err(wacore::voip::GroupStateApply::UnknownCall);
804                    }
805                }
806                PendingCallLinkTransition::Terminated {
807                    call_creator: staged_creator,
808                    sender,
809                } => {
810                    if !self.call_registry.group_creator_authorized_if_current(
811                        &call_id,
812                        generation,
813                        &staged_creator,
814                        &sender,
815                    ) {
816                        continue;
817                    }
818                    self.call_registry.remove_if_current(&call_id, generation);
819                    return Err(wacore::voip::GroupStateApply::InvalidSnapshot);
820                }
821                PendingCallLinkTransition::Saturated => {
822                    self.call_registry.remove_if_current(&call_id, generation);
823                    return Err(wacore::voip::GroupStateApply::InvalidSnapshot);
824                }
825                _ => {}
826            }
827        }
828        Ok(generation)
829    }
830
831    #[cfg(feature = "voip-runtime")]
832    fn apply_pending_call_link_update(
833        &self,
834        update: GroupCallUpdate,
835        generation: u64,
836    ) -> wacore::voip::GroupStateApply {
837        self.call_registry
838            .apply_group_update_if_current(update, generation)
839    }
840
841    /// Lock the striped answer-transition lane for `call_id`. Incoming answer registration and
842    /// answer teardown both use this, preventing a replacement generation from being installed
843    /// after the old one is claimed but before its terminal stanza reaches the wire.
844    #[cfg(feature = "voip-runtime")]
845    pub(crate) fn answer_transition_lock(&self, call_id: &str) -> Arc<async_lock::Mutex<()>> {
846        use std::hash::{Hash, Hasher};
847
848        let mut hasher = std::collections::hash_map::DefaultHasher::new();
849        call_id.hash(&mut hasher);
850        let lane = hasher.finish() as usize % self.answer_transition_locks.len();
851        self.answer_transition_locks[lane].clone()
852    }
853
854    #[cfg(feature = "voip-runtime")]
855    pub(crate) async fn lock_answer_transition(
856        &self,
857        call_id: &str,
858    ) -> async_lock::MutexGuardArc<()> {
859        self.answer_transition_lock(call_id).lock_arc().await
860    }
861}
862
863/// Errors from call-control operations. `#[non_exhaustive]` so new variants stay
864/// non-breaking after 1.0.
865#[derive(Debug, thiserror::Error)]
866#[non_exhaustive]
867pub enum CallError {
868    #[error("{0}")]
869    Send(#[from] ClientError),
870    #[error("call_id cannot be empty")]
871    EmptyCallId,
872    /// `accept` was called with an `IncomingCall` that is not an `<offer>` (nothing to answer).
873    #[cfg(feature = "voip-runtime")]
874    #[error("not an incoming call offer")]
875    NotAnOffer,
876    /// `accept().start()` was called without PCM or encoded audio endpoints.
877    #[cfg(feature = "voip-runtime")]
878    #[error("accept() requires audio(...) or encoded_audio(...) before start()")]
879    MissingAudio,
880    /// The selected media profile was not present in the incoming offer.
881    #[cfg(feature = "voip-runtime")]
882    #[error("incoming offer does not advertise the selected audio rate {0}")]
883    AudioFormatNotOffered(u32),
884    /// Video endpoints were supplied for an offer that only advertised audio.
885    #[cfg(feature = "voip-runtime")]
886    #[error("incoming offer did not advertise video; use start_video() after answering")]
887    VideoNotOffered,
888    /// The peer ended or superseded the call while the answer was being prepared.
889    #[cfg(feature = "voip-runtime")]
890    #[error("call ended during answer setup")]
891    CallEndedDuringSetup,
892    /// Decrypting the offer's encrypted callKey failed.
893    #[cfg(feature = "voip-runtime")]
894    #[error("callKey decrypt failed: {0}")]
895    Decrypt(String),
896    /// Assembling the call config from the offer's relay block failed.
897    #[cfg(feature = "voip-runtime")]
898    #[error("call setup failed: {0}")]
899    Setup(String),
900    /// Connecting the relay media transport (UDP/DTLS/SCTP) failed.
901    #[cfg(feature = "voip-runtime")]
902    #[error("relay connect failed: {0}")]
903    Connect(String),
904    /// The offer was missing media material (no `<enc>`/`<relay>`, no callKey, no own LID, etc.).
905    #[cfg(feature = "voip-runtime")]
906    #[error("media offer error: {0}")]
907    Media(&'static str),
908    /// The peer cancelled or replaced the upgrade before its video source became ready.
909    #[cfg(feature = "voip-runtime")]
910    #[error("video upgrade request is no longer current")]
911    VideoUpgradeExpired,
912    /// `call(peer)` resolved zero devices for the peer (nothing to address an offer to).
913    #[cfg(feature = "voip-runtime")]
914    #[error("peer has no resolvable devices")]
915    NoDevices,
916    /// An outgoing offer would emit a pkmsg `<enc>` but we hold no ADV account, so the peer could
917    /// not validate the pre-key message. Refused before send to avoid advancing the sender chain
918    /// (mirrors the peer-send path's `<device-identity>` requirement).
919    #[cfg(feature = "voip-runtime")]
920    #[error("offer pkmsg requires <device-identity> (account is None)")]
921    MissingDeviceIdentity,
922    /// A call-service response was malformed or rejected.
923    #[cfg(feature = "voip-runtime")]
924    #[error("call service response failed: {0}")]
925    Response(String),
926    /// The call service did not answer within its bounded request window.
927    #[cfg(feature = "voip-runtime")]
928    #[error("call service request timed out")]
929    ResponseTimeout,
930}
931
932impl Voip<'_> {
933    /// Reject an incoming call. Fire-and-forget — no server response is expected.
934    pub async fn reject(&self, incoming: &IncomingCall) -> Result<(), CallError> {
935        self.reject_call_inner(
936            incoming.action.call_id(),
937            &incoming.from,
938            incoming.action.call_creator(),
939            incoming.ringing_generation(),
940        )
941        .await
942    }
943
944    /// Reject a call when its signaling identifiers are already available.
945    /// `peer` is the outer `<call to>` target, while `call_creator` is the
946    /// action's `call-creator` attribute; preserve them separately because
947    /// they may differ for companion-device signaling.
948    /// Fire-and-forget — no server response is expected.
949    pub async fn reject_call(
950        &self,
951        call_id: &str,
952        peer: &Jid,
953        call_creator: &Jid,
954    ) -> Result<(), CallError> {
955        self.reject_call_inner(call_id, peer, call_creator, None)
956            .await
957    }
958
959    async fn reject_call_inner(
960        &self,
961        call_id: &str,
962        peer: &Jid,
963        call_creator: &Jid,
964        _ringing_generation: Option<u64>,
965    ) -> Result<(), CallError> {
966        if call_id.is_empty() {
967            return Err(CallError::EmptyCallId);
968        }
969        let id = self.client.generate_request_id();
970        let stanza = build_reject(call_id, peer, call_creator, &id);
971        // Consume the ringing flag BEFORE the async send: a caller <terminate> processed while we await
972        // the send would otherwise hit take_ringing first and surface a phantom missed call for a call
973        // we already declined (WA Web deletes it from _ringingCalls on reject). No-op if never ringing.
974        #[cfg(feature = "voip-runtime")]
975        {
976            let registry = self.client.call_registry();
977            if let Some(generation) = _ringing_generation {
978                if !registry.reject_ringing_if_current(call_id, generation) {
979                    return Err(CallError::CallEndedDuringSetup);
980                }
981            } else {
982                let generation = registry.ringing_group_generation(call_id, call_creator);
983                registry.take_ringing(call_id);
984                if let Some(generation) = generation {
985                    registry.remove_if_current(call_id, generation);
986                }
987            }
988        }
989        self.client.send_node(stanza).await?;
990        Ok(())
991    }
992
993    /// Begin answering an incoming call: returns a builder; call `.audio(source, sink)` then
994    /// `.start().await` to send `<preaccept>`, decrypt the callKey, send `<accept>`, connect the relay,
995    /// and drive the call, yielding a [`CallHandle`](crate::voip::CallHandle). Requires
996    /// `voip-runtime` or a profile that enables it: `voip`, `voip-encoded`, `voip-mlow`, or
997    /// `voip-libopus`.
998    #[cfg(feature = "voip-runtime")]
999    pub fn accept<'b>(&'b self, incoming: &'b IncomingCall) -> crate::voip::AcceptCall<'b> {
1000        crate::voip::facade::AcceptCall::new(self.client, incoming)
1001    }
1002
1003    /// Begin placing an outgoing 1:1 call to `peer`: returns a builder; call `.audio(source, sink)`
1004    /// then `.start().await` to generate the callKey, encrypt it per peer device, send the `<offer>`,
1005    /// and register the call, yielding a [`CallHandle`](crate::voip::CallHandle). The media engine
1006    /// only attaches once the server hands back the relay for our call-id (live), so the returned
1007    /// handle is dormant until then. Requires `voip-runtime` or a profile that enables it: `voip`,
1008    /// `voip-encoded`, `voip-mlow`, or `voip-libopus`.
1009    #[cfg(feature = "voip-runtime")]
1010    pub fn call<'b>(&'b self, peer: &'b Jid) -> crate::voip::OutgoingCall<'b> {
1011        crate::voip::facade::OutgoingCall::new(self.client, peer)
1012    }
1013
1014    /// Begin a native group call to two or more selected users.
1015    #[cfg(feature = "voip-runtime")]
1016    pub fn group_call<'b>(&'b self, targets: &'b [Jid]) -> crate::voip::OutgoingGroupCall<'b> {
1017        crate::voip::facade::OutgoingGroupCall::new(self.client, targets)
1018    }
1019
1020    /// Begin a native call bound to an existing group. The current roster is resolved at
1021    /// [`start`](crate::voip::GroupBoundCall::start), with this account excluded automatically.
1022    #[cfg(feature = "voip-runtime")]
1023    pub fn group_call_by_id<'b>(&'b self, group_jid: &'b Jid) -> crate::voip::GroupBoundCall<'b> {
1024        crate::voip::facade::GroupBoundCall::new(self.client, group_jid)
1025    }
1026
1027    /// Join a reusable call link and attach group media after admission.
1028    #[cfg(feature = "voip-runtime")]
1029    pub fn call_link<'b>(
1030        &'b self,
1031        token_or_url: &'b str,
1032        media: CallLinkMedia,
1033    ) -> crate::voip::CallLinkCall<'b> {
1034        crate::voip::facade::CallLinkCall::new(self.client, token_or_url, media)
1035    }
1036
1037    /// Send the eager preparation response for an active group-call invitation.
1038    #[cfg(feature = "voip-runtime")]
1039    pub async fn preaccept_group_invite(&self, incoming: &IncomingCall) -> Result<(), CallError> {
1040        let CallAction::Offer {
1041            call_id,
1042            call_creator,
1043            is_video,
1044            ..
1045        } = &incoming.action
1046        else {
1047            return Err(CallError::NotAnOffer);
1048        };
1049        if incoming.group.is_none() {
1050            return Err(CallError::Media("offer is not an active group invitation"));
1051        }
1052        let registry = self.client.call_registry();
1053        let Some(retained_generation) = incoming.ringing_generation() else {
1054            return Err(CallError::CallEndedDuringSetup);
1055        };
1056        let generation = registry
1057            .ringing_group_generation(call_id, call_creator)
1058            .ok_or(CallError::CallEndedDuringSetup)?;
1059        if generation != retained_generation {
1060            return Err(CallError::CallEndedDuringSetup);
1061        }
1062        let node = build_active_group_preaccept(
1063            call_id,
1064            call_creator,
1065            &self.client.generate_request_id(),
1066            *is_video,
1067        )
1068        .map_err(|error| CallError::Response(error.to_string()))?;
1069        self.client.send_node(node).await?;
1070        if registry.ringing_group_generation(call_id, call_creator) != Some(generation) {
1071            return Err(CallError::CallEndedDuringSetup);
1072        }
1073        Ok(())
1074    }
1075
1076    /// Send an early call-scoped accept for an active group invitation.
1077    ///
1078    /// The retained offer remains ringing so [`accept`](Self::accept) can subsequently attach the
1079    /// application's media endpoints to the exact same generation.
1080    #[cfg(feature = "voip-runtime")]
1081    pub async fn accept_group_invite(&self, incoming: &IncomingCall) -> Result<(), CallError> {
1082        let CallAction::Offer {
1083            call_id,
1084            call_creator,
1085            is_video,
1086            ..
1087        } = &incoming.action
1088        else {
1089            return Err(CallError::NotAnOffer);
1090        };
1091        if incoming.group.is_none() {
1092            return Err(CallError::Media("offer is not an active group invitation"));
1093        }
1094        let registry = self.client.call_registry();
1095        let Some(retained_generation) = incoming.ringing_generation() else {
1096            return Err(CallError::CallEndedDuringSetup);
1097        };
1098        let generation = registry
1099            .ringing_group_generation(call_id, call_creator)
1100            .ok_or(CallError::CallEndedDuringSetup)?;
1101        if generation != retained_generation {
1102            return Err(CallError::CallEndedDuringSetup);
1103        }
1104        let node = build_active_group_accept(
1105            call_id,
1106            call_creator,
1107            &self.client.generate_request_id(),
1108            *is_video,
1109        )
1110        .map_err(|error| CallError::Response(error.to_string()))?;
1111        self.client.send_node(node).await?;
1112        if registry.ringing_group_generation(call_id, call_creator) != Some(generation) {
1113            return Err(CallError::CallEndedDuringSetup);
1114        }
1115        Ok(())
1116    }
1117
1118    /// Create a reusable audio or video call link.
1119    #[cfg(feature = "voip-runtime")]
1120    pub async fn create_call_link(&self, media: CallLinkMedia) -> Result<CallLink, CallError> {
1121        let request_id = self.client.generate_request_id();
1122        let request = build_call_link_create(media, &request_id)
1123            .map_err(|error| CallError::Response(error.to_string()))?;
1124        let link = execute_call_service_request(
1125            self.client,
1126            &request_id,
1127            request,
1128            parse_call_link_create_ack,
1129        )
1130        .await?;
1131        if link.media != media {
1132            return Err(CallError::Response(
1133                "call-link creation changed the requested media mode".to_string(),
1134            ));
1135        }
1136        Ok(link)
1137    }
1138
1139    /// Inspect a call link without joining it.
1140    #[cfg(feature = "voip-runtime")]
1141    pub async fn preview_call_link(
1142        &self,
1143        token_or_url: &str,
1144        media: CallLinkMedia,
1145    ) -> Result<CallLinkPreview, CallError> {
1146        let token = normalize_call_link_token(token_or_url, media)?;
1147        let request_id = self.client.generate_request_id();
1148        let request = build_call_link_query(&token, media, &request_id)
1149            .map_err(|error| CallError::Response(error.to_string()))?;
1150        let preview = execute_call_service_request(
1151            self.client,
1152            &request_id,
1153            request,
1154            parse_call_link_query_ack,
1155        )
1156        .await?;
1157        if preview.token != token || preview.media != media {
1158            return Err(CallError::Response(
1159                "call-link preview changed the requested link identity".to_string(),
1160            ));
1161        }
1162        Ok(preview)
1163    }
1164
1165    /// Join a call link. The result explicitly reports whether this endpoint was admitted or placed
1166    /// in the waiting room; media starts only after an admitted authoritative group snapshot.
1167    #[cfg(feature = "voip-runtime")]
1168    pub async fn join_call_link(
1169        &self,
1170        token_or_url: &str,
1171        media: CallLinkMedia,
1172    ) -> Result<CallLinkJoin, CallError> {
1173        self.join_call_link_with_audio(token_or_url, media, AudioFormat::MLOW_16KHZ_60MS)
1174            .await
1175    }
1176
1177    #[cfg(feature = "voip-runtime")]
1178    pub(crate) async fn join_call_link_with_audio(
1179        &self,
1180        token_or_url: &str,
1181        media: CallLinkMedia,
1182        audio_format: AudioFormat,
1183    ) -> Result<CallLinkJoin, CallError> {
1184        Ok(self
1185            .join_call_link_registration_with_audio(token_or_url, media, audio_format)
1186            .await?
1187            .join)
1188    }
1189
1190    #[cfg(feature = "voip-runtime")]
1191    pub(crate) async fn join_call_link_registration_with_audio(
1192        &self,
1193        token_or_url: &str,
1194        media: CallLinkMedia,
1195        audio_format: AudioFormat,
1196    ) -> Result<CallLinkJoinRegistration, CallError> {
1197        // Before the ACK arrives, creator-authenticated admission traffic has no trusted call id
1198        // to associate with this request. Keep one such request active at a time so bounded-buffer
1199        // saturation can fail only its owning join; other joins wait here and start with clean
1200        // staging state.
1201        let pending_join_lane = self.client.pending_call_link_join_lane.lock().await;
1202        let own_lid = self.client.lid().ok_or(CallError::Media("no own LID"))?;
1203        let token = normalize_call_link_token(token_or_url, media)?;
1204        let capability =
1205            crate::voip::facade::offer_capability(media == CallLinkMedia::Video, audio_format);
1206        let pending_join = self.client.begin_call_link_join();
1207        let mut join =
1208            execute_call_link_join_request(self.client, &token, media, capability).await?;
1209        if join.media != media {
1210            return Err(CallError::Response(
1211                "call-link response changed the requested media mode".to_string(),
1212            ));
1213        }
1214        if join.call_id.is_empty() {
1215            return Err(CallError::EmptyCallId);
1216        }
1217        if self
1218            .client
1219            .prepare_pending_call_link_join_retry(&join.call_id)
1220        {
1221            let first_call_id = join.call_id.clone();
1222            let first_call_creator = join.call_creator.clone();
1223            let refreshed =
1224                execute_call_link_join_request(self.client, &token, media, capability).await?;
1225            if refreshed.media != media
1226                || refreshed.call_id != first_call_id
1227                || refreshed.call_creator != first_call_creator
1228            {
1229                return Err(CallError::Response(
1230                    "call-link identity changed while refreshing admission state".to_string(),
1231                ));
1232            }
1233            join = refreshed;
1234        }
1235
1236        let mut session = CallSession::new_outgoing(
1237            &join.call_id,
1238            Jid::new(&join.call_id, Server::Call),
1239            join.call_creator.clone(),
1240        );
1241        session.audio_format = Some(audio_format);
1242        session.is_video = media == CallLinkMedia::Video;
1243        session.group = join.group.clone();
1244        let _ = session.transition_to(CallPhase::Calling);
1245        let _ = session.transition_to(if join.in_waiting_room {
1246            CallPhase::WaitingRoom
1247        } else {
1248            CallPhase::Connecting
1249        });
1250        let registry = self.client.call_registry();
1251        let generation = self
1252            .client
1253            .register_call_link_session(session, join.waiting_room.clone(), media, &token)
1254            .await
1255            .map_err(|_| {
1256                CallError::Response("call-link admission snapshot was rejected".to_string())
1257            })?;
1258        // Publication transfers admission controls to the generation-scoped registry. Clear the
1259        // provisional binding before another serialized unknown-id join starts with a clean buffer.
1260        drop(pending_join);
1261        drop(pending_join_lane);
1262        let mut registration = CallLinkRegistrationGuard::new(
1263            self.client,
1264            registry.clone(),
1265            &join.call_id,
1266            join.call_creator.clone(),
1267            generation,
1268        );
1269
1270        if join.in_waiting_room && join.waiting_room.is_none() {
1271            return Err(CallError::Response(
1272                "call-link join omitted its waiting-room state".to_string(),
1273            ));
1274        }
1275
1276        registry.set_group_invite_self_device(
1277            &join.call_id,
1278            generation,
1279            wacore::types::group_call::GroupCallDevice::new(own_lid).with_capability(1, capability),
1280        );
1281        let rekey_required = join
1282            .group
1283            .as_ref()
1284            .is_some_and(|update| update.rekey_requested);
1285        let mut still_waiting = self
1286            .synchronize_call_link_admission(&mut join, generation, rekey_required)
1287            .await?;
1288        if still_waiting {
1289            let heartbeat = self
1290                .waiting_room_heartbeat(&join.call_id, &join.call_creator)
1291                .await;
1292            // The heartbeat crosses an unbounded transport await. Admission may have committed
1293            // while it was in flight, so re-read the generation before publishing the result or
1294            // starting a task that now belongs to an admitted call.
1295            still_waiting = self
1296                .synchronize_call_link_admission(&mut join, generation, rekey_required)
1297                .await?;
1298            if still_waiting {
1299                heartbeat?;
1300            }
1301        }
1302        if still_waiting {
1303            self.start_waiting_room_heartbeat(
1304                join.call_id.clone(),
1305                join.call_creator.clone(),
1306                generation,
1307            );
1308        }
1309
1310        registration.disarm();
1311        Ok(CallLinkJoinRegistration { join, generation })
1312    }
1313
1314    #[cfg(feature = "voip-runtime")]
1315    async fn synchronize_call_link_admission(
1316        &self,
1317        join: &mut CallLinkJoin,
1318        generation: u64,
1319        rekey_required: bool,
1320    ) -> Result<bool, CallError> {
1321        let registry = self.client.call_registry();
1322        let transition_lock = registry
1323            .group_transition_lock(&join.call_id, generation)
1324            .ok_or(CallError::CallEndedDuringSetup)?;
1325        let _transition_guard = transition_lock.lock().await;
1326        let state = registry
1327            .group_state_if_current(&join.call_id, generation)
1328            .ok_or(CallError::CallEndedDuringSetup)?;
1329        if let Some(room) = state.waiting_room().cloned() {
1330            join.waiting_room_enabled = room.enabled;
1331            join.is_admin = room.is_admin;
1332            join.waiting_room = Some(room);
1333        }
1334        let phase = registry
1335            .phase_if_current(&join.call_id, generation)
1336            .ok_or(CallError::CallEndedDuringSetup)?;
1337        if phase == CallPhase::WaitingRoom {
1338            join.in_waiting_room = true;
1339            return Ok(true);
1340        }
1341
1342        let update = state.snapshot().cloned().ok_or(CallError::Media(
1343            "admitted call link has no authoritative group snapshot",
1344        ))?;
1345        join.in_waiting_room = false;
1346        join.group = Some(update.clone());
1347        let retained_epoch =
1348            registry.pending_group_epoch_transaction_if_current(&join.call_id, generation);
1349        if (rekey_required || update.rekey_requested)
1350            && retained_epoch.is_none_or(|transaction| transaction < update.transaction_id)
1351        {
1352            // The shared transition lane keeps roster selection, fan-out, and publication on the
1353            // same transaction even if a post-registration update tries to overtake the ACK.
1354            crate::voip::facade::fanout_group_epoch(self.client, &update)
1355                .await?
1356                .commit(|epoch| {
1357                    registry
1358                        .send_group_epoch_if_current(
1359                            &join.call_id,
1360                            generation,
1361                            update.transaction_id,
1362                            epoch.to_vec(),
1363                        )
1364                        .then_some(())
1365                        .ok_or(CallError::Media(
1366                            "call-link group epoch could not be retained",
1367                        ))
1368                })?;
1369        }
1370        Ok(false)
1371    }
1372
1373    /// Enable or disable approval for a live call-link waiting room.
1374    #[cfg(feature = "voip-runtime")]
1375    pub async fn set_approval_required(
1376        &self,
1377        call_id: &str,
1378        call_creator: &Jid,
1379        enabled: bool,
1380    ) -> Result<(), CallError> {
1381        let generation = self
1382            .client
1383            .call_registry()
1384            .generation_of(call_id)
1385            .ok_or(CallError::Media("call is no longer active"))?;
1386        self.set_approval_required_for_generation(call_id, call_creator, generation, enabled)
1387            .await
1388    }
1389
1390    #[cfg(feature = "voip-runtime")]
1391    pub(crate) async fn set_approval_required_for_generation(
1392        &self,
1393        call_id: &str,
1394        call_creator: &Jid,
1395        generation: u64,
1396        enabled: bool,
1397    ) -> Result<(), CallError> {
1398        let registry = self.client.call_registry();
1399        let transition_lock = registry
1400            .group_transition_lock(call_id, generation)
1401            .ok_or(CallError::Media("call is no longer active"))?;
1402        let _transition_guard = transition_lock.lock().await;
1403        self.ensure_waiting_room_admin_if_current(call_id, generation)?;
1404        let request_id = self.client.generate_request_id();
1405        execute_call_service_request(
1406            self.client,
1407            &request_id,
1408            build_waiting_room_toggle(call_id, call_creator, enabled, &request_id)
1409                .map_err(|error| CallError::Response(error.to_string()))?,
1410            parse_waiting_room_toggle_ack,
1411        )
1412        .await?;
1413        if registry.set_waiting_room_enabled_if_current(call_id, generation, enabled) {
1414            Ok(())
1415        } else {
1416            Err(CallError::Media(
1417                "call was replaced while applying group control",
1418            ))
1419        }
1420    }
1421
1422    /// Keep a pending call-link admission alive.
1423    #[cfg(feature = "voip-runtime")]
1424    pub async fn waiting_room_heartbeat(
1425        &self,
1426        call_id: &str,
1427        call_creator: &Jid,
1428    ) -> Result<(), CallError> {
1429        self.send_group_control(
1430            call_id,
1431            build_waiting_room_heartbeat(call_id, call_creator, &self.client.generate_request_id())
1432                .map_err(|error| CallError::Response(error.to_string()))?,
1433        )
1434        .await
1435    }
1436
1437    /// Admit one user from a call-link waiting room.
1438    #[cfg(feature = "voip-runtime")]
1439    pub async fn admit_waiting_user(
1440        &self,
1441        call_id: &str,
1442        call_creator: &Jid,
1443        user: &Jid,
1444    ) -> Result<(), CallError> {
1445        let generation = self
1446            .client
1447            .call_registry()
1448            .generation_of(call_id)
1449            .ok_or(CallError::Media("call is no longer active"))?;
1450        self.admit_waiting_user_for_generation(call_id, call_creator, generation, user)
1451            .await
1452    }
1453
1454    #[cfg(feature = "voip-runtime")]
1455    pub(crate) async fn admit_waiting_user_for_generation(
1456        &self,
1457        call_id: &str,
1458        call_creator: &Jid,
1459        generation: u64,
1460        user: &Jid,
1461    ) -> Result<(), CallError> {
1462        self.waiting_room_user_action_for_generation(
1463            call_id,
1464            call_creator,
1465            generation,
1466            user,
1467            WaitingRoomUserAction::Admit,
1468        )
1469        .await
1470    }
1471
1472    /// Deny one user from a call-link waiting room.
1473    #[cfg(feature = "voip-runtime")]
1474    pub async fn deny_waiting_user(
1475        &self,
1476        call_id: &str,
1477        call_creator: &Jid,
1478        user: &Jid,
1479    ) -> Result<(), CallError> {
1480        let generation = self
1481            .client
1482            .call_registry()
1483            .generation_of(call_id)
1484            .ok_or(CallError::Media("call is no longer active"))?;
1485        self.deny_waiting_user_for_generation(call_id, call_creator, generation, user)
1486            .await
1487    }
1488
1489    #[cfg(feature = "voip-runtime")]
1490    pub(crate) async fn deny_waiting_user_for_generation(
1491        &self,
1492        call_id: &str,
1493        call_creator: &Jid,
1494        generation: u64,
1495        user: &Jid,
1496    ) -> Result<(), CallError> {
1497        self.waiting_room_user_action_for_generation(
1498            call_id,
1499            call_creator,
1500            generation,
1501            user,
1502            WaitingRoomUserAction::Deny,
1503        )
1504        .await
1505    }
1506
1507    #[cfg(feature = "voip-runtime")]
1508    async fn waiting_room_user_action_for_generation(
1509        &self,
1510        call_id: &str,
1511        call_creator: &Jid,
1512        generation: u64,
1513        user: &Jid,
1514        action: WaitingRoomUserAction,
1515    ) -> Result<(), CallError> {
1516        self.ensure_waiting_room_admin_if_current(call_id, generation)?;
1517        let request_id = self.client.generate_request_id();
1518        let (request, parse) = match action {
1519            WaitingRoomUserAction::Admit => (
1520                build_waiting_room_admit(call_id, call_creator, user, &request_id),
1521                parse_waiting_room_admit_ack
1522                    as fn(&wacore_binary::NodeRef<'_>) -> anyhow::Result<()>,
1523            ),
1524            WaitingRoomUserAction::Deny => (
1525                build_waiting_room_deny(call_id, call_creator, user, &request_id),
1526                parse_waiting_room_deny_ack
1527                    as fn(&wacore_binary::NodeRef<'_>) -> anyhow::Result<()>,
1528            ),
1529        };
1530        execute_call_service_request(
1531            self.client,
1532            &request_id,
1533            request.map_err(|error| CallError::Response(error.to_string()))?,
1534            parse,
1535        )
1536        .await?;
1537        if self.client.call_registry().is_current(call_id, generation) {
1538            Ok(())
1539        } else {
1540            Err(CallError::Media(
1541                "call was replaced while applying group control",
1542            ))
1543        }
1544    }
1545
1546    /// Publish the local persistent raise/lower-hand state.
1547    #[cfg(feature = "voip-runtime")]
1548    pub async fn set_hand_raised(
1549        &self,
1550        call_id: &str,
1551        call_creator: &Jid,
1552        raised: bool,
1553    ) -> Result<(), CallError> {
1554        let generation = self
1555            .client
1556            .call_registry()
1557            .generation_of(call_id)
1558            .ok_or(CallError::Media("call is no longer active"))?;
1559        self.set_hand_raised_for_generation(call_id, call_creator, generation, raised)
1560            .await
1561    }
1562
1563    #[cfg(feature = "voip-runtime")]
1564    pub(crate) async fn set_hand_raised_for_generation(
1565        &self,
1566        call_id: &str,
1567        call_creator: &Jid,
1568        generation: u64,
1569        raised: bool,
1570    ) -> Result<(), CallError> {
1571        let registry = self.client.call_registry();
1572        let transition_lock = registry
1573            .group_transition_lock(call_id, generation)
1574            .ok_or(CallError::Media("call is no longer active"))?;
1575        let _transition_guard = transition_lock.lock().await;
1576        if !registry.group_creator_matches_if_current(call_id, generation, call_creator) {
1577            return Err(CallError::Media(
1578                "call creator does not match the active group call",
1579            ));
1580        }
1581        let participant = self
1582            .client
1583            .lid()
1584            .ok_or(CallError::Media("no own LID"))?
1585            .to_non_ad();
1586        let target = Jid::new(call_id, Server::Call);
1587        self.send_group_control(
1588            call_id,
1589            build_raise_hand(
1590                call_id,
1591                &target,
1592                call_creator,
1593                &self.client.generate_request_id(),
1594                raised,
1595            )
1596            .map_err(|error| CallError::Response(error.to_string()))?,
1597        )
1598        .await?;
1599        if registry.set_raised_hand_if_current(call_id, generation, &participant, raised) {
1600            registry.send_call_event_if_current(
1601                call_id,
1602                generation,
1603                CallEvent::HandRaised {
1604                    participant,
1605                    raised,
1606                },
1607            );
1608            Ok(())
1609        } else {
1610            Err(CallError::Media(
1611                "call was replaced while applying group control",
1612            ))
1613        }
1614    }
1615
1616    /// Publish a screen-share start/stop transition.
1617    #[cfg(feature = "voip-runtime")]
1618    pub async fn set_screen_share(
1619        &self,
1620        call_id: &str,
1621        call_creator: &Jid,
1622        state: ScreenShareState,
1623        screen_share_id: Option<u32>,
1624    ) -> Result<(), CallError> {
1625        let generation = self
1626            .client
1627            .call_registry()
1628            .generation_of(call_id)
1629            .ok_or(CallError::Media("call is no longer active"))?;
1630        self.set_screen_share_for_generation(
1631            call_id,
1632            call_creator,
1633            generation,
1634            state,
1635            screen_share_id,
1636        )
1637        .await
1638    }
1639
1640    #[cfg(feature = "voip-runtime")]
1641    pub(crate) async fn set_screen_share_for_generation(
1642        &self,
1643        call_id: &str,
1644        call_creator: &Jid,
1645        generation: u64,
1646        state: ScreenShareState,
1647        screen_share_id: Option<u32>,
1648    ) -> Result<(), CallError> {
1649        let registry = self.client.call_registry();
1650        let transition_lock = registry
1651            .group_transition_lock(call_id, generation)
1652            .ok_or(CallError::Media("call is no longer active"))?;
1653        let _transition_guard = transition_lock.lock().await;
1654        if !registry.group_creator_matches_if_current(call_id, generation, call_creator) {
1655            return Err(CallError::Media(
1656                "call creator does not match the active group call",
1657            ));
1658        }
1659        let group = registry
1660            .group_state_if_current(call_id, generation)
1661            .ok_or(CallError::Media("call is not an active group call"))?;
1662        if state == ScreenShareState::Started
1663            && (group
1664                .snapshot()
1665                .is_none_or(|snapshot| snapshot.media != "video")
1666                || !matches!(
1667                    registry.video_states(call_id, generation),
1668                    Some((VideoState::Enabled, _))
1669                ))
1670        {
1671            return Err(CallError::Media(
1672                "screen sharing requires an active local video plane",
1673            ));
1674        }
1675        let participant = self
1676            .client
1677            .lid()
1678            .ok_or(CallError::Media("no own LID"))?
1679            .to_non_ad();
1680        let target = Jid::new(call_id, Server::Call);
1681        self.send_group_control(
1682            call_id,
1683            build_screen_share(
1684                call_id,
1685                &target,
1686                call_creator,
1687                &self.client.generate_request_id(),
1688                state,
1689                screen_share_id,
1690            )
1691            .map_err(|error| CallError::Response(error.to_string()))?,
1692        )
1693        .await?;
1694        let screen_share = ScreenShare::new(state, screen_share_id);
1695        if registry.set_screen_share_if_current(
1696            call_id,
1697            generation,
1698            &participant,
1699            screen_share.clone(),
1700        ) {
1701            registry.send_call_event_if_current(
1702                call_id,
1703                generation,
1704                CallEvent::ScreenShareChanged {
1705                    participant,
1706                    screen_share,
1707                },
1708            );
1709        } else {
1710            return Err(CallError::Media(
1711                "call was replaced while applying group control",
1712            ));
1713        }
1714        // Both directions swap the encoder source, so the peer needs an IDR before either stream
1715        // can safely resume.
1716        registry.send_video_ctl(call_id, generation, VideoControl::RequireKeyframe);
1717        Ok(())
1718    }
1719
1720    #[cfg(feature = "voip-runtime")]
1721    async fn send_group_control(&self, call_id: &str, node: Node) -> Result<(), CallError> {
1722        if call_id.is_empty() {
1723            return Err(CallError::EmptyCallId);
1724        }
1725        self.client.send_node(node).await?;
1726        Ok(())
1727    }
1728
1729    #[cfg(feature = "voip-runtime")]
1730    fn ensure_waiting_room_admin_if_current(
1731        &self,
1732        call_id: &str,
1733        generation: u64,
1734    ) -> Result<(), CallError> {
1735        let room = self
1736            .client
1737            .call_registry()
1738            .group_state_if_current(call_id, generation)
1739            .and_then(|state| state.waiting_room().cloned())
1740            .ok_or(CallError::Media("call has no waiting-room state"))?;
1741        if !room.is_admin {
1742            return Err(CallError::Media(
1743                "waiting-room control requires an administrator",
1744            ));
1745        }
1746        Ok(())
1747    }
1748
1749    #[cfg(feature = "voip-runtime")]
1750    fn start_waiting_room_heartbeat(&self, call_id: String, call_creator: Jid, generation: u64) {
1751        let weak_client = self.client.self_weak.get().cloned().unwrap_or_default();
1752        let runtime = self.client.runtime.clone();
1753        let sleeper = runtime.clone();
1754        let heartbeat_call_id = call_id.clone();
1755        let task = runtime.spawn(Box::pin(async move {
1756            let mut consecutive_failures = 0;
1757            loop {
1758                sleeper.sleep(WAITING_ROOM_HEARTBEAT_INTERVAL).await;
1759                let Some(client) = weak_client.upgrade() else {
1760                    break;
1761                };
1762                if client
1763                    .call_registry()
1764                    .phase_if_current(&heartbeat_call_id, generation)
1765                    != Some(CallPhase::WaitingRoom)
1766                {
1767                    break;
1768                }
1769                let request_id = client.generate_request_id();
1770                let heartbeat = match build_waiting_room_heartbeat(
1771                    &heartbeat_call_id,
1772                    &call_creator,
1773                    &request_id,
1774                ) {
1775                    Ok(heartbeat) => heartbeat,
1776                    Err(error) => {
1777                        warn!(
1778                            "voip: invalid waiting-room heartbeat for call {}: {error}",
1779                            heartbeat_call_id
1780                        );
1781                        break;
1782                    }
1783                };
1784                if let Err(error) = client
1785                    .send_node(heartbeat)
1786                    .await
1787                {
1788                    consecutive_failures += 1;
1789                    warn!(
1790                        "voip: waiting-room heartbeat failed for call {} ({consecutive_failures}/{WAITING_ROOM_HEARTBEAT_MAX_CONSECUTIVE_FAILURES}): {error}",
1791                        heartbeat_call_id,
1792                    );
1793                    if consecutive_failures >= WAITING_ROOM_HEARTBEAT_MAX_CONSECUTIVE_FAILURES {
1794                        client.call_registry().send_call_event_if_current(
1795                            &heartbeat_call_id,
1796                            generation,
1797                            CallEvent::WaitingRoomHeartbeatFailed,
1798                        );
1799                        client
1800                            .call_registry()
1801                            .remove_if_current(&heartbeat_call_id, generation);
1802                        break;
1803                    }
1804                    continue;
1805                }
1806                consecutive_failures = 0;
1807            }
1808        }));
1809        self.client
1810            .call_registry()
1811            .set_waiting_room_task(&call_id, generation, task);
1812    }
1813
1814    /// Terminate an active call.
1815    pub async fn terminate(
1816        &self,
1817        call_id: &str,
1818        peer: &Jid,
1819        call_creator: &Jid,
1820    ) -> Result<(), CallError> {
1821        if call_id.is_empty() {
1822            return Err(CallError::EmptyCallId);
1823        }
1824        let id = self.client.generate_request_id();
1825        let stanza = build_terminate(&TerminateParams {
1826            call_id,
1827            to: peer,
1828            id: Some(&id),
1829            call_creator,
1830            reason: None,
1831        });
1832        let sent = self.client.send_node(stanza).await;
1833        // Tear the local call down regardless of whether the stanza reached the peer: the app asked to
1834        // hang up, and a failed signaling send must not leave the media task capturing/sending (or a
1835        // dormant outgoing call free to attach on a late relay ack). Reuse the same teardown the peer's
1836        // `<terminate>` triggers so the public hangup actually ends our side too.
1837        #[cfg(feature = "voip-runtime")]
1838        crate::voip::facade::terminate_call(self.client, call_id);
1839        sent?;
1840        Ok(())
1841    }
1842}
1843
1844#[cfg(feature = "voip-runtime")]
1845fn normalize_call_link_token(
1846    token_or_url: &str,
1847    expected_media: CallLinkMedia,
1848) -> Result<String, CallError> {
1849    let value = token_or_url.trim();
1850    if value.is_empty() {
1851        return Err(CallError::Response(
1852            "call-link token is required".to_string(),
1853        ));
1854    }
1855    const PREFIX: &str = "https://call.whatsapp.com/";
1856    if let Some(path) = value.strip_prefix(PREFIX) {
1857        let path = path.split_once(['?', '#']).map_or(path, |(path, _)| path);
1858        let mut parts = path.split('/');
1859        let media = parts.next();
1860        let Some(token) = parts.next().filter(|token| !token.is_empty()) else {
1861            return Err(CallError::Response(
1862                "invalid call-link URL or media mode".to_string(),
1863            ));
1864        };
1865        if parts.next().is_some() || media != Some(expected_media.as_str()) {
1866            return Err(CallError::Response(
1867                "invalid call-link URL or media mode".to_string(),
1868            ));
1869        }
1870        return Ok(token.to_string());
1871    }
1872    if value.contains("://") || value.contains('/') {
1873        return Err(CallError::Response("invalid call-link token".to_string()));
1874    }
1875    Ok(value.to_string())
1876}
1877
1878#[cfg(feature = "voip-runtime")]
1879#[inline(never)]
1880async fn execute_call_link_join_request(
1881    client: &Client,
1882    token: &str,
1883    media: CallLinkMedia,
1884    capability: &[u8],
1885) -> Result<CallLinkJoin, CallError> {
1886    let request_id = client.generate_request_id();
1887    let request = build_call_link_join_with_capability(token, media, &request_id, capability)
1888        .map_err(|error| CallError::Response(error.to_string()))?;
1889    execute_call_service_request(client, &request_id, request, |response| {
1890        parse_call_link_join_ack(response, token)
1891    })
1892    .await
1893}
1894
1895#[cfg(feature = "voip-runtime")]
1896async fn execute_call_service_request<T>(
1897    client: &Client,
1898    request_id: &str,
1899    request: Node,
1900    parse: impl FnOnce(&wacore_binary::NodeRef<'_>) -> anyhow::Result<T>,
1901) -> Result<T, CallError> {
1902    let (tx, response) = futures::channel::oneshot::channel();
1903    let cleanup_generation = client
1904        .response_waiters_guard()
1905        .try_insert_guarded(request_id.to_string(), ResponseWaiter::Iq(tx))
1906        .ok_or_else(|| CallError::Response("duplicate call-service request id".to_string()))?;
1907    let _waiter_guard = crate::request::ResponseWaiterGuard::new(
1908        client.response_waiters.clone(),
1909        request_id.to_string(),
1910        cleanup_generation,
1911    );
1912    client.send_node(request).await?;
1913    let response =
1914        match wacore::runtime::timeout(&*client.runtime, CALL_SERVICE_REQUEST_TIMEOUT, response)
1915            .await
1916        {
1917            Ok(Ok(response)) => response,
1918            Ok(Err(_)) => return Err(CallError::Response("response channel closed".to_string())),
1919            Err(_) => return Err(CallError::ResponseTimeout),
1920        };
1921    parse(response.get()).map_err(|error| CallError::Response(error.to_string()))
1922}
1923
1924#[cfg(test)]
1925mod tests {
1926    #[cfg(feature = "voip-runtime")]
1927    use super::PendingCallLinkBuffer;
1928    use std::sync::Arc;
1929    use std::sync::atomic::{AtomicUsize, Ordering};
1930    #[cfg(feature = "voip-runtime")]
1931    use std::time::Duration;
1932
1933    use async_trait::async_trait;
1934    use bytes::Bytes;
1935    use wacore::handshake::NoiseCipher;
1936    use wacore::types::call::{CallAction, IncomingCall};
1937    #[cfg(feature = "voip-runtime")]
1938    use wacore::types::group_call::{
1939        CallLinkMedia, GroupCallDevice, GroupCallParticipant, GroupCallRelay,
1940        GroupCallRelayEndpoint, GroupCallUpdate, ScreenShareState, WaitingRoom,
1941    };
1942    #[cfg(feature = "voip-runtime")]
1943    use wacore::voip::{
1944        AudioFormat, CallEvent, CallPhase, CallSession, VideoControl, video_control_channel,
1945    };
1946    #[cfg(feature = "voip-runtime")]
1947    use wacore_binary::builder::NodeBuilder;
1948    use wacore_binary::{Jid, Server};
1949
1950    #[cfg(feature = "voip-runtime")]
1951    use super::{
1952        MAX_PENDING_CALL_LINK_SATURATION_FINGERPRINTS, MAX_PENDING_CALL_LINK_TRANSITION_BYTES,
1953        MAX_PENDING_CALL_LINK_TRANSITIONS, WaitingRoomUserAction,
1954    };
1955    use crate::client::Client;
1956    #[cfg(feature = "voip-runtime")]
1957    use crate::client::{CallError, ResponseWaiter};
1958
1959    #[cfg(feature = "voip-runtime")]
1960    #[test]
1961    fn call_link_urls_strip_query_and_fragment_without_relaxing_validation() {
1962        assert_eq!(
1963            super::normalize_call_link_token(
1964                "https://call.whatsapp.com/video/TEST-TOKEN?utm_source=test#join",
1965                CallLinkMedia::Video,
1966            )
1967            .unwrap(),
1968            "TEST-TOKEN"
1969        );
1970        assert!(
1971            super::normalize_call_link_token(
1972                "https://call.whatsapp.com/audio/TEST-TOKEN?x=1",
1973                CallLinkMedia::Video,
1974            )
1975            .is_err()
1976        );
1977        assert!(
1978            super::normalize_call_link_token(
1979                "https://call.whatsapp.com/video/?x=1",
1980                CallLinkMedia::Video,
1981            )
1982            .is_err()
1983        );
1984    }
1985
1986    struct CountingTransport {
1987        count: Arc<AtomicUsize>,
1988    }
1989
1990    #[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
1991    #[cfg_attr(not(target_arch = "wasm32"), async_trait)]
1992    impl crate::transport::Transport for CountingTransport {
1993        async fn send(&self, _data: Bytes) -> Result<(), anyhow::Error> {
1994            self.count.fetch_add(1, Ordering::SeqCst);
1995            Ok(())
1996        }
1997        async fn disconnect(&self) {}
1998    }
1999
2000    async fn make_client_with_count() -> (Arc<Client>, Arc<AtomicUsize>) {
2001        let client = crate::test_utils::create_test_client().await;
2002
2003        let count = Arc::new(AtomicUsize::new(0));
2004        let socket_transport: Arc<dyn crate::transport::Transport> = Arc::new(CountingTransport {
2005            count: count.clone(),
2006        });
2007        let key = [0u8; 32];
2008        let noise_socket = crate::socket::NoiseSocket::new(
2009            Arc::new(crate::runtime_impl::TokioRuntime),
2010            socket_transport,
2011            NoiseCipher::new(&key).expect("valid key"),
2012            NoiseCipher::new(&key).expect("valid key"),
2013        );
2014        *client.noise_socket.lock().await = Some(Arc::new(noise_socket));
2015        (client, count)
2016    }
2017
2018    #[cfg(feature = "voip-runtime")]
2019    struct FailingTransport;
2020
2021    #[cfg(feature = "voip-runtime")]
2022    #[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
2023    #[cfg_attr(not(target_arch = "wasm32"), async_trait)]
2024    impl crate::transport::Transport for FailingTransport {
2025        async fn send(&self, _data: Bytes) -> Result<(), anyhow::Error> {
2026            Err(anyhow::anyhow!("transport down"))
2027        }
2028        async fn disconnect(&self) {}
2029    }
2030
2031    #[cfg(feature = "voip-runtime")]
2032    async fn make_client_failing() -> Arc<Client> {
2033        let client = crate::test_utils::create_test_client().await;
2034        let socket_transport: Arc<dyn crate::transport::Transport> = Arc::new(FailingTransport);
2035        let key = [0u8; 32];
2036        let noise_socket = crate::socket::NoiseSocket::new(
2037            Arc::new(crate::runtime_impl::TokioRuntime),
2038            socket_transport,
2039            NoiseCipher::new(&key).expect("valid key"),
2040            NoiseCipher::new(&key).expect("valid key"),
2041        );
2042        *client.noise_socket.lock().await = Some(Arc::new(noise_socket));
2043        client
2044    }
2045
2046    fn caller() -> Jid {
2047        Jid::new("111111111111111", Server::Lid)
2048    }
2049
2050    fn call_creator() -> Jid {
2051        Jid::new("222222222222222", Server::Lid)
2052    }
2053
2054    fn incoming_reject() -> IncomingCall {
2055        IncomingCall::new_for_test(
2056            caller(),
2057            "STANZA-ID-0001".into(),
2058            wacore::time::from_secs(1_766_847_151_i64).expect("valid ts"),
2059            CallAction::Offer {
2060                call_id: "CALL-ID-0001".into(),
2061                call_creator: caller(),
2062                caller_pn: None,
2063                caller_country_code: None,
2064                device_class: None,
2065                joinable: false,
2066                is_video: false,
2067                audio: Vec::new(),
2068                group_jid: None,
2069            },
2070        )
2071    }
2072
2073    #[tokio::test]
2074    async fn reject_sends_stanza() {
2075        let (client, count) = make_client_with_count().await;
2076        client
2077            .voip()
2078            .reject(&incoming_reject())
2079            .await
2080            .expect("reject should send");
2081        assert_eq!(count.load(Ordering::SeqCst), 1);
2082    }
2083
2084    #[tokio::test]
2085    async fn reject_call_sends_stanza_without_event_context() {
2086        let (client, count) = make_client_with_count().await;
2087        let waiter = client.wait_for_sent_node(crate::client::NodeFilter::tag("call"));
2088        let peer = caller();
2089        let creator = call_creator();
2090        client
2091            .voip()
2092            .reject_call("CALL-ID-0001", &peer, &creator)
2093            .await
2094            .expect("reject should send");
2095        assert_eq!(count.load(Ordering::SeqCst), 1);
2096
2097        let sent = waiter.await.expect("reject stanza should be observable");
2098        let call = sent.as_node_ref();
2099        assert_eq!(
2100            call.attrs().optional_string("to").as_deref(),
2101            Some(peer.to_string().as_str())
2102        );
2103        let reject = &call.children().expect("call action")[0];
2104        assert_eq!(reject.tag, "reject");
2105        assert_eq!(
2106            reject.attrs().optional_string("call-id").as_deref(),
2107            Some("CALL-ID-0001")
2108        );
2109        assert_eq!(
2110            reject.attrs().optional_string("call-creator").as_deref(),
2111            Some(creator.to_string().as_str())
2112        );
2113        assert_eq!(
2114            reject.attrs().optional_string("count").as_deref(),
2115            Some("0")
2116        );
2117    }
2118
2119    #[cfg(feature = "voip-runtime")]
2120    #[tokio::test]
2121    async fn rejecting_an_incoming_group_offer_removes_its_ringing_generation() {
2122        let (client, _count) = make_client_with_count().await;
2123        let creator = caller();
2124        let call_id = "INCOMING-GROUP-CALL";
2125        let mut session = CallSession::new_incoming(call_id, creator.clone(), creator.clone());
2126        session.group = Some(
2127            GroupCallUpdate::builder()
2128                .call_id(call_id.to_string())
2129                .call_creator(creator.clone())
2130                .transaction_id(1)
2131                .media("audio".to_string())
2132                .connected_limit(32)
2133                .joinable(true)
2134                .av_upgradable(true)
2135                .rekey_requested(false)
2136                .participants(Vec::new())
2137                .build(),
2138        );
2139        let generation = client
2140            .call_registry()
2141            .insert_ringing_group_if_inactive(session)
2142            .expect("valid group snapshot")
2143            .expect("ringing generation");
2144
2145        client
2146            .voip()
2147            .reject_call(call_id, &creator, &creator)
2148            .await
2149            .expect("reject");
2150
2151        assert_ne!(
2152            client.call_registry().generation_of(call_id),
2153            Some(generation),
2154            "reject must reap the exact eagerly registered group offer"
2155        );
2156    }
2157
2158    #[cfg(feature = "voip-runtime")]
2159    #[tokio::test]
2160    async fn rejecting_a_stale_group_offer_event_preserves_the_replacement_generation() {
2161        let (client, count) = make_client_with_count().await;
2162        let creator = caller();
2163        let call_id = "REPLACED-INCOMING-GROUP-CALL";
2164        let update = GroupCallUpdate::builder()
2165            .call_id(call_id.to_string())
2166            .call_creator(creator.clone())
2167            .transaction_id(1)
2168            .media("audio".to_string())
2169            .connected_limit(32)
2170            .joinable(true)
2171            .av_upgradable(true)
2172            .rekey_requested(false)
2173            .participants(Vec::new())
2174            .build();
2175        let mut session = CallSession::new_incoming(call_id, creator.clone(), creator.clone());
2176        session.group = Some(update.clone());
2177        let stale = client
2178            .call_registry()
2179            .insert_ringing_group_if_inactive(session)
2180            .expect("valid group snapshot")
2181            .expect("ringing generation");
2182        let mut incoming = IncomingCall::new_for_test(
2183            creator.clone(),
2184            "STALE-GROUP-OFFER".to_string(),
2185            wacore::time::from_secs(1_766_847_151_i64).expect("valid ts"),
2186            CallAction::Offer {
2187                call_id: call_id.to_string(),
2188                call_creator: creator.clone(),
2189                caller_pn: None,
2190                caller_country_code: None,
2191                device_class: None,
2192                joinable: true,
2193                is_video: false,
2194                audio: Vec::new(),
2195                group_jid: None,
2196            },
2197        );
2198        incoming.group = Some(Box::new(update.clone()));
2199        incoming.set_ringing_generation(stale);
2200
2201        let mut replacement = CallSession::new_incoming(call_id, creator.clone(), creator.clone());
2202        replacement.group = Some(update);
2203        let replacement = client.call_registry().insert_ringing_group(replacement);
2204
2205        assert!(matches!(
2206            client.voip().reject(&incoming).await,
2207            Err(CallError::CallEndedDuringSetup)
2208        ));
2209        assert_eq!(
2210            count.load(Ordering::SeqCst),
2211            0,
2212            "a stale application event must not reject the replacement on the wire"
2213        );
2214
2215        assert_eq!(
2216            client.call_registry().generation_of(call_id),
2217            Some(replacement),
2218            "a retained application event must not reap a newer same-id generation"
2219        );
2220        assert_eq!(
2221            client
2222                .call_registry()
2223                .ringing_group_generation(call_id, &creator),
2224            Some(replacement),
2225            "the newer offer must remain available for the application to answer or reject"
2226        );
2227        client
2228            .call_registry()
2229            .remove_if_current(call_id, replacement);
2230        client.call_registry().take_ringing(call_id);
2231    }
2232
2233    #[tokio::test]
2234    async fn terminate_sends_stanza() {
2235        let (client, count) = make_client_with_count().await;
2236        client
2237            .voip()
2238            .terminate("CALL-ID-0001", &caller(), &caller())
2239            .await
2240            .expect("terminate should send");
2241        assert_eq!(count.load(Ordering::SeqCst), 1);
2242    }
2243
2244    #[cfg(feature = "voip-runtime")]
2245    #[tokio::test]
2246    async fn terminate_aborts_the_local_call() {
2247        use wacore::voip::CallSession;
2248        let (client, _count) = make_client_with_count().await;
2249        let reg = client.call_registry();
2250        reg.insert(CallSession::new_outgoing(
2251            "CALL-ID-0001",
2252            caller(),
2253            caller(),
2254        ));
2255        assert_eq!(reg.active_count(), 1);
2256        client
2257            .voip()
2258            .terminate("CALL-ID-0001", &caller(), &caller())
2259            .await
2260            .expect("terminate should send");
2261        assert_eq!(
2262            reg.active_count(),
2263            0,
2264            "terminate must tear the local call down, not just signal the peer"
2265        );
2266    }
2267
2268    #[cfg(feature = "voip-runtime")]
2269    #[tokio::test]
2270    async fn terminate_tears_down_local_even_when_send_fails() {
2271        use wacore::voip::CallSession;
2272        let client = make_client_failing().await;
2273        let reg = client.call_registry();
2274        reg.insert(CallSession::new_outgoing(
2275            "CALL-ID-0001",
2276            caller(),
2277            caller(),
2278        ));
2279        assert_eq!(reg.active_count(), 1);
2280        let res = client
2281            .voip()
2282            .terminate("CALL-ID-0001", &caller(), &caller())
2283            .await;
2284        assert!(
2285            res.is_err(),
2286            "a failed signaling send must surface the error"
2287        );
2288        assert_eq!(
2289            reg.active_count(),
2290            0,
2291            "a failed signaling send must still tear the local media task down"
2292        );
2293    }
2294
2295    #[tokio::test]
2296    async fn reject_empty_call_id_errors() {
2297        let (client, _count) = make_client_with_count().await;
2298        let mut call = incoming_reject();
2299        call.action = CallAction::Reject {
2300            call_id: String::new(),
2301            call_creator: caller(),
2302            reason: None,
2303        };
2304        assert!(client.voip().reject(&call).await.is_err());
2305    }
2306
2307    #[cfg(feature = "voip-runtime")]
2308    #[tokio::test]
2309    async fn local_group_controls_commit_state_events_and_screen_keyframe_gate() {
2310        let (client, transport) = crate::test_utils::create_iq_test_client().await;
2311        let own_device = Jid::new("111111111111111", Server::Lid).with_device(1);
2312        client
2313            .persistence_manager()
2314            .process_command(crate::store::commands::DeviceCommand::SetLid(Some(
2315                own_device,
2316            )))
2317            .await;
2318        let participant = Jid::new("111111111111111", Server::Lid);
2319        let creator = participant.clone();
2320        let call_id = "TEST-GROUP-CONTROLS";
2321        let registry = client.call_registry();
2322        let mut session =
2323            CallSession::new_outgoing(call_id, Jid::new(call_id, Server::Call), creator.clone());
2324        session.group = Some(
2325            GroupCallUpdate::builder()
2326                .call_id(call_id.to_string())
2327                .call_creator(creator.clone())
2328                .transaction_id(1)
2329                .media("video".to_string())
2330                .connected_limit(32)
2331                .joinable(true)
2332                .av_upgradable(true)
2333                .rekey_requested(false)
2334                .participants(vec![GroupCallParticipant::new(
2335                    participant.clone(),
2336                    vec![GroupCallDevice::new(participant.clone().with_device(1))],
2337                )])
2338                .build(),
2339        );
2340        let generation = registry.insert(session);
2341        let (event_tx, event_rx) = async_channel::bounded(4);
2342        let (video_tx, video_rx) = video_control_channel();
2343        registry.set_video_channels(call_id, generation, event_tx, video_tx, Box::new(|| {}));
2344
2345        client
2346            .voip()
2347            .set_hand_raised(call_id, &creator, true)
2348            .await
2349            .expect("raise hand");
2350        assert!(
2351            registry
2352                .group_state(call_id)
2353                .expect("group state")
2354                .raised_hands()
2355                .contains(&participant)
2356        );
2357        assert!(matches!(
2358            event_rx.try_recv(),
2359            Ok(CallEvent::HandRaised {
2360                participant: event_participant,
2361                raised: true,
2362            }) if event_participant == participant
2363        ));
2364
2365        assert!(
2366            client
2367                .voip()
2368                .set_screen_share(call_id, &creator, ScreenShareState::Started, Some(7))
2369                .await
2370                .is_err(),
2371            "a call without a local video plane must not advertise an unsendable screen share"
2372        );
2373        assert_eq!(
2374            transport.sent_count(),
2375            1,
2376            "the rejected screen-share transition must stay off the wire"
2377        );
2378        assert!(registry.set_is_video(call_id, generation, true));
2379
2380        client
2381            .voip()
2382            .set_screen_share(call_id, &creator, ScreenShareState::Started, Some(7))
2383            .await
2384            .expect("start screen share");
2385        let share = registry
2386            .group_state(call_id)
2387            .expect("group state")
2388            .screen_shares()
2389            .get(&participant)
2390            .cloned()
2391            .expect("local screen share");
2392        assert_eq!(share.state, ScreenShareState::Started);
2393        assert_eq!(share.version, 2);
2394        assert_eq!(share.screen_share_id, Some(7));
2395        assert!(matches!(
2396            event_rx.try_recv(),
2397            Ok(CallEvent::ScreenShareChanged {
2398                participant: event_participant,
2399                screen_share,
2400            }) if event_participant == participant && screen_share == share
2401        ));
2402        assert_eq!(
2403            video_rx.try_recv(),
2404            Ok(VideoControl::RequireKeyframe),
2405            "starting a replacement screen source must re-arm the H.264 recovery gate"
2406        );
2407
2408        client
2409            .voip()
2410            .set_screen_share(call_id, &creator, ScreenShareState::Stopped, None)
2411            .await
2412            .expect("stop screen share");
2413        assert!(
2414            registry
2415                .group_state(call_id)
2416                .expect("group state")
2417                .screen_shares()
2418                .is_empty()
2419        );
2420        assert!(matches!(
2421            event_rx.try_recv(),
2422            Ok(CallEvent::ScreenShareChanged {
2423                participant: event_participant,
2424                screen_share,
2425            }) if event_participant == participant
2426                && screen_share.state == ScreenShareState::Stopped
2427        ));
2428        assert_eq!(
2429            video_rx.try_recv(),
2430            Ok(VideoControl::RequireKeyframe),
2431            "returning to the camera must re-arm the H.264 recovery gate"
2432        );
2433        assert_eq!(transport.sent_count(), 3);
2434
2435        let mut audio_only = registry
2436            .group_state_if_current(call_id, generation)
2437            .and_then(|state| state.snapshot().cloned())
2438            .expect("authoritative roster");
2439        audio_only.transaction_id = 2;
2440        audio_only.media = "audio".to_string();
2441        assert_eq!(
2442            registry.apply_group_update_if_current(audio_only, generation),
2443            wacore::voip::GroupStateApply::Applied
2444        );
2445        assert!(
2446            client
2447                .voip()
2448                .set_screen_share(call_id, &creator, ScreenShareState::Started, Some(8))
2449                .await
2450                .is_err(),
2451            "an authoritative audio downgrade must disable screen sharing even if local video was negotiated"
2452        );
2453        assert_eq!(
2454            transport.sent_count(),
2455            3,
2456            "the rejected post-downgrade transition must stay off the wire"
2457        );
2458
2459        let replacement_creator = Jid::new("222222222222222", Server::Lid);
2460        let mut replacement = CallSession::new_outgoing(
2461            call_id,
2462            Jid::new(call_id, Server::Call),
2463            replacement_creator.clone(),
2464        );
2465        replacement.group = Some(
2466            GroupCallUpdate::builder()
2467                .call_id(call_id.to_string())
2468                .call_creator(replacement_creator)
2469                .transaction_id(1)
2470                .media("video".to_string())
2471                .connected_limit(32)
2472                .joinable(true)
2473                .av_upgradable(true)
2474                .rekey_requested(false)
2475                .participants(vec![GroupCallParticipant::new(
2476                    participant,
2477                    vec![GroupCallDevice::new(
2478                        Jid::new("111111111111111", Server::Lid).with_device(1),
2479                    )],
2480                )])
2481                .build(),
2482        );
2483        let replacement_generation = registry.insert(replacement);
2484        assert!(
2485            client
2486                .voip()
2487                .set_hand_raised(call_id, &creator, true)
2488                .await
2489                .is_err(),
2490            "stale creator metadata cannot mutate a replacement generation"
2491        );
2492        assert_eq!(
2493            transport.sent_count(),
2494            3,
2495            "a stale group identity must be rejected before signaling"
2496        );
2497        registry.remove_if_current(call_id, replacement_generation);
2498    }
2499
2500    #[cfg(feature = "voip-runtime")]
2501    #[tokio::test]
2502    async fn local_group_controls_wait_for_the_authoritative_transition_lane() {
2503        let (client, transport) = crate::test_utils::create_iq_test_client().await;
2504        let own_device = Jid::new("111111111111111", Server::Lid).with_device(1);
2505        client
2506            .persistence_manager()
2507            .process_command(crate::store::commands::DeviceCommand::SetLid(Some(
2508                own_device,
2509            )))
2510            .await;
2511        let participant = Jid::new("111111111111111", Server::Lid);
2512        let creator = participant.clone();
2513        let call_id = "TEST-GROUP-CONTROL-SERIALIZATION";
2514        let registry = client.call_registry();
2515        let mut session =
2516            CallSession::new_outgoing(call_id, Jid::new(call_id, Server::Call), creator.clone());
2517        session.group = Some(
2518            GroupCallUpdate::builder()
2519                .call_id(call_id.to_string())
2520                .call_creator(creator.clone())
2521                .transaction_id(1)
2522                .media("video".to_string())
2523                .connected_limit(32)
2524                .joinable(true)
2525                .av_upgradable(true)
2526                .rekey_requested(false)
2527                .participants(vec![GroupCallParticipant::new(
2528                    participant,
2529                    vec![GroupCallDevice::new(
2530                        Jid::new("111111111111111", Server::Lid).with_device(1),
2531                    )],
2532                )])
2533                .build(),
2534        );
2535        let generation = registry.insert(session);
2536        assert!(registry.set_is_video(call_id, generation, true));
2537        let transition_lock = registry
2538            .group_transition_lock(call_id, generation)
2539            .expect("group transition lane");
2540
2541        let guard = transition_lock.lock().await;
2542        let hand_client = client.clone();
2543        let hand_creator = creator.clone();
2544        let hand = tokio::spawn(async move {
2545            hand_client
2546                .voip()
2547                .set_hand_raised_for_generation(call_id, &hand_creator, generation, true)
2548                .await
2549        });
2550        tokio::task::yield_now().await;
2551        assert_eq!(
2552            transport.sent_count(),
2553            0,
2554            "raise-hand signaling must wait for an authoritative transition"
2555        );
2556        drop(guard);
2557        hand.await
2558            .expect("raise-hand task")
2559            .expect("raise-hand transition");
2560
2561        let guard = transition_lock.lock().await;
2562        let screen_client = client.clone();
2563        let screen = tokio::spawn(async move {
2564            screen_client
2565                .voip()
2566                .set_screen_share_for_generation(
2567                    call_id,
2568                    &creator,
2569                    generation,
2570                    ScreenShareState::Started,
2571                    Some(7),
2572                )
2573                .await
2574        });
2575        tokio::task::yield_now().await;
2576        assert_eq!(
2577            transport.sent_count(),
2578            1,
2579            "screen-share signaling must wait for an authoritative transition"
2580        );
2581        drop(guard);
2582        screen
2583            .await
2584            .expect("screen-share task")
2585            .expect("screen-share transition");
2586        assert_eq!(transport.sent_count(), 2);
2587        registry.remove_if_current(call_id, generation);
2588    }
2589
2590    #[cfg(feature = "voip-runtime")]
2591    #[tokio::test]
2592    async fn direct_calls_reject_group_controls_before_sending() {
2593        let (client, transport) = crate::test_utils::create_iq_test_client().await;
2594        let own_device = Jid::new("111111111111111", Server::Lid).with_device(1);
2595        client
2596            .persistence_manager()
2597            .process_command(crate::store::commands::DeviceCommand::SetLid(Some(
2598                own_device,
2599            )))
2600            .await;
2601        let call_id = "TEST-DIRECT-CONTROLS";
2602        let creator = Jid::new("111111111111111", Server::Lid);
2603        let generation = client.call_registry().insert(CallSession::new_outgoing(
2604            call_id,
2605            Jid::new("222222222222222", Server::Lid),
2606            creator.clone(),
2607        ));
2608
2609        assert!(
2610            client
2611                .voip()
2612                .set_hand_raised(call_id, &creator, true)
2613                .await
2614                .is_err()
2615        );
2616        assert!(
2617            client
2618                .voip()
2619                .set_screen_share(call_id, &creator, ScreenShareState::Started, Some(7))
2620                .await
2621                .is_err()
2622        );
2623        assert_eq!(
2624            transport.sent_count(),
2625            0,
2626            "group-only controls must not be emitted for a direct call"
2627        );
2628        assert!(client.call_registry().group_state(call_id).is_none());
2629        client
2630            .call_registry()
2631            .remove_if_current(call_id, generation);
2632    }
2633
2634    #[cfg(feature = "voip-runtime")]
2635    #[tokio::test(start_paused = true)]
2636    async fn call_link_requests_round_trip_through_bounded_response_waiters() {
2637        async fn wait_for_frames(
2638            transport: &crate::transport::mock::CapturingMockTransport,
2639            expected: usize,
2640        ) {
2641            for _ in 0..10_000 {
2642                if transport.sent_count() >= expected {
2643                    return;
2644                }
2645                tokio::task::yield_now().await;
2646            }
2647            panic!("timed out waiting for {expected} captured call frames");
2648        }
2649
2650        let (client, transport) = crate::test_utils::create_iq_test_client().await;
2651        let own_lid = Jid::new("111111111111111", Server::Lid).with_device(1);
2652        client
2653            .persistence_manager()
2654            .process_command(crate::store::commands::DeviceCommand::SetLid(Some(
2655                own_lid.clone(),
2656            )))
2657            .await;
2658        let creator = Jid::new("333333333333333", Server::Lid);
2659
2660        let sent = client.wait_for_sent_node(crate::client::NodeFilter::tag("call"));
2661        let create_client = client.clone();
2662        let create = tokio::spawn(async move {
2663            create_client
2664                .voip()
2665                .create_call_link(CallLinkMedia::Video)
2666                .await
2667        });
2668        let request = sent.await.expect("link_create request");
2669        let request_id = request
2670            .as_node_ref()
2671            .attrs()
2672            .optional_string("id")
2673            .expect("request id")
2674            .into_owned();
2675        crate::test_utils::answer_iq(
2676            &client,
2677            &request_id,
2678            &NodeBuilder::new("ack")
2679                .attr("class", "call")
2680                .attr("type", "link_create")
2681                .attr("id", request_id.as_str())
2682                .children([NodeBuilder::new("link_create")
2683                    .attr("token", "TEST-CALL-LINK")
2684                    .attr("media", "video")
2685                    .build()])
2686                .build(),
2687        )
2688        .await;
2689        let link = create.await.expect("create task").expect("create response");
2690        assert_eq!(link.token, "TEST-CALL-LINK");
2691        assert_eq!(link.media, CallLinkMedia::Video);
2692
2693        let sent = client.wait_for_sent_node(crate::client::NodeFilter::tag("call"));
2694        let preview_client = client.clone();
2695        let preview = tokio::spawn(async move {
2696            preview_client
2697                .voip()
2698                .preview_call_link("TEST-CALL-LINK", CallLinkMedia::Video)
2699                .await
2700        });
2701        let request = sent.await.expect("link_query request");
2702        let request_id = request
2703            .as_node_ref()
2704            .attrs()
2705            .optional_string("id")
2706            .expect("request id")
2707            .into_owned();
2708        crate::test_utils::answer_iq(
2709            &client,
2710            &request_id,
2711            &NodeBuilder::new("ack")
2712                .attr("class", "call")
2713                .attr("type", "link_query")
2714                .attr("id", request_id.as_str())
2715                .children([NodeBuilder::new("link_query")
2716                    .attr("token", "TEST-CALL-LINK")
2717                    .attr("media", "video")
2718                    .attr("link_creator", creator.clone())
2719                    .children([NodeBuilder::new("waiting_room")
2720                        .attr("enabled", "1")
2721                        .attr("is_admin", "0")
2722                        .build()])
2723                    .build()])
2724                .build(),
2725        )
2726        .await;
2727        let preview = preview
2728            .await
2729            .expect("preview task")
2730            .expect("preview response");
2731        assert_eq!(preview.creator, creator);
2732        assert!(preview.waiting_room_enabled);
2733        assert!(!preview.is_admin);
2734
2735        let sent = client.wait_for_sent_node(crate::client::NodeFilter::tag("call"));
2736        let join_client = client.clone();
2737        let join = tokio::spawn(async move {
2738            join_client
2739                .voip()
2740                .join_call_link_with_audio(
2741                    "TEST-CALL-LINK",
2742                    CallLinkMedia::Video,
2743                    AudioFormat::OPUS_16KHZ_60MS,
2744                )
2745                .await
2746        });
2747        let request = sent.await.expect("link_join request");
2748        let request_ref = request.as_node_ref();
2749        let action = &request_ref.children().expect("join action children")[0];
2750        assert_eq!(
2751            action
2752                .get_optional_child("capability")
2753                .expect("join capability")
2754                .content_bytes(),
2755            Some(wacore::stanza::call::CAPABILITY_STANDARD_OPUS_VIDEO_OFFER.as_slice())
2756        );
2757        let request_id = request
2758            .as_node_ref()
2759            .attrs()
2760            .optional_string("id")
2761            .expect("request id")
2762            .into_owned();
2763        crate::test_utils::answer_iq(
2764            &client,
2765            &request_id,
2766            &NodeBuilder::new("ack")
2767                .attr("class", "call")
2768                .attr("type", "link_join")
2769                .attr("id", request_id.as_str())
2770                .children([NodeBuilder::new("waiting_room")
2771                    .attr("call-id", "TEST-CALL-ID")
2772                    .attr("call-creator", creator.clone())
2773                    .attr("link-token", "TEST-CALL-LINK")
2774                    .attr("media", "video")
2775                    .attr("enabled", "1")
2776                    .attr("is_admin", "0")
2777                    .attr("transaction-id", "7")
2778                    .children([NodeBuilder::new("user")
2779                        .attr("jid", Jid::new("444444444444444", Server::Lid))
2780                        .attr("state", "pending")
2781                        .build()])
2782                    .build()])
2783                .build(),
2784        )
2785        .await;
2786        let join = join.await.expect("join task").expect("join response");
2787        assert!(join.in_waiting_room);
2788        assert!(join.waiting_room_enabled);
2789        assert_eq!(join.call_id, "TEST-CALL-ID");
2790        assert!(join.group.is_none());
2791        assert_eq!(
2792            client.call_registry().phase("TEST-CALL-ID"),
2793            Some(CallPhase::WaitingRoom)
2794        );
2795        let room = client
2796            .call_registry()
2797            .group_state("TEST-CALL-ID")
2798            .and_then(|state| state.waiting_room().cloned())
2799            .expect("waiting-room state retained");
2800        assert_eq!(room.transaction_id, Some(7));
2801        assert_eq!(room.users.len(), 1);
2802
2803        wait_for_frames(&transport, 4).await;
2804        let immediate = crate::test_utils::decode_sent_iq(&transport, 3).await;
2805        let heartbeat = &immediate.get().children().expect("heartbeat action")[0];
2806        assert_eq!(heartbeat.tag, "heartbeat");
2807        assert_eq!(
2808            heartbeat.attrs().optional_string("type").as_deref(),
2809            Some("waiting_room")
2810        );
2811
2812        tokio::time::advance(Duration::from_secs(10)).await;
2813        wait_for_frames(&transport, 5).await;
2814        let scheduled = crate::test_utils::decode_sent_iq(&transport, 4).await;
2815        assert_eq!(
2816            scheduled.get().children().expect("heartbeat action")[0].tag,
2817            "heartbeat"
2818        );
2819
2820        let admitted = NodeBuilder::new("group_update")
2821            .attr("call-id", "TEST-CALL-ID")
2822            .attr("call-creator", creator)
2823            .children([NodeBuilder::new("group_info")
2824                .attr("transaction-id", "8")
2825                .attr("connected-limit", "32")
2826                .attr("media", "video")
2827                .children([NodeBuilder::new("user")
2828                    .attr("jid", own_lid.to_non_ad())
2829                    .attr("state", "connected")
2830                    .children([NodeBuilder::new("device").attr("jid", own_lid).build()])
2831                    .build()])
2832                .build()])
2833            .build();
2834        let update = wacore::stanza::group_call::parse_group_update(&admitted.as_node_ref())
2835            .expect("admitted group snapshot");
2836        assert_eq!(
2837            client.call_registry().apply_group_update(update),
2838            wacore::voip::GroupStateApply::Applied
2839        );
2840        assert_eq!(
2841            client.call_registry().phase("TEST-CALL-ID"),
2842            Some(CallPhase::Connecting)
2843        );
2844        let heartbeat_count = transport.sent_count();
2845        tokio::time::advance(Duration::from_secs(20)).await;
2846        tokio::task::yield_now().await;
2847        assert_eq!(
2848            transport.sent_count(),
2849            heartbeat_count,
2850            "admission must cancel the repeating heartbeat"
2851        );
2852        let generation = client
2853            .call_registry()
2854            .generation_of("TEST-CALL-ID")
2855            .expect("registered call-link generation");
2856        client
2857            .call_registry()
2858            .remove_if_current("TEST-CALL-ID", generation);
2859    }
2860
2861    #[cfg(feature = "voip-runtime")]
2862    #[tokio::test]
2863    async fn call_link_preview_rejects_a_changed_token_or_media() {
2864        let (client, _transport) = crate::test_utils::create_iq_test_client().await;
2865        let creator = Jid::new("333333333333333", Server::Lid);
2866        for (response_token, response_media) in
2867            [("OTHER-CALL-LINK", "video"), ("TEST-CALL-LINK", "audio")]
2868        {
2869            let sent = client.wait_for_sent_node(crate::client::NodeFilter::tag("call"));
2870            let preview_client = client.clone();
2871            let preview = tokio::spawn(async move {
2872                preview_client
2873                    .voip()
2874                    .preview_call_link("TEST-CALL-LINK", CallLinkMedia::Video)
2875                    .await
2876            });
2877            let request = sent.await.expect("link_query request");
2878            let request_id = request
2879                .as_node_ref()
2880                .attrs()
2881                .optional_string("id")
2882                .expect("request id")
2883                .into_owned();
2884            crate::test_utils::answer_iq(
2885                &client,
2886                &request_id,
2887                &NodeBuilder::new("ack")
2888                    .attr("class", "call")
2889                    .attr("type", "link_query")
2890                    .attr("id", request_id.as_str())
2891                    .children([NodeBuilder::new("link_query")
2892                        .attr("token", response_token)
2893                        .attr("media", response_media)
2894                        .attr("link_creator", creator.clone())
2895                        .build()])
2896                    .build(),
2897            )
2898            .await;
2899            assert!(matches!(
2900                preview.await.expect("preview task"),
2901                Err(CallError::Response(message))
2902                    if message == "call-link preview changed the requested link identity"
2903            ));
2904        }
2905    }
2906
2907    #[cfg(feature = "voip-runtime")]
2908    #[tokio::test]
2909    async fn call_link_creation_rejects_a_changed_media_mode() {
2910        let (client, _transport) = crate::test_utils::create_iq_test_client().await;
2911        let sent = client.wait_for_sent_node(crate::client::NodeFilter::tag("call"));
2912        let create_client = client.clone();
2913        let create = tokio::spawn(async move {
2914            create_client
2915                .voip()
2916                .create_call_link(CallLinkMedia::Video)
2917                .await
2918        });
2919        let request = sent.await.expect("link_create request");
2920        let request_id = request
2921            .as_node_ref()
2922            .attrs()
2923            .optional_string("id")
2924            .expect("request id")
2925            .into_owned();
2926        crate::test_utils::answer_iq(
2927            &client,
2928            &request_id,
2929            &NodeBuilder::new("ack")
2930                .attr("class", "call")
2931                .attr("type", "link_create")
2932                .attr("id", request_id.as_str())
2933                .children([NodeBuilder::new("link_create")
2934                    .attr("token", "TEST-CALL-LINK")
2935                    .attr("media", "audio")
2936                    .build()])
2937                .build(),
2938        )
2939        .await;
2940        assert!(matches!(
2941            create.await.expect("create task"),
2942            Err(CallError::Response(message))
2943                if message == "call-link creation changed the requested media mode"
2944        ));
2945    }
2946
2947    #[cfg(feature = "voip-runtime")]
2948    #[tokio::test]
2949    async fn approval_ack_cannot_commit_to_a_replacement_generation() {
2950        let (client, _transport) = crate::test_utils::create_iq_test_client().await;
2951        let call_id = "TEST-APPROVAL-GENERATION";
2952        let creator = Jid::new("333333333333333", Server::Lid);
2953        let registry = client.call_registry();
2954        let first = registry
2955            .insert_call_link_checked(CallSession::new_outgoing(
2956                call_id,
2957                Jid::new(call_id, Server::Call),
2958                creator.clone(),
2959            ))
2960            .expect("valid call-link session");
2961        assert_eq!(
2962            registry.apply_waiting_room(
2963                WaitingRoom::builder()
2964                    .call_id(call_id.to_string())
2965                    .call_creator(creator.clone())
2966                    .link_token("TEST-CALL-LINK".to_string())
2967                    .media(CallLinkMedia::Audio)
2968                    .enabled(false)
2969                    .is_admin(true)
2970                    .transaction_id(1)
2971                    .users(Vec::new())
2972                    .build(),
2973            ),
2974            wacore::voip::GroupStateApply::Applied
2975        );
2976
2977        let sent = client.wait_for_sent_node(crate::client::NodeFilter::tag("call"));
2978        let request_client = client.clone();
2979        let request_creator = creator.clone();
2980        let toggle = tokio::spawn(async move {
2981            request_client
2982                .voip()
2983                .set_approval_required(call_id, &request_creator, true)
2984                .await
2985        });
2986        let request = sent.await.expect("waiting-room toggle request");
2987        let request_id = request
2988            .as_node_ref()
2989            .attrs()
2990            .optional_string("id")
2991            .expect("request id")
2992            .into_owned();
2993
2994        let replacement = registry.insert(CallSession::new_outgoing(
2995            call_id,
2996            Jid::new(call_id, Server::Call),
2997            creator,
2998        ));
2999        assert_ne!(replacement, first);
3000        crate::test_utils::answer_iq(
3001            &client,
3002            &request_id,
3003            &NodeBuilder::new("ack")
3004                .attr("class", "call")
3005                .attr("type", "waiting_room_toggle")
3006                .attr("id", request_id.as_str())
3007                .build(),
3008        )
3009        .await;
3010
3011        assert!(matches!(
3012            toggle.await.expect("toggle task"),
3013            Err(CallError::Media(
3014                "call was replaced while applying group control"
3015            ))
3016        ));
3017        assert!(
3018            registry.group_state(call_id).is_none(),
3019            "the stale ACK must not synthesize waiting-room state on the replacement"
3020        );
3021        registry.remove_if_current(call_id, replacement);
3022    }
3023
3024    #[cfg(feature = "voip-runtime")]
3025    #[tokio::test]
3026    async fn approval_toggle_serializes_with_authoritative_waiting_room_updates() {
3027        let (client, _transport) = crate::test_utils::create_iq_test_client().await;
3028        let call_id = "TEST-APPROVAL-SERIALIZATION";
3029        let creator = Jid::new("333333333333333", Server::Lid);
3030        let registry = client.call_registry();
3031        let generation = registry
3032            .insert_call_link_checked(CallSession::new_outgoing(
3033                call_id,
3034                Jid::new(call_id, Server::Call),
3035                creator.clone(),
3036            ))
3037            .expect("valid call-link session");
3038        let room = |transaction_id, enabled| {
3039            WaitingRoom::builder()
3040                .call_id(call_id.to_string())
3041                .call_creator(creator.clone())
3042                .link_token("TEST-CALL-LINK".to_string())
3043                .media(CallLinkMedia::Audio)
3044                .enabled(enabled)
3045                .is_admin(true)
3046                .transaction_id(transaction_id)
3047                .users(Vec::new())
3048                .build()
3049        };
3050        assert_eq!(
3051            registry.apply_waiting_room(room(1, false)),
3052            wacore::voip::GroupStateApply::Applied
3053        );
3054        let transition_lock = registry
3055            .group_transition_lock(call_id, generation)
3056            .expect("active group transition");
3057
3058        let sent = client.wait_for_sent_node(crate::client::NodeFilter::tag("call"));
3059        let request_client = client.clone();
3060        let request_creator = creator.clone();
3061        let toggle = tokio::spawn(async move {
3062            request_client
3063                .voip()
3064                .set_approval_required_for_generation(call_id, &request_creator, generation, true)
3065                .await
3066        });
3067        let request = sent.await.expect("waiting-room toggle request");
3068        let request_id = request
3069            .as_node_ref()
3070            .attrs()
3071            .optional_string("id")
3072            .expect("request id")
3073            .into_owned();
3074
3075        let update_registry = registry.clone();
3076        let update = room(2, false);
3077        let authoritative = tokio::spawn(async move {
3078            let _guard = transition_lock.lock().await;
3079            update_registry.apply_waiting_room_if_current(update, generation)
3080        });
3081        tokio::task::yield_now().await;
3082        assert!(
3083            !authoritative.is_finished(),
3084            "the authoritative snapshot must wait for the toggle ACK and local commit"
3085        );
3086
3087        crate::test_utils::answer_iq(
3088            &client,
3089            &request_id,
3090            &NodeBuilder::new("ack")
3091                .attr("class", "call")
3092                .attr("type", "waiting_room_toggle")
3093                .attr("id", request_id.as_str())
3094                .build(),
3095        )
3096        .await;
3097        toggle.await.expect("toggle task").expect("toggle response");
3098        assert_eq!(
3099            authoritative.await.expect("authoritative update task"),
3100            wacore::voip::GroupStateApply::Applied
3101        );
3102        assert!(
3103            registry
3104                .group_state_if_current(call_id, generation)
3105                .and_then(|state| state.waiting_room().cloned())
3106                .is_some_and(|room| room.transaction_id == Some(2) && !room.enabled),
3107            "the newer authoritative snapshot must win after the serialized local toggle"
3108        );
3109        registry.remove_if_current(call_id, generation);
3110    }
3111
3112    #[cfg(feature = "voip-runtime")]
3113    #[tokio::test]
3114    async fn waiting_room_user_acks_are_bound_to_the_originating_generation() {
3115        let user = Jid::new("444444444444444", Server::Lid);
3116        for (index, action) in [WaitingRoomUserAction::Admit, WaitingRoomUserAction::Deny]
3117            .into_iter()
3118            .enumerate()
3119        {
3120            let (client, _transport) = crate::test_utils::create_iq_test_client().await;
3121            let call_id = format!("TEST-WAITING-ACTION-{index}");
3122            let creator = Jid::new("333333333333333", Server::Lid);
3123            let registry = client.call_registry();
3124            let first = registry
3125                .insert_call_link_checked(CallSession::new_outgoing(
3126                    &call_id,
3127                    Jid::new(&call_id, Server::Call),
3128                    creator.clone(),
3129                ))
3130                .expect("valid call-link session");
3131            assert_eq!(
3132                registry.apply_waiting_room(
3133                    WaitingRoom::builder()
3134                        .call_id(call_id.clone())
3135                        .call_creator(creator.clone())
3136                        .link_token("TEST-CALL-LINK".to_string())
3137                        .media(CallLinkMedia::Audio)
3138                        .enabled(true)
3139                        .is_admin(true)
3140                        .transaction_id(1)
3141                        .users(Vec::new())
3142                        .build(),
3143                ),
3144                wacore::voip::GroupStateApply::Applied
3145            );
3146
3147            let sent = client.wait_for_sent_node(crate::client::NodeFilter::tag("call"));
3148            let request_client = client.clone();
3149            let request_call_id = call_id.clone();
3150            let request_creator = creator.clone();
3151            let request_user = user.clone();
3152            let control = tokio::spawn(async move {
3153                match action {
3154                    WaitingRoomUserAction::Admit => {
3155                        request_client
3156                            .voip()
3157                            .admit_waiting_user_for_generation(
3158                                &request_call_id,
3159                                &request_creator,
3160                                first,
3161                                &request_user,
3162                            )
3163                            .await
3164                    }
3165                    WaitingRoomUserAction::Deny => {
3166                        request_client
3167                            .voip()
3168                            .deny_waiting_user_for_generation(
3169                                &request_call_id,
3170                                &request_creator,
3171                                first,
3172                                &request_user,
3173                            )
3174                            .await
3175                    }
3176                }
3177            });
3178            let request = sent.await.expect("waiting-room user request");
3179            let request_id = request
3180                .as_node_ref()
3181                .attrs()
3182                .optional_string("id")
3183                .expect("request id")
3184                .into_owned();
3185
3186            let replacement = registry.insert(CallSession::new_outgoing(
3187                &call_id,
3188                Jid::new(&call_id, Server::Call),
3189                creator,
3190            ));
3191            assert_ne!(replacement, first);
3192            let action_type = match action {
3193                WaitingRoomUserAction::Admit => "waiting_room_admit",
3194                WaitingRoomUserAction::Deny => "waiting_room_deny",
3195            };
3196            crate::test_utils::answer_iq(
3197                &client,
3198                &request_id,
3199                &NodeBuilder::new("ack")
3200                    .attr("class", "call")
3201                    .attr("type", action_type)
3202                    .attr("id", request_id.as_str())
3203                    .build(),
3204            )
3205            .await;
3206
3207            assert!(matches!(
3208                control.await.expect("waiting-room control task"),
3209                Err(CallError::Media(
3210                    "call was replaced while applying group control"
3211                ))
3212            ));
3213            registry.remove_if_current(&call_id, replacement);
3214        }
3215    }
3216
3217    #[cfg(feature = "voip-runtime")]
3218    #[tokio::test]
3219    async fn call_link_admission_is_buffered_until_ack_registration() {
3220        let (client, _transport) = crate::test_utils::create_iq_test_client().await;
3221        let creator = Jid::new("333333333333333", Server::Lid);
3222        let call_id = "BUFFERED-ADMISSION";
3223        let local_device = Jid::new("111111111111111", Server::Lid).with_device(1);
3224        let mut participant = GroupCallParticipant::new(
3225            local_device.to_non_ad(),
3226            vec![GroupCallDevice::new(local_device.clone())],
3227        );
3228        participant.state = Some("connected".to_string());
3229        let update = GroupCallUpdate::builder()
3230            .call_id(call_id.to_string())
3231            .call_creator(creator.clone())
3232            .transaction_id(8)
3233            .media("audio".to_string())
3234            .connected_limit(32)
3235            .joinable(true)
3236            .av_upgradable(true)
3237            .rekey_requested(false)
3238            .participants(vec![participant])
3239            .build();
3240        let creator_sender = creator.clone().with_device(1);
3241
3242        let pending = client.begin_call_link_join();
3243        assert_eq!(
3244            client.buffer_pending_call_link_update(&update, &creator_sender),
3245            PendingCallLinkBuffer::Buffered,
3246            "the creator's admission update must survive until the ACK registers its call id"
3247        );
3248        assert_eq!(
3249            client.buffer_pending_call_link_update(
3250                &update,
3251                &Jid::new("999999999999999", Server::Lid)
3252            ),
3253            PendingCallLinkBuffer::NotPending,
3254            "an unrelated sender cannot populate the pre-registration buffer"
3255        );
3256        let pending_memory = client.memory_report().await.pending_call_link_updates;
3257        assert_eq!(pending_memory.entries, 1);
3258        assert!(pending_memory.bytes > 0);
3259
3260        let mut session =
3261            CallSession::new_outgoing(call_id, Jid::new(call_id, Server::Call), creator);
3262        let _ = session.transition_to(CallPhase::Calling);
3263        let _ = session.transition_to(CallPhase::WaitingRoom);
3264        let generation = client
3265            .register_call_link_session(session, None, CallLinkMedia::Audio, "TEST-CALL-LINK")
3266            .await
3267            .expect("valid buffered admission");
3268        assert!(client.call_registry().set_group_invite_self_device(
3269            call_id,
3270            generation,
3271            GroupCallDevice::new(local_device).with_capability(1, [1]),
3272        ));
3273        assert_eq!(
3274            client.call_registry().phase_if_current(call_id, generation),
3275            Some(CallPhase::Connecting),
3276            "consuming the buffered admission must perform the waiting-room transition"
3277        );
3278        assert_eq!(
3279            client
3280                .call_registry()
3281                .group_state_if_current(call_id, generation)
3282                .and_then(|state| { state.snapshot().map(|snapshot| snapshot.transaction_id) }),
3283            Some(8)
3284        );
3285        assert_eq!(
3286            client
3287                .memory_report()
3288                .await
3289                .pending_call_link_updates
3290                .entries,
3291            0
3292        );
3293        let mut later = update;
3294        later.transaction_id = 9;
3295        assert_eq!(
3296            client.buffer_pending_call_link_update(&later, &creator_sender),
3297            PendingCallLinkBuffer::NotPending,
3298            "an already registered generation must dispatch instead of entering an orphan buffer"
3299        );
3300
3301        drop(pending);
3302        client
3303            .call_registry()
3304            .remove_if_current(call_id, generation);
3305    }
3306
3307    #[cfg(feature = "voip-runtime")]
3308    #[tokio::test]
3309    async fn call_link_termination_before_registration_rejects_the_join() {
3310        let (client, _transport) = crate::test_utils::create_iq_test_client().await;
3311        let creator = Jid::new("333333333333333", Server::Lid);
3312        let call_id = "TERMINATED-CALL-LINK";
3313        let _pending = client.begin_call_link_join();
3314        assert!(
3315            client
3316                .retain_or_apply_pending_call_link_terminate(
3317                    call_id,
3318                    &creator,
3319                    &creator.clone().with_device(1),
3320                )
3321                .await
3322        );
3323
3324        let mut session =
3325            CallSession::new_outgoing(call_id, Jid::new(call_id, Server::Call), creator);
3326        let _ = session.transition_to(CallPhase::Calling);
3327        let _ = session.transition_to(CallPhase::WaitingRoom);
3328        assert_eq!(
3329            client
3330                .register_call_link_session(session, None, CallLinkMedia::Audio, "TEST-CALL-LINK")
3331                .await,
3332            Err(wacore::voip::GroupStateApply::InvalidSnapshot)
3333        );
3334        assert_eq!(
3335            client.call_registry().generation_of(call_id),
3336            None,
3337            "a terminal control that overtakes registration must prevent publication"
3338        );
3339    }
3340
3341    #[cfg(feature = "voip-runtime")]
3342    #[tokio::test]
3343    async fn call_link_termination_removes_a_generation_that_won_registration() {
3344        let (client, _transport) = crate::test_utils::create_iq_test_client().await;
3345        let creator = Jid::new("333333333333333", Server::Lid);
3346        let call_id = "REGISTERED-THEN-TERMINATED-CALL-LINK";
3347        let _pending = client.begin_call_link_join();
3348        let mut session =
3349            CallSession::new_outgoing(call_id, Jid::new(call_id, Server::Call), creator.clone());
3350        let _ = session.transition_to(CallPhase::Calling);
3351        let _ = session.transition_to(CallPhase::WaitingRoom);
3352        let generation = client
3353            .register_call_link_session(session, None, CallLinkMedia::Audio, "TEST-CALL-LINK")
3354            .await
3355            .expect("registration wins the answer-transition lane");
3356
3357        assert!(
3358            client
3359                .retain_or_apply_pending_call_link_terminate(
3360                    call_id,
3361                    &creator,
3362                    &creator.with_device(1),
3363                )
3364                .await
3365        );
3366        assert_eq!(
3367            client.call_registry().generation_of(call_id),
3368            None,
3369            "the terminal control must remove the just-published generation"
3370        );
3371        assert!(
3372            !client.call_registry().is_current(call_id, generation),
3373            "the removed generation cannot remain active"
3374        );
3375    }
3376
3377    #[cfg(feature = "voip-runtime")]
3378    #[tokio::test]
3379    async fn call_link_epoch_before_registration_is_replayed_to_the_generation() {
3380        let (client, _transport) = crate::test_utils::create_iq_test_client().await;
3381        let creator = Jid::new("333333333333333", Server::Lid);
3382        let call_id = "EPOCH-CALL-LINK";
3383        let _pending = client.begin_call_link_join();
3384        assert_eq!(
3385            client.buffer_pending_call_link_epoch(
3386                call_id,
3387                &creator,
3388                &creator.clone().with_device(1),
3389                7,
3390                &[7; 32],
3391            ),
3392            PendingCallLinkBuffer::Buffered
3393        );
3394
3395        let mut session =
3396            CallSession::new_outgoing(call_id, Jid::new(call_id, Server::Call), creator);
3397        let _ = session.transition_to(CallPhase::Calling);
3398        let _ = session.transition_to(CallPhase::Connecting);
3399        let generation = client
3400            .register_call_link_session(session, None, CallLinkMedia::Audio, "TEST-CALL-LINK")
3401            .await
3402            .expect("valid call-link generation");
3403        assert_eq!(
3404            client
3405                .call_registry()
3406                .pending_group_epoch_transaction_if_current(call_id, generation),
3407            Some(7),
3408            "the decrypted epoch must survive until the media driver attaches"
3409        );
3410        client
3411            .call_registry()
3412            .remove_if_current(call_id, generation);
3413    }
3414
3415    #[cfg(feature = "voip-runtime")]
3416    #[tokio::test]
3417    async fn staged_call_link_epoch_and_termination_revalidate_provenance() {
3418        let (client, _transport) = crate::test_utils::create_iq_test_client().await;
3419        let call_id = "PROVENANCE-CALL-LINK";
3420        let creator = Jid::new("333333333333333", Server::Lid);
3421        let asserted_creator = Jid::new("999999999999999", Server::Lid);
3422        let asserted_sender = asserted_creator.clone().with_device(7);
3423        let _pending = client.begin_call_link_join();
3424
3425        assert_eq!(
3426            client.buffer_pending_call_link_epoch(
3427                call_id,
3428                &asserted_creator,
3429                &asserted_sender,
3430                7,
3431                &[7; 32],
3432            ),
3433            PendingCallLinkBuffer::Buffered
3434        );
3435        assert_eq!(
3436            client
3437                .buffer_pending_call_link_terminate(call_id, &asserted_creator, &asserted_sender,),
3438            PendingCallLinkBuffer::Buffered
3439        );
3440
3441        let mut session =
3442            CallSession::new_outgoing(call_id, Jid::new(call_id, Server::Call), creator);
3443        let _ = session.transition_to(CallPhase::Calling);
3444        let _ = session.transition_to(CallPhase::Connecting);
3445        let generation = client
3446            .register_call_link_session(session, None, CallLinkMedia::Audio, "TEST-CALL-LINK")
3447            .await
3448            .expect("unauthorized staged controls must not abort the legitimate join");
3449        assert!(
3450            client.call_registry().is_current(call_id, generation),
3451            "the unauthorized terminal marker must be ignored after registration"
3452        );
3453        assert_eq!(
3454            client
3455                .call_registry()
3456                .pending_group_epoch_transaction_if_current(call_id, generation),
3457            None,
3458            "the unauthorized epoch must not enter the registered generation"
3459        );
3460        client
3461            .call_registry()
3462            .remove_if_current(call_id, generation);
3463    }
3464
3465    #[cfg(feature = "voip-runtime")]
3466    #[tokio::test]
3467    async fn concurrent_call_link_join_waits_for_the_unknown_call_id_lane() {
3468        use std::time::Duration;
3469
3470        let (client, _transport) = crate::test_utils::create_iq_test_client().await;
3471        client
3472            .persistence_manager()
3473            .process_command(crate::store::commands::DeviceCommand::SetLid(Some(
3474                Jid::new("111111111111111", Server::Lid).with_device(1),
3475            )))
3476            .await;
3477
3478        let first_request = client.wait_for_sent_node(crate::client::NodeFilter::tag("call"));
3479        let first_client = client.clone();
3480        let first = tokio::spawn(async move {
3481            first_client
3482                .voip()
3483                .join_call_link_registration_with_audio(
3484                    "FIRST-CALL-LINK",
3485                    CallLinkMedia::Audio,
3486                    AudioFormat::OPUS_16KHZ_60MS,
3487                )
3488                .await
3489        });
3490        first_request.await.expect("first link_join request");
3491
3492        let second_request = client.wait_for_sent_node(crate::client::NodeFilter::tag("call"));
3493        let second_client = client.clone();
3494        let second = tokio::spawn(async move {
3495            second_client
3496                .voip()
3497                .join_call_link_registration_with_audio(
3498                    "SECOND-CALL-LINK",
3499                    CallLinkMedia::Audio,
3500                    AudioFormat::OPUS_16KHZ_60MS,
3501                )
3502                .await
3503        });
3504        assert!(
3505            tokio::time::timeout(Duration::from_millis(25), second_request)
3506                .await
3507                .is_err(),
3508            "a second unknown-call-id join must wait instead of sharing the first join's buffer"
3509        );
3510
3511        let released_request = client.wait_for_sent_node(crate::client::NodeFilter::tag("call"));
3512        first.abort();
3513        let _ = first.await;
3514        tokio::time::timeout(Duration::from_secs(1), released_request)
3515            .await
3516            .expect("the second join lane should be released")
3517            .expect("second link_join request");
3518        second.abort();
3519        let _ = second.await;
3520    }
3521
3522    #[cfg(feature = "voip-runtime")]
3523    #[tokio::test]
3524    async fn registered_call_link_releases_the_unknown_id_lane_before_heartbeat() {
3525        use std::time::Duration;
3526        use wacore::handshake::NoiseCipher;
3527
3528        struct GatedTransport {
3529            started: async_channel::Sender<()>,
3530            release: async_channel::Receiver<()>,
3531            gate_next_send: std::sync::atomic::AtomicBool,
3532        }
3533
3534        #[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))]
3535        #[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)]
3536        impl crate::transport::Transport for GatedTransport {
3537            async fn send(&self, _data: Bytes) -> Result<(), anyhow::Error> {
3538                if self.gate_next_send.swap(false, Ordering::AcqRel) {
3539                    self.started
3540                        .send(())
3541                        .await
3542                        .map_err(|_| anyhow::anyhow!("heartbeat observer closed"))?;
3543                    self.release
3544                        .recv()
3545                        .await
3546                        .map_err(|_| anyhow::anyhow!("heartbeat gate closed"))?;
3547                }
3548                Ok(())
3549            }
3550
3551            async fn disconnect(&self) {}
3552        }
3553
3554        let (client, _transport) = crate::test_utils::create_iq_test_client().await;
3555        client
3556            .persistence_manager()
3557            .process_command(crate::store::commands::DeviceCommand::SetLid(Some(
3558                Jid::new("111111111111111", Server::Lid).with_device(1),
3559            )))
3560            .await;
3561        let creator = Jid::new("333333333333333", Server::Lid);
3562        let first_request = client.wait_for_sent_node(crate::client::NodeFilter::tag("call"));
3563        let first_client = client.clone();
3564        let first = tokio::spawn(async move {
3565            first_client
3566                .voip()
3567                .join_call_link_registration_with_audio(
3568                    "FIRST-CALL-LINK",
3569                    CallLinkMedia::Audio,
3570                    AudioFormat::OPUS_16KHZ_60MS,
3571                )
3572                .await
3573        });
3574        let request = first_request.await.expect("first link_join request");
3575        let request_id = request
3576            .as_node_ref()
3577            .attrs()
3578            .optional_string("id")
3579            .expect("request id")
3580            .into_owned();
3581        let (started_tx, started_rx) = async_channel::bounded(1);
3582        let (release_tx, release_rx) = async_channel::bounded(1);
3583        let gated_socket = crate::socket::NoiseSocket::new(
3584            Arc::new(crate::runtime_impl::TokioRuntime),
3585            Arc::new(GatedTransport {
3586                started: started_tx,
3587                release: release_rx,
3588                gate_next_send: std::sync::atomic::AtomicBool::new(true),
3589            }),
3590            NoiseCipher::new(&[0u8; 32]).expect("valid key"),
3591            NoiseCipher::new(&[0u8; 32]).expect("valid key"),
3592        );
3593        *client.noise_socket.lock().await = Some(Arc::new(gated_socket));
3594        crate::test_utils::answer_iq(
3595            &client,
3596            &request_id,
3597            &NodeBuilder::new("ack")
3598                .attr("class", "call")
3599                .attr("type", "link_join")
3600                .attr("id", request_id.as_str())
3601                .children([NodeBuilder::new("waiting_room")
3602                    .attr("call-id", "FIRST-CALL-ID")
3603                    .attr("call-creator", creator.clone())
3604                    .attr("link-token", "FIRST-CALL-LINK")
3605                    .attr("media", "audio")
3606                    .attr("enabled", "1")
3607                    .attr("is_admin", "0")
3608                    .attr("transaction-id", "1")
3609                    .build()])
3610                .build(),
3611        )
3612        .await;
3613        started_rx
3614            .recv()
3615            .await
3616            .expect("first waiting-room heartbeat entered the gated transport");
3617
3618        let second_request = client.wait_for_sent_node(crate::client::NodeFilter::tag("call"));
3619        let second_client = client.clone();
3620        let second = tokio::spawn(async move {
3621            second_client
3622                .voip()
3623                .join_call_link_registration_with_audio(
3624                    "SECOND-CALL-LINK",
3625                    CallLinkMedia::Audio,
3626                    AudioFormat::OPUS_16KHZ_60MS,
3627                )
3628                .await
3629        });
3630        let request = tokio::time::timeout(Duration::from_secs(1), second_request)
3631            .await
3632            .expect("registration must release the lane while the heartbeat remains gated")
3633            .expect("second link_join request");
3634        assert_eq!(
3635            request
3636                .as_node_ref()
3637                .children()
3638                .expect("second request action")[0]
3639                .tag,
3640            "link_join"
3641        );
3642        let second_sender = creator.clone().with_device(1);
3643        let second_update = GroupCallUpdate::builder()
3644            .call_id("SECOND-CALL-ID".to_string())
3645            .call_creator(creator)
3646            .transaction_id(1)
3647            .media("audio".to_string())
3648            .connected_limit(32)
3649            .joinable(true)
3650            .av_upgradable(true)
3651            .rekey_requested(false)
3652            .participants(Vec::new())
3653            .build();
3654        assert_eq!(
3655            client.buffer_pending_call_link_update(&second_update, &second_sender),
3656            PendingCallLinkBuffer::Buffered,
3657            "the second join must not inherit the first call id's provisional binding"
3658        );
3659
3660        release_tx.send(()).await.expect("release heartbeat send");
3661        second.abort();
3662        let _ = second.await;
3663        let registration = first
3664            .await
3665            .expect("first join task")
3666            .expect("first waiting-room registration");
3667        drop(registration);
3668    }
3669
3670    #[cfg(feature = "voip-runtime")]
3671    #[tokio::test]
3672    async fn pending_call_link_transitions_are_bounded_by_retained_bytes() {
3673        let (client, _transport) = crate::test_utils::create_iq_test_client().await;
3674        let creator = Jid::new("333333333333333", Server::Lid);
3675        let sender = creator.clone().with_device(1);
3676        let _pending = client.begin_call_link_join();
3677        let chunk = MAX_PENDING_CALL_LINK_TRANSITION_BYTES / 3;
3678        let mut accepted = 0;
3679        for index in 0..4 {
3680            let participant = GroupCallParticipant::new(
3681                creator.clone(),
3682                vec![GroupCallDevice::new(sender.clone()).with_capability(1, vec![7; chunk])],
3683            );
3684            let update = GroupCallUpdate::builder()
3685                .call_id(format!("BUFFERED-BYTES-{index}"))
3686                .call_creator(creator.clone())
3687                .transaction_id(1)
3688                .media("audio".to_string())
3689                .connected_limit(32)
3690                .joinable(true)
3691                .av_upgradable(true)
3692                .rekey_requested(false)
3693                .participants(vec![participant])
3694                .build();
3695            accepted += usize::from(
3696                client.buffer_pending_call_link_update(&update, &sender)
3697                    == PendingCallLinkBuffer::Buffered,
3698            );
3699        }
3700        let stats = client.memory_report().await.pending_call_link_updates;
3701        assert!(
3702            accepted < 4,
3703            "the aggregate byte budget must reject excess staged snapshots"
3704        );
3705        assert!(
3706            stats.bytes <= MAX_PENDING_CALL_LINK_TRANSITION_BYTES as u64,
3707            "retained staged snapshots must stay within the aggregate byte budget"
3708        );
3709
3710        let oversized = GroupCallUpdate::builder()
3711            .call_id("BUFFERED-OVERSIZED".to_string())
3712            .call_creator(creator.clone())
3713            .transaction_id(1)
3714            .media("audio".to_string())
3715            .connected_limit(32)
3716            .joinable(true)
3717            .av_upgradable(true)
3718            .rekey_requested(false)
3719            .participants(vec![GroupCallParticipant::new(
3720                creator,
3721                vec![
3722                    GroupCallDevice::new(sender.clone())
3723                        .with_capability(1, vec![7; MAX_PENDING_CALL_LINK_TRANSITION_BYTES]),
3724                ],
3725            )])
3726            .build();
3727        assert_eq!(
3728            client.buffer_pending_call_link_update(&oversized, &sender),
3729            PendingCallLinkBuffer::Saturated,
3730            "one staged snapshot cannot consume the entire retained-byte budget"
3731        );
3732    }
3733
3734    #[cfg(feature = "voip-runtime")]
3735    #[tokio::test]
3736    async fn saturated_call_link_admission_fails_instead_of_falling_back() {
3737        let (client, _transport) = crate::test_utils::create_iq_test_client().await;
3738        let creator = Jid::new("333333333333333", Server::Lid);
3739        let sender = creator.clone().with_device(1);
3740        let call_id = "SATURATED-ADMISSION";
3741        let _pending = client.begin_call_link_join();
3742        let update = GroupCallUpdate::builder()
3743            .call_id(call_id.to_string())
3744            .call_creator(creator.clone())
3745            .transaction_id(1)
3746            .media("audio".to_string())
3747            .connected_limit(32)
3748            .joinable(true)
3749            .av_upgradable(true)
3750            .rekey_requested(false)
3751            .participants(vec![GroupCallParticipant::new(
3752                creator.clone(),
3753                vec![
3754                    GroupCallDevice::new(sender.clone())
3755                        .with_capability(1, vec![7; MAX_PENDING_CALL_LINK_TRANSITION_BYTES]),
3756                ],
3757            )])
3758            .build();
3759        assert_eq!(
3760            client.buffer_pending_call_link_update(&update, &sender),
3761            PendingCallLinkBuffer::Saturated,
3762            "the admission is handled locally even when it exceeds the staging budget"
3763        );
3764
3765        let mut session =
3766            CallSession::new_outgoing(call_id, Jid::new(call_id, Server::Call), creator);
3767        let _ = session.transition_to(CallPhase::Calling);
3768        let _ = session.transition_to(CallPhase::WaitingRoom);
3769        assert_eq!(
3770            client
3771                .register_call_link_session(session, None, CallLinkMedia::Audio, "TEST-CALL-LINK",)
3772                .await,
3773            Err(wacore::voip::GroupStateApply::InvalidSnapshot),
3774            "a saturated join must fail rather than wait forever for a discarded admission"
3775        );
3776        assert_eq!(client.call_registry().generation_of(call_id), None);
3777    }
3778
3779    #[cfg(feature = "voip-runtime")]
3780    #[tokio::test]
3781    async fn saturated_admission_is_retained_when_unrelated_call_ids_fill_the_payload_budget() {
3782        let (client, _transport) = crate::test_utils::create_iq_test_client().await;
3783        let creator = Jid::new("333333333333333", Server::Lid);
3784        let sender = creator.clone().with_device(1);
3785        let _pending = client.begin_call_link_join();
3786        for index in 0..MAX_PENDING_CALL_LINK_TRANSITIONS {
3787            let unrelated_creator = Jid::new(format!("55555555555{index:04}"), Server::Lid);
3788            let unrelated_sender = unrelated_creator.clone().with_device(1);
3789            assert_eq!(
3790                client.buffer_pending_call_link_terminate(
3791                    &format!("UNRELATED-{index}"),
3792                    &unrelated_creator,
3793                    &unrelated_sender,
3794                ),
3795                PendingCallLinkBuffer::Buffered
3796            );
3797        }
3798
3799        let call_id = "SATURATED-AFTER-UNRELATED";
3800        let oversized = GroupCallUpdate::builder()
3801            .call_id(call_id.to_string())
3802            .call_creator(creator.clone())
3803            .transaction_id(1)
3804            .media("audio".to_string())
3805            .connected_limit(32)
3806            .joinable(true)
3807            .av_upgradable(true)
3808            .rekey_requested(false)
3809            .participants(vec![GroupCallParticipant::new(
3810                creator.clone(),
3811                vec![
3812                    GroupCallDevice::new(sender.clone())
3813                        .with_capability(1, vec![7; MAX_PENDING_CALL_LINK_TRANSITION_BYTES]),
3814                ],
3815            )])
3816            .build();
3817        assert_eq!(
3818            client.buffer_pending_call_link_update(&oversized, &sender),
3819            PendingCallLinkBuffer::Saturated
3820        );
3821
3822        let mut session =
3823            CallSession::new_outgoing(call_id, Jid::new(call_id, Server::Call), creator);
3824        let _ = session.transition_to(CallPhase::Calling);
3825        let _ = session.transition_to(CallPhase::WaitingRoom);
3826        assert_eq!(
3827            client
3828                .register_call_link_session(session, None, CallLinkMedia::Audio, "TEST-CALL-LINK",)
3829                .await,
3830            Err(wacore::voip::GroupStateApply::InvalidSnapshot),
3831            "binding the ACK must retain the exact overflow identity outside the full payload map"
3832        );
3833        assert_eq!(client.call_registry().generation_of(call_id), None);
3834    }
3835
3836    #[cfg(feature = "voip-runtime")]
3837    #[tokio::test]
3838    async fn unrelated_saturation_does_not_reject_the_valid_call_link_join() {
3839        let (client, _transport) = crate::test_utils::create_iq_test_client().await;
3840        let unrelated_creator = Jid::new("333333333333333", Server::Lid);
3841        let unrelated_sender = unrelated_creator.clone().with_device(1);
3842        let _pending = client.begin_call_link_join();
3843        for index in 0..MAX_PENDING_CALL_LINK_TRANSITIONS {
3844            assert!(
3845                client
3846                    .buffer_pending_call_link_terminate(
3847                        &format!("UNRELATED-{index}"),
3848                        &unrelated_creator,
3849                        &unrelated_sender,
3850                    )
3851                    .suppresses_dispatch()
3852            );
3853        }
3854        let mut oversized = GroupCallUpdate::builder()
3855            .call_id("UNRELATED-SATURATED-CALL-0".to_string())
3856            .call_creator(unrelated_creator.clone())
3857            .transaction_id(1)
3858            .media("audio".to_string())
3859            .connected_limit(32)
3860            .joinable(true)
3861            .av_upgradable(true)
3862            .rekey_requested(false)
3863            .participants(vec![GroupCallParticipant::new(
3864                unrelated_creator,
3865                vec![
3866                    GroupCallDevice::new(unrelated_sender.clone())
3867                        .with_capability(1, vec![7; MAX_PENDING_CALL_LINK_TRANSITION_BYTES]),
3868                ],
3869            )])
3870            .build();
3871        for index in 0..=MAX_PENDING_CALL_LINK_SATURATION_FINGERPRINTS {
3872            oversized.call_id = format!("UNRELATED-SATURATED-CALL-{index}");
3873            assert_eq!(
3874                client.buffer_pending_call_link_update(&oversized, &unrelated_sender),
3875                PendingCallLinkBuffer::Saturated,
3876                "every oversized unrelated identity must remain handled locally"
3877            );
3878        }
3879
3880        let call_id = "VALID-CALL-LINK";
3881        let call_creator = Jid::new("444444444444444", Server::Lid);
3882        let ack = NodeBuilder::new("ack")
3883            .attr("class", "call")
3884            .attr("type", "link_join")
3885            .children([NodeBuilder::new("waiting_room")
3886                .attr("call-id", call_id)
3887                .build()])
3888            .build();
3889        client.bind_pending_call_link_join_ack(&ack.as_node_ref());
3890        assert!(
3891            client.prepare_pending_call_link_join_retry(call_id),
3892            "exhausted pre-ACK identity metadata requires one exact-call refresh"
3893        );
3894        assert_eq!(
3895            client
3896                .memory_report()
3897                .await
3898                .pending_call_link_updates
3899                .entries,
3900            0,
3901            "binding the ACK must discard every unrelated candidate bucket"
3902        );
3903        assert_eq!(
3904            client.buffer_pending_call_link_terminate(
3905                "LATE-UNRELATED",
3906                &unrelated_sender.to_non_ad(),
3907                &unrelated_sender,
3908            ),
3909            PendingCallLinkBuffer::NotPending,
3910            "later unrelated controls cannot consume the bound join's budget"
3911        );
3912        let mut participant = GroupCallParticipant::new(
3913            call_creator.clone(),
3914            vec![GroupCallDevice::new(call_creator.clone().with_device(1))],
3915        );
3916        participant.state = Some("connected".to_string());
3917        let admitted = GroupCallUpdate::builder()
3918            .call_id(call_id.to_string())
3919            .call_creator(call_creator.clone())
3920            .transaction_id(1)
3921            .media("audio".to_string())
3922            .connected_limit(32)
3923            .joinable(true)
3924            .av_upgradable(true)
3925            .rekey_requested(false)
3926            .participants(vec![participant])
3927            .build();
3928        assert_eq!(
3929            client
3930                .buffer_pending_call_link_update(&admitted, &call_creator.clone().with_device(1),),
3931            PendingCallLinkBuffer::Buffered
3932        );
3933        let mut session =
3934            CallSession::new_outgoing(call_id, Jid::new(call_id, Server::Call), call_creator);
3935        let _ = session.transition_to(CallPhase::Calling);
3936        let _ = session.transition_to(CallPhase::WaitingRoom);
3937        let generation = client
3938            .register_call_link_session(session, None, CallLinkMedia::Audio, "VALID-CALL-LINK")
3939            .await
3940            .expect("an unrelated saturated control cannot abort the ACK's actual call id");
3941        assert!(
3942            client.call_registry().is_current(call_id, generation),
3943            "the legitimate call-link generation must remain registered"
3944        );
3945        assert_eq!(
3946            client
3947                .call_registry()
3948                .group_state_if_current(call_id, generation)
3949                .and_then(|state| state.snapshot().map(|snapshot| snapshot.transaction_id)),
3950            Some(1)
3951        );
3952        client
3953            .call_registry()
3954            .remove_if_current(call_id, generation);
3955    }
3956
3957    #[cfg(feature = "voip-runtime")]
3958    #[tokio::test]
3959    async fn ambiguous_pre_ack_saturation_retries_after_binding_the_exact_call_id() {
3960        let (client, _transport) = crate::test_utils::create_iq_test_client().await;
3961        client
3962            .persistence_manager()
3963            .process_command(crate::store::commands::DeviceCommand::SetLid(Some(
3964                Jid::new("111111111111111", Server::Lid).with_device(1),
3965            )))
3966            .await;
3967        let creator = Jid::new("333333333333333", Server::Lid);
3968        let sender = creator.clone().with_device(1);
3969        let sent = client.wait_for_sent_node(crate::client::NodeFilter::tag("call"));
3970        let join_client = client.clone();
3971        let join = tokio::spawn(async move {
3972            join_client
3973                .voip()
3974                .join_call_link_registration_with_audio(
3975                    "RETRIED-CALL-LINK",
3976                    CallLinkMedia::Audio,
3977                    AudioFormat::OPUS_16KHZ_60MS,
3978                )
3979                .await
3980        });
3981        let first_request = sent.await.expect("initial link_join request");
3982
3983        for index in 0..MAX_PENDING_CALL_LINK_TRANSITIONS {
3984            assert_eq!(
3985                client.buffer_pending_call_link_terminate(
3986                    &format!("UNRELATED-FILLED-{index}"),
3987                    &creator,
3988                    &sender,
3989                ),
3990                PendingCallLinkBuffer::Buffered
3991            );
3992        }
3993        let mut oversized = GroupCallUpdate::builder()
3994            .call_id("UNRELATED-OVERFLOW-0".to_string())
3995            .call_creator(creator.clone())
3996            .transaction_id(1)
3997            .media("audio".to_string())
3998            .connected_limit(32)
3999            .joinable(true)
4000            .av_upgradable(true)
4001            .rekey_requested(false)
4002            .participants(vec![GroupCallParticipant::new(
4003                creator.clone(),
4004                vec![
4005                    GroupCallDevice::new(sender.clone())
4006                        .with_capability(1, vec![7; MAX_PENDING_CALL_LINK_TRANSITION_BYTES]),
4007                ],
4008            )])
4009            .build();
4010        for index in 0..=MAX_PENDING_CALL_LINK_SATURATION_FINGERPRINTS {
4011            oversized.call_id = format!("UNRELATED-OVERFLOW-{index}");
4012            assert_eq!(
4013                client.buffer_pending_call_link_update(&oversized, &sender),
4014                PendingCallLinkBuffer::Saturated
4015            );
4016        }
4017
4018        let first_request_id = first_request
4019            .as_node_ref()
4020            .attrs()
4021            .optional_string("id")
4022            .expect("initial request id")
4023            .into_owned();
4024        let refreshed_sent = client.wait_for_sent_node(crate::client::NodeFilter::tag("call"));
4025        crate::test_utils::answer_iq(
4026            &client,
4027            &first_request_id,
4028            &NodeBuilder::new("ack")
4029                .attr("class", "call")
4030                .attr("type", "link_join")
4031                .attr("id", first_request_id.as_str())
4032                .children([NodeBuilder::new("waiting_room")
4033                    .attr("call-id", "RETRIED-CALL-ID")
4034                    .attr("call-creator", creator.clone())
4035                    .attr("link-token", "RETRIED-CALL-LINK")
4036                    .attr("media", "audio")
4037                    .attr("enabled", "1")
4038                    .attr("is_admin", "0")
4039                    .attr("transaction-id", "1")
4040                    .build()])
4041                .build(),
4042        )
4043        .await;
4044        let refreshed_request = refreshed_sent.await.expect("refreshed link_join request");
4045        assert_eq!(
4046            refreshed_request
4047                .as_node_ref()
4048                .children()
4049                .expect("refreshed request action")[0]
4050                .tag,
4051            "link_join"
4052        );
4053        let refreshed_request_id = refreshed_request
4054            .as_node_ref()
4055            .attrs()
4056            .optional_string("id")
4057            .expect("refreshed request id")
4058            .into_owned();
4059        crate::test_utils::answer_iq(
4060            &client,
4061            &refreshed_request_id,
4062            &NodeBuilder::new("ack")
4063                .attr("class", "call")
4064                .attr("type", "link_join")
4065                .attr("id", refreshed_request_id.as_str())
4066                .children([NodeBuilder::new("waiting_room")
4067                    .attr("call-id", "RETRIED-CALL-ID")
4068                    .attr("call-creator", creator)
4069                    .attr("link-token", "RETRIED-CALL-LINK")
4070                    .attr("media", "audio")
4071                    .attr("enabled", "1")
4072                    .attr("is_admin", "0")
4073                    .attr("transaction-id", "2")
4074                    .build()])
4075                .build(),
4076        )
4077        .await;
4078        let registration = join
4079            .await
4080            .expect("join task")
4081            .expect("an unrelated overflow must recover through the bound retry");
4082        assert_eq!(registration.join.call_id, "RETRIED-CALL-ID");
4083        assert!(
4084            client
4085                .call_registry()
4086                .is_current("RETRIED-CALL-ID", registration.generation)
4087        );
4088        client
4089            .call_registry()
4090            .remove_if_current("RETRIED-CALL-ID", registration.generation);
4091    }
4092
4093    #[cfg(feature = "voip-runtime")]
4094    #[tokio::test]
4095    async fn call_link_ack_paths_bind_before_waking_the_waiter() {
4096        for owned_fast_path in [false, true] {
4097            let (client, _transport) = crate::test_utils::create_iq_test_client().await;
4098            let unrelated_creator = Jid::new("333333333333333", Server::Lid);
4099            let unrelated_sender = unrelated_creator.clone().with_device(1);
4100            let _pending = client.begin_call_link_join();
4101            assert_eq!(
4102                client.buffer_pending_call_link_terminate(
4103                    "UNRELATED-CALL-LINK",
4104                    &unrelated_creator,
4105                    &unrelated_sender,
4106                ),
4107                PendingCallLinkBuffer::Buffered
4108            );
4109
4110            let request_id = if owned_fast_path {
4111                "OWNED-LINK-JOIN-ACK"
4112            } else {
4113                "SHARED-LINK-JOIN-ACK"
4114            };
4115            let (sender, receiver) = futures::channel::oneshot::channel();
4116            client
4117                .response_waiters_guard()
4118                .insert(request_id.to_string(), ResponseWaiter::Iq(sender));
4119            let ack = NodeBuilder::new("ack")
4120                .attr("id", request_id)
4121                .attr("class", "call")
4122                .attr("type", "link_join")
4123                .children([NodeBuilder::new("waiting_room")
4124                    .attr("call-id", "ACTUAL-CALL-LINK")
4125                    .build()])
4126                .build();
4127            let node = crate::test_utils::node_to_owned_ref(&ack);
4128            let handled = if owned_fast_path {
4129                let node = Arc::try_unwrap(node)
4130                    .unwrap_or_else(|_| panic!("the test owns the ACK allocation"));
4131                client.handle_ack_response_owned(node)
4132            } else {
4133                client.handle_ack_response_arc(&node)
4134            };
4135            assert!(handled, "the ACK must resolve its registered waiter");
4136            assert_eq!(
4137                client
4138                    .memory_report()
4139                    .await
4140                    .pending_call_link_updates
4141                    .entries,
4142                0,
4143                "the ACK call id must be bound before the waiter can observe the response"
4144            );
4145            assert_eq!(
4146                client.buffer_pending_call_link_terminate(
4147                    "LATE-UNRELATED-CALL",
4148                    &unrelated_creator,
4149                    &unrelated_sender,
4150                ),
4151                PendingCallLinkBuffer::NotPending,
4152                "later unrelated controls cannot consume the bound join's budget"
4153            );
4154            let response = receiver.await.expect("the ACK waiter should be woken");
4155            assert!(
4156                response
4157                    .get()
4158                    .get_attr("id")
4159                    .is_some_and(|value| value.as_str() == request_id)
4160            );
4161        }
4162    }
4163
4164    #[cfg(feature = "voip-runtime")]
4165    #[tokio::test]
4166    async fn call_link_registration_replays_staged_transitions_in_order() {
4167        let (client, _transport) = crate::test_utils::create_iq_test_client().await;
4168        let creator = Jid::new("333333333333333", Server::Lid);
4169        let call_id = "ORDERED-CALL-LINK";
4170        let local_device = Jid::new("111111111111111", Server::Lid).with_device(1);
4171        let mut participant = GroupCallParticipant::new(
4172            local_device.to_non_ad(),
4173            vec![GroupCallDevice::new(local_device.clone())],
4174        );
4175        participant.state = Some("connected".to_string());
4176        let relay = GroupCallRelay::builder()
4177            .transaction_id(8)
4178            .self_pid(1)
4179            .uuid("TEST-RELAY".to_string())
4180            .participant_uuid("TEST-PARTICIPANT".to_string())
4181            .attribute_padding(false)
4182            .warp_mi_tag_len(4)
4183            .key(vec![7; 32])
4184            .tokens(vec![vec![9; 16]])
4185            .endpoints(vec![
4186                GroupCallRelayEndpoint::builder()
4187                    .relay_id(1)
4188                    .token_id(0)
4189                    .auth_token_id(0)
4190                    .relay_name("test-relay".to_string())
4191                    .is_fna(false)
4192                    .ipv4("203.0.113.7".to_string())
4193                    .port(3478)
4194                    .build(),
4195            ])
4196            .build();
4197        let first = GroupCallUpdate::builder()
4198            .call_id(call_id.to_string())
4199            .call_creator(creator.clone())
4200            .transaction_id(8)
4201            .media("audio".to_string())
4202            .connected_limit(32)
4203            .joinable(true)
4204            .av_upgradable(true)
4205            .rekey_requested(true)
4206            .participants(vec![participant.clone()])
4207            .relay(relay)
4208            .build();
4209        let second = GroupCallUpdate::builder()
4210            .call_id(call_id.to_string())
4211            .call_creator(creator.clone())
4212            .transaction_id(9)
4213            .media("audio".to_string())
4214            .connected_limit(32)
4215            .joinable(true)
4216            .av_upgradable(true)
4217            .rekey_requested(false)
4218            .participants(vec![participant])
4219            .build();
4220        let initial_room = WaitingRoom::builder()
4221            .call_id(call_id.to_string())
4222            .call_creator(creator.clone())
4223            .link_token("TEST-CALL-LINK".to_string())
4224            .media(CallLinkMedia::Audio)
4225            .enabled(true)
4226            .is_admin(false)
4227            .transaction_id(1)
4228            .users(Vec::new())
4229            .build();
4230        let newer_room = WaitingRoom::builder()
4231            .call_id(call_id.to_string())
4232            .call_creator(creator.clone())
4233            .link_token("TEST-CALL-LINK".to_string())
4234            .media(CallLinkMedia::Audio)
4235            .enabled(true)
4236            .is_admin(true)
4237            .transaction_id(2)
4238            .users(Vec::new())
4239            .build();
4240
4241        let pending = client.begin_call_link_join();
4242        let sender = creator.clone().with_device(1);
4243        assert_eq!(
4244            client.buffer_pending_call_link_update(&first, &sender),
4245            PendingCallLinkBuffer::Buffered
4246        );
4247        assert_eq!(
4248            client.buffer_pending_call_link_waiting_room(&newer_room, &sender),
4249            PendingCallLinkBuffer::Buffered
4250        );
4251        assert_eq!(
4252            client.buffer_pending_call_link_update(&second, &sender),
4253            PendingCallLinkBuffer::Buffered
4254        );
4255        assert_eq!(
4256            client
4257                .memory_report()
4258                .await
4259                .pending_call_link_updates
4260                .entries,
4261            3
4262        );
4263
4264        let mut session =
4265            CallSession::new_outgoing(call_id, Jid::new(call_id, Server::Call), creator);
4266        let _ = session.transition_to(CallPhase::Calling);
4267        let _ = session.transition_to(CallPhase::WaitingRoom);
4268        let registration_lane = client.lock_answer_transition(call_id).await;
4269        let register_client = client.clone();
4270        let registration = tokio::spawn(async move {
4271            register_client
4272                .register_call_link_session(
4273                    session,
4274                    Some(initial_room),
4275                    CallLinkMedia::Audio,
4276                    "TEST-CALL-LINK",
4277                )
4278                .await
4279        });
4280        tokio::task::yield_now().await;
4281        assert_eq!(
4282            client.call_registry().generation_of(call_id),
4283            None,
4284            "call-link insertion must share the call-id registration lane"
4285        );
4286        drop(registration_lane);
4287        let generation = registration
4288            .await
4289            .expect("registration task")
4290            .expect("valid staged transitions");
4291        assert!(client.call_registry().set_group_invite_self_device(
4292            call_id,
4293            generation,
4294            GroupCallDevice::new(local_device).with_capability(1, [1]),
4295        ));
4296        let state = client
4297            .call_registry()
4298            .group_state_if_current(call_id, generation)
4299            .expect("registered group state");
4300        let snapshot = state.snapshot().expect("latest admission roster");
4301        assert_eq!(snapshot.transaction_id, 9);
4302        assert!(snapshot.relay.is_some(), "roster-only update retains relay");
4303        assert!(
4304            snapshot.rekey_requested,
4305            "the earlier unfulfilled rekey obligation survives the roster-only update"
4306        );
4307        assert!(
4308            state
4309                .waiting_room()
4310                .is_some_and(|room| room.transaction_id == Some(2) && room.is_admin)
4311        );
4312        assert_eq!(
4313            client.call_registry().phase_if_current(call_id, generation),
4314            Some(CallPhase::Connecting)
4315        );
4316        assert_eq!(
4317            client
4318                .memory_report()
4319                .await
4320                .pending_call_link_updates
4321                .entries,
4322            0
4323        );
4324
4325        drop(pending);
4326        client
4327            .call_registry()
4328            .remove_if_current(call_id, generation);
4329    }
4330
4331    #[cfg(feature = "voip-runtime")]
4332    #[tokio::test]
4333    async fn call_link_rekey_targets_the_latest_post_registration_roster() {
4334        let (client, _transport) = crate::test_utils::create_iq_test_client().await;
4335        let own_lid = Jid::new("111111111111111", Server::Lid).with_device(1);
4336        client
4337            .persistence_manager()
4338            .process_command(crate::store::commands::DeviceCommand::SetLid(Some(
4339                own_lid.clone(),
4340            )))
4341            .await;
4342        let creator = Jid::new("333333333333333", Server::Lid);
4343        let call_id = "POST-REGISTRATION-REKEY";
4344        let mut participant =
4345            GroupCallParticipant::new(own_lid.to_non_ad(), vec![GroupCallDevice::new(own_lid)]);
4346        participant.state = Some("connected".to_string());
4347        let initial = GroupCallUpdate::builder()
4348            .call_id(call_id.to_string())
4349            .call_creator(creator.clone())
4350            .transaction_id(1)
4351            .media("audio".to_string())
4352            .connected_limit(32)
4353            .joinable(true)
4354            .av_upgradable(true)
4355            .rekey_requested(true)
4356            .participants(vec![participant.clone()])
4357            .build();
4358        let mut session =
4359            CallSession::new_outgoing(call_id, Jid::new(call_id, Server::Call), creator.clone());
4360        session.group = Some(initial.clone());
4361        let _ = session.transition_to(CallPhase::Calling);
4362        let _ = session.transition_to(CallPhase::Connecting);
4363        let generation = client
4364            .register_call_link_session(session, None, CallLinkMedia::Audio, "TEST-CALL-LINK")
4365            .await
4366            .expect("registered admitted call link");
4367
4368        let mut current = initial.clone();
4369        current.transaction_id = 2;
4370        current.rekey_requested = false;
4371        assert_eq!(
4372            client
4373                .call_registry()
4374                .apply_group_update_if_current(current, generation),
4375            wacore::voip::GroupStateApply::Applied
4376        );
4377        let mut join = wacore::types::group_call::CallLinkJoin::builder()
4378            .token("TEST-CALL-LINK".to_string())
4379            .media(CallLinkMedia::Audio)
4380            .call_id(call_id.to_string())
4381            .call_creator(creator)
4382            .waiting_room_enabled(false)
4383            .in_waiting_room(false)
4384            .is_admin(false)
4385            .group(initial)
4386            .build();
4387
4388        assert!(
4389            !client
4390                .voip()
4391                .synchronize_call_link_admission(&mut join, generation, true)
4392                .await
4393                .expect("latest admission state")
4394        );
4395        assert_eq!(
4396            join.group.as_ref().map(|update| update.transaction_id),
4397            Some(2)
4398        );
4399        assert_eq!(
4400            client
4401                .call_registry()
4402                .pending_group_epoch_transaction_if_current(call_id, generation),
4403            Some(2),
4404            "the ACK rekey obligation must publish against the latest serialized roster"
4405        );
4406        client
4407            .call_registry()
4408            .remove_if_current(call_id, generation);
4409    }
4410
4411    #[cfg(feature = "voip-runtime")]
4412    #[tokio::test]
4413    async fn invalid_admitted_call_link_snapshot_is_not_registered() {
4414        let (client, _transport) = crate::test_utils::create_iq_test_client().await;
4415        let creator = Jid::new("333333333333333", Server::Lid);
4416        let call_id = "INVALID-CALL-LINK";
4417        let mut invalid = GroupCallUpdate::builder()
4418            .call_id(call_id.to_string())
4419            .call_creator(creator.clone())
4420            .transaction_id(1)
4421            .media("audio".to_string())
4422            .connected_limit(32)
4423            .joinable(true)
4424            .av_upgradable(true)
4425            .rekey_requested(false)
4426            .participants(Vec::new())
4427            .build();
4428        invalid.connected_limit = 0;
4429        let mut session =
4430            CallSession::new_outgoing(call_id, Jid::new(call_id, Server::Call), creator);
4431        session.group = Some(invalid);
4432
4433        assert_eq!(
4434            client
4435                .register_call_link_session(session, None, CallLinkMedia::Audio, "TEST-CALL-LINK",)
4436                .await,
4437            Err(wacore::voip::GroupStateApply::InvalidSnapshot)
4438        );
4439        assert_eq!(client.call_registry().generation_of(call_id), None);
4440    }
4441
4442    #[cfg(feature = "voip-runtime")]
4443    #[tokio::test]
4444    async fn buffered_call_link_admission_cannot_cross_generations() {
4445        let (client, _transport) = crate::test_utils::create_iq_test_client().await;
4446        let creator = Jid::new("333333333333333", Server::Lid);
4447        let call_id = "BUFFERED-ADMISSION-GENERATION";
4448        let mut participant = GroupCallParticipant::new(
4449            creator.clone(),
4450            vec![GroupCallDevice::new(creator.clone().with_device(1))],
4451        );
4452        participant.state = Some("connected".to_string());
4453        let update = GroupCallUpdate::builder()
4454            .call_id(call_id.to_string())
4455            .call_creator(creator.clone())
4456            .transaction_id(8)
4457            .media("audio".to_string())
4458            .connected_limit(32)
4459            .joinable(true)
4460            .av_upgradable(true)
4461            .rekey_requested(false)
4462            .participants(vec![participant])
4463            .build();
4464
4465        let registry = client.call_registry();
4466        let stale = registry.insert(CallSession::new_outgoing(
4467            call_id,
4468            Jid::new(call_id, Server::Call),
4469            creator.clone(),
4470        ));
4471        let replacement = registry.insert(CallSession::new_outgoing(
4472            call_id,
4473            Jid::new(call_id, Server::Call),
4474            creator,
4475        ));
4476        assert_ne!(stale, replacement);
4477        assert_eq!(
4478            client.apply_pending_call_link_update(update, stale),
4479            wacore::voip::GroupStateApply::UnknownCall
4480        );
4481        assert!(
4482            registry
4483                .group_state_if_current(call_id, replacement)
4484                .is_none(),
4485            "a buffered snapshot from the joining generation must not mutate its replacement"
4486        );
4487        registry.remove_if_current(call_id, replacement);
4488    }
4489
4490    #[cfg(feature = "voip-runtime")]
4491    #[tokio::test]
4492    async fn call_link_waiting_room_cannot_cross_generations() {
4493        let (client, _transport) = crate::test_utils::create_iq_test_client().await;
4494        let creator = Jid::new("333333333333333", Server::Lid);
4495        let call_id = "WAITING-ROOM-GENERATION";
4496        let registry = client.call_registry();
4497        let stale = registry.insert(CallSession::new_outgoing(
4498            call_id,
4499            Jid::new(call_id, Server::Call),
4500            creator.clone(),
4501        ));
4502        let replacement = registry.insert(CallSession::new_outgoing(
4503            call_id,
4504            Jid::new(call_id, Server::Call),
4505            creator.clone(),
4506        ));
4507        let room = WaitingRoom::builder()
4508            .call_id(call_id.to_string())
4509            .call_creator(creator)
4510            .link_token("TEST-CALL-LINK".to_string())
4511            .media(CallLinkMedia::Audio)
4512            .enabled(true)
4513            .is_admin(true)
4514            .transaction_id(1)
4515            .users(Vec::new())
4516            .build();
4517
4518        assert_eq!(
4519            registry.apply_waiting_room_if_current(room, stale),
4520            wacore::voip::GroupStateApply::UnknownCall
4521        );
4522        assert!(
4523            registry
4524                .group_state_if_current(call_id, replacement)
4525                .and_then(|state| state.waiting_room().cloned())
4526                .is_none(),
4527            "a stale join cannot grant waiting-room admin state to its replacement"
4528        );
4529        registry.remove_if_current(call_id, replacement);
4530    }
4531
4532    #[cfg(feature = "voip-runtime")]
4533    #[tokio::test]
4534    async fn early_group_invite_accept_preserves_media_attachment_generation() {
4535        let (client, _transport) = crate::test_utils::create_iq_test_client().await;
4536        let creator = call_creator();
4537        let call_id = "ATTACHABLE-GROUP-INVITE";
4538        let update = GroupCallUpdate::builder()
4539            .call_id(call_id.to_string())
4540            .call_creator(creator.clone())
4541            .transaction_id(1)
4542            .media("audio".to_string())
4543            .connected_limit(32)
4544            .joinable(true)
4545            .av_upgradable(true)
4546            .rekey_requested(false)
4547            .participants(Vec::new())
4548            .build();
4549        let mut incoming = IncomingCall::new_for_test(
4550            creator.clone(),
4551            "ATTACHABLE-GROUP-INVITE-STANZA".to_string(),
4552            wacore::time::from_secs(1_766_847_151_i64).expect("valid ts"),
4553            CallAction::Offer {
4554                call_id: call_id.to_string(),
4555                call_creator: creator.clone(),
4556                caller_pn: None,
4557                caller_country_code: None,
4558                device_class: None,
4559                joinable: true,
4560                is_video: false,
4561                audio: Vec::new(),
4562                group_jid: None,
4563            },
4564        );
4565        incoming.group = Some(Box::new(update.clone()));
4566        let mut ringing = CallSession::new_incoming(call_id, creator.clone(), creator.clone());
4567        ringing.group = Some(update);
4568        let generation = client
4569            .call_registry()
4570            .insert_ringing_group_if_inactive(ringing)
4571            .expect("valid group snapshot")
4572            .expect("ringing invitation");
4573        incoming.set_ringing_generation(generation);
4574
4575        client
4576            .voip()
4577            .accept_group_invite(&incoming)
4578            .await
4579            .expect("early group invitation accept");
4580
4581        assert_eq!(
4582            client
4583                .call_registry()
4584                .ringing_group_generation(call_id, &creator),
4585            Some(generation),
4586            "the media accept builder must still be able to claim the exact ringing generation"
4587        );
4588        assert_eq!(
4589            client.call_registry().phase_if_current(call_id, generation),
4590            Some(CallPhase::Ringing)
4591        );
4592        assert!(client.call_registry().take_ringing(call_id));
4593        client
4594            .call_registry()
4595            .remove_if_current(call_id, generation);
4596    }
4597
4598    #[cfg(feature = "voip-runtime")]
4599    #[tokio::test]
4600    async fn group_invite_preaccept_and_accept_are_bound_to_the_retained_offer_generation() {
4601        let client = crate::test_utils::create_test_client().await;
4602        let retained_creator = call_creator();
4603        let replacement_creator = retained_creator.clone();
4604        let call_id = "REPLACED-GROUP-INVITE";
4605        let group_update = |creator: &Jid| {
4606            GroupCallUpdate::builder()
4607                .call_id(call_id.to_string())
4608                .call_creator(creator.clone())
4609                .transaction_id(1)
4610                .media("audio".to_string())
4611                .connected_limit(32)
4612                .joinable(true)
4613                .av_upgradable(true)
4614                .rekey_requested(false)
4615                .participants(Vec::new())
4616                .build()
4617        };
4618        let retained_update = group_update(&retained_creator);
4619        let mut incoming = IncomingCall::new_for_test(
4620            retained_creator.clone(),
4621            "RETAINED-GROUP-INVITE".to_string(),
4622            wacore::time::from_secs(1_766_847_151_i64).expect("valid ts"),
4623            CallAction::Offer {
4624                call_id: call_id.to_string(),
4625                call_creator: retained_creator.clone(),
4626                caller_pn: None,
4627                caller_country_code: None,
4628                device_class: None,
4629                joinable: true,
4630                is_video: false,
4631                audio: Vec::new(),
4632                group_jid: None,
4633            },
4634        );
4635        incoming.group = Some(Box::new(retained_update.clone()));
4636        let mut retained =
4637            CallSession::new_incoming(call_id, retained_creator.clone(), retained_creator);
4638        retained.group = Some(retained_update);
4639        let stale = client.call_registry().insert_ringing_group(retained);
4640        incoming.set_ringing_generation(stale);
4641
4642        let replacement_update = group_update(&replacement_creator);
4643        let mut replacement =
4644            CallSession::new_incoming(call_id, replacement_creator.clone(), replacement_creator);
4645        replacement.group = Some(replacement_update);
4646        let current = client.call_registry().insert_ringing_group(replacement);
4647
4648        assert!(matches!(
4649            client.voip().preaccept_group_invite(&incoming).await,
4650            Err(CallError::CallEndedDuringSetup)
4651        ));
4652        assert!(matches!(
4653            client.voip().accept_group_invite(&incoming).await,
4654            Err(CallError::CallEndedDuringSetup)
4655        ));
4656        assert_eq!(client.call_registry().generation_of(call_id), Some(current));
4657        assert_eq!(
4658            client.call_registry().phase_if_current(call_id, current),
4659            Some(CallPhase::Ringing)
4660        );
4661    }
4662
4663    #[cfg(feature = "voip-runtime")]
4664    #[tokio::test]
4665    async fn group_invite_accept_does_not_consume_a_replacement_generation() {
4666        struct GatedTransport {
4667            started: async_channel::Sender<()>,
4668            release: async_channel::Receiver<()>,
4669        }
4670
4671        #[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))]
4672        #[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)]
4673        impl crate::transport::Transport for GatedTransport {
4674            async fn send(&self, _data: Bytes) -> Result<(), anyhow::Error> {
4675                let _ = self.started.try_send(());
4676                self.release.recv().await?;
4677                Ok(())
4678            }
4679
4680            async fn disconnect(&self) {}
4681        }
4682
4683        let client = crate::test_utils::create_test_client().await;
4684        let creator = call_creator();
4685        let call_id = "ACTIVE-GROUP-INVITE";
4686        let update = GroupCallUpdate::builder()
4687            .call_id(call_id.to_string())
4688            .call_creator(creator.clone())
4689            .transaction_id(1)
4690            .media("audio".to_string())
4691            .connected_limit(32)
4692            .joinable(true)
4693            .av_upgradable(true)
4694            .rekey_requested(false)
4695            .participants(Vec::new())
4696            .build();
4697        let mut incoming = IncomingCall::new_for_test(
4698            creator.clone(),
4699            "GROUP-INVITE-STANZA".to_string(),
4700            wacore::time::from_secs(1_766_847_151_i64).expect("valid ts"),
4701            CallAction::Offer {
4702                call_id: call_id.to_string(),
4703                call_creator: creator.clone(),
4704                caller_pn: None,
4705                caller_country_code: None,
4706                device_class: None,
4707                joinable: true,
4708                is_video: false,
4709                audio: Vec::new(),
4710                group_jid: None,
4711            },
4712        );
4713        incoming.group = Some(Box::new(update.clone()));
4714        let mut ringing = CallSession::new_incoming(call_id, creator.clone(), creator.clone());
4715        ringing.group = Some(update.clone());
4716        let stale = client
4717            .call_registry()
4718            .insert_ringing_group_if_inactive(ringing)
4719            .expect("valid group snapshot")
4720            .expect("ringing invitation");
4721        incoming.set_ringing_generation(stale);
4722
4723        let (started_tx, started_rx) = async_channel::bounded(1);
4724        let (release_tx, release_rx) = async_channel::bounded(1);
4725        let noise_socket = crate::socket::NoiseSocket::new(
4726            Arc::new(crate::runtime_impl::TokioRuntime),
4727            Arc::new(GatedTransport {
4728                started: started_tx,
4729                release: release_rx,
4730            }),
4731            NoiseCipher::new(&[0u8; 32]).expect("valid key"),
4732            NoiseCipher::new(&[0u8; 32]).expect("valid key"),
4733        );
4734        *client.noise_socket.lock().await = Some(Arc::new(noise_socket));
4735
4736        let accept = tokio::spawn({
4737            let client = client.clone();
4738            let incoming = incoming.clone();
4739            async move { client.voip().accept_group_invite(&incoming).await }
4740        });
4741        started_rx.recv().await.expect("accept send entered");
4742        let mut replacement = CallSession::new_incoming(call_id, creator.clone(), creator);
4743        replacement.group = Some(update);
4744        let current = client.call_registry().insert_ringing_group(replacement);
4745        assert_ne!(current, stale);
4746        release_tx.send(()).await.expect("release accept send");
4747
4748        assert!(matches!(
4749            accept.await.expect("accept task"),
4750            Err(CallError::CallEndedDuringSetup)
4751        ));
4752        assert_eq!(client.call_registry().generation_of(call_id), Some(current));
4753        assert_eq!(
4754            client.call_registry().phase_if_current(call_id, current),
4755            Some(CallPhase::Ringing)
4756        );
4757        assert!(
4758            client.call_registry().take_ringing(call_id),
4759            "the stale accept must leave the replacement ringing"
4760        );
4761    }
4762
4763    #[cfg(feature = "voip-runtime")]
4764    #[tokio::test]
4765    async fn cancelling_call_link_request_removes_response_waiter() {
4766        let (client, _transport) = crate::test_utils::create_iq_test_client().await;
4767        let sent = client.wait_for_sent_node(crate::client::NodeFilter::tag("call"));
4768        let request_client = client.clone();
4769        let request = tokio::spawn(async move {
4770            request_client
4771                .voip()
4772                .create_call_link(CallLinkMedia::Audio)
4773                .await
4774        });
4775        let node = sent.await.expect("link_create request");
4776        let request_id = node
4777            .as_node_ref()
4778            .attrs()
4779            .optional_string("id")
4780            .expect("request id")
4781            .into_owned();
4782        assert!(
4783            client.response_waiters_guard().contains_key(&request_id),
4784            "the request must register its ACK waiter before sending"
4785        );
4786
4787        request.abort();
4788        assert!(
4789            request
4790                .await
4791                .expect_err("request should be cancelled")
4792                .is_cancelled()
4793        );
4794        tokio::task::yield_now().await;
4795        assert!(
4796            !client.response_waiters_guard().contains_key(&request_id),
4797            "cancelling a call-service request must not leak its waiter"
4798        );
4799    }
4800
4801    #[cfg(feature = "voip-runtime")]
4802    #[tokio::test]
4803    async fn cancelling_registered_call_link_join_removes_its_generation() {
4804        use wacore::handshake::NoiseCipher;
4805
4806        struct BlockingTransport {
4807            started: async_channel::Sender<()>,
4808        }
4809
4810        #[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))]
4811        #[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)]
4812        impl crate::transport::Transport for BlockingTransport {
4813            async fn send(&self, _data: Bytes) -> Result<(), anyhow::Error> {
4814                let _ = self.started.try_send(());
4815                futures::future::pending().await
4816            }
4817
4818            async fn disconnect(&self) {}
4819        }
4820
4821        let (client, _transport) = crate::test_utils::create_iq_test_client().await;
4822        client
4823            .persistence_manager()
4824            .process_command(crate::store::commands::DeviceCommand::SetLid(Some(
4825                Jid::new("111111111111111", Server::Lid).with_device(1),
4826            )))
4827            .await;
4828        let creator = Jid::new("333333333333333", Server::Lid);
4829        let join_sent = client.wait_for_sent_node(crate::client::NodeFilter::tag("call"));
4830        let join_client = client.clone();
4831        let join = tokio::spawn(async move {
4832            join_client
4833                .voip()
4834                .join_call_link_with_audio(
4835                    "CANCELLED-CALL-LINK",
4836                    CallLinkMedia::Audio,
4837                    AudioFormat::OPUS_16KHZ_60MS,
4838                )
4839                .await
4840        });
4841        let request = join_sent.await.expect("link_join request");
4842        let request_id = request
4843            .as_node_ref()
4844            .attrs()
4845            .optional_string("id")
4846            .expect("request id")
4847            .into_owned();
4848        let (started_tx, started_rx) = async_channel::bounded(1);
4849        let blocking_socket = crate::socket::NoiseSocket::new(
4850            Arc::new(crate::runtime_impl::TokioRuntime),
4851            Arc::new(BlockingTransport {
4852                started: started_tx,
4853            }),
4854            NoiseCipher::new(&[0u8; 32]).expect("valid key"),
4855            NoiseCipher::new(&[0u8; 32]).expect("valid key"),
4856        );
4857        *client.noise_socket.lock().await = Some(Arc::new(blocking_socket));
4858        crate::test_utils::answer_iq(
4859            &client,
4860            &request_id,
4861            &NodeBuilder::new("ack")
4862                .attr("class", "call")
4863                .attr("type", "link_join")
4864                .attr("id", request_id.as_str())
4865                .children([NodeBuilder::new("waiting_room")
4866                    .attr("call-id", "CANCELLED-CALL-ID")
4867                    .attr("call-creator", creator)
4868                    .attr("link-token", "CANCELLED-CALL-LINK")
4869                    .attr("media", "audio")
4870                    .attr("enabled", "1")
4871                    .attr("is_admin", "0")
4872                    .attr("transaction-id", "1")
4873                    .build()])
4874                .build(),
4875        )
4876        .await;
4877        started_rx.recv().await.expect("heartbeat send must start");
4878        assert!(
4879            client
4880                .call_registry()
4881                .generation_of("CANCELLED-CALL-ID")
4882                .is_some(),
4883            "the join must register before its heartbeat completes"
4884        );
4885
4886        join.abort();
4887        assert!(
4888            join.await
4889                .expect_err("join should be cancelled")
4890                .is_cancelled()
4891        );
4892        tokio::task::yield_now().await;
4893        assert_eq!(
4894            client.call_registry().generation_of("CANCELLED-CALL-ID"),
4895            None,
4896            "cancelling after registration must reap only that generation"
4897        );
4898    }
4899
4900    #[cfg(feature = "voip-runtime")]
4901    #[tokio::test]
4902    async fn cancelling_an_admitted_call_link_registration_sends_terminate() {
4903        let (client, sends) = make_client_with_count().await;
4904        let call_id = "CANCELLED-ADMITTED-CALL";
4905        let creator = Jid::new("333333333333333", Server::Lid);
4906        let mut session =
4907            CallSession::new_outgoing(call_id, Jid::new(call_id, Server::Call), creator.clone());
4908        let _ = session.transition_to(CallPhase::Calling);
4909        let _ = session.transition_to(CallPhase::Connecting);
4910        let registry = client.call_registry();
4911        let generation = registry.insert(session);
4912        let registration = super::CallLinkRegistrationGuard::new(
4913            &client,
4914            registry.clone(),
4915            call_id,
4916            creator,
4917            generation,
4918        );
4919
4920        drop(registration);
4921
4922        tokio::time::timeout(Duration::from_secs(2), async {
4923            while sends.load(Ordering::SeqCst) == 0 {
4924                tokio::task::yield_now().await;
4925            }
4926        })
4927        .await
4928        .expect("admitted cancellation must send a call-scoped terminate");
4929        assert_eq!(sends.load(Ordering::SeqCst), 1);
4930        assert_eq!(registry.generation_of(call_id), None);
4931    }
4932
4933    #[cfg(feature = "voip-runtime")]
4934    #[tokio::test]
4935    async fn call_link_join_reports_admission_committed_during_heartbeat() {
4936        use wacore::handshake::NoiseCipher;
4937
4938        struct GatedTransport {
4939            started: async_channel::Sender<()>,
4940            release: async_channel::Receiver<()>,
4941        }
4942
4943        #[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))]
4944        #[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)]
4945        impl crate::transport::Transport for GatedTransport {
4946            async fn send(&self, _data: Bytes) -> Result<(), anyhow::Error> {
4947                self.started
4948                    .send(())
4949                    .await
4950                    .map_err(|_| anyhow::anyhow!("heartbeat observer closed"))?;
4951                self.release
4952                    .recv()
4953                    .await
4954                    .map_err(|_| anyhow::anyhow!("heartbeat gate closed"))?;
4955                Ok(())
4956            }
4957
4958            async fn disconnect(&self) {}
4959        }
4960
4961        let (client, _transport) = crate::test_utils::create_iq_test_client().await;
4962        let own_lid = Jid::new("111111111111111", Server::Lid).with_device(1);
4963        client
4964            .persistence_manager()
4965            .process_command(crate::store::commands::DeviceCommand::SetLid(Some(
4966                own_lid.clone(),
4967            )))
4968            .await;
4969        let creator = Jid::new("333333333333333", Server::Lid);
4970        let sent = client.wait_for_sent_node(crate::client::NodeFilter::tag("call"));
4971        let join_client = client.clone();
4972        let join = tokio::spawn(async move {
4973            join_client
4974                .voip()
4975                .join_call_link_registration_with_audio(
4976                    "ADMISSION-RACE-LINK",
4977                    CallLinkMedia::Audio,
4978                    AudioFormat::OPUS_16KHZ_60MS,
4979                )
4980                .await
4981        });
4982        let request = sent.await.expect("link_join request");
4983        let request_id = request
4984            .as_node_ref()
4985            .attrs()
4986            .optional_string("id")
4987            .expect("request id")
4988            .into_owned();
4989        let (started_tx, started_rx) = async_channel::bounded(1);
4990        let (release_tx, release_rx) = async_channel::bounded(1);
4991        let gated_socket = crate::socket::NoiseSocket::new(
4992            Arc::new(crate::runtime_impl::TokioRuntime),
4993            Arc::new(GatedTransport {
4994                started: started_tx,
4995                release: release_rx,
4996            }),
4997            NoiseCipher::new(&[0u8; 32]).expect("valid key"),
4998            NoiseCipher::new(&[0u8; 32]).expect("valid key"),
4999        );
5000        *client.noise_socket.lock().await = Some(Arc::new(gated_socket));
5001        crate::test_utils::answer_iq(
5002            &client,
5003            &request_id,
5004            &NodeBuilder::new("ack")
5005                .attr("class", "call")
5006                .attr("type", "link_join")
5007                .attr("id", request_id.as_str())
5008                .children([NodeBuilder::new("waiting_room")
5009                    .attr("call-id", "ADMISSION-RACE-CALL")
5010                    .attr("call-creator", creator.clone())
5011                    .attr("link-token", "ADMISSION-RACE-LINK")
5012                    .attr("media", "audio")
5013                    .attr("enabled", "1")
5014                    .attr("is_admin", "0")
5015                    .attr("transaction-id", "1")
5016                    .build()])
5017                .build(),
5018        )
5019        .await;
5020        started_rx.recv().await.expect("heartbeat send started");
5021
5022        let registry = client.call_registry();
5023        let generation = registry
5024            .generation_of("ADMISSION-RACE-CALL")
5025            .expect("registered waiting-room generation");
5026        let mut participant =
5027            GroupCallParticipant::new(own_lid.to_non_ad(), vec![GroupCallDevice::new(own_lid)]);
5028        participant.state = Some("connected".to_string());
5029        let admitted = GroupCallUpdate::builder()
5030            .call_id("ADMISSION-RACE-CALL".to_string())
5031            .call_creator(creator)
5032            .transaction_id(2)
5033            .media("audio".to_string())
5034            .connected_limit(32)
5035            .joinable(true)
5036            .av_upgradable(true)
5037            .rekey_requested(false)
5038            .participants(vec![participant])
5039            .build();
5040        let transition_lock = registry
5041            .group_transition_lock("ADMISSION-RACE-CALL", generation)
5042            .expect("group transition lane");
5043        let transition_guard = transition_lock.lock().await;
5044        assert_eq!(
5045            registry.apply_group_update_if_current(admitted, generation),
5046            wacore::voip::GroupStateApply::Applied
5047        );
5048        assert_eq!(
5049            registry.phase_if_current("ADMISSION-RACE-CALL", generation),
5050            Some(CallPhase::Connecting)
5051        );
5052        drop(transition_guard);
5053        release_tx.send(()).await.expect("release heartbeat send");
5054
5055        let registration = join.await.expect("join task").expect("join response");
5056        assert_eq!(registration.generation, generation);
5057        assert!(!registration.join.in_waiting_room);
5058        assert_eq!(
5059            registration
5060                .join
5061                .group
5062                .as_ref()
5063                .map(|update| update.transaction_id),
5064            Some(2),
5065            "the public result must report admission committed during the heartbeat"
5066        );
5067        registry.remove_if_current("ADMISSION-RACE-CALL", generation);
5068    }
5069
5070    #[cfg(feature = "voip-runtime")]
5071    #[tokio::test]
5072    async fn immediately_admitted_call_link_preserves_token_and_origin_generation() {
5073        let (client, _transport) = crate::test_utils::create_iq_test_client().await;
5074        client
5075            .persistence_manager()
5076            .process_command(crate::store::commands::DeviceCommand::SetLid(Some(
5077                Jid::new("111111111111111", Server::Lid).with_device(1),
5078            )))
5079            .await;
5080        let creator = Jid::new("333333333333333", Server::Lid);
5081        let sent = client.wait_for_sent_node(crate::client::NodeFilter::tag("call"));
5082        let join_client = client.clone();
5083        let join = tokio::spawn(async move {
5084            join_client
5085                .voip()
5086                .join_call_link_registration_with_audio(
5087                    "REQUESTED-CALL-LINK",
5088                    CallLinkMedia::Video,
5089                    AudioFormat::OPUS_16KHZ_60MS,
5090                )
5091                .await
5092        });
5093        let request = sent.await.expect("link_join request");
5094        let request_id = request
5095            .as_node_ref()
5096            .attrs()
5097            .optional_string("id")
5098            .expect("request id")
5099            .into_owned();
5100        crate::test_utils::answer_iq(
5101            &client,
5102            &request_id,
5103            &NodeBuilder::new("ack")
5104                .attr("class", "call")
5105                .attr("type", "link_join")
5106                .attr("id", request_id.as_str())
5107                .children([
5108                    NodeBuilder::new("waiting_room")
5109                        .attr("call-id", "ADMITTED-CALL-ID")
5110                        .attr("call-creator", creator.clone())
5111                        .attr("link-token", "REQUESTED-CALL-LINK")
5112                        .attr("media", "video")
5113                        .attr("enabled", "1")
5114                        .attr("is_admin", "1")
5115                        .attr("transaction-id", "1")
5116                        .build(),
5117                    NodeBuilder::new("group_info")
5118                        .attr("call-id", "ADMITTED-CALL-ID")
5119                        .attr("call-creator", creator)
5120                        .attr("transaction-id", "1")
5121                        .attr("connected-limit", "32")
5122                        .attr("media", "video")
5123                        .build(),
5124                ])
5125                .build(),
5126        )
5127        .await;
5128
5129        let admitted_registration = join.await.expect("join task").expect("join response");
5130        let admitted = admitted_registration.join;
5131        assert_eq!(admitted.token, "REQUESTED-CALL-LINK");
5132        assert!(!admitted.in_waiting_room);
5133        let generation = client
5134            .call_registry()
5135            .generation_of("ADMITTED-CALL-ID")
5136            .expect("registered admitted call");
5137        assert_eq!(
5138            admitted_registration.generation, generation,
5139            "the join result must retain the generation it created"
5140        );
5141        assert!(
5142            client
5143                .call_registry()
5144                .group_state("ADMITTED-CALL-ID")
5145                .and_then(|state| state.waiting_room().cloned())
5146                .is_some_and(|room| room.is_admin && room.enabled),
5147            "admitted joins must retain waiting-room admin state from the ACK"
5148        );
5149        let replacement = client.call_registry().insert(CallSession::new_outgoing(
5150            "ADMITTED-CALL-ID",
5151            Jid::new("ADMITTED-CALL-ID", Server::Call),
5152            Jid::new("333333333333333", Server::Lid),
5153        ));
5154        assert_ne!(replacement, admitted_registration.generation);
5155        assert!(
5156            client
5157                .call_registry()
5158                .snapshot_if_current("ADMITTED-CALL-ID", admitted_registration.generation)
5159                .is_none(),
5160            "a stale starter must not attach through a replacement generation"
5161        );
5162        client
5163            .call_registry()
5164            .remove_if_current("ADMITTED-CALL-ID", replacement);
5165    }
5166}