From 55bbdf2f339bc1d9deaa00782fcb0dec0f717b37 Mon Sep 17 00:00:00 2001 From: RedStar Date: Fri, 9 Oct 2026 13:12:19 +0200 Subject: [PATCH 1/3] fix(plugin-gateway): keep the fetch calls that stay right on a cache miss Un-deprecate fetchHighest, fetchPermissions, fetchPermissionsIn and the fetchManageable/Kickable/Bannable/Moderatable family, and document on the getters that they skip roles missing from the cache. --- .changeset/gateway-keep-hierarchy-fetches.md | 5 ++ packages/plugin-gateway/README.md | 15 ++++- .../src/managers/GuildMemberRoleManager.ts | 21 +++++-- .../src/structures/guilds/GuildMember.ts | 57 ++++++++++++------- .../plugin-gateway/tests/sync-getters.test.ts | 30 ++++++++++ 5 files changed, 100 insertions(+), 28 deletions(-) create mode 100644 .changeset/gateway-keep-hierarchy-fetches.md diff --git a/.changeset/gateway-keep-hierarchy-fetches.md b/.changeset/gateway-keep-hierarchy-fetches.md new file mode 100644 index 00000000..895be336 --- /dev/null +++ b/.changeset/gateway-keep-hierarchy-fetches.md @@ -0,0 +1,5 @@ +--- +"@wolfstar/plugin-gateway": patch +--- + +Keep the single-call checks that stay right on a cache miss: `GuildMemberRoleManager#fetchHighest`, `GuildMember#fetchPermissions`, `GuildMember#fetchPermissionsIn`, `GuildMember#fetchManageable`, `GuildMember#fetchKickable`, `GuildMember#fetchBannable` and `GuildMember#fetchModeratable` are no longer `@deprecated`, reversing part of the deprecation of the `fetch*` twins. The getters they mirror (`roles.highest`, `permissions`, `permissionsIn`, `manageable`, `kickable`, `bannable`, `moderatable`) skip a role the cache lacks without an error, so a hierarchy or permission check can under-report with a filtered cache or a `plugin-broker` worker. Their docs and the README now say so and point at the `fetch*` call. diff --git a/packages/plugin-gateway/README.md b/packages/plugin-gateway/README.md index de8df19f..f576c688 100644 --- a/packages/plugin-gateway/README.md +++ b/packages/plugin-gateway/README.md @@ -805,9 +805,22 @@ With an entity they need missing from the cache they throw `GuildUncached`, `Gui `ChannelUncached`, or `GuildMemberUncached`, as discord.js throws `GuildUncachedMe`. Each of them also has a `fetch*` twin (`fetchPermissions()`, `fetchKickable()`, `fetchDeletable()`, -...) that asks the API for what is not cached. The twins are deprecated: the getter is the API, as +...) that asks the API for what is not cached. Most twins are deprecated: the getter is the API, as in discord.js. They will be removed in a later release. +The getters skip a role that is not in the cache, without an error: `member.roles.highest` answers +the highest role the cache holds and `member.permissions` the permissions of the roles it holds, so +a role-hierarchy or permission check can under-report. These twins are not deprecated, because they +are the single call that stays right on a cache miss: `member.roles.fetchHighest()`, +`member.fetchPermissions()`, `member.fetchPermissionsIn(channel)`, `member.fetchManageable()`, +`member.fetchKickable()`, `member.fetchBannable()` and `member.fetchModeratable()`. + +```ts +// Right even when a role of the author is not cached: +const [mine, theirs] = await Promise.all([me.roles.fetchHighest(), author.roles.fetchHighest()]); +const canManageRoles = (await me.fetchPermissions()).has("ManageRoles"); +``` + When the cache may lack an entity (a size-limited or filtered cache, or a `plugin-broker` worker that only receives some dispatches), fetch what is missing yourself, then read the getter: diff --git a/packages/plugin-gateway/src/managers/GuildMemberRoleManager.ts b/packages/plugin-gateway/src/managers/GuildMemberRoleManager.ts index dbae5ca8..87fe4da4 100644 --- a/packages/plugin-gateway/src/managers/GuildMemberRoleManager.ts +++ b/packages/plugin-gateway/src/managers/GuildMemberRoleManager.ts @@ -41,7 +41,10 @@ function highestFirst(roles: Iterable): Role[] { * (`highest`, `hoist`, `color`, ...) are `Awaitable`, like `manager.cache.get`. They are synchronous with the default * in-memory cache and a promise with a remote store, so `await` works with both. * - * They only know the roles the cache holds. The `fetch*` methods fall back to the API when a role is not cached. + * They only know the roles the cache holds, and skip the ones it lacks without an error. With a filtered cache or a + * `plugin-broker` worker that makes `highest` answer a lower role than the member's real one. Call + * {@link GuildMemberRoleManager.fetch} first, or {@link GuildMemberRoleManager.fetchHighest}, where a hierarchy check + * must not under-report. The other `fetch*` methods fall back to the API when a role is not cached. */ export class GuildMemberRoleManager extends BaseManager { /** @@ -94,8 +97,8 @@ export class GuildMemberRoleManager extends BaseManager { } /** - * The member's roles held in the cache, by ID, `@everyone` included. Roles that are not cached are skipped: use - * {@link GuildMemberRoleManager.fetch} to get them all. + * The member's roles held in the cache, by ID, `@everyone` included. Roles that are not cached are skipped, without + * an error: use {@link GuildMemberRoleManager.fetch} to get them all. */ public get cache(): Awaitable> { return cachedRoles(this.client, this.guildId, [...this.ids, this.guildId]); @@ -103,6 +106,11 @@ export class GuildMemberRoleManager extends BaseManager { /** * The member's highest cached role. `null` when none of their roles is cached, `@everyone` included. + * + * @remarks + * Roles missing from the cache are skipped, not an error: the answer is the highest role the cache holds, so a + * hierarchy check can compare a higher-ranked member as a lower one. Use {@link GuildMemberRoleManager.fetchHighest} + * for an answer that is right on a cache miss, or `await member.roles.fetch()` before reading this. */ public get highest(): Awaitable { return this.fromCache(pick.highest); @@ -163,9 +171,10 @@ export class GuildMemberRoleManager extends BaseManager { /** * Fetches the member's highest role, `@everyone` when they have no other. * - * @deprecated Use {@link GuildMemberRoleManager.highest}. When some of the member's roles may be missing from the - * cache (a filtered cache, a `plugin-broker` worker), call {@link GuildMemberRoleManager.fetch} first, then read the - * getter. + * @remarks + * The call for a role-hierarchy check that must be right on a cache miss: unlike + * {@link GuildMemberRoleManager.highest}, it asks the API for the roles the cache lacks, so it never answers a lower + * role than the member's real one. With a complete cache it makes no request. */ public async fetchHighest(): Promise { return pick.highest(await this.fetch()); diff --git a/packages/plugin-gateway/src/structures/guilds/GuildMember.ts b/packages/plugin-gateway/src/structures/guilds/GuildMember.ts index 9508c652..9c534c0d 100644 --- a/packages/plugin-gateway/src/structures/guilds/GuildMember.ts +++ b/packages/plugin-gateway/src/structures/guilds/GuildMember.ts @@ -299,6 +299,11 @@ export class GuildMember extends Structure { * The member's guild-wide permissions, before channel overwrites, like discord.js's `GuildMember#permissions`: * computed from the cached guild and roles, skipping the roles that are not cached. * + * @remarks + * A role missing from the cache is skipped, not an error, so the answer can hold fewer permissions than the member + * has. Use {@link GuildMember.fetchPermissions} for an answer that is right on a cache miss, or + * `await member.roles.fetch()` before reading this. + * * @throws A `GatewayError`: `GuildUncached` when the guild is not cached. */ public get permissions(): CacheRead> { @@ -319,9 +324,9 @@ export class GuildMember extends Structure { * Fetches the member's guild-wide permissions, before channel overwrites: {@link GuildMember.permissions} with the * guild and the roles fetched from the API when they are not cached, whatever the cache. * - * @deprecated Use {@link GuildMember.permissions}. When the guild or some roles may be missing from the cache (a - * filtered cache, a `plugin-broker` worker), fetch them first (`client.guilds.fetch(guildId)`, - * `member.roles.fetch()`), then read the getter. + * @remarks + * The call for a permission check that must be right on a cache miss: the getter skips the roles the cache lacks + * and can answer fewer permissions than the member has. */ public async fetchPermissions(): Promise> { const client = this.client; @@ -340,10 +345,11 @@ export class GuildMember extends Structure { * Fetches the member's permissions in a channel: their guild permissions with the channel's overwrites applied. * discord.js: `member.permissionsIn(channel)`. * - * @param channel The channel, or its ID. Threads use their parent's overwrites. + * @remarks + * Like {@link GuildMember.fetchPermissions}, the call for a check that must be right on a cache miss: it also + * fetches the channel and the roles when they are not cached. * - * @deprecated Use {@link GuildMember.permissionsIn}. See {@link GuildMember.fetchPermissions} for the cache-miss - * fallback; fetch the channel too (`client.channels.fetch(channelId)`). + * @param channel The channel, or its ID. Threads use their parent's overwrites. */ public fetchPermissionsIn(channel: AnyChannel | string): Promise> { return computePermissionsIn(channel, this); @@ -354,6 +360,10 @@ export class GuildMember extends Structure { * with the channel's overwrites applied, read from the cache. * * @param channel The channel, or the ID of a cached one. Threads use their parent's overwrites. + * @remarks + * A role missing from the cache is skipped, as in {@link GuildMember.permissions}: use + * {@link GuildMember.fetchPermissionsIn} for an answer that is right on a cache miss. + * * @throws A `GatewayError`: `ChannelUncached` or `GuildUncached` when they are not cached. */ public permissionsIn(channel: AnyChannel | string): CacheRead> { @@ -453,6 +463,11 @@ export class GuildMember extends Structure { * Whether the bot ranks above the member, like discord.js's `GuildMember#manageable`: it is not the guild owner, not * the bot itself, and the bot's highest role is higher than theirs (or the bot owns the guild). * + * @remarks + * It reads {@link GuildMemberRoleManager.highest}, which skips the roles missing from the cache: a higher role that + * is not cached makes the member look lower. Use {@link GuildMember.fetchManageable} for an answer that is right on + * a cache miss, or `await member.roles.fetch()` and `await me.roles.fetch()` before reading this. + * * @throws A `GatewayError`: `GuildUncached` or `GuildUncachedMe` when the guild or the bot's member is * not cached. */ @@ -476,9 +491,9 @@ export class GuildMember extends Structure { * Fetches whether the bot ranks above the member: {@link GuildMember.manageable} with the guild, the bot's member, * and the roles fetched from the API when they are not cached, whatever the cache. * - * @deprecated Use {@link GuildMember.manageable}. When the guild, the bot's member or some roles may be missing from - * the cache (a filtered cache, a `plugin-broker` worker), fetch them first (`client.guilds.fetch(guildId)`, - * `client.members.fetchMe(guildId)`, `member.roles.fetch()`), then read the getter. + * @remarks + * The call for a hierarchy check that must be right on a cache miss: {@link GuildMember.manageable} compares the + * highest roles the cache holds. */ public async fetchManageable(): Promise { const client = this.client; @@ -493,7 +508,8 @@ export class GuildMember extends Structure { /** * Whether the bot can kick the member, like discord.js's `GuildMember#kickable`: it outranks them and has - * `KickMembers`. It throws like {@link GuildMember.manageable}. + * `KickMembers`. It throws, and skips uncached roles, like {@link GuildMember.manageable}: see + * {@link GuildMember.fetchKickable} for the call that is right on a cache miss. */ public get kickable(): CacheRead { return cacheRead(this.managedWithCached("KickMembers")); @@ -501,7 +517,8 @@ export class GuildMember extends Structure { /** * Whether the bot can ban the member, like discord.js's `GuildMember#bannable`: it outranks them and has - * `BanMembers`. It throws like {@link GuildMember.manageable}. + * `BanMembers`. It throws, and skips uncached roles, like {@link GuildMember.manageable}: see + * {@link GuildMember.fetchBannable} for the call that is right on a cache miss. */ public get bannable(): CacheRead { return cacheRead(this.managedWithCached("BanMembers")); @@ -509,7 +526,9 @@ export class GuildMember extends Structure { /** * Whether the bot can time the member out, like discord.js's `GuildMember#moderatable`: it outranks them, has - * `ModerateMembers`, and they are no administrator. It throws like {@link GuildMember.manageable}. + * `ModerateMembers`, and they are no administrator. It throws, and skips uncached roles, like + * {@link GuildMember.manageable}: see {@link GuildMember.fetchModeratable} for the call that is right on a cache + * miss. */ public get moderatable(): CacheRead { return cacheRead( @@ -522,18 +541,16 @@ export class GuildMember extends Structure { } /** - * Whether the bot can kick the member: it outranks them and has `KickMembers`. - * - * @deprecated Use {@link GuildMember.kickable}. See {@link GuildMember.fetchManageable} for the cache-miss fallback. + * Whether the bot can kick the member: it outranks them and has `KickMembers`. Right on a cache miss, like + * {@link GuildMember.fetchManageable}. */ public fetchKickable(): Promise { return this.managedWith("KickMembers"); } /** - * Whether the bot can ban the member: it outranks them and has `BanMembers`. - * - * @deprecated Use {@link GuildMember.bannable}. See {@link GuildMember.fetchManageable} for the cache-miss fallback. + * Whether the bot can ban the member: it outranks them and has `BanMembers`. Right on a cache miss, like + * {@link GuildMember.fetchManageable}. */ public fetchBannable(): Promise { return this.managedWith("BanMembers"); @@ -541,9 +558,7 @@ export class GuildMember extends Structure { /** * Whether the bot can time the member out: it outranks them, has `ModerateMembers`, and they are no administrator. - * - * @deprecated Use {@link GuildMember.moderatable}. See {@link GuildMember.fetchManageable} for the cache-miss - * fallback. + * Right on a cache miss, like {@link GuildMember.fetchManageable}. */ public async fetchModeratable(): Promise { if (!(await this.managedWith("ModerateMembers"))) return false; diff --git a/packages/plugin-gateway/tests/sync-getters.test.ts b/packages/plugin-gateway/tests/sync-getters.test.ts index 9ff8cbb0..00e25a37 100644 --- a/packages/plugin-gateway/tests/sync-getters.test.ts +++ b/packages/plugin-gateway/tests/sync-getters.test.ts @@ -267,6 +267,36 @@ describe.each(synchronousModes)("member getters with %s", (_, options) => { } }); + test("GIVEN a role missing from the cache THEN the getters under-report and the fetch calls stay right", async () => { + const client = createClient(options()); + await seed(client); + const [top, bot] = await Promise.all([topId, botId].map((id) => memberOf(client, id))); + await client.cache!.roles.delete(roleKey(guildId, topRoleId)); + const get = vi + .spyOn(container.rest, "get") + .mockResolvedValue([ + role(guildId, 0, flags.ViewChannel), + role(modRoleId, 2, flags.KickMembers | flags.BanMembers | flags.ModerateMembers), + role(topRoleId, 3, flags.ManageRoles), + ]); + + // The cache lacks the member's only role: the getters fall back to @everyone. + expect((await top.roles.highest)?.id).toBe(guildId); + expect(top.permissions.has("ManageRoles")).toBe(false); + expect(top.manageable).toBe(true); + expect(get).not.toHaveBeenCalled(); + + expect((await top.roles.fetchHighest())?.id).toBe(topRoleId); + expect((await top.fetchPermissions()).has("ManageRoles")).toBe(true); + expect((await top.fetchPermissionsIn(channelId)).has("ManageRoles")).toBe(true); + expect(await top.fetchManageable()).toBe(false); + expect(await top.fetchKickable()).toBe(false); + expect(await top.fetchBannable()).toBe(false); + expect(await top.fetchModeratable()).toBe(false); + expect((await bot.roles.fetchHighest())?.id).toBe(modRoleId); + get.mockRestore(); + }); + test("GIVEN the bot owns the guild THEN everyone else is manageable", async () => { const client = createClient(options()); await seed(client, { guildOwner: botId }); From f85b37d03e66289430d1fe75395930a8c1dee986 Mon Sep 17 00:00:00 2001 From: RedStar Date: Fri, 9 Oct 2026 13:18:33 +0200 Subject: [PATCH 2/3] fix(plugin-gateway): keep only fetchHighest, fetchPermissions and fetchPermissionsIn Re-deprecate the fetchManageable family, as in #172; only the three calls named in #234 stay. --- .changeset/gateway-keep-hierarchy-fetches.md | 2 +- packages/plugin-gateway/README.md | 8 ++--- .../src/structures/guilds/GuildMember.ts | 33 ++++++++++--------- .../plugin-gateway/tests/sync-getters.test.ts | 8 ++--- 4 files changed, 25 insertions(+), 26 deletions(-) diff --git a/.changeset/gateway-keep-hierarchy-fetches.md b/.changeset/gateway-keep-hierarchy-fetches.md index 895be336..7e1228a3 100644 --- a/.changeset/gateway-keep-hierarchy-fetches.md +++ b/.changeset/gateway-keep-hierarchy-fetches.md @@ -2,4 +2,4 @@ "@wolfstar/plugin-gateway": patch --- -Keep the single-call checks that stay right on a cache miss: `GuildMemberRoleManager#fetchHighest`, `GuildMember#fetchPermissions`, `GuildMember#fetchPermissionsIn`, `GuildMember#fetchManageable`, `GuildMember#fetchKickable`, `GuildMember#fetchBannable` and `GuildMember#fetchModeratable` are no longer `@deprecated`, reversing part of the deprecation of the `fetch*` twins. The getters they mirror (`roles.highest`, `permissions`, `permissionsIn`, `manageable`, `kickable`, `bannable`, `moderatable`) skip a role the cache lacks without an error, so a hierarchy or permission check can under-report with a filtered cache or a `plugin-broker` worker. Their docs and the README now say so and point at the `fetch*` call. +Keep the single-call checks that stay right on a cache miss: `GuildMemberRoleManager#fetchHighest`, `GuildMember#fetchPermissions` and `GuildMember#fetchPermissionsIn` are no longer `@deprecated`, reversing part of the deprecation of the `fetch*` twins. The getters they mirror (`roles.highest`, `permissions`, `permissionsIn`, `manageable`, `kickable`, `bannable`, `moderatable`) skip a role the cache lacks without an error, so a hierarchy or permission check can under-report with a filtered cache or a `plugin-broker` worker. Their docs and the README now say so and point at the `fetch*` call or `roles.fetch()`. diff --git a/packages/plugin-gateway/README.md b/packages/plugin-gateway/README.md index f576c688..b0fc209d 100644 --- a/packages/plugin-gateway/README.md +++ b/packages/plugin-gateway/README.md @@ -810,10 +810,10 @@ in discord.js. They will be removed in a later release. The getters skip a role that is not in the cache, without an error: `member.roles.highest` answers the highest role the cache holds and `member.permissions` the permissions of the roles it holds, so -a role-hierarchy or permission check can under-report. These twins are not deprecated, because they -are the single call that stays right on a cache miss: `member.roles.fetchHighest()`, -`member.fetchPermissions()`, `member.fetchPermissionsIn(channel)`, `member.fetchManageable()`, -`member.fetchKickable()`, `member.fetchBannable()` and `member.fetchModeratable()`. +a role-hierarchy or permission check can under-report. These three twins are not deprecated, because +they are the single call that stays right on a cache miss: `member.roles.fetchHighest()`, +`member.fetchPermissions()` and `member.fetchPermissionsIn(channel)`. For `manageable`, `kickable`, +`bannable` and `moderatable`, `await member.roles.fetch()` (and the bot's) before reading the getter. ```ts // Right even when a role of the author is not cached: diff --git a/packages/plugin-gateway/src/structures/guilds/GuildMember.ts b/packages/plugin-gateway/src/structures/guilds/GuildMember.ts index 9c534c0d..7393f35d 100644 --- a/packages/plugin-gateway/src/structures/guilds/GuildMember.ts +++ b/packages/plugin-gateway/src/structures/guilds/GuildMember.ts @@ -465,8 +465,8 @@ export class GuildMember extends Structure { * * @remarks * It reads {@link GuildMemberRoleManager.highest}, which skips the roles missing from the cache: a higher role that - * is not cached makes the member look lower. Use {@link GuildMember.fetchManageable} for an answer that is right on - * a cache miss, or `await member.roles.fetch()` and `await me.roles.fetch()` before reading this. + * is not cached makes the member look lower. Call `await member.roles.fetch()` and `await me.roles.fetch()` before + * reading this, or compare {@link GuildMemberRoleManager.fetchHighest} of both. * * @throws A `GatewayError`: `GuildUncached` or `GuildUncachedMe` when the guild or the bot's member is * not cached. @@ -491,9 +491,9 @@ export class GuildMember extends Structure { * Fetches whether the bot ranks above the member: {@link GuildMember.manageable} with the guild, the bot's member, * and the roles fetched from the API when they are not cached, whatever the cache. * - * @remarks - * The call for a hierarchy check that must be right on a cache miss: {@link GuildMember.manageable} compares the - * highest roles the cache holds. + * @deprecated Use {@link GuildMember.manageable}. When the guild, the bot's member or some roles may be missing from + * the cache (a filtered cache, a `plugin-broker` worker), fetch them first (`client.guilds.fetch(guildId)`, + * `client.members.fetchMe(guildId)`, `member.roles.fetch()`), then read the getter. */ public async fetchManageable(): Promise { const client = this.client; @@ -508,8 +508,7 @@ export class GuildMember extends Structure { /** * Whether the bot can kick the member, like discord.js's `GuildMember#kickable`: it outranks them and has - * `KickMembers`. It throws, and skips uncached roles, like {@link GuildMember.manageable}: see - * {@link GuildMember.fetchKickable} for the call that is right on a cache miss. + * `KickMembers`. It throws, and skips uncached roles, like {@link GuildMember.manageable}. */ public get kickable(): CacheRead { return cacheRead(this.managedWithCached("KickMembers")); @@ -517,8 +516,7 @@ export class GuildMember extends Structure { /** * Whether the bot can ban the member, like discord.js's `GuildMember#bannable`: it outranks them and has - * `BanMembers`. It throws, and skips uncached roles, like {@link GuildMember.manageable}: see - * {@link GuildMember.fetchBannable} for the call that is right on a cache miss. + * `BanMembers`. It throws, and skips uncached roles, like {@link GuildMember.manageable}. */ public get bannable(): CacheRead { return cacheRead(this.managedWithCached("BanMembers")); @@ -527,8 +525,7 @@ export class GuildMember extends Structure { /** * Whether the bot can time the member out, like discord.js's `GuildMember#moderatable`: it outranks them, has * `ModerateMembers`, and they are no administrator. It throws, and skips uncached roles, like - * {@link GuildMember.manageable}: see {@link GuildMember.fetchModeratable} for the call that is right on a cache - * miss. + * {@link GuildMember.manageable}. */ public get moderatable(): CacheRead { return cacheRead( @@ -541,16 +538,18 @@ export class GuildMember extends Structure { } /** - * Whether the bot can kick the member: it outranks them and has `KickMembers`. Right on a cache miss, like - * {@link GuildMember.fetchManageable}. + * Whether the bot can kick the member: it outranks them and has `KickMembers`. + * + * @deprecated Use {@link GuildMember.kickable}. See {@link GuildMember.fetchManageable} for the cache-miss fallback. */ public fetchKickable(): Promise { return this.managedWith("KickMembers"); } /** - * Whether the bot can ban the member: it outranks them and has `BanMembers`. Right on a cache miss, like - * {@link GuildMember.fetchManageable}. + * Whether the bot can ban the member: it outranks them and has `BanMembers`. + * + * @deprecated Use {@link GuildMember.bannable}. See {@link GuildMember.fetchManageable} for the cache-miss fallback. */ public fetchBannable(): Promise { return this.managedWith("BanMembers"); @@ -558,7 +557,9 @@ export class GuildMember extends Structure { /** * Whether the bot can time the member out: it outranks them, has `ModerateMembers`, and they are no administrator. - * Right on a cache miss, like {@link GuildMember.fetchManageable}. + * + * @deprecated Use {@link GuildMember.moderatable}. See {@link GuildMember.fetchManageable} for the cache-miss + * fallback. */ public async fetchModeratable(): Promise { if (!(await this.managedWith("ModerateMembers"))) return false; diff --git a/packages/plugin-gateway/tests/sync-getters.test.ts b/packages/plugin-gateway/tests/sync-getters.test.ts index 00e25a37..800d4c22 100644 --- a/packages/plugin-gateway/tests/sync-getters.test.ts +++ b/packages/plugin-gateway/tests/sync-getters.test.ts @@ -267,7 +267,7 @@ describe.each(synchronousModes)("member getters with %s", (_, options) => { } }); - test("GIVEN a role missing from the cache THEN the getters under-report and the fetch calls stay right", async () => { + test("GIVEN a role missing from the cache THEN the getters under-report and fetchHighest, fetchPermissions and fetchPermissionsIn stay right", async () => { const client = createClient(options()); await seed(client); const [top, bot] = await Promise.all([topId, botId].map((id) => memberOf(client, id))); @@ -289,10 +289,8 @@ describe.each(synchronousModes)("member getters with %s", (_, options) => { expect((await top.roles.fetchHighest())?.id).toBe(topRoleId); expect((await top.fetchPermissions()).has("ManageRoles")).toBe(true); expect((await top.fetchPermissionsIn(channelId)).has("ManageRoles")).toBe(true); - expect(await top.fetchManageable()).toBe(false); - expect(await top.fetchKickable()).toBe(false); - expect(await top.fetchBannable()).toBe(false); - expect(await top.fetchModeratable()).toBe(false); + await top.roles.fetch(); + expect(top.manageable).toBe(false); expect((await bot.roles.fetchHighest())?.id).toBe(modRoleId); get.mockRestore(); }); From 6aa864e1b41922d9d32bd67a74cdbfd20dc25a74 Mon Sep 17 00:00:00 2001 From: RedStar Date: Fri, 9 Oct 2026 13:21:02 +0200 Subject: [PATCH 3/3] docs(plugin-gateway): list only deprecated twins as examples in the readme --- packages/plugin-gateway/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/plugin-gateway/README.md b/packages/plugin-gateway/README.md index b0fc209d..640df0b4 100644 --- a/packages/plugin-gateway/README.md +++ b/packages/plugin-gateway/README.md @@ -804,7 +804,7 @@ discord.js's derived getters are there too: `member.permissions`, `permissionsIn With an entity they need missing from the cache they throw `GuildUncached`, `GuildUncachedMe`, `ChannelUncached`, or `GuildMemberUncached`, as discord.js throws `GuildUncachedMe`. -Each of them also has a `fetch*` twin (`fetchPermissions()`, `fetchKickable()`, `fetchDeletable()`, +Each of them also has a `fetch*` twin (`fetchKickable()`, `fetchDeletable()`, `fetchEditable()`, ...) that asks the API for what is not cached. Most twins are deprecated: the getter is the API, as in discord.js. They will be removed in a later release.