Skip to content

Commit eaac1da

Browse files
enkoclaude
andcommitted
fix(backend): Prevent integration test flake from container startup timeouts
The suite hooks (beforeAll/afterAll) used the same 120s budget as the container startup timeout, leaving zero headroom for migrations and app boot. Under load (e.g. pre-push hooks running tests, PHP tests, and builds concurrently) a slow container start ate the whole hook budget and the suite failed with "Hook timed out in 120000ms". Introduce shared timeout constants in tests/integration/timeouts.ts and use them everywhere: container startup stays at 120s, suite hooks get 300s to leave headroom. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent ed01060 commit eaac1da

6 files changed

Lines changed: 38 additions & 13 deletions

File tree

apps/backend/tests/integration/auth.helpers.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { resetAuth } from '../../src/lib/auth.js';
1313
import { resetRateLimiters } from '../../src/middleware/rate-limit.js';
1414
import type { AppContext } from '../../src/types/context.js';
1515
import { resetConfig } from '../../src/utils/config.js';
16+
import { CONTAINER_STARTUP_TIMEOUT_MS, SUITE_HOOK_TIMEOUT_MS } from './timeouts.js';
1617

1718
const __filename = fileURLToPath(import.meta.url);
1819
const __dirname = path.dirname(__filename);
@@ -33,7 +34,7 @@ export async function setupAuthTests(): Promise<AuthTestContext> {
3334
.withDatabase('test')
3435
.withUsername('test')
3536
.withPassword('test')
36-
.withStartupTimeout(120000)
37+
.withStartupTimeout(CONTAINER_STARTUP_TIMEOUT_MS)
3738
.withWaitStrategy(Wait.forHealthCheck())
3839
.start();
3940

@@ -373,7 +374,7 @@ export function setupAuthTestSuite() {
373374
vi.stubEnv('LOG_LEVEL', 'silent');
374375

375376
context = await setupAuthTests();
376-
}, 120000); // 120 second timeout for container startup
377+
}, SUITE_HOOK_TIMEOUT_MS);
377378

378379
beforeEach(() => {
379380
// Reset rate limiters before each test to avoid test interference
@@ -384,7 +385,7 @@ export function setupAuthTestSuite() {
384385
await teardownAuthTests(context);
385386
vi.unstubAllEnvs();
386387
resetConfig();
387-
}, 120000);
388+
}, SUITE_HOOK_TIMEOUT_MS);
388389

389390
return {
390391
getContext: () => context,

apps/backend/tests/integration/friends.helpers.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
setupAuthTests,
1212
teardownAuthTests,
1313
} from './auth.helpers.js';
14+
import { SUITE_HOOK_TIMEOUT_MS } from './timeouts.js';
1415

1516
export interface FriendsTestContext extends AuthTestContext {
1617
testUser: {
@@ -182,7 +183,7 @@ export function setupFriendsTestSuite() {
182183
...authContext,
183184
testUser,
184185
};
185-
}, 120000);
186+
}, SUITE_HOOK_TIMEOUT_MS);
186187

187188
beforeEach(async () => {
188189
resetRateLimiters();
@@ -196,7 +197,7 @@ export function setupFriendsTestSuite() {
196197
await teardownAuthTests(context);
197198
vi.unstubAllEnvs();
198199
resetConfig();
199-
}, 120000);
200+
}, SUITE_HOOK_TIMEOUT_MS);
200201

201202
return {
202203
getContext: () => context,

apps/backend/tests/integration/health.test.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest';
77
import { createApp } from '../../src/index.js';
88
import type { AppContext } from '../../src/types/context.js';
99
import { resetConfig } from '../../src/utils/config.js';
10+
import { CONTAINER_STARTUP_TIMEOUT_MS, SUITE_HOOK_TIMEOUT_MS } from './timeouts.js';
1011

1112
describe('Health Endpoint Integration Tests', () => {
1213
let container: StartedPostgreSqlContainer;
@@ -27,7 +28,7 @@ describe('Health Endpoint Integration Tests', () => {
2728
.withDatabase('test')
2829
.withUsername('test')
2930
.withPassword('test')
30-
.withStartupTimeout(120000)
31+
.withStartupTimeout(CONTAINER_STARTUP_TIMEOUT_MS)
3132
.withWaitStrategy(Wait.forHealthCheck())
3233
.start();
3334

@@ -49,7 +50,7 @@ describe('Health Endpoint Integration Tests', () => {
4950

5051
// Create the Hono app
5152
app = await createApp(pool);
52-
}, 120000); // 120 second timeout for container startup
53+
}, SUITE_HOOK_TIMEOUT_MS);
5354

5455
afterAll(async () => {
5556
// Clean up
@@ -61,7 +62,7 @@ describe('Health Endpoint Integration Tests', () => {
6162
}
6263
vi.unstubAllEnvs();
6364
resetConfig();
64-
}, 120000);
65+
}, SUITE_HOOK_TIMEOUT_MS);
6566

6667
describe('GET /health', () => {
6768
it('should return 200 and healthy status when database is connected', async () => {

apps/backend/tests/integration/postgis.helpers.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { Wait } from 'testcontainers';
99
import { afterAll, beforeAll, vi } from 'vitest';
1010
import { PostGISAddressClient } from '../../src/services/external/postgis-address.client.js';
1111
import { resetConfig } from '../../src/utils/config.js';
12+
import { CONTAINER_STARTUP_TIMEOUT_MS, SUITE_HOOK_TIMEOUT_MS } from './timeouts.js';
1213

1314
const __filename = fileURLToPath(import.meta.url);
1415
const __dirname = path.dirname(__filename);
@@ -43,7 +44,7 @@ export async function setupPostGISTests(): Promise<PostGISTestContext> {
4344
.withDatabase('test')
4445
.withUsername('test')
4546
.withPassword('test')
46-
.withStartupTimeout(120000)
47+
.withStartupTimeout(CONTAINER_STARTUP_TIMEOUT_MS)
4748
.withWaitStrategy(Wait.forHealthCheck())
4849
.start();
4950

@@ -181,13 +182,13 @@ export function setupPostGISTestSuite() {
181182
vi.stubEnv('LOG_LEVEL', 'silent');
182183

183184
context = await setupPostGISTests();
184-
}, 180000); // 180 second timeout for container startup
185+
}, SUITE_HOOK_TIMEOUT_MS);
185186

186187
afterAll(async () => {
187188
await teardownPostGISTests(context);
188189
vi.unstubAllEnvs();
189190
resetConfig();
190-
}, 60000);
191+
}, SUITE_HOOK_TIMEOUT_MS);
191192

192193
return {
193194
getContext: () => context,

apps/backend/tests/integration/search.helpers.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
createTestFriend,
1111
type FriendsTestContext,
1212
} from './friends.helpers.js';
13+
import { SUITE_HOOK_TIMEOUT_MS } from './timeouts.js';
1314

1415
// Re-export common helpers
1516
export { authHeaders, createTestFriend };
@@ -323,7 +324,7 @@ export function setupSearchTestSuite() {
323324
...authContext,
324325
testUser,
325326
};
326-
}, 120000);
327+
}, SUITE_HOOK_TIMEOUT_MS);
327328

328329
beforeEach(async () => {
329330
resetRateLimiters();
@@ -340,7 +341,7 @@ export function setupSearchTestSuite() {
340341
await teardownAuthTests(context);
341342
vi.unstubAllEnvs();
342343
resetConfig();
343-
}, 120000);
344+
}, SUITE_HOOK_TIMEOUT_MS);
344345

345346
return {
346347
getContext: () => context,
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
/**
2+
* Shared timeouts for integration test suites.
3+
*
4+
* The integration tests start one PostGIS testcontainer per test file. The
5+
* suite hooks (beforeAll/afterAll) must allow for the container startup
6+
* timeout PLUS migrations and app boot - if both budgets are equal, a slow
7+
* container start (e.g. while pre-push hooks run tests, PHP tests, and builds
8+
* concurrently) eats the whole hook budget and the suite flakes with
9+
* "Hook timed out".
10+
*/
11+
12+
/** Maximum time for the PostgreSQL/PostGIS container itself to become healthy. */
13+
export const CONTAINER_STARTUP_TIMEOUT_MS = 120_000;
14+
15+
/**
16+
* Timeout for beforeAll/afterAll suite hooks. Deliberately larger than
17+
* CONTAINER_STARTUP_TIMEOUT_MS to leave headroom for migrations, app boot,
18+
* and teardown under load.
19+
*/
20+
export const SUITE_HOOK_TIMEOUT_MS = 300_000;

0 commit comments

Comments
 (0)