Phase 0: - Synced latest live-status.js from workspace (9928 bytes) - Fixed 43 lint issues: empty catch blocks, console statements - Added pino dependency - Created src/tool-labels.json with all known tool mappings - make check passes Phase 1 (Core Components): - src/config.js: env-var config with validation, throws on missing required vars - src/logger.js: pino singleton with child loggers, level validation - src/circuit-breaker.js: CLOSED/OPEN/HALF_OPEN state machine with callbacks - src/tool-labels.js: exact/prefix/regex tool->label resolver with external override - src/status-box.js: Mattermost post manager (keepAlive, throttle, retry, circuit breaker) - src/status-formatter.js: pure SessionState->text formatter (nested, compact) - src/health.js: HTTP health endpoint + metrics - src/status-watcher.js: JSONL file watcher (inotify, compaction detection, idle detection) Tests: - test/unit/config.test.js: 7 tests - test/unit/circuit-breaker.test.js: 12 tests - test/unit/logger.test.js: 5 tests - test/unit/status-formatter.test.js: 20 tests - test/unit/tool-labels.test.js: 15 tests All 59 unit tests pass. make check clean.
208 lines
6.2 KiB
JavaScript
208 lines
6.2 KiB
JavaScript
'use strict';
|
|
|
|
/**
|
|
* Unit tests for status-formatter.js
|
|
*/
|
|
|
|
const { describe, it } = require('node:test');
|
|
const assert = require('node:assert/strict');
|
|
|
|
const {
|
|
format,
|
|
formatElapsed,
|
|
formatTokens,
|
|
statusIcon,
|
|
truncateLine,
|
|
extractAgentId,
|
|
} = require('../../src/status-formatter');
|
|
|
|
const NOW = Date.now();
|
|
|
|
function makeState(overrides = {}) {
|
|
return {
|
|
sessionKey: 'agent:main:mattermost:channel:abc:thread:xyz',
|
|
status: 'active',
|
|
startTime: NOW - 38000, // 38s ago
|
|
lines: [],
|
|
children: [],
|
|
agentId: 'main',
|
|
depth: 0,
|
|
tokenCount: 0,
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
describe('status-formatter.js', () => {
|
|
describe('format()', () => {
|
|
it('formats active session with header', () => {
|
|
const state = makeState();
|
|
const result = format(state);
|
|
assert.ok(result.includes('[ACTIVE]'));
|
|
assert.ok(result.includes('main'));
|
|
assert.ok(result.match(/\d+s/));
|
|
});
|
|
|
|
it('formats done session with footer', () => {
|
|
const state = makeState({ status: 'done' });
|
|
const result = format(state);
|
|
assert.ok(result.includes('[DONE]'));
|
|
});
|
|
|
|
it('formats error session', () => {
|
|
const state = makeState({ status: 'error' });
|
|
const result = format(state);
|
|
assert.ok(result.includes('[ERROR]'));
|
|
});
|
|
|
|
it('formats interrupted session', () => {
|
|
const state = makeState({ status: 'interrupted' });
|
|
const result = format(state);
|
|
assert.ok(result.includes('[INTERRUPTED]'));
|
|
});
|
|
|
|
it('includes status lines', () => {
|
|
const state = makeState({
|
|
lines: ['Reading files...', ' exec: ls [OK]', 'Writing results...'],
|
|
});
|
|
const result = format(state);
|
|
assert.ok(result.includes('Reading files...'));
|
|
assert.ok(result.includes('exec: ls [OK]'));
|
|
assert.ok(result.includes('Writing results...'));
|
|
});
|
|
|
|
it('limits status lines to maxLines', () => {
|
|
const lines = Array.from({ length: 30 }, (_, i) => `Line ${i + 1}`);
|
|
const state = makeState({ lines });
|
|
const result = format(state, { maxLines: 5 });
|
|
// Only last 5 lines should appear
|
|
assert.ok(result.includes('Line 26'));
|
|
assert.ok(result.includes('Line 30'));
|
|
assert.ok(!result.includes('Line 1'));
|
|
});
|
|
|
|
it('includes token count in done footer', () => {
|
|
const state = makeState({ status: 'done', tokenCount: 12400 });
|
|
const result = format(state);
|
|
assert.ok(result.includes('12.4k'));
|
|
});
|
|
|
|
it('no token count in footer when zero', () => {
|
|
const state = makeState({ status: 'done', tokenCount: 0 });
|
|
const result = format(state);
|
|
// Should not include "tokens" for zero count
|
|
assert.ok(!result.includes('tokens'));
|
|
});
|
|
|
|
it('renders nested child sessions', () => {
|
|
const child = makeState({
|
|
sessionKey: 'agent:main:subagent:uuid-1',
|
|
agentId: 'proj035-planner',
|
|
depth: 1,
|
|
status: 'done',
|
|
lines: ['Reading protocol...'],
|
|
});
|
|
const parent = makeState({
|
|
lines: ['Starting plan...'],
|
|
children: [child],
|
|
});
|
|
const result = format(parent);
|
|
assert.ok(result.includes('proj035-planner'));
|
|
assert.ok(result.includes('Reading protocol...'));
|
|
// Child should be indented
|
|
const childLine = result.split('\n').find((l) => l.includes('proj035-planner'));
|
|
assert.ok(childLine && childLine.startsWith(' '));
|
|
});
|
|
|
|
it('active session has no done footer', () => {
|
|
const state = makeState({ status: 'active' });
|
|
const result = format(state);
|
|
const lines = result.split('\n');
|
|
// No line should contain [DONE], [ERROR], [INTERRUPTED]
|
|
assert.ok(!lines.some((l) => /\[(DONE|ERROR|INTERRUPTED)\]/.test(l)));
|
|
});
|
|
});
|
|
|
|
describe('formatElapsed()', () => {
|
|
it('formats seconds', () => {
|
|
assert.equal(formatElapsed(0), '0s');
|
|
assert.equal(formatElapsed(1000), '1s');
|
|
assert.equal(formatElapsed(59000), '59s');
|
|
});
|
|
|
|
it('formats minutes', () => {
|
|
assert.equal(formatElapsed(60000), '1m0s');
|
|
assert.equal(formatElapsed(90000), '1m30s');
|
|
assert.equal(formatElapsed(3599000), '59m59s');
|
|
});
|
|
|
|
it('formats hours', () => {
|
|
assert.equal(formatElapsed(3600000), '1h0m');
|
|
assert.equal(formatElapsed(7260000), '2h1m');
|
|
});
|
|
|
|
it('handles negative values', () => {
|
|
assert.equal(formatElapsed(-1000), '0s');
|
|
});
|
|
});
|
|
|
|
describe('formatTokens()', () => {
|
|
it('formats small counts', () => {
|
|
assert.equal(formatTokens(0), '0');
|
|
assert.equal(formatTokens(999), '999');
|
|
});
|
|
|
|
it('formats thousands', () => {
|
|
assert.equal(formatTokens(1000), '1.0k');
|
|
assert.equal(formatTokens(12400), '12.4k');
|
|
assert.equal(formatTokens(999900), '999.9k');
|
|
});
|
|
|
|
it('formats millions', () => {
|
|
assert.equal(formatTokens(1000000), '1.0M');
|
|
assert.equal(formatTokens(2500000), '2.5M');
|
|
});
|
|
});
|
|
|
|
describe('statusIcon()', () => {
|
|
it('returns correct icons', () => {
|
|
assert.equal(statusIcon('active'), '[ACTIVE]');
|
|
assert.equal(statusIcon('done'), '[DONE]');
|
|
assert.equal(statusIcon('error'), '[ERROR]');
|
|
assert.equal(statusIcon('interrupted'), '[INTERRUPTED]');
|
|
assert.equal(statusIcon('unknown'), '[UNKNOWN]');
|
|
assert.equal(statusIcon(''), '[UNKNOWN]');
|
|
});
|
|
});
|
|
|
|
describe('truncateLine()', () => {
|
|
it('does not truncate short lines', () => {
|
|
const line = 'Short line';
|
|
assert.equal(truncateLine(line), line);
|
|
});
|
|
|
|
it('truncates long lines', () => {
|
|
const line = 'x'.repeat(200);
|
|
const result = truncateLine(line);
|
|
assert.ok(result.length <= 120);
|
|
assert.ok(result.endsWith('...'));
|
|
});
|
|
});
|
|
|
|
describe('extractAgentId()', () => {
|
|
it('extracts agent ID from session key', () => {
|
|
assert.equal(extractAgentId('agent:main:mattermost:channel:abc'), 'main');
|
|
assert.equal(extractAgentId('agent:coder-agent:session:123'), 'coder-agent');
|
|
});
|
|
|
|
it('handles non-standard keys', () => {
|
|
assert.equal(extractAgentId('main'), 'main');
|
|
assert.equal(extractAgentId(''), 'unknown');
|
|
});
|
|
|
|
it('handles null/undefined', () => {
|
|
assert.equal(extractAgentId(null), 'unknown');
|
|
assert.equal(extractAgentId(undefined), 'unknown');
|
|
});
|
|
});
|
|
});
|