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.
This commit is contained in:
Michel de Graaf
2026-09-10 13:59:04 +02:00
parent e39427ada5
commit 7060d3521a
3 changed files with 30 additions and 4 deletions
+5 -3
View File
@@ -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());
}
}
+19
View File
@@ -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();