// ── 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 it against the manifest rather than against a literal is // what keeps the test true after you add a prefix. 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('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') })