basant307/AI_Governance_Project
048
1/**2 * @license3 * Copyright 2025 Qwen4 * SPDX-License-Identifier: Apache-2.05 */6 7import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';8import {9 initializeWarningHandler,10 resetWarningHandlerForTests,11} from './warningHandler.js';12 13const ENV_KEYS = ['NODE_ENV', 'DEBUG', 'QWEN_DEBUG'] as const;14 15describe('initializeWarningHandler', () => {16 const originalEnv: Partial<Record<(typeof ENV_KEYS)[number], string>> = {};17 let originalListeners: NodeJS.WarningListener[] = [];18 // Mock prior listener — installed before initializeWarningHandler so it19 // becomes one of the captured "priorListeners" the handler fans out to.20 // This is the channel the real Node default printer travels on, so21 // asserting fan-out here is equivalent to asserting "the default printer22 // would have fired" without coupling tests to internal Node behavior.23 let priorListener: ReturnType<typeof vi.fn>;24 25 beforeEach(() => {26 for (const k of ENV_KEYS) originalEnv[k] = process.env[k];27 for (const k of ENV_KEYS) delete process.env[k];28 originalListeners = [...process.listeners('warning')];29 process.removeAllListeners('warning');30 resetWarningHandlerForTests();31 priorListener = vi.fn();32 process.on('warning', priorListener);33 });34 35 afterEach(() => {36 resetWarningHandlerForTests();37 process.removeAllListeners('warning');38 for (const l of originalListeners) process.on('warning', l);39 for (const k of ENV_KEYS) {40 if (originalEnv[k] === undefined) delete process.env[k];41 else process.env[k] = originalEnv[k];42 }43 });44 45 function makeWarning(name: string, message: string): Error {46 const err = new Error(message);47 err.name = name;48 return err;49 }50 51 function emit(warning: Error): void {52 // Drive the real listener chain (process.emit dispatches synchronously53 // to every registered 'warning' listener). After initializeWarningHandler,54 // the only listener is ours; it decides whether to fan out to the55 // captured priorListener mock.56 process.emit('warning', warning);57 }58 59 it('suppresses MaxListenersExceededWarning for AbortSignal in production', () => {60 initializeWarningHandler();61 emit(62 makeWarning(63 'MaxListenersExceededWarning',64 'Possible EventTarget memory leak detected. 1509 abort listeners added to [AbortSignal].',65 ),66 );67 expect(priorListener).not.toHaveBeenCalled();68 });69 70 it('does NOT suppress generic [EventTarget] warnings — only AbortSignal', () => {71 initializeWarningHandler();72 emit(73 makeWarning(74 'MaxListenersExceededWarning',75 'Possible EventTarget memory leak detected. 11 listeners added to [EventTarget].',76 ),77 );78 expect(priorListener).toHaveBeenCalledTimes(1);79 });80 81 it('suppresses AbortSignal warnings with class metadata, e.g. [AbortSignal{aborted: false}]', () => {82 initializeWarningHandler();83 emit(84 makeWarning(85 'MaxListenersExceededWarning',86 'Possible EventTarget memory leak detected. 11 abort listeners added to [AbortSignal{aborted: false}].',87 ),88 );89 expect(priorListener).not.toHaveBeenCalled();90 });91 92 it('fans out unrelated warnings to captured prior listeners (e.g. Node default printer)', () => {93 initializeWarningHandler();94 const warning = makeWarning('DeprecationWarning', 'Some legacy thing');95 emit(warning);96 expect(priorListener).toHaveBeenCalledTimes(1);97 expect(priorListener).toHaveBeenCalledWith(warning);98 });99 100 it('preserves third-party warning listeners — they still fire for non-suppressed warnings', () => {101 const telemetryHook = vi.fn();102 process.on('warning', telemetryHook);103 initializeWarningHandler();104 emit(makeWarning('DeprecationWarning', 'X'));105 expect(priorListener).toHaveBeenCalledTimes(1);106 expect(telemetryHook).toHaveBeenCalledTimes(1);107 });108 109 it('a buggy prior listener does not break the chain for the rest', () => {110 const buggy = vi.fn(() => {111 throw new Error('boom');112 });113 const downstream = vi.fn();114 process.on('warning', buggy);115 process.on('warning', downstream);116 initializeWarningHandler();117 emit(makeWarning('DeprecationWarning', 'X'));118 expect(buggy).toHaveBeenCalledTimes(1);119 expect(downstream).toHaveBeenCalledTimes(1);120 });121 122 it('keeps suppressed warnings visible when DEBUG is set', () => {123 process.env['DEBUG'] = '1';124 initializeWarningHandler();125 emit(126 makeWarning(127 'MaxListenersExceededWarning',128 'Possible EventTarget memory leak detected. 1500 abort listeners added to [AbortSignal].',129 ),130 );131 expect(priorListener).toHaveBeenCalledTimes(1);132 });133 134 it('treats DEBUG=0 and DEBUG=false as not set', () => {135 process.env['DEBUG'] = '0';136 initializeWarningHandler();137 emit(138 makeWarning(139 'MaxListenersExceededWarning',140 'Possible EventTarget memory leak detected. 1500 abort listeners added to [AbortSignal].',141 ),142 );143 expect(priorListener).not.toHaveBeenCalled();144 });145 146 it('keeps warnings visible when QWEN_DEBUG is set', () => {147 process.env['QWEN_DEBUG'] = '1';148 initializeWarningHandler();149 emit(150 makeWarning(151 'MaxListenersExceededWarning',152 'Possible EventTarget memory leak detected. 1500 abort listeners added to [AbortSignal].',153 ),154 );155 expect(priorListener).toHaveBeenCalledTimes(1);156 });157 158 it('keeps warnings visible when NODE_ENV=development', () => {159 process.env['NODE_ENV'] = 'development';160 initializeWarningHandler();161 emit(162 makeWarning(163 'MaxListenersExceededWarning',164 'Possible EventTarget memory leak detected. 1500 abort listeners added to [AbortSignal].',165 ),166 );167 expect(priorListener).toHaveBeenCalledTimes(1);168 });169 170 it('is idempotent — repeated calls install only one listener', () => {171 initializeWarningHandler();172 initializeWarningHandler();173 initializeWarningHandler();174 expect(process.listeners('warning').length).toBe(1);175 });176 177 it('honors runtime DEBUG toggles — debug check is evaluated per warning', () => {178 initializeWarningHandler();179 // Initially DEBUG unset → suppression active.180 emit(181 makeWarning(182 'MaxListenersExceededWarning',183 'Possible EventTarget memory leak detected. 1500 abort listeners added to [AbortSignal].',184 ),185 );186 expect(priorListener).not.toHaveBeenCalled();187 188 // Flip DEBUG at runtime → next suppressed-pattern warning passes through.189 process.env['DEBUG'] = '1';190 emit(191 makeWarning(192 'MaxListenersExceededWarning',193 'Possible EventTarget memory leak detected. 1500 abort listeners added to [AbortSignal].',194 ),195 );196 expect(priorListener).toHaveBeenCalledTimes(1);197 });198});199 200describe('initializeWarningHandler — end-to-end stderr behavior', () => {201 // Integration test: spawn a child Node process to verify the real202 // emitWarning → default printer path is actually suppressed. The unit203 // tests above can't catch this because the default printer lives inside204 // Node and writes to stderr via internal mechanisms, not via the same205 // process.stderr.write spy.206 it('a child process with the handler installed does not print suppressed AbortSignal warnings to stderr', async () => {207 const { spawn } = await import('node:child_process');208 const { fileURLToPath, pathToFileURL } = await import('node:url');209 const { dirname, join } = await import('node:path');210 const { writeFile, mkdtemp, rm } = await import('node:fs/promises');211 const { tmpdir } = await import('node:os');212 213 const here = dirname(fileURLToPath(import.meta.url));214 // Convert the absolute path to a `file://` URL — on Windows, Node's ESM215 // loader rejects raw absolute paths (it treats `D:` as a URL scheme),216 // so import specifiers MUST be file URLs.217 const helperImportSpecifier = pathToFileURL(218 join(here, 'warningHandler.ts'),219 ).href;220 221 const dir = await mkdtemp(join(tmpdir(), 'warning-handler-e2e-'));222 try {223 const script = `224 import { initializeWarningHandler } from ${JSON.stringify(helperImportSpecifier)};225 delete process.env.DEBUG; delete process.env.QWEN_DEBUG;226 process.env.NODE_ENV = 'production';227 initializeWarningHandler();228 process.emitWarning(229 'Possible EventTarget memory leak detected. 1509 abort listeners added to [AbortSignal].',230 'MaxListenersExceededWarning'231 );232 process.emitWarning('Plain deprecation', 'DeprecationWarning');233 `;234 const scriptPath = join(dir, 'run.mjs');235 await writeFile(scriptPath, script);236 237 const child = spawn(process.execPath, ['--import', 'tsx', scriptPath], {238 stdio: ['ignore', 'pipe', 'pipe'],239 });240 let stderr = '';241 child.stderr.on('data', (b) => {242 stderr += b.toString();243 });244 await new Promise<void>((resolve) => child.on('exit', () => resolve()));245 246 expect(stderr).not.toMatch(/abort listeners added to \[AbortSignal\]/);247 // Deprecation should still print via the fanned-out default printer.248 expect(stderr).toMatch(/DeprecationWarning.*Plain deprecation/);249 } finally {250 await rm(dir, { recursive: true, force: true });251 }252 }, 15_000);253});254 