const { test } = require('node:test') const assert = require('node:assert/strict') const fs = require('node:fs') const os = require('node:os') const path = require('node:path') const { ClilocSourceError, CUSTOM_DIR, resolveBase, listCustom, readSources, hashSources, sameSources, missingSources, readCliloc, } = require('../src/utils/clilocSource') // The fs layer, exercised against real temp directories rather than mocks — // the behaviours that matter here (which file wins, what a directory listing // yields, what happens when one vanishes) are precisely the ones a mock would // define away. function tmpdir() { const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'cliloc-')) return dir } const tsv = (entries) => entries.map(([n, t]) => `${n}\t${t}`).join('\n') + '\n' function write(dir, name, contents) { const file = path.join(dir, name) fs.mkdirSync(path.dirname(file), { recursive: true }) fs.writeFileSync(file, contents) return file } // ── Resolution ───────────────────────────────────────────────────────────── test('resolveBase: a directory picks the most specific candidate', () => { const dir = tmpdir() // A converted file sitting next to the client's own compressed one must win — // otherwise pointing at a client folder finds the file that will be rejected. write(dir, 'cliloc.enu', 'ignored') write(dir, 'clilocs.tsv', tsv([[1023721, 'quarter staff']])) assert.equal(path.basename(resolveBase(dir).base), 'clilocs.tsv') }) test('resolveBase: a file path roots overlays at its DIRECTORY', () => { // An operator who pointed at a file should not have to re-point at its folder // just to add a custom/ directory beside it. const dir = tmpdir() const file = write(dir, 'clilocs.tsv', tsv([[1, 'a']])) assert.deepEqual(resolveBase(file), { root: dir, base: file }) }) test('resolveBase: a missing path and an empty path are different errors', () => { assert.throws(() => resolveBase(''), (err) => err.code === 'NO_PATH') assert.throws(() => resolveBase(path.join(tmpdir(), 'nope')), (err) => err.code === 'NOT_FOUND') }) test('resolveBase: a directory with no cliloc file names what it looked for', () => { assert.throws(() => resolveBase(tmpdir()), (err) => { assert.ok(err instanceof ClilocSourceError) assert.equal(err.code, 'NO_FILE') assert.match(err.message, /clilocs\.tsv/) return true }) }) // ── Overlays ─────────────────────────────────────────────────────────────── test('listCustom: no overlay directory is normal, not an error', () => { const dir = tmpdir() write(dir, 'clilocs.tsv', tsv([[1, 'a']])) assert.deepEqual(listCustom(dir), []) }) test('listCustom: sorted, and only recognised extensions', () => { const dir = tmpdir() write(dir, 'clilocs.tsv', tsv([[1, 'a']])) write(dir, `${CUSTOM_DIR}/b.tsv`, tsv([[2, 'b']])) write(dir, `${CUSTOM_DIR}/a.csv`, tsv([[3, 'c']])) write(dir, `${CUSTOM_DIR}/notes.md`, 'ignore me') assert.deepEqual(listCustom(dir).map((f) => path.basename(f)), ['a.csv', 'b.tsv']) }) test('readSources: base first, then overlays, with root-relative labels', () => { const dir = tmpdir() write(dir, 'clilocs.tsv', tsv([[1, 'a']])) write(dir, `${CUSTOM_DIR}/shard.tsv`, tsv([[2, 'b']])) const { files } = readSources(dir) // Forward-slashed so the same directory read on Windows and Linux fingerprints // identically — otherwise every boot on one of them looks like a change. assert.deepEqual(files.map((f) => [f.label, f.kind]), [ ['clilocs.tsv', 'base'], ['custom/shard.tsv', 'custom'], ]) }) // ── Merging ──────────────────────────────────────────────────────────────── test('readCliloc: an overlay ADDS ids the base never had', () => { // The whole point: shards add items, and those carry cliloc ids no stock // client table has. const dir = tmpdir() write(dir, 'clilocs.tsv', tsv([[1023721, 'quarter staff']])) write(dir, `${CUSTOM_DIR}/shard.tsv`, tsv([[1180001, 'Runic Gateway Sigil']])) const byNumber = new Map(readCliloc(dir).entries.map((e) => [e.number, e.text])) assert.equal(byNumber.get(1023721), 'quarter staff') assert.equal(byNumber.get(1180001), 'Runic Gateway Sigil') }) test('readCliloc: an overlay OVERRIDES a stock id', () => { const dir = tmpdir() write(dir, 'clilocs.tsv', tsv([[1023721, 'quarter staff']])) write(dir, `${CUSTOM_DIR}/shard.tsv`, tsv([[1023721, 'gnarled staff of testing']])) const byNumber = new Map(readCliloc(dir).entries.map((e) => [e.number, e.text])) assert.equal(byNumber.get(1023721), 'gnarled staff of testing') }) test('readCliloc: later overlays beat earlier ones, deterministically', () => { const dir = tmpdir() write(dir, 'clilocs.tsv', tsv([[7, 'base']])) write(dir, `${CUSTOM_DIR}/01-first.tsv`, tsv([[7, 'first']])) write(dir, `${CUSTOM_DIR}/02-second.tsv`, tsv([[7, 'second']])) const byNumber = new Map(readCliloc(dir).entries.map((e) => [e.number, e.text])) assert.equal(byNumber.get(7), 'second') }) test('readCliloc: reports what each source contributed', () => { // An operator who adds an overlay wants to see it took effect; "overrode: 0" // on a file meant to re-label stock items says it did not. const dir = tmpdir() write(dir, 'clilocs.tsv', tsv([[1, 'a'], [2, 'b']])) write(dir, `${CUSTOM_DIR}/shard.tsv`, tsv([[2, 'B!'], [3, 'c']])) const { source } = readCliloc(dir) assert.deepEqual(source.sources, [ { label: 'clilocs.tsv', kind: 'base', entries: 2, added: 2, overrode: 0 }, { label: 'custom/shard.tsv', kind: 'custom', entries: 2, added: 1, overrode: 1 }, ]) }) test('readCliloc: a malformed overlay names the file it came from', () => { // "Which of my six overlay files is broken" is otherwise a guessing game. const dir = tmpdir() write(dir, 'clilocs.tsv', tsv([[1, 'a']])) write(dir, `${CUSTOM_DIR}/broken.tsv`, 'no separators here\nnor here\n') assert.throws(() => readCliloc(dir), (err) => { assert.equal(err.code, 'EMPTY') assert.match(err.message, /^custom\/broken\.tsv: /) return true }) }) test('readCliloc: a compressed BASE is still rejected by name', () => { const dir = tmpdir() write(dir, 'cliloc.enu', Buffer.concat([Buffer.from([0xe8, 0x79, 0x67, 0x8e]), Buffer.alloc(32, 0x41)])) assert.throws(() => readCliloc(dir), (err) => err.code === 'COMPRESSED') }) // ── Drift over the SET ───────────────────────────────────────────────────── test('hashSources: fingerprints every source, and counts the overlays', () => { const dir = tmpdir() write(dir, 'clilocs.tsv', tsv([[1, 'a']])) write(dir, `${CUSTOM_DIR}/shard.tsv`, tsv([[2, 'b']])) const fp = hashSources(dir) assert.deepEqual(Object.keys(fp.hashes).sort(), ['clilocs.tsv', 'custom/shard.tsv']) assert.equal(fp.customCount, 1) }) test('sameSources: adding or editing an overlay counts as drift', () => { const dir = tmpdir() write(dir, 'clilocs.tsv', tsv([[1, 'a']])) const before = hashSources(dir).hashes write(dir, `${CUSTOM_DIR}/shard.tsv`, tsv([[2, 'b']])) const added = hashSources(dir).hashes assert.equal(sameSources(before, added), false, 'a new overlay is drift') write(dir, `${CUSTOM_DIR}/shard.tsv`, tsv([[2, 'b changed']])) const edited = hashSources(dir).hashes assert.equal(sameSources(added, edited), false, 'an edited overlay is drift') assert.equal(sameSources(edited, hashSources(dir).hashes), true, 'an untouched set is not') }) test('missingSources: a vanished source is detected, an added one is not "missing"', () => { const loaded = { 'clilocs.tsv': 'aaa', 'custom/shard.tsv': 'bbb' } assert.deepEqual(missingSources({ 'clilocs.tsv': 'aaa' }, loaded), ['custom/shard.tsv']) assert.deepEqual(missingSources({ ...loaded, 'custom/new.tsv': 'ccc' }, loaded), []) // Nothing loaded yet (a first import) is not a vanished source. assert.deepEqual(missingSources({ 'clilocs.tsv': 'aaa' }, null), []) })