// Which cliloc source runs, and what the shard path does with the answer // (docs/link/v8.md §9, docs/website/CLILOCS.md). // // The model is the only place that decides between the two pipelines, so these // drive it with the shard, the database and the filesystem all stubbed. Nothing // here reaches the real sidecar or a real table. 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 clilocs = require('../model/shardClilocs/shardClilocs.model') const db = require('../model/shardClilocs/shardClilocs.db') const bridge = require('../utils/clilocBridge') const uoLinkConfig = require('../model/uoLinkConfig/uoLinkConfig.model') const { ctx } = require('./_setup') const saved = { getMeta: db.getMeta, replaceAll: db.replaceAll, count: db.count, fingerprint: bridge.fingerprint, readCliloc: bridge.readCliloc, getSafe: uoLinkConfig.getSafe, settingsGet: ctx.settings.get, } function restore() { db.getMeta = saved.getMeta db.replaceAll = saved.replaceAll db.count = saved.count bridge.fingerprint = saved.fingerprint bridge.readCliloc = saved.readCliloc uoLinkConfig.getSafe = saved.getSafe ctx.settings.get = saved.settingsGet } const FINGERPRINT = { kind: 'bridge', file: 'cliloc.enu', size: 4989921, mtime: 1757462400000, sha256: 'abc', extractorVersion: 1, hashing: false, complete: true, } /** * A rig with the shard reachable (or not), the configured overlay path pointed * at a temp directory, and every write captured rather than made. */ function rig({ linked = true, meta = null, clientPath = '', rows = [] } = {}) { const applied = [] uoLinkConfig.getSafe = async () => ({ enabled: linked, baseUrl: linked ? 'http://127.0.0.1:8099' : null }) ctx.settings.get = async (key) => (key === clilocs.SETTING_KEY ? clientPath : null) db.getMeta = async () => meta db.count = async () => meta?.count ?? 0 db.replaceAll = async (entries, writtenMeta) => { applied.push({ entries, meta: writtenMeta }) return { count: entries.length, blank: 0, duplicates: 0 } } bridge.fingerprint = async () => FINGERPRINT bridge.readCliloc = async () => ({ entries: rows, source: { kind: 'bridge', lang: 'enu', file: 'cliloc.enu', size: FINGERPRINT.size, mtime: FINGERPRINT.mtime, pages: 1, reported: rows.length, received: rows.length, }, }) return applied } function tmpWithOverlay(contents) { const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'cliloc-sel-')) fs.mkdirSync(path.join(dir, 'custom'), { recursive: true }) if (contents !== undefined) fs.writeFileSync(path.join(dir, 'custom', 'shard.tsv'), contents) return dir } // ── Which source runs ────────────────────────────────────────────────────── test('a configured shard is the base source, and the file path is not consulted', async (t) => { const applied = rig({ rows: [{ number: 1, flag: 0, text: 'a' }] }) t.after(restore) const result = await clilocs.refresh() assert.equal(result.status, 'imported') assert.equal(result.source, 'bridge') assert.equal(applied.length, 1) assert.equal(applied[0].meta.source, 'bridge') }) test('no shard link falls back to the file pipeline, unchanged', async (t) => { rig({ linked: false }) t.after(restore) // No path configured either, so the file path reports exactly what it always // did — which is the assertion: the fallback is the OLD code, not a new one. const result = await clilocs.refresh() assert.equal(result.status, 'skipped') assert.equal(result.reason, 'no cliloc path configured') }) test('an explicit path is still an escape hatch, even with a shard linked', async (t) => { let asked = false rig({ linked: true }) bridge.fingerprint = async () => { asked = true return FINGERPRINT } t.after(restore) const result = await clilocs.refresh({ path: path.join(os.tmpdir(), 'nope-does-not-exist') }) assert.equal(asked, false, 'the shard must not be asked when a file was named') assert.equal(result.status, 'unavailable') }) // Boot deliberately does not call the shard: it would put a sidecar round trip // in the startup sequence to answer a question whose answer is "no" except after // a client patch, which is an operator action. test('boot imports nothing over the bridge and leaves the loaded table serving', async (t) => { let asked = false rig({ linked: true }) bridge.fingerprint = async () => { asked = true return FINGERPRINT } t.after(restore) const result = await clilocs.refreshOnBoot() assert.equal(result.status, 'skipped') assert.equal(result.source, 'bridge') assert.equal(asked, false) }) // ── The gate ─────────────────────────────────────────────────────────────── test('an unchanged client file and no overlays is a no-op', async (t) => { const applied = rig({ meta: { source: 'bridge', base: FINGERPRINT, hashes: {}, parserVersion: 1, count: 67496 }, }) t.after(restore) const result = await clilocs.refresh() assert.equal(result.status, 'unchanged') assert.equal(result.count, 67496) assert.equal(applied.length, 0) }) test('a patched client re-imports', async (t) => { const applied = rig({ meta: { source: 'bridge', base: { ...FINGERPRINT, sha256: 'older' }, hashes: {}, parserVersion: 1, count: 10, }, rows: [{ number: 1, flag: 0, text: 'a' }], }) t.after(restore) assert.equal((await clilocs.refresh()).status, 'imported') assert.equal(applied.length, 1) }) // The upgrade path. An install that used the converted-file pipeline carries its // base label in the stored fingerprint; on the bridge that label is SUPPOSED to // disappear. Counting it as a vanished source would make the first import after // the upgrade demand an approval for a change the upgrade itself made. test('the retired file base is not reported as a vanished source', async (t) => { const applied = rig({ meta: { source: 'file', hashes: { 'clilocs.plain': 'aaa' }, parserVersion: 1, count: 67496, }, rows: [{ number: 1, flag: 0, text: 'a' }], }) t.after(restore) const result = await clilocs.refresh() assert.equal(result.status, 'imported', result.reason) assert.equal(applied.length, 1) }) // An overlay is a different matter: it vanished, and an unmounted volume looks // exactly like a deliberate deletion from here. test('a vanished OVERLAY still stages for review', async (t) => { const applied = rig({ meta: { source: 'bridge', base: FINGERPRINT, hashes: { 'custom/shard.tsv': 'aaa' }, parserVersion: 1, count: 5, }, }) t.after(restore) const result = await clilocs.refresh() assert.equal(result.status, 'needsReview') assert.deepEqual(result.missingSources, ['custom/shard.tsv']) assert.equal(applied.length, 0) const accepted = await clilocs.refresh({ approve: true }) assert.equal(accepted.status, 'imported') assert.deepEqual(accepted.acceptedMissing, ['custom/shard.tsv']) }) // ── The merge ────────────────────────────────────────────────────────────── test('an overlay overrides the shard table, and says so', async (t) => { const dir = tmpWithOverlay('1023721\ta better staff\n900001\ta shard-only item\n') const applied = rig({ clientPath: dir, rows: [ { number: 1023721, flag: 0, text: 'quarter staff' }, { number: 3000001, flag: 0, text: 'Entering Britannia...' }, ], }) t.after(() => { restore() fs.rmSync(dir, { recursive: true, force: true }) }) const result = await clilocs.refresh() assert.equal(result.status, 'imported', result.reason) const stored = new Map(applied[0].entries.map((e) => [e.number, e.text])) assert.equal(stored.get(1023721), 'a better staff', 'the overlay must win') assert.equal(stored.get(3000001), 'Entering Britannia...') assert.equal(stored.get(900001), 'a shard-only item') const overlay = result.sources.find((s) => s.kind === 'custom') assert.equal(overlay.label, 'custom/shard.tsv') assert.equal(overlay.added, 1) assert.equal(overlay.overrode, 1) // Only overlay hashes are stored now — the base is fingerprinted separately, // and mixing them is what made the upgrade case above ambiguous. assert.deepEqual(Object.keys(applied[0].meta.hashes), ['custom/shard.tsv']) assert.equal(applied[0].meta.base.sha256, 'abc') }) test('a malformed overlay names the file rather than failing the import namelessly', async (t) => { const dir = tmpWithOverlay('not a cliloc file at all\n') rig({ clientPath: dir, rows: [{ number: 1, flag: 0, text: 'a' }] }) t.after(() => { restore() fs.rmSync(dir, { recursive: true, force: true }) }) const result = await clilocs.refresh() assert.equal(result.status, 'unavailable') assert.match(result.reason, /custom\/shard\.tsv/) }) // An overlay path an operator has mistyped must not stop a base table that // arrived perfectly well — but it must be visible, or the site silently serves a // table missing every shard-added name. test('an unreadable overlay path is reported beside a successful import', async (t) => { const applied = rig({ clientPath: path.join(os.tmpdir(), 'cliloc-does-not-exist-at-all'), rows: [{ number: 1, flag: 0, text: 'a' }], }) t.after(restore) const result = await clilocs.refresh() assert.equal(result.status, 'imported') assert.match(result.overlayProblem, /does not exist/) assert.equal(applied.length, 1) }) // ── Status ───────────────────────────────────────────────────────────────── test('status describes the shard source, hash state and drift', async (t) => { rig({ meta: { source: 'bridge', base: FINGERPRINT, hashes: {}, parserVersion: 1, count: 67496 } }) t.after(restore) const status = await clilocs.status() assert.equal(status.source, 'bridge') assert.equal(status.file, 'cliloc.enu') assert.equal(status.fileReadable, true) assert.equal(status.drift, false) assert.equal(status.shard.extractorVersion, 1) assert.equal(status.shard.hashing, false) }) test('a shard that cannot be reached is a problem on the status, not a throw', async (t) => { rig({}) bridge.fingerprint = async () => { throw new bridge.ClilocBridgeError('The shard did not answer: timeout', 'SHARD_DOWN') } t.after(restore) const status = await clilocs.status() assert.equal(status.source, 'bridge') assert.equal(status.fileReadable, false) assert.equal(status.code, 'SHARD_DOWN') // Null, not false: with no fingerprint there is nothing to compare, and // reporting "no drift" would read as "up to date". assert.equal(status.drift, null) })