logger.test.js
1 /** 2 * Unit tests for Logger utility 3 */ 4 5 import { describe, test, mock, beforeEach, afterEach } from 'node:test'; 6 import assert from 'node:assert'; 7 import fs from 'fs'; 8 import os from 'os'; 9 import path from 'path'; 10 import Logger from '../../src/utils/logger.js'; 11 12 describe('Logger Module', () => { 13 let consoleLogSpy; 14 let consoleWarnSpy; 15 let consoleErrorSpy; 16 let stdoutWriteSpy; 17 let originalDebug; 18 19 beforeEach(() => { 20 // Mock console methods to capture output 21 consoleLogSpy = mock.method(console, 'log', () => {}); 22 consoleWarnSpy = mock.method(console, 'warn', () => {}); 23 consoleErrorSpy = mock.method(console, 'error', () => {}); 24 stdoutWriteSpy = mock.method(process.stdout, 'write', () => {}); 25 originalDebug = process.env.DEBUG; 26 }); 27 28 afterEach(() => { 29 // Restore console methods 30 consoleLogSpy.mock.restore(); 31 consoleWarnSpy.mock.restore(); 32 consoleErrorSpy.mock.restore(); 33 stdoutWriteSpy.mock.restore(); 34 process.env.DEBUG = originalDebug; 35 }); 36 37 describe('constructor', () => { 38 test('should create logger with default context', () => { 39 const logger = new Logger(); 40 assert.strictEqual(logger.context, '333'); 41 }); 42 43 test('should create logger with custom context', () => { 44 const logger = new Logger('test-context'); 45 assert.strictEqual(logger.context, 'test-context'); 46 }); 47 }); 48 49 describe('info', () => { 50 test('should log info message without data', () => { 51 const logger = new Logger('test'); 52 logger.info('Test info message'); 53 54 assert.strictEqual(consoleLogSpy.mock.calls.length, 1); 55 const logOutput = consoleLogSpy.mock.calls[0].arguments[0]; 56 assert.ok(logOutput.includes('[test]')); 57 assert.ok(logOutput.includes('[INFO]')); 58 assert.ok(logOutput.includes('Test info message')); 59 }); 60 61 test('should log info message with data', () => { 62 const logger = new Logger('test'); 63 const testData = { key: 'value', number: 42 }; 64 logger.info('Test with data', testData); 65 66 assert.strictEqual(consoleLogSpy.mock.calls.length, 1); 67 const logOutput = consoleLogSpy.mock.calls[0].arguments[0]; 68 assert.ok(logOutput.includes('Test with data')); 69 assert.ok(logOutput.includes('"key": "value"')); 70 assert.ok(logOutput.includes('"number": 42')); 71 }); 72 }); 73 74 describe('success', () => { 75 test('should log success message without data', () => { 76 const logger = new Logger('test'); 77 logger.success('Operation succeeded'); 78 79 assert.strictEqual(consoleLogSpy.mock.calls.length, 1); 80 const logOutput = consoleLogSpy.mock.calls[0].arguments[0]; 81 assert.ok(logOutput.includes('[SUCCESS]')); 82 assert.ok(logOutput.includes('Operation succeeded')); 83 }); 84 85 test('should log success message with data', () => { 86 const logger = new Logger('test'); 87 logger.success('Success with stats', { processed: 10, failed: 0 }); 88 89 assert.strictEqual(consoleLogSpy.mock.calls.length, 1); 90 const logOutput = consoleLogSpy.mock.calls[0].arguments[0]; 91 assert.ok(logOutput.includes('Success with stats')); 92 assert.ok(logOutput.includes('"processed": 10')); 93 }); 94 }); 95 96 describe('warn', () => { 97 test('should log warning message without data', () => { 98 const logger = new Logger('test'); 99 logger.warn('Warning message'); 100 101 assert.strictEqual(consoleWarnSpy.mock.calls.length, 1); 102 const logOutput = consoleWarnSpy.mock.calls[0].arguments[0]; 103 assert.ok(logOutput.includes('[WARN]')); 104 assert.ok(logOutput.includes('Warning message')); 105 }); 106 107 test('should log warning message with data', () => { 108 const logger = new Logger('test'); 109 logger.warn('Warning with details', { retry: true }); 110 111 assert.strictEqual(consoleWarnSpy.mock.calls.length, 1); 112 const logOutput = consoleWarnSpy.mock.calls[0].arguments[0]; 113 assert.ok(logOutput.includes('Warning with details')); 114 assert.ok(logOutput.includes('"retry": true')); 115 }); 116 }); 117 118 describe('error', () => { 119 test('should log error message without error object', () => { 120 const logger = new Logger('test'); 121 logger.error('Error occurred'); 122 123 assert.strictEqual(consoleErrorSpy.mock.calls.length, 1); 124 const logOutput = consoleErrorSpy.mock.calls[0].arguments[0]; 125 assert.ok(logOutput.includes('[ERROR]')); 126 assert.ok(logOutput.includes('Error occurred')); 127 }); 128 129 test('should log error message with error object', () => { 130 const logger = new Logger('test'); 131 const testError = new Error('Test error'); 132 logger.error('Operation failed', testError); 133 134 assert.strictEqual(consoleErrorSpy.mock.calls.length, 1); 135 const logOutput = consoleErrorSpy.mock.calls[0].arguments[0]; 136 assert.ok(logOutput.includes('Operation failed')); 137 assert.ok(logOutput.includes('Test error')); 138 assert.ok(logOutput.includes('stack')); 139 }); 140 }); 141 142 describe('debug', () => { 143 test('should log debug message when DEBUG=true', () => { 144 process.env.DEBUG = 'true'; 145 const logger = new Logger('test'); 146 logger.debug('Debug message'); 147 148 assert.strictEqual(consoleLogSpy.mock.calls.length, 1); 149 const logOutput = consoleLogSpy.mock.calls[0].arguments[0]; 150 assert.ok(logOutput.includes('[DEBUG]')); 151 assert.ok(logOutput.includes('Debug message')); 152 }); 153 154 test('should not log debug message when DEBUG is not set', () => { 155 delete process.env.DEBUG; 156 const logger = new Logger('test'); 157 logger.debug('Debug message'); 158 159 assert.strictEqual(consoleLogSpy.mock.calls.length, 0); 160 }); 161 162 test('should not log debug message when DEBUG=false', () => { 163 process.env.DEBUG = 'false'; 164 const logger = new Logger('test'); 165 logger.debug('Debug message'); 166 167 assert.strictEqual(consoleLogSpy.mock.calls.length, 0); 168 }); 169 170 test('should log debug message with data when DEBUG=true', () => { 171 process.env.DEBUG = 'true'; 172 const logger = new Logger('test'); 173 logger.debug('Debug with data', { detail: 'value' }); 174 175 assert.strictEqual(consoleLogSpy.mock.calls.length, 1); 176 const logOutput = consoleLogSpy.mock.calls[0].arguments[0]; 177 assert.ok(logOutput.includes('Debug with data')); 178 assert.ok(logOutput.includes('"detail": "value"')); 179 }); 180 }); 181 182 describe('progress', () => { 183 test('should display progress bar at 0%', () => { 184 const logger = new Logger('test'); 185 logger.progress(0, 100); 186 187 assert.strictEqual(stdoutWriteSpy.mock.calls.length, 1); 188 const output = stdoutWriteSpy.mock.calls[0].arguments[0]; 189 assert.ok(output.includes('0%')); 190 assert.ok(output.includes('░'.repeat(50))); 191 }); 192 193 test('should display progress bar at 50%', () => { 194 const logger = new Logger('test'); 195 logger.progress(50, 100); 196 197 assert.strictEqual(stdoutWriteSpy.mock.calls.length, 1); 198 const output = stdoutWriteSpy.mock.calls[0].arguments[0]; 199 assert.ok(output.includes('50%')); 200 assert.ok(output.includes('█'.repeat(25))); 201 assert.ok(output.includes('░'.repeat(25))); 202 }); 203 204 test('should display progress bar at 100% and add newline', () => { 205 const logger = new Logger('test'); 206 logger.progress(100, 100); 207 208 // Should call stdout.write once for progress bar 209 assert.strictEqual(stdoutWriteSpy.mock.calls.length, 1); 210 const output = stdoutWriteSpy.mock.calls[0].arguments[0]; 211 assert.ok(output.includes('100%')); 212 assert.ok(output.includes('█'.repeat(50))); 213 214 // Should call console.log once for newline 215 assert.strictEqual(consoleLogSpy.mock.calls.length, 1); 216 }); 217 218 test('should display progress bar with custom message', () => { 219 const logger = new Logger('test'); 220 logger.progress(25, 100, 'Processing files...'); 221 222 assert.strictEqual(stdoutWriteSpy.mock.calls.length, 1); 223 const output = stdoutWriteSpy.mock.calls[0].arguments[0]; 224 assert.ok(output.includes('25%')); 225 assert.ok(output.includes('Processing files...')); 226 }); 227 }); 228 229 describe('_format', () => { 230 test('should format message without data', () => { 231 const logger = new Logger('test'); 232 const formatted = logger._format('INFO', 'Test message'); 233 234 assert.ok(formatted.includes('[test]')); 235 assert.ok(formatted.includes('[INFO]')); 236 assert.ok(formatted.includes('Test message')); 237 // Should include ISO timestamp 238 assert.ok(formatted.match(/\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/)); 239 }); 240 241 test('should format message with data', () => { 242 const logger = new Logger('test'); 243 const testData = { key: 'value' }; 244 const formatted = logger._format('INFO', 'Test message', testData); 245 246 assert.ok(formatted.includes('Test message')); 247 assert.ok(formatted.includes('"key": "value"')); 248 }); 249 }); 250 251 describe('_getLogDomain', () => { 252 test('returns pipeline for pipeline context', () => { 253 const logger = new Logger('pipeline'); 254 assert.strictEqual(logger._getLogDomain('pipeline'), 'pipeline'); 255 }); 256 257 test('returns pipeline for pipelineservice context', () => { 258 const logger = new Logger('test'); 259 assert.strictEqual(logger._getLogDomain('pipelineservice'), 'pipeline'); 260 }); 261 262 test('returns keywords for keywords context', () => { 263 const logger = new Logger('test'); 264 assert.strictEqual(logger._getLogDomain('keywords'), 'keywords'); 265 }); 266 267 test('returns keywords for keywordmanager context', () => { 268 const logger = new Logger('test'); 269 assert.strictEqual(logger._getLogDomain('keywordmanager'), 'keywords'); 270 }); 271 272 test('returns serps for serps context', () => { 273 const logger = new Logger('test'); 274 assert.strictEqual(logger._getLogDomain('serps'), 'serps'); 275 }); 276 277 test('returns serps for scraper context', () => { 278 const logger = new Logger('test'); 279 assert.strictEqual(logger._getLogDomain('scraper'), 'serps'); 280 }); 281 282 test('returns assets for assets context', () => { 283 const logger = new Logger('test'); 284 assert.strictEqual(logger._getLogDomain('assets'), 'assets'); 285 }); 286 287 test('returns assets for capture context', () => { 288 const logger = new Logger('test'); 289 assert.strictEqual(logger._getLogDomain('capture'), 'assets'); 290 }); 291 292 test('returns scoring for scoring context', () => { 293 const logger = new Logger('test'); 294 assert.strictEqual(logger._getLogDomain('scoring'), 'scoring'); 295 }); 296 297 test('returns rescoring for rescoring context', () => { 298 const logger = new Logger('test'); 299 assert.strictEqual(logger._getLogDomain('rescoring'), 'rescoring'); 300 }); 301 302 test('returns enrich for enrich context', () => { 303 const logger = new Logger('test'); 304 assert.strictEqual(logger._getLogDomain('enrich'), 'enrich'); 305 }); 306 307 test('returns proposals for proposals context', () => { 308 const logger = new Logger('test'); 309 assert.strictEqual(logger._getLogDomain('proposals'), 'proposals'); 310 }); 311 312 test('returns outreach for outreach context', () => { 313 const logger = new Logger('test'); 314 assert.strictEqual(logger._getLogDomain('outreach'), 'outreach'); 315 }); 316 317 test('returns outreach for smsoutreach context', () => { 318 const logger = new Logger('test'); 319 assert.strictEqual(logger._getLogDomain('smsoutreach'), 'outreach'); 320 }); 321 322 test('returns outreach for emailoutreach context', () => { 323 const logger = new Logger('test'); 324 assert.strictEqual(logger._getLogDomain('emailoutreach'), 'outreach'); 325 }); 326 327 test('returns outreach for formoutreach context', () => { 328 const logger = new Logger('test'); 329 assert.strictEqual(logger._getLogDomain('formoutreach'), 'outreach'); 330 }); 331 332 test('returns inbound for replies context', () => { 333 const logger = new Logger('test'); 334 assert.strictEqual(logger._getLogDomain('replies'), 'inbound'); 335 }); 336 337 test('returns inbound for inboundsms context', () => { 338 const logger = new Logger('test'); 339 assert.strictEqual(logger._getLogDomain('inboundsms'), 'inbound'); 340 }); 341 342 test('returns dashboard for dashboard context', () => { 343 const logger = new Logger('test'); 344 assert.strictEqual(logger._getLogDomain('dashboard'), 'dashboard'); 345 }); 346 347 test('returns dashboard for overview context', () => { 348 const logger = new Logger('test'); 349 assert.strictEqual(logger._getLogDomain('overview'), 'dashboard'); 350 }); 351 352 test('returns agents for agents context', () => { 353 const logger = new Logger('test'); 354 assert.strictEqual(logger._getLogDomain('agents'), 'agents'); 355 }); 356 357 test('returns agents for developer context', () => { 358 const logger = new Logger('test'); 359 assert.strictEqual(logger._getLogDomain('developer'), 'agents'); 360 }); 361 362 test('returns agents for qa context', () => { 363 const logger = new Logger('test'); 364 assert.strictEqual(logger._getLogDomain('qa'), 'agents'); 365 }); 366 367 test('returns cron for cron context', () => { 368 const logger = new Logger('test'); 369 assert.strictEqual(logger._getLogDomain('cron'), 'cron'); 370 }); 371 372 test('returns cron for dailylogrotation context', () => { 373 const logger = new Logger('test'); 374 assert.strictEqual(logger._getLogDomain('dailylogrotation'), 'cron'); 375 }); 376 377 test('returns utils for dedupe context', () => { 378 const logger = new Logger('test'); 379 assert.strictEqual(logger._getLogDomain('dedupe'), 'utils'); 380 }); 381 382 test('returns utils for errorhandler context', () => { 383 const logger = new Logger('test'); 384 assert.strictEqual(logger._getLogDomain('errorhandler'), 'utils'); 385 }); 386 387 test('returns tests for test context', () => { 388 const logger = new Logger('test'); 389 assert.strictEqual(logger._getLogDomain('test'), 'tests'); 390 }); 391 392 test('returns app for unknown context', () => { 393 const logger = new Logger('test'); 394 assert.strictEqual(logger._getLogDomain('unknownmodule'), 'app'); 395 }); 396 397 test('normalizes context by lowercasing before lookup', () => { 398 const logger = new Logger('test'); 399 assert.strictEqual(logger._getLogDomain('PIPELINE'), 'pipeline'); 400 }); 401 402 test('normalizes context by stripping special chars', () => { 403 const logger = new Logger('test'); 404 // 'cronjobs' is a dashboard page key in the domain map 405 assert.strictEqual(logger._getLogDomain('cronjobs'), 'dashboard'); 406 }); 407 408 test('strips hyphens from context before lookup', () => { 409 const logger = new Logger('test'); 410 // 'cron-job' normalizes to 'cronjob' which is not in map, falls back to app 411 assert.strictEqual(logger._getLogDomain('cron-job'), 'app'); 412 }); 413 }); 414 415 describe('file writing (logToFile: true)', () => { 416 let tmpDir; 417 let savedNodeEnv; 418 419 // Helper: create a logger with file writing enabled by temporarily clearing NODE_ENV. 420 // Logger constructor disables file logging when NODE_ENV === 'test', so we clear it 421 // before constructing and restore it immediately after. 422 function makeFileLogger(context, extraOpts = {}) { 423 const env = process.env.NODE_ENV; 424 delete process.env.NODE_ENV; 425 const logger = new Logger(context, { logDir: tmpDir, ...extraOpts }); 426 if (env !== undefined) { 427 process.env.NODE_ENV = env; 428 } 429 return logger; 430 } 431 432 // Helper: close logger and flush the write stream before the file is read 433 async function closeAndFlush(logger) { 434 await new Promise(resolve => { 435 if (logger.logStream && !logger.logStream.destroyed) { 436 logger.logStream.end(resolve); 437 } else { 438 resolve(); 439 } 440 }); 441 } 442 443 beforeEach(() => { 444 tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'logger-test-')); 445 savedNodeEnv = process.env.NODE_ENV; 446 }); 447 448 afterEach(() => { 449 if (savedNodeEnv !== undefined) { 450 process.env.NODE_ENV = savedNodeEnv; 451 } 452 // Clean up temp directory 453 try { 454 fs.rmSync(tmpDir, { recursive: true, force: true }); 455 } catch (_e) { 456 // ignore cleanup errors 457 } 458 }); 459 460 test('creates log directory if it does not exist', () => { 461 const newDir = path.join(tmpDir, 'nested', 'logs'); 462 const logger = makeFileLogger('test', { logDir: newDir }); 463 // mkdirSync is synchronous so dir is created immediately 464 const exists = fs.existsSync(newDir); 465 logger.close(); 466 assert.ok(exists, 'log directory should be created'); 467 }); 468 469 test('logFile path is set on initialization', () => { 470 const logger = makeFileLogger('test'); 471 // logFile path is set synchronously even before first write 472 assert.ok(logger.logFile, 'logFile should be set'); 473 assert.ok(logger.logFile.includes(tmpDir), 'logFile should be in tmpDir'); 474 logger.close(); 475 }); 476 477 test('log file path includes domain and date', () => { 478 const logger = makeFileLogger('pipeline'); 479 assert.ok(logger.logFile.includes('pipeline'), 'log file path should include domain'); 480 assert.ok(logger.logFile.match(/\d{4}-\d{2}-\d{2}/), 'log file path should include date'); 481 logger.close(); 482 }); 483 484 test('writes info messages to file', async () => { 485 const logger = makeFileLogger('test'); 486 logger.info('File write test message'); 487 await closeAndFlush(logger); 488 const contents = fs.readFileSync(logger.logFile, 'utf8'); 489 assert.ok(contents.includes('File write test message'), 'file should contain log message'); 490 assert.ok(contents.includes('[INFO]'), 'file should contain log level'); 491 // ANSI codes should be stripped from file output 492 assert.ok(!contents.includes('\x1b['), 'file should not contain ANSI codes'); 493 }); 494 495 test('writes success messages to file', async () => { 496 const logger = makeFileLogger('test'); 497 logger.success('Success write test'); 498 await closeAndFlush(logger); 499 const contents = fs.readFileSync(logger.logFile, 'utf8'); 500 assert.ok(contents.includes('Success write test'), 'file should contain success message'); 501 assert.ok(contents.includes('[SUCCESS]'), 'file should contain SUCCESS level'); 502 }); 503 504 test('writes warn messages to file', async () => { 505 const logger = makeFileLogger('test'); 506 logger.warn('Warning write test'); 507 await closeAndFlush(logger); 508 const contents = fs.readFileSync(logger.logFile, 'utf8'); 509 assert.ok(contents.includes('Warning write test'), 'file should contain warn message'); 510 assert.ok(contents.includes('[WARN]'), 'file should contain WARN level'); 511 }); 512 513 test('writes error messages to file', async () => { 514 const logger = makeFileLogger('test'); 515 const err = new Error('test error detail'); 516 logger.error('Error write test', err); 517 await closeAndFlush(logger); 518 const contents = fs.readFileSync(logger.logFile, 'utf8'); 519 assert.ok(contents.includes('Error write test'), 'file should contain error message'); 520 assert.ok(contents.includes('[ERROR]'), 'file should contain ERROR level'); 521 assert.ok(contents.includes('test error detail'), 'file should contain error detail'); 522 }); 523 524 test('writes debug messages to file regardless of DEBUG env var', async () => { 525 delete process.env.DEBUG; 526 const logger = makeFileLogger('test'); 527 logger.debug('Debug write test'); 528 await closeAndFlush(logger); 529 const contents = fs.readFileSync(logger.logFile, 'utf8'); 530 assert.ok(contents.includes('Debug write test'), 'file should contain debug message'); 531 assert.ok(contents.includes('[DEBUG]'), 'file should contain DEBUG level'); 532 }); 533 534 test('appends multiple messages to same file', async () => { 535 const logger = makeFileLogger('test'); 536 logger.info('First message'); 537 logger.info('Second message'); 538 logger.warn('Third message'); 539 await closeAndFlush(logger); 540 const contents = fs.readFileSync(logger.logFile, 'utf8'); 541 assert.ok(contents.includes('First message')); 542 assert.ok(contents.includes('Second message')); 543 assert.ok(contents.includes('Third message')); 544 }); 545 546 test('uses app domain for unknown context when writing to file', () => { 547 const logger = makeFileLogger('unknownmodule'); 548 assert.ok(logger.logFile.includes('app'), 'unknown context should use app domain'); 549 logger.close(); 550 }); 551 552 test('logStream is open before close() is called', () => { 553 const logger = makeFileLogger('test'); 554 assert.ok(logger.logStream, 'logStream should exist'); 555 assert.ok(!logger.logStream.destroyed, 'stream should not be destroyed before close'); 556 logger.close(); 557 }); 558 559 test('close() ends the log stream', async () => { 560 const logger = makeFileLogger('test'); 561 await closeAndFlush(logger); 562 assert.ok( 563 logger.logStream.destroyed || logger.logStream.writableEnded, 564 'stream should be ended after close' 565 ); 566 }); 567 568 test('_writeToFile does nothing if stream is destroyed', async () => { 569 const logger = makeFileLogger('test'); 570 const logFilePath = logger.logFile; 571 // Write something first to ensure the file exists 572 logger.info('before close'); 573 await closeAndFlush(logger); 574 const beforeSize = fs.existsSync(logFilePath) ? fs.statSync(logFilePath).size : 0; 575 // After close, writing should be a no-op (stream is destroyed/ended) 576 logger._writeToFile('should not appear in file'); 577 await new Promise(resolve => setTimeout(resolve, 50)); 578 const afterSize = fs.existsSync(logFilePath) ? fs.statSync(logFilePath).size : 0; 579 assert.strictEqual( 580 beforeSize, 581 afterSize, 582 'file size should not change after stream destroyed' 583 ); 584 }); 585 }); 586 587 describe('constructor logToFile option', () => { 588 test('logToFile is disabled in test environment (NODE_ENV=test)', () => { 589 // NODE_ENV is set to 'test' by the test runner prefix, so logToFile defaults to false 590 process.env.NODE_ENV = 'test'; 591 const logger = new Logger('test'); 592 assert.strictEqual(logger.logToFile, false, 'logToFile should be false in NODE_ENV=test'); 593 assert.strictEqual(logger.logStream, null, 'logStream should be null when logToFile=false'); 594 }); 595 596 test('logToFile can be explicitly disabled', () => { 597 const logger = new Logger('test', { logToFile: false }); 598 assert.strictEqual(logger.logToFile, false); 599 assert.strictEqual(logger.logStream, null); 600 }); 601 602 test('close() is safe to call when logStream is null', () => { 603 process.env.NODE_ENV = 'test'; 604 const logger = new Logger('test'); 605 // Should not throw even when logStream is null 606 assert.doesNotThrow(() => logger.close()); 607 }); 608 }); 609 610 describe('additional branch coverage', () => { 611 let tmpDir; 612 let savedNodeEnv; 613 614 function makeFileLogger(context, extraOpts = {}) { 615 const env = process.env.NODE_ENV; 616 delete process.env.NODE_ENV; 617 const logger = new Logger(context, { logDir: tmpDir, ...extraOpts }); 618 if (env !== undefined) { 619 process.env.NODE_ENV = env; 620 } 621 return logger; 622 } 623 624 beforeEach(() => { 625 tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'logger-branch-test-')); 626 savedNodeEnv = process.env.NODE_ENV; 627 }); 628 629 afterEach(() => { 630 if (savedNodeEnv !== undefined) { 631 process.env.NODE_ENV = savedNodeEnv; 632 } 633 try { 634 fs.rmSync(tmpDir, { recursive: true, force: true }); 635 } catch (_e) { 636 // ignore 637 } 638 }); 639 640 test('does not recreate log directory when it already exists', () => { 641 // logDir already exists from beforeEach — _initializeLogFile should skip mkdirSync 642 const logger = makeFileLogger('test'); 643 // If it tries to create it again it would still succeed, but we verify no error thrown 644 assert.ok(logger.logFile, 'logFile should be set even when dir already exists'); 645 logger.close(); 646 }); 647 648 test('logStream error event handler logs to console.error without throwing', () => { 649 const logger = makeFileLogger('test'); 650 // Replace console.error to capture the error log (the outer beforeEach spy is already 651 // on console.error, but we want to check the raw string, so use the spy's captured calls) 652 // The outer consoleErrorSpy intercepts console.error calls. 653 // We emit a stream error and check that console.error was called with the expected message. 654 logger.logStream.emit('error', new Error('stream write failure')); 655 // console.error is currently mocked by consoleErrorSpy from the outer describe 656 const { calls } = consoleErrorSpy.mock; 657 const found = calls.some(c => String(c.arguments[0]).includes('Failed to write to log file')); 658 assert.ok(found, 'error handler should call console.error with failure message'); 659 logger.close(); 660 }); 661 662 test('uses LOGS_DIR env variable as default logDir', () => { 663 const customDir = path.join(tmpDir, 'from-env'); 664 fs.mkdirSync(customDir, { recursive: true }); 665 process.env.LOGS_DIR = customDir; 666 const env = process.env.NODE_ENV; 667 delete process.env.NODE_ENV; 668 const logger = new Logger('test'); 669 if (env !== undefined) process.env.NODE_ENV = env; 670 delete process.env.LOGS_DIR; 671 assert.ok(logger.logFile.startsWith(customDir), 'should use LOGS_DIR env var as logDir'); 672 logger.close(); 673 }); 674 }); 675 });