THROWAWAY BRANCH — evidence for the Phase 1 contract, never merged. See
modules/uo/SPIKE.md and docs/website/MODULE_API.md Part 7.
The six public spawn-atlas routes now live in modules/uo/, reached only through
the ctx/register surface, with the client half loading as a prebuilt ESM chunk.
All three exit criteria met:
• zero internal-file imports from the module into core; the built chunk has
zero bare import specifiers and bundles no React
• routes.manifest.json AND routes.guards.json are byte-identical
• /uo/atlas renders from /modules/uo/entry.js under script-src 'self' with
zero CSP violation reports
729 core tests and 81 module tests pass. Verified end to end against the real
database: the schema fragment replays after core's, onBoot runs the atlas
refresh, and the six API URLs answer unchanged.
Two things the spike changed in the contract:
• ctx.express / ctx.validator. A module lives outside server/, so Node never
reaches server/node_modules and require('express') fails outright — the
server-side twin of the one-React rule, which §2.6 had only for the client.
• window.__rg.jsxRuntime, so a module can build with the automatic JSX
runtime its tooling already assumes rather than being forced to classic.
And it confirmed §6.1 empirically: regenerating the OpenAPI spec silently
deleted all 361 lines of the atlas paths with "Swagger-autogen: Success", while
the route manifest kept all six in the same run. That is exactly the
static-analysis-vs-runtime split the fragment merge exists to prevent.
Co-Authored-By: Claude <noreply@anthropic.com>
249 lines
10 KiB
JavaScript
249 lines
10 KiB
JavaScript
// Point the DB at a closed port BEFORE requiring the controllers (their models
|
||
// build the pool). Every model call is monkeypatched, so no query runs;
|
||
// db.close() at the end releases the pool so the process exits cleanly.
|
||
process.env.DB_HOST = '127.0.0.1'
|
||
process.env.DB_PORT = '59999'
|
||
|
||
const { test, after, afterEach } = require('node:test')
|
||
const assert = require('node:assert/strict')
|
||
|
||
// The spawn atlas API, public and admin. What is worth asserting here is not the
|
||
// SQL (that is the parser suite's job) but the contracts the two surfaces make:
|
||
//
|
||
// • the public reads project through the visibility framework — v3.md §3.6.1's
|
||
// standing rule is that a read path returning shard data and not calling
|
||
// projectFeature is a bug, and `atlas` declaring no sensitive fields TODAY is
|
||
// exactly why the call has to be there before one does;
|
||
// • the public /meta route reports the game world only, never the operator's
|
||
// filesystem — the ServUO path, the per-file hashes and any pending refresh
|
||
// stay on the admin route;
|
||
// • a missing creature is a 404, not an empty 200;
|
||
// • an unreadable ServUO tree is a 200 carrying `status: 'unavailable'`, NOT a
|
||
// 500. The refresh contract reports outcomes rather than throwing (so boot is
|
||
// never blocked by a bad tree), and the admin needs to be told what is wrong
|
||
// with their path;
|
||
// • a model failure degrades to a 500 rather than a thrown/uncaught error.
|
||
// The module's files read core through their `core` shim, which register()
|
||
// normally fills. Nothing registers modules in a unit test, so install the
|
||
// module's own fake ctx first — before any of its files are required, since the
|
||
// controller resolves its logger at require time.
|
||
require('../../modules/uo/server/test/_ctx').installFakeCtx()
|
||
|
||
// SPIKE ARTIFACT (see admin/shardAtlas.controller.js): the public atlas
|
||
// controller and its model are module-uo's now. Phase 3 moves this test into the
|
||
// module alongside them; until the admin half moves too, one test file has to
|
||
// see both sides.
|
||
const pub = require('../../modules/uo/server/router/atlas.controller')
|
||
const admin = require('../src/router/v1/admin/shardAtlas.controller')
|
||
const atlas = require('../../modules/uo/server/model/shardAtlas/shardAtlas.model')
|
||
const activity = require('../src/model/activity/activity.model')
|
||
const visibility = require('../../modules/uo/server/utils/visibility')
|
||
const db = require('../src/utils/db')
|
||
|
||
after(() => db.close())
|
||
|
||
// Stub the visibility MODEL rather than the util's exports: project() calls the
|
||
// module-internal getConfig, which an exports-level stub would not intercept — it
|
||
// would hit the closed DB port and cost a ~10s pool timeout per test before
|
||
// falling back to these same defaults.
|
||
const visibilityModel = require('../../modules/uo/server/model/shardVisibility/shardVisibility.model')
|
||
visibilityModel.listAll = async () => [] // no overrides ⇒ compiled defaults
|
||
visibility.viewerLevel = async (req) => req?.viewerLevel || 'anonymous'
|
||
|
||
// The admin controller logs every action; keep it off the DB.
|
||
activity.log = async () => {}
|
||
|
||
function mockRes() {
|
||
return {
|
||
statusCode: 200,
|
||
body: null,
|
||
status(c) {
|
||
this.statusCode = c
|
||
return this
|
||
},
|
||
json(b) {
|
||
this.body = b
|
||
return this
|
||
},
|
||
}
|
||
}
|
||
|
||
const originals = {
|
||
searchCreatures: atlas.searchCreatures,
|
||
getCreature: atlas.getCreature,
|
||
listRegions: atlas.listRegions,
|
||
listLandmarks: atlas.listLandmarks,
|
||
listChampions: atlas.listChampions,
|
||
publicMeta: atlas.publicMeta,
|
||
status: atlas.status,
|
||
refresh: atlas.refresh,
|
||
approvePending: atlas.approvePending,
|
||
rejectPending: atlas.rejectPending,
|
||
setServuoPath: atlas.setServuoPath,
|
||
}
|
||
afterEach(() => Object.assign(atlas, originals))
|
||
|
||
// ── Public reads ────────────────────────────────────────────────────────
|
||
test('getCreatures passes the search through and returns the page shape', async () => {
|
||
let seen = null
|
||
atlas.searchCreatures = async (opts) => {
|
||
seen = opts
|
||
return { total: 1, limit: 50, offset: 0, creatures: [{ slug: 'lizardman', name: 'Lizardman' }] }
|
||
}
|
||
const res = mockRes()
|
||
await pub.getCreatures({ query: { q: ' lizard ', facet: 'Felucca', limit: '10', offset: '20' } }, res)
|
||
assert.deepEqual(seen, { q: 'lizard', facet: 'Felucca', limit: 10, offset: 20 })
|
||
assert.equal(res.body.total, 1)
|
||
assert.equal(res.body.creatures[0].slug, 'lizardman')
|
||
})
|
||
|
||
test('getCreatures falls back to the documented defaults when nothing is passed', async () => {
|
||
let seen = null
|
||
atlas.searchCreatures = async (opts) => {
|
||
seen = opts
|
||
return { total: 0, limit: 50, offset: 0, creatures: [] }
|
||
}
|
||
await pub.getCreatures({ query: {} }, mockRes())
|
||
assert.deepEqual(seen, { q: '', facet: '', limit: 50, offset: 0 })
|
||
})
|
||
|
||
test('an unknown creature is a 404, not an empty 200', async () => {
|
||
atlas.getCreature = async () => null
|
||
const res = mockRes()
|
||
await pub.getCreature({ params: { slug: 'nosuchthing' }, query: {} }, res)
|
||
assert.equal(res.statusCode, 404)
|
||
})
|
||
|
||
test('getCreature returns places and spawners, and `points` stays the COUNT', async () => {
|
||
atlas.getCreature = async () => ({
|
||
slug: 'lizardman',
|
||
name: 'Lizardman',
|
||
total: 214,
|
||
points: 62,
|
||
places: [{ facet: 'Trammel', label: 'Shrines', spawners: 7, maxAlive: 21 }],
|
||
spawners: [{ id: 1, facet: 'Trammel', label: 'Shrines', x: 1, y: 2 }],
|
||
spawnersTruncated: false,
|
||
alsoHere: [],
|
||
})
|
||
const res = mockRes()
|
||
await pub.getCreature({ params: { slug: 'lizardman' }, query: {} }, res)
|
||
// The list route uses `points` as a number; the detail route must not quietly
|
||
// turn the same key into an array.
|
||
assert.equal(typeof res.body.points, 'number')
|
||
assert.ok(Array.isArray(res.body.spawners))
|
||
assert.equal(res.body.places[0].label, 'Shrines')
|
||
})
|
||
|
||
// ── The projection rule (§3.6.1) ────────────────────────────────────────
|
||
test('public reads run through projectFeature, so a locked field can never survive', async () => {
|
||
// `atlas` declares no sensitive fields, so nothing here is stripped by a
|
||
// FEATURE rule. acct/webId are stripped anyway — they are locked by meaning,
|
||
// for every feature, and this is what proves the read path projects at all.
|
||
atlas.searchCreatures = async () => ({
|
||
total: 1,
|
||
limit: 50,
|
||
offset: 0,
|
||
creatures: [{ slug: 'lizardman', name: 'Lizardman', acct: 'someacct', ownerWebId: 7 }],
|
||
})
|
||
const res = mockRes()
|
||
await pub.getCreatures({ query: {}, viewerLevel: 'anonymous' }, res)
|
||
const row = res.body.creatures[0]
|
||
assert.equal(row.name, 'Lizardman')
|
||
assert.ok(!('acct' in row), 'acct must never reach an anonymous caller')
|
||
assert.ok(!('ownerWebId' in row), 'a flattened webId spelling is locked too')
|
||
})
|
||
|
||
test('getMeta reports the game world only — never the operator’s filesystem', async () => {
|
||
// The model is what enforces this; the assertion documents the boundary so a
|
||
// future "just return status() here" shortcut fails loudly.
|
||
atlas.publicMeta = async () => ({
|
||
importedAt: '2026-07-28T00:00:00.000Z',
|
||
generatedAt: '2026-07-28T00:00:00.000Z',
|
||
counts: { points: 6455, creatures: 800 },
|
||
facets: ['Felucca', 'Trammel'],
|
||
})
|
||
const res = mockRes()
|
||
await pub.getMeta({ query: {} }, res)
|
||
assert.deepEqual(Object.keys(res.body).sort(), ['counts', 'facets', 'generatedAt', 'importedAt'])
|
||
assert.ok(!('path' in res.body))
|
||
assert.ok(!('pending' in res.body))
|
||
})
|
||
|
||
test('a model failure degrades to a 500 rather than throwing', async () => {
|
||
atlas.listChampions = async () => {
|
||
throw new Error('table is gone')
|
||
}
|
||
const res = mockRes()
|
||
await pub.getChampions({ query: {} }, res)
|
||
assert.equal(res.statusCode, 500)
|
||
})
|
||
|
||
// ── Admin ───────────────────────────────────────────────────────────────
|
||
test('an unreadable tree answers 200 with the reason, not a 500', async () => {
|
||
atlas.refresh = async () => ({ status: 'unavailable', reason: 'no Spawns directory', path: '/bad' })
|
||
const res = mockRes()
|
||
await admin.importAtlas({ body: {}, user: { id: 1 } }, res)
|
||
assert.equal(res.statusCode, 200)
|
||
assert.equal(res.body.status, 'unavailable')
|
||
assert.equal(res.body.reason, 'no Spawns directory')
|
||
})
|
||
|
||
test('import passes `force` through and coerces it to a boolean', async () => {
|
||
let seen = null
|
||
atlas.refresh = async (opts) => {
|
||
seen = opts
|
||
return { status: 'unchanged' }
|
||
}
|
||
await admin.importAtlas({ body: { force: true }, user: { id: 1 } }, mockRes())
|
||
assert.deepEqual(seen, { force: true })
|
||
})
|
||
|
||
test('approve applies a staged refresh (facet loss included)', async () => {
|
||
let called = false
|
||
atlas.approvePending = async () => {
|
||
called = true
|
||
return { status: 'imported', removedFacets: ['Malas'], counts: { points: 6162 } }
|
||
}
|
||
const res = mockRes()
|
||
await admin.approve({ user: { id: 1 } }, res)
|
||
assert.ok(called)
|
||
assert.equal(res.body.status, 'imported')
|
||
})
|
||
|
||
test('rejecting when nothing is staged is a 404', async () => {
|
||
atlas.rejectPending = async () => ({ status: 'none' })
|
||
const res = mockRes()
|
||
await admin.reject({ user: { id: 1 } }, res)
|
||
assert.equal(res.statusCode, 404)
|
||
})
|
||
|
||
test('setPath trims, persists, and answers with fresh status — it does not import', async () => {
|
||
let saved = null
|
||
let imported = false
|
||
atlas.setServuoPath = async (value) => {
|
||
saved = value
|
||
}
|
||
atlas.refresh = async () => {
|
||
imported = true
|
||
return { status: 'imported' }
|
||
}
|
||
atlas.status = async () => ({ configured: true, path: '/srv/servuo', treeReadable: true })
|
||
const res = mockRes()
|
||
await admin.setPath({ body: { path: ' /srv/servuo ' }, user: { id: 3 } }, res)
|
||
assert.equal(saved, '/srv/servuo')
|
||
assert.equal(imported, false, 'changing the path must not reload the atlas as a side effect')
|
||
assert.equal(res.body.path, '/srv/servuo')
|
||
})
|
||
|
||
test('setPath accepts a blank path (clearing it turns the atlas off)', async () => {
|
||
let saved = 'unset'
|
||
atlas.setServuoPath = async (value) => {
|
||
saved = value
|
||
}
|
||
atlas.status = async () => ({ configured: false, path: '' })
|
||
const res = mockRes()
|
||
await admin.setPath({ body: {}, user: { id: 3 } }, res)
|
||
assert.equal(saved, '')
|
||
assert.equal(res.statusCode, 200)
|
||
})
|