← back to Desktop Dotbar

test/jev-dots.test.js

172 lines

'use strict';
// Verification for jev-dots: classification (builtin $0), cache-gate, spend cap
// (fail-closed), fallback, and a negative fault-injection test. Zero deps.
const test = require('node:test');
const assert = require('node:assert');
const jev = require('../jev-dots');

const row = (tty, color, label, variant) => ({ tty, color, label, variant: variant || '', live: true, parked: false });
const todayLine = (usd) => JSON.stringify({ app: 'desktop-dotbar', cost_usd: usd, ts: new Date().toISOString() });
// A real paid-call ledger row carries app+api+cost (see defaultLogSpend / cost-tracker log.js).
const paidLine = () => JSON.stringify({ app: 'desktop-dotbar', api: 'typesafe_jev', cost_usd: 0.001, ts: new Date().toISOString() });
const nPaidLines = (n) => Array.from({ length: n }, paidLine).join('\n');

test('builtin classifies synthetic states to sane colors', () => {
  const cases = [
    [row('t1', 'green', 'TK-1 · 2 pastes waiting'), 'orange'],   // label overrides heuristic
    [row('t2', 'none', 'parked — handed off', 'stopped'), 'pink'],
    [row('t3', 'green', 'monitoring · next 14:00'), 'green'],
    [row('t4', 'purple', 'gated · 1 memo in pending-approval'), 'purple'],
    [row('t5', 'none', 'needs Steve to approve the login'), 'lightblue'],
    [row('t6', 'yellow', '1 question — which approach'), 'yellow'],
    [row('t7', 'green', 'building the importer'), 'green'],
  ];
  for (const [r, expect] of cases) {
    const res = jev.classifyBuiltin(jev.extractState(r));
    assert.equal(res.color, expect, `${r.label} -> ${res.color}, expected ${expect}`);
    assert.equal(res.source, 'builtin');
    assert.ok(res.confidence > 0 && res.confidence <= 1);
  }
});

test('cache-gate: unchanged state does NOT reclassify on later refreshes', async () => {
  jev._resetForTest();
  const rows = [row('a', 'green', 'working'), row('b', 'purple', 'gated · 1'), row('c', 'none', 'parked', 'stopped')];
  await jev.classifyDots(rows);
  assert.equal(jev.getStats().classifications, 3, 'first pass classifies all 3');
  await jev.classifyDots(rows);
  await jev.classifyDots(rows);
  const s = jev.getStats();
  assert.equal(s.classifications, 3, 'no new classifications when state unchanged');
  assert.equal(s.cacheHits, 6, 'both later passes were pure cache hits');
});

test('negative/fault-injection: a REAL state change reclassifies (stale hash cannot hide it)', async () => {
  jev._resetForTest();
  const before = [row('a', 'green', 'working')];
  await jev.classifyDots(before);
  assert.equal(jev.getStats().classifications, 1);
  const after = [row('a', 'orange', 'TK-9 · 1 paste')];   // same tty, changed state
  const map = await jev.classifyDots(after);
  assert.equal(jev.getStats().classifications, 2, 'changed state forces a reclassify');
  assert.equal(map.get('a').color, 'orange', 'color follows the new state, not the stale cache');
});

test('paid OFF by default: no paid calls, everything via $0 builtin', async () => {
  jev._resetForTest();
  const map = await jev.classifyDots([row('a', 'green', 'working')]); // no opts, env unset
  const s = jev.getStats();
  assert.equal(s.paidCalls, 0);
  assert.equal(s.builtinCalls, 1);
  assert.equal(map.get('a').source, 'builtin');
});

test('cap fail-closed: cap reached -> paid transport is NEVER called, falls back to builtin', async () => {
  jev._resetForTest();
  let paidAttempts = 0;
  const spyTransport = async () => { paidAttempts++; return { color: 'green', source: 'typesafe', confidence: 0.9 }; };
  const map = await jev.classifyDots([row('a', 'purple', 'gated · 1'), row('b', 'green', 'working')], {
    paidEnabled: true, capUsd: 5, readLedger: () => todayLine(5.0), paidTransport: spyTransport,
  });
  const s = jev.getStats();
  assert.equal(paidAttempts, 0, 'no paid attempt once cap is reached');
  assert.ok(s.capBlocked >= 2, 'cap-block counted for each miss');
  assert.equal(s.paidCalls, 0);
  assert.ok(map.get('a').color, 'still shows a color (builtin fallback)');
});

test('ledger-read failure is fail-closed (Infinity spend -> no paid)', async () => {
  jev._resetForTest();
  let paidAttempts = 0;
  await jev.classifyDots([row('a', 'green', 'working')], {
    paidEnabled: true, capUsd: 5, readLedger: () => { throw new Error('ledger unreadable'); },
    paidTransport: async () => { paidAttempts++; return { color: 'green', source: 'typesafe' }; },
  });
  assert.equal(paidAttempts, 0, 'unreadable ledger must not enable spend');
  assert.ok(jev.getStats().capBlocked >= 1);
});

test('paid fallback: transport error -> builtin color, never blank, never throws', async () => {
  jev._resetForTest();
  const map = await jev.classifyDots([row('a', 'purple', 'gated · 1 memo')], {
    paidEnabled: true, capUsd: 5, readLedger: () => '', // $0 spent -> under cap -> paid attempted
    paidTransport: async () => { throw new Error('boom'); },
  });
  const s = jev.getStats();
  assert.equal(s.paidFellBack, 1);
  assert.equal(map.get('a').color, 'purple', 'fell back to a real builtin color');
  assert.equal(map.get('a').source, 'builtin');
});

test('COUNT-CAP fail-closed: count >= cap -> paid transport is NEVER called, falls back to builtin', async () => {
  jev._resetForTest();
  let paidAttempts = 0;
  const spyTransport = async () => { paidAttempts++; return { color: 'green', source: 'typesafe', confidence: 0.9 }; };
  // capUsd high so the DOLLAR cap can't be the blocker; count cap of 3 with 3 rows already logged.
  const map = await jev.classifyDots([row('a', 'purple', 'gated · 1'), row('b', 'green', 'working')], {
    paidEnabled: true, capUsd: 100, maxPaidCallsPerDay: 3, readLedger: () => nPaidLines(3),
    paidTransport: spyTransport, logSpend: () => {},
  });
  const s = jev.getStats(() => nPaidLines(3)); // same source the cap read -> displayed == enforced
  assert.equal(paidAttempts, 0, 'no paid attempt once the call-count cap is reached');
  assert.ok(s.countCapBlocked >= 2, 'count-cap block counted for each miss');
  assert.equal(s.capBlocked, 0, 'the $ cap was NOT the binding limit here');
  assert.equal(s.paidCalls, 0);
  assert.equal(s.paidCallsToday, 3, 'reads the paid-call count from the ledger');
  assert.ok(map.get('a').color && map.get('b').color, 'still shows colors (builtin fallback)');
});

test('COUNT-CAP boundary + burst: one call under cap fires, then the reserve blocks the next in the SAME refresh', async () => {
  jev._resetForTest();
  let paidAttempts = 0;
  // cap=2, 1 paid row already today -> exactly ONE call of headroom. Two misses in one refresh:
  // first fires (count 1<2), reserves to 2, second is blocked by the in-loop reservation.
  const map = await jev.classifyDots([row('a', 'yellow', 'needs direction'), row('b', 'purple', 'gated · 1')], {
    paidEnabled: true, capUsd: 100, maxPaidCallsPerDay: 2, readLedger: () => nPaidLines(1),
    paidTransport: async () => { paidAttempts++; return { color: 'green', source: 'typesafe', confidence: 0.9 }; },
    logSpend: () => {}, // MOCK — never touch the real cost ledger from a test
  });
  const s = jev.getStats();
  assert.equal(paidAttempts, 1, 'exactly one paid call fires before the reserve hits the cap');
  assert.equal(s.paidCalls, 1);
  assert.equal(s.countCapBlocked, 1, 'the second miss in the same refresh is count-cap blocked');
  assert.ok(map.get('a') && map.get('b'));
});

test('countPaidCallsToday is fail-closed: a ledger-read throw returns Infinity (treated as at-cap)', () => {
  assert.equal(jev.countPaidCallsToday(() => { throw new Error('ledger unreadable'); }), Infinity);
  assert.equal(jev.countPaidCallsToday(() => ''), 0, 'empty ledger = 0 paid calls');
  assert.equal(jev.countPaidCallsToday(() => nPaidLines(4)), 4, 'counts only app+api+today rows');
  // A dollar-only synthetic row (no api) must NOT count as a paid call.
  assert.equal(jev.countPaidCallsToday(() => todayLine(5.0)), 0, 'a $-only row without api=typesafe_jev is not a paid call');
});

test('paid used when armed + under BOTH caps + transport ok, and logs the spend', async () => {
  jev._resetForTest();
  let logged = 0;
  const map = await jev.classifyDots([row('a', 'yellow', 'needs direction')], {
    paidEnabled: true, capUsd: 5, readLedger: () => '',
    paidTransport: async () => ({ color: 'green', source: 'typesafe', confidence: 0.88 }),
    logSpend: () => { logged++; },
  });
  const s = jev.getStats();
  assert.equal(s.paidCalls, 1);
  assert.equal(logged, 1, 'every paid call is logged to the cost ledger');
  assert.equal(map.get('a').color, 'green');
  assert.equal(map.get('a').source, 'typesafe');
});

test('observability: getStats().paidCallsToday == the ledger count the cap enforces (no drift)', () => {
  jev._resetForTest();
  // Displayed stat is derived from the SAME ledger source the count-cap reads.
  assert.equal(jev.getStats(() => nPaidLines(4)).paidCallsToday, 4, 'shows the true ledger count');
  assert.equal(jev.getStats(() => '').paidCallsToday, 0, 'empty ledger -> 0');
  assert.equal(jev.getStats(() => { throw new Error('x'); }).paidCallsToday, null, 'unreadable -> null (cap treats as at-cap)');
});

test('classifyDots never throws on junk rows', async () => {
  jev._resetForTest();
  const map = await jev.classifyDots([null, {}, row('a', 'green', 'x'), { tty: '', color: 'green' }]);
  assert.ok(map.get('a'));
});