Files
Module-uo/server/test/_fakes.js
Claude 2d1d91e372 feat(guilds): /guild, the module's own chat command
The first command through `api.registerSlashCommands` (MODULE_API 1.6.0,
TEAMS.md §7.1). The definition and the handler both live here; the bot pulls the
definition and runs no line of this module.

`/guild` and not `/team`, deliberately. Core does not own the word for a Team —
that is what deleted its Team pages in phase 3 — so it does not publish the noun
in a channel either. Core ships the dispatcher and zero commands.

The audience rungs are re-resolved in the handler rather than assumed: a shard
that gates guilds to staff does not become public because the question arrived
over Discord. The provider's own staleness guard is honoured too, so a stale
board answers "not connected" instead of reporting what it still holds, and
`resolveUserId` is exported rather than copied so "linked" means here what it
means on the roster.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-18 18:53:45 -05:00

128 lines
5.4 KiB
JavaScript

// Test doubles for what core hands the module.
//
// The module's server half is testable WITHOUT core, and that is not a
// convenience — it is the contract holding. Everything the module may touch
// arrives on `ctx` (MODULE_API.md §2.3), so a `ctx` this file can build is a
// complete statement of the module's dependencies. If a test ever needs
// something that is not here, either the module reached past the boundary or
// §2.3 needs a member; both are worth stopping for.
//
// `fakeCtx` mirrors §2.3 member for member, including the freezing, so a module
// that assigns to `ctx.something` fails here the way it would in core.
const express = require('express')
/** Records every call, so a test can assert what a module asked for. */
function spy(returns) {
const fn = (...args) => {
fn.calls.push(args)
return typeof returns === 'function' ? returns(...args) : returns
}
fn.calls = []
return fn
}
function fakeLog() {
const log = { error: spy(), warn: spy(), info: spy(), debug: spy() }
return log
}
function fakeCtx(overrides = {}) {
// `freeze: false` is for _setup.js, which installs one process-wide ctx a test
// may adjust. Core always freezes; the unfrozen variant is a test seam and
// never a claim about what a module is handed in production.
const { freeze = true, ...rest } = overrides
const logs = []
const ctx = {
moduleId: 'uo',
paths: { moduleRoot: require('path').resolve(__dirname, '..', '..') },
express,
validator: require('express-validator'),
db: { query: spy(Promise.resolve([])), pool: {} },
log: (namespace) => {
const log = fakeLog()
logs.push({ namespace, log })
return log
},
settings: { get: spy(Promise.resolve(null)), set: spy(Promise.resolve()), getInstanceName: spy(Promise.resolve('Test')) },
auth: { getUserFromRequest: spy(null) },
push: { publish: spy(Promise.resolve()) },
secretBox: { encrypt: spy('enc'), decrypt: spy('dec') },
middleware: {
requireAuth: (req, res, next) => next(),
requireRole: () => (req, res, next) => next(),
siteMode: (req, res, next) => next(),
validate: (req, res, next) => next(),
noindex: (req, res, next) => next(),
// API 1.1.0. The factory returns a pass-through rather than a real
// limiter: a test that tripped a rate limit would be a test whose result
// depended on how many times the suite had run.
rateLimit: (options) => Object.assign((req, res, next) => next(), { options }),
accountChangeLimiter: (req, res, next) => next(),
},
uploads: { upload: {}, UPLOAD_DIR: '/tmp', MIME_EXT: {} },
posts: { listAll: spy(Promise.resolve([])), getById: spy(Promise.resolve(null)), linkAnnounceJob: spy(Promise.resolve()), markAnnounced: spy(Promise.resolve()) },
// The three §2.3 members API 1.1.0 added for this extraction.
activity: { log: spy(Promise.resolve()) },
users: { getById: spy(Promise.resolve(null)) },
site: { baseUrl: 'http://localhost:5173' },
...rest,
}
// Non-enumerable, and that is not tidiness. Core freezes every object value on
// `ctx` one level deep, so an enumerable recorder hung off it would be frozen
// by the loop below and every `log.info` call would throw on push — which is
// how this was found. Keeping it off the enumeration also makes the fake more
// faithful: a module iterating `ctx` sees exactly §2.3's members and nothing
// a test put there.
Object.defineProperty(ctx, 'logs', { value: logs, enumerable: false })
if (!freeze) return ctx
for (const value of Object.values(ctx)) {
if (value && typeof value === 'object') Object.freeze(value)
}
return Object.freeze(ctx)
}
/**
* The registration api, recording rather than mounting.
*
* Copies core's `once()` rule (§2.4: "calling twice is an error") because a
* module that registers the same thing twice must fail in its own test suite
* and not first on an operator's install.
*/
function fakeApi() {
const record = {
routes: null,
extensions: [],
streams: null,
legs: [],
teamProvider: null,
slashCommands: [],
hooks: {},
}
const called = new Set()
const once = (name) => {
if (called.has(name)) throw new Error(`${name}() called twice`)
called.add(name)
}
const api = {
registerRoutes(mounts) { once('registerRoutes'); record.routes = mounts },
registerExtension(slot, router) { record.extensions.push({ slot, router }) },
registerNotificationStreams(streams) { once('registerNotificationStreams'); record.streams = streams },
registerAnnounceLeg(leg) { record.legs.push(leg) },
// MODULE_API 1.6.0. `once` because core holds a single provider per
// deployment — a second registration is a collision there, so it has to be
// one here too, or this suite would pass a shape core rejects at load.
registerTeamProvider(provider) { once('registerTeamProvider'); record.teamProvider = provider },
// MODULE_API 1.6.0, live since phase 7. `once` for the same reason core
// takes it: a second call is a module changing its mind halfway through
// register(), which core rejects.
registerSlashCommands(commands) { once('registerSlashCommands'); record.slashCommands = commands },
onBoot(fn) { once('onBoot'); record.hooks.onBoot = fn },
onShutdown(fn) { once('onShutdown'); record.hooks.onShutdown = fn },
}
api.record = record
return api
}
module.exports = { fakeCtx, fakeApi, spy }