diff --git a/jest.config.js b/jest.config.js index 75b84aaf7..fa21ccb17 100644 --- a/jest.config.js +++ b/jest.config.js @@ -124,8 +124,10 @@ export default { // Run tests from one or more projects // projects: null, - // Use this configuration option to add custom reporters to Jest - // reporters: undefined, + // Custom reporters — default reporter kept for human-readable output; gcReporter + // calls global.gc() after each test suite when --expose-gc is in NODE_OPTIONS + // (active for test:coverage only). No-op when gc() is unavailable. + reporters: ['default', './scripts/jest.gcReporter.cjs'], // Automatically reset mock state between every test // resetMocks: false, diff --git a/package.json b/package.json index a973d3db0..1d77dcd99 100644 --- a/package.json +++ b/package.json @@ -34,7 +34,7 @@ "test:integration": "cross-env NODE_ENV=test NODE_OPTIONS=--experimental-vm-modules jest --runInBand --testPathPatterns='integration'", "test:e2e": "cross-env NODE_ENV=test NODE_OPTIONS=--experimental-vm-modules jest --runInBand --testPathPatterns='e2e'", "test:watch": "cross-env NODE_ENV=test NODE_OPTIONS=--experimental-vm-modules jest --watchAll", - "test:coverage": "cross-env NODE_ENV=test NODE_OPTIONS=--experimental-vm-modules jest --coverage --runInBand", + "test:coverage": "cross-env NODE_ENV=test NODE_OPTIONS=\"--experimental-vm-modules --expose-gc\" jest --coverage --runInBand", "test:parallel-smoke": "cross-env NODE_ENV=test node scripts/parallel-integration.smoke.js", "lint": "eslint ./modules ./lib ./config ./scripts", "seed:dev": "cross-env NODE_ENV=development node scripts/seed.js seed", diff --git a/scripts/jest.gcReporter.cjs b/scripts/jest.gcReporter.cjs new file mode 100644 index 000000000..a72b39fca --- /dev/null +++ b/scripts/jest.gcReporter.cjs @@ -0,0 +1,34 @@ +/** + * Jest reporter that calls global.gc() after each test suite when available. + * + * Registered in jest.config.js reporters array alongside the default reporter. + * Requires --expose-gc in NODE_OPTIONS (already set in test:coverage script). + * + * Why: jest --runInBand accumulates module registry snapshots, mock closures, and + * Mongoose schema objects across 99 suites in a single process. V8's heuristic GC + * trigger is based on heap growth rate; calling gc() explicitly after each suite + * ensures the old generation is collected before the next suite loads its modules. + * This reduces peak RSS by releasing memory that V8 would otherwise hold until + * it reached the old-space limit. + * + * When --expose-gc is absent (test:all, test:unit, test:integration), global.gc + * is undefined and this reporter is a no-op, so it is safe to leave enabled for + * all test runs. + */ + +class GcReporter { + /** + * Called by Jest after each test file completes. + * Triggers an explicit GC cycle when --expose-gc is active (test:coverage only). + * @param {object} _test - Jest test descriptor (unused). + * @param {object} _testResult - Aggregated results for this file (unused). + * @returns {void} + */ + onTestFileResult() { + if (typeof global.gc === 'function') { + global.gc(); + } + } +} + +module.exports = GcReporter;