// `/guild` — the chat command registered through `api.registerSlashCommands` // (TEAMS.md §7.1, MODULE_API 1.6.0). // // The properties worth pinning are all about the ANSWER being the same answer // the website gives, because that is the whole risk of a second surface: the // audience rungs are re-resolved here rather than assumed, the shard's own // offline guard is honoured, and the link prompt appears only when linking would // actually change what the caller is told. const { test, afterEach } = require('node:test') const assert = require('node:assert/strict') const command = require('../commands/guild.command') const db = require('../model/teamProvider/teamProvider.db') const provider = require('../model/teamProvider/teamProvider.model') const visibility = require('../utils/shardVisibility') const originals = { getConfig: visibility.getConfig, viewerLevel: visibility.viewerLevel, boardIsCurrent: provider.boardIsCurrent, listGuilds: db.listGuilds, listGuildMembers: db.listGuildMembers, } afterEach(() => { visibility.getConfig = originals.getConfig visibility.viewerLevel = originals.viewerLevel provider.boardIsCurrent = originals.boardIsCurrent db.listGuilds = originals.listGuilds db.listGuildMembers = originals.listGuildMembers }) const GUILDS = [ { id: 7, name: 'Knights of the Codex', abbr: 'KOC', alliance: 'The Accord', members: 12, online: 3, leader_name: 'Dain' }, { id: 9, name: 'Knights Hospitaller', abbr: 'KH', alliance: null, members: 4, online: 0, leader_name: null }, ] const MEMBERS = [ { serial: 1, name: 'Dain', rank: 4, web_id: '31', linked_user_id: null }, { serial: 2, name: 'Elowen', rank: 4, web_id: null, linked_user_id: 44 }, { serial: 3, name: 'Wat', rank: 2, web_id: null, linked_user_id: null }, ] function stub({ audience = 'anonymous', enabled = true, level = 'anonymous', current = true } = {}) { visibility.getConfig = async () => ({ guilds: { enabled, audience } }) visibility.viewerLevel = async () => level provider.boardIsCurrent = async () => (current ? { ok: true } : { ok: false, reason: 'socket down' }) db.listGuilds = async () => GUILDS db.listGuildMembers = async () => MEMBERS } const anonymous = { platform: 'discord', platformUserId: '1', userId: null, role: null, isLinked: false, isStaff: false } const linked = { platform: 'discord', platformUserId: '2', userId: 31, role: 'player', isLinked: true, isStaff: false } test('the definition stays inside the option schema §7.1.1 allows', () => { assert.equal(command.name, 'guild') assert.equal(command.access, 'everyone') for (const option of command.options) { assert.ok(['string', 'integer', 'boolean', 'user'].includes(option.type)) assert.ok(option.description.length <= 100) } }) test('the guilds feature being off withholds everything, staff included', async () => { stub({ enabled: false, level: 'admin' }) const res = await command.handler({ options: {}, actor: { ...linked, role: 'admin', isStaff: true } }) assert.match(res.text, /does not publish guild information/) assert.equal(res.ephemeral, true) }) // The reason this command is not a thin wrapper over a public route: a rung // below the feature's audience must be refused HERE, or a shard that gates // guilds to staff would publish them to a Discord channel. test('a caller below the feature audience is refused', async () => { stub({ audience: 'staff', level: 'anonymous' }) const res = await command.handler({ options: {}, actor: anonymous }) assert.match(res.text, /not shown to your account/) assert.equal(res.ephemeral, true) }) test('an unlinked caller is invited to link — but only when linking would change the answer', async () => { stub({ audience: 'player', level: 'anonymous' }) const gated = await command.handler({ options: {}, actor: anonymous }) assert.match(gated.notice, /Link your account/) // Public guilds: there is nothing more to see, so there is nothing to prompt. stub({ audience: 'anonymous', level: 'anonymous' }) const open = await command.handler({ options: {}, actor: anonymous }) assert.equal(open.notice, null) }) test('a stale board answers offline rather than reporting what it still holds', async () => { stub({ current: false }) const res = await command.handler({ options: {}, actor: anonymous }) assert.match(res.text, /not connected right now/) }) test('no argument lists the largest guilds', async () => { stub() const res = await command.handler({ options: {}, actor: anonymous }) 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/) }) test('a name resolves by abbreviation, then exactly, then by unique prefix', async () => { stub() const byAbbr = await command.handler({ options: { name: 'koc' }, actor: anonymous }) assert.match(byAbbr.title, /Knights of the Codex/) const exact = await command.handler({ options: { name: 'Knights Hospitaller' }, actor: anonymous }) assert.match(exact.title, /Hospitaller/) // "knights" hits both, and answering with either would be worse than asking. const ambiguous = await command.handler({ options: { name: 'knights' }, actor: anonymous }) assert.match(ambiguous.text, /Several guilds match/) assert.equal(ambiguous.ephemeral, true) }) test('a miss is an answer, not a failure', async () => { stub() const res = await command.handler({ options: { name: 'nobody' }, actor: anonymous }) assert.match(res.text, /No guild matches/) }) // `linked` counts BOTH sources the roster uses — the shard's asserted web id and // the link table — because that is what "linked" means everywhere else here. test('the detail carries the counts, the leaders and a link to the module page', async () => { stub({ level: 'player' }) const res = await command.handler({ options: { name: 'KOC' }, actor: linked }) const field = (name) => res.fields.find((f) => f.name === name).value assert.equal(field('Members'), '12') assert.equal(field('Online'), '3') assert.equal(field('Linked accounts'), '2') assert.equal(field('Leaders'), 'Dain, Elowen') assert.match(res.url, /\/uo\/guilds\/7$/) assert.equal(res.notice, null) })