Files
Module-uo/server/test/entry.test.js
wtclaude 6b99d7e220
All checks were successful
PR Checks / client-build (pull_request) Successful in 17s
PR Checks / server-tests (pull_request) Successful in 8m47s
test(server): port core's UO suite onto the ctx harness
22 test files moved from core, plus the two that were split out of files core
keeps. 351 tests pass.

One change runs through every moved test, and it is the boundary rather than a
chore: core internals can no longer be stubbed by requiring them, because there
are none to require. `../utils/db` and `../model/settings` do not exist here.
What a test controls instead is the ctx core would have handed over, installed
once by test/_setup.js -- which is a better seam anyway, since it is exactly the
surface the contract promises and nothing wider.

The ctx _setup installs is deliberately unfrozen. Core freezes what it hands a
module and entry.test.js still asserts against a frozen one; but a test that
needs settings.get to return a path has to be able to say so.

Two tests changed SHAPE, and that is the boundary too. fromShardEvent used to
assert through publish() into pushDevices and a captured fetch -- which
endpoints were hit, how many requests went out. None of that is this module's
any more: publish is ctx.push.publish, and the device registry and the relay are
behind it. Reaching for them from here would be reaching past ctx. What remains
is what the module owns and is the part worth guarding: a game account resolves
to a website user, a personal target that resolves to nobody is dropped rather
than published, and a sensitive kind never reaches publish at all.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-11 12:07:15 -05:00

117 lines
5.1 KiB
JavaScript

// The entry point's contract with core (MODULE_API.md §2.2).
//
// Slice 0 registers nothing, so there is very little behaviour to assert — and
// the rules that DO apply are the ones that would otherwise be discovered on an
// operator's install: registering synchronously, never awaiting, never touching
// a database, never mutating what it was handed. Those hold for every slice
// after this one too, which is why they are tested against the entry point
// rather than against whatever it happens to register today.
const test = require('node:test')
const assert = require('node:assert')
const register = require('../index')
const { fakeCtx, fakeApi } = require('./_fakes')
test('exports a single register function', () => {
assert.strictEqual(typeof register, 'function')
})
test('registers synchronously and returns nothing to await', () => {
const result = register(fakeCtx(), fakeApi())
// Not `assert.strictEqual(result, undefined)` alone: a module that returned a
// promise would be a module whose registration core silently never waits for.
assert.ok(!result || typeof result.then !== 'function', 'register() must not return a thenable')
})
test('touches no database at registration time', () => {
const ctx = fakeCtx()
register(ctx, fakeApi())
assert.deepStrictEqual(ctx.db.query.calls, [], 'register() queried the database')
})
test('registers exactly what module.json declares', () => {
// The loader compares these two in BOTH directions and rejects a mismatch
// either way, so a prefix registered without being declared and a prefix
// declared without being registered are both module-breaking. Asserting
// against the manifest rather than a literal list means the test cannot drift
// from the file core actually reads.
const api = fakeApi()
register(fakeCtx(), api)
const manifest = require('../../module.json')
for (const tier of ['public', 'admin', 'player']) {
assert.deepStrictEqual(
Object.keys(api.record.routes[tier]).sort(),
[...manifest.mounts[tier]].sort(),
`${tier} mounts disagree with module.json`,
)
for (const router of Object.values(api.record.routes[tier])) {
assert.strictEqual(typeof router, 'function', `${tier} router is not a router`)
}
}
assert.deepStrictEqual(api.record.extensions.map((e) => e.slot), manifest.extensions)
assert.deepStrictEqual(api.record.legs.map((l) => l.leg), ['towncrier'])
assert.ok(api.record.streams.length > 0)
assert.strictEqual(typeof api.record.hooks.onBoot, 'function')
assert.strictEqual(typeof api.record.hooks.onShutdown, 'function')
})
test('every registered stream is namespaced or grandfathered', () => {
// Core rejects a stream id that carries neither this module's prefix nor a
// §6.5 grandfathered name. The seven legacy ids are stored in
// `notification_subs` and read by a shipped Android client, so they are
// allowlisted rather than renamed — but a NEW id must be namespaced, and this
// is where that is caught before an install refuses to load the module.
// Copied from core's loader (LEGACY_STREAM_IDS), deliberately rather than
// imported — this repo has no dependency on core's source, and a copy that
// drifts is caught by the module failing to load, which is the failure this
// test exists to move earlier.
const GRANDFATHERED = new Set([
'server.status', 'idoc.warning', 'champ.start', 'governor.election',
'vendor.sale', 'house.idoc', 'account.login',
])
const api = fakeApi()
register(fakeCtx(), api)
for (const s of api.record.streams) {
assert.ok(
s.id.startsWith('uo.') || GRANDFATHERED.has(s.id),
`stream "${s.id}" is neither namespaced "uo." nor grandfathered`,
)
assert.ok(s.label && s.description, `stream "${s.id}" is missing its wire shape`)
assert.strictEqual(typeof s.personal, 'boolean')
assert.strictEqual(typeof s.requiresLinkedAccount, 'boolean')
}
})
test('takes a frozen ctx and does not try to write to it', () => {
const ctx = fakeCtx()
assert.ok(Object.isFrozen(ctx))
// Core freezes one level deep; a module that assigned to ctx would throw here
// in strict mode and fail silently outside it. Either way it must not.
assert.doesNotThrow(() => register(ctx, fakeApi()))
})
test('logs through ctx.log, never through console', () => {
const ctx = fakeCtx()
register(ctx, fakeApi())
assert.strictEqual(ctx.logs.length, 1, 'expected exactly one logger to be taken')
const { log } = ctx.logs[0]
assert.strictEqual(log.info.calls.length, 1)
assert.strictEqual(log.info.calls[0][0], 'registered')
})
test('carries no hidden state between calls', () => {
// Core calls register() exactly once, and the `once()` guard that enforces
// that lives in core's `api` — not here. What this asserts is the module's
// own half of it: registering into a second `api` produces the same result as
// the first, so nothing is memoised at file scope where a re-register would
// silently do less than it appears to.
const first = fakeApi()
const second = fakeApi()
register(fakeCtx(), first)
register(fakeCtx(), second)
assert.deepStrictEqual(second.record, first.record)
})