146 lines
5.2 KiB
JavaScript
146 lines
5.2 KiB
JavaScript
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')), []);
|
|
});
|