From 7fa68a9aa4178102d985057c62e365416cfadd51 Mon Sep 17 00:00:00 2001 From: Mikayla Date: Sun, 10 Sep 2023 22:48:25 -0700 Subject: [PATCH] WIP: improve move and link handling around 'root paths', currently very incorrect and in need of a deeper rework --- .../src/channel_store/channel_index.rs | 66 ++-- crates/collab/src/db/queries/channels.rs | 38 +-- crates/collab/src/rpc.rs | 42 ++- .../collab/src/tests/channel_buffer_tests.rs | 10 +- crates/collab/src/tests/channel_tests.rs | 302 ++++++++++++------ crates/collab/src/tests/test_server.rs | 71 +++- 6 files changed, 362 insertions(+), 167 deletions(-) diff --git a/crates/channel/src/channel_store/channel_index.rs b/crates/channel/src/channel_store/channel_index.rs index a61109fa79..90cde46e90 100644 --- a/crates/channel/src/channel_store/channel_index.rs +++ b/crates/channel/src/channel_store/channel_index.rs @@ -1,10 +1,11 @@ -use std::{sync::Arc, ops::Deref}; +use std::{ops::Deref, sync::Arc}; use collections::HashMap; use rpc::proto; -use serde_derive::{Serialize, Deserialize}; +use serde_derive::{Deserialize, Serialize}; -use crate::{ChannelId, Channel}; + +use crate::{Channel, ChannelId}; pub type ChannelsById = HashMap>; @@ -21,9 +22,7 @@ impl Deref for ChannelPath { impl ChannelPath { pub fn parent_id(&self) -> Option { - self.0.len().checked_sub(2).map(|i| { - self.0[i] - }) + self.0.len().checked_sub(2).map(|i| self.0[i]) } } @@ -39,7 +38,6 @@ pub struct ChannelIndex { channels_by_id: ChannelsById, } - impl ChannelIndex { pub fn by_id(&self) -> &ChannelsById { &self.channels_by_id @@ -62,24 +60,53 @@ impl ChannelIndex { self.paths.iter() } - /// Remove the given edge from this index. This will not remove the channel - /// and may result in dangling channels. - pub fn delete_edge(&mut self, parent_id: ChannelId, channel_id: ChannelId) { - self.paths.retain(|path| { - !path - .windows(2) - .any(|window| window == [parent_id, channel_id]) - }); + /// Remove the given edge from this index. This will not remove the channel. + /// If this operation would result in a dangling edge, re-insert it. + pub fn delete_edge(&mut self, parent_id: Option, channel_id: ChannelId) { + if let Some(parent_id) = parent_id { + self.paths.retain(|path| { + !path + .windows(2) + .any(|window| window == [parent_id, channel_id]) + }); + } else { + self.paths.retain(|path| path.first() != Some(&channel_id)); + } + + // Ensure that there is at least one channel path in the index + if !self + .paths + .iter() + .any(|path| path.iter().any(|id| id == &channel_id)) + { + let path = ChannelPath(Arc::from([channel_id])); + let current_item: Vec<_> = + channel_path_sorting_key(&path, &self.channels_by_id).collect(); + match self.paths.binary_search_by(|channel_path| { + current_item + .iter() + .copied() + .cmp(channel_path_sorting_key(channel_path, &self.channels_by_id)) + }) { + Ok(ix) => self.paths.insert(ix, path), + Err(ix) => self.paths.insert(ix, path), + } + } } /// Delete the given channels from this index. pub fn delete_channels(&mut self, channels: &[ChannelId]) { - self.channels_by_id.retain(|channel_id, _| !channels.contains(channel_id)); - self.paths.retain(|channel_path| !channel_path.iter().any(|channel_id| {channels.contains(channel_id)})) + self.channels_by_id + .retain(|channel_id, _| !channels.contains(channel_id)); + self.paths.retain(|channel_path| { + !channel_path + .iter() + .any(|channel_id| channels.contains(channel_id)) + }) } /// Upsert one or more channels into this index. - pub fn start_upsert(& mut self) -> ChannelPathsUpsertGuard { + pub fn start_upsert(&mut self) -> ChannelPathsUpsertGuard { ChannelPathsUpsertGuard { paths: &mut self.paths, channels_by_id: &mut self.channels_by_id, @@ -90,7 +117,7 @@ impl ChannelIndex { /// A guard for ensuring that the paths index maintains its sort and uniqueness /// invariants after a series of insertions pub struct ChannelPathsUpsertGuard<'a> { - paths: &'a mut Vec, + paths: &'a mut Vec, channels_by_id: &'a mut ChannelsById, } @@ -151,7 +178,6 @@ impl<'a> Drop for ChannelPathsUpsertGuard<'a> { } } - fn channel_path_sorting_key<'a>( path: &'a [ChannelId], channels_by_id: &'a ChannelsById, diff --git a/crates/collab/src/db/queries/channels.rs b/crates/collab/src/db/queries/channels.rs index 2bfbb7abdb..449f48992f 100644 --- a/crates/collab/src/db/queries/channels.rs +++ b/crates/collab/src/db/queries/channels.rs @@ -870,36 +870,22 @@ impl Database { &self, user: UserId, channel: ChannelId, - from: Option, + from: ChannelId, tx: &DatabaseTransaction, ) -> Result<()> { - if let Some(from) = from { - self.check_user_is_channel_admin(from, user, &*tx).await?; + self.check_user_is_channel_admin(from, user, &*tx).await?; - let sql = r#" + let sql = r#" DELETE FROM channel_paths WHERE id_path LIKE '%' || $1 || '/' || $2 || '%' "#; - let channel_paths_stmt = Statement::from_sql_and_values( - self.pool.get_database_backend(), - sql, - [from.to_proto().into(), channel.to_proto().into()], - ); - tx.execute(channel_paths_stmt).await?; - } else { - let sql = r#" - DELETE FROM channel_paths - WHERE - id_path = '/' || $1 || '/' - "#; - let channel_paths_stmt = Statement::from_sql_and_values( - self.pool.get_database_backend(), - sql, - [channel.to_proto().into()], - ); - tx.execute(channel_paths_stmt).await?; - } + let channel_paths_stmt = Statement::from_sql_and_values( + self.pool.get_database_backend(), + sql, + [from.to_proto().into(), channel.to_proto().into()], + ); + tx.execute(channel_paths_stmt).await?; // Make sure that there is always at least one path to the channel let sql = r#" @@ -929,7 +915,7 @@ impl Database { &self, user: UserId, channel: ChannelId, - from: Option, + from: ChannelId, to: ChannelId, ) -> Result> { self.transaction(|tx| async move { @@ -941,6 +927,10 @@ impl Database { self.unlink_channel_internal(user, channel, from, &*tx) .await?; + dbg!(channel_path::Entity::find().all(&*tx).await); + + dbg!(&moved_channels); + Ok(moved_channels) }) .await diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index 325d2e390b..8a6488459b 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -2435,22 +2435,23 @@ async fn unlink_channel( let db = session.db().await; let channel_id = ChannelId::from_proto(request.channel_id); let from = request.from.map(ChannelId::from_proto); + + // Get the members before we remove it, so we know who to notify + let members = db.get_channel_members(channel_id).await?; + db.unlink_channel(session.user_id, channel_id, from).await?; - if let Some(from_parent) = from { - let members = db.get_channel_members(from_parent).await?; - let update = proto::UpdateChannels { - delete_channel_edge: vec![proto::ChannelEdge { - channel_id: channel_id.to_proto(), - parent_id: from_parent.to_proto(), - }], - ..Default::default() - }; - let connection_pool = session.connection_pool().await; - for member_id in members { - for connection_id in connection_pool.user_connection_ids(member_id) { - session.peer.send(connection_id, update.clone())?; - } + let update = proto::UpdateChannels { + delete_channel_edge: vec![proto::ChannelEdge { + channel_id: channel_id.to_proto(), + parent_id: from.map(ChannelId::to_proto), + }], + ..Default::default() + }; + let connection_pool = session.connection_pool().await; + for member_id in members { + for connection_id in connection_pool.user_connection_ids(member_id) { + session.peer.send(connection_id, update.clone())?; } } @@ -2468,16 +2469,24 @@ async fn move_channel( let channel_id = ChannelId::from_proto(request.channel_id); let from_parent = request.from.map(ChannelId::from_proto); let to = ChannelId::from_proto(request.to); + + let mut members = db.get_channel_members(channel_id).await?; + let channels_to_send: Vec = db .move_channel(session.user_id, channel_id, from_parent, to) .await?; + let members_after = db.get_channel_members(channel_id).await?; + + members.extend(members_after); + members.sort(); + members.dedup(); + if let Some(from_parent) = from_parent { - let members = db.get_channel_members(from_parent).await?; let update = proto::UpdateChannels { delete_channel_edge: vec![proto::ChannelEdge { channel_id: channel_id.to_proto(), - parent_id: from_parent.to_proto(), + parent_id: Some(from_parent.to_proto()), }], ..Default::default() }; @@ -2489,7 +2498,6 @@ async fn move_channel( } } - let members = db.get_channel_members(to).await?; let connection_pool = session.connection_pool().await; let update = proto::UpdateChannels { channels: channels_to_send diff --git a/crates/collab/src/tests/channel_buffer_tests.rs b/crates/collab/src/tests/channel_buffer_tests.rs index fe286895b4..5cfff053cb 100644 --- a/crates/collab/src/tests/channel_buffer_tests.rs +++ b/crates/collab/src/tests/channel_buffer_tests.rs @@ -25,7 +25,7 @@ async fn test_core_channel_buffers( let client_b = server.create_client(cx_b, "user_b").await; let channel_id = server - .make_channel("zed", (&client_a, cx_a), &mut [(&client_b, cx_b)]) + .make_channel("zed", None, (&client_a, cx_a), &mut [(&client_b, cx_b)]) .await; // Client A joins the channel buffer @@ -135,6 +135,7 @@ async fn test_channel_buffer_replica_ids( let channel_id = server .make_channel( "the-channel", + None, (&client_a, cx_a), &mut [(&client_b, cx_b), (&client_c, cx_c)], ) @@ -279,7 +280,7 @@ async fn test_reopen_channel_buffer(deterministic: Arc, cx_a: &mu let client_a = server.create_client(cx_a, "user_a").await; let channel_id = server - .make_channel("the-channel", (&client_a, cx_a), &mut []) + .make_channel("the-channel", None, (&client_a, cx_a), &mut []) .await; let channel_buffer_1 = client_a @@ -341,7 +342,7 @@ async fn test_channel_buffer_disconnect( let client_b = server.create_client(cx_b, "user_b").await; let channel_id = server - .make_channel("the-channel", (&client_a, cx_a), &mut [(&client_b, cx_b)]) + .make_channel("the-channel", None, (&client_a, cx_a), &mut [(&client_b, cx_b)]) .await; let channel_buffer_a = client_a @@ -411,7 +412,7 @@ async fn test_rejoin_channel_buffer( let client_b = server.create_client(cx_b, "user_b").await; let channel_id = server - .make_channel("the-channel", (&client_a, cx_a), &mut [(&client_b, cx_b)]) + .make_channel("the-channel", None, (&client_a, cx_a), &mut [(&client_b, cx_b)]) .await; let channel_buffer_a = client_a @@ -491,6 +492,7 @@ async fn test_channel_buffers_and_server_restarts( let channel_id = server .make_channel( "the-channel", + None, (&client_a, cx_a), &mut [(&client_b, cx_b), (&client_c, cx_c)], ) diff --git a/crates/collab/src/tests/channel_tests.rs b/crates/collab/src/tests/channel_tests.rs index a0d480e57f..008dc0abc5 100644 --- a/crates/collab/src/tests/channel_tests.rs +++ b/crates/collab/src/tests/channel_tests.rs @@ -326,7 +326,7 @@ async fn test_joining_channel_ancestor_member( let client_b = server.create_client(cx_b, "user_b").await; let parent_id = server - .make_channel("parent", (&client_a, cx_a), &mut [(&client_b, cx_b)]) + .make_channel("parent", None, (&client_a, cx_a), &mut [(&client_b, cx_b)]) .await; let sub_id = client_a @@ -361,6 +361,7 @@ async fn test_channel_room( let zed_id = server .make_channel( "zed", + None, (&client_a, cx_a), &mut [(&client_b, cx_b), (&client_c, cx_c)], ) @@ -544,9 +545,11 @@ async fn test_channel_jumping(deterministic: Arc, cx_a: &mut Test let mut server = TestServer::start(&deterministic).await; let client_a = server.create_client(cx_a, "user_a").await; - let zed_id = server.make_channel("zed", (&client_a, cx_a), &mut []).await; + let zed_id = server + .make_channel("zed", None, (&client_a, cx_a), &mut []) + .await; let rust_id = server - .make_channel("rust", (&client_a, cx_a), &mut []) + .make_channel("rust", None, (&client_a, cx_a), &mut []) .await; let active_call_a = cx_a.read(ActiveCall::global); @@ -597,7 +600,7 @@ async fn test_permissions_update_while_invited( let client_b = server.create_client(cx_b, "user_b").await; let rust_id = server - .make_channel("rust", (&client_a, cx_a), &mut []) + .make_channel("rust", None, (&client_a, cx_a), &mut []) .await; client_a @@ -658,7 +661,7 @@ async fn test_channel_rename( let client_b = server.create_client(cx_b, "user_b").await; let rust_id = server - .make_channel("rust", (&client_a, cx_a), &mut [(&client_b, cx_b)]) + .make_channel("rust", None, (&client_a, cx_a), &mut [(&client_b, cx_b)]) .await; // Rename the channel @@ -716,6 +719,7 @@ async fn test_call_from_channel( let channel_id = server .make_channel( "x", + None, (&client_a, cx_a), &mut [(&client_b, cx_b), (&client_c, cx_c)], ) @@ -776,6 +780,7 @@ async fn test_lost_channel_creation( deterministic: Arc, cx_a: &mut TestAppContext, cx_b: &mut TestAppContext, + cx_c: &mut TestAppContext, ) { deterministic.forbid_parking(); let mut server = TestServer::start(&deterministic).await; @@ -786,7 +791,9 @@ async fn test_lost_channel_creation( .make_contacts(&mut [(&client_a, cx_a), (&client_b, cx_b)]) .await; - let channel_id = server.make_channel("x", (&client_a, cx_a), &mut []).await; + let channel_id = server + .make_channel("x", None, (&client_a, cx_a), &mut []) + .await; // Invite a member client_a @@ -875,140 +882,216 @@ async fn test_lost_channel_creation( } #[gpui::test] -async fn test_channel_moving(deterministic: Arc, cx_a: &mut TestAppContext) { +async fn test_channel_moving( + deterministic: Arc, + cx_a: &mut TestAppContext, + cx_b: &mut TestAppContext, + cx_c: &mut TestAppContext, +) { deterministic.forbid_parking(); let mut server = TestServer::start(&deterministic).await; let client_a = server.create_client(cx_a, "user_a").await; + let client_b = server.create_client(cx_b, "user_b").await; + let client_c = server.create_client(cx_c, "user_c").await; - let channel_a_id = client_a - .channel_store() - .update(cx_a, |channel_store, cx| { - channel_store.create_channel("channel-a", None, cx) - }) - .await - .unwrap(); - let channel_b_id = client_a - .channel_store() - .update(cx_a, |channel_store, cx| { - channel_store.create_channel("channel-b", Some(channel_a_id), cx) - }) - .await - .unwrap(); - let channel_c_id = client_a - .channel_store() - .update(cx_a, |channel_store, cx| { - channel_store.create_channel("channel-c", Some(channel_b_id), cx) - }) - .await - .unwrap(); + let channels = server + .make_channel_tree( + &[ + ("channel-a", None), + ("channel-b", Some("channel-a")), + ("channel-c", Some("channel-b")), + ("channel-d", Some("channel-c")), + ], + (&client_a, cx_a), + ) + .await; + let channel_a_a_id = channels[0]; + let channel_a_b_id = channels[1]; + let channel_a_c_id = channels[2]; + let channel_a_d_id = channels[3]; // Current shape: - // a - b - c - deterministic.run_until_parked(); - assert_channels( + // a - b - c - d + assert_channels_list_shape( client_a.channel_store(), cx_a, &[ - ExpectedChannel { - id: channel_a_id, - name: "channel-a".to_string(), - depth: 0, - user_is_admin: true, - }, - ExpectedChannel { - id: channel_b_id, - name: "channel-b".to_string(), - depth: 1, - user_is_admin: true, - }, - ExpectedChannel { - id: channel_c_id, - name: "channel-c".to_string(), - depth: 2, - user_is_admin: true, - }, + (channel_a_a_id, 0), + (channel_a_b_id, 1), + (channel_a_c_id, 2), + (channel_a_d_id, 3), ], ); client_a .channel_store() .update(cx_a, |channel_store, cx| { - channel_store.move_channel(channel_c_id, Some(channel_b_id), channel_a_id, cx) + channel_store.move_channel(channel_a_d_id, Some(channel_a_c_id), channel_a_b_id, cx) }) .await .unwrap(); // Current shape: - // /- c - // a -- b - deterministic.run_until_parked(); - assert_channels( + // /- d + // a - b -- c + assert_channels_list_shape( client_a.channel_store(), cx_a, &[ - ExpectedChannel { - id: channel_a_id, - name: "channel-a".to_string(), - depth: 0, - user_is_admin: true, - }, - ExpectedChannel { - id: channel_b_id, - name: "channel-b".to_string(), - depth: 1, - user_is_admin: true, - }, - ExpectedChannel { - id: channel_c_id, - name: "channel-c".to_string(), - depth: 1, - user_is_admin: true, - }, + (channel_a_a_id, 0), + (channel_a_b_id, 1), + (channel_a_c_id, 2), + (channel_a_d_id, 2), ], ); client_a .channel_store() .update(cx_a, |channel_store, cx| { - channel_store.link_channel(channel_c_id, channel_b_id, cx) + channel_store.link_channel(channel_a_d_id, channel_a_c_id, cx) }) .await .unwrap(); // Current shape: - // /------\ - // a -- b -- c - deterministic.run_until_parked(); - assert_channels( + // /------\ + // a - b -- c -- d + assert_channels_list_shape( client_a.channel_store(), cx_a, &[ - ExpectedChannel { - id: channel_a_id, - name: "channel-a".to_string(), - depth: 0, - user_is_admin: true, - }, - ExpectedChannel { - id: channel_b_id, - name: "channel-b".to_string(), - depth: 1, - user_is_admin: true, - }, - ExpectedChannel { - id: channel_c_id, - name: "channel-c".to_string(), - depth: 2, - user_is_admin: true, - }, - ExpectedChannel { - id: channel_c_id, - name: "channel-c".to_string(), - depth: 1, - user_is_admin: true, - }, + (channel_a_a_id, 0), + (channel_a_b_id, 1), + (channel_a_c_id, 2), + (channel_a_d_id, 3), + (channel_a_d_id, 2), ], ); + + let b_channels = server + .make_channel_tree( + &[ + ("channel-mu", None), + ("channel-gamma", Some("channel-mu")), + ("channel-epsilon", Some("channel-mu")), + ], + (&client_b, cx_b), + ) + .await; + let channel_b_mu_id = b_channels[0]; + let channel_b_gamma_id = b_channels[1]; + let channel_b_epsilon_id = b_channels[2]; + + // Current shape for B: + // /- ep + // mu -- ga + assert_channels_list_shape( + client_b.channel_store(), + cx_b, + &[ + (channel_b_mu_id, 0), + (channel_b_gamma_id, 1), + (channel_b_epsilon_id, 1) + ], + ); + + client_a.add_admin_to_channel((&client_b, cx_b), channel_a_b_id, cx_a).await; + // Current shape for B: + // /- ep + // mu -- ga + // /---------\ + // b -- c -- d + assert_channels_list_shape( + client_b.channel_store(), + cx_b, + &[ + // B's old channels + (channel_b_mu_id, 0), + (channel_b_gamma_id, 1), + (channel_b_epsilon_id, 1), + + // New channels from a + (channel_a_b_id, 0), + (channel_a_c_id, 1), + (channel_a_d_id, 1), + (channel_a_d_id, 2), + ], + ); + + client_b + .channel_store() + .update(cx_a, |channel_store, cx| { + channel_store.move_channel(channel_a_b_id, None, channel_b_epsilon_id, cx) + }) + .await + .unwrap(); + + // Current shape for B: + // /---------\ + // /- ep -- b -- c -- d + // mu -- ga + assert_channels_list_shape( + client_b.channel_store(), + cx_b, + &[ + // B's old channels + (channel_b_mu_id, 0), + (channel_b_gamma_id, 1), + (channel_b_epsilon_id, 1), + + // New channels from a, now under epsilon + (channel_a_b_id, 2), + (channel_a_c_id, 3), + (channel_a_d_id, 3), + (channel_a_d_id, 4), + ], + ); + + client_b + .channel_store() + .update(cx_a, |channel_store, cx| { + channel_store.link_channel(channel_b_gamma_id, channel_a_b_id, cx) + }) + .await + .unwrap(); + + // Current shape for B: + // /---------\ + // /- ep -- b -- c -- d + // / \ + // mu ---------- ga + assert_channels_list_shape( + client_b.channel_store(), + cx_b, + &[ + // B's old channels + (channel_b_mu_id, 0), + (channel_b_gamma_id, 1), + (channel_b_epsilon_id, 1), + + // New channels from a, now under epsilon, with gamma + (channel_a_b_id, 2), + (channel_b_gamma_id, 3), + (channel_a_c_id, 3), + (channel_a_d_id, 3), + (channel_a_d_id, 4), + ], + ); + + // Current shape for A: + assert_channels_list_shape( + client_a.channel_store(), + cx_a, + &[ + (channel_a_a_id, 0), + (channel_a_b_id, 1), + (channel_b_gamma_id, 1), + (channel_a_c_id, 2), + (channel_a_d_id, 3), + (channel_a_d_id, 2), + ], + ); + // TODO: Make sure to test that non-local root removing problem I was thinking about } #[derive(Debug, PartialEq)] @@ -1059,3 +1142,20 @@ fn assert_channels( }); pretty_assertions::assert_eq!(actual, expected_channels); } + +#[track_caller] +fn assert_channels_list_shape( + channel_store: &ModelHandle, + cx: &TestAppContext, + expected_channels: &[(u64, usize)], +) { + cx.foreground().run_until_parked(); + + let actual = channel_store.read_with(cx, |store, _| { + store + .channels() + .map(|(depth, channel)| (channel.id, depth)) + .collect::>() + }); + pretty_assertions::assert_eq!(actual, expected_channels); +} diff --git a/crates/collab/src/tests/test_server.rs b/crates/collab/src/tests/test_server.rs index a7dbd97239..7f1a91f426 100644 --- a/crates/collab/src/tests/test_server.rs +++ b/crates/collab/src/tests/test_server.rs @@ -288,6 +288,7 @@ impl TestServer { pub async fn make_channel( &self, channel: &str, + parent: Option, admin: (&TestClient, &mut TestAppContext), members: &mut [(&TestClient, &mut TestAppContext)], ) -> u64 { @@ -296,7 +297,7 @@ impl TestServer { .app_state .channel_store .update(admin_cx, |channel_store, cx| { - channel_store.create_channel(channel, None, cx) + channel_store.create_channel(channel, parent, cx) }) .await .unwrap(); @@ -331,6 +332,39 @@ impl TestServer { channel_id } + pub async fn make_channel_tree( + &self, + channels: &[(&str, Option<&str>)], + creator: (&TestClient, &mut TestAppContext), + ) -> Vec { + let mut observed_channels = HashMap::default(); + let mut result = Vec::new(); + for (channel, parent) in channels { + let id; + if let Some(parent) = parent { + if let Some(parent_id) = observed_channels.get(parent) { + id = self + .make_channel(channel, Some(*parent_id), (creator.0, creator.1), &mut []) + .await; + } else { + panic!( + "Edge {}->{} referenced before {} was created", + parent, channel, parent + ) + } + } else { + id = self + .make_channel(channel, None, (creator.0, creator.1), &mut []) + .await; + } + + observed_channels.insert(channel, id); + result.push(id); + } + + result + } + pub async fn create_room(&self, clients: &mut [(&TestClient, &mut TestAppContext)]) { self.make_contacts(clients).await; @@ -549,6 +583,41 @@ impl TestClient { ) -> WindowHandle { cx.add_window(|cx| Workspace::new(0, project.clone(), self.app_state.clone(), cx)) } + + pub async fn add_admin_to_channel( + &self, + user: (&TestClient, &mut TestAppContext), + channel: u64, + cx_self: &mut TestAppContext, + ) { + let (other_client, other_cx) = user; + + self + .app_state + .channel_store + .update(cx_self, |channel_store, cx| { + channel_store.invite_member( + channel, + other_client.user_id().unwrap(), + true, + cx, + ) + }) + .await + .unwrap(); + + cx_self.foreground().run_until_parked(); + + other_client + .app_state + .channel_store + .update(other_cx, |channels, _| { + channels.respond_to_channel_invite(channel, true) + }) + .await + .unwrap(); + + } } impl Drop for TestClient {