171 lines
12 KiB
Diff
171 lines
12 KiB
Diff
diff --git a/packages/bus/tests/channel-proof.test.mjs b/packages/bus/tests/channel-proof.test.mjs
|
|
index ba0179832..435d8fa2d 100644
|
|
--- a/packages/bus/tests/channel-proof.test.mjs
|
|
+++ b/packages/bus/tests/channel-proof.test.mjs
|
|
@@ -11,0 +12,2 @@
|
|
+// Row 225 (C2-10b) re-pins webui bus.mjs for one STATUS entry, library-revert-not-agent: 409; no proof
|
|
+// or transport change.
|
|
@@ -32 +34 @@ const baselines = {
|
|
- "packages/webui/src/bus.mjs": "604092cb706b152d458f299ff1071f6cd488774629012f96b672f7e9a16c0b2c",
|
|
+ "packages/webui/src/bus.mjs": "9f33e9ca4feb7265ae33fd3ddf0ead0b75375d07ecbe9d8e55c618d29edea658",
|
|
diff --git a/packages/library/tests/render.test.mjs b/packages/library/tests/render.test.mjs
|
|
index b07e26d25..9c861bfe9 100644
|
|
--- a/packages/library/tests/render.test.mjs
|
|
+++ b/packages/library/tests/render.test.mjs
|
|
@@ -3 +3 @@ import assert from 'node:assert/strict';
|
|
-import { renderItems, UNREADABLE } from '../src/index.mjs';
|
|
+import { renderReply } from '../src/index.mjs';
|
|
@@ -4,0 +5 @@ import { renderItems, UNREADABLE } from '../src/index.mjs';
|
|
+// Row 225 (C2-10b): renderReply replaces renderItems (design 7.4, the reply schema of 5.2).
|
|
@@ -6,6 +7,12 @@ const A = 'a'.repeat(40), B = 'b'.repeat(40);
|
|
-const cite = (path, lines, quote, commit = A) => ({ path, commit, lines, quote });
|
|
-const options = {
|
|
- scopeOf: path => path.includes('/Playground/') ? 'playground' : path.endsWith('.md') ? 'canon' : undefined,
|
|
- changed: ({ path, commit }) => path === 'Book One/Characters/Mara Vell.md' && commit === A,
|
|
-};
|
|
-const render = items => renderItems(JSON.stringify({ items }), options);
|
|
+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;
|
|
@@ -13,21 +20,4 @@ const render = items => renderItems(JSON.stringify({ items }), options);
|
|
-// One snapshot per item kind: the plain text both channels show.
|
|
-const SNAPSHOTS = {
|
|
- fact: [{ kind: 'fact', text: 'Tomas is the ferryman.', cites: [cite('Book One/Characters/Tomas.md', [3, 3], 'the ferryman', B)] }],
|
|
- finding: [{ kind: 'finding', text: 'Eye colour differs.', cites: [cite('Book One/Chapters/Chapter 01.md', [41, 41], 'her grey eyes'),
|
|
- cite('Book One/Characters/Mara Vell.md', [9, 10], 'green eyes')] }],
|
|
- correction: [{ kind: 'correction', text: 'Chapter 1 says grey.', from: 'green eyes', to: 'grey eyes',
|
|
- cites: [cite('Book One/Characters/Mara Vell.md', [9, 9], 'green eyes', B)] }],
|
|
- question: [{ kind: 'question', text: 'Is the harbor north of the town?', cites: [cite('Book One/Places/Harbor Town.md', [2, 4], 'the harbor', B)] }],
|
|
- idea: [{ kind: 'idea', text: 'A storm could close the harbor.', cites: [cite('Book One/Playground/What If.md', [5, 5], 'a storm', B),
|
|
- { url: 'https://example.invalid/tides', quote: 'spring tides' }] }],
|
|
-};
|
|
-const EXPECTED = {
|
|
- fact: '1. Fact: Tomas is the ferryman.\n [canon] Book One/Characters/Tomas.md:3 @bbbbbbb "the ferryman"',
|
|
- finding: '1. Finding: Eye colour differs.\n [canon] Book One/Chapters/Chapter 01.md:41 @aaaaaaa "her grey eyes"\n'
|
|
- + ' [canon] Book One/Characters/Mara Vell.md:9-10 @aaaaaaa "green eyes" (changed since)',
|
|
- correction: '1. Correction: Chapter 1 says grey.\n From: "green eyes"\n To: "grey eyes"\n'
|
|
- + ' [canon] Book One/Characters/Mara Vell.md:9 @bbbbbbb "green eyes"',
|
|
- question: '1. Question: Is the harbor north of the town?\n [canon] Book One/Places/Harbor Town.md:2-4 @bbbbbbb "the harbor"',
|
|
- idea: '1. Idea: A storm could close the harbor.\n [playground] Book One/Playground/What If.md:5 @bbbbbbb "a storm"\n'
|
|
- + ' [web] https://example.invalid/tides "spring tides"',
|
|
-};
|
|
+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 });
|
|
+});
|
|
@@ -35,8 +25,15 @@ const EXPECTED = {
|
|
-for (const kind of Object.keys(SNAPSHOTS)) {
|
|
- test(`renderItems snapshot: ${kind}`, () => {
|
|
- const out = render(SNAPSHOTS[kind]);
|
|
- assert.equal(out.kind, 'items');
|
|
- assert.equal(out.text, EXPECTED[kind]);
|
|
- assert.equal(out.items[0].kind, kind);
|
|
- });
|
|
-}
|
|
+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 });
|
|
+});
|
|
@@ -44,6 +41,2 @@ for (const kind of Object.keys(SNAPSHOTS)) {
|
|
-test('renderItems numbers several items and takes a parsed body as well as a string', () => {
|
|
- const items = [...SNAPSHOTS.fact, ...SNAPSHOTS.question];
|
|
- const out = renderItems({ items }, options);
|
|
- assert.equal(out.text, `${EXPECTED.fact}\n${EXPECTED.question.replace(/^1\./, '2.')}`);
|
|
- assert.deepEqual(out.items[1].cites[0], { badge: 'canon', path: 'Book One/Places/Harbor Town.md', commit: B, lines: [2, 4],
|
|
- ref: 'Book One/Places/Harbor Town.md:2-4 @bbbbbbb', quote: 'the harbor', 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 }));
|
|
@@ -52,4 +45,4 @@ test('renderItems numbers several items and takes a parsed body as well as a str
|
|
-test('without options a path cite has no badge and is never marked changed', () => {
|
|
- assert.equal(renderItems({ items: SNAPSHOTS.finding }).text,
|
|
- '1. Finding: Eye colour differs.\n Book One/Chapters/Chapter 01.md:41 @aaaaaaa "her grey eyes"\n'
|
|
- + ' Book One/Characters/Mara Vell.md:9-10 @aaaaaaa "green eyes"');
|
|
+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]]);
|
|
@@ -58,6 +51,6 @@ test('without options a path cite has no badge and is never marked changed', ()
|
|
-const NOTICE_SNAPSHOTS = { 'no-cited-answer': 'No cited answer', 'no-answer': 'No answer', 'turn-failed': 'The turn failed' };
|
|
-for (const [notice, text] of Object.entries(NOTICE_SNAPSHOTS)) {
|
|
- test(`renderItems snapshot: notice ${notice}`, () => {
|
|
- assert.deepEqual(renderItems(JSON.stringify({ items: [], notice }), options), { kind: 'notice', notice, text });
|
|
- });
|
|
-}
|
|
+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]]);
|
|
+});
|
|
@@ -65,14 +58,8 @@ for (const [notice, text] of Object.entries(NOTICE_SNAPSHOTS)) {
|
|
-test('a body without the reply shape shows a fixed line and none of its text', () => {
|
|
- const secret = 'free prose the guard would refuse';
|
|
- const bodies = [secret, JSON.stringify(secret), '{"items": [], "notice": "other"}', JSON.stringify({ items: [], notice: 'no-answer', extra: secret }),
|
|
- JSON.stringify({ items: [] }), JSON.stringify({ items: [{ kind: 'story', text: secret, cites: [] }] }),
|
|
- JSON.stringify({ items: [{ kind: 'fact', text: secret }] }), JSON.stringify({ items: SNAPSHOTS.fact, prose: secret }),
|
|
- JSON.stringify({ items: [{ kind: 'correction', text: secret, cites: [] }] }),
|
|
- JSON.stringify({ items: [{ kind: 'fact', text: secret, cites: [cite('a.md', [2, 1], 'q')] }] }),
|
|
- JSON.stringify({ items: [{ kind: 'fact', text: secret, cites: [{ path: 'a.md', commit: 'abc', lines: [1, 1], quote: 'q' }] }] }),
|
|
- JSON.stringify({ items: [{ kind: 'fact', text: secret, cites: [{ url: 'https://x.invalid', path: 'a.md', quote: 'q' }] }] }),
|
|
- null, 42, '[]'];
|
|
- for (const body of bodies) {
|
|
- const out = renderItems(body, options);
|
|
- assert.deepEqual(out, { kind: 'unreadable', text: UNREADABLE }, String(body));
|
|
- assert.ok(!JSON.stringify(out).includes(secret));
|
|
+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);
|
|
@@ -80,0 +68,27 @@ test('a body without the reply shape shows a fixed line and none of its text', (
|
|
+
|
|
+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.' });
|
|
+});
|