process.env.DB_HOST = '127.0.0.1' process.env.DB_PORT = '59999' 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, } = require('../src/utils/spawnAtlasSource') const shardAtlas = require('../src/model/shardAtlas/shardAtlas.model') const atlasDb = require('../src/model/shardAtlas/shardAtlas.db') const settings = require('../src/model/settings/settings.model') const db = require('../src/utils/db') after(() => db.close()) // ── 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 }) for (const facet of facets) { fs.writeFileSync( path.join(root, 'Spawns', `${facet}.xml`), ` ${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: 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 = { source: buildAtlas(root).meta.source } const result = await shardAtlas.refresh({ path: root }) assert.equal(result.status, 'unchanged') assert.equal(applied, null) }) test('refresh: --force reimports an unchanged tree', async () => { const root = tempTree({ facets: ['Sosaria'] }) metaRow = { source: buildAtlas(root).meta.source } 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']) })