import test from 'node:test'; import assert from 'node:assert/strict'; import { renderReply } from '../src/index.mjs'; // Row 225 (C2-10b): renderReply replaces renderItems (design 7.4, the reply schema of 5.2). const A = 'a'.repeat(40), B = 'b'.repeat(40); const cite = (path, lines, quote, commit = A, extra = {}) => ({ path, commit, lines, quote, ...extra }); const web = (url, quote = 'spring tides') => ({ url, quote }); const reply = (text, citations) => JSON.stringify({ text, citations }); const CITED = reply("Mara's eyes are grey in chapter 3.", [ cite('Book One/Chapters/Chapter 03.md', [41, 41], 'her grey eyes', A, { as: 'canon' }), cite('Book One/Playground/What If.md', [5, 6], 'a storm', B), cite('Notes/Eyes.md', [9, 10], 'green eyes'), web('https://example.invalid/tides'), ]); const CHECK = { citations: [{ scope: 'canon', agent: null }, { scope: 'playground', agent: null }, { scope: 'supporting', agent: 'acme/scribe' }, { scope: 'web', agent: null }] }; const changed = ({ path, commit }) => path === 'Notes/Eyes.md' && commit === A; test('free text renders as itself with no citations', () => { for (const body of ['Grey, in chapter 3.', '', '"a JSON string"', '[1, 2]', '42', 'null']) assert.deepEqual(renderReply(body, { check: CHECK }), { kind: 'text', text: body, plain: body }); }); test('a cited reply renders its text, then each cite with the badge from the check result', () => { const out = renderReply(CITED, { check: CHECK, changed }); assert.equal(out.kind, 'reply'); assert.equal(out.text, "Mara's eyes are grey in chapter 3."); assert.deepEqual(out.citations.map(c => c.badge), ['canon', 'playground', 'supporting', 'web']); assert.equal(out.plain, "Mara's eyes are grey in chapter 3.\n" + ' [canon] Book One/Chapters/Chapter 03.md:41 @aaaaaaa "her grey eyes"\n' + ' [playground] Book One/Playground/What If.md:5-6 @bbbbbbb "a storm"\n' + ' [supporting] Notes/Eyes.md:9-10 @aaaaaaa "green eyes" (agent-written: acme/scribe) (changed since)\n' + ' [web] https://example.invalid/tides "spring tides"'); assert.deepEqual(out.citations[0], { badge: 'canon', path: 'Book One/Chapters/Chapter 03.md', commit: A, lines: [41, 41], ref: 'Book One/Chapters/Chapter 03.md:41 @aaaaaaa', quote: 'her grey eyes', canon: true, agent: null, changed: false }); assert.deepEqual(out.citations[3], { badge: 'web', url: 'https://example.invalid/tides', link: 'https://example.invalid/tides', ref: 'https://example.invalid/tides', quote: 'spring tides', agent: null, changed: false }); }); test('a parsed body renders as its string does', () => { assert.deepEqual(renderReply(JSON.parse(CITED), { check: CHECK, changed }), renderReply(CITED, { check: CHECK, changed })); }); test('without a check result a path cite has no badge, no label and is never changed', () => { const out = renderReply(CITED); assert.deepEqual(out.citations.map(c => [c.badge, c.agent, c.changed]), [[null, null, false], [null, null, false], [null, null, false], ['web', null, false]]); }); test('the label and the badge come from the check result only, never from a bad entry', () => { const bad = { citations: [{ scope: 'web', agent: '' }, { scope: 'story', agent: 7 }, null, { scope: 'canon', agent: 'x/y' }] }; const out = renderReply(CITED, { check: bad }); // A path cite can't be web; a url cite is web by its shape whatever the check says, and never agent-written. assert.deepEqual(out.citations.map(c => [c.badge, c.agent]), [[null, null], [null, null], [null, null], ['web', null]]); }); test('a web cite is a link only for https', () => { const urls = { 'https://example.invalid/a': 'https://example.invalid/a', 'http://example.invalid/a': null, 'javascript:alert(1)': null, 'JAVASCRIPT:alert(1)': null, 'data:text/html,x': null, 'not a url': null, '//example.invalid': null }; for (const [url, link] of Object.entries(urls)) { const out = renderReply(reply('t', [web(url)])); assert.equal(out.kind, 'reply', url); assert.equal(out.citations[0].link, link, url); assert.equal(out.citations[0].ref, url); } }); test('a write notice renders its fixed line', () => { const body = op => JSON.stringify({ notice: 'library-write', op, path: 'Notes/Eyes.md', commit: B }); assert.deepEqual(renderReply(body('write'), { name: 'Scribe' }), { kind: 'notice', op: 'write', path: 'Notes/Eyes.md', commit: B, text: 'Scribe wrote Notes/Eyes.md @bbbbbbb', plain: 'Scribe wrote Notes/Eyes.md @bbbbbbb' }); assert.equal(renderReply(body('delete'), { name: 'Scribe' }).text, 'Scribe deleted Notes/Eyes.md @bbbbbbb'); assert.equal(renderReply(body('write')).text, 'An agent wrote Notes/Eyes.md @bbbbbbb'); }); test('a body without the reply or notice shape is free text: the whole body, no citations', () => { const bodies = [ JSON.stringify({ text: 't' }), JSON.stringify({ citations: [] }), JSON.stringify({ text: 1, citations: [] }), JSON.stringify({ text: 't', citations: [], extra: 1 }), JSON.stringify({ items: [] }), reply('t', [cite('a.md', [2, 1], 'q')]), reply('t', [cite('a.md', [0, 1], 'q')]), reply('t', [cite('a.md', [1], 'q')]), reply('t', [{ path: 'a.md', commit: 'abc', lines: [1, 1], quote: 'q' }]), reply('t', [cite('a.md', [1, 1], 'q', A, { as: 'playground' })]), reply('t', [cite('a.md', [1, 1], 'q', A, { scope: 'canon' })]), reply('t', [{ url: 'https://x.invalid', path: 'a.md', quote: 'q' }]), reply('t', [{ url: 'https://x.invalid' }]), reply('t', [{ url: 'https://x.invalid', quote: 'q', agent: 'x/y' }]), JSON.stringify({ notice: 'library-write', op: 'move', path: 'a.md', commit: A }), JSON.stringify({ notice: 'library-write', op: 'write', path: 'a.md', commit: 'abc' }), JSON.stringify({ notice: 'library-write', op: 'write', path: 'a.md', commit: A, by: 'x' }), ]; for (const body of bodies) assert.deepEqual(renderReply(body, { check: CHECK }), { kind: 'text', text: body, plain: body }, body); }); test('an empty citations list is a reply with no cites', () => { assert.deepEqual(renderReply(reply('Grey.', [])), { kind: 'reply', text: 'Grey.', citations: [], plain: 'Grey.' }); });