// Jason's 2026-09-13 report (#1507 comment 26082): sending works, the return // flow does not. This replays his sequence against the real board scanner and // WebUI: send, then the seat appends user, toolCall, toolResult and a new final // answer. The answer must appear in the SAME open inspector through auto-refresh // alone, once, without the previous answer passing as the reply, and without // losing a draft typed meanwhile. Session-file polling only: the live adapters // and streaming belong to CHAT-03, not this test. import { test } from 'node:test'; import assert from 'node:assert/strict'; import { mkdtempSync, mkdirSync, writeFileSync, appendFileSync, rmSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { startServer as startBoard } from '../../control-board/src/serve.mjs'; import { makeRegistration, writeRegistration } from '../../seat/src/seat.mjs'; import { startServer } from '../src/serve.mjs'; import { browser } from './browser.mjs'; import { close } from './fixture.mjs'; const at = s => new Date(Date.now() + s * 1000).toISOString(); const message = (timestamp, m) => JSON.stringify({ type: 'message', timestamp, message: m }) + '\n'; const assistant = (timestamp, text) => message(timestamp, { role: 'assistant', stopReason: 'stop', content: [{ type: 'text', text }] }); test('reported return flow and relative Age: reply sent from the inspector, then the new answer appears there without manual refresh', { timeout: 90000 }, async () => { const root = mkdtempSync(join(tmpdir(), 'webui-return-')); let board, web, b; try { const sessionsDir = join(root, 'sessions'); mkdirSync(sessionsDir); const file = join(sessionsDir, 's.jsonl'); writeFileSync(file, message(at(-120), { role: 'user', content: [{ type: 'text', text: 'Start' }] }) + assistant(at(-90), 'Input needed: OLD_ANSWER')); const seatsDir = join(root, 'seats'), captures = []; writeRegistration(seatsDir, makeRegistration({ resolved: { seat: 'fixture', project: 'repo', sessionsDir, seatDir: root, launchScript: join(root, 'unused.sh'), layout: 'repo', defaultWorkspace: root }, task: 'Fixed task', tmux: { session: 'fixture', socket: null }, pid: process.pid, now: () => new Date(Date.now() - 300000) })); board = await startBoard({ port: 0, specs: [{ agent: 'fixture', project: 'repo', sessionsDir, tmux: {} }], boardDir: join(root, 'board'), seatsDir, isAlive: () => true, isPidAlive: () => true, exec: (f, args) => { captures.push(args); return { status: 0, stdout: '', stderr: '' }; } }); web = await startServer({ port: 0, board: `http://127.0.0.1:${board.address().port}` }); b = await browser(); await b.viewport(1440, 1000); const wait = (expression, ms = 20000) => b.evaluate(`(async()=>{for(const end=Date.now()+${ms};Date.now()setTimeout(r,100))}throw Error('timeout: '+${JSON.stringify(expression)})})()`); const inspection = () => b.evaluate('document.querySelector("#inspection").textContent'); await b.navigate(`http://127.0.0.1:${web.address().port}`); await wait('document.querySelector("table.sessions [data-open]")'); // Relative Age (#1507): the board's ageSeconds, labelled as time since last activity, with the timestamp kept. assert.match(await b.evaluate('document.querySelector("table.sessions tbody td:last-child").textContent'), /^1m ago\d{4}-\d\d-\d\dT/); await b.evaluate('document.querySelector("table.sessions [data-open]").click()'); assert.match(await inspection(), /OLD_ANSWER/); assert.match(await inspection(), /Last activity1m ago \(\d{4}-/); await b.evaluate('(() => { const t = document.querySelector("#reply"); t.value = "REPLY_FROM_JASON"; t.dispatchEvent(new Event("input", { bubbles: true })); document.querySelector(".reply-form").requestSubmit(); })()'); await wait('document.querySelector(".receipt")?.textContent.includes("delivered")'); assert.ok(captures.some(args => args.some(a => a.startsWith('REPLY_FROM_JASON\n'))), 'reply reached the transport'); assert.equal(await b.evaluate('document.querySelector("#reply").value'), '', 'delivered draft cleared'); // The seat takes the message and starts a tool call. No answer yet. appendFileSync(file, message(at(0), { role: 'user', content: [{ type: 'text', text: 'REPLY_FROM_JASON' }] }) + message(at(1), { role: 'assistant', stopReason: 'toolUse', content: [{ type: 'toolCall', id: 'call-1', name: 'read', arguments: { path: 'x' } }] })); await wait('[...document.querySelectorAll("#inspection dt")].find(d => d.textContent === "State")?.nextElementSibling.textContent === "working"'); const pending = await inspection(); assert.match(pending, /Waiting for a reply/, 'the page says a reply is pending'); assert.doesNotMatch(pending, /Last assistant text/, 'the previous answer is not presented as the latest reply'); // Typed while the seat works; auto-refresh must keep it and the caret. await b.evaluate('(() => { const t = document.querySelector("#reply"); t.focus(); t.value = "NEXT_DRAFT"; t.setSelectionRange(4, 4); t.dispatchEvent(new Event("input", { bubbles: true })); })()'); appendFileSync(file, message(at(2), { role: 'toolResult', toolCallId: 'call-1', toolName: 'read', content: [{ type: 'text', text: 'file body' }], isError: false }) + assistant(at(3), 'Input needed: NEW_ANSWER')); await wait('document.querySelector("#inspection").textContent.includes("NEW_ANSWER")'); const answered = await inspection(); assert.equal(answered.split('NEW_ANSWER').length, 2, 'answer shown once in the inspector'); assert.doesNotMatch(answered, /OLD_ANSWER|Waiting for a reply/); assert.equal(await b.evaluate('document.querySelector("#inspector-title").textContent'), 'fixture', 'same conversation still open'); assert.deepEqual(await b.evaluate('(() => { const t = document.querySelector("#reply"); return [t.value, t.selectionStart, document.activeElement === t]; })()'), ['NEXT_DRAFT', 4, true]); // The notice clears once. Later work with no new send must not bring back the old sentAt (Filbert R1 item 2). appendFileSync(file, message(at(4), { role: 'user', content: [{ type: 'text', text: 'typed in the seat terminal' }] })); await b.evaluate('document.querySelector("#refresh").click()'); await wait('[...document.querySelectorAll("#inspection dt")].find(d => d.textContent === "State")?.nextElementSibling.textContent === "working"'); const later = await inspection(); assert.doesNotMatch(later, /Waiting for a reply|before your message/); assert.match(later, /Last assistant text/); } finally { if (b) await b.close(); if (web) await close(web); if (board) await close(board); rmSync(root, { recursive: true, force: true }); } });