Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 1 addition & 11 deletions apps/server/src/api/routes/health.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,9 @@
*/

import { Hono } from 'hono'
import type { HealthWatchdog } from '../../lib/health-watchdog'

interface HealthRouteConfig {
watchdog?: HealthWatchdog
}

/**
* Health check route group.
* Records health check timestamps for the watchdog (if enabled).
*/
export function createHealthRoute(config: HealthRouteConfig = {}) {
export function createHealthRoute() {
return new Hono().get('/', (c) => {
config.watchdog?.recordHealthCheck()
return c.json({ status: 'ok' })
})
}
4 changes: 2 additions & 2 deletions apps/server/src/api/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,12 +50,12 @@ export async function createHttpServer(config: HttpServerConfig) {
allowRemote,
} = config

const { healthWatchdog, onShutdown } = config
const { onShutdown } = config

// DECLARATIVE route composition - chain .route() calls for type inference
const app = new Hono<Env>()
.use('/*', cors(defaultCorsConfig))
.route('/health', createHealthRoute({ watchdog: healthWatchdog }))
.route('/health', createHealthRoute())
.route(
'/shutdown',
createShutdownRoute({ onShutdown: onShutdown ?? (() => {}) }),
Expand Down
5 changes: 1 addition & 4 deletions apps/server/src/api/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import { z } from 'zod'
import { VercelAIConfigSchema } from '../agent/provider-adapter/types'
import type { McpContext } from '../browser/cdp/context'
import type { ControllerContext } from '../browser/extension/context'
import type { HealthWatchdog } from '../lib/health-watchdog'

import type { MutexPool } from '../lib/mutex'
import type { RateLimiter } from '../lib/rate-limiter/rate-limiter'
import type { ToolDefinition } from '../tools/types/tool-definition'
Expand Down Expand Up @@ -82,9 +82,6 @@ export interface HttpServerConfig {
// For Graph routes
codegenServiceUrl?: string

// For health monitoring
healthWatchdog?: HealthWatchdog

// For shutdown route
onShutdown?: () => void
}
Expand Down
104 changes: 0 additions & 104 deletions apps/server/src/lib/health-watchdog.ts

This file was deleted.

16 changes: 3 additions & 13 deletions apps/server/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import { ControllerContext } from './browser/extension/context'
import type { ServerConfig } from './config'
import { INLINED_ENV } from './env'
import { initializeDb } from './lib/db'
import { HealthWatchdog } from './lib/health-watchdog'

import { identity } from './lib/identity'
import { logger } from './lib/logger'
import { metrics } from './lib/metrics'
Expand All @@ -35,7 +35,6 @@ import { VERSION } from './version'
export class Application {
private config: ServerConfig
private db: Database | null = null
private healthWatchdog: HealthWatchdog | null = null

constructor(config: ServerConfig) {
this.config = config
Expand Down Expand Up @@ -72,12 +71,6 @@ export class Application {
const tools = createToolRegistry(cdpContext, controllerContext)
const mutexPool = new MutexPool()

const isDev = process.env.NODE_ENV === 'development'
if (!isDev) {
this.healthWatchdog = new HealthWatchdog({ logger })
logger.info('Health watchdog enabled')
}

try {
await createHttpServer({
port: this.config.serverPort,
Expand All @@ -92,7 +85,7 @@ export class Application {
executionDir: this.config.executionDir,
rateLimiter: new RateLimiter(this.getDb(), dailyRateLimit),
codegenServiceUrl: this.config.codegenServiceUrl,
healthWatchdog: this.healthWatchdog ?? undefined,

onShutdown: () => this.stop(),
})
} catch (error) {
Expand All @@ -106,17 +99,14 @@ export class Application {
`Health endpoint: http://127.0.0.1:${this.config.serverPort}/health`,
)

// Start the watchdog after HTTP server is ready
this.healthWatchdog?.start()

this.logStartupSummary()

metrics.log('http_server.started', { version: VERSION })
}

stop(): void {
logger.info('Shutting down server...')
this.healthWatchdog?.stop()

// Immediate exit without graceful shutdown. Chromium may kill us on update/restart,
// and we need to free the port instantly so the HTTP port doesn't keep switching.
process.exit(EXIT_CODES.SUCCESS)
Expand Down
59 changes: 7 additions & 52 deletions apps/server/tests/api/routes/health.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,63 +3,18 @@
* Copyright 2025 BrowserOS
*/

import { describe, it, mock } from 'bun:test'
import { describe, it } from 'bun:test'
import assert from 'node:assert'

import { createHealthRoute } from '../../../src/api/routes/health'
import type { HealthWatchdog } from '../../../src/lib/health-watchdog'

describe('createHealthRoute', () => {
describe('without watchdog', () => {
it('returns status ok', async () => {
const route = createHealthRoute()
const response = await route.request('/')
it('returns status ok', async () => {
const route = createHealthRoute()
const response = await route.request('/')

assert.strictEqual(response.status, 200)
const body = await response.json()
assert.deepStrictEqual(body, { status: 'ok' })
})
})

describe('with watchdog', () => {
it('returns status ok and records health check', async () => {
const mockRecordHealthCheck = mock(() => {})
const mockWatchdog = {
recordHealthCheck: mockRecordHealthCheck,
} as unknown as HealthWatchdog

const route = createHealthRoute({ watchdog: mockWatchdog })
const response = await route.request('/')

assert.strictEqual(response.status, 200)
const body = await response.json()
assert.deepStrictEqual(body, { status: 'ok' })

// Verify watchdog was notified
assert.strictEqual(
mockRecordHealthCheck.mock.calls.length,
1,
'recordHealthCheck should be called once',
)
})

it('records health check on every request', async () => {
const mockRecordHealthCheck = mock(() => {})
const mockWatchdog = {
recordHealthCheck: mockRecordHealthCheck,
} as unknown as HealthWatchdog

const route = createHealthRoute({ watchdog: mockWatchdog })

await route.request('/')
await route.request('/')
await route.request('/')

assert.strictEqual(
mockRecordHealthCheck.mock.calls.length,
3,
'recordHealthCheck should be called for each request',
)
})
assert.strictEqual(response.status, 200)
const body = await response.json()
assert.deepStrictEqual(body, { status: 'ok' })
})
})
Loading