1
0

fatal-handler.test.ts 3.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100
  1. import { describe, it, expect } from 'vitest';
  2. import { EventEmitter } from 'events';
  3. import { describeFatal, installFatalHandlers } from '../src/bin/fatal-handler';
  4. /**
  5. * Regression coverage for #850 (and the related #799): a fault that reaches the
  6. * process-wide handler must NOT be swallowed-and-kept-running, and rendering it
  7. * must NEVER touch `error.stack` — the lazy stack getter is what can wedge a
  8. * core in a V8 source-position loop.
  9. */
  10. describe('describeFatal', () => {
  11. it('renders name + message for an Error', () => {
  12. expect(describeFatal(new TypeError('boom'))).toBe('TypeError: boom');
  13. });
  14. it('falls back to the name when the message is empty', () => {
  15. expect(describeFatal(new Error(''))).toBe('Error');
  16. });
  17. it('stringifies non-Error values', () => {
  18. expect(describeFatal('a string reason')).toBe('a string reason');
  19. expect(describeFatal(42)).toBe('42');
  20. expect(describeFatal(null)).toBe('null');
  21. expect(describeFatal(undefined)).toBe('undefined');
  22. });
  23. it('NEVER reads error.stack (the #850 hang lives in the lazy stack getter)', () => {
  24. const err = new Error('boom');
  25. let stackAccessed = false;
  26. Object.defineProperty(err, 'stack', {
  27. configurable: true,
  28. get() {
  29. // Simulates the pathological case: formatting the stack never returns.
  30. stackAccessed = true;
  31. throw new Error('stack formatting wedged');
  32. },
  33. });
  34. const rendered = describeFatal(err);
  35. expect(stackAccessed).toBe(false);
  36. expect(rendered).toBe('Error: boom');
  37. expect(rendered).not.toMatch(/\bat\b/); // no stack frames leaked in
  38. });
  39. it('never throws on a value with a hostile toString', () => {
  40. const hostile = {
  41. toString() {
  42. throw new Error('no stringification for you');
  43. },
  44. };
  45. expect(describeFatal(hostile)).toBe('<unstringifiable value>');
  46. });
  47. });
  48. describe('installFatalHandlers', () => {
  49. function harness() {
  50. const target = new EventEmitter();
  51. const writes: string[] = [];
  52. const exits: number[] = [];
  53. installFatalHandlers({
  54. target,
  55. write: (line) => writes.push(line),
  56. exit: (code) => {
  57. exits.push(code);
  58. },
  59. });
  60. return { target, writes, exits };
  61. }
  62. it('logs a bounded line and exits non-zero on an uncaught exception', () => {
  63. const { target, writes, exits } = harness();
  64. target.emit('uncaughtException', new RangeError('kaboom'));
  65. expect(writes).toEqual(['[CodeGraph] Uncaught exception: RangeError: kaboom\n']);
  66. expect(exits).toEqual([1]);
  67. });
  68. it('logs a bounded line and exits non-zero on an unhandled rejection', () => {
  69. const { target, writes, exits } = harness();
  70. target.emit('unhandledRejection', 'promise went sideways');
  71. expect(writes).toEqual(['[CodeGraph] Unhandled rejection: promise went sideways\n']);
  72. expect(exits).toEqual([1]);
  73. });
  74. it('still exits — without touching the stack — when stack formatting would wedge', () => {
  75. const { target, writes, exits } = harness();
  76. const err = new Error('wedged');
  77. Object.defineProperty(err, 'stack', {
  78. configurable: true,
  79. get() {
  80. throw new Error('stack formatting wedged');
  81. },
  82. });
  83. // Must not throw or hang: the handler renders message-only and exits.
  84. expect(() => target.emit('uncaughtException', err)).not.toThrow();
  85. expect(writes).toEqual(['[CodeGraph] Uncaught exception: Error: wedged\n']);
  86. expect(exits).toEqual([1]);
  87. });
  88. });