import test from 'node:test' import assert from 'node:assert/strict' import { readFileSync } from 'node:fs' import { fileURLToPath } from 'node:url' import { dirname, join } from 'node:path' import vm from 'node:vm' const dir = dirname(fileURLToPath(import.meta.url)) /** * @param {string} rel * @param {string[]} names */ function loadFns(rel, names) { const src = readFileSync(join(dir, rel), 'utf8') const sandbox = { console, bareEditSgr: (cls, on) => (on ? '\x1b[2m' : ''), EDIT_ANSI_RESET: '\x1b[0m', bareEditChunkBytes: (chunk) => { if (typeof chunk === 'string') { const out = [] for (let i = 0; i < chunk.length; i++) out.push(chunk.charCodeAt(i) & 0xff) return out } return [] }, bareEditTryConsumeKey: () => null } vm.createContext(sandbox) vm.runInContext(src, sandbox) /** @type {Record} */ const api = {} for (const n of names) { assert.equal(typeof sandbox[n], 'function', 'missing ' + n) api[n] = sandbox[n] } return api } test('markdown renders headings lists and bold', () => { const api = loadFns('../lib/agent/agent-markdown.js', ['bareAgentRenderMarkdown']) const out = api.bareAgentRenderMarkdown( '# Title\n\nI use **bold** and `code`.\n\n1. One\n2. Two\n\n- Alpha\n- Beta\n', { useColor: false, width: 72 } ) assert.match(out, /Title/) assert.match(out, /bold/) assert.match(out, /code/) assert.match(out, /1\. One/) assert.match(out, /• Alpha/) }) test('markdown fences keep code body', () => { const api = loadFns('../lib/agent/agent-markdown.js', ['bareAgentRenderMarkdown']) const out = api.bareAgentRenderMarkdown('```js\nconst x = 1\n```\n', { useColor: false, width: 72 }) assert.match(out, /const x = 1/) assert.match(out, /js/) }) test('think panel scrollbar and scroll keys', () => { const api = loadFns('../lib/agent/agent-think-panel.js', [ 'bareAgentCreateThinkPanel', 'bareAgentThinkScrollbar' ]) const sb = api.bareAgentThinkScrollbar(4, 20, 0, false) assert.equal(sb.length, 4) assert.ok(sb.some((c) => c === '#')) /** @type {string[]} */ const writes = [] const panel = api.bareAgentCreateThinkPanel( { env: { COLUMNS: '72', BARE_OS_AGENT_ASCII: '1' } }, { isTTY: true, columns: 72, write: () => true }, { useColor: false, bodyLines: 4, write: (_c, _o, s) => writes.push(s) } ) for (let i = 0; i < 20; i++) { panel.append('Line number ' + i + ' about tools and skills.\n') } assert.ok(writes.length >= 1) const before = writes.length panel.scrollUp(3) assert.ok(writes.length > before) panel.handleKey({ type: 'nav', key: 'pageup' }) panel.seal() assert.ok(panel.hasContent()) panel.setStatus('answering…') panel.clearStatus() }) test('term cols use full width without 120 cap', () => { const api = loadFns('../lib/agent/agent-think-panel.js', [ 'bareAgentResolveTermCols' ]) const w = api.bareAgentResolveTermCols( { env: { COLUMNS: '80' } }, { columns: 200 } ) assert.equal(w, 200) const w2 = api.bareAgentResolveTermCols({ env: { COLUMNS: '160' } }, {}) assert.equal(w2, 160) }) test('empty think panel never draws', () => { const api = loadFns('../lib/agent/agent-think-panel.js', [ 'bareAgentCreateThinkPanel' ]) /** @type {string[]} */ const writes = [] const panel = api.bareAgentCreateThinkPanel( { env: { COLUMNS: '100' } }, { isTTY: true, columns: 100, write: () => true }, { useColor: false, bodyLines: 4, write: (_c, _o, s) => writes.push(s) } ) panel.append('\n\n \n') panel.seal() panel.setStatus('should not show') assert.equal(writes.length, 0) assert.equal(panel.hasContent(), false) panel.append('real thought') assert.ok(writes.length >= 1) assert.ok(panel.hasContent()) }) test('think tag splitter still works', () => { const api = loadFns('../lib/agent/agent-think-panel.js', [ 'bareAgentCreateThinkTagSplitter' ]) /** @type {string[]} */ const think = [] /** @type {string[]} */ const content = [] const split = api.bareAgentCreateThinkTagSplitter({ onThink: (s) => think.push(s), onContent: (s) => content.push(s) }) split.push('plan\nAnswer **here**') split.flush() assert.equal(think.join(''), 'plan') assert.equal(content.join(''), '\nAnswer **here**') })