Skip to content

Commit 5172265

Browse files
committed
test(web): add unit tests for healthz endpoint
- Refactor healthz route to use dependency injection pattern for testability - Add comprehensive tests for success and error cases - Tests verify response structure, error handling, and timestamp format Prevents regressions in health check behavior.
1 parent d8c47f3 commit 5172265

File tree

3 files changed

+128
-21
lines changed

3 files changed

+128
-21
lines changed
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
import { describe, test, expect } from 'bun:test'
2+
3+
import { getHealthz } from '../_get'
4+
5+
import type { HealthzDeps } from '../_get'
6+
7+
describe('/api/healthz route', () => {
8+
describe('Success cases', () => {
9+
test('returns 200 with status ok and agent count', async () => {
10+
const mockGetAgentCount = async () => 42
11+
12+
const response = await getHealthz({ getAgentCount: mockGetAgentCount })
13+
14+
expect(response.status).toBe(200)
15+
const body = await response.json()
16+
expect(body.status).toBe('ok')
17+
expect(body.cached_agents).toBe(42)
18+
expect(body.timestamp).toBeDefined()
19+
expect(typeof body.timestamp).toBe('string')
20+
})
21+
22+
test('returns correct count when no agents exist', async () => {
23+
const mockGetAgentCount = async () => 0
24+
25+
const response = await getHealthz({ getAgentCount: mockGetAgentCount })
26+
27+
expect(response.status).toBe(200)
28+
const body = await response.json()
29+
expect(body.status).toBe('ok')
30+
expect(body.cached_agents).toBe(0)
31+
})
32+
33+
test('returns correct count for large number of agents', async () => {
34+
const mockGetAgentCount = async () => 10000
35+
36+
const response = await getHealthz({ getAgentCount: mockGetAgentCount })
37+
38+
expect(response.status).toBe(200)
39+
const body = await response.json()
40+
expect(body.status).toBe('ok')
41+
expect(body.cached_agents).toBe(10000)
42+
})
43+
})
44+
45+
describe('Error handling', () => {
46+
test('returns 200 with error flag when getAgentCount throws', async () => {
47+
const mockGetAgentCount = async () => {
48+
throw new Error('Database connection failed')
49+
}
50+
51+
const response = await getHealthz({ getAgentCount: mockGetAgentCount })
52+
53+
// Should still return 200 so health check passes
54+
expect(response.status).toBe(200)
55+
const body = await response.json()
56+
expect(body.status).toBe('ok')
57+
expect(body.agent_count_error).toBe(true)
58+
expect(body.error).toBe('Database connection failed')
59+
expect(body.cached_agents).toBeUndefined()
60+
})
61+
62+
test('handles non-Error exceptions gracefully', async () => {
63+
const mockGetAgentCount = async () => {
64+
throw 'String error'
65+
}
66+
67+
const response = await getHealthz({ getAgentCount: mockGetAgentCount })
68+
69+
expect(response.status).toBe(200)
70+
const body = await response.json()
71+
expect(body.status).toBe('ok')
72+
expect(body.agent_count_error).toBe(true)
73+
expect(body.error).toBe('Unknown error')
74+
})
75+
})
76+
77+
describe('Response format', () => {
78+
test('response has correct Content-Type header', async () => {
79+
const mockGetAgentCount = async () => 100
80+
81+
const response = await getHealthz({ getAgentCount: mockGetAgentCount })
82+
83+
expect(response.headers.get('content-type')).toContain('application/json')
84+
})
85+
86+
test('timestamp is in ISO format', async () => {
87+
const mockGetAgentCount = async () => 50
88+
89+
const response = await getHealthz({ getAgentCount: mockGetAgentCount })
90+
const body = await response.json()
91+
92+
// Verify timestamp is valid ISO date
93+
const timestamp = new Date(body.timestamp)
94+
expect(timestamp.toString()).not.toBe('Invalid Date')
95+
expect(body.timestamp).toMatch(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/)
96+
})
97+
})
98+
})

web/src/app/api/healthz/_get.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { NextResponse } from 'next/server'
2+
3+
export interface HealthzDeps {
4+
getAgentCount: () => Promise<number>
5+
}
6+
7+
export const getHealthz = async ({ getAgentCount }: HealthzDeps) => {
8+
try {
9+
// Get a lightweight count of agents without caching the full data
10+
// This avoids the unstable_cache 2MB limit warning
11+
const agentCount = await getAgentCount()
12+
13+
return NextResponse.json({
14+
status: 'ok',
15+
cached_agents: agentCount,
16+
timestamp: new Date().toISOString(),
17+
})
18+
} catch (error) {
19+
console.error('[Healthz] Failed to get agent count:', error)
20+
21+
// Still return 200 so health check passes, but indicate the error
22+
return NextResponse.json({
23+
status: 'ok',
24+
agent_count_error: true,
25+
error: error instanceof Error ? error.message : 'Unknown error',
26+
})
27+
}
28+
}

web/src/app/api/healthz/route.ts

Lines changed: 2 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,6 @@
1-
import { NextResponse } from 'next/server'
21
import { getAgentCount } from '@/server/agents-data'
2+
import { getHealthz } from './_get'
33

44
export const GET = async () => {
5-
try {
6-
// Get a lightweight count of agents without caching the full data
7-
// This avoids the unstable_cache 2MB limit warning
8-
const agentCount = await getAgentCount()
9-
10-
return NextResponse.json({
11-
status: 'ok',
12-
cached_agents: agentCount,
13-
timestamp: new Date().toISOString(),
14-
})
15-
} catch (error) {
16-
console.error('[Healthz] Failed to get agent count:', error)
17-
18-
// Still return 200 so health check passes, but indicate the error
19-
return NextResponse.json({
20-
status: 'ok',
21-
agent_count_error: true,
22-
error: error instanceof Error ? error.message : 'Unknown error',
23-
})
24-
}
5+
return getHealthz({ getAgentCount })
256
}

0 commit comments

Comments
 (0)