import test from 'brittle' import { readFile } from 'node:fs/promises' import { dirname, join } from 'node:path' import { fileURLToPath } from 'node:url' import { attachBareOsTuiSdk } from '../../bare-os-booter/lib/bare-os-tui-sdk.js' const __dirname = dirname(fileURLToPath(import.meta.url)) const libPath = join(__dirname, '../lib/chat-tui.js') async function loadChatTui() { const src = await readFile(libPath, 'utf8') const hook = {} new Function( 'exports', src + '\nexports.bareChatCreateTuiApp = bareChatCreateTuiApp;\nexports.bareOsRunChatTui = bareOsRunChatTui;\nexports.bareChatTuiRunOpts = bareChatTuiRunOpts;\n' )(hook) return hook } function makeStream(isTTY, writes) { return { isTTY: !!isTTY, raw: false, columns: 80, rows: 24, write(s) { writes.push(String(s)) }, setRawMode(v) { this.raw = !!v }, resume() {}, pause() {}, on() {}, removeListener() {} } } function makeCtx(extra) { const writes = [] const stdin = makeStream(true, writes) const stdout = makeStream(true, writes) const ctx = { env: { BARE_CHAT_STATUS_MS: '0' }, console: { error() {}, log() {} }, replStdin: stdin, replStdout: stdout, suspendReplForSubprocess() {}, resumeReplAfterSubprocess() {}, ...(extra || {}) } attachBareOsTuiSdk(ctx) return ctx } async function drain(app, cmd) { if (!cmd) return if (Array.isArray(cmd)) { for (const c of cmd) await drain(app, c) return } if (typeof cmd !== 'function') return const msg = await cmd() if (!msg) return const pair = app.update(msg) if (pair && pair[1]) await drain(app, pair[1]) } test('chat-tui source prefers ctx.tui.run', async (t) => { const src = await readFile(libPath, 'utf8') t.ok(src.includes('ctx.tui.run'), 'TEA path uses ctx.tui.run') t.ok(src.includes('bareChatCreateTuiApp'), 'exports TEA factory') t.ok(src.includes('bareOsRunChatTuiLegacy'), 'keeps pre-SDK fallback') }) test('chat TEA app loads history, types, and sends', async (t) => { const hook = await loadChatTui() const sent = [] const ctx = makeCtx({ bareOsChatSend(text) { sent.push(text) }, bareOsChatHistory() { return [ { body: 'hello', displayName: 'alice', tsMs: 0 } ] }, bareOsChatSubscribe() { return function () {} }, bareOsChatProcSnapshot() { return { swarmPeers: 2, metrics: { rxEvent: 1, txEvent: 3 }, protomuxChatRxTotal: 9 } }, bareOsChatRooms() { return ['general'] } }) const app = hook.bareChatCreateTuiApp(ctx, 'chat') await drain(app, app.init()) let plain = ctx.tui.style.stripAnsi(app.view()) t.ok(plain.includes('alice'), 'history line in transcript') t.ok(plain.includes('hello')) t.ok(plain.includes('peers 2'), 'proc snapshot in title') t.ok(plain.includes('rx 1')) t.ok(plain.includes('tx 3')) app.update(ctx.tui.decode('h')[0]) app.update(ctx.tui.decode('i')[0]) t.is(app.input.value, 'hi') await drain(app, app.update(ctx.tui.decode('\r')[0])[1]) t.alike(sent, ['hi']) t.is(app.input.value, '') }) test('chat TEA app help, scroll, and incoming events', async (t) => { const hook = await loadChatTui() const ctx = makeCtx({ bareOsChatHistory() { return Array.from({ length: 20 }, (_, i) => ({ body: 'm' + i, displayName: 'n', tsMs: i })) }, bareOsChatSubscribe() { return function () {} }, bareOsChatProcSnapshot() { return { swarmPeers: 0 } } }) const app = hook.bareChatCreateTuiApp(ctx, 'chat') app.height = 12 await drain(app, app.init()) t.is(app.mode, 'main') t.ok(app.stickToBottom) app.update(ctx.tui.decode('?')[0]) t.is(app.mode, 'help') const help = ctx.tui.style.stripAnsi(app.view()) t.ok(help.includes('Press any key')) app.update(ctx.tui.decode('x')[0]) t.is(app.mode, 'main') app.update(ctx.tui.decode('\x1b[A')[0]) t.absent(app.stickToBottom) t.ok(app.scrollTop < app.transcript.length) app.update({ type: 'chat.event', ev: { body: 'yo', displayName: 'bob', receivedAtMs: 99 } }) const after = ctx.tui.style.stripAnsi(app.view()) t.ok( app.transcript.some((line) => line.includes('bob') && line.includes('yo')) ) void after }) test('kernel chat bin includes ctx.tui.run after build', async (t) => { const bin = await readFile( join(__dirname, '../../../kernel/bin/chat'), 'utf8' ) t.ok(bin.includes('ctx.tui.run'), 'kernel chat uses ctx.tui.run') t.ok(bin.includes('bareChatCreateTuiApp'), 'kernel chat includes TEA factory') }) test('chat TEA app maps BARE_EDIT_NO_ALTSCREEN', async (t) => { const hook = await loadChatTui() const ctx = makeCtx() ctx.env.BARE_EDIT_NO_ALTSCREEN = '1' const opts = hook.bareChatTuiRunOpts(ctx) t.is(opts.altScreen, false) })