// Sources arrived in retrieval order — an order the reader never sees and // cannot follow. An answer whose first citation was [7] opened a list that // began at [1], so matching a marker to a source meant hunting for it. // // Borrowed from the quiz app, where validating citations and ordering them fall // out of the same pass: it collects the sources the answer actually used into an // insertion-ordered map, so the list comes back in first-citation order for // free. Reference lists in published writing are numbered by first appearance // for the same reason. const test = require('node:test'); const assert = require('node:assert/strict'); const fs = require('node:fs'); const path = require('node:path'); const vm = require('node:vm'); function load() { const src = fs.readFileSync(path.join(__dirname, '..', 'public/js/assistant/citations.js'), 'utf8') .replace(/^import[\s\S]*?from ['"][^'"]+['"];\s*/gm, '') .replace(/^export /gm, ''); const ctx = { window: {}, document: undefined, console }; vm.createContext(ctx); vm.runInContext(src + '\nthis.orderSourcesByCitation = orderSourcesByCitation;', ctx); return ctx; } const four = [ { number: 1, title: 'Alpha' }, { number: 2, title: 'Beta' }, { number: 3, title: 'Gamma' }, { number: 4, title: 'Delta' } ]; test('sources come back in the order the answer cites them', () => { const { orderSourcesByCitation } = load(); const out = orderSourcesByCitation('Third first [3]. Then the first [1].', four); assert.deepEqual(Array.from(out.sources.slice(0, 2), s => s.title), ['Gamma', 'Alpha']); assert.equal(out.text, 'Third first [1]. Then the first [2].'); }); test('a cluster is renumbered as a whole, in its own order', () => { const { orderSourcesByCitation } = load(); const out = orderSourcesByCitation('Both of these [4, 2] agree.', four); assert.equal(out.text, 'Both of these [1, 2] agree.'); assert.deepEqual(Array.from(out.sources.slice(0, 2), s => s.title), ['Delta', 'Beta']); }); test('renumbering happens in one pass, so nothing is renumbered twice', () => { // Rewriting number by number turns 2 into 1, then that 1 into whatever 1 // maps to. The whole text is rewritten once instead. const { orderSourcesByCitation } = load(); const out = orderSourcesByCitation('[2] then [1] then [2] again.', four); assert.equal(out.text, '[1] then [2] then [1] again.'); }); test('a source cited twice keeps its first position', () => { const { orderSourcesByCitation } = load(); const out = orderSourcesByCitation('[3] ... [1] ... [3] again.', four); assert.deepEqual(Array.from(out.sources.slice(0, 2), s => s.title), ['Gamma', 'Alpha']); }); test('retrieved but uncited sources follow, marked and still numbered', () => { // The panel is also a view of what the search returned, so they stay — just // no longer mixed in among the numbers the answer used. const { orderSourcesByCitation } = load(); const out = orderSourcesByCitation('Only this one [2].', four); assert.equal(out.sources[0].title, 'Beta'); assert.equal(out.sources[0].uncited, undefined); assert.equal(out.sources.length, 4, 'nothing is dropped'); assert.deepEqual(Array.from(out.sources.slice(1), s => s.uncited), [true, true, true]); assert.deepEqual(Array.from(out.sources, s => s.number), [1, 2, 3, 4], 'numbering stays contiguous'); }); test('an invented citation reserves no place and is left alone', () => { // It is not turned into a link either; the audit records it. What matters // here is that it cannot push a real source down the list. const { orderSourcesByCitation } = load(); const out = orderSourcesByCitation('Invented [9]. Real [2].', four); assert.equal(out.sources[0].title, 'Beta'); assert.match(out.text, /Invented \[9\]/, 'the unresolved marker is untouched'); assert.match(out.text, /Real \[1\]/); }); test('a cluster containing an invented number is left whole', () => { // Renumbering half of it would silently change which source the good half // points at. const { orderSourcesByCitation } = load(); const out = orderSourcesByCitation('Mixed [2, 9].', four); assert.equal(out.text, 'Mixed [2, 9].'); }); test('an answer that cites nothing is returned untouched', () => { const { orderSourcesByCitation } = load(); const out = orderSourcesByCitation('No citations here.', four); assert.equal(out.text, 'No citations here.'); assert.deepEqual(Array.from(out.sources), four); }); test('the originals are not mutated, so a stored answer stays readable', () => { // Its text still holds the original markers; renumbering in place would make // the two disagree. const { orderSourcesByCitation } = load(); const before = JSON.parse(JSON.stringify(four)); orderSourcesByCitation('[3] [1]', four); assert.deepEqual(four, before); }); test('sources with no number field fall back to position', () => { const { orderSourcesByCitation } = load(); const bare = [{ title: 'One' }, { title: 'Two' }, { title: 'Three' }]; const out = orderSourcesByCitation('Cite the third [3].', bare); assert.equal(out.sources[0].title, 'Three'); assert.equal(out.text, 'Cite the third [1].'); });