From a71e099fa8a00a313fa371746db345e115396610 Mon Sep 17 00:00:00 2001 From: Justin Scroggins Date: Tue, 20 Oct 2026 09:37:44 -0400 Subject: [PATCH] fix: answer Discord members asked before READY as pending, not unsupported A Members request that came before READY found no server described yet and was refused as Unsupported ("Discord cannot do this yet"). The member pane takes a refusal as a failed page it does not ask for again, and the details pane drops it, so a list opened while connecting stayed empty after the account connected. Before READY the answer is now pending with no members, as a cache miss after connect already is. Both ask again every 250 ms, and once READY has placed the Watch the list is subscribed and fills. Nothing is placed from the early request. Once any server is known, one that is not is still refused. Refs #43 Co-Authored-By: Claude Opus 5.5 --- crates/chatd/src/adapters/discord/members.rs | 7 +++- crates/chatd/tests/discord.rs | 36 ++++++++++++++++++++ 2 files changed, 42 insertions(+), 1 deletion(-) diff --git a/crates/chatd/src/adapters/discord/members.rs b/crates/chatd/src/adapters/discord/members.rs index 0129f1a..7ec64e0 100644 --- a/crates/chatd/src/adapters/discord/members.rs +++ b/crates/chatd/src/adapters/discord/members.rs @@ -401,7 +401,12 @@ impl Discord { } let (groups, members, pending) = { let mut state = self.state.lock().unwrap(); - if !state.guilds.contains_key(&guild) { return answer(reply, Err(Refusal::Unsupported)); } + if !state.guilds.contains_key(&guild) { + if !state.guilds.is_empty() { return answer(reply, Err(Refusal::Unsupported)); } + // Before READY no server is known yet: the list is still to come, not refused. + // Nothing is placed here; READY places the Watch once it has said what the servers are. + return answer(reply, Ok(Members { pending: true, sections: Vec::new(), members: Vec::new() })); + } let watched = &mut state.members; if watched.seen.members.is_some() && watched.list.as_deref() != Some(&channel) { // A delayed query from a previous conversation cannot take over diff --git a/crates/chatd/tests/discord.rs b/crates/chatd/tests/discord.rs index 8ee83b3..1890d61 100644 --- a/crates/chatd/tests/discord.rs +++ b/crates/chatd/tests/discord.rs @@ -1486,6 +1486,42 @@ fn member_viewport_selected_before_ready_is_subscribed_after_connect() { assert!(matches!(pending, Response::Members { pending: true, ref members, .. } if members.is_empty()), "{pending:?}"); } +#[test] +fn members_asked_before_ready_are_pending_and_fill_after_connect() { + let mut world = World::start(); + world.tree("4000"); + // chatd starts again with the server stored from the last run, and READY is slow to come. + *world.discord.ready_after.lock().unwrap() = Duration::from_millis(1500); + world.restart(); + wait("the identify", || (world.discord.frames_of(2).len() == 2).then_some(())); + let members = || world.ask(Request::Members { conversation: world.key("4105"), offset: 0, limit: 50 }); + let early = members(); + assert_ne!(world.link(), Link::Connected, "READY came before the question"); + // Not refused: the shell keeps asking for a pending page, and gives up on a refused one. + assert!(matches!(early, Response::Members { pending: true, ref members, .. } if members.is_empty()), "{early:?}"); + wait("the account to connect", || (world.link() == Link::Connected).then_some(())); + + // Asked again, as the shell does for a pending page, the list is subscribed and fills. + wait("the member window", || { + members(); + world.discord.frames_of(37).pop().filter(|frame| frame["d"]["subscriptions"]["4000"]["channels"]["4105"].is_array()) + }); + world.discord.push("GUILD_MEMBER_LIST_UPDATE", json!({ + "guild_id": "4000", + "groups": [{"id": "online", "count": 1}], + "ops": [{"op": "SYNC", "range": [0, 99], "items": [ + {"group": {"id": "online", "count": 1}}, + {"member": {"user": {"id": "2006", "username": "sam", "global_name": "Sam Whitlock", "discriminator": "0", "avatar": null}, "roles": []}}, + ]}], + })); + let filled = wait("the members", || { + let response = members(); + matches!(response, Response::Members { pending: false, .. }).then_some(response) + }); + let Response::Members { members, .. } = filled else { unreachable!() }; + assert_eq!(members.iter().map(|person| person.name.as_str()).collect::>(), ["Sam Whitlock"]); +} + #[test] fn servers_channels_people_and_settings_follow_live_events() { let world = World::start();