← back to Desktop Dotbar
test/live-filter.test.js
145 lines
'use strict';
// TK-12236: the PARKED panel shows only LIVE rows. Negative tests inject a dead pid, a recycled
// pid, a closed ticket and a probe failure, and assert the right row is hidden / kept / marked.
const fs = require('node:fs');
const path = require('node:path');
const test = require('node:test');
const assert = require('node:assert');
const lf = require('../live-filter');
const PS = [
' 100 ttys020 claude',
' 200 ttys004 claude bg-spare',
' 300 ttys011 /usr/bin/vim', // pid 300 was a claude once; now recycled to vim
' 400 ttys030 claude', // a claude, but on a different tty than recorded
' 500 ttys021 claude', // holds ttys021 (for the no-pid fallback)
' 600 ?? /usr/sbin/cfprefsd',
' 1634 ttys002 /Users/x/.npm-global/lib/node_modules/@anthropic-ai/claude-code/bin/claude.exe',
' 1700 ttys040 node',
' 1800 ttys041 /bin/zsh',
].join('\n');
const procs = lf.parsePs(PS);
const tickets = lf.ticketStatusMap([
{ id: 'TK-11155-ios-fleet', status: 'doing' },
{ id: 'TK-12103-phillipe-romano', status: 'blocked' },
{ id: 'TK-9001-finished', status: 'done' },
{ id: 'TK-9002-cancelled', status: 'cancelled' },
]);
const tab = (tty, pid) => ({ kind: 'tab', id: tty, tty, pid, label: `TK-1 · ${tty}` });
const tkt = (id, tty) => ({ kind: 'ticket', id, tty, label: 'parked by Steve' });
const ids = (out) => out.items.map(i => i.id);
test('parsePs keeps renamed claude comms and rejects an empty probe', () => {
assert.ok(procs.claudeTtys.has('ttys004'), '"claude bg-spare" is a claude process');
assert.ok(!procs.claudeTtys.has('ttys011'), 'vim is not claude');
assert.equal(lf.parsePs(''), null, 'empty ps output is NOT-MEASURED, not "nothing alive"');
assert.equal(lf.ticketStatusMap([]), null, 'empty board is NOT-MEASURED (0 of 0)');
});
test('NEGATIVE: dead pid, recycled pid, and claude-on-another-tty are all hidden', () => {
const out = lf.buildParked([
tab('ttys020', 100), // live
tab('ttys016', 999), // pid gone
tab('ttys011', 300), // pid recycled to vim
tab('ttys012', 400), // pid is a claude, but on ttys030 -> recycled
], { procs, tickets });
assert.deepEqual(ids(out), ['ttys020']);
assert.deepEqual(out.hidden.map(h => h.id).sort(), ['ttys011', 'ttys012', 'ttys016']);
assert.equal(out.items[0].liveness, 'live');
});
test('tab with no pid falls back to "some claude holds that tty"', () => {
const out = lf.buildParked([tab('ttys021'), tab('ttys099')], { procs, tickets });
assert.deepEqual(ids(out), ['ttys021']);
});
test('parked TICKETS are NEVER hidden (TK-12174 / always-show rule): open, closed, off-board all shown', () => {
const out = lf.buildParked([
tkt('TK-11155', 'ttys026'), tkt('TK-12103', 'ttys016'),
tkt('TK-9001', 'ttys026'), tkt('TK-9002', 'ttys026'), tkt('TK-4', 'ttys026'),
], { procs, tickets });
assert.deepEqual(ids(out), ['TK-11155', 'TK-12103', 'TK-9001', 'TK-9002', 'TK-4']);
assert.equal(out.hidden.length, 0, 'no ticket park is ever hidden');
const by = Object.fromEntries(out.items.map(i => [i.id, i]));
assert.equal(by['TK-11155'].liveness, 'live');
assert.equal(by['TK-9001'].liveness, 'closed'); assert.equal(by['TK-9001'].status, 'done');
assert.equal(by['TK-4'].liveness, 'unknown', 'not in board summary = NOT-MEASURED, shown');
for (const i of out.items) {
assert.equal(i.tty, '', 'ticket tty is provenance, never rendered as a session');
assert.equal(i.attached, undefined, 'no orphan/attached state on a ticket park');
}
});
test('FAIL-OPEN: probe failures show the row with liveness unknown (never hidden, never live)', () => {
const out = lf.buildParked([tab('ttys016', 999), tkt('TK-9001', 'ttys026')], { procs: null, tickets: null });
assert.deepEqual(ids(out), ['ttys016', 'TK-9001']);
for (const i of out.items) assert.equal(i.liveness, 'unknown');
assert.equal(out.hidden.length, 0);
});
test('live tab without an iTerm2 tab keeps attached:false (renders "running · no tab")', () => {
const out = lf.buildParked([tab('ttys020', 100)], { procs, tickets, isAttached: () => false });
assert.equal(out.items[0].attached, false);
});
test('parked chip count == panel row count (payload + UI read the same array)', () => {
const fixtures = [
[tab('ttys020', 100), tab('ttys016', 999), tkt('TK-11155'), tkt('TK-9001')],
[],
[tab('ttys016', 999)],
];
for (const f of fixtures) {
for (const probes of [{ procs, tickets }, { procs: null, tickets: null }]) {
const out = lf.buildParked(f, probes);
assert.equal(out.count, out.items.length);
}
}
const html = fs.readFileSync(path.join(__dirname, '..', 'public', 'index.html'), 'utf8');
assert.match(html, /const pItems = \(data\.parked && data\.parked\.items\) \|\| \[\];/);
assert.match(html, /<span class="cnt">\$\{pItems\.length\}<\/span>/, 'chip count must be the panel item array length');
assert.match(html, /const items = \(data\.parked && data\.parked\.items\) \|\| \[\];/, 'panel renders data.parked.items');
});
test('NEGATIVE: an UNCLAIMED daemon spare (tty ??) never makes a tty or pid row live', () => {
const p = lf.parsePs([' 700 ?? claude bg-spare --bg-spare /tmp/x.claim.sock',
' 701 ?? claude bg-pty-host --bg-pty-host /tmp/x.pty.sock',
' 702 ttys099 /bin/zsh'].join('\n'));
assert.ok(!p.claudeTtys.has('??'), 'unclaimed spares do not populate claudeTtys');
const out = lf.buildParked([tab('ttys099'), tab('ttys098', 700)], { procs: p, tickets });
assert.deepEqual(ids(out), [], 'no-pid row on a spare-less tty and a pid that is an unclaimed spare elsewhere are both hidden');
});
test('REGRESSION: native-installer comm ".../bin/claude.exe" counts as live (was hidden as "recycled")', () => {
assert.ok(lf.isClaudeComm('/Users/x/.npm-global/lib/node_modules/@anthropic-ai/claude-code/bin/claude.exe'));
assert.ok(!lf.isClaudeComm('/usr/bin/vim'));
assert.ok(!lf.isClaudeComm('/bin/zsh'));
const out = lf.buildParked([tab('ttys002', 1634)], { procs, tickets });
assert.deepEqual(ids(out), ['ttys002']);
assert.equal(out.items[0].liveness, 'live');
});
test('NEGATIVE (all panels): filterSessions hides a fake dead-tty row, keeps a live one, keeps unknown', () => {
const rows = [
{ tty: 'ttys020', pid: '100', color: 'green' }, // live claude
{ tty: 'ttys040', pid: '1700', color: 'yellow' }, // live node session
{ tty: 'ttys041', pid: '1800', color: 'pink' }, // pid is a bare shell -> claude gone -> dead
{ tty: 'ttys077', pid: '4242', color: 'purple' }, // FAKE dead tty: pid not running
{ tty: 'ttys078', color: 'orange' }, // no pid, no claude on tty -> dead
];
const { kept, hidden } = lf.filterSessions(rows, procs);
assert.deepEqual(kept.map(r => r.tty), ['ttys020', 'ttys040']);
assert.ok(kept.every(r => r.liveness === 'live'));
assert.deepEqual(hidden.map(h => h.tty).sort(), ['ttys041', 'ttys077', 'ttys078']);
// UNKNOWN liveness (ps probe failed) -> every row shown, none hidden (fail-open)
const blind = lf.filterSessions(rows, null);
assert.equal(blind.kept.length, rows.length);
assert.equal(blind.hidden.length, 0);
assert.ok(blind.kept.every(r => r.liveness === 'unknown'));
});
test('server wires filterSessions into the colour groups (not just PARKED)', () => {
const src = fs.readFileSync(path.join(__dirname, '..', 'server.js'), 'utf8');
assert.match(src, /liveFilter\.filterSessions\(rows, procs\)/);
assert.match(src, /rows = sess\.kept;/);
});