qerrors
Version:
Intelligent error handling middleware with AI-powered analysis, environment validation, caching, and production-ready logging. Provides OpenAI-based error suggestions, queue management, retry mechanisms, and comprehensive configuration options for Node.js
182 lines (174 loc) • 10.3 kB
JavaScript
const test = require('node:test'); //node test runner
const assert = require('node:assert/strict'); //assert helpers
const qtests = require('qtests'); //stub utilities
function reloadQerrors() { //load fresh module with env
delete require.cache[require.resolve('../lib/qerrors')];
return require('../lib/qerrors');
}
test('scheduleAnalysis rejects when queue exceeds limit', async () => {
const origConc = process.env.QERRORS_CONCURRENCY; //save original concurrency
const origQueue = process.env.QERRORS_QUEUE_LIMIT; //save original queue limit
process.env.QERRORS_CONCURRENCY = '1'; //one concurrent analysis
process.env.QERRORS_QUEUE_LIMIT = '1'; //allow single queued item
const qerrors = reloadQerrors(); //reload with env vars
const logger = await require('../lib/logger'); //logger instance
let logged; //capture logged error
const restoreLog = qtests.stubMethod(logger, 'error', (e) => { logged = e; });
const restoreAnalyze = qtests.stubMethod(qerrors, 'analyzeError', async () => {
return new Promise((r) => setTimeout(r, 20)); //simulate long analysis
});
try {
qerrors(new Error('one')); //first call fills active slot
qerrors(new Error('two')); //second call should exceed queue limit
qerrors(new Error('three')); //third call also exceeds queue limit
await new Promise((r) => setTimeout(r, 30)); //wait for processing
} finally {
restoreLog(); //restore logger stub
restoreAnalyze(); //restore analyze stub
if (origConc === undefined) { delete process.env.QERRORS_CONCURRENCY; } else { process.env.QERRORS_CONCURRENCY = origConc; }
if (origQueue === undefined) { delete process.env.QERRORS_QUEUE_LIMIT; } else { process.env.QERRORS_QUEUE_LIMIT = origQueue; }
reloadQerrors(); //reset module
}
assert.ok(logged instanceof Error); //expect error object logged
assert.equal(logged.message, 'queue full'); //message indicates queue full
});
test('queue reject count increments when queue exceeds limit', async () => {
const origConc = process.env.QERRORS_CONCURRENCY; //backup env
const origQueue = process.env.QERRORS_QUEUE_LIMIT; //backup env
process.env.QERRORS_CONCURRENCY = '1'; //force single concurrency
process.env.QERRORS_QUEUE_LIMIT = '1'; //allow one queued before rejection
const qerrors = reloadQerrors(); //reload to apply env
const logger = await require('../lib/logger'); //logger instance
const restoreWarn = qtests.stubMethod(logger, 'warn', () => {}); //silence warn
const restoreError = qtests.stubMethod(logger, 'error', () => {}); //silence err
const restoreAnalyze = qtests.stubMethod(qerrors, 'analyzeError', async () => {
return new Promise((r) => setTimeout(r, 20)); //simulate analysis time
});
try {
qerrors(new Error('one')); //consume active slot
qerrors(new Error('two')); //first rejection increments counter
qerrors(new Error('three')); //second rejection increments counter
await new Promise((r) => setTimeout(r, 30)); //allow tasks
} finally {
restoreWarn(); //restore warn stub
restoreError(); //restore error stub
restoreAnalyze(); //restore analyze stub
if (origConc === undefined) { delete process.env.QERRORS_CONCURRENCY; } else { process.env.QERRORS_CONCURRENCY = origConc; }
if (origQueue === undefined) { delete process.env.QERRORS_QUEUE_LIMIT; } else { process.env.QERRORS_QUEUE_LIMIT = origQueue; }
reloadQerrors(); //reset state
}
assert.equal(qerrors.getQueueRejectCount(), 2); //expect two rejections
});
test('getQueueLength reflects queued analyses', async () => {
const origConc = process.env.QERRORS_CONCURRENCY; //backup env
const origQueue = process.env.QERRORS_QUEUE_LIMIT; //backup env
process.env.QERRORS_CONCURRENCY = '1'; //force single concurrency
process.env.QERRORS_QUEUE_LIMIT = '2'; //allow one queued item
process.env.OPENAI_TOKEN = 'tkn'; //enable analyzeError path
const qerrors = reloadQerrors(); //reload to apply env
const logger = await require('../lib/logger'); //logger instance
const restoreWarn = qtests.stubMethod(logger, 'warn', () => {}); //silence warn
const restoreError = qtests.stubMethod(logger, 'error', () => {}); //silence err
const capture = {}; //track axios args
const restorePost = qtests.stubMethod(qerrors.axiosInstance, 'post', async () => {
return new Promise((r) => setTimeout(() => r({ data: { choices: [{ message: { content: '{}' } }] } }), 20)); //simulate delay
});
try {
qerrors(new Error('one')); //consume concurrency slot
qerrors(new Error('two')); //queued under limit
await new Promise((r) => setTimeout(r, 15)); //allow queue update
assert.equal(qerrors.getQueueLength(), 1); //expect one item queued
await new Promise((r) => setTimeout(r, 30)); //allow tasks
} finally {
restoreWarn(); //restore warn stub
restoreError(); //restore error stub
restorePost(); //restore axios stub
if (origConc === undefined) { delete process.env.QERRORS_CONCURRENCY; } else { process.env.QERRORS_CONCURRENCY = origConc; }
if (origQueue === undefined) { delete process.env.QERRORS_QUEUE_LIMIT; } else { process.env.QERRORS_QUEUE_LIMIT = origQueue; }
delete process.env.OPENAI_TOKEN; //cleanup token
reloadQerrors(); //reset state
}
});
test('scheduleAnalysis uses defaults on invalid env', () => { //verify helper fallback
const origConc = process.env.QERRORS_CONCURRENCY; //backup
const origQueue = process.env.QERRORS_QUEUE_LIMIT; //backup
process.env.QERRORS_CONCURRENCY = 'abc'; //invalid concurrency
process.env.QERRORS_QUEUE_LIMIT = 'abc'; //invalid queue limit
delete require.cache[require.resolve('../lib/config')]; //reload config
const cfg = require('../lib/config'); //load fresh config
try {
assert.equal(cfg.getInt('QERRORS_CONCURRENCY'), 5); //falls back to default
assert.equal(cfg.getInt('QERRORS_QUEUE_LIMIT'), 100); //falls back to default
} finally {
if (origConc === undefined) { delete process.env.QERRORS_CONCURRENCY; } else { process.env.QERRORS_CONCURRENCY = origConc; }
if (origQueue === undefined) { delete process.env.QERRORS_QUEUE_LIMIT; } else { process.env.QERRORS_QUEUE_LIMIT = origQueue; }
delete require.cache[require.resolve('../lib/config')]; //reset module
require('../lib/config'); //reapply defaults
}
});
test('queue never exceeds limit under high concurrency', async () => {
const origConc = process.env.QERRORS_CONCURRENCY; //backup env
const origQueue = process.env.QERRORS_QUEUE_LIMIT; //backup env
process.env.QERRORS_CONCURRENCY = '3'; //allow more active analyses
process.env.QERRORS_QUEUE_LIMIT = '1'; //only one queued task
const qerrors = reloadQerrors(); //reload config vars
const logger = await require('../lib/logger'); //logger instance
const restoreWarn = qtests.stubMethod(logger, 'warn', () => {}); //silence warn
const restoreError = qtests.stubMethod(logger, 'error', () => {}); //silence error
const restoreAnalyze = qtests.stubMethod(qerrors, 'analyzeError', async () => new Promise(r => setTimeout(r, 20))); //simulate work
try {
qerrors(new Error('one')); //start first analysis
qerrors(new Error('two')); //rejected since limit reached
qerrors(new Error('three')); //rejected since limit reached
qerrors(new Error('four')); //rejected since limit reached
qerrors(new Error('five')); //another rejection due to limit
await new Promise(r => setTimeout(r, 50)); //allow processing
} finally {
restoreWarn();
restoreError();
restoreAnalyze();
if (origConc === undefined) { delete process.env.QERRORS_CONCURRENCY; } else { process.env.QERRORS_CONCURRENCY = origConc; }
if (origQueue === undefined) { delete process.env.QERRORS_QUEUE_LIMIT; } else { process.env.QERRORS_QUEUE_LIMIT = origQueue; }
reloadQerrors(); //reset state
}
assert.ok(qerrors.getQueueLength() <= 1); //queue length at most limit
assert.equal(qerrors.getQueueRejectCount(), 4); //four tasks rejected
});
test('metrics stop when queue drains then restart on new analysis', async () => {
const origConc = process.env.QERRORS_CONCURRENCY; //backup concurrency
const origInterval = process.env.QERRORS_METRIC_INTERVAL_MS; //backup metric interval
const realSet = global.setInterval; //save original setInterval
const realClear = global.clearInterval; //save original clearInterval
let startCount = 0; //track interval creation
global.setInterval = (fn, ms) => { startCount++; fn(); return { unref() {} }; }; //simulate immediate tick
global.clearInterval = () => {}; //noop for test
process.env.QERRORS_CONCURRENCY = '1'; //single task
process.env.QERRORS_METRIC_INTERVAL_MS = '5'; //fast metrics
const qerrors = reloadQerrors(); //reload with env
const logger = await require('../lib/logger'); //logger instance
let metrics = 0; //metric log count
const restoreInfo = qtests.stubMethod(logger, 'info', (m) => { if (String(m).startsWith('metrics')) metrics++; });
const restoreWarn = qtests.stubMethod(logger, 'warn', () => {}); //silence warn
const restoreError = qtests.stubMethod(logger, 'error', () => {}); //silence error
const restoreAnalyze = qtests.stubMethod(qerrors, 'analyzeError', async () => new Promise(r => setTimeout(r, 10))); //simulate work
try {
qerrors(new Error('one')); //start first analysis
await new Promise(r => setTimeout(r, 20)); //wait for completion
const first = metrics; //capture metric count
await new Promise(r => setTimeout(r, 5)); //allow stopQueueMetrics
qerrors(new Error('two')); //restart queue
await new Promise(r => setTimeout(r, 20)); //wait for run
assert.ok(metrics > first); //metrics resumed
assert.equal(startCount, 3); //cleanup once and metrics twice
} finally {
global.setInterval = realSet; //restore interval
global.clearInterval = realClear; //restore clear
restoreInfo();
restoreWarn();
restoreError();
restoreAnalyze();
if (origConc === undefined) { delete process.env.QERRORS_CONCURRENCY; } else { process.env.QERRORS_CONCURRENCY = origConc; }
if (origInterval === undefined) { delete process.env.QERRORS_METRIC_INTERVAL_MS; } else { process.env.QERRORS_METRIC_INTERVAL_MS = origInterval; }
reloadQerrors();
}
});