// stats.test.mjs — Unit tests for stats tab computation functions. // // Run with: node --test web/test/stats.test.mjs import { describe, it } from 'node:test'; import assert from 'node:assert/strict'; import { computeTaskStats, computeExecutionStats } from '../app.js'; // ── Helpers ──────────────────────────────────────────────────────────────────── function makeTask(state) { return { id: state, name: `task-${state}`, state }; } function makeExec(state, costUSD = 0, durationMs = null) { const started = new Date('2024-01-01T10:00:00Z'); const finished = durationMs != null ? new Date(started.getTime() + durationMs).toISOString() : null; return { id: `exec-${Math.random()}`, task_id: 'task-1', task_name: 'Test Task', state, started_at: started.toISOString(), finished_at: finished, duration_ms: durationMs, cost_usd: costUSD, exit_code: state === 'completed' ? 0 : 1, }; } // ── computeTaskStats ─────────────────────────────────────────────────────────── describe('computeTaskStats', () => { it('groups tasks by state', () => { const tasks = [ makeTask('RUNNING'), makeTask('RUNNING'), makeTask('PENDING'), makeTask('COMPLETED'), makeTask('COMPLETED'), makeTask('COMPLETED'), ]; const stats = computeTaskStats(tasks); assert.equal(stats.byState.RUNNING, 2); assert.equal(stats.byState.PENDING, 1); assert.equal(stats.byState.COMPLETED, 3); }); it('only includes states that have tasks', () => { const tasks = [makeTask('RUNNING')]; const stats = computeTaskStats(tasks); assert.equal(stats.byState.RUNNING, 1); assert.equal(stats.byState.PENDING, undefined); assert.equal(stats.byState.COMPLETED, undefined); }); it('returns empty byState for empty task list', () => { const stats = computeTaskStats([]); assert.deepEqual(stats.byState, {}); }); it('counts all distinct states correctly', () => { const states = ['PENDING', 'QUEUED', 'RUNNING', 'READY', 'COMPLETED', 'FAILED', 'TIMED_OUT', 'CANCELLED', 'BUDGET_EXCEEDED', 'BLOCKED']; const tasks = states.map(makeTask); const stats = computeTaskStats(tasks); for (const s of states) { assert.equal(stats.byState[s], 1, `expected count 1 for state ${s}`); } }); }); // ── computeExecutionStats ────────────────────────────────────────────────────── describe('computeExecutionStats', () => { it('returns zeros for empty executions', () => { const stats = computeExecutionStats([]); assert.equal(stats.total, 0); assert.equal(stats.successRate, 0); assert.equal(stats.totalCostUSD, 0); assert.equal(stats.avgDurationMs, null); assert.deepEqual(stats.byOutcome, {}); }); it('calculates total correctly', () => { const execs = [makeExec('completed'), makeExec('failed'), makeExec('cancelled')]; const stats = computeExecutionStats(execs); assert.equal(stats.total, 3); }); it('calculates success rate as fraction of completed out of total', () => { const execs = [ makeExec('completed'), makeExec('completed'), makeExec('completed'), makeExec('failed'), ]; const stats = computeExecutionStats(execs); assert.equal(stats.successRate, 0.75); }); it('returns success rate 0 when all executions failed', () => { const execs = [makeExec('failed'), makeExec('failed')]; const stats = computeExecutionStats(execs); assert.equal(stats.successRate, 0); }); it('returns success rate 1 when all executions completed', () => { const execs = [makeExec('completed'), makeExec('completed')]; const stats = computeExecutionStats(execs); assert.equal(stats.successRate, 1); }); it('sums total cost correctly', () => { const execs = [makeExec('completed', 0.5), makeExec('completed', 0.25), makeExec('failed', 0.1)]; const stats = computeExecutionStats(execs); assert.ok(Math.abs(stats.totalCostUSD - 0.85) < 0.0001, `expected 0.85, got ${stats.totalCostUSD}`); }); it('calculates average duration from executions with duration_ms', () => { const execs = [ makeExec('completed', 0, 60000), // 1 min makeExec('completed', 0, 120000), // 2 min makeExec('failed', 0, 30000), // 30 sec ]; const stats = computeExecutionStats(execs); assert.equal(stats.avgDurationMs, 70000); // (60000+120000+30000)/3 }); it('ignores executions without duration_ms in avg calculation', () => { const execs = [ makeExec('completed', 0, 60000), makeExec('running', 0, null), // still running, no duration ]; const stats = computeExecutionStats(execs); assert.equal(stats.avgDurationMs, 60000); }); it('returns null avgDurationMs when no executions have duration_ms', () => { const execs = [makeExec('running', 0, null)]; const stats = computeExecutionStats(execs); assert.equal(stats.avgDurationMs, null); }); it('groups executions by outcome in byOutcome', () => { const execs = [ makeExec('completed'), makeExec('completed'), makeExec('failed'), makeExec('cancelled'), ]; const stats = computeExecutionStats(execs); assert.equal(stats.byOutcome.completed, 2); assert.equal(stats.byOutcome.failed, 1); assert.equal(stats.byOutcome.cancelled, 1); }); });