@@ -16,6 +16,7 @@ import {
1616} from './config.js' ;
1717import { Storage } from './storage.js' ;
1818import * as fs from 'node:fs' ;
19+ import * as os from 'node:os' ;
1920import * as path from 'node:path' ;
2021import { setGeminiMdFilename as mockSetGeminiMdFilename } from '../memory/const.js' ;
2122import {
@@ -288,6 +289,13 @@ vi.mock('../ide/ide-client.js', () => ({
288289
289290import { BaseLlmClient } from '../core/baseLlmClient.js' ;
290291
292+ const MEMORY_PRESSURE_ENV_KEYS = [
293+ 'QWEN_MEMORY_PRESSURE_SOFT' ,
294+ 'QWEN_MEMORY_PRESSURE_HARD' ,
295+ 'QWEN_MEMORY_PRESSURE_CRITICAL' ,
296+ 'QWEN_MEMORY_ENABLE_GC' ,
297+ ] ;
298+
291299vi . mock ( '../core/baseLlmClient.js' ) ;
292300// Mock fireNotificationHook from toolHookTriggers
293301vi . mock ( '../core/toolHookTriggers.js' , ( ) => ( {
@@ -328,6 +336,9 @@ describe('Server Config (config.ts)', () => {
328336 beforeEach ( ( ) => {
329337 // Reset mocks if necessary
330338 vi . clearAllMocks ( ) ;
339+ for ( const envName of MEMORY_PRESSURE_ENV_KEYS ) {
340+ delete process . env [ envName ] ;
341+ }
331342 ( fs . existsSync as Mock ) . mockReturnValue ( true ) ;
332343 ( fs . readdirSync as Mock ) . mockReturnValue ( [ ] ) ;
333344 ( fs . statSync as Mock ) . mockReturnValue ( {
@@ -422,6 +433,192 @@ describe('Server Config (config.ts)', () => {
422433 } ) ;
423434 } ) ;
424435
436+ describe ( 'MemoryPressureMonitor isolation' , ( ) => {
437+ it ( 'returns a distinct monitor for child Configs created via Object.create' , async ( ) => {
438+ const parent = new Config ( baseParams ) ;
439+ await parent . initialize ( { skipGeminiInitialization : true } ) ;
440+ const child = Object . create ( parent ) as Config ;
441+
442+ const parentMonitor = parent . getMemoryPressureMonitor ( ) ;
443+ const childMonitor = child . getMemoryPressureMonitor ( ) ;
444+
445+ expect ( parentMonitor ) . toBeDefined ( ) ;
446+ expect ( childMonitor ) . toBeDefined ( ) ;
447+ expect ( childMonitor ) . not . toBe ( parentMonitor ) ;
448+ expect ( child . getMemoryPressureMonitor ( ) ) . toBe ( childMonitor ) ;
449+ } ) ;
450+
451+ it ( 'resets monitor cleanup failures when starting a new session' , async ( ) => {
452+ const config = new Config ( baseParams ) ;
453+ await config . initialize ( { skipGeminiInitialization : true } ) ;
454+ const monitor = config . getMemoryPressureMonitor ( ) ;
455+ expect ( monitor ) . toBeDefined ( ) ;
456+ const resetSpy = vi . spyOn ( monitor ! , 'resetConsecutiveFailures' ) ;
457+
458+ config . startNewSession ( ) ;
459+
460+ expect ( resetSpy ) . toHaveBeenCalledTimes ( 1 ) ;
461+ } ) ;
462+ } ) ;
463+
464+ describe ( 'MemoryPressure configuration environment' , ( ) => {
465+ const restorers : Array < ( ) => void > = [ ] ;
466+ const originalEnv = new Map < string , string | undefined > ( ) ;
467+
468+ beforeEach ( ( ) => {
469+ originalEnv . clear ( ) ;
470+ for ( const envName of MEMORY_PRESSURE_ENV_KEYS ) {
471+ originalEnv . set ( envName , process . env [ envName ] ) ;
472+ delete process . env [ envName ] ;
473+ }
474+ } ) ;
475+
476+ afterEach ( ( ) => {
477+ while ( restorers . length > 0 ) {
478+ restorers . pop ( ) ?.( ) ;
479+ }
480+ for ( const [ envName , value ] of originalEnv ) {
481+ if ( value === undefined ) {
482+ delete process . env [ envName ] ;
483+ } else {
484+ process . env [ envName ] = value ;
485+ }
486+ }
487+ originalEnv . clear ( ) ;
488+ } ) ;
489+
490+ function mockMemoryRatio ( rssRatio : number , heapUsedBytes = 0 ) : void {
491+ const spy = vi . spyOn ( process , 'memoryUsage' ) . mockReturnValue ( {
492+ rss : Math . ceil ( os . totalmem ( ) * rssRatio ) ,
493+ heapTotal : 512 * 1024 * 1024 ,
494+ heapUsed : heapUsedBytes ,
495+ external : 0 ,
496+ arrayBuffers : 0 ,
497+ } ) ;
498+ restorers . push ( ( ) => spy . mockRestore ( ) ) ;
499+ }
500+
501+ function mockStderrWrite ( ) : Mock {
502+ const spy = vi
503+ . spyOn ( process . stderr , 'write' )
504+ . mockImplementation ( ( ) => true ) ;
505+ restorers . push ( ( ) => spy . mockRestore ( ) ) ;
506+ return spy as unknown as Mock ;
507+ }
508+
509+ it ( 'applies valid memory pressure env overrides' , async ( ) => {
510+ process . env [ 'QWEN_MEMORY_PRESSURE_SOFT' ] = '0.3' ;
511+ process . env [ 'QWEN_MEMORY_PRESSURE_HARD' ] = '0.6' ;
512+ process . env [ 'QWEN_MEMORY_PRESSURE_CRITICAL' ] = '0.9' ;
513+
514+ const config = new Config ( baseParams ) ;
515+ await config . initialize ( { skipGeminiInitialization : true } ) ;
516+ mockMemoryRatio ( 0.35 ) ;
517+
518+ expect ( config . getMemoryPressureMonitor ( ) ?. getPressureLevel ( ) ) . toBe (
519+ 'soft' ,
520+ ) ;
521+ } ) ;
522+
523+ it ( 'falls back to defaults and warns on strict env parse failures' , async ( ) => {
524+ const stderrSpy = mockStderrWrite ( ) ;
525+ process . env [ 'QWEN_MEMORY_PRESSURE_SOFT' ] = '0.3extra' ;
526+ process . env [ 'QWEN_MEMORY_PRESSURE_HARD' ] = '0.6' ;
527+ process . env [ 'QWEN_MEMORY_PRESSURE_CRITICAL' ] = '0.9' ;
528+
529+ const config = new Config ( baseParams ) ;
530+ await config . initialize ( { skipGeminiInitialization : true } ) ;
531+ mockMemoryRatio ( 0.35 ) ;
532+
533+ expect ( config . getMemoryPressureMonitor ( ) ?. getPressureLevel ( ) ) . toBe (
534+ 'normal' ,
535+ ) ;
536+ expect ( stderrSpy ) . toHaveBeenCalledWith (
537+ expect . stringContaining ( 'Invalid memory pressure config' ) ,
538+ ) ;
539+ } ) ;
540+
541+ it ( 'falls back to defaults and warns on invalid threshold ordering' , async ( ) => {
542+ const stderrSpy = mockStderrWrite ( ) ;
543+ process . env [ 'QWEN_MEMORY_PRESSURE_SOFT' ] = '0.7' ;
544+
545+ const config = new Config ( baseParams ) ;
546+ await config . initialize ( { skipGeminiInitialization : true } ) ;
547+
548+ expect ( config . getMemoryPressureMonitor ( ) ) . toBeDefined ( ) ;
549+ expect ( stderrSpy ) . toHaveBeenCalledWith (
550+ expect . stringContaining (
551+ 'softPressureRatio must be < hardPressureRatio' ,
552+ ) ,
553+ ) ;
554+ } ) ;
555+
556+ it . each ( [ 'NaN' , 'Infinity' , '0' ] ) (
557+ 'falls back to defaults for invalid soft threshold %s' ,
558+ async ( value ) => {
559+ const stderrSpy = mockStderrWrite ( ) ;
560+ process . env [ 'QWEN_MEMORY_PRESSURE_SOFT' ] = value ;
561+
562+ const config = new Config ( baseParams ) ;
563+ await config . initialize ( { skipGeminiInitialization : true } ) ;
564+ mockMemoryRatio ( 0.35 ) ;
565+
566+ expect ( config . getMemoryPressureMonitor ( ) ?. getPressureLevel ( ) ) . toBe (
567+ 'normal' ,
568+ ) ;
569+ expect ( stderrSpy ) . toHaveBeenCalledWith (
570+ expect . stringContaining ( 'Invalid memory pressure config' ) ,
571+ ) ;
572+ } ,
573+ ) ;
574+
575+ it ( 'enables explicit GC when requested by env' , async ( ) => {
576+ process . env [ 'QWEN_MEMORY_ENABLE_GC' ] = '1' ;
577+ const globalWithGc = global as typeof global & { gc ?: ( ) => void } ;
578+ const originalGc = globalWithGc . gc ;
579+ const gcSpy = vi . fn ( ) ;
580+ Object . defineProperty ( globalWithGc , 'gc' , {
581+ value : gcSpy ,
582+ configurable : true ,
583+ } ) ;
584+ restorers . push ( ( ) => {
585+ if ( originalGc ) {
586+ Object . defineProperty ( globalWithGc , 'gc' , {
587+ value : originalGc ,
588+ configurable : true ,
589+ } ) ;
590+ } else {
591+ delete globalWithGc . gc ;
592+ }
593+ } ) ;
594+
595+ const config = new Config ( baseParams ) ;
596+ await config . initialize ( { skipGeminiInitialization : true } ) ;
597+ mockMemoryRatio ( 0.85 ) ;
598+
599+ config . getMemoryPressureMonitor ( ) ?. performCheck ( ) ;
600+ await Promise . resolve ( ) ;
601+
602+ expect ( gcSpy ) . toHaveBeenCalledTimes ( 1 ) ;
603+ } ) ;
604+
605+ it ( 'child Config monitors inherit the parent memory pressure config snapshot' , async ( ) => {
606+ process . env [ 'QWEN_MEMORY_PRESSURE_SOFT' ] = '0.3' ;
607+ process . env [ 'QWEN_MEMORY_PRESSURE_HARD' ] = '0.6' ;
608+ process . env [ 'QWEN_MEMORY_PRESSURE_CRITICAL' ] = '0.9' ;
609+ const parent = new Config ( baseParams ) ;
610+ await parent . initialize ( { skipGeminiInitialization : true } ) ;
611+
612+ process . env [ 'QWEN_MEMORY_PRESSURE_SOFT' ] = '0.9' ;
613+ process . env [ 'QWEN_MEMORY_PRESSURE_HARD' ] = '0.95' ;
614+ process . env [ 'QWEN_MEMORY_PRESSURE_CRITICAL' ] = '0.97' ;
615+ const child = Object . create ( parent ) as Config ;
616+ mockMemoryRatio ( 0.35 ) ;
617+
618+ expect ( child . getMemoryPressureMonitor ( ) ?. getPressureLevel ( ) ) . toBe ( 'soft' ) ;
619+ } ) ;
620+ } ) ;
621+
425622 describe ( 'startNewSession' , ( ) => {
426623 it ( 'clears the FileReadCache so a new session does not inherit prior reads' , ( ) => {
427624 // Regression guard: the file-read cache backs ReadFile's
0 commit comments