agmission/Development/server/tests/test_fatal_error_reporter.js

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 };