import { test, before, after } from 'node:test';
import assert from 'node:assert/strict';
import { readFile } from 'node:fs/promises';
import { chromium } from 'playwright';
let browser;
const source = await readFile(new URL('../static/js/turnRendering.js', import.meta.url), 'utf8');
before(async () => { browser = await chromium.launch({ headless: true }); });
after(async () => { await browser?.close(); });
async function run(html, action) {
const page = await browser.newPage();
try {
await page.setContent(`
Hello world.
Hello world.
', raw: 'Hello **world**.', messageId: '42' }); return { ...rendered, same: strong === body.querySelector('strong'), selection: selection.toString(), focused: document.activeElement === button, id: body.parentElement.dataset.dbId }; }); assert.deepEqual(result, { accepted: true, changed: false, same: true, selection: 'world', focused: true, id: '42' }); }); test('matching raw accumulator does not excuse an empty visible answer', async () => { const result = await run('Notes
', raw: 'Notes' }); return { ...rendered, visible: body.innerText }; }); assert.deepEqual(result, { accepted: true, changed: true, visible: 'Notes' }); }); test('hidden or collapsed note text is not evidence of a visible answer', async () => { for (const content of ['Notes', '', 'Notes', 'Notes
Notes
'); owner.render({ body, html: 'Notes
', raw: 'Notes' }); return { before, after: owner.isVisible(body, 'Notes
'), visible: body.innerText }; }); assert.deepEqual(result, { before: false, after: true, visible: 'Notes' }); } }); test('structured note list renders once across final response and saved completion', async () => { const result = await run('First
', '']) { const result = await run(`Notes
', raw: 'Notes' }); }); assert.deepEqual(result, { changed: true, accepted: false }); }); test('deduplicates answer bodies only, preserving preamble and tool evidence', async () => { const result = await run('Notes
Notes
', raw: 'Notes' }); return { answers: [...document.querySelectorAll('.body')].map(el => el.innerText), evidence: document.querySelector('details').open }; }); assert.deepEqual(result, { answers: ['Looking up notes.', 'Notes'], evidence: true }); }); test('completion never consumes the next user turn or another stream', async () => { const result = await run('Notes
', raw: 'Notes' }); const rejected = owner.render({ body: document.querySelector('#next'), html: 'Overwrite
', raw: 'Overwrite' }); return { ...rejected, next: document.querySelector('#next').innerText }; }); assert.deepEqual(result, { changed: false, accepted: false, next: 'Notes' }); }); test('deduplication retains an earlier round’s thinking node without a second answer', async () => { const result = await run('Notes
Notes
', raw: 'Notes' }); return { sameThinking: thinking === document.querySelector('.thinking-section'), answers: [...document.querySelectorAll('.body p')].map(el => el.innerText) }; }); assert.deepEqual(result, { sameThinking: true, answers: ['Notes'] }); }); test('detached turn rejects late saved responses', async () => { const result = await run('Stale
', raw: 'Stale' }); return { ...result, text: body.innerText }; }); assert.deepEqual(result, { changed: false, accepted: false, text: 'Original' }); }); test('a changed saved answer patches only the answer and preserves viewport anchor', async () => { const result = await run('Corrected answer
', raw: 'Corrected answer' }); return { sameThread: thread === document.querySelector('.agent-thread'), sameBody: body === document.querySelector('.body'), sameScroll: scrollY === scroll, visible: body.innerText }; }); assert.deepEqual(result, { sameThread: true, sameBody: true, sameScroll: true, visible: 'Corrected answer' }); }); test('structured ownership replaces all turn prose but preserves other turns and tool nodes', async () => { const result = await run('Draft list
Canonical
', raw: 'Canonical', render_owner: 'structured' }); const paragraph = body.firstChild; const events = [ { delta: 'Late prose', render_owner: 'streamed' }, { delta: 'Late prose', render_owner: 'structured' }, { delta: 'Legacy prose' }, { type: 'final_response', content: 'Conflicting answer', render_owner: 'streamed' }, { type: 'final_response', content: 'Legacy answer' }, { type: 'agent_step', round: 2 }, { type: 'teacher_takeover' }, ]; const decisions = events.map(event => { const accepted = owner.accepts(event); if (accepted) body.append(event.delta || event.content || 'New round'); return accepted; }); const conflictingRender = owner.render({ body, html: 'Overwrite
', raw: 'Overwrite', render_owner: 'streamed' }); return { decisions, conflictingRender, same: paragraph === body.firstChild, text: body.innerText }; }); assert.deepEqual(result, { decisions: Array(7).fill(false), conflictingRender: { changed: false, accepted: false }, same: true, text: 'Canonical' }); }); test('repeated structured snapshots are idempotent and a newer snapshot can correct the answer', async () => { const result = await run('Canonical
', raw: 'Canonical', render_owner: 'structured' }; owner.render(first); const paragraph = body.firstChild; const repeated = owner.render(first); const same = paragraph === body.firstChild; const corrected = owner.render({ ...first, html: 'Corrected
', raw: 'Corrected' }); const saved = owner.render({ body, html: 'Corrected
', raw: 'Corrected', messageId: '42' }); return { repeated, same, corrected, saved, text: body.innerText, id: body.parentElement.dataset.dbId }; }); assert.deepEqual(result, { repeated: { changed: false, accepted: true }, same: true, corrected: { changed: true, accepted: true }, saved: { changed: false, accepted: true }, text: 'Corrected', id: '42' }); }); test('streamed ownership preserves distinct rounds and structured ownership does not leak to another turn', async () => { const result = await run('Answer
', raw: 'Answer', render_owner: 'streamed' }); const preamble = root.innerText.includes('Preamble'); owner.render({ body, html: 'Canonical
', raw: 'Canonical', render_owner: 'structured' }); const start = document.createComment('live-stream-turn'); root.append(start); const next = createTurnRendering({ root, start }); return { streamed, preamble, old: owner.accepts({ delta: 'old' }), next: next.accepts({ delta: 'New turn', render_owner: 'streamed' }) }; }); assert.deepEqual(result, { streamed: true, preamble: true, old: false, next: true }); }); test('explicit turn replacement resumes legitimate synthesis after an intermediate structured answer', async () => { const result = await run('Intermediate notes
', raw: 'Intermediate notes', render_owner: 'structured' }); const first = { delta: 'Synthesis ', render_owner: 'streamed', replacement_scope: 'turn' }; const resumed = owner.accepts(first); if (resumed) { owner.render({ body, html: '', raw: '', render_owner: first.render_owner, replacement_scope: first.replacement_scope }); body.append(first.delta); } const second = { delta: 'continues', render_owner: 'streamed' }; const appended = owner.accepts(second); if (appended) body.append(second.delta); return { resumed, appended, text: body.innerText, sameThread: thread === document.querySelector('.agent-thread') }; }); assert.deepEqual(result, { resumed: true, appended: true, text: 'Synthesis continues', sameThread: true }); }); test('a scoped streamed final can replace structured content but thinking cannot transfer ownership', async () => { const result = await run('Notes
', raw: 'Notes', render_owner: 'structured' }); const thinking = owner.accepts({ delta: 'Reasoning', thinking: true, render_owner: 'streamed', replacement_scope: 'turn' }); const rendered = owner.render({ body, html: 'Synthesis
', raw: 'Synthesis', render_owner: 'streamed', replacement_scope: 'turn' }); return { thinking, rendered, text: body.innerText }; }); assert.deepEqual(result, { thinking: false, rendered: { changed: true, accepted: true }, text: 'Synthesis' }); }); test('settling ordinary prose clears every turn marker without replacing streamed nodes or touching the next turn', async () => { const result = await run('First round
tailFinal round
Old draft
Canonical
Canonical
', raw: 'Canonical', render_owner: 'structured' }); return { rendered, same: paragraph === body.firstChild, markers: document.querySelectorAll('.streaming').length, text: document.querySelector('#history').textContent }; }); assert.deepEqual(result, { rendered: { changed: false, accepted: true }, same: true, markers: 0, text: 'ReasoningToolsCanonical' }); }); test('terminal cleanup removes transient rounds introduced after a structured snapshot', async () => { const result = await run('Canonical
', raw: 'Canonical', render_owner: 'structured' }); const canonical = body.firstChild; const transient = document.createElement('div'); transient.className = 'msg msg-ai streaming'; transient.innerHTML = '