268 lines
7.7 KiB
JavaScript
268 lines
7.7 KiB
JavaScript
'use strict';
|
|
|
|
/**
|
|
* Test suite for fatal_error_reporter.js
|
|
*
|
|
* Tests:
|
|
* 1. Atomic write (no corruption under concurrent failures)
|
|
* 2. Corrupt JSON recovery (archives bad files)
|
|
* 3. Throttling (duplicate errors within window)
|
|
* 4. Email notification (when enabled)
|
|
* 5. Process exit behavior (when enabled)
|
|
*/
|
|
|
|
const fs = require('fs-extra');
|
|
const path = require('path');
|
|
const { reportFatal } = require('../helpers/fatal_error_reporter');
|
|
const env = require('../helpers/env');
|
|
|
|
const TEST_LOG_DIR = path.join(__dirname, '.test-fatal-logs');
|
|
const TEST_LOG_FILE = path.join(TEST_LOG_DIR, 'test_fatal.rlog');
|
|
|
|
async function setup() {
|
|
await fs.ensureDir(TEST_LOG_DIR);
|
|
await fs.remove(TEST_LOG_FILE); // Clean slate
|
|
console.log('✓ Test setup complete\n');
|
|
}
|
|
|
|
async function teardown() {
|
|
await fs.remove(TEST_LOG_DIR);
|
|
console.log('\n✓ Test teardown complete');
|
|
}
|
|
|
|
async function test1_atomicWrite() {
|
|
console.log('Test 1: Atomic write (no corruption)');
|
|
|
|
const err = new Error('Test atomic write');
|
|
err.code = 'TEST_ATOMIC';
|
|
|
|
await reportFatal({
|
|
filePath: TEST_LOG_FILE,
|
|
kind: 'test:atomic',
|
|
error: err,
|
|
message: err.stack,
|
|
throttleMs: 0, // No throttling for this test
|
|
emailEnabled: false,
|
|
});
|
|
|
|
const content = await fs.readFile(TEST_LOG_FILE, 'utf8');
|
|
const parsed = JSON.parse(content); // Should not throw
|
|
|
|
console.assert(parsed.code === 'TEST_ATOMIC', 'Code should match');
|
|
console.assert(parsed.kind === 'test:atomic', 'Kind should match');
|
|
console.assert(parsed.when, 'Timestamp should exist');
|
|
|
|
console.log(' ✓ Single write is atomic and parseable\n');
|
|
}
|
|
|
|
async function test2_corruptRecovery() {
|
|
console.log('Test 2: Corrupt JSON recovery');
|
|
|
|
// Write corrupt JSON
|
|
await fs.writeFile(TEST_LOG_FILE, '{ "broken": json here }', 'utf8');
|
|
|
|
const err = new Error('Test corrupt recovery');
|
|
err.code = 'TEST_CORRUPT';
|
|
|
|
await reportFatal({
|
|
filePath: TEST_LOG_FILE,
|
|
kind: 'test:corrupt',
|
|
error: err,
|
|
message: err.stack,
|
|
throttleMs: 0,
|
|
emailEnabled: false,
|
|
});
|
|
|
|
// Should have archived corrupt file
|
|
const archiveFiles = await fs.readdir(TEST_LOG_DIR);
|
|
const archived = archiveFiles.filter(f => f.includes('.corrupt.'));
|
|
console.assert(archived.length === 1, 'Should have archived corrupt file');
|
|
|
|
// New log should be valid
|
|
const content = await fs.readFile(TEST_LOG_FILE, 'utf8');
|
|
const parsed = JSON.parse(content);
|
|
console.assert(parsed.code === 'TEST_CORRUPT', 'New log should be valid');
|
|
|
|
console.log(' ✓ Corrupt JSON archived and replaced\n');
|
|
}
|
|
|
|
async function test3_throttling() {
|
|
console.log('Test 3: Throttling duplicate errors');
|
|
|
|
await fs.remove(TEST_LOG_FILE);
|
|
|
|
const err = new Error('Test throttle');
|
|
err.code = 'TEST_THROTTLE';
|
|
|
|
// First write
|
|
await reportFatal({
|
|
filePath: TEST_LOG_FILE,
|
|
kind: 'test:throttle',
|
|
error: err,
|
|
message: err.stack,
|
|
throttleMs: 10000, // 10 seconds
|
|
emailEnabled: false,
|
|
});
|
|
|
|
const firstWrite = await fs.readFile(TEST_LOG_FILE, 'utf8');
|
|
const firstParsed = JSON.parse(firstWrite);
|
|
const firstTime = new Date(firstParsed.when);
|
|
|
|
// Second write immediately (should be throttled)
|
|
await reportFatal({
|
|
filePath: TEST_LOG_FILE,
|
|
kind: 'test:throttle',
|
|
error: err,
|
|
message: err.stack,
|
|
throttleMs: 10000,
|
|
emailEnabled: false,
|
|
});
|
|
|
|
const secondWrite = await fs.readFile(TEST_LOG_FILE, 'utf8');
|
|
const secondParsed = JSON.parse(secondWrite);
|
|
const secondTime = new Date(secondParsed.when);
|
|
|
|
console.assert(firstTime.getTime() === secondTime.getTime(), 'Timestamp should not change (throttled)');
|
|
|
|
console.log(' ✓ Duplicate errors throttled within window\n');
|
|
}
|
|
|
|
async function test4_differentErrors() {
|
|
console.log('Test 4: Different errors are not throttled');
|
|
|
|
await fs.remove(TEST_LOG_FILE);
|
|
|
|
const err1 = new Error('First error');
|
|
err1.code = 'ERR_FIRST';
|
|
|
|
await reportFatal({
|
|
filePath: TEST_LOG_FILE,
|
|
kind: 'test:different',
|
|
error: err1,
|
|
message: err1.stack,
|
|
throttleMs: 10000,
|
|
emailEnabled: false,
|
|
});
|
|
|
|
const firstWrite = await fs.readFile(TEST_LOG_FILE, 'utf8');
|
|
const firstParsed = JSON.parse(firstWrite);
|
|
|
|
const err2 = new Error('Second error');
|
|
err2.code = 'ERR_SECOND';
|
|
|
|
await reportFatal({
|
|
filePath: TEST_LOG_FILE,
|
|
kind: 'test:different',
|
|
error: err2,
|
|
message: err2.stack,
|
|
throttleMs: 10000,
|
|
emailEnabled: false,
|
|
});
|
|
|
|
const secondWrite = await fs.readFile(TEST_LOG_FILE, 'utf8');
|
|
const secondParsed = JSON.parse(secondWrite);
|
|
|
|
console.assert(firstParsed.code === 'ERR_FIRST', 'First error code should be ERR_FIRST');
|
|
console.assert(secondParsed.code === 'ERR_SECOND', 'Second error code should be ERR_SECOND');
|
|
console.assert(secondParsed.when !== firstParsed.when, 'Timestamp should update for different error');
|
|
|
|
console.log(' ✓ Different errors are not throttled\n');
|
|
}
|
|
|
|
async function test5_processHandlers() {
|
|
console.log('Test 5: Process-level handlers integration');
|
|
|
|
const { registerFatalHandlers, createServerIgnore } = require('../helpers/process_fatal_handlers');
|
|
|
|
// Create a mock process object
|
|
const mockProcess = {
|
|
_handlers: {},
|
|
on(event, handler) {
|
|
this._handlers[event] = handler;
|
|
return this;
|
|
},
|
|
off(event, handler) {
|
|
delete this._handlers[event];
|
|
return this;
|
|
}
|
|
};
|
|
|
|
const mockDebug = (msg) => console.log(` [mock-debug] ${msg}`);
|
|
|
|
const cleanup = registerFatalHandlers(mockProcess, {
|
|
env: {
|
|
FATAL_REPORT_ENABLED: true,
|
|
FATAL_REPORT_FILE: TEST_LOG_FILE,
|
|
FATAL_REPORT_EMAIL_ENABLED: false,
|
|
FATAL_EXIT_ON_ERROR: false,
|
|
FATAL_THROTTLE_MS: 0,
|
|
},
|
|
debug: mockDebug,
|
|
kindPrefix: 'test_process',
|
|
reportFilePath: TEST_LOG_FILE,
|
|
ignore: createServerIgnore(),
|
|
});
|
|
|
|
console.assert(mockProcess._handlers.uncaughtException, 'Should register uncaughtException');
|
|
console.assert(mockProcess._handlers.unhandledRejection, 'Should register unhandledRejection');
|
|
|
|
// Test ignore filter for HTTP stream errors
|
|
const httpErr = new Error("Cannot read properties of undefined (reading 'readable')");
|
|
httpErr.stack = 'Error: ...\n at IncomingMessage._read ...';
|
|
|
|
await mockProcess._handlers.uncaughtException(httpErr);
|
|
|
|
// Should be ignored, so no file write
|
|
const exists = await fs.pathExists(TEST_LOG_FILE);
|
|
console.assert(!exists, 'HTTP stream error should be ignored (no file write)');
|
|
|
|
// Test non-ignored error
|
|
const realErr = new Error('Real fatal error');
|
|
realErr.code = 'REAL_FATAL';
|
|
|
|
await mockProcess._handlers.uncaughtException(realErr);
|
|
|
|
const content = await fs.readFile(TEST_LOG_FILE, 'utf8');
|
|
const parsed = JSON.parse(content);
|
|
console.assert(parsed.code === 'REAL_FATAL', 'Real error should be logged');
|
|
console.assert(parsed.kind === 'test_process:uncaughtException', 'Kind should include prefix');
|
|
|
|
cleanup(); // Unregister handlers
|
|
|
|
console.log(' ✓ Process handlers registered and filters applied\n');
|
|
}
|
|
|
|
async function runTests() {
|
|
console.log('===================================');
|
|
console.log('Fatal Error Reporter Test Suite');
|
|
console.log('===================================\n');
|
|
|
|
try {
|
|
await setup();
|
|
await test1_atomicWrite();
|
|
await test2_corruptRecovery();
|
|
await test3_throttling();
|
|
await test4_differentErrors();
|
|
await test5_processHandlers();
|
|
|
|
console.log('===================================');
|
|
console.log('All tests passed! ✓');
|
|
console.log('===================================');
|
|
} catch (err) {
|
|
console.error('\n✗ Test failed:', err);
|
|
process.exit(1);
|
|
} finally {
|
|
await teardown();
|
|
}
|
|
}
|
|
|
|
// Run if executed directly
|
|
if (require.main === module) {
|
|
runTests().catch(err => {
|
|
console.error('Test runner error:', err);
|
|
process.exit(1);
|
|
});
|
|
}
|
|
|
|
module.exports = { runTests };
|