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