Skip to main content

wechat_protocol/calls/
client.rs

1//! Stateful personal-voice signaling over the maintained native host transport.
2//! Account ownership, journal persistence, logout fences, cancellation and timeout
3//! supervision belong to the caller. These methods never retry an RPC.
4
5use super::{
6    app_frame::{self, AppFrameParameters},
7    app_session::{AppAuthority, AppSession, PendingAppAuth},
8    host_policy::CallHostRoute,
9    personal::{self, VoiceInputs, VoiceRoom},
10    relay::SelectedRelay,
11    signaling::{CallOperation, CommonRequest, CommonResponse},
12};
13use crate::{
14    protobuf::{self as p, Field, Value},
15    wire, AccountState, NativeClient,
16};
17use anyhow::{bail, ensure, Context, Result};
18use std::collections::{BTreeMap, BTreeSet};
19use zeroize::Zeroizing;
20
21/// A host RPC boundary for signaling. Implementations must authenticate transport
22/// replies, update accepted host cookies and perform exactly one submission.
23/// The default native implementation also enforces active per-CGI route policy.
24pub trait PersonalTransport {
25    fn call_rpc(
26        &self,
27        state: &mut AccountState,
28        route: CallHostRoute,
29        request: &[u8],
30    ) -> Result<Vec<u8>>;
31}
32/// A local transport stopped before handing business request bytes to its
33/// submission layer. An already-recorded prepared intent is preserved. Only
34/// this proven pre-submission case may use this marker;
35/// timeouts, server replies and other ambiguous outcomes must use other errors.
36#[derive(Debug)]
37pub struct CallRpcNotSubmitted;
38impl std::fmt::Display for CallRpcNotSubmitted {
39    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
40        f.write_str("Call control request was not submitted")
41    }
42}
43impl std::error::Error for CallRpcNotSubmitted {}
44
45/// A pre-answer LiveSync was superseded locally. The transport may have sent
46/// it, but its reply must never update the room cursor or account state.
47#[derive(Debug)]
48pub struct CallLiveSyncInterrupted;
49impl std::fmt::Display for CallLiveSyncInterrupted {
50    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
51        f.write_str("Incoming LiveSync was interrupted for Answer")
52    }
53}
54impl std::error::Error for CallLiveSyncInterrupted {}
55
56impl PersonalTransport for NativeClient {
57    fn call_rpc(
58        &self,
59        state: &mut AccountState,
60        route: CallHostRoute,
61        request: &[u8],
62    ) -> Result<Vec<u8>> {
63        self.experimental_call_rpc(state, route, request)
64    }
65}
66/// The room/control exchange is understood, but its selected media profile is
67/// outside this implementation. This is distinct from an ambiguous RPC outcome;
68/// the owner can end the known call once using its current control phase.
69#[derive(Debug)]
70pub struct UnsupportedCallMedia;
71impl std::fmt::Display for UnsupportedCallMedia {
72    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
73        f.write_str("Selected call media is unsupported")
74    }
75}
76impl std::error::Error for UnsupportedCallMedia {}
77
78mod incoming;
79mod termination;
80pub use super::personal::RemoteExitReason;
81pub use incoming::{IncomingPhase, NativeIncomingSession};
82pub use termination::{LocalStopReason, RemoteTermination};
83
84type AvSnapshot = BTreeMap<u16, (u32, Option<Vec<u8>>)>;
85
86/// Metadata and advertised voice support supplied by the embedding client.
87/// Request buffers are not returned codec/format/channel selections. This is
88/// independent client metadata, never a copied official profile/session.
89#[derive(Clone)]
90pub struct VoiceParameters {
91    pub sdk: Vec<u8>,
92    pub group: Vec<u8>,
93    pub device: Vec<u8>,
94    pub engine_capability: Vec<u8>,
95    pub channel_capability: Vec<u8>,
96    pub network_type: u32,
97    pub sim_type: u32,
98    pub video_length: u32,
99    pub video_ratio: u32,
100    pub nics: Vec<Vec<u8>>,
101    pub cloud_device_id: Vec<u8>,
102    pub app_config_version: u32,
103}
104impl VoiceParameters {
105    pub fn inputs(&self) -> VoiceInputs<'_> {
106        VoiceInputs {
107            sdk: &self.sdk,
108            group: &self.group,
109            device: &self.device,
110            engine_capability: &self.engine_capability,
111            channel_capability: &self.channel_capability,
112            network_type: self.network_type,
113            sim_type: self.sim_type,
114            video_length: self.video_length,
115            video_ratio: self.video_ratio,
116            nics: &self.nics,
117        }
118    }
119    fn sdk_version(&self) -> Result<u32> {
120        u32::try_from(number(&fields(&self.sdk)?, 1, None)?).map_err(Into::into)
121    }
122}
123
124#[derive(Clone, Copy, Debug, PartialEq, Eq)]
125pub struct TaskIds {
126    pub sdk: u32,
127    pub host: u32,
128}
129#[derive(Clone, Copy, Debug, PartialEq, Eq)]
130pub enum PersonalPhase {
131    Idle,
132    Inviting,
133    Ringing,
134    Joined,
135    Ending,
136    TerminalPending,
137    Uncertain,
138    Finished,
139}
140#[derive(Clone, Copy, Debug, PartialEq, Eq)]
141pub enum SyncProgress {
142    Waiting,
143    MediaReady,
144    Terminal(RemoteTermination),
145}
146
147#[derive(Clone)]
148struct Member {
149    member: u16,
150    status: u32,
151    openid: Vec<u8>,
152}
153enum ControlOrigin {
154    Outgoing { invite_id: u64 },
155    Incoming,
156}
157struct PersonalControl {
158    identity: VoiceRoom,
159    peer: String,
160    origin: ControlOrigin,
161    peer_joined: Option<bool>,
162    terminal: Option<RemoteTermination>,
163    invitation_pending: bool,
164    pending_recipients: Vec<String>,
165}
166
167#[derive(Clone)]
168struct ActiveRoom {
169    identity: VoiceRoom,
170    peer: String,
171    live_id: u64,
172    cookies: Vec<u8>,
173    strategy: Zeroizing<Vec<u8>>,
174    members: BTreeMap<String, Member>,
175    original_members: BTreeMap<String, Member>,
176    unbound_peer_openid: Option<Vec<u8>>,
177    member_sequence: u32,
178    cap_sequence: u32,
179    engine: Vec<u8>,
180    channel: Vec<u8>,
181    av_sequence: u32,
182    av: AvSnapshot,
183    media_taken: bool,
184}
185
186pub struct NegotiatedMedia {
187    pub relay: SelectedRelay,
188    pub peer_member: u16,
189    pub frames_per_packet: u8,
190}
191
192/// One generic app session with at most one personal call. It is not Clone and
193/// never auto-renews authentication, redials, fabricates a callee Join for caller
194/// context, or retries an uncertain action. Callers persist state.cookie changes
195/// even when subsequent app parsing fails.
196pub struct NativePersonalSession {
197    app: AppSession,
198    authority: AppAuthority,
199    own_username: String,
200    parameters: VoiceParameters,
201    phase: PersonalPhase,
202    active: Option<ActiveRoom>,
203    control: Option<PersonalControl>,
204    host_tasks: BTreeSet<u32>,
205    sdk_tasks: BTreeSet<u32>,
206}
207impl NativePersonalSession {
208    /// Perform exactly one generic7994 request after the host has journaled its
209    /// original intent. `device32` is fresh own app identity from a secure RNG.
210    pub fn authenticate(
211        client: &impl PersonalTransport,
212        state: &mut AccountState,
213        authority: AppAuthority,
214        host_task: u32,
215        device32: [u8; 32],
216        parameters: VoiceParameters,
217    ) -> Result<Self> {
218        ensure!(host_task != 0, "Missing app auth task");
219        parameters.inputs().validate()?;
220        ensure!(
221            number(&fields(&parameters.group)?, 5, None)? == 1,
222            "Selected caller sync requires group mode1"
223        );
224        let pending = PendingAppAuth::new(authority, device32)?;
225        let request = common(state, host_task, &pending.request_body()?)?;
226        let reply = client.call_rpc(state, CallHostRoute::AppAuth, &request)?;
227        let app = pending.complete(&reply, authority)?;
228        Ok(Self {
229            app,
230            authority,
231            own_username: state.own_username.clone(),
232            parameters,
233            phase: PersonalPhase::Idle,
234            active: None,
235            control: None,
236            host_tasks: BTreeSet::from([host_task]),
237            sdk_tasks: BTreeSet::new(),
238        })
239    }
240    pub fn phase(&self) -> PersonalPhase {
241        self.phase
242    }
243    /// Current authenticated remote microphone state while joined. This is
244    /// independent of membership, codec selection and whether media is audible.
245    pub fn peer_microphone_muted(&self) -> Option<bool> {
246        if self.phase != PersonalPhase::Joined {
247            return None;
248        }
249        let room = self.active.as_ref()?;
250        let member = room.members.get(&room.peer)?;
251        if member.status != 1 {
252            return None;
253        }
254        match room.av.get(&member.member)?.0 {
255            0 => Some(true),
256            1 => Some(false),
257            _ => None,
258        }
259    }
260    /// Allocated control identity survives a later media-admission failure. It
261    /// does not by itself authorize Cancel or Exit; the current phase still must
262    /// prove pending versus joined membership.
263    pub fn has_allocated_control(&self) -> bool {
264        self.control.is_some()
265    }
266    pub fn media_ready(&self, state: &AccountState, current: AppAuthority) -> Result<bool> {
267        self.check(state, current)?;
268        if self.phase != PersonalPhase::Joined {
269            return Ok(false);
270        }
271        match self.active.as_ref() {
272            Some(active) => ready(active, &self.own_username),
273            None => Ok(false),
274        }
275    }
276    fn check(&self, state: &AccountState, current: AppAuthority) -> Result<()> {
277        self.app.validate_authority(current)?;
278        ensure!(
279            current == self.authority && state.own_username == self.own_username,
280            "Personal call account changed"
281        );
282        Ok(())
283    }
284    fn reserve(&mut self, tasks: TaskIds) -> Result<()> {
285        ensure!(
286            tasks.host != 0
287                && tasks.sdk != 0
288                && self.host_tasks.len() < 4096
289                && self.sdk_tasks.len() < 4096,
290            "Invalid or exhausted call task IDs"
291        );
292        ensure!(
293            !self.host_tasks.contains(&tasks.host) && !self.sdk_tasks.contains(&tasks.sdk),
294            "Call task already spent"
295        );
296        self.host_tasks.insert(tasks.host);
297        self.sdk_tasks.insert(tasks.sdk);
298        Ok(())
299    }
300    fn host(
301        &mut self,
302        client: &impl PersonalTransport,
303        state: &mut AccountState,
304        route: CallHostRoute,
305        body: &[u8],
306        host_task: u32,
307    ) -> Result<Vec<u8>> {
308        let request = common(state, host_task, body)?;
309        let raw = client.call_rpc(state, route, &request)?;
310        let reply = CommonResponse::decode(&raw)?;
311        ensure!(
312            reply.return_code()? == 0,
313            "Personal call host operation rejected"
314        );
315        reply
316            .body
317            .ok_or_else(|| anyhow::anyhow!("Missing personal call host reply"))
318    }
319    pub fn invite(
320        &mut self,
321        client: &impl PersonalTransport,
322        state: &mut AccountState,
323        current: AppAuthority,
324        peer: &str,
325        invite_id: u64,
326        tasks: TaskIds,
327    ) -> Result<()> {
328        self.check(state, current)?;
329        ensure!(
330            self.phase == PersonalPhase::Idle && peer != self.own_username,
331            "Personal session busy or invalid peer"
332        );
333        self.reserve(tasks)?;
334        self.phase = PersonalPhase::Inviting;
335        let result = self.invite_once(client, state, peer, invite_id, tasks);
336        let understood_media_error = result
337            .as_ref()
338            .is_err_and(|error| error.is::<UnsupportedCallMedia>())
339            && self
340                .control
341                .as_ref()
342                .is_some_and(|control| control.peer_joined.is_some())
343            && matches!(self.phase, PersonalPhase::Ringing | PersonalPhase::Joined);
344        if result.is_err() && !understood_media_error {
345            self.phase = PersonalPhase::Uncertain;
346        }
347        result
348    }
349    fn invite_once(
350        &mut self,
351        client: &impl PersonalTransport,
352        state: &mut AccountState,
353        peer: &str,
354        invite_id: u64,
355        tasks: TaskIds,
356    ) -> Result<()> {
357        let request =
358            personal::invite(&self.parameters.inputs(), peer, invite_id, tasks.sdk, None)?;
359        let body = self.host(
360            client,
361            state,
362            request.operation.into(),
363            request.body(),
364            tasks.host,
365        )?;
366        let wrapper = personal::InviteResponse::decode(&body)?;
367        ensure!(
368            wrapper.custom_auth.as_ref().is_none_or(|v| v.is_empty()),
369            "Additional custom call auth unsupported"
370        );
371        ensure!(
372            wrapper.invite_results.len() <= 1
373                && wrapper.invite_results.iter().all(|(name, _)| name == peer),
374            "Personal invitation result identity mismatch"
375        );
376        ensure!(
377            wrapper.unknown_fields.is_empty(),
378            "Unknown personal allocation fields"
379        );
380        let own_member = u16::try_from(wrapper.own_member)?;
381        self.control = Some(PersonalControl {
382            identity: VoiceRoom {
383                room: wrapper.room,
384                room_key: wrapper.room_key,
385                own_member: wrapper.own_member,
386            },
387            peer: peer.to_owned(),
388            origin: ControlOrigin::Outgoing { invite_id },
389            peer_joined: None,
390            terminal: None,
391            invitation_pending: false,
392            pending_recipients: vec![],
393        });
394        // The personal host wrapper's per-target status is not the overall
395        // allocation outcome. The source reduces the initial full roster before
396        // AV/codec setup, including busy/rejected peers and our own removal.
397        let initial = fields(&wrapper.client_response)?;
398        only(&initial, &[1, 2, 3, 4, 5, 6, 7, 8, 9, 10])?;
399        let raw_members = data(&initial, 2)?;
400        let list = fields(raw_members)?;
401        only(&list, &[1, 2, 3])?;
402        p::optional_data(&list, 3)?;
403        let member_sequence = u32::try_from(number(&list, 1, Some(0))?)?;
404        ensure!(
405            member_sequence != 0,
406            "Missing initial current member sequence"
407        );
408        let members = parse_roster_entries(repeated(&list, 2, 16)?, &self.own_username, peer)?;
409        ensure!(
410            members
411                .get(&self.own_username)
412                .is_none_or(|own| own.member == own_member)
413                && members
414                    .get(peer)
415                    .is_none_or(|peer| peer.member != own_member),
416            "Initial personal member identity mismatch"
417        );
418        let peer_joined = members.get(peer).is_some_and(|peer| peer.status == 1);
419        let pending = members
420            .get(peer)
421            .is_some_and(|peer| matches!(peer.status, 2 | 3));
422        let control = self.control.as_mut().expect("allocated control");
423        control.peer_joined = Some(peer_joined);
424        control.invitation_pending = pending;
425        if pending {
426            control.pending_recipients = vec![peer.to_owned()];
427        }
428        let terminal = termination::classify(
429            termination::Origin::Caller,
430            1,
431            members.get(&self.own_username).map(|own| own.status),
432            members.get(peer).map(|peer| peer.status),
433            pending,
434        )?;
435        if let Some(action @ RemoteTermination::LocalStop(_)) = terminal {
436            control.terminal = Some(action);
437            self.phase = PersonalPhase::Finished;
438            return Ok(());
439        }
440        let live_id = number(&initial, 5, None)?;
441        ensure!(live_id != 0, "Missing initial live identity");
442        self.parameters.group = replace_group(&self.parameters.group, live_id, &wrapper.group_id)?;
443        if let Some(action) = terminal {
444            self.control.as_mut().expect("allocated control").terminal = Some(action);
445            self.phase = PersonalPhase::TerminalPending;
446            return Ok(());
447        }
448        ensure!(
449            members.len() == 2 && members[&self.own_username].status == 1,
450            "Incomplete initial personal roster"
451        );
452        self.phase = if peer_joined {
453            PersonalPhase::Joined
454        } else {
455            PersonalPhase::Ringing
456        };
457        let client_response = personal::JoinClientResponse::decode(&wrapper.client_response)?;
458        let strategy = fields(&client_response.strategy)?;
459        ensure!(
460            number(&strategy, 14, None)? & (1 << 36) == 0,
461            "Alternate caller sync unsupported"
462        );
463        let (cap_sequence, engine, channel) = cap(data(&strategy, 13)?)?;
464        let (av_sequence, av) = av(
465            client_response
466                .av_status
467                .as_deref()
468                .ok_or_else(|| anyhow::anyhow!("Missing initial AV snapshot"))?,
469            own_member,
470            members[peer].member,
471        )?;
472        let active = ActiveRoom {
473            identity: VoiceRoom {
474                room: wrapper.room,
475                room_key: wrapper.room_key,
476                own_member: wrapper.own_member,
477            },
478            peer: peer.to_owned(),
479            live_id: client_response.live_id,
480            cookies: client_response.cookies,
481            strategy: Zeroizing::new(client_response.strategy),
482            original_members: members.clone(),
483            unbound_peer_openid: None,
484            members,
485            member_sequence,
486            cap_sequence,
487            engine,
488            channel,
489            av_sequence,
490            av,
491            media_taken: false,
492        };
493        self.active = Some(active);
494        let _ = ready(
495            self.active.as_ref().expect("accepted room"),
496            &self.own_username,
497        )?;
498        Ok(())
499    }
500    /// One caller LiveSync with source-selected timeout. Install all supported
501    /// effects atomically, then cookies. Unknown or post-media reconfiguration
502    /// stops with the old cursor retained and no automatic next poll.
503    pub fn poll(
504        &mut self,
505        client: &impl PersonalTransport,
506        state: &mut AccountState,
507        current: AppAuthority,
508        tasks: TaskIds,
509    ) -> Result<SyncProgress> {
510        self.check(state, current)?;
511        ensure!(
512            matches!(self.phase, PersonalPhase::Ringing | PersonalPhase::Joined),
513            "Personal caller sync unavailable"
514        );
515        self.reserve(tasks)?;
516        let result = self.poll_once(client, state, tasks);
517        if result.as_ref().is_err_and(|error| {
518            !error.is::<CallRpcNotSubmitted>()
519                && !error.is::<CallLiveSyncInterrupted>()
520                && !error.is::<UnsupportedCallMedia>()
521        }) {
522            self.phase = PersonalPhase::Uncertain;
523        }
524        result
525    }
526    fn poll_once(
527        &mut self,
528        client: &impl PersonalTransport,
529        state: &mut AccountState,
530        tasks: TaskIds,
531    ) -> Result<SyncProgress> {
532        let active = self
533            .active
534            .as_ref()
535            .ok_or_else(|| anyhow::anyhow!("Missing personal room"))?;
536        let strategy = fields(&active.strategy)?;
537        let policies = strategy
538            .iter()
539            .filter(|v| v.number == 11)
540            .map(|v| match v.value {
541                Value::Varint(n) => u32::try_from(n).map_err(Into::into),
542                _ => bail!("Invalid call policy wire type"),
543            })
544            .collect::<Result<Vec<u32>>>()?;
545        let timeout = if policies.len() < 83 {
546            6
547        } else if (3..=50).contains(&policies[82]) {
548            policies[82]
549        } else {
550            10
551        };
552        let request = personal::live_sync(active.live_id, &active.cookies, timeout, tasks.sdk)?;
553        let body = self.host(
554            client,
555            state,
556            CallHostRoute::LiveSync,
557            request.body(),
558            tasks.host,
559        )?;
560        let response = personal::LiveSyncResponse::decode(&body)?;
561        ensure!(
562            response.unknown_fields.is_empty(),
563            "Unknown caller sync response field"
564        );
565        let control = self
566            .control
567            .as_ref()
568            .ok_or_else(|| anyhow::anyhow!("Missing personal control"))?;
569        let origin = match control.origin {
570            ControlOrigin::Outgoing { .. } => termination::Origin::Caller,
571            ControlOrigin::Incoming => termination::Origin::Callee,
572        };
573        let local_status = if matches!(control.origin, ControlOrigin::Incoming)
574            && self.phase != PersonalPhase::Joined
575        {
576            3
577        } else {
578            1
579        };
580        let original_invite =
581            matches!(control.origin,ControlOrigin::Outgoing{invite_id}if invite_id!=0);
582        let mut invitation_pending = control.invitation_pending;
583        let mut pending_recipients = control.pending_recipients.clone();
584        let mut terminal = None;
585        let mut next = self.active.as_ref().expect("active room").clone();
586        'notifications: for item in response.items {
587            ensure!(
588                item.kind == 1 && item.unknown_fields.is_empty(),
589                "Unsupported caller sync item"
590            );
591            let message = fields(&item.body)?;
592            only(&message, &[1, 2])?;
593            ensure!(
594                number(&message, 1, None)? == 10002,
595                "Unsupported caller sync command"
596            );
597            let notify = fields(data(&message, 2)?)?;
598            only(&notify, &[1, 2, 3, 4, 5])?;
599            ensure!(
600                number(&notify, 3, None)? == next.identity.room
601                    && matches_room_key(&notify, 4, next.identity.room_key)?,
602                "Caller notification room mismatch"
603            );
604            for raw in repeated(&notify, 5, 64)? {
605                let item = fields(raw)?;
606                only(&item, &[1, 2])?;
607                let kind = number(&item, 1, None)?;
608                let bytes = data(&item, 2)?;
609                match kind {
610                    10 => {
611                        let update = fields(bytes)?;
612                        only(&update, &[1, 2, 3, 4])?;
613                        ensure!(
614                            matches_room_key(&update, 1, next.identity.room)?
615                                && matches_room_key(&update, 2, next.identity.room_key)?,
616                            "Caller roster room mismatch"
617                        );
618                        let sequence = u32::try_from(number(&update, 3, Some(0))?)?;
619                        if sequence <= next.member_sequence {
620                            continue;
621                        }
622                        let members = parse_roster_entries(
623                            repeated(&update, 4, 16)?,
624                            &self.own_username,
625                            &next.peer,
626                        )?;
627                        for (name, member) in &members {
628                            if let Some(old) = next.original_members.get(name) {
629                                ensure!(
630                                    old.member == member.member && old.openid == member.openid,
631                                    "Caller member identity changed"
632                                );
633                            } else {
634                                ensure!(
635                                    matches!(origin, termination::Origin::Callee)
636                                        && name == &next.peer
637                                        && next.unbound_peer_openid.as_deref()
638                                            == Some(member.openid.as_slice())
639                                        && u32::from(member.member) != next.identity.own_member,
640                                    "Unbound caller member identity mismatch"
641                                );
642                                next.original_members.insert(name.clone(), member.clone());
643                                next.unbound_peer_openid = None;
644                            }
645                        }
646                        if members
647                            .get(&next.peer)
648                            .is_some_and(|member| member.status == 1)
649                        {
650                            invitation_pending = false;
651                            pending_recipients.clear();
652                        }
653                        let action = termination::classify(
654                            origin,
655                            local_status,
656                            members.get(&self.own_username).map(|m| m.status),
657                            members.get(&next.peer).map(|m| m.status),
658                            original_invite && invitation_pending && !pending_recipients.is_empty(),
659                        )?;
660                        // A later authoritative own-removal proof suppresses
661                        // an unsubmitted teardown; no later row revives us.
662                        if !matches!(terminal, Some(RemoteTermination::LocalStop(_)))
663                            && (terminal.is_none()
664                                || matches!(action, Some(RemoteTermination::LocalStop(_))))
665                        {
666                            terminal = action;
667                        }
668                        next.members = members;
669                        next.member_sequence = sequence;
670                        if matches!(terminal, Some(RemoteTermination::LocalStop(_))) {
671                            // Verified loss/handling of this local participant
672                            // ends its reducer. Do not revive it or turn later
673                            // ancillary items into a reason to stay on the call.
674                            // The transport retains the complete received reply.
675                            break 'notifications;
676                        }
677                    }
678                    11 => {
679                        let (sequence, engine, channel) = cap(bytes)?;
680                        if sequence <= next.cap_sequence {
681                            continue;
682                        }
683                        ensure!(
684                            !next.media_taken || terminal.is_some(),
685                            "Post-start call capability reconfiguration unsupported"
686                        );
687                        next.cap_sequence = sequence;
688                        next.engine = engine;
689                        next.channel = channel;
690                    }
691                    12 => {
692                        let (sequence, av) = av(
693                            bytes,
694                            next.identity.own_member as u16,
695                            next.original_members
696                                .get(&next.peer)
697                                .context("Call AV arrived before caller member binding")?
698                                .member,
699                        )?;
700                        if sequence <= next.av_sequence {
701                            continue;
702                        }
703                        ensure!(
704                            !next.media_taken
705                                || terminal.is_some()
706                                || microphone_update(&next, &av, &self.own_username),
707                            "Post-start call AV reconfiguration unsupported"
708                        );
709                        next.av_sequence = sequence;
710                        next.av = av;
711                    }
712                    17 if matches!(origin, termination::Origin::Callee) => {
713                        ensure!(
714                            number(&notify, 4, None)? == next.identity.room_key,
715                            "Callee relay notification room key mismatch"
716                        );
717                        // This advertises caller connections. A callee validates
718                        // the message without changing its admitted relay.
719                        validate_callee_relay_info(bytes)?;
720                    }
721                    _ => bail!("Unsupported caller notification kind"),
722                }
723            }
724        }
725        if let Some(action) = terminal {
726            next.cookies = response.cookies.unwrap_or_default();
727            let control = self.control.as_mut().expect("personal control");
728            control.peer_joined = Some(
729                next.members
730                    .get(&next.peer)
731                    .is_some_and(|member| member.status == 1),
732            );
733            control.invitation_pending = invitation_pending;
734            control.pending_recipients = pending_recipients;
735            control.terminal = Some(action);
736            self.phase = if matches!(action, RemoteTermination::LocalStop(_)) {
737                PersonalPhase::Finished
738            } else {
739                PersonalPhase::TerminalPending
740            };
741            self.active = Some(next);
742            return Ok(SyncProgress::Terminal(action));
743        }
744        let media_result = match if local_status == 3 {
745            Ok(false)
746        } else {
747            ready(&next, &self.own_username)
748        } {
749            Err(error) if !error.is::<UnsupportedCallMedia>() => return Err(error),
750            result => result,
751        };
752        // A supported full batch can publish its control state. Unsupported
753        // media retains the previous cursor and permits no further media work.
754        if media_result.is_ok() {
755            next.cookies = response.cookies.unwrap_or_default();
756        }
757        let joined = next
758            .members
759            .get(&next.peer)
760            .is_some_and(|member| member.status == 1);
761        self.control
762            .as_mut()
763            .ok_or_else(|| anyhow::anyhow!("Missing allocated control"))?
764            .peer_joined = Some(joined);
765        let control = self.control.as_mut().expect("personal control");
766        control.invitation_pending = invitation_pending;
767        control.pending_recipients = pending_recipients;
768        self.phase = if (joined || !invitation_pending) && local_status == 1 {
769            PersonalPhase::Joined
770        } else {
771            PersonalPhase::Ringing
772        };
773        self.active = Some(next);
774        Ok(if media_result? {
775            SyncProgress::MediaReady
776        } else {
777            SyncProgress::Waiting
778        })
779    }
780    /// Move one admitted media context out only after current roster/capability/AV
781    /// state agrees. This never constructs a synthetic callee Join response.
782    pub fn take_media(
783        &mut self,
784        state: &AccountState,
785        current: AppAuthority,
786        connection_index: usize,
787        udp_index: usize,
788    ) -> Result<NegotiatedMedia> {
789        self.check(state, current)?;
790        let active = self
791            .active
792            .as_mut()
793            .ok_or_else(|| anyhow::anyhow!("Missing personal room"))?;
794        ensure!(
795            self.phase == PersonalPhase::Joined
796                && !active.media_taken
797                && ready(active, &self.own_username)?,
798            "Personal media not ready or already taken"
799        );
800        let relay = SelectedRelay::from_strategy(
801            active.identity.room,
802            active.identity.room_key,
803            active.identity.own_member as u16,
804            &active.strategy,
805            connection_index,
806            udp_index,
807        )?;
808        active.media_taken = true;
809        Ok(NegotiatedMedia {
810            relay,
811            peer_member: active.members[&active.peer].member,
812            frames_per_packet: (profile_shape(&active.engine, &active.channel)?[1] >> 8) as u8,
813        })
814    }
815    /// Cancel the original unanswered personal invitation once. This selected
816    /// client owns one original pending peer; no unrelated recipients are added.
817    pub fn cancel(
818        &mut self,
819        client: &impl PersonalTransport,
820        state: &mut AccountState,
821        current: AppAuthority,
822        tasks: TaskIds,
823    ) -> Result<()> {
824        self.check(state, current)?;
825        ensure!(
826            self.phase == PersonalPhase::Ringing,
827            "Personal invitation no longer cancellable"
828        );
829        self.reserve(tasks)?;
830        self.phase = PersonalPhase::Ending;
831        let result = (|| {
832            let control = self
833                .control
834                .as_ref()
835                .ok_or_else(|| anyhow::anyhow!("Missing allocated control"))?;
836            let room = control.identity;
837            let ControlOrigin::Outgoing { invite_id } = control.origin else {
838                bail!("Incoming calls cannot use caller Cancel")
839            };
840            ensure!(
841                control.peer_joined == Some(false)
842                    && control.invitation_pending
843                    && !control.pending_recipients.is_empty(),
844                "Pending invitation provenance missing or already joined"
845            );
846            let request = personal::cancel(
847                &self.parameters.inputs(),
848                room.room,
849                room.room_key,
850                invite_id,
851                &control.pending_recipients,
852                personal::CancelReason::Local,
853                tasks.sdk,
854            )?;
855            let body = self.host(
856                client,
857                state,
858                CallHostRoute::Cancel,
859                request.body(),
860                tasks.host,
861            )?;
862            let outer = fields(&body)?;
863            let cancelled = fields(data(&outer, 1)?)?;
864            ensure!(
865                number(&cancelled, 1, None)? == room.room
866                    && number(&cancelled, 2, None)? == room.room_key,
867                "Personal cancellation room mismatch"
868            );
869            Ok(())
870        })();
871        self.phase = if result.is_ok() {
872            PersonalPhase::Finished
873        } else {
874            PersonalPhase::Uncertain
875        };
876        result
877    }
878    /// Complete one source-classified remote termination. This is separate from
879    /// local Hangup: the peer may already be absent or nonjoined. The embedding
880    /// owner must stop media before calling. Failed replies remain uncertain.
881    pub fn finish_remote(
882        &mut self,
883        client: &impl PersonalTransport,
884        state: &mut AccountState,
885        current: AppAuthority,
886        tasks: TaskIds,
887        talk_success: bool,
888    ) -> Result<()> {
889        self.check(state, current)?;
890        ensure!(
891            self.phase == PersonalPhase::TerminalPending,
892            "No pending remote teardown"
893        );
894        self.reserve(tasks)?;
895        self.phase = PersonalPhase::Ending;
896        let result = (|| {
897            let control = self
898                .control
899                .as_mut()
900                .ok_or_else(|| anyhow::anyhow!("Missing remote termination control"))?;
901            let action = control
902                .terminal
903                .take()
904                .ok_or_else(|| anyhow::anyhow!("Remote termination already spent"))?;
905            let identity = control.identity;
906            match action {
907                RemoteTermination::Exit(reason) => {
908                    let request = personal::exit_remote(
909                        &self.parameters.inputs(),
910                        identity,
911                        reason,
912                        talk_success,
913                        tasks.sdk,
914                    )?;
915                    let parameters = AppFrameParameters {
916                        sdk_version: self.parameters.sdk_version()?,
917                        cloud_device_id: &self.parameters.cloud_device_id,
918                        host_client_version: state.device.client_version as u32,
919                        app_config_version: self.parameters.app_config_version,
920                    };
921                    let body = app_frame::prepare_transfer(
922                        &self.app,
923                        current,
924                        CallOperation::Exit,
925                        request.body.as_deref().expect("prepared remote Exit"),
926                        &parameters,
927                    )?;
928                    let reply =
929                        self.host(client, state, CallHostRoute::Transfer, &body, tasks.host)?;
930                    let exit = fields(&app_frame::decode_transfer_reply(
931                        &self.app,
932                        current,
933                        CallOperation::Exit,
934                        &reply,
935                    )?)?;
936                    ensure!(
937                        number(&exit, 1, None)? == identity.room
938                            && number(&exit, 2, None)? == identity.room_key
939                            && number(&exit, 3, None)? == u64::from(identity.own_member),
940                        "Remote Exit identity mismatch"
941                    );
942                }
943                RemoteTermination::NoResponseCancel => {
944                    let ControlOrigin::Outgoing { invite_id } = control.origin else {
945                        bail!("Callee cannot cancel an original invitation")
946                    };
947                    ensure!(
948                        control.invitation_pending
949                            && !control.pending_recipients.is_empty()
950                            && control
951                                .pending_recipients
952                                .iter()
953                                .all(|recipient| recipient == &control.peer),
954                        "Missing current pending-recipient provenance"
955                    );
956                    control.invitation_pending = false;
957                    let recipients = std::mem::take(&mut control.pending_recipients);
958                    let request = personal::cancel(
959                        &self.parameters.inputs(),
960                        identity.room,
961                        identity.room_key,
962                        invite_id,
963                        &recipients,
964                        personal::CancelReason::NoResponse,
965                        tasks.sdk,
966                    )?;
967                    let body = self.host(
968                        client,
969                        state,
970                        CallHostRoute::Cancel,
971                        request.body(),
972                        tasks.host,
973                    )?;
974                    let outer = fields(&body)?;
975                    let cancelled = fields(data(&outer, 1)?)?;
976                    ensure!(
977                        number(&cancelled, 1, None)? == identity.room
978                            && number(&cancelled, 2, None)? == identity.room_key,
979                        "Remote cancellation room mismatch"
980                    );
981                }
982                RemoteTermination::LocalStop(_) => {
983                    bail!("Local-only stop must not submit a teardown")
984                }
985            }
986            Ok(())
987        })();
988        self.phase = if result.is_ok() {
989            PersonalPhase::Finished
990        } else {
991            PersonalPhase::Uncertain
992        };
993        result
994    }
995
996    /// Submit joined Exit once. `talk_success` is the independently established
997    /// source predicate, not inferred here from a phone timer. Reliable peer
998    /// notice and canonical history remain distinct from accepted server Exit.
999    pub fn exit(
1000        &mut self,
1001        client: &impl PersonalTransport,
1002        state: &mut AccountState,
1003        current: AppAuthority,
1004        tasks: TaskIds,
1005        talk_success: bool,
1006    ) -> Result<()> {
1007        self.check(state, current)?;
1008        ensure!(
1009            self.phase == PersonalPhase::Joined,
1010            "Personal call not joined"
1011        );
1012        self.reserve(tasks)?;
1013        self.phase = PersonalPhase::Ending;
1014        let result = (|| {
1015            let control = self
1016                .control
1017                .as_ref()
1018                .ok_or_else(|| anyhow::anyhow!("Missing allocated control"))?;
1019            ensure!(
1020                matches!(control.origin, ControlOrigin::Incoming) || !control.invitation_pending,
1021                "Joined control state unknown"
1022            );
1023            let identity = control.identity;
1024            let request = personal::exit(
1025                &self.parameters.inputs(),
1026                identity,
1027                1,
1028                talk_success,
1029                tasks.sdk,
1030            )?;
1031            let parameters = AppFrameParameters {
1032                sdk_version: self.parameters.sdk_version()?,
1033                cloud_device_id: &self.parameters.cloud_device_id,
1034                host_client_version: state.device.client_version as u32,
1035                app_config_version: self.parameters.app_config_version,
1036            };
1037            let body = app_frame::prepare_transfer(
1038                &self.app,
1039                current,
1040                CallOperation::Exit,
1041                request.body.as_deref().expect("prepared Exit body"),
1042                &parameters,
1043            )?;
1044            let delegate = self.host(client, state, CallHostRoute::Transfer, &body, tasks.host)?;
1045            let exit = fields(&app_frame::decode_transfer_reply(
1046                &self.app,
1047                current,
1048                CallOperation::Exit,
1049                &delegate,
1050            )?)?;
1051            ensure!(
1052                number(&exit, 1, None)? == identity.room
1053                    && number(&exit, 2, None)? == identity.room_key
1054                    && number(&exit, 3, None)? == u64::from(identity.own_member),
1055                "Personal Exit identity mismatch"
1056            );
1057            Ok(())
1058        })();
1059        self.phase = if result.is_ok() {
1060            PersonalPhase::Finished
1061        } else {
1062            PersonalPhase::Uncertain
1063        };
1064        result
1065    }
1066}
1067
1068fn common(state: &AccountState, host_task: u32, body: &[u8]) -> Result<Vec<u8>> {
1069    CommonRequest {
1070        base_request: Some(wire::base_request(&state.device, state.uin, host_task)?),
1071        body: Some(body.to_vec()),
1072        unknown_fields: vec![],
1073    }
1074    .encode()
1075}
1076fn fields(raw: &[u8]) -> Result<Vec<Field>> {
1077    ensure!(raw.len() <= 64 * 1024, "Personal signaling body too large");
1078    super::signaling::decode_fields(raw)
1079}
1080fn data(values: &[Field], n: u32) -> Result<&[u8]> {
1081    p::optional_data(values, n)?.ok_or_else(|| anyhow::anyhow!("Missing personal bytes field {n}"))
1082}
1083fn number(values: &[Field], n: u32, default: Option<u64>) -> Result<u64> {
1084    match p::optional(values, n)? {
1085        Some(Value::Varint(v)) => Ok(*v),
1086        None => default.ok_or_else(|| anyhow::anyhow!("Missing personal integer {n}")),
1087        _ => bail!("Invalid personal integer {n}"),
1088    }
1089}
1090fn repeated(values: &[Field], n: u32, limit: usize) -> Result<Vec<&[u8]>> {
1091    let mut out = Vec::new();
1092    for f in values.iter().filter(|v| v.number == n) {
1093        ensure!(out.len() < limit, "Too many personal entries");
1094        if let Value::Bytes(raw) = &f.value {
1095            out.push(raw.as_slice());
1096        } else {
1097            bail!("Invalid personal entry wire type");
1098        }
1099    }
1100    Ok(out)
1101}
1102fn only(values: &[Field], allowed: &[u32]) -> Result<()> {
1103    ensure!(
1104        values.iter().all(|f| allowed.contains(&f.number)),
1105        "Unknown personal signaling field"
1106    );
1107    Ok(())
1108}
1109fn validate_callee_relay_info(raw: &[u8]) -> Result<()> {
1110    let info = fields(raw)?;
1111    only(&info, &[1, 2])?;
1112    for raw in repeated(&info, 1, 32)? {
1113        let connection = fields(raw)?;
1114        only(&connection, &[1, 2, 3, 4, 5])?;
1115        i32::try_from(number(&connection, 1, Some(0))? as i64)?;
1116        i32::try_from(number(&connection, 3, Some(0))? as i64)?;
1117        if let Some(nic) = p::optional_data(&connection, 2)? {
1118            std::str::from_utf8(nic)?;
1119        }
1120        for addr in repeated(&connection, 4, 32)?
1121            .into_iter()
1122            .chain(repeated(&connection, 5, 32)?)
1123        {
1124            validate_advertised_addr(addr)?;
1125        }
1126    }
1127    for addr in repeated(&info, 2, 32)? {
1128        validate_advertised_addr(addr)?;
1129    }
1130    Ok(())
1131}
1132fn validate_advertised_addr(raw: &[u8]) -> Result<()> {
1133    let addr = fields(raw)?;
1134    only(&addr, &[1, 2])?;
1135    if let Some(ip) = p::optional_data(&addr, 1)? {
1136        std::str::from_utf8(ip)?;
1137    }
1138    u32::try_from(number(&addr, 2, Some(0))?)?;
1139    Ok(())
1140}
1141fn matches_room_key(values: &[Field], n: u32, expected: u64) -> Result<bool> {
1142    let value = number(values, n, Some(0))?;
1143    Ok(value == 0 || value == expected)
1144}
1145fn roster(raw: &[u8], own: &str, peer: &str, own_member: u16) -> Result<BTreeMap<String, Member>> {
1146    let values = fields(raw)?;
1147    roster_entries(repeated(&values, 2, 16)?, own, peer, own_member)
1148}
1149fn roster_entries(
1150    entries: Vec<&[u8]>,
1151    own: &str,
1152    peer: &str,
1153    own_member: u16,
1154) -> Result<BTreeMap<String, Member>> {
1155    let members = parse_roster_entries(entries, own, peer)?;
1156    ensure!(
1157        members.len() == 2
1158            && members
1159                .get(own)
1160                .is_some_and(|m| m.member == own_member && m.status == 1)
1161            && members
1162                .get(peer)
1163                .is_some_and(|m| (1..=3).contains(&m.status)),
1164        "Incomplete or unsupported personal roster"
1165    );
1166    Ok(members)
1167}
1168fn parse_roster_entries(
1169    entries: Vec<&[u8]>,
1170    own: &str,
1171    peer: &str,
1172) -> Result<BTreeMap<String, Member>> {
1173    ensure!(entries.len() <= 2, "Unsupported direct roster size");
1174    let mut members = BTreeMap::new();
1175    let mut ids = BTreeSet::new();
1176    for raw in entries {
1177        let values = fields(raw)?;
1178        only(&values, &[1, 2, 3, 4, 5, 6, 7, 8, 9])?;
1179        for tag in [4, 5, 9] {
1180            if let Some(Value::Varint(value)) = p::optional(&values, tag)? {
1181                u32::try_from(*value)?;
1182            } else {
1183                ensure!(
1184                    p::optional(&values, tag)?.is_none(),
1185                    "Invalid member scalar wire type"
1186                );
1187            }
1188        }
1189        for tag in [6, 8] {
1190            if let Some(value) = p::optional_data(&values, tag)? {
1191                std::str::from_utf8(value)?;
1192            }
1193        }
1194        let name = std::str::from_utf8(data(&values, 7)?)?;
1195        ensure!(
1196            (name == own || name == peer) && !members.contains_key(name),
1197            "Unexpected personal roster identity"
1198        );
1199        let member = u16::try_from(number(&values, 2, None)?)?;
1200        let status = u32::try_from(number(&values, 3, None)?)?;
1201        let openid = data(&values, 1)?.to_vec();
1202        ensure!(
1203            !openid.is_empty() && ids.insert(member),
1204            "Incomplete or duplicate personal member"
1205        );
1206        members.insert(
1207            name.to_owned(),
1208            Member {
1209                member,
1210                status,
1211                openid,
1212            },
1213        );
1214    }
1215    Ok(members)
1216}
1217fn cap(raw: &[u8]) -> Result<(u32, Vec<u8>, Vec<u8>)> {
1218    let values = fields(raw)?;
1219    only(&values, &[1, 2])?;
1220    let sequence = u32::try_from(number(&values, 1, Some(0))?)?;
1221    ensure!(sequence != 0, "Missing current call capability sequence");
1222    let content = fields(data(&values, 2)?)?;
1223    only(&content, &[1, 2])?;
1224    let engine = data(&content, 1)?.to_vec();
1225    let channel = data(&content, 2)?.to_vec();
1226    ensure!(
1227        !engine.is_empty() && !channel.is_empty(),
1228        "Partial call capabilities unsupported"
1229    );
1230    profile_shape(&engine, &channel)?;
1231    Ok((sequence, engine, channel))
1232}
1233fn av(raw: &[u8], own: u16, peer: u16) -> Result<(u32, AvSnapshot)> {
1234    let values = fields(raw)?;
1235    only(&values, &[1, 3, 4])?;
1236    let sequence = u32::try_from(number(&values, 3, Some(0))?)?;
1237    let mut members = BTreeMap::new();
1238    for raw in repeated(&values, 4, 2)? {
1239        let values = fields(raw)?;
1240        only(&values, &[1, 2, 3])?;
1241        let member = u16::try_from(number(&values, 1, None)?)?;
1242        ensure!(
1243            [own, peer].contains(&member) && !members.contains_key(&member),
1244            "Unexpected AV member"
1245        );
1246        members.insert(
1247            member,
1248            (
1249                u32::try_from(number(&values, 2, None)?)?,
1250                p::optional_data(&values, 3)?
1251                    .map(|info| {
1252                        ensure!(info.len() <= 1024, "Microphone metadata too large");
1253                        Ok(info.to_vec())
1254                    })
1255                    .transpose()?,
1256            ),
1257        );
1258    }
1259    Ok((sequence, members))
1260}
1261fn profile_shape(engine: &[u8], channel: &[u8]) -> Result<[u64; 6]> {
1262    let ef = fields(engine)?;
1263    let cf = fields(channel)?;
1264    let mut seen = BTreeSet::new();
1265    for field in &ef {
1266        ensure!(
1267            (1..=42).contains(&field.number) && field.number != 41 && seen.insert(field.number),
1268            "Unsupported engine capability shape"
1269        );
1270        let Value::Varint(value) = field.value else {
1271            bail!("Invalid engine capability wire type")
1272        };
1273        if [1, 2, 3, 4, 25, 26, 27].contains(&field.number) {
1274            i32::try_from(value as i64)?;
1275        } else {
1276            u32::try_from(value)?;
1277        }
1278    }
1279    only(&cf, &[1, 2])?;
1280    Ok([
1281        number(&ef, 5, Some(0))?,
1282        number(&ef, 6, Some(0))?,
1283        number(&ef, 7, Some(0))?,
1284        number(&ef, 39, Some(0))?,
1285        u64::from(u32::try_from(number(&cf, 1, Some(0))?)?),
1286        u64::from(u32::try_from(number(&cf, 2, Some(0))?)?),
1287    ])
1288}
1289fn profile(engine: &[u8], channel: &[u8]) -> Result<()> {
1290    if !matches!(
1291        profile_shape(engine, channel)?,
1292        [17, 0x106 | 0x206, 0x800000, 0, 2, 3]
1293    ) {
1294        return Err(UnsupportedCallMedia.into());
1295    }
1296    Ok(())
1297}
1298/// A microphone change does not replace the already admitted media context.
1299/// Keep microphone metadata opaque: it cannot reset codec, crypto or counters.
1300fn microphone_update(room: &ActiveRoom, av: &AvSnapshot, own: &str) -> bool {
1301    let Some(local) = room.members.get(own) else {
1302        return false;
1303    };
1304    let Some(peer) = room.members.get(&room.peer) else {
1305        return false;
1306    };
1307    av.len() == 2
1308        && av
1309            .get(&local.member)
1310            .is_some_and(|(status, info)| *status == 1 && info.is_none())
1311        && av.get(&peer.member).is_some_and(|(status, _)| *status <= 1)
1312}
1313fn ready(room: &ActiveRoom, own: &str) -> Result<bool> {
1314    // Current capability bytes are validated even while a peer/AV stream is
1315    // pending. Codec0 is then unselected data, never decoder permission.
1316    profile_shape(&room.engine, &room.channel)?;
1317    let own_id = room
1318        .members
1319        .get(own)
1320        .context("Missing current own member")?
1321        .member;
1322    let peer = room
1323        .members
1324        .get(&room.peer)
1325        .context("Missing current peer member")?;
1326    if peer.status != 1
1327        || room.av.len() != 2
1328        || !(room.media_taken && microphone_update(room, &room.av, own)
1329            || [own_id, peer.member].iter().all(|id| {
1330                room.av
1331                    .get(id)
1332                    .is_some_and(|(status, info)| *status == 1 && info.is_none())
1333            }))
1334    {
1335        return Ok(false);
1336    }
1337    profile(&room.engine, &room.channel)?;
1338    Ok(true)
1339}
1340fn replace_group(raw: &[u8], live_id: u64, group: &str) -> Result<Vec<u8>> {
1341    let values = fields(raw)?;
1342    let mut encoded = Vec::new();
1343    for field in values {
1344        let value = match field.number {
1345            2 => p::uint(2, live_id),
1346            4 => p::bytes(4, group.as_bytes()),
1347            _ => match field.value {
1348                Value::Varint(v) => p::uint(field.number, v),
1349                Value::Bytes(v) => p::bytes(field.number, &v),
1350                Value::Fixed64(_) | Value::Fixed32(_) => bail!("Unsupported group field wire type"),
1351            },
1352        };
1353        encoded.push(value);
1354    }
1355    p::message(&encoded)
1356}
1357
1358#[cfg(test)]
1359mod tests {
1360    use super::*;
1361
1362    pub(super) struct FakeTransport(
1363        pub(super) std::cell::RefCell<std::collections::VecDeque<(CallHostRoute, Vec<u8>)>>,
1364    );
1365    impl PersonalTransport for FakeTransport {
1366        fn call_rpc(
1367            &self,
1368            state: &mut AccountState,
1369            route: CallHostRoute,
1370            request: &[u8],
1371        ) -> Result<Vec<u8>> {
1372            let (expected, reply) = self
1373                .0
1374                .borrow_mut()
1375                .pop_front()
1376                .ok_or_else(|| anyhow::anyhow!("Unexpected fake call RPC"))?;
1377            ensure!(route == expected, "Unexpected fake call route");
1378            let request = CommonRequest::decode(request)?;
1379            ensure!(
1380                request.base_request.is_some() && request.body.is_some(),
1381                "Missing host common wrapper"
1382            );
1383            source_request(route, &request)?;
1384            state.cookie = vec![route.command() as u8];
1385            Ok(reply)
1386        }
1387    }
1388    /// Independent selected request contract at the actual session/transport
1389    /// boundary. Literal field numbers and command/path values are not obtained
1390    /// by serializing another production request or reading its enum accessors.
1391    fn source_request(route: CallHostRoute, request: &CommonRequest) -> Result<()> {
1392        let base = fields(
1393            request
1394                .base_request
1395                .as_deref()
1396                .context("Missing host base")?,
1397        )?;
1398        assert_eq!(data(&base, 1)?, &[0]);
1399        assert_eq!(number(&base, 2, None)?, 1);
1400        assert!(number(&base, 6, None)? > 0);
1401        let body = fields(request.body.as_deref().context("Missing host body")?)?;
1402        match route {
1403            CallHostRoute::AppAuth => {
1404                let device = data(&fields(data(&body, 1)?)?, 1)?.to_vec();
1405                assert_eq!(device.len(), 32);
1406                let raw = request.body.as_deref().unwrap();
1407                assert_eq!(&raw[..4], &[10, 34, 10, 32]);
1408                assert_eq!(raw.len(), 36);
1409            }
1410            CallHostRoute::Invite => {
1411                assert_eq!(data(&body, 1)?, b"peer");
1412                assert_eq!(number(&body, 2, None)?, 2);
1413                assert!(number(&body, 5, None)? > 0);
1414                assert!(p::optional(&body, 6)?.is_none());
1415                assert_eq!(data(&body, 7)?, b"voip-2p");
1416                let client = fields(data(&body, 4)?)?;
1417                assert_eq!(number(&client, 1, None)?, 0x050000cb);
1418                assert_eq!(data(&client, 2)?, b"test");
1419                assert_eq!(number(&client, 3, None)?, 0);
1420                assert_eq!(number(&client, 4, None)?, 1);
1421                let caps = fields(data(&client, 5)?)?;
1422                assert_eq!(number(&fields(data(&caps, 1)?)?, 5, None)?, 0x40000);
1423                assert_eq!(data(&caps, 2)?.len(), 12);
1424            }
1425            CallHostRoute::LiveSync => {
1426                assert_eq!(number(&body, 1, None)?, 99);
1427                data(&body, 2)?;
1428                assert_eq!(data(&body, 4)?, b"voip-2p");
1429                assert_eq!(number(&body, 5, None)?, 6);
1430            }
1431            CallHostRoute::Cancel => {
1432                let inner = fields(data(&body, 1)?)?;
1433                assert_eq!(number(&inner, 3, None)?, 7);
1434                assert_eq!(number(&inner, 4, None)?, 9);
1435                assert_eq!(number(&inner, 5, None)?, number(&body, 2, None)?);
1436                assert!([1, 2].contains(&number(&inner, 6, None)?));
1437                assert_eq!(number(&body, 3, None)?, 7);
1438                assert_eq!(data(&body, 4)?, b"peer");
1439                assert_eq!(data(&body, 5)?, b"voip-2p");
1440            }
1441            CallHostRoute::RingingAck => {
1442                let inner = fields(data(&body, 1)?)?;
1443                assert_eq!(number(&inner, 4, None)?, 1);
1444                assert_eq!(number(&inner, 5, None)?, 7);
1445                assert_eq!(number(&inner, 6, None)?, 9);
1446                assert_eq!(number(&inner, 7, None)?, 0);
1447                assert_eq!(number(&inner, 13, None)?, 1);
1448                let caps = fields(data(&inner, 12)?)?;
1449                assert_eq!(number(&fields(data(&caps, 1)?)?, 5, None)?, 0x40000);
1450                assert_eq!(data(&caps, 2)?.len(), 12);
1451                assert_eq!(data(&body, 3)?, b"voip-2p");
1452            }
1453            CallHostRoute::Transfer => {
1454                assert_eq!(number(&body, 1, None)?, 1901);
1455                let command = number(&body, 2, None)? as u32;
1456                let path: &[u8] = match command {
1457                    3361 => b"/cloudim/cloud-bin/cloudvoipmtjoin",
1458                    3095 => b"/cloudim/cloud-bin/cloudvoipmtreject",
1459                    3380 => b"/cloudim/cloud-bin/cloudvoipmtexit",
1460                    _ => bail!("Unexpected synthetic Cloud command"),
1461                };
1462                assert_eq!(data(&body, 3)?, b"wechat");
1463                assert_eq!(data(&body, 4)?, path);
1464                let frame = app_frame::TaskFrame::decode(data(&body, 5)?, command)?;
1465                assert_eq!(frame.integers[&2], frame.integers[&22]);
1466                assert!(frame.integers[&2] > 0);
1467                assert_eq!(frame.strings[&24], b"wechat");
1468                let clear = app_frame::lz4_decode(&frame.payload, 64 * 1024)?;
1469                let cloud = fields(&clear)?;
1470                let app_base = fields(data(&cloud, 1)?)?;
1471                assert_eq!(number(&app_base, 1, None)?, frame.integers[&22]);
1472                assert_eq!(data(&app_base, 2)?, b"Ldeviceid");
1473                let inner = fields(data(&cloud, 2)?)?;
1474                if command == 3361 {
1475                    assert_eq!(number(&inner, 5, None)?, 2);
1476                    assert_eq!(number(&inner, 6, None)?, 1);
1477                    assert_eq!(number(&inner, 8, None)?, 0);
1478                    assert_eq!(number(&inner, 11, None)?, 7);
1479                    assert_eq!(number(&inner, 12, None)?, 9);
1480                } else {
1481                    assert_eq!(number(&inner, 3, None)?, 7);
1482                    assert_eq!(number(&inner, 4, None)?, 9);
1483                    assert_eq!(number(&inner, 5, None)?, 0);
1484                    let reason = number(&inner, 6, None)?;
1485                    if command == 3095 {
1486                        assert_eq!(reason, 1);
1487                    } else {
1488                        assert!([1, 5, 6, 7].contains(&reason));
1489                    }
1490                }
1491            }
1492        }
1493        Ok(())
1494    }
1495    pub(super) fn host_reply(body: &[u8]) -> Vec<u8> {
1496        p::message(&[p::bytes(1, &[8, 0]), p::bytes(2, body)]).unwrap()
1497    }
1498    pub(super) fn state() -> AccountState {
1499        AccountState {
1500            schema: 1,
1501            device: crate::DeviceProfile {
1502                device_id: vec![b'x'; 16],
1503                device_type: "Linux".to_owned(),
1504                client_version: -227271398,
1505            },
1506            uin: 1,
1507            own_username: "own".to_owned(),
1508            cookie: vec![],
1509            session_key: vec![1; 16],
1510            request_key: vec![],
1511            response_key: vec![],
1512            auto_auth_key: vec![],
1513            key_info: vec![],
1514            database_password: vec![],
1515            auth_snapshot: crate::AuthSnapshot::default(),
1516            long_host: "localhost".to_owned(),
1517            short_host: "localhost".to_owned(),
1518        }
1519    }
1520    pub(super) fn authority() -> AppAuthority {
1521        AppAuthority {
1522            account_generation: 1,
1523            logout_epoch: 0,
1524            credential_digest: [4; 32],
1525        }
1526    }
1527
1528    pub(super) fn capabilities_for_codec(codec: u64) -> (Vec<u8>, Vec<u8>) {
1529        (
1530            p::message(&[p::uint(5, codec), p::uint(6, 0x206), p::uint(7, 0x800000)]).unwrap(),
1531            p::message(&[p::uint(1, 2), p::uint(2, 3)]).unwrap(),
1532        )
1533    }
1534    pub(super) fn parameters() -> VoiceParameters {
1535        let offer =
1536            super::super::voice_offer::VoiceOffer::from_owned_random(0x01000000, [5; 32]).unwrap();
1537        VoiceParameters {
1538            sdk: p::message(&[p::uint(1, 0x050000cb), p::bytes(2, b"test")]).unwrap(),
1539            group: p::message(&[
1540                p::bytes(1, b"wechat"),
1541                p::uint(2, 0),
1542                p::bytes(3, b"voip-2p"),
1543                p::bytes(4, &[]),
1544                p::uint(5, 1),
1545                p::bytes(6, &[]),
1546                p::uint(7, 0),
1547            ])
1548            .unwrap(),
1549            device: vec![],
1550            engine_capability: offer.engine_advertisement().to_vec(),
1551            channel_capability: offer.advertised_channel_identity().to_vec(),
1552            network_type: 0,
1553            sim_type: 0,
1554            video_length: 0,
1555            video_ratio: 0,
1556            nics: vec![],
1557            cloud_device_id: b"Ldeviceid".to_vec(),
1558            app_config_version: 1,
1559        }
1560    }
1561    pub(super) fn member(name: &str, id: u16, status: u32) -> Vec<u8> {
1562        p::message(&[
1563            p::bytes(1, name.as_bytes()),
1564            p::uint(2, u64::from(id)),
1565            p::uint(3, u64::from(status)),
1566            p::bytes(7, name.as_bytes()),
1567        ])
1568        .unwrap()
1569    }
1570    pub(super) fn av_status(sequence: u32, peer_status: u32) -> Vec<u8> {
1571        let own = p::message(&[p::uint(1, 0), p::uint(2, 1)]).unwrap();
1572        let peer = p::message(&[p::uint(1, 1), p::uint(2, u64::from(peer_status))]).unwrap();
1573        p::message(&[
1574            p::uint(3, u64::from(sequence)),
1575            p::bytes(4, &own),
1576            p::bytes(4, &peer),
1577        ])
1578        .unwrap()
1579    }
1580    pub(super) fn auth_reply() -> Vec<u8> {
1581        auth_reply_uin(17)
1582    }
1583    fn auth_reply_uin(uin: u64) -> Vec<u8> {
1584        let auth = p::message(&[p::bytes(9, b"server"), p::uint(11, 1)]).unwrap();
1585        let unify = p::message(&[p::bytes(2, &auth), p::bytes(3, &p::uint(1, uin))]).unwrap();
1586        host_reply(&p::message(&[p::bytes(1, &[8, 0]), p::bytes(2, &unify)]).unwrap())
1587    }
1588    fn invite_reply() -> Vec<u8> {
1589        invite_reply_profile(17, 2)
1590    }
1591    fn invite_reply_profile(codec: u64, peer_status: u32) -> Vec<u8> {
1592        invite_reply_full(codec, peer_status, 0)
1593    }
1594    fn invite_reply_full(codec: u64, peer_status: u32, peer_av: u32) -> Vec<u8> {
1595        let (engine, channel) = capabilities_for_codec(codec);
1596        // Scalar0 may be absent in a current, unselected capability message.
1597        let engine = if codec == 0 {
1598            p::message(&[p::uint(6, 0x206), p::uint(7, 0x800000)]).unwrap()
1599        } else {
1600            engine
1601        };
1602        let cap = p::message(&[
1603            p::uint(1, 1),
1604            p::bytes(
1605                2,
1606                &p::message(&[p::bytes(1, &engine), p::bytes(2, &channel)]).unwrap(),
1607            ),
1608        ])
1609        .unwrap();
1610        let address = p::message(&[p::bytes(1, b"127.0.0.1"), p::uint(2, 6000)]).unwrap();
1611        let connection = p::message(&[p::uint(1, 1), p::bytes(4, &address)]).unwrap();
1612        let strategy = p::message(&[
1613            p::bytes(10, &[7; 32]),
1614            p::bytes(13, &cap),
1615            p::uint(14, 0),
1616            p::bytes(15, &connection),
1617        ])
1618        .unwrap();
1619        let roster = p::message(&[
1620            p::uint(1, 1),
1621            p::bytes(2, &member("own", 0, 1)),
1622            p::bytes(2, &member("peer", 1, peer_status)),
1623        ])
1624        .unwrap();
1625        let client = p::message(&[
1626            p::bytes(1, &strategy),
1627            p::bytes(2, &roster),
1628            p::bytes(3, &av_status(1, peer_av)),
1629            p::uint(5, 99),
1630            p::bytes(6, b"old"),
1631        ])
1632        .unwrap();
1633        host_reply(
1634            &p::message(&[
1635                p::uint(1, 7),
1636                p::uint(2, 9),
1637                p::uint(3, 0),
1638                p::bytes(4, b"group"),
1639                p::bytes(5, &client),
1640            ])
1641            .unwrap(),
1642        )
1643    }
1644    fn with_result(reply: &[u8], recipient: &str, code: u64) -> Vec<u8> {
1645        let common = fields(reply).unwrap();
1646        let mut body = data(&common, 2).unwrap().to_vec();
1647        body.extend(p::bytes(
1648            8,
1649            &p::message(&[p::bytes(1, recipient.as_bytes()), p::uint(2, code)]).unwrap(),
1650        ));
1651        host_reply(&body)
1652    }
1653    fn initial_roster_reply(sequence: u32, own: Option<u32>, peer: Option<u32>) -> Vec<u8> {
1654        let mut list = vec![p::uint(1, u64::from(sequence))];
1655        if let Some(status) = own {
1656            list.push(p::bytes(2, &member("own", 0, status)));
1657        }
1658        if let Some(status) = peer {
1659            list.push(p::bytes(2, &member("peer", 1, status)));
1660        }
1661        // No AV/codec data is needed for the source's earlier terminal reducer.
1662        let client = p::message(&[
1663            p::bytes(2, &p::message(&list).unwrap()),
1664            p::uint(5, 99),
1665            p::bytes(6, b"initial"),
1666        ])
1667        .unwrap();
1668        with_result(
1669            &host_reply(
1670                &p::message(&[
1671                    p::uint(1, 7),
1672                    p::uint(2, 9),
1673                    p::uint(3, 0),
1674                    p::bytes(4, b"group"),
1675                    p::bytes(5, &client),
1676                ])
1677                .unwrap(),
1678            ),
1679            "peer",
1680            23,
1681        )
1682    }
1683    fn capability_sync(engine: &[u8], channel: &[u8]) -> Vec<u8> {
1684        let content = p::message(&[p::bytes(1, engine), p::bytes(2, channel)]).unwrap();
1685        let cap = p::message(&[p::uint(1, 2), p::bytes(2, &content)]).unwrap();
1686        let notify = p::message(&[
1687            p::uint(3, 7),
1688            p::uint(4, 9),
1689            p::bytes(
1690                5,
1691                &p::message(&[p::uint(1, 11), p::bytes(2, &cap)]).unwrap(),
1692            ),
1693        ])
1694        .unwrap();
1695        let message = p::message(&[p::uint(1, 10002), p::bytes(2, &notify)]).unwrap();
1696        host_reply(
1697            &p::message(&[
1698                p::bytes(
1699                    1,
1700                    &p::message(&[p::uint(1, 1), p::bytes(2, &message)]).unwrap(),
1701                ),
1702                p::bytes(2, b"bad-cursor"),
1703            ])
1704            .unwrap(),
1705        )
1706    }
1707    fn microphone_sync(sequence: u32, status: u32, info: Option<&[u8]>) -> Vec<u8> {
1708        let own = p::message(&[p::uint(1, 0), p::uint(2, 1)]).unwrap();
1709        let mut peer = vec![p::uint(1, 1), p::uint(2, u64::from(status))];
1710        if let Some(info) = info {
1711            peer.push(p::bytes(3, info));
1712        }
1713        let av = p::message(&[
1714            p::uint(3, u64::from(sequence)),
1715            p::bytes(4, &own),
1716            p::bytes(4, &p::message(&peer).unwrap()),
1717        ])
1718        .unwrap();
1719        let notify = p::message(&[
1720            p::uint(3, 7),
1721            p::uint(4, 9),
1722            p::bytes(5, &p::message(&[p::uint(1, 12), p::bytes(2, &av)]).unwrap()),
1723        ])
1724        .unwrap();
1725        let message = p::message(&[p::uint(1, 10002), p::bytes(2, &notify)]).unwrap();
1726        host_reply(
1727            &p::message(&[
1728                p::bytes(
1729                    1,
1730                    &p::message(&[p::uint(1, 1), p::bytes(2, &message)]).unwrap(),
1731                ),
1732                p::bytes(2, b"mic-update"),
1733            ])
1734            .unwrap(),
1735        )
1736    }
1737    fn joined_sync(extra_unknown: bool) -> Vec<u8> {
1738        joined_sync_profile(extra_unknown, None)
1739    }
1740    fn joined_sync_profile(extra_unknown: bool, codec: Option<u64>) -> Vec<u8> {
1741        let members = p::message(&[
1742            p::uint(1, 7),
1743            p::uint(2, 9),
1744            p::uint(3, 2),
1745            p::bytes(4, &member("own", 0, 1)),
1746            p::bytes(4, &member("peer", 1, 1)),
1747        ])
1748        .unwrap();
1749        let mut notify = vec![
1750            p::uint(3, 7),
1751            p::uint(4, 9),
1752            p::bytes(
1753                5,
1754                &p::message(&[p::uint(1, 10), p::bytes(2, &members)]).unwrap(),
1755            ),
1756            p::bytes(
1757                5,
1758                &p::message(&[p::uint(1, 12), p::bytes(2, &av_status(2, 1))]).unwrap(),
1759            ),
1760        ];
1761        if let Some(codec) = codec {
1762            let (engine, channel) = capabilities_for_codec(codec);
1763            let content = p::message(&[p::bytes(1, &engine), p::bytes(2, &channel)]).unwrap();
1764            let cap = p::message(&[p::uint(1, 2), p::bytes(2, &content)]).unwrap();
1765            notify.push(p::bytes(
1766                5,
1767                &p::message(&[p::uint(1, 11), p::bytes(2, &cap)]).unwrap(),
1768            ));
1769        }
1770        if extra_unknown {
1771            notify.push(p::bytes(
1772                5,
1773                &p::message(&[p::uint(1, 999), p::bytes(2, &[])]).unwrap(),
1774            ));
1775        }
1776        let message = p::message(&[
1777            p::uint(1, 10002),
1778            p::bytes(2, &p::message(&notify).unwrap()),
1779        ])
1780        .unwrap();
1781        host_reply(
1782            &p::message(&[
1783                p::bytes(
1784                    1,
1785                    &p::message(&[p::uint(1, 1), p::bytes(2, &message)]).unwrap(),
1786                ),
1787                p::bytes(2, b"new"),
1788            ])
1789            .unwrap(),
1790        )
1791    }
1792    pub(super) fn exit_reply() -> Vec<u8> {
1793        exit_reply_uin(17)
1794    }
1795    fn exit_reply_uin(uin: u64) -> Vec<u8> {
1796        let exit = p::message(&[p::uint(1, 7), p::uint(2, 9), p::uint(3, 0)]).unwrap();
1797        let cloud =
1798            p::message(&[p::bytes(1, &[8, 0]), p::uint(2, uin), p::bytes(3, &exit)]).unwrap();
1799        let frame = app_frame::TaskFrame {
1800            command: 3380,
1801            integers: BTreeMap::from([
1802                (4, 0),
1803                (7, 0),
1804                (8, 0),
1805                (21, 0),
1806                (22, uin),
1807                (23, 0),
1808                (25, 0),
1809                (26, 0),
1810            ]),
1811            strings: BTreeMap::new(),
1812            payload: cloud,
1813        }
1814        .encode()
1815        .unwrap();
1816        host_reply(&p::message(&[p::uint(1, 0), p::bytes(2, &frame)]).unwrap())
1817    }
1818    #[test]
1819    fn fake_caller_auth_invite_full_sync_media_and_exit() {
1820        let transport = RecordingTransport {
1821            inner: FakeTransport(std::cell::RefCell::new(std::collections::VecDeque::from([
1822                (CallHostRoute::AppAuth, auth_reply_uin(u64::MAX)),
1823                (CallHostRoute::Invite, invite_reply()),
1824                (CallHostRoute::LiveSync, joined_sync(false)),
1825                (
1826                    CallHostRoute::LiveSync,
1827                    microphone_sync(3, 0, Some(&[8, 2])),
1828                ),
1829                (
1830                    CallHostRoute::LiveSync,
1831                    microphone_sync(4, 1, Some(&[8, 3])),
1832                ),
1833                (CallHostRoute::Transfer, exit_reply_uin(u64::MAX)),
1834            ]))),
1835            requests: Default::default(),
1836        };
1837        let mut state = state();
1838        let mut session = NativePersonalSession::authenticate(
1839            &transport,
1840            &mut state,
1841            authority(),
1842            1,
1843            [9; 32],
1844            parameters(),
1845        )
1846        .unwrap();
1847        session
1848            .invite(
1849                &transport,
1850                &mut state,
1851                authority(),
1852                "peer",
1853                123,
1854                TaskIds { sdk: 1, host: 2 },
1855            )
1856            .unwrap();
1857        assert_eq!(session.phase(), PersonalPhase::Ringing);
1858        assert_eq!(
1859            session
1860                .poll(
1861                    &transport,
1862                    &mut state,
1863                    authority(),
1864                    TaskIds { sdk: 2, host: 3 }
1865                )
1866                .unwrap(),
1867            SyncProgress::MediaReady
1868        );
1869        let media = session.take_media(&state, authority(), 0, 0).unwrap();
1870        assert_eq!(session.peer_microphone_muted(), Some(false));
1871        assert_eq!(media.peer_member, 1);
1872        assert_eq!(media.relay.room_id, 7);
1873        assert!(session.take_media(&state, authority(), 0, 0).is_err());
1874        for (sdk, host, status) in [(3, 4, 0), (4, 5, 1)] {
1875            assert_eq!(
1876                session
1877                    .poll(&transport, &mut state, authority(), TaskIds { sdk, host })
1878                    .unwrap(),
1879                SyncProgress::MediaReady
1880            );
1881            assert_eq!(session.phase(), PersonalPhase::Joined);
1882            assert_eq!(session.peer_microphone_muted(), Some(status == 0));
1883            let active = session.active.as_ref().unwrap();
1884            assert_eq!(active.av.get(&1).unwrap().0, status);
1885            assert!(active.media_taken);
1886            assert!(session.take_media(&state, authority(), 0, 0).is_err());
1887        }
1888        let active = session.active.as_ref().unwrap();
1889        let mut unsupported = active.av.clone();
1890        unsupported.get_mut(&1).unwrap().0 = 4;
1891        assert!(!microphone_update(
1892            active,
1893            &unsupported,
1894            &state.own_username
1895        ));
1896        session
1897            .exit(
1898                &transport,
1899                &mut state,
1900                authority(),
1901                TaskIds { sdk: 5, host: 6 },
1902                false,
1903            )
1904            .unwrap();
1905        assert_eq!(session.phase(), PersonalPhase::Finished);
1906        assert_eq!(session.peer_microphone_muted(), None);
1907        assert!(transport.inner.0.borrow().is_empty());
1908        let requests = transport.requests.borrow();
1909        let host_tasks = requests
1910            .iter()
1911            .map(|(_, raw)| {
1912                let common = CommonRequest::decode(raw).unwrap();
1913                number(
1914                    &fields(common.base_request.as_deref().unwrap()).unwrap(),
1915                    6,
1916                    None,
1917                )
1918                .unwrap()
1919            })
1920            .collect::<Vec<_>>();
1921        assert_eq!(host_tasks, [1, 2, 3, 4, 5, 6]);
1922        let common = CommonRequest::decode(&requests.last().unwrap().1).unwrap();
1923        let delegate = fields(common.body.as_deref().unwrap()).unwrap();
1924        let frame = app_frame::TaskFrame::decode(data(&delegate, 5).unwrap(), 3380).unwrap();
1925        assert_eq!(frame.integers[&2], u64::MAX);
1926        assert_eq!(frame.integers[&22], u64::MAX);
1927        let common = fields(&session.app.common_session(authority()).unwrap()).unwrap();
1928        assert_eq!(number(&common, 4, None).unwrap(), u64::MAX);
1929        assert_eq!(data(&common, 5).unwrap(), b"server");
1930        assert_eq!(number(&common, 6, None).unwrap(), 1);
1931        assert!(p::optional(&common, 1).unwrap().is_none());
1932    }
1933    #[test]
1934    fn later_unknown_sync_item_prevents_cookie_and_partial_member_install() {
1935        let transport = FakeTransport(std::cell::RefCell::new(std::collections::VecDeque::from([
1936            (CallHostRoute::AppAuth, auth_reply()),
1937            (CallHostRoute::Invite, invite_reply()),
1938            (CallHostRoute::LiveSync, joined_sync(true)),
1939        ])));
1940        let mut state = state();
1941        let mut session = NativePersonalSession::authenticate(
1942            &transport,
1943            &mut state,
1944            authority(),
1945            1,
1946            [9; 32],
1947            parameters(),
1948        )
1949        .unwrap();
1950        session
1951            .invite(
1952                &transport,
1953                &mut state,
1954                authority(),
1955                "peer",
1956                123,
1957                TaskIds { sdk: 1, host: 2 },
1958            )
1959            .unwrap();
1960        assert!(session
1961            .poll(
1962                &transport,
1963                &mut state,
1964                authority(),
1965                TaskIds { sdk: 2, host: 3 }
1966            )
1967            .is_err());
1968        assert_eq!(session.phase(), PersonalPhase::Uncertain);
1969        let active = session.active.as_ref().unwrap();
1970        assert_eq!(active.cookies, b"old");
1971        assert_eq!(active.members["peer"].status, 2);
1972        assert!(session
1973            .poll(
1974                &transport,
1975                &mut state,
1976                authority(),
1977                TaskIds { sdk: 3, host: 4 }
1978            )
1979            .is_err());
1980    }
1981
1982    #[test]
1983    fn only_proven_unsent_poll_retains_known_room_for_exit() {
1984        struct StopTransport<'a> {
1985            inner: &'a FakeTransport,
1986            unsent: bool,
1987        }
1988        impl PersonalTransport for StopTransport<'_> {
1989            fn call_rpc(
1990                &self,
1991                state: &mut AccountState,
1992                route: CallHostRoute,
1993                request: &[u8],
1994            ) -> Result<Vec<u8>> {
1995                if route == CallHostRoute::LiveSync {
1996                    if self.unsent {
1997                        return Err(CallRpcNotSubmitted.into());
1998                    }
1999                    bail!("Ambiguous transport failure")
2000                }
2001                self.inner.call_rpc(state, route, request)
2002            }
2003        }
2004        for unsent in [true, false] {
2005            let transport =
2006                FakeTransport(std::cell::RefCell::new(std::collections::VecDeque::from([
2007                    (CallHostRoute::AppAuth, auth_reply()),
2008                    (CallHostRoute::Invite, invite_reply()),
2009                    (CallHostRoute::LiveSync, joined_sync(false)),
2010                    (CallHostRoute::Transfer, exit_reply()),
2011                ])));
2012            let mut state = state();
2013            let mut session = NativePersonalSession::authenticate(
2014                &transport,
2015                &mut state,
2016                authority(),
2017                1,
2018                [9; 32],
2019                parameters(),
2020            )
2021            .unwrap();
2022            session
2023                .invite(
2024                    &transport,
2025                    &mut state,
2026                    authority(),
2027                    "peer",
2028                    123,
2029                    TaskIds { sdk: 1, host: 2 },
2030                )
2031                .unwrap();
2032            session
2033                .poll(
2034                    &transport,
2035                    &mut state,
2036                    authority(),
2037                    TaskIds { sdk: 2, host: 3 },
2038                )
2039                .unwrap();
2040            let cookie = state.cookie.clone();
2041            let stop = StopTransport {
2042                inner: &transport,
2043                unsent,
2044            };
2045            assert!(session
2046                .poll(&stop, &mut state, authority(), TaskIds { sdk: 3, host: 4 })
2047                .is_err());
2048            assert_eq!(state.cookie, cookie);
2049            if unsent {
2050                assert_eq!(session.phase(), PersonalPhase::Joined);
2051                session
2052                    .exit(
2053                        &transport,
2054                        &mut state,
2055                        authority(),
2056                        TaskIds { sdk: 4, host: 5 },
2057                        false,
2058                    )
2059                    .unwrap();
2060                assert_eq!(session.phase(), PersonalPhase::Finished);
2061            } else {
2062                assert_eq!(session.phase(), PersonalPhase::Uncertain);
2063            }
2064        }
2065    }
2066
2067    #[test]
2068    fn pending_codec_zero_continues_sync_until_both_av_and_selected_codec_are_ready() {
2069        let transport = FakeTransport(std::cell::RefCell::new(std::collections::VecDeque::from([
2070            (CallHostRoute::AppAuth, auth_reply()),
2071            (
2072                CallHostRoute::Invite,
2073                with_result(&invite_reply_profile(0, 2), "peer", 23),
2074            ),
2075            (
2076                CallHostRoute::LiveSync,
2077                host_reply(&p::bytes(2, b"waiting")),
2078            ),
2079            (
2080                CallHostRoute::LiveSync,
2081                joined_sync_profile(false, Some(17)),
2082            ),
2083        ])));
2084        let mut state = state();
2085        let mut session = NativePersonalSession::authenticate(
2086            &transport,
2087            &mut state,
2088            authority(),
2089            1,
2090            [9; 32],
2091            parameters(),
2092        )
2093        .unwrap();
2094        session
2095            .invite(
2096                &transport,
2097                &mut state,
2098                authority(),
2099                "peer",
2100                123,
2101                TaskIds { sdk: 1, host: 2 },
2102            )
2103            .unwrap();
2104        assert_eq!(session.phase(), PersonalPhase::Ringing);
2105        assert!(!session.media_ready(&state, authority()).unwrap());
2106        assert!(session.take_media(&state, authority(), 0, 0).is_err());
2107        assert!(p::optional(
2108            &fields(&session.active.as_ref().unwrap().engine).unwrap(),
2109            5
2110        )
2111        .unwrap()
2112        .is_none());
2113        assert_eq!(
2114            session
2115                .poll(
2116                    &transport,
2117                    &mut state,
2118                    authority(),
2119                    TaskIds { sdk: 2, host: 3 }
2120                )
2121                .unwrap(),
2122            SyncProgress::Waiting
2123        );
2124        assert_eq!(session.active.as_ref().unwrap().cookies, b"waiting");
2125        assert_eq!(
2126            session
2127                .poll(
2128                    &transport,
2129                    &mut state,
2130                    authority(),
2131                    TaskIds { sdk: 3, host: 4 }
2132                )
2133                .unwrap(),
2134            SyncProgress::MediaReady
2135        );
2136        assert!(session.media_ready(&state, authority()).unwrap());
2137        assert_eq!(
2138            session
2139                .take_media(&state, authority(), 0, 0)
2140                .unwrap()
2141                .peer_member,
2142            1
2143        );
2144    }
2145    #[test]
2146    fn pending_capability_shapes_are_checked_before_cursor_installation() {
2147        let (engine, channel) = capabilities_for_codec(0);
2148        for (bad_engine, bad_channel) in [
2149            ([engine.clone(), p::uint(99, 0)].concat(), channel.clone()),
2150            ([engine.clone(), p::uint(6, 0)].concat(), channel.clone()),
2151            (p::bytes(6, b"wrong-wire"), channel.clone()),
2152            (
2153                engine.clone(),
2154                p::message(&[p::uint(1, u64::MAX), p::uint(2, 3)]).unwrap(),
2155            ),
2156        ] {
2157            let transport =
2158                FakeTransport(std::cell::RefCell::new(std::collections::VecDeque::from([
2159                    (CallHostRoute::AppAuth, auth_reply()),
2160                    (CallHostRoute::Invite, invite_reply_profile(0, 2)),
2161                    (
2162                        CallHostRoute::LiveSync,
2163                        capability_sync(&bad_engine, &bad_channel),
2164                    ),
2165                ])));
2166            let mut state = state();
2167            let mut session = NativePersonalSession::authenticate(
2168                &transport,
2169                &mut state,
2170                authority(),
2171                1,
2172                [9; 32],
2173                parameters(),
2174            )
2175            .unwrap();
2176            session
2177                .invite(
2178                    &transport,
2179                    &mut state,
2180                    authority(),
2181                    "peer",
2182                    123,
2183                    TaskIds { sdk: 1, host: 2 },
2184                )
2185                .unwrap();
2186            assert!(session
2187                .poll(
2188                    &transport,
2189                    &mut state,
2190                    authority(),
2191                    TaskIds { sdk: 2, host: 3 }
2192                )
2193                .is_err());
2194            assert_eq!(session.phase(), PersonalPhase::Uncertain);
2195            let active = session.active.as_ref().unwrap();
2196            assert_eq!(active.cookies, b"old");
2197            assert_eq!(active.cap_sequence, 1);
2198            assert_eq!(active.members["peer"].status, 2);
2199            assert!(session.take_media(&state, authority(), 0, 0).is_err());
2200        }
2201    }
2202    #[test]
2203    fn initial_full_roster_terminates_before_profile_and_recipient_result_is_not_overall_error() {
2204        for (own, peer, action) in [
2205            (
2206                Some(1),
2207                Some(6),
2208                RemoteTermination::Exit(personal::RemoteExitReason::PeerBusy),
2209            ),
2210            (
2211                Some(1),
2212                Some(7),
2213                RemoteTermination::Exit(personal::RemoteExitReason::PeerRejected),
2214            ),
2215            (
2216                Some(1),
2217                None,
2218                RemoteTermination::Exit(personal::RemoteExitReason::LastPeerLeft),
2219            ),
2220            (
2221                None,
2222                Some(1),
2223                RemoteTermination::LocalStop(LocalStopReason::SelfRemoved),
2224            ),
2225            (
2226                Some(2),
2227                Some(1),
2228                RemoteTermination::LocalStop(LocalStopReason::SelfRemoved),
2229            ),
2230        ] {
2231            let local = matches!(action, RemoteTermination::LocalStop(_));
2232            let mut replies = std::collections::VecDeque::from([
2233                (CallHostRoute::AppAuth, auth_reply()),
2234                (CallHostRoute::Invite, initial_roster_reply(1, own, peer)),
2235            ]);
2236            if !local {
2237                replies.push_back((CallHostRoute::Transfer, exit_reply()));
2238            }
2239            let transport = RecordingTransport {
2240                inner: FakeTransport(std::cell::RefCell::new(replies)),
2241                requests: Default::default(),
2242            };
2243            let mut state = state();
2244            let mut session = NativePersonalSession::authenticate(
2245                &transport,
2246                &mut state,
2247                authority(),
2248                1,
2249                [9; 32],
2250                parameters(),
2251            )
2252            .unwrap();
2253            session
2254                .invite(
2255                    &transport,
2256                    &mut state,
2257                    authority(),
2258                    "peer",
2259                    123,
2260                    TaskIds { sdk: 1, host: 2 },
2261                )
2262                .unwrap();
2263            assert_eq!(session.control.as_ref().unwrap().terminal, Some(action));
2264            assert!(session.active.is_none());
2265            assert!(!session.media_ready(&state, authority()).unwrap());
2266            if local {
2267                assert_eq!(session.phase(), PersonalPhase::Finished);
2268            } else {
2269                assert_eq!(session.phase(), PersonalPhase::TerminalPending);
2270                session
2271                    .finish_remote(
2272                        &transport,
2273                        &mut state,
2274                        authority(),
2275                        TaskIds { sdk: 2, host: 3 },
2276                        false,
2277                    )
2278                    .unwrap();
2279                let expected = match action {
2280                    RemoteTermination::Exit(reason) => reason.wire(),
2281                    _ => unreachable!(),
2282                };
2283                assert_eq!(
2284                    recorded_exit_reason(&transport.requests.borrow().last().unwrap().1),
2285                    expected
2286                );
2287            }
2288            assert_eq!(session.phase(), PersonalPhase::Finished);
2289            assert!(transport.inner.0.borrow().is_empty());
2290        }
2291    }
2292    #[test]
2293    fn initial_zero_sequence_or_unknown_membership_is_not_terminal_proof() {
2294        for (sequence, own, peer) in [
2295            (0, None, Some(1)),
2296            (1, Some(1), Some(99)),
2297            (1, Some(1), Some(8)),
2298        ] {
2299            let transport =
2300                FakeTransport(std::cell::RefCell::new(std::collections::VecDeque::from([
2301                    (CallHostRoute::AppAuth, auth_reply()),
2302                    (
2303                        CallHostRoute::Invite,
2304                        initial_roster_reply(sequence, own, peer),
2305                    ),
2306                ])));
2307            let mut state = state();
2308            let mut session = NativePersonalSession::authenticate(
2309                &transport,
2310                &mut state,
2311                authority(),
2312                1,
2313                [9; 32],
2314                parameters(),
2315            )
2316            .unwrap();
2317            assert!(session
2318                .invite(
2319                    &transport,
2320                    &mut state,
2321                    authority(),
2322                    "peer",
2323                    123,
2324                    TaskIds { sdk: 1, host: 2 }
2325                )
2326                .is_err());
2327            assert_eq!(session.phase(), PersonalPhase::Uncertain);
2328            assert!(session.control.as_ref().unwrap().terminal.is_none());
2329            assert!(session
2330                .finish_remote(
2331                    &transport,
2332                    &mut state,
2333                    authority(),
2334                    TaskIds { sdk: 2, host: 3 },
2335                    false
2336                )
2337                .is_err());
2338            assert!(transport.0.borrow().is_empty());
2339        }
2340    }
2341    #[test]
2342    fn pending_unselected_control_is_not_media_admission_and_cancels_once() {
2343        let cancel = host_reply(
2344            &p::message(&[p::bytes(
2345                1,
2346                &p::message(&[p::uint(1, 7), p::uint(2, 9)]).unwrap(),
2347            )])
2348            .unwrap(),
2349        );
2350        let transport = FakeTransport(std::cell::RefCell::new(std::collections::VecDeque::from([
2351            (CallHostRoute::AppAuth, auth_reply()),
2352            (CallHostRoute::Invite, invite_reply_profile(0, 2)),
2353            (CallHostRoute::Cancel, cancel),
2354        ])));
2355        let mut state = state();
2356        let mut session = NativePersonalSession::authenticate(
2357            &transport,
2358            &mut state,
2359            authority(),
2360            1,
2361            [9; 32],
2362            parameters(),
2363        )
2364        .unwrap();
2365        session
2366            .invite(
2367                &transport,
2368                &mut state,
2369                authority(),
2370                "peer",
2371                123,
2372                TaskIds { sdk: 1, host: 2 },
2373            )
2374            .unwrap();
2375        assert!(!session.media_ready(&state, authority()).unwrap());
2376        assert!(session.has_allocated_control());
2377        assert_eq!(session.phase(), PersonalPhase::Ringing);
2378        assert!(session.take_media(&state, authority(), 0, 0).is_err());
2379        struct CheckCancel<'a>(&'a FakeTransport);
2380        impl PersonalTransport for CheckCancel<'_> {
2381            fn call_rpc(
2382                &self,
2383                state: &mut AccountState,
2384                route: CallHostRoute,
2385                request: &[u8],
2386            ) -> Result<Vec<u8>> {
2387                assert_eq!(route, CallHostRoute::Cancel);
2388                let common = CommonRequest::decode(request)?;
2389                let outer = fields(common.body.as_deref().unwrap())?;
2390                let inner = fields(data(&outer, 1)?)?;
2391                assert_eq!(number(&inner, 3, None)?, 7);
2392                assert_eq!(number(&inner, 4, None)?, 9);
2393                assert_eq!(number(&inner, 5, None)?, 123);
2394                assert_eq!(data(&outer, 4)?, b"peer");
2395                self.0.call_rpc(state, route, request)
2396            }
2397        }
2398        session
2399            .cancel(
2400                &CheckCancel(&transport),
2401                &mut state,
2402                authority(),
2403                TaskIds { sdk: 2, host: 3 },
2404            )
2405            .unwrap();
2406        assert_eq!(session.phase(), PersonalPhase::Finished);
2407        assert!(transport.0.borrow().is_empty());
2408        assert!(session
2409            .cancel(
2410                &transport,
2411                &mut state,
2412                authority(),
2413                TaskIds { sdk: 3, host: 4 }
2414            )
2415            .is_err());
2416    }
2417    #[test]
2418    fn missing_cleanup_reply_stays_uncertain_and_is_not_retried() {
2419        let transport = FakeTransport(std::cell::RefCell::new(std::collections::VecDeque::from([
2420            (CallHostRoute::AppAuth, auth_reply()),
2421            (CallHostRoute::Invite, invite_reply_profile(0, 2)),
2422        ])));
2423        let mut state = state();
2424        let mut session = NativePersonalSession::authenticate(
2425            &transport,
2426            &mut state,
2427            authority(),
2428            1,
2429            [9; 32],
2430            parameters(),
2431        )
2432        .unwrap();
2433        session
2434            .invite(
2435                &transport,
2436                &mut state,
2437                authority(),
2438                "peer",
2439                123,
2440                TaskIds { sdk: 1, host: 2 },
2441            )
2442            .unwrap();
2443        assert!(session
2444            .cancel(
2445                &transport,
2446                &mut state,
2447                authority(),
2448                TaskIds { sdk: 2, host: 3 }
2449            )
2450            .is_err());
2451        assert_eq!(session.phase(), PersonalPhase::Uncertain);
2452        assert!(session
2453            .cancel(
2454                &transport,
2455                &mut state,
2456                authority(),
2457                TaskIds { sdk: 3, host: 4 }
2458            )
2459            .is_err());
2460        assert!(transport.0.borrow().is_empty());
2461    }
2462    pub(super) fn roster_sync(
2463        sequence: u32,
2464        own: Option<u32>,
2465        peer: Option<u32>,
2466        unknown_after: bool,
2467    ) -> Vec<u8> {
2468        let mut roster = vec![
2469            p::uint(1, 7),
2470            p::uint(2, 9),
2471            p::uint(3, u64::from(sequence)),
2472        ];
2473        if let Some(status) = own {
2474            roster.push(p::bytes(4, &member("own", 0, status)));
2475        }
2476        if let Some(status) = peer {
2477            roster.push(p::bytes(4, &member("peer", 1, status)));
2478        }
2479        let mut notify = vec![
2480            p::uint(3, 7),
2481            p::uint(4, 9),
2482            p::bytes(
2483                5,
2484                &p::message(&[p::uint(1, 10), p::bytes(2, &p::message(&roster).unwrap())]).unwrap(),
2485            ),
2486        ];
2487        if unknown_after {
2488            notify.push(p::bytes(
2489                5,
2490                &p::message(&[p::uint(1, 999), p::bytes(2, &[])]).unwrap(),
2491            ));
2492        }
2493        let message = p::message(&[
2494            p::uint(1, 10002),
2495            p::bytes(2, &p::message(&notify).unwrap()),
2496        ])
2497        .unwrap();
2498        host_reply(
2499            &p::message(&[
2500                p::bytes(
2501                    1,
2502                    &p::message(&[p::uint(1, 1), p::bytes(2, &message)]).unwrap(),
2503                ),
2504                p::bytes(2, b"terminal-cursor"),
2505            ])
2506            .unwrap(),
2507        )
2508    }
2509    pub(super) struct RecordingTransport {
2510        pub(super) inner: FakeTransport,
2511        pub(super) requests: std::cell::RefCell<Vec<(CallHostRoute, Vec<u8>)>>,
2512    }
2513    impl PersonalTransport for RecordingTransport {
2514        fn call_rpc(
2515            &self,
2516            state: &mut AccountState,
2517            route: CallHostRoute,
2518            request: &[u8],
2519        ) -> Result<Vec<u8>> {
2520            self.requests.borrow_mut().push((route, request.to_vec()));
2521            self.inner.call_rpc(state, route, request)
2522        }
2523    }
2524    fn caller_with(
2525        transport: &impl PersonalTransport,
2526        state: &mut AccountState,
2527    ) -> NativePersonalSession {
2528        let mut session = NativePersonalSession::authenticate(
2529            transport,
2530            state,
2531            authority(),
2532            1,
2533            [9; 32],
2534            parameters(),
2535        )
2536        .unwrap();
2537        session
2538            .invite(
2539                transport,
2540                state,
2541                authority(),
2542                "peer",
2543                123,
2544                TaskIds { sdk: 1, host: 2 },
2545            )
2546            .unwrap();
2547        session
2548    }
2549    pub(super) fn recorded_exit_reason(request: &[u8]) -> u64 {
2550        let common = CommonRequest::decode(request).unwrap();
2551        let delegate = fields(common.body.as_deref().unwrap()).unwrap();
2552        assert_eq!(number(&delegate, 2, None).unwrap(), 3380);
2553        let frame = app_frame::TaskFrame::decode(data(&delegate, 5).unwrap(), 3380).unwrap();
2554        let cloud = fields(&app_frame::lz4_decode(&frame.payload, 64 * 1024).unwrap()).unwrap();
2555        number(&fields(data(&cloud, 2).unwrap()).unwrap(), 6, None).unwrap()
2556    }
2557    #[test]
2558    fn caller_remote_last_peer_uses_one_exit_with_distinct_reason_and_no_local_cancel() {
2559        for (peer, wire) in [(4, 5), (6, 7), (7, 6)] {
2560            let transport = RecordingTransport {
2561                inner: FakeTransport(std::cell::RefCell::new(std::collections::VecDeque::from([
2562                    (CallHostRoute::AppAuth, auth_reply()),
2563                    (CallHostRoute::Invite, invite_reply()),
2564                    (
2565                        CallHostRoute::LiveSync,
2566                        roster_sync(2, Some(1), Some(peer), false),
2567                    ),
2568                    (CallHostRoute::Transfer, exit_reply()),
2569                ]))),
2570                requests: Default::default(),
2571            };
2572            let mut state = state();
2573            let mut session = caller_with(&transport, &mut state);
2574            assert!(matches!(
2575                session
2576                    .poll(
2577                        &transport,
2578                        &mut state,
2579                        authority(),
2580                        TaskIds { sdk: 2, host: 3 }
2581                    )
2582                    .unwrap(),
2583                SyncProgress::Terminal(RemoteTermination::Exit(_))
2584            ));
2585            assert_eq!(session.phase(), PersonalPhase::TerminalPending);
2586            assert!(session.take_media(&state, authority(), 0, 0).is_err());
2587            session
2588                .finish_remote(
2589                    &transport,
2590                    &mut state,
2591                    authority(),
2592                    TaskIds { sdk: 3, host: 4 },
2593                    false,
2594                )
2595                .unwrap();
2596            let requests = transport.requests.borrow();
2597            let last = requests.last().unwrap();
2598            assert_eq!(last.0, CallHostRoute::Transfer);
2599            assert_eq!(recorded_exit_reason(&last.1), wire);
2600            assert_eq!(session.phase(), PersonalPhase::Finished);
2601            assert!(session
2602                .finish_remote(
2603                    &transport,
2604                    &mut state,
2605                    authority(),
2606                    TaskIds { sdk: 4, host: 5 },
2607                    false
2608                )
2609                .is_err());
2610            assert!(transport.inner.0.borrow().is_empty());
2611        }
2612    }
2613    #[test]
2614    fn no_response_cancel_uses_original_id_and_direct_invite_timeout_wire_reason() {
2615        let cancel = host_reply(&p::bytes(
2616            1,
2617            &p::message(&[p::uint(1, 7), p::uint(2, 9)]).unwrap(),
2618        ));
2619        let transport = RecordingTransport {
2620            inner: FakeTransport(std::cell::RefCell::new(std::collections::VecDeque::from([
2621                (CallHostRoute::AppAuth, auth_reply()),
2622                (CallHostRoute::Invite, invite_reply()),
2623                (
2624                    CallHostRoute::LiveSync,
2625                    roster_sync(2, Some(1), Some(8), false),
2626                ),
2627                (CallHostRoute::Cancel, cancel),
2628            ]))),
2629            requests: Default::default(),
2630        };
2631        let mut state = state();
2632        let mut session = caller_with(&transport, &mut state);
2633        assert_eq!(
2634            session
2635                .poll(
2636                    &transport,
2637                    &mut state,
2638                    authority(),
2639                    TaskIds { sdk: 2, host: 3 }
2640                )
2641                .unwrap(),
2642            SyncProgress::Terminal(RemoteTermination::NoResponseCancel)
2643        );
2644        session
2645            .finish_remote(
2646                &transport,
2647                &mut state,
2648                authority(),
2649                TaskIds { sdk: 3, host: 4 },
2650                false,
2651            )
2652            .unwrap();
2653        let requests = transport.requests.borrow();
2654        let last = requests.last().unwrap();
2655        assert_eq!(last.0, CallHostRoute::Cancel);
2656        let common = CommonRequest::decode(&last.1).unwrap();
2657        let outer = fields(common.body.as_deref().unwrap()).unwrap();
2658        assert_eq!(number(&outer, 2, None).unwrap(), 123);
2659        assert_eq!(repeated(&outer, 4, 16).unwrap(), vec![b"peer".as_slice()]);
2660
2661        let inner = fields(data(&outer, 1).unwrap()).unwrap();
2662        assert_eq!(number(&inner, 5, None).unwrap(), 123);
2663        assert_eq!(number(&inner, 6, None).unwrap(), 2);
2664    }
2665    #[test]
2666    fn previously_joined_peer_permanently_disarms_original_pending_cancel() {
2667        let transport = FakeTransport(std::cell::RefCell::new(std::collections::VecDeque::from([
2668            (CallHostRoute::AppAuth, auth_reply()),
2669            (CallHostRoute::Invite, invite_reply()),
2670            (CallHostRoute::LiveSync, joined_sync(false)),
2671            (
2672                CallHostRoute::LiveSync,
2673                roster_sync(3, Some(1), Some(8), false),
2674            ),
2675        ])));
2676        let mut state = state();
2677        let mut session = caller_with(&transport, &mut state);
2678        assert_eq!(
2679            session
2680                .poll(
2681                    &transport,
2682                    &mut state,
2683                    authority(),
2684                    TaskIds { sdk: 2, host: 3 }
2685                )
2686                .unwrap(),
2687            SyncProgress::MediaReady
2688        );
2689        assert!(!session.control.as_ref().unwrap().invitation_pending);
2690        assert!(session
2691            .control
2692            .as_ref()
2693            .unwrap()
2694            .pending_recipients
2695            .is_empty());
2696        assert!(session
2697            .poll(
2698                &transport,
2699                &mut state,
2700                authority(),
2701                TaskIds { sdk: 3, host: 4 }
2702            )
2703            .is_err());
2704        assert_eq!(session.phase(), PersonalPhase::Uncertain);
2705        assert_eq!(session.active.as_ref().unwrap().cookies, b"new");
2706        assert!(session
2707            .finish_remote(
2708                &transport,
2709                &mut state,
2710                authority(),
2711                TaskIds { sdk: 4, host: 5 },
2712                false
2713            )
2714            .is_err());
2715        assert!(transport.0.borrow().is_empty());
2716    }
2717}