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
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@tangle-network/agent-eval",
"version": "0.34.1",
"version": "0.35.0",
"description": "Substrate for self-improving agents: traces, verifiable rewards, preferences, GEPA / reflective mutation, auto-research, replay, sequential anytime-valid stats, and release gates.",
"homepage": "https://github.com/tangle-network/agent-eval#readme",
"repository": {
Expand Down
2 changes: 1 addition & 1 deletion src/wire/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,4 @@ export { buildOpenApi } from './openapi'
export { dispatchRpc, runRpcBatch, runRpcOnce } from './rpc'
export { BUILTIN_RUBRICS, getBuiltinRubric, listBuiltinRubrics } from './rubrics'
export * from './schemas'
export { createApp, type ServeOptions, startServer } from './server'
export { createApp, type ServeOptions, type StartedServer, startServer, startServerAsync } from './server'
47 changes: 47 additions & 0 deletions src/wire/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -201,3 +201,50 @@ export function startServer(opts: ServeOptions = {}): ServerType {
console.log(`[agent-eval] serving on http://${address}:${actualPort}`)
})
}

export interface StartedServer {
server: ServerType
/** The OS-assigned port. When opts.port was 0, this is the actual port the
* kernel bound — callers that need to dial back (smoke tests, sidecars
* registering with a parent) read this rather than guessing a free port. */
port: number
/** Resolved host the server bound to (defaults to 127.0.0.1). */
host: string
/** Close the server. Resolves once active connections have drained. */
close(): Promise<void>
}

/**
* Promise-returning variant of `startServer` that resolves once the server is
* listening and surfaces the resolved bound port. Use this from smoke tests
* (`startServerAsync({ port: 0 })`) and any caller that needs to dial back.
*/
export function startServerAsync(opts: ServeOptions = {}): Promise<StartedServer> {
const app = createApp(opts)
const port = opts.port ?? 5005
const host = opts.host ?? '127.0.0.1'
return new Promise((resolve, reject) => {
let settled = false
let server: ServerType | undefined
server = serve({ fetch: app.fetch, port, hostname: host }, ({ address, port: actualPort }) => {
if (settled) return
settled = true
// eslint-disable-next-line no-console
console.log(`[agent-eval] serving on http://${address}:${actualPort}`)
resolve({
server: server!,
port: actualPort,
host: address,
close: () =>
new Promise<void>((res, rej) => {
server!.close((err) => (err ? rej(err) : res()))
}),
})
})
server.on('error', (err) => {
if (settled) return
settled = true
reject(err)
})
})
}
30 changes: 29 additions & 1 deletion tests/wire/server.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
*/
import { describe, expect, it } from 'vitest'

import { createApp } from '../../src/wire/server'
import { createApp, startServerAsync } from '../../src/wire/server'

const app = createApp()

Expand Down Expand Up @@ -97,3 +97,31 @@ describe('POST /v1/judge', () => {
expect(['rubric_not_found', 'validation_error']).toContain(err.error.code)
})
})

describe('startServerAsync', () => {
it('resolves with the actual bound port when opts.port=0', async () => {
const started = await startServerAsync({ port: 0 })
try {
expect(started.port).toBeGreaterThan(0)
expect(started.port).toBeLessThan(65536)
expect(started.host).toBe('127.0.0.1')

const res = await fetch(`http://127.0.0.1:${started.port}/healthz`)
expect(res.status).toBe(200)
const body = (await res.json()) as { status: string }
expect(body.status).toBe('ok')
} finally {
await started.close()
}
})

it('two concurrent servers on port 0 receive distinct ports', async () => {
const [a, b] = await Promise.all([startServerAsync({ port: 0 }), startServerAsync({ port: 0 })])
try {
expect(a.port).not.toBe(b.port)
} finally {
await a.close()
await b.close()
}
})
})
Loading