← 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;/);
});