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 { scan, markSeen, deriveState } from '../src/scan.mjs'; test('explicit request, Seen, ordinary completion and a new request have distinct attention states', () => { const root = mkdtempSync(join(tmpdir(), 'board-attention-flow-')); try { const sessionsDir = join(root, 'sessions'); mkdirSync(sessionsDir); const file = join(sessionsDir, 's.jsonl'); writeFileSync(file, JSON.stringify({ type: 'session', id: 'fixture', cwd: root, timestamp: '2026-09-13T00:00:00Z' }) + '\n'); const specs = [{ agent: 'researcher', project: 'fixture', sessionsDir, tmux: {} }]; const boardDir = join(root, 'board'); const get = () => scan(specs, { boardDir, isAlive: () => true }).sessions[0]; const append = (second, text) => appendFileSync(file, JSON.stringify({ type: 'message', timestamp: `2026-09-13T00:00:0${second}Z`, message: { role: 'assistant', stopReason: 'stop', content: [{ type: 'text', text }] }, }) + '\n'); append(1, 'Input needed: Choose A or B.'); let r = get(); assert.equal(r.state, 'waiting'); assert.equal(r.waitingOnYou, true); markSeen(boardDir, { project: 'fixture', agent: 'researcher', lastActivity: r.lastActivity, seen: true }); r = get(); assert.equal(r.state, 'waiting', 'acknowledgment does not resolve an input request'); assert.equal(r.seen, true); assert.equal(r.waitingOnYou, false); append(2, 'BOARD_REPLY_OK'); r = get(); assert.equal(r.state, 'idle'); assert.equal(r.waitingOnYou, false); assert.equal(r.seen, false, 'ordinary completion needs no Seen action'); append(3, 'Input needed: Confirm the revised choice.'); r = get(); assert.equal(r.state, 'waiting'); assert.equal(r.waitingOnYou, true, 'old Seen does not suppress a new request'); } finally { rmSync(root, { recursive: true, force: true }); } }); test('attention convention ignores reasoning/quoted examples and permits leading blank lines', () => { const derive = (content, alive = true) => deriveState({ alive, session: { lastMessage: { role: 'assistant', stopReason: 'stop', content } } }); const text = (value) => [{ type: 'text', text: value }]; for (const value of ['> Input needed: Choose A.', ' Input needed: Choose A.', '`Input needed: Choose A.`', 'Input needed:\tChoose A.']) { assert.equal(derive(text(value)), 'idle', value); } assert.equal(derive([{ type: 'thinking', thinking: 'Input needed: Choose A.' }, ...text('Done.')]), 'idle'); assert.equal(derive(text('\n\r\nInput needed: Choose A.')), 'waiting'); assert.equal(derive(text('Input needed: Choose A.'), false), 'offline'); assert.equal(derive(text('Input needed: Choose A.'), null), 'unknown'); });