fix: resolve test hang issues in CI

- Fixed MSW event listener memory leaks
- Added proper database connection cleanup
- Fixed MSW server lifecycle management
- Reduced global test timeout to 30s for faster failure detection
- Added resource cleanup in all integration tests

This should resolve the GitHub Actions test hanging issue
This commit is contained in:
czlonkowski
2025-07-29 13:07:51 +02:00
parent c824fb5ebf
commit 115bb6f36c
5 changed files with 88 additions and 25 deletions

Binary file not shown.

View File

@@ -138,9 +138,13 @@ describe('Database Connection Management', () => {
// Test concurrent reads // Test concurrent reads
const promises = connections.map((conn, index) => { const promises = connections.map((conn, index) => {
return new Promise((resolve) => { return new Promise((resolve, reject) => {
const result = conn.prepare('SELECT ? as id').get(index); try {
resolve(result); const result = conn.prepare('SELECT ? as id').get(index);
resolve(result);
} catch (error) {
reject(error);
}
}); });
}); });
@@ -148,12 +152,32 @@ describe('Database Connection Management', () => {
expect(results).toHaveLength(connectionCount); expect(results).toHaveLength(connectionCount);
} finally { } finally {
// Cleanup connections // Cleanup connections - ensure all are closed even if some fail
connections.forEach(conn => conn.close()); await Promise.all(
if (fs.existsSync(dbPath)) { connections.map(async (conn) => {
fs.unlinkSync(dbPath); try {
fs.unlinkSync(`${dbPath}-wal`); if (conn.open) {
fs.unlinkSync(`${dbPath}-shm`); conn.close();
}
} catch (error) {
// Ignore close errors
}
})
);
// Clean up files with error handling
try {
if (fs.existsSync(dbPath)) {
fs.unlinkSync(dbPath);
}
if (fs.existsSync(`${dbPath}-wal`)) {
fs.unlinkSync(`${dbPath}-wal`);
}
if (fs.existsSync(`${dbPath}-shm`)) {
fs.unlinkSync(`${dbPath}-shm`);
}
} catch (error) {
// Ignore cleanup errors
} }
} }
}); });
@@ -285,7 +309,7 @@ describe('Database Connection Management', () => {
db.exec('ROLLBACK'); db.exec('ROLLBACK');
conn2.close(); conn2.close();
} }
}); }, { timeout: 5000 }); // Add explicit timeout
}); });
describe('Database Configuration', () => { describe('Database Configuration', () => {

View File

@@ -50,13 +50,21 @@ describe('MSW Setup Verification', () => {
}); });
describe('Integration Test Server', () => { describe('Integration Test Server', () => {
let serverStarted = false;
beforeAll(() => { beforeAll(() => {
// Start a separate MSW instance for more control // Only start if not already running
mswTestServer.start({ onUnhandledRequest: 'error' }); if (!serverStarted) {
mswTestServer.start({ onUnhandledRequest: 'error' });
serverStarted = true;
}
}); });
afterAll(() => { afterAll(() => {
mswTestServer.stop(); if (serverStarted) {
mswTestServer.stop();
serverStarted = false;
}
}); });
it('should handle workflow creation with custom response', async () => { it('should handle workflow creation with custom response', async () => {
@@ -163,7 +171,7 @@ describe('MSW Setup Verification', () => {
expect(requests).toHaveLength(2); expect(requests).toHaveLength(2);
expect(requests[0].url).toContain('/api/v1/workflows'); expect(requests[0].url).toContain('/api/v1/workflows');
expect(requests[1].url).toContain('/api/v1/executions'); expect(requests[1].url).toContain('/api/v1/executions');
}); }, { timeout: 10000 }); // Increase timeout for this specific test
it('should work with scoped handlers', async () => { it('should work with scoped handlers', async () => {
const result = await mswTestServer.withScope( const result = await mswTestServer.withScope(

View File

@@ -66,17 +66,34 @@ export const mswTestServer = {
waitForRequests: (count: number, timeout = 5000): Promise<Request[]> => { waitForRequests: (count: number, timeout = 5000): Promise<Request[]> => {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const requests: Request[] = []; const requests: Request[] = [];
const timeoutId = setTimeout(() => { let timeoutId: NodeJS.Timeout | null = null;
reject(new Error(`Timeout waiting for ${count} requests. Got ${requests.length}`));
}, timeout); // Event handler function to allow cleanup
const handleRequest = ({ request }: { request: Request }) => {
integrationTestServer.events.on('request:match', ({ request }) => {
requests.push(request); requests.push(request);
if (requests.length === count) { if (requests.length === count) {
clearTimeout(timeoutId); cleanup();
resolve(requests); resolve(requests);
} }
}); };
// Cleanup function to remove listener and clear timeout
const cleanup = () => {
if (timeoutId) {
clearTimeout(timeoutId);
timeoutId = null;
}
integrationTestServer.events.removeListener('request:match', handleRequest);
};
// Set timeout
timeoutId = setTimeout(() => {
cleanup();
reject(new Error(`Timeout waiting for ${count} requests. Got ${requests.length}`));
}, timeout);
// Add event listener
integrationTestServer.events.on('request:match', handleRequest);
}); });
}, },
@@ -86,14 +103,28 @@ export const mswTestServer = {
verifyNoUnhandledRequests: (): Promise<void> => { verifyNoUnhandledRequests: (): Promise<void> => {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
let hasUnhandled = false; let hasUnhandled = false;
let timeoutId: NodeJS.Timeout | null = null;
integrationTestServer.events.on('request:unhandled', ({ request }) => { const handleUnhandled = ({ request }: { request: Request }) => {
hasUnhandled = true; hasUnhandled = true;
cleanup();
reject(new Error(`Unhandled request: ${request.method} ${request.url}`)); reject(new Error(`Unhandled request: ${request.method} ${request.url}`));
}); };
const cleanup = () => {
if (timeoutId) {
clearTimeout(timeoutId);
timeoutId = null;
}
integrationTestServer.events.removeListener('request:unhandled', handleUnhandled);
};
// Add event listener
integrationTestServer.events.on('request:unhandled', handleUnhandled);
// Give a small delay to allow any pending requests // Give a small delay to allow any pending requests
setTimeout(() => { timeoutId = setTimeout(() => {
cleanup();
if (!hasUnhandled) { if (!hasUnhandled) {
resolve(); resolve();
} }

View File

@@ -77,7 +77,7 @@ function setTestDefaults(): void {
TEST_TIMEOUT_UNIT: '5000', TEST_TIMEOUT_UNIT: '5000',
TEST_TIMEOUT_INTEGRATION: '15000', TEST_TIMEOUT_INTEGRATION: '15000',
TEST_TIMEOUT_E2E: '30000', TEST_TIMEOUT_E2E: '30000',
TEST_TIMEOUT_GLOBAL: '60000', TEST_TIMEOUT_GLOBAL: '30000', // Reduced from 60s to 30s to catch hangs faster
// Test execution // Test execution
TEST_RETRY_ATTEMPTS: '2', TEST_RETRY_ATTEMPTS: '2',