Skip to content

Fix Jest force exit from unclosed pg pool event listeners#94

Closed
Copilot wants to merge 3 commits into
copilot/combine-integration-tests-and-fixesfrom
copilot/fix-jest-force-exit-issues
Closed

Fix Jest force exit from unclosed pg pool event listeners#94
Copilot wants to merge 3 commits into
copilot/combine-integration-tests-and-fixesfrom
copilot/fix-jest-force-exit-issues

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented Jan 30, 2026

Jest force-exited after all tests passed due to lingering event listeners on PostgreSQL connection pools, causing CI failures despite successful test execution.

Root Cause

DatabaseClient registered error handlers on the pg Pool but never removed them before calling pool.end(). Node's event loop remained active with registered listeners.

Changes

Pool cleanup (src/database/client.ts)

  • Call removeAllListeners() before pool.end() to clear error handlers

Test lifecycle (src/database/module.test.ts, src/knowledge-base/module.test.ts)

  • Add afterAll hooks to close DatabaseClient instances created in unit tests

Timing (jest.globalTeardown.ts)

  • Increase cleanup delay 2s → 3s for async completion

Config (jest.unit.config.js)

  • Remove forceExit: true (no longer required)
// Before: listeners keep event loop alive
async close(): Promise<void> {
  await this.pool.end();
  console.log('Database connections closed');
}

// After: clean removal of all listeners
async close(): Promise<void> {
  this.pool.removeAllListeners();
  await this.pool.end();
  console.log('Database connections closed');
}

Tests now exit cleanly with code 0.

Original prompt

Problem

PR #93 has Jest force exit issues identical to what PR #90 fixed. All 251 tests pass, but Jest cannot exit cleanly:

Test Suites: 17 passed, 17 total
Tests:       251 passed, 251 total
Force exiting Jest: Have you considered using `--detectOpenHandles` to detect async operations that kept running after all tests finished?
Error: Process completed with exit code 1.

Root Cause

The new integration test infrastructure adds DatabaseClient usage, which creates PostgreSQL connection pools via the pg library. These connections are not being properly cleaned up in test files, leaving open handles.

Required Fixes

1. Update src/database/client.ts

The DatabaseClient needs to ensure the pool doesn't have dangling event listeners. Update the constructor:

constructor(config?: PoolConfig) {
  const defaultConfig: PoolConfig = {
    host: process.env.DATABASE_HOST || 'localhost',
    port: parseInt(process.env.DATABASE_PORT || '5432'),
    database: process.env.DATABASE_NAME || 'vexel',
    user: process.env.DATABASE_USER || 'vexel_user',
    password: process.env.DATABASE_PASSWORD || '',
    max: 20,
    idleTimeoutMillis: 30000,
    connectionTimeoutMillis: 2000,
  };

  this.pool = new Pool({ ...defaultConfig, ...config });

  // Handle pool errors (prevent unhandled rejections)
  this.pool.on('error', (err: Error) => {
    console.error('Unexpected database error:', err);
  });
}

And ensure the close() method properly removes listeners:

async close(): Promise<void> {
  // Remove all event listeners before closing
  this.pool.removeAllListeners();
  await this.pool.end();
  console.log('Database connections closed');
}

2. Check All Test Files Using DatabaseClient

Search for any test files that instantiate DatabaseClient or use the singleton db instance and ensure they call close() in afterAll:

describe('Some test suite', () => {
  let db: DatabaseClient;

  beforeAll(() => {
    db = new DatabaseClient();
  });

  afterAll(async () => {
    await db.close();
  });

  // ... tests ...
});

3. Update jest.integration.setup.ts and jest.integration.teardown.ts

These files already close the database connection, but verify they're using the updated close() method properly.

File: jest.integration.setup.ts (lines 80-82):

// Close the setup connection to prevent handle leaks
await globalDb.close();
console.log('✓ Setup database connection closed');

File: jest.integration.teardown.ts (lines 47-49):

// Close database connections
await db.close();
console.log('✓ Database connections closed');

4. Update src/database/test-seeder.ts

Ensure TestDataSeeder doesn't create its own connections - it should reuse the passed DatabaseClient:

export class TestDataSeeder {
  constructor(private db: DatabaseClient) {}
  
  // All methods use this.db, never create new connections
}

5. Increase Global Teardown Delay

If connections are still slow to close, increase the cleanup delay in jest.globalTeardown.ts:

// Configurable cleanup delay - increase default from 2000ms to 3000ms
const CLEANUP_DELAY_MS = process.env.JEST_CLEANUP_DELAY 
  ? parseInt(process.env.JEST_CLEANUP_DELAY, 10) 
  : 3000;

6. Add forceExit Option to Jest Configs (Last Resort)

If the issue persists, add forceExit: true to jest.config.js as a temporary workaround:

module.exports = {
  preset: 'ts-jest',
  testEnvironment: 'node',
  roots: ['<rootDir>/src'],
  testMatch: ['**/__tests__/**/*.ts', '**/?(*.)+(spec|test).ts'],
  setupFilesAfterEnv: ['<rootDir>/jest.setup.ts'],
  globalTeardown: '<rootDir>/jest.globalTeardown.ts',
  forceExit: true,  // Add this line
  // ... rest of config
};

Priority Order

  1. First: Update DatabaseClient.close() to remove all listeners
  2. Second: Verify all test files call close() in afterAll
  3. Third: Increase teardown delay to 3000ms
  4. Last Resort: Add forceExit: true to Jest config

Expected Outcome

  • All 251 tests pass
  • Jest exits cleanly without force exit
  • No open handles warning
  • CI check passes with exit code 0

Verification

After applying fixes, run:

npm test
# Should exit cleanly without "Force exiting Jest" message

This pull request was created from Copilot chat.


💡 You can make Copilot smarter by setting up custom instructions, customizing its development environment and configuring Model Context Protocol (MCP) servers. Learn more Copilot coding agent tips in the docs.

Copilot AI and others added 2 commits January 30, 2026 20:54
Co-authored-by: colleenpridemore <4281084+colleenpridemore@users.noreply.github.com>
Co-authored-by: colleenpridemore <4281084+colleenpridemore@users.noreply.github.com>
Copilot AI changed the title [WIP] Fix Jest force exit issues by cleaning up database connections Fix Jest force exit by cleaning up DatabaseClient event listeners Jan 30, 2026
Copilot AI changed the title Fix Jest force exit by cleaning up DatabaseClient event listeners Fix Jest force exit from unclosed pg pool event listeners Jan 30, 2026
Copy link
Copy Markdown
Contributor

@colleenpridemore colleenpridemore left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok!

Copy link
Copy Markdown
Contributor

@colleenpridemore colleenpridemore left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok!

Copy link
Copy Markdown
Contributor

@colleenpridemore colleenpridemore left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok!

@colleenpridemore colleenpridemore marked this pull request as ready for review January 30, 2026 21:40
Copilot AI review requested due to automatic review settings January 30, 2026 21:40
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR attempts to fix Jest force-exit issues caused by unclosed PostgreSQL connection pool event listeners. The root cause was correctly identified: DatabaseClient registered error handlers that weren't removed before calling pool.end(), keeping Node's event loop active.

Changes:

  • Added removeAllListeners() call before pool.end() in DatabaseClient cleanup
  • Added afterAll hooks in unit test files to close DatabaseClient instances
  • Increased global teardown delay from 2s to 3s for async completion
  • Removed forceExit: true from Jest unit config

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
src/database/client.ts Added removeAllListeners() before pool cleanup to clear error handlers
src/database/module.test.ts Added afterAll hook to close test DatabaseClient instances
src/knowledge-base/module.test.ts Added afterAll hook to close test DatabaseClient instances
jest.globalTeardown.ts Increased cleanup delay from 2000ms to 3000ms
jest.unit.config.js Removed forceExit: true flag

Comment thread src/database/module.test.ts
Comment thread src/database/module.test.ts
Comment thread src/knowledge-base/module.test.ts

it('should have required methods', () => {
const db = new DatabaseClient();
testClient = db;
Copy link

Copilot AI Jan 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This assignment will overwrite the testClient reference from line 40, causing that DatabaseClient pool to leak. Each subsequent assignment in this test file compounds the problem.

Copilot uses AI. Check for mistakes.
Comment thread src/knowledge-base/module.test.ts
Comment thread src/knowledge-base/module.test.ts
Comment thread src/knowledge-base/module.test.ts
Copy link
Copy Markdown
Contributor

Superseded by #100

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants