From 466842c6f27fcbcb1fcab8dce833ceb3f9b69ad1 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 18 Aug 2026 19:08:22 -0500 Subject: [PATCH] fix(guilds): do not offer linking where linking cannot reach MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Found on the live rig, with the shard's guild feature gated to staff: the refusal still read "link your account — this shard shows guild information to linked players". Signing in reaches `logged_in` and linking a game account reaches `player`; `staff` and `admin` are roles an operator grants, and no amount of linking earns them. Inviting someone to do something that changes nothing is worse than plainly saying no. Also drops the host name from the list embed's title. `ctx.site` carries a base URL and no brand name, so naming the deployment there could only ever mean printing its hostname into a title on the shard's own Discord server. Co-Authored-By: Claude --- server/commands/guild.command.js | 24 ++++++++++++++++++------ server/test/guildCommand.test.js | 9 +++++++++ 2 files changed, 27 insertions(+), 6 deletions(-) diff --git a/server/commands/guild.command.js b/server/commands/guild.command.js index 2958a56..9908016 100644 --- a/server/commands/guild.command.js +++ b/server/commands/guild.command.js @@ -42,13 +42,22 @@ async function levelFor(actor) { return visibility.viewerLevel({ user: { id: actor.userId, role: actor.role } }) } -// The nudge §9 answer 5 asks for, and only when it is TRUE. An unlinked caller -// who was told nothing because the shard publishes nothing is not helped by -// being invited to link; the prompt appears when linking is what would actually -// change the answer. +// The nudge §9 answer 5 asks for, and only when it is TRUE. +// +// **Linking reaches exactly two rungs and no further.** Signing in gets a caller +// to `logged_in` and linking a game account to `player`; `staff` and `admin` are +// roles an operator grants and no amount of linking will earn. So a shard that +// gates guilds to staff refuses an unlinked caller WITHOUT the invitation — +// telling them to link would be telling them to do something that changes +// nothing, which is worse than saying no. +// +// The live walk found this: gated to `staff`, the refusal still read "this shard +// shows guild information to linked players". +const LINKING_REACHES = new Set(['logged_in', 'player']) + function linkPrompt(actor, audience) { if (actor.isLinked) return null - if (audience === 'anonymous') return null + if (!LINKING_REACHES.has(audience)) return null return 'Link your account on the site to see more — this shard shows guild information to linked players.' } @@ -154,7 +163,10 @@ async function handler({ options, actor }) { if (!wanted) { const top = [...rows].sort((a, b) => (b.members || 0) - (a.members || 0)).slice(0, LIST_LIMIT) return { - title: `Guilds on ${core.baseUrl.replace(/^https?:\/\//, '')}`, + // Not "Guilds on ": `ctx.site` carries a base URL and no brand name, + // so naming the deployment here can only mean printing its hostname into + // an embed title, which is noise on a shard's own Discord server. + title: 'Guilds on this shard', fields: top.map((g) => ({ name: g.abbr ? `${g.name} [${g.abbr}]` : g.name, value: `${g.members || 0} members · ${g.online || 0} online`, diff --git a/server/test/guildCommand.test.js b/server/test/guildCommand.test.js index af33659..7782632 100644 --- a/server/test/guildCommand.test.js +++ b/server/test/guildCommand.test.js @@ -88,6 +88,14 @@ test('an unlinked caller is invited to link — but only when linking would chan stub({ audience: 'anonymous', level: 'anonymous' }) const open = await command.handler({ options: {}, actor: anonymous }) assert.equal(open.notice, null) + + // Gated to staff: linking reaches `player` and stops there, so the invitation + // would be an instruction to do something that changes nothing. Found on the + // live rig, where a staff-gated shard still offered it. + stub({ audience: 'staff', level: 'anonymous' }) + const unreachable = await command.handler({ options: {}, actor: anonymous }) + assert.match(unreachable.text, /not shown to your account/) + assert.equal(unreachable.notice, null) }) test('a stale board answers offline rather than reporting what it still holds', async () => { @@ -99,6 +107,7 @@ test('a stale board answers offline rather than reporting what it still holds', test('no argument lists the largest guilds', async () => { stub() const res = await command.handler({ options: {}, actor: anonymous }) + assert.equal(res.title, 'Guilds on this shard') assert.equal(res.fields.length, 2) assert.match(res.fields[0].name, /Knights of the Codex/) assert.match(res.fields[0].value, /12 members · 3 online/)