// Ported from core in Phase 3 (MODULE_SYSTEM.md §2.7.1). One change runs through // every moved test: 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 the seam the // contract actually promises. const fs = require('fs') const os = require('os') const path = require('path') const { test, after, beforeEach } = require('node:test') const assert = require('node:assert/strict') const { AtlasSourceError, aggregateCreatures, displayName, sameSources, hashSources, buildAtlas, PARSER_VERSION, } = require('../utils/spawnAtlasSource') const shardAtlas = require('../model/shardAtlas/shardAtlas.model') const atlasDb = require('../model/shardAtlas/shardAtlas.db') const { ctx } = require('./_setup') const settings = ctx.settings // ── A tiny synthetic ServUO tree ─────────────────────────────────────────── // // Deliberately uses facets that do NOT exist in stock UO. The atlas must not // contain a built-in facet list anywhere: a shard may add facets, replace them // outright, or rename them when its maps are updated, and everything has to keep // working with no code change. function writeTree(root, { facets = ['Sosaria'], includeChampions = true } = {}) { fs.mkdirSync(path.join(root, 'Spawns'), { recursive: true }) fs.mkdirSync(path.join(root, 'Data', 'Locations'), { recursive: true }) fs.mkdirSync(path.join(root, 'Config'), { recursive: true }) // Decoration, NESTED, because the real tree nests two deep in places // (`Magincia/Trammel`, `Stygian Abyss/Ter Mur`) and a flat read would index a // fraction of it while looking like it worked. fs.mkdirSync(path.join(root, 'Data', 'Decoration', 'Deep', 'Deeper'), { recursive: true }) fs.writeFileSync( path.join(root, 'Data', 'Decoration', 'top.cfg'), '# a brazier\nBrazier 0x0E31\n100 100 0\n200 200 -5\n\nStatic 0x108F\n300 300 0\n', 'utf8', ) fs.writeFileSync( path.join(root, 'Data', 'Decoration', 'Deep', 'Deeper', 'nested.cfg'), 'Brazier 0x0E31\n400 400 0\nLargeCrate 0x0E3C\n500 500 0\n', 'utf8', ) for (const facet of facets) { fs.writeFileSync( path.join(root, 'Spawns', `${facet}.xml`), ` ${facet}Auid-${facet}-A ${facet}11001100 3True Lizardman:MX=3:SB=0:OBJ=Orc:MX=1:SB=0 ${facet}B${facet}90009000 1True lizardman:MX=2:SB=0 ${facet}Off${facet}11 1False Ghost:MX=1 `, 'utf8', ) // The location file names its facet differently from , the real // `Ter Mur` / `Tokuno Islands` drift. fs.writeFileSync( path.join(root, 'Data', 'Locations', `${facet.toLowerCase()}.xml`), ` `, 'utf8', ) } fs.writeFileSync( path.join(root, 'Data', 'Regions.xml'), `${facets .map( (facet) => ` `, ) .join('')}`, 'utf8', ) if (includeChampions) { fs.writeFileSync( path.join(root, 'Config', 'ChampionSpawns.xml'), ` `, 'utf8', ) } } function tempTree(options) { const root = fs.mkdtempSync(path.join(os.tmpdir(), 'atlas-test-')) writeTree(root, options) return root } // ── buildAtlas against a custom-facet tree ───────────────────────────────── test('buildAtlas: a point keeps the UniqueId a property lease targets', () => { // The field is asserted on the AGGREGATOR's output, not the parser's, which is // the whole point of this test. `parsePoints` produced it from Phase 12b // onwards and `PARSER_VERSION`'s own note said a point kept it, while the // mapping in `buildAtlas` rebuilt each point from an explicit field list that // omitted it — so `shard_spawn_points.unique_id` was NULL on every row, and // `listSpawners`, whose WHERE is `unique_id IS NOT NULL`, answered empty. That // left `uo.options.spawners` an empty dropdown and every Phase 12b // object-property lease unauthorable. Found by the Phase 16b released-artefact // walk, against a real tree whose files carry ~6,400 of these. // // The fixture above had no at all until this test, which is exactly // why a green suite said nothing about it. const root = tempTree({ facets: ['Sosaria'] }) const atlas = buildAtlas(root) const named = atlas.points.find((p) => p.name === 'SosariaA') assert.equal(named.uniqueId, 'uid-Sosaria-A') // And a point whose file names none is absent rather than empty-string, so the // DB layer's `unique_id IS NOT NULL AND <> ''` reads it the same way either way. const unnamed = atlas.points.find((p) => p.name === 'SosariaB') assert.ok(!unnamed.uniqueId) }) test('buildAtlas: works entirely on facets that do not exist in stock UO', () => { const root = tempTree({ facets: ['Sosaria', 'Underdark'] }) const atlas = buildAtlas(root) assert.deepEqual(atlas.facets, ['Sosaria', 'Underdark']) assert.equal(atlas.meta.counts.facets, 2) }) test('buildAtlas: reconciles a location file that spells the facet differently', () => { // "Sosaria Isles" inside the file vs Sosaria — the same drift that // silently emptied the Ter Mur / Tokuno landmark buckets. const root = tempTree({ facets: ['Sosaria'] }) const atlas = buildAtlas(root) assert.deepEqual([...new Set(atlas.landmarks.map((l) => l.facet))], ['Sosaria']) // And the fallback actually fires, rather than the point reading Wilderness. const far = atlas.points.find((p) => p.name === 'SosariaB') assert.equal(far.landmark, 'Deep Cave') assert.equal(far.label, 'Deep Cave') }) test('buildAtlas: resolves a contained point to its region', () => { const atlas = buildAtlas(tempTree({ facets: ['Sosaria'] })) const inCity = atlas.points.find((p) => p.name === 'SosariaA') assert.equal(inCity.region, 'Sosaria City') assert.equal(inCity.label, 'Sosaria City') }) test('buildAtlas: drops spawners that are switched off in-world', () => { const atlas = buildAtlas(tempTree({ facets: ['Sosaria'] })) assert.equal(atlas.points.some((p) => p.name === 'SosariaOff'), false) assert.equal(atlas.meta.counts.pointsDisabled, 1) }) test('buildAtlas: a champion altar resolves through the same placement index', () => { const atlas = buildAtlas(tempTree({ facets: ['Sosaria'] })) assert.equal(atlas.champions[0].label, 'Sosaria City') assert.equal(atlas.champions[0].facet, 'Sosaria') }) test('buildAtlas: a tree with no champion file still builds', () => { const atlas = buildAtlas(tempTree({ facets: ['Sosaria'], includeChampions: false })) assert.deepEqual(atlas.champions, []) }) test('buildAtlas: missing path and empty path raise typed errors', () => { assert.throws(() => buildAtlas(''), (err) => err instanceof AtlasSourceError && err.code === 'NO_PATH') assert.throws( () => buildAtlas(path.join(os.tmpdir(), 'definitely-not-a-servuo-tree-xyz')), (err) => err instanceof AtlasSourceError && err.code === 'NOT_FOUND', ) }) test('buildAtlas: a directory with no spawn files raises rather than building empty', () => { const root = fs.mkdtempSync(path.join(os.tmpdir(), 'atlas-empty-')) fs.mkdirSync(path.join(root, 'Data'), { recursive: true }) fs.writeFileSync(path.join(root, 'Data', 'Regions.xml'), '', 'utf8') assert.throws(() => buildAtlas(root), (err) => err.code === 'NO_SPAWNS') }) // ── Hashing ──────────────────────────────────────────────────────────────── test('hashSources: stable across reads, changes when a file changes', () => { const root = tempTree({ facets: ['Sosaria'] }) const first = hashSources(root) assert.ok(sameSources(first, hashSources(root))) fs.appendFileSync(path.join(root, 'Spawns', 'Sosaria.xml'), '', 'utf8') assert.equal(sameSources(first, hashSources(root)), false) }) test('sameSources: a missing or extra file is a difference', () => { assert.equal(sameSources({ a: '1' }, { a: '1', b: '2' }), false) assert.equal(sameSources({ a: '1' }, { a: '2' }), false) assert.equal(sameSources({ a: '1' }, { a: '1' }), true) assert.equal(sameSources(null, { a: '1' }), false) assert.equal(sameSources({ a: '1' }, null), false) }) // ── Aggregation ──────────────────────────────────────────────────────────── const POINTS = [ { facet: 'Sosaria', types: [{ type: 'Lizardman', max: 3 }, { type: 'Orc', max: 1 }] }, { facet: 'Sosaria', types: [{ type: 'Lizardman', max: 2 }] }, { facet: 'Underdark', types: [{ type: 'lizardman', max: 5 }] }, ] test('aggregateCreatures: sums each type’s own max and counts per facet', () => { const lizardman = aggregateCreatures(POINTS).find((c) => c.slug === 'lizardman') assert.equal(lizardman.total, 10) assert.equal(lizardman.points, 3) assert.deepEqual(lizardman.facets, { Sosaria: 2, Underdark: 1 }) }) test('aggregateCreatures: differing case collapses to one creature', () => { const creatures = aggregateCreatures(POINTS) assert.equal(creatures.filter((c) => c.slug === 'lizardman').length, 1) assert.deepEqual(creatures.map((c) => c.slug), ['lizardman', 'orc']) assert.equal(Object.hasOwn(creatures[0], 'spellings'), false) }) test('displayName: most common wins, ties break to the capitalised form', () => { assert.equal(displayName(new Map([['lizardman', 9], ['Lizardman', 2]])), 'lizardman') assert.equal(displayName(new Map([['lizardman', 5], ['Lizardman', 5]])), 'Lizardman') // Deterministic regardless of insertion order — a committed artifact is gone, // but a spurious diff in the DB on every restart would be just as wrong. assert.equal( displayName(new Map([['abc', 1], ['abd', 1]])), displayName(new Map([['abd', 1], ['abc', 1]])), ) }) test('pointTypeRows: collapses a repeated type to the larger max', () => { // The primary key is (point_id, slug), so a duplicate would otherwise fail the // insert and take the whole transaction with it. const rows = shardAtlas.pointTypeRows([ { types: [{ type: 'Orc', max: 1 }, { type: 'orc', max: 4 }, { type: 'Rat', max: 2 }] }, ]) assert.deepEqual(rows.sort(), [[1, 'orc', 4], [1, 'rat', 2]].sort()) }) test('pointTypeRows: point ids are 1-based and line up with insert order', () => { const rows = shardAtlas.pointTypeRows([ { types: [{ type: 'A', max: 1 }] }, { types: [{ type: 'B', max: 1 }] }, ]) assert.deepEqual(rows, [[1, 'a', 1], [2, 'b', 1]]) }) // ── The refresh decision ─────────────────────────────────────────────────── // // The boot path's two contracts: it never blocks startup, and it never applies a // facet removal on its own. let applied let pendingRow let facetsInDb let metaRow beforeEach(() => { applied = null pendingRow = null facetsInDb = [] metaRow = null atlasDb.replaceAtlas = async (atlas) => { applied = atlas return { points: atlas.points.length, creatures: atlas.creatures.length } } atlasDb.getMeta = async () => metaRow atlasDb.getFacets = async () => facetsInDb atlasDb.getPending = async () => pendingRow atlasDb.setPending = async (payload, status) => { pendingRow = { ...payload, status } } atlasDb.clearPending = async () => { pendingRow = null } settings.get = async () => '' process.env.SERVUO_PATH = '' }) test('refresh: no configured path is skipped, not an error', async () => { const result = await shardAtlas.refresh() assert.equal(result.status, 'skipped') }) test('refresh: an unreadable tree reports unavailable rather than throwing', async () => { const result = await shardAtlas.refresh({ path: path.join(os.tmpdir(), 'no-such-tree-abc') }) assert.equal(result.status, 'unavailable') assert.equal(result.code, 'NOT_FOUND') }) test('refresh: a fresh database imports', async () => { const root = tempTree({ facets: ['Sosaria'] }) const result = await shardAtlas.refresh({ path: root }) assert.equal(result.status, 'imported') assert.ok(applied) assert.deepEqual(result.addedFacets, ['Sosaria']) }) test('refresh: an unchanged tree parses nothing and writes nothing', async () => { const root = tempTree({ facets: ['Sosaria'] }) metaRow = buildAtlas(root).meta const result = await shardAtlas.refresh({ path: root }) assert.equal(result.status, 'unchanged') assert.equal(applied, null) }) // The hash gate alone would strand an install whose maps never change on // whatever an older build derived: a corrected parse would ship and never reach // the data, because the only thing compared is the tree. test('refresh: an unchanged tree is REIMPORTED when the parser has moved on', async () => { const root = tempTree({ facets: ['Sosaria'] }) metaRow = { ...buildAtlas(root).meta, parserVersion: PARSER_VERSION - 1 } const result = await shardAtlas.refresh({ path: root }) assert.equal(result.status, 'imported') assert.ok(applied) }) test('refresh: an atlas imported before parser versions existed is stale', async () => { const root = tempTree({ facets: ['Sosaria'] }) const meta = buildAtlas(root).meta delete meta.parserVersion metaRow = meta const result = await shardAtlas.refresh({ path: root }) assert.equal(result.status, 'imported') }) test('refresh: --force reimports an unchanged tree', async () => { const root = tempTree({ facets: ['Sosaria'] }) metaRow = buildAtlas(root).meta const result = await shardAtlas.refresh({ path: root, force: true }) assert.equal(result.status, 'imported') assert.ok(applied) }) test('refresh: a NEW facet applies straight away', async () => { // Additions cannot destroy anything an operator would miss. const root = tempTree({ facets: ['Sosaria', 'Underdark'] }) facetsInDb = ['Sosaria'] const result = await shardAtlas.refresh({ path: root }) assert.equal(result.status, 'imported') assert.deepEqual(result.addedFacets, ['Underdark']) }) test('refresh: a REMOVED facet is staged, not applied', async () => { const root = tempTree({ facets: ['Sosaria'] }) facetsInDb = ['Sosaria', 'Underdark'] const result = await shardAtlas.refresh({ path: root }) assert.equal(result.status, 'needsReview') assert.deepEqual(result.removedFacets, ['Underdark']) // The critical part: the existing atlas was left alone. assert.equal(applied, null) assert.equal(pendingRow.status, 'pending') }) test('refresh: approving applies the removal', async () => { const root = tempTree({ facets: ['Sosaria'] }) facetsInDb = ['Sosaria', 'Underdark'] await shardAtlas.refresh({ path: root }) assert.equal(applied, null) const result = await shardAtlas.approvePending({ path: root }) assert.equal(result.status, 'imported') assert.ok(applied) assert.deepEqual(result.removedFacets, ['Underdark']) }) test('refresh: a rejected refresh does not re-prompt while the tree is unchanged', async () => { const root = tempTree({ facets: ['Sosaria'] }) facetsInDb = ['Sosaria', 'Underdark'] await shardAtlas.refresh({ path: root }) await shardAtlas.rejectPending() assert.equal(pendingRow.status, 'rejected') const again = await shardAtlas.refresh({ path: root }) assert.equal(again.status, 'unchanged') assert.equal(applied, null) }) test('refresh: changing the tree asks again after a rejection', async () => { const root = tempTree({ facets: ['Sosaria'] }) facetsInDb = ['Sosaria', 'Underdark'] await shardAtlas.refresh({ path: root }) await shardAtlas.rejectPending() fs.appendFileSync(path.join(root, 'Spawns', 'Sosaria.xml'), '', 'utf8') const again = await shardAtlas.refresh({ path: root }) assert.equal(again.status, 'needsReview') }) test('refreshOnBoot: never throws, whatever goes wrong', async () => { atlasDb.getMeta = async () => { throw new Error('database is on fire') } atlasDb.getFacets = async () => { throw new Error('still on fire') } atlasDb.replaceAtlas = async () => { throw new Error('and the import too') } process.env.SERVUO_PATH = tempTree({ facets: ['Sosaria'] }) const result = await shardAtlas.refreshOnBoot() assert.equal(result.status, 'failed') }) test('refreshOnBoot: a missing tree is survivable, not fatal', async () => { process.env.SERVUO_PATH = path.join(os.tmpdir(), 'nope-not-here-xyz') const result = await shardAtlas.refreshOnBoot() assert.equal(result.status, 'unavailable') }) test('refresh: an explicit path overrides the configured one', async () => { const configured = tempTree({ facets: ['Configured'] }) const override = tempTree({ facets: ['Override'] }) settings.get = async () => configured const result = await shardAtlas.refresh({ path: override }) assert.equal(result.status, 'imported') assert.deepEqual(result.addedFacets, ['Override']) }) // ── The decoration index (Phase 12a) ──────────────────────── test('decoration is read recursively and rolled up per type', () => { const root = fs.mkdtempSync(path.join(os.tmpdir(), 'atlas-decor-')) try { writeTree(root) const atlas = buildAtlas(root) // Sorted by type, and `uses` counts every header line across the whole tree // — the nested file's Brazier is the second use of the same type, not a // second type. assert.deepEqual(atlas.decor, [ { type: 'Brazier', itemId: 0x0e31, uses: 2 }, { type: 'LargeCrate', itemId: 0x0e3c, uses: 1 }, { type: 'Static', itemId: 0x108f, uses: 1 }, ]) assert.equal(atlas.meta.counts.decor, 3) // Every decoration file is fingerprinted like every other source, so an // operator editing one is a tree change the boot path notices. const labels = Object.keys(atlas.meta.source).filter((l) => l.startsWith('Data/Decoration/')) assert.deepEqual(labels.sort(), ['Data/Decoration/Deep/Deeper/nested.cfg', 'Data/Decoration/top.cfg']) } finally { fs.rmSync(root, { recursive: true, force: true }) } }) test('two spellings of one decoration type fold into one row', () => { // The Phase 16 acceptance walk's blocking finding. Stock ServUO 57.4's own // `Data/Decoration/` names four types under two casings each — // CheckerBoard/Checkerboard, ChessBoard/Chessboard, MetalChest/Metalchest, // SpinningWheelEastAddon/SpinningwheelEastAddon — and in every pair exactly one // is a real class; the other is a mis-cased line the shard's own loader resolves // anyway. // // A case-SENSITIVE Map keeps both. `shard_decor_types.type` is a PRIMARY KEY // under MariaDB's default `..._ai_ci` collation, which folds case, so the second // row raised `1062 Duplicate entry` and took the WHOLE atlas import transaction // down with it. The blast radius is not decoration: with no atlas, EVERY option // source answers empty and no world verb can be authored at all. // // Asserted on the count as well as the row, because the failure mode was two // rows that a database — not this function — would later refuse. const root = fs.mkdtempSync(path.join(os.tmpdir(), 'atlas-decorcase-')) try { writeTree(root) fs.writeFileSync( path.join(root, 'Data', 'Decoration', 'miscased.cfg'), 'checkerboard 0x0FA6\n600 600 0\nCheckerBoard 0x0FA6\n700 700 0\n', ) const atlas = buildAtlas(root) const boards = atlas.decor.filter((d) => d.type.toLowerCase() === 'checkerboard') assert.equal(boards.length, 1, 'two casings of one type must not be two rows') // First spelling seen wins, exactly as the first item id does. Which one // survives is cosmetic — the shard resolves either. assert.equal(boards[0].type, 'checkerboard') assert.equal(boards[0].uses, 2, 'both lines still count as uses of the one type') } finally { fs.rmSync(root, { recursive: true, force: true }) } }) test('a tree with no decoration at all still builds', () => { // Optional, like the champion file. A shard that has stripped its decoration // has a perfectly good atlas; the decoration verb simply has nothing to offer. const root = fs.mkdtempSync(path.join(os.tmpdir(), 'atlas-nodecor-')) try { writeTree(root) fs.rmSync(path.join(root, 'Data', 'Decoration'), { recursive: true, force: true }) const atlas = buildAtlas(root) assert.deepEqual(atlas.decor, []) assert.equal(atlas.meta.counts.decor, 0) } finally { fs.rmSync(root, { recursive: true, force: true }) } })