// `module.json` is what core validates before it will load anything, and every // rule it is checked against lives in core's loader (MODULE_API.md §2.1, §4.3). // Restating those rules here means a manifest mistake fails in this repo's CI, // which can say what is wrong, rather than on an install, where the symptom is a // module that is simply absent. // // These are the loader's own patterns, copied deliberately rather than imported: // this repo has no dependency on core's source, and a copy that drifts is // exactly what the coreApi range exists to catch. const test = require('node:test') const assert = require('node:assert') const fs = require('node:fs') const path = require('node:path') const ROOT = path.resolve(__dirname, '..', '..') const manifest = JSON.parse(fs.readFileSync(path.join(ROOT, 'module.json'), 'utf8')) const KEYS = new Set([ 'id', 'name', 'version', 'coreApi', 'server', 'client', 'schema', 'purge', 'mounts', 'extensions', 'capabilities', ]) const ID = /^[a-z][a-z0-9-]{1,31}$/ const PREFIX = /^\/[a-z0-9][a-z0-9-]*$/ const TIERS = ['public', 'admin', 'player'] test('declares no key core would reject', () => { for (const key of Object.keys(manifest)) { assert.ok(KEYS.has(key), `unknown key "${key}" — core rejects rather than ignores it`) } }) test('id is "uo" and matches the directory it installs as', () => { assert.ok(ID.test(manifest.id)) assert.strictEqual(manifest.id, 'uo') }) test('version and coreApi are present and semver-shaped', () => { assert.match(manifest.version, /^\d+\.\d+\.\d+/) assert.match(manifest.coreApi, /^[\^~]?\d+\.\d+\.\d+/) }) test('every declared file exists', () => { for (const key of ['server', 'schema', 'purge']) { if (manifest[key]) { assert.ok(fs.existsSync(path.join(ROOT, manifest[key])), `${key}: ${manifest[key]} is missing`) } } }) test('a schema fragment always comes with a purge', () => { // Core enforces this too. A module that can create tables and cannot drop them // leaves an operator with orphaned data and no supported way to remove it. if (manifest.schema) assert.ok(manifest.purge, 'declares schema but no purge') }) test('client.entry is in a subdirectory, because its directory is what gets served', () => { assert.ok(manifest.client, 'module-uo ships a client half') const entry = manifest.client.entry assert.match(path.basename(entry), /^[A-Za-z0-9][A-Za-z0-9._-]*\.js$/) // The rule worth a test of its own: core serves `dirname(entry)`, so an entry // in the module root would publish the server source and module.json. assert.notStrictEqual(path.dirname(path.resolve(ROOT, entry)), ROOT) assert.ok(path.resolve(ROOT, entry).startsWith(ROOT + path.sep), 'entry escapes the module root') }) test('declared mounts are single lowercase segments in known tiers', () => { for (const [tier, prefixes] of Object.entries(manifest.mounts || {})) { assert.ok(TIERS.includes(tier), `unknown tier "${tier}"`) for (const prefix of prefixes) assert.match(prefix, PREFIX) } }) test('notification stream ids and announce legs stay namespaced or grandfathered', () => { // Nothing to check yet — slice 0 registers neither. The assertion that matters // is that the manifest does not quietly claim capabilities the module does not // serve, since `GET /api/v1/public/modules` publishes them to clients. assert.deepStrictEqual(manifest.capabilities || [], []) assert.deepStrictEqual(manifest.mounts || {}, {}) assert.deepStrictEqual(manifest.extensions || [], []) })