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.
136 lines
3.8 KiB
JavaScript
136 lines
3.8 KiB
JavaScript
'use strict';
|
|
|
|
/**
|
|
* Unit tests for config.js
|
|
*/
|
|
|
|
const { describe, it, beforeEach, afterEach } = require('node:test');
|
|
const assert = require('node:assert/strict');
|
|
|
|
const { buildConfig, resetConfig } = require('../../src/config');
|
|
|
|
describe('config.js', () => {
|
|
const originalEnv = {};
|
|
|
|
beforeEach(() => {
|
|
// Save and clear relevant env vars
|
|
const keys = [
|
|
'MM_BOT_TOKEN',
|
|
'MM_BASE_URL',
|
|
'MM_MAX_SOCKETS',
|
|
'TRANSCRIPT_DIR',
|
|
'THROTTLE_MS',
|
|
'IDLE_TIMEOUT_S',
|
|
'SESSION_POLL_MS',
|
|
'MAX_ACTIVE_SESSIONS',
|
|
'MAX_MESSAGE_CHARS',
|
|
'MAX_STATUS_LINES',
|
|
'MAX_RETRIES',
|
|
'CIRCUIT_BREAKER_THRESHOLD',
|
|
'CIRCUIT_BREAKER_COOLDOWN_S',
|
|
'HEALTH_PORT',
|
|
'LOG_LEVEL',
|
|
'PID_FILE',
|
|
'OFFSET_FILE',
|
|
'TOOL_LABELS_FILE',
|
|
'DEFAULT_CHANNEL',
|
|
'ENABLE_FS_WATCH',
|
|
'MM_PORT',
|
|
];
|
|
for (const k of keys) {
|
|
originalEnv[k] = process.env[k];
|
|
delete process.env[k];
|
|
}
|
|
resetConfig();
|
|
});
|
|
|
|
afterEach(() => {
|
|
// Restore env
|
|
for (const [k, v] of Object.entries(originalEnv)) {
|
|
if (v === undefined) delete process.env[k];
|
|
else process.env[k] = v;
|
|
}
|
|
resetConfig();
|
|
});
|
|
|
|
it('throws if MM_BOT_TOKEN is missing', () => {
|
|
assert.throws(() => buildConfig(), /MM_BOT_TOKEN/);
|
|
});
|
|
|
|
it('builds config with only required vars', () => {
|
|
process.env.MM_BOT_TOKEN = 'test-token';
|
|
const cfg = buildConfig();
|
|
assert.equal(cfg.mm.token, 'test-token');
|
|
assert.equal(cfg.mm.baseUrl, 'https://slack.solio.tech');
|
|
assert.equal(cfg.throttleMs, 500);
|
|
assert.equal(cfg.idleTimeoutS, 60);
|
|
assert.equal(cfg.maxActiveSessions, 20);
|
|
assert.equal(cfg.healthPort, 9090);
|
|
assert.equal(cfg.logLevel, 'info');
|
|
});
|
|
|
|
it('reads all env vars correctly', () => {
|
|
process.env.MM_BOT_TOKEN = 'mytoken';
|
|
process.env.MM_BASE_URL = 'https://mm.example.com';
|
|
process.env.MM_MAX_SOCKETS = '8';
|
|
process.env.THROTTLE_MS = '250';
|
|
process.env.IDLE_TIMEOUT_S = '120';
|
|
process.env.MAX_ACTIVE_SESSIONS = '10';
|
|
process.env.MAX_MESSAGE_CHARS = '5000';
|
|
process.env.LOG_LEVEL = 'debug';
|
|
process.env.HEALTH_PORT = '8080';
|
|
process.env.ENABLE_FS_WATCH = 'false';
|
|
|
|
const cfg = buildConfig();
|
|
assert.equal(cfg.mm.token, 'mytoken');
|
|
assert.equal(cfg.mm.baseUrl, 'https://mm.example.com');
|
|
assert.equal(cfg.mm.maxSockets, 8);
|
|
assert.equal(cfg.throttleMs, 250);
|
|
assert.equal(cfg.idleTimeoutS, 120);
|
|
assert.equal(cfg.maxActiveSessions, 10);
|
|
assert.equal(cfg.maxMessageChars, 5000);
|
|
assert.equal(cfg.logLevel, 'debug');
|
|
assert.equal(cfg.healthPort, 8080);
|
|
assert.equal(cfg.enableFsWatch, false);
|
|
});
|
|
|
|
it('throws on invalid MM_BASE_URL', () => {
|
|
process.env.MM_BOT_TOKEN = 'token';
|
|
process.env.MM_BASE_URL = 'not-a-url';
|
|
assert.throws(() => buildConfig(), /MM_BASE_URL/);
|
|
});
|
|
|
|
it('throws on non-integer THROTTLE_MS', () => {
|
|
process.env.MM_BOT_TOKEN = 'token';
|
|
process.env.THROTTLE_MS = 'abc';
|
|
assert.throws(() => buildConfig(), /THROTTLE_MS/);
|
|
});
|
|
|
|
it('ENABLE_FS_WATCH accepts "1", "true", "yes"', () => {
|
|
process.env.MM_BOT_TOKEN = 'token';
|
|
|
|
process.env.ENABLE_FS_WATCH = '1';
|
|
assert.equal(buildConfig().enableFsWatch, true);
|
|
resetConfig();
|
|
|
|
process.env.ENABLE_FS_WATCH = 'true';
|
|
assert.equal(buildConfig().enableFsWatch, true);
|
|
resetConfig();
|
|
|
|
process.env.ENABLE_FS_WATCH = 'yes';
|
|
assert.equal(buildConfig().enableFsWatch, true);
|
|
resetConfig();
|
|
|
|
process.env.ENABLE_FS_WATCH = '0';
|
|
assert.equal(buildConfig().enableFsWatch, false);
|
|
resetConfig();
|
|
});
|
|
|
|
it('nullish defaults for optional string fields', () => {
|
|
process.env.MM_BOT_TOKEN = 'token';
|
|
const cfg = buildConfig();
|
|
assert.equal(cfg.toolLabelsFile, null);
|
|
assert.equal(cfg.defaultChannel, null);
|
|
});
|
|
});
|