// tasks-board.test.mjs — Unit tests for the Tasks tab's pure column logic: // column-mapping completeness, fallback-state behaviour, and per-column // grouping/sorting correctness. // // Run with: node --test web/test/tasks-board.test.mjs import { describe, it } from 'node:test'; import assert from 'node:assert/strict'; import { TASK_COLUMNS, columnForTaskState, groupTasksByColumn, } from '../app.js'; describe('columnForTaskState', () => { it('maps every documented task state to a column', () => { assert.equal(columnForTaskState('PENDING'), 'queue'); assert.equal(columnForTaskState('QUEUED'), 'queue'); assert.equal(columnForTaskState('RUNNING'), 'running'); assert.equal(columnForTaskState('BLOCKED'), 'running'); assert.equal(columnForTaskState('READY'), 'ready'); assert.equal(columnForTaskState('FAILED'), 'interrupted'); assert.equal(columnForTaskState('TIMED_OUT'), 'interrupted'); assert.equal(columnForTaskState('CANCELLED'), 'interrupted'); assert.equal(columnForTaskState('BUDGET_EXCEEDED'), 'interrupted'); assert.equal(columnForTaskState('COMPLETED'), 'done'); }); it('falls back to queue for an unrecognized/empty state', () => { assert.equal(columnForTaskState(''), 'queue'); assert.equal(columnForTaskState(undefined), 'queue'); assert.equal(columnForTaskState('SOME_FUTURE_STATE'), 'queue'); }); it('every column key is unique and covers all 10 task states', () => { const keys = TASK_COLUMNS.map(c => c.key); assert.equal(new Set(keys).size, keys.length, 'column keys must be unique'); const allStates = TASK_COLUMNS.flatMap(c => c.states); for (const s of [ 'PENDING', 'QUEUED', 'RUNNING', 'BLOCKED', 'READY', 'FAILED', 'TIMED_OUT', 'CANCELLED', 'BUDGET_EXCEEDED', 'COMPLETED', ]) { assert.ok(allStates.includes(s), `${s} missing from any column`); } assert.equal(new Set(allStates).size, allStates.length, 'a state must map to exactly one column'); }); }); describe('groupTasksByColumn', () => { it('returns an object with a key per column, all initially empty arrays', () => { const groups = groupTasksByColumn([]); for (const col of TASK_COLUMNS) { assert.ok(Array.isArray(groups[col.key]), `groups.${col.key} should be an array`); assert.equal(groups[col.key].length, 0); } }); it('places tasks in the correct column', () => { const tasks = [ { id: '1', state: 'PENDING', created_at: '2024-01-01T00:00:00Z' }, { id: '2', state: 'RUNNING', created_at: '2024-01-02T00:00:00Z' }, { id: '3', state: 'READY', created_at: '2024-01-03T00:00:00Z' }, { id: '4', state: 'FAILED', created_at: '2024-01-04T00:00:00Z' }, { id: '5', state: 'COMPLETED', created_at: '2024-01-05T00:00:00Z' }, ]; const groups = groupTasksByColumn(tasks); assert.equal(groups.queue.length, 1); assert.equal(groups.queue[0].id, '1'); assert.equal(groups.running.length, 1); assert.equal(groups.running[0].id, '2'); assert.equal(groups.ready.length, 1); assert.equal(groups.ready[0].id, '3'); assert.equal(groups.interrupted.length, 1); assert.equal(groups.interrupted[0].id, '4'); assert.equal(groups.done.length, 1); assert.equal(groups.done[0].id, '5'); }); it('sorts queue oldest-first (ascending created_at)', () => { const tasks = [ { id: 'newer', state: 'QUEUED', created_at: '2024-02-01T00:00:00Z' }, { id: 'older', state: 'PENDING', created_at: '2024-01-01T00:00:00Z' }, ]; const groups = groupTasksByColumn(tasks); assert.deepEqual(groups.queue.map(t => t.id), ['older', 'newer']); }); it('sorts ready oldest-first (ascending created_at)', () => { const tasks = [ { id: 'newer', state: 'READY', created_at: '2024-02-01T00:00:00Z' }, { id: 'older', state: 'READY', created_at: '2024-01-01T00:00:00Z' }, ]; const groups = groupTasksByColumn(tasks); assert.deepEqual(groups.ready.map(t => t.id), ['older', 'newer']); }); it('sorts interrupted newest-first (descending created_at)', () => { const tasks = [ { id: 'older', state: 'FAILED', created_at: '2024-01-01T00:00:00Z' }, { id: 'newer', state: 'CANCELLED', created_at: '2024-02-01T00:00:00Z' }, ]; const groups = groupTasksByColumn(tasks); assert.deepEqual(groups.interrupted.map(t => t.id), ['newer', 'older']); }); it('sorts done newest-first (descending created_at)', () => { const tasks = [ { id: 'older', state: 'COMPLETED', created_at: '2024-01-01T00:00:00Z' }, { id: 'newer', state: 'COMPLETED', created_at: '2024-02-01T00:00:00Z' }, ]; const groups = groupTasksByColumn(tasks); assert.deepEqual(groups.done.map(t => t.id), ['newer', 'older']); }); it('sorts running newest-first (descending created_at)', () => { const tasks = [ { id: 'older', state: 'RUNNING', created_at: '2024-01-01T00:00:00Z' }, { id: 'newer', state: 'BLOCKED', created_at: '2024-02-01T00:00:00Z' }, ]; const groups = groupTasksByColumn(tasks); assert.deepEqual(groups.running.map(t => t.id), ['newer', 'older']); }); it('sorts a task with no created_at LAST in an ascending column (queue)', () => { const tasks = [ { id: 'no-date', state: 'PENDING' }, { id: 'has-date', state: 'QUEUED', created_at: '2024-01-01T00:00:00Z' }, ]; const groups = groupTasksByColumn(tasks); assert.deepEqual(groups.queue.map(t => t.id), ['has-date', 'no-date']); }); it('sorts a task with no created_at LAST in a descending column (interrupted)', () => { const tasks = [ { id: 'no-date', state: 'FAILED' }, { id: 'has-date', state: 'CANCELLED', created_at: '2024-01-01T00:00:00Z' }, ]; const groups = groupTasksByColumn(tasks); assert.deepEqual(groups.interrupted.map(t => t.id), ['has-date', 'no-date']); }); it('handles null/undefined gracefully', () => { const groups = groupTasksByColumn(null); for (const col of TASK_COLUMNS) { assert.equal(groups[col.key].length, 0); } }); });