import test from 'node:test'; import assert from 'node:assert/strict'; import { parseTmuxPanes } from '../src/services/tmux.js'; test('parseTmuxPanes maps explicit claude and codex panes to engines', () => { const output = [ 'main\t0\tclaude:kanban\t%3\tclaude\t/home/gahow/projects/kanban', 'main\t1\tcodex:vllm\t%5\tcodex\t/home/gahow/projects/vllm', 'main\t2\tshell\t%7\tbash\t/home/gahow' ].join('\n'); assert.deepEqual(parseTmuxPanes(output), [ { engine: 'claude', session: 'main', windowIndex: 0, windowName: 'claude:kanban', paneId: '%3', cwd: '/home/gahow/projects/kanban' }, { engine: 'codex', session: 'main', windowIndex: 1, windowName: 'codex:vllm', paneId: '%5', cwd: '/home/gahow/projects/vllm' } ]); }); test('parseTmuxPanes drops unrelated commands', () => { const output = [ 'dev\t0\teditor\t%1\tvim\t/home/gahow', 'dev\t1\tmonitor\t%2\thtop\t/home/gahow' ].join('\n'); assert.deepEqual(parseTmuxPanes(output), []); }); test('parseTmuxPanes hides temporary web terminal sessions', () => { const output = [ 'main\t1\tcodex:kanban\t%5\tcodex\t/home/gahow/projects/kanban', 'web-1234-5678\t1\tcodex:kanban\t%5\tcodex\t/home/gahow/projects/kanban', 'web-development\t2\tcodex:web\t%6\tcodex\t/home/gahow/projects/web' ].join('\n'); assert.deepEqual(parseTmuxPanes(output), [ { engine: 'codex', session: 'main', windowIndex: 1, windowName: 'codex:kanban', paneId: '%5', cwd: '/home/gahow/projects/kanban' }, { engine: 'codex', session: 'web-development', windowIndex: 2, windowName: 'codex:web', paneId: '%6', cwd: '/home/gahow/projects/web' } ]); }); test('parseTmuxPanes handles empty output', () => { assert.deepEqual(parseTmuxPanes(''), []); assert.deepEqual(parseTmuxPanes(null), []); }); test('parseTmuxPanes ignores malformed lines', () => { assert.deepEqual(parseTmuxPanes('garbage-without-tabs\n\n'), []); }); function pane(command = 'node', pid = 100, name = 'renamed') { return `main\t0\t${name}\t%1\t${command}\t/tmp\t${pid}`; } function processTable(command, { foreground = 101, background = '', child = '' } = {}) { return `100 1 100 ${foreground} /bin/bash\n101 100 101 ${foreground} ${command}\n${background}\n${child}`; } test('ordinary Node servers and agent names in arguments/window names are not agents', () => { for (const command of [ 'node /srv/server.js codex claude omp', 'node /srv/codex/server.js', 'node -e "codex"', 'node --title codex /srv/server.js', 'node -ecodex codex', '/bin/bash -c codex', 'bun run /srv/server.ts omp' ]) { assert.deepEqual(parseTmuxPanes(pane('node', 100, 'codex:server'), processTable(command)), []); } assert.deepEqual(parseTmuxPanes(pane('node')), []); }); test('recognizes native and runtime agent entrypoints regardless of window name', () => { for (const [command, engine] of [ ['/usr/local/bin/codex --model example', 'codex'], ['node /opt/node_modules/@openai/codex/bin/codex.js', 'codex'], ['node /home/user/.nvm/versions/node/v24.13.1/bin/codex', 'codex'], ['node --require preload.js /opt/node_modules/@anthropic-ai/claude-code/cli.js', 'claude'], ['/usr/local/bin/claude', 'claude'], ['/home/user/.local/share/claude/versions/2.1.241', 'claude'], ['/home/user/.qoder/bin/qodercli/qodercli-1.1.65', 'qoder'], ['qodercli', 'qoder'], ['/usr/local/bin/omp', 'omp'], ['bun /opt/node_modules/@oh-my-pi/pi-coding-agent/src/cli.ts', 'omp'], ['node /opt/node_modules/@oh-my-pi/pi-coding-agent/dist/cli.js', 'omp'] ]) { const agents = parseTmuxPanes(pane(), processTable(command)); assert.equal(agents.length, 1, command); assert.equal(agents[0].engine, engine, command); assert.equal(agents[0].windowName, 'renamed'); } }); test('only inspects this pane and its foreground job, ignoring stopped/background agents', () => { const output = processTable('node /srv/server.js', { background: '102 100 102 101 codex\n200 1 200 201 /bin/bash\n201 200 201 201 claude' }); assert.deepEqual(parseTmuxPanes(pane(), output), []); assert.deepEqual(parseTmuxPanes(pane(), processTable('codex', { foreground: 100 })), []); }); test('keeps the parent agent visible while it runs a foreground tool', () => { const output = processTable('codex', { foreground: 102, child: '102 101 102 102 node /srv/server.js' }); assert.equal(parseTmuxPanes(pane('node'), output)[0].engine, 'codex'); }); test('uses exact Linux argv to distinguish runtime entrypoint from prompt text', () => { const argv = new Map([[101, ['node', '-e', 'console.log("hello")', 'codex']]]); assert.deepEqual(parseTmuxPanes(pane(), processTable('node -e console.log("hello") codex'), argv), []); argv.set(101, ['bun', '/opt/space dir/node_modules/@oh-my-pi/pi-coding-agent/src/cli.ts']); assert.equal(parseTmuxPanes(pane(), processTable('bun /opt/space dir/...'), argv)[0].engine, 'omp'); }); test('expired pane PIDs and empty process snapshots do not reuse stale command names', () => { assert.deepEqual(parseTmuxPanes(pane('codex'), ''), []); assert.deepEqual(parseTmuxPanes(pane('codex', 999), processTable('codex')), []); });