// ── The slash commands (phase 16) ───────────────────────────────────────── // // The handlers against stubbed models. What matters most is D127: **an answer // is public only when everything in it is public.** Every refusal is private, // and a permitted answer narrower than `public` goes to the caller alone — the // case core cannot catch, because an answer without the flag is posted to the // channel. const test = require('node:test') const assert = require('node:assert') const { fakeCtx, fakeApi } = require('./_fakes') // Accounts as `core.users.getById` answers them. The role on the ACTOR is never // trusted — the row is re-read — so a test gives the actor one role and the row // another to prove it. const USERS = { 1: { id: 1, role: 'user', status: 'active' }, 2: { id: 2, role: 'moderator', status: 'active' }, 3: { id: 3, role: 'moderator', status: 'banned' }, } function withCore() { const ctx = fakeCtx({ users: { getById: async (id) => USERS[id] || null } }) require('../core')._reset() require('../core').init(ctx) return ctx } const actor = (userId, role = 'user') => ({ platform: 'discord', platformUserId: `d${userId || 0}`, guildId: 'g1', userId: userId || null, role: userId ? role : null, isLinked: Boolean(userId), isStaff: role === 'admin' || role === 'moderator', }) const STRANGER = actor(null) const PLAYER = actor(1) const MOD = actor(2, 'moderator') const server = (over = {}) => ({ id: 'main', name: 'Main', online: true, players: 3, maxPlayers: 100, worldSize: 4000, seed: 1234, wipeId: 'w-2026-09', wipedAt: '2026-09-03T18:00:00Z', lastSeenAt: '2026-09-25T11:59:00Z', stale: false, nextWipe: { at: '2026-10-01T18:00:00.000Z', source: 'forced' }, ...over, }) /** Swap model functions for one test, and put them back whatever happens. */ async function stubbed(stubs, body) { const saved = [] for (const [mod, name, fn] of stubs) { saved.push([mod, name, mod[name]]) mod[name] = fn } try { return await body() } finally { for (const [mod, name, fn] of saved) mod[name] = fn } } function models() { return { servers: require('../model/servers/servers.model'), events: require('../model/events/events.model'), visibility: require('../model/visibility/visibility.model'), clans: require('../model/clans/clans.model'), visibilityDb: require('../model/visibility/visibility.db'), } } const command = (name) => require('../commands').find((cmd) => cmd.name === name) /** Everything an answer would put in front of a reader, as one string. */ const said = (env) => JSON.stringify([env.title, env.text, env.fields, env.notice]) // ── Registration ────────────────────────────────────────────────────────── test('the entry point registers the five commands as one batch', () => { withCore() const api = fakeApi() require('../index')(fakeCtx(), api) assert.deepStrictEqual(api.record.slashCommands.map((cmd) => cmd.name), ['status', 'wipe', 'top', 'online', 'clan']) }) test('every definition is one core will accept, and one Discord will', () => { // Core's `checkSlashCommandShape` rules, restated: a definition it refuses // fails the module at register(), and one Discord refuses takes every command // in the batch down with it, the bot's own included. const BUILT_INS = ['announce', 'autorole', 'ban', 'filter', 'filterallow', 'invite', 'kick', 'modlog', 'mute', 'news', 'ping', 'role', 'rolemenu', 'roles', 'schedule', 'warn', 'warnings', 'wiki'] for (const cmd of require('../commands')) { assert.match(cmd.name, /^[a-z0-9_-]{1,32}$/) assert.ok(!BUILT_INS.includes(cmd.name), `${cmd.name} collides with a bot built-in`) assert.ok(cmd.description.length >= 1 && cmd.description.length <= 100, `${cmd.name}: description length`) assert.strictEqual(cmd.access, 'everyone') assert.strictEqual(typeof cmd.handler, 'function') let optionalSeen = false for (const o of cmd.options) { assert.match(o.name, /^[a-z0-9_-]{1,32}$/) assert.ok(['string', 'integer', 'boolean', 'user'].includes(o.type)) assert.ok(o.description.length >= 1 && o.description.length <= 100, `${cmd.name}.${o.name}: description length`) if (o.choices) assert.ok(['string', 'integer'].includes(o.type)) if (!o.required) optionalSeen = true else assert.ok(!optionalSeen, `${cmd.name}: a required option after an optional one`) } } }) // ── Picking a server ────────────────────────────────────────────────────── test('a server is named by id, then name, then a unique prefix — never guessed', () => { withCore() const { pickServer } = require('../commands/common') const list = [server(), server({ id: 'eu-2x', name: 'EU 2x' }), server({ id: 'eu-5x', name: 'EU 5x' })] assert.strictEqual(pickServer(list, 'MAIN').server.id, 'main') assert.strictEqual(pickServer(list, 'eu 2x').server.id, 'eu-2x') assert.strictEqual(pickServer(list, 'eu-5').server.id, 'eu-5x') assert.strictEqual(pickServer(list, 'eu').ambiguous.length, 2) assert.strictEqual(pickServer(list, 'us').missing, 'us') assert.strictEqual(pickServer(list, undefined).all.length, 3) assert.strictEqual(pickServer([server()], undefined).server.id, 'main', 'a fleet of one is that server') assert.ok(pickServer([], 'main').none) }) // ── Every refusal is private ────────────────────────────────────────────── test('every refusal is ephemeral: unknown, ambiguous, no servers, /top on a fleet', async () => { withCore() const { servers } = models() const two = [server(), server({ id: 'main-2', name: 'Main 2' })] await stubbed([[servers, 'listPublic', async () => two]], async () => { for (const [name, options] of [ ['status', { server: 'nope' }], ['wipe', { server: 'mai' }], ['online', { server: 'nope' }], ['top', {}], ['top', { server: 'm' }], ['clan', { name: '' }], ]) { // eslint-disable-next-line no-await-in-loop const env = await command(name).handler({ command: name, options, actor: STRANGER }) assert.strictEqual(env.ephemeral, true, `/${name} ${JSON.stringify(options)} refused in public: ${env.text}`) } }) await stubbed([[servers, 'listPublic', async () => []]], async () => { const env = await command('status').handler({ options: {}, actor: STRANGER }) assert.strictEqual(env.ephemeral, true) assert.match(env.text, /No Rust servers/) }) }) // ── /status and /wipe ───────────────────────────────────────────────────── test('/status answers in public, with the next wipe, and says when an offline server was last seen', async () => { withCore() const { servers } = models() await stubbed([[servers, 'listPublic', async () => [server(), server({ id: 'b', name: 'B', online: false, players: 0 })]]], async () => { const one = await command('status').handler({ options: { server: 'main' }, actor: STRANGER }) assert.ok(!one.ephemeral) assert.strictEqual(one.url, 'http://localhost:5173/rust/servers/main') assert.match(said(one), /3\/100/) assert.match(said(one), /Next wipe.*Thu 1 Oct, 18:00 UTC.*monthly forced wipe/) const down = await command('status').handler({ options: { server: 'b' }, actor: STRANGER }) assert.match(said(down), /Offline/) assert.match(said(down), /Last seen/) const fleet = await command('status').handler({ options: {}, actor: STRANGER }) assert.ok(!fleet.ephemeral) assert.strictEqual(fleet.fields.length, 2) }) }) test('/wipe says "no schedule set" rather than inventing a forecast', async () => { withCore() const { servers } = models() await stubbed([[servers, 'listPublic', async () => [server({ nextWipe: null })]]], async () => { const env = await command('wipe').handler({ options: {}, actor: STRANGER }) assert.ok(!env.ephemeral) assert.match(env.text, /Next: no schedule set/) assert.match(env.text, /Last: Thu 3 Sept?, 18:00 UTC/) }) await stubbed([[servers, 'listPublic', async () => [server({ nextWipe: { at: '2026-10-03T17:00:00.000Z', source: 'once' } })]]], async () => { const env = await command('wipe').handler({ options: {}, actor: STRANGER }) assert.match(env.text, /rescheduled by the operator/) }) }) // ── /top ────────────────────────────────────────────────────────────────── test('/top reads the current wipe, never asks for presence, and prints no Steam id', async () => { withCore() const { servers, events } = models() const asked = [] const rows = [{ steamId: '76561198000000001', name: 'Alice', kills: 9, deaths: 1, npcKills: 0, playtimeSec: 7260, lastSeen: 'x' }] await stubbed( [ [servers, 'listPublic', async () => [server()]], [events, 'leaderboard', async (q) => { asked.push(q); return rows }], ], async () => { const env = await command('top').handler({ options: {}, actor: MOD }) assert.ok(!env.ephemeral, 'the leaderboard names are public at every setting') assert.deepStrictEqual(asked[0], { serverId: 'main', wipeId: 'w-2026-09', sort: 'kills', limit: 10, presence: false }) assert.match(env.text, /1\. Alice — 9/) assert.ok(!said(env).includes('76561198000000001')) const all = await command('top').handler({ options: { stat: 'playtime', alltime: true }, actor: STRANGER }) assert.strictEqual(asked[1].wipeId, null) assert.strictEqual(asked[1].sort, 'playtime') assert.match(all.text, /2h 1m/) assert.match(all.title, /all time/) }, ) }) // ── /online — D127 ──────────────────────────────────────────────────────── async function online(audience, who, over = {}) { const { servers, events, visibility } = models() return stubbed( [ [servers, 'listPublic', async () => [server(over)]], [visibility, 'presenceFor', async () => audience], [events, 'online', async () => [{ name: 'Alice', sleeping: false }, { name: 'Bob', sleeping: true }]], ], () => command('online').handler({ options: {}, actor: who }), ) } test('/online below the audience: the count, in public, and no names', async () => { withCore() const env = await online('staff', STRANGER) assert.ok(!env.ephemeral) assert.match(env.title, /3 online/) assert.ok(!said(env).includes('Alice')) // Staff is not something linking earns, so there is no nudge to link. assert.ok(!env.notice) }) test('/online inside a narrower audience: the names, to the caller ALONE', async () => { withCore() const env = await online('staff', MOD) assert.strictEqual(env.ephemeral, true, 'a moderator’s roll call was posted to the channel') assert.match(env.text, /Alice/) assert.match(env.text, /Bob \(sleeping\)/) const signedIn = await online('signed_in', PLAYER) assert.strictEqual(signedIn.ephemeral, true) }) test('/online with a public audience posts the names in the open', async () => { withCore() const env = await online('public', STRANGER) assert.ok(!env.ephemeral) assert.match(env.text, /Alice/) }) test('/online nudges an unlinked caller to link only when linking would reach the names', async () => { withCore() const env = await online('signed_in', STRANGER) assert.ok(!env.ephemeral) assert.match(env.notice, /Link your Discord account/) }) test('/online judges the ACCOUNT, not the actor: a banned moderator is nobody', async () => { withCore() const env = await online('staff', actor(3, 'moderator')) assert.ok(!said(env).includes('Alice')) }) test('/online for an offline server names nobody, whatever the audience', async () => { withCore() const env = await online('public', MOD, { online: false, players: 0 }) assert.match(env.text, /offline/) assert.ok(!said(env).includes('Alice')) }) // ── /clan — D127 with D48 ───────────────────────────────────────────────── const CLAN = { externalId: 'main:7:1', name: 'Wolves', color: '#aa3300', score: 50, memberCount: 2, maxMembers: 8 } async function clan(rosterAudience, who, options = { name: 'wolves' }, board = { supported: true, fresh: true }) { const { servers, clans, visibilityDb, visibility } = models() return stubbed( [ [servers, 'listPublic', async () => [server()]], [clans, 'listForServer', async () => ({ clans: [CLAN, { ...CLAN, externalId: 'main:8:1', name: 'Wolverines' }], board })], [visibilityDb, 'getSetting', async (key) => (key === visibility.CLAN_ROSTER_KEY ? rosterAudience : null)], [require('../model/clans/clans.db'), 'findClan', async () => ({ ...CLAN, serverId: 'main', serverName: 'Main', createdMs: 1 })], [require('../model/clans/clans.db'), 'userIsMember', async (_ext, userId) => userId === 1], [require('../model/clans/clans.db'), 'listMembers', async () => [ { name: 'Alice', rank: 1, online: 1 }, { name: 'Bob', rank: 2, online: 0 }, ]], ], () => command('clan').handler({ options, actor: who }), ) } test('/clan outside the roster audience: the public facts, in public, no names', async () => { withCore() const env = await clan('members', STRANGER) assert.ok(!env.ephemeral) assert.strictEqual(env.title, 'Wolves') assert.match(said(env), /50/) assert.ok(!said(env).includes('Alice')) }) test('/clan inside a narrower roster audience: the roster, to the caller ALONE', async () => { withCore() const member = await clan('members', PLAYER) assert.strictEqual(member.ephemeral, true, 'a clan roster was posted to the channel') assert.match(said(member), /Alice \(leader, online\)/) const mod = await clan('members', MOD) assert.strictEqual(mod.ephemeral, true) }) test('/clan with a public roster audience posts the roster in the open', async () => { withCore() const env = await clan('public', STRANGER) assert.ok(!env.ephemeral) assert.match(said(env), /Alice/) }) test('/clan: an exact name wins over a prefix, and a shared prefix is ambiguous and private', async () => { withCore() assert.strictEqual((await clan('members', STRANGER, { name: 'WOLVES' })).title, 'Wolves') const env = await clan('members', STRANGER, { name: 'wol' }) assert.strictEqual(env.ephemeral, true) assert.match(env.text, /Wolves, Wolverines/) }) test('/clan does not say "no such clan" from a board it cannot trust', async () => { withCore() const env = await clan('members', STRANGER, { name: 'bears' }, { supported: true, fresh: false }) assert.strictEqual(env.ephemeral, true) assert.match(env.text, /not available, so it may be there/) const sure = await clan('members', STRANGER, { name: 'bears' }) assert.match(sure.text, /^No clan called “bears”\.$/) }) // ── Words ───────────────────────────────────────────────────────────────── test('times are written in UTC with the distance beside them', () => { withCore() const { when, relative } = require('../commands/common') const now = Date.parse('2026-09-25T12:00:00Z') assert.strictEqual(when('2026-10-01T18:00:00Z', now), 'Thu 1 Oct, 18:00 UTC (in 6 days)') assert.strictEqual(relative(now - 3 * 3_600_000, now), '3 hours ago') assert.strictEqual(relative(now + 90_000, now), 'in 2 minutes') assert.strictEqual(when(null, now), null) })