// ── The registration handshake ──────────────────────────────────────────── // // The one suite every module should have, whatever else it does. Core validates // all of this at boot and refuses to mount a module that fails — so testing it // here is the difference between finding out in half a second and finding out on // an operator's install. const test = require('node:test') const assert = require('node:assert') const { fakeCtx, fakeApi } = require('./_fakes') const manifest = require('../../module.json') /** A fresh registration. `core.js` holds a module-level `ctx`, so reset it. */ function register(ctx = fakeCtx()) { require('../core')._reset() const api = fakeApi() require('../index')(ctx, api) return { api, ctx } } test('registers exactly the mounts module.json declares', () => { const { api } = register() // Core compares these two and rejects a mismatch in EITHER direction: a prefix // declared and never registered is as fatal as a route registered and never // declared. Asserting against the manifest rather than against a literal is // what keeps the test true after a prefix is added. assert.deepStrictEqual( Object.keys(api.record.routes).sort(), Object.keys(manifest.mounts).sort(), ) for (const [tier, prefixes] of Object.entries(manifest.mounts)) { assert.deepStrictEqual(Object.keys(api.record.routes[tier]).sort(), [...prefixes].sort()) } }) test('all three tiers are mounted (R14)', () => { const { api } = register() // Not the assertion above restated. That one says the manifest and the code // agree; this one says WHICH answer they agree on, so that deleting a tier from // both halves at once still fails. R14 puts this module on all three from the // start precisely so that a later phase adding a player surface does not have // to move an address clients are already calling. assert.deepStrictEqual(Object.keys(api.record.routes).sort(), ['admin', 'player', 'public']) for (const tier of ['admin', 'player', 'public']) { assert.deepStrictEqual(Object.keys(api.record.routes[tier]), ['/rust']) } }) test('every registered mount is a real express router', () => { const { api } = register() for (const byPrefix of Object.values(api.record.routes)) { for (const [prefix, router] of Object.entries(byPrefix)) { assert.strictEqual(typeof router, 'function', `${prefix} is not a router`) assert.ok(router.stack, `${prefix} has no middleware stack`) } } }) test('prefixes are one segment, lowercase, no parameters', () => { // §2.4's rule, restated where a typo is cheap to find. Core enforces it, and a // module that fails it does not mount at all. for (const prefixes of Object.values(manifest.mounts)) { for (const prefix of prefixes) { assert.match(prefix, /^\/[a-z0-9][a-z0-9-]*$/, `illegal mount prefix ${prefix}`) } } }) test('registration touches no database and awaits nothing', () => { const ctx = fakeCtx() register(ctx) // §2.2's first rule. Core requires `app.js` with the pool pointed at a dead // port in two build tools, so a query here would hang both — and the symptom is // a build that never finishes rather than an error naming this module. assert.deepStrictEqual(ctx.db.query.calls, []) }) test('registers both lifecycle hooks', () => { const { api } = register() assert.strictEqual(typeof api.record.hooks.onBoot, 'function') assert.strictEqual(typeof api.record.hooks.onShutdown, 'function') }) test('the manifest declares what the loader requires', () => { assert.match(manifest.id, /^[a-z][a-z0-9-]{1,31}$/) assert.match(manifest.version, /^\d+\.\d+\.\d+/) assert.ok(manifest.coreApi, 'coreApi is required — it is the version check') // Declaring a schema without a purge is refused: a module that can create // tables and cannot drop them leaves an operator with orphaned data. if (manifest.schema) assert.ok(manifest.purge, 'a schema fragment requires a purge file') // The chunk must be in a SUBDIRECTORY — the directory it sits in is what core // serves, so an entry in the module root would publish the whole module. if (manifest.client) assert.ok(manifest.client.entry.includes('/'), 'client.entry must be in a subdirectory') }) test('the manifest declares no extension slot it does not fill', () => { const { api } = register() // §11.3 of the plan reads `extensions` as "declared, and held against reality // by the loader". Only the first half is true: the loader checks that a named // slot EXISTS (`registries.hasSlot`) and never checks that the module went on // to fill it — `checkDeclared` covers `mounts` alone. So a declaration with // nothing behind it loads cleanly and means nothing, which is exactly why this // module does not write one until it has an extension to register. // // The other half of that correction: `admin.users.detail` is the ONLY server // slot core declares. `site.footer.status` is a CLIENT slot and is registered // from the chunk — naming it here would fail the load with // `unknown extension slot "site.footer.status"`. const declared = manifest.extensions || [] const filled = api.record.extensions.map((e) => e.slot) assert.deepStrictEqual([...declared].sort(), [...filled].sort()) }) test('nothing is registered that has nothing behind it yet', () => { const { api } = register() // The phase-1 statement, written down so that removing it is deliberate. A // declared trigger nothing emits and a declared slot nothing fills are both // surfaces an operator can configure and then wait on — worse than an absent // one, because the absence is visible. Each of these arrives with the phase // that has something real to put in it, and this assertion is what that phase // deletes. assert.strictEqual(api.record.teamProvider, null) assert.strictEqual(api.record.triggers, null) assert.strictEqual(api.record.audiences, null) assert.strictEqual(api.record.engagementSeeds, null) assert.strictEqual(api.record.streams, null) assert.strictEqual(api.record.eventBudgets, null) assert.strictEqual(api.record.eventOptionSources, null) assert.strictEqual(api.record.eventLeases, null) assert.strictEqual(api.record.eventActions, null) }) test('the module’s protocol version agrees with the manifest it ships beside', () => { const sidecar = require('../sidecarClient') // The wire version is declared in three repos — here, `PROTOCOL_VERSION` in // the sidecar, and `overlay.toml` in the plugin overlay — and nothing in one // repo can check the other two. What CAN be checked is that this repo says one // thing: the number the client sends is the number an operator sees on a // freshly created server row, so a bump that edits one and not the other // configures every new server against a version the client does not speak. assert.strictEqual(typeof sidecar.PROTOCOL_VERSION, 'number') assert.ok(sidecar.PROTOCOL_VERSION >= 1) })