From 7060d3521a06c516bffa1a9b37d44fe8e3a0631b Mon Sep 17 00:00:00 2001 From: Michel de Graaf Date: Thu, 10 Sep 2026 13:59:04 +0200 Subject: [PATCH] fix(rooms): start a room's roster from the member list, not the join echo soulfind tells a room about a new member, the new member included, before it sends them the room's member list. The roster was started from that echo, so for a moment after joining a room read as holding nobody but us, and `room users` returns on the first roster it sees: it could print only the asking user. That is the flake in room_users_lists_who_is_in_the_room. A join or leave now only updates a roster the server has sent in full, which is also what room_members already promised for a room we have not joined. The e2e test waits for the resident's own member list instead of a fixed second, so it no longer races the resident's join either. --- soulseek-rs-lib/src/client/context.rs | 8 +++++--- soulseek-rs-lib/src/client/tests/rooms.rs | 19 +++++++++++++++++++ soulseek-rs/tests/cli_e2e.rs | 7 ++++++- 3 files changed, 30 insertions(+), 4 deletions(-) diff --git a/soulseek-rs-lib/src/client/context.rs b/soulseek-rs-lib/src/client/context.rs index b51a58a..8606abf 100644 --- a/soulseek-rs-lib/src/client/context.rs +++ b/soulseek-rs-lib/src/client/context.rs @@ -100,10 +100,12 @@ impl ClientContext { // the whole board again on the next join. self.room_tickers.remove(room); } + // Only a roster the server has sent in full is kept current: the + // joiner hears of their own arrival before the member list. RoomEvent::UserJoined { room, username } => { - let members = - self.room_members.entry(room.clone()).or_default(); - if let Err(at) = members.binary_search(username) { + if let Some(members) = self.room_members.get_mut(room) + && let Err(at) = members.binary_search(username) + { members.insert(at, username.clone()); } } diff --git a/soulseek-rs-lib/src/client/tests/rooms.rs b/soulseek-rs-lib/src/client/tests/rooms.rs index 0af6784..d504839 100644 --- a/soulseek-rs-lib/src/client/tests/rooms.rs +++ b/soulseek-rs-lib/src/client/tests/rooms.rs @@ -39,6 +39,25 @@ fn the_room_roster_follows_the_joins_and_leaves_the_server_reports() { assert!(context.room_members("lobby").is_empty()); } +#[test] +fn a_join_announced_before_the_member_list_does_not_start_a_roster() { + // soulfind tells a room about a joiner, the joiner included, before it + // sends them the member list. A roster started from that echo reads as a + // room holding nobody else. + let mut context = ClientContext::new(); + context.apply_room_event(RoomEvent::UserJoined { + room: "lobby".to_string(), + username: "me".to_string(), + }); + assert!(context.room_members("lobby").is_empty()); + + context.apply_room_event(RoomEvent::Joined { + room: "lobby".to_string(), + users: vec!["me".to_string(), "alice".to_string()], + }); + assert_eq!(context.room_members("lobby"), ["alice", "me"]); +} + #[test] fn a_user_joining_twice_is_listed_once() { let mut context = ClientContext::new(); diff --git a/soulseek-rs/tests/cli_e2e.rs b/soulseek-rs/tests/cli_e2e.rs index 040e663..a65e3e4 100644 --- a/soulseek-rs/tests/cli_e2e.rs +++ b/soulseek-rs/tests/cli_e2e.rs @@ -2418,7 +2418,12 @@ fn room_users_lists_who_is_in_the_room() { let server = server_or_skip!(); let resident = server.client("cli_e2e_resident", Vec::new()); resident.join_room("cli_e2e_roster").expect("join"); - settle(); + // The member list reaches the resident once the server has them in. + let deadline = Instant::now() + Duration::from_secs(10); + while resident.room_members("cli_e2e_roster").is_empty() { + assert!(Instant::now() < deadline, "the resident never got in"); + std::thread::sleep(Duration::from_millis(50)); + } let output = cli( &server,