mirror of
https://github.com/deepseek-ai/deepseek-harness.git
synced 2026-09-14 04:01:35 +00:00
204 lines
7.2 KiB
TypeScript
204 lines
7.2 KiB
TypeScript
import { EventEmitter } from 'node:events'
|
|
import { Context } from '@deepseek-ai/cordis'
|
|
import { afterEach, describe, expect, it, vi } from 'vitest'
|
|
|
|
const httpMock = vi.hoisted(() => ({ createServer: vi.fn() }))
|
|
|
|
vi.mock('node:http', () => ({ createServer: httpMock.createServer }))
|
|
|
|
// Snapshot plugins are plain runtime JavaScript loaded by cordis.yml.
|
|
// @ts-expect-error The fixture intentionally has no declaration artifact.
|
|
import * as searchFixtureModule from '../snapshots/session/web-search-endpoint-guidance/web-search-error-fixture.mjs'
|
|
// @ts-expect-error The fixture intentionally has no declaration artifact.
|
|
import * as loopbackFixtureModule from '../snapshots/session/loopback-fixture-server.mjs'
|
|
|
|
const RECORDED_ENDPOINT = 'http://127.0.0.1:43118/anthropic/v1/messages'
|
|
|
|
interface FixturePlugin {
|
|
readonly name: string
|
|
readonly inject?: readonly string[]
|
|
apply(ctx: Context): Promise<void>
|
|
}
|
|
|
|
interface LoopbackFixtureOptions {
|
|
readonly label: string
|
|
readonly onCleanup: () => void
|
|
readonly onListening: (address: { port: number }) => void
|
|
readonly requestListener: () => void
|
|
}
|
|
|
|
const searchFixture = searchFixtureModule as unknown as FixturePlugin
|
|
const typedLoopbackFixtureModule = loopbackFixtureModule as unknown as {
|
|
readonly applyLoopbackServerEffect: (ctx: Context, options: LoopbackFixtureOptions) => Promise<void>
|
|
}
|
|
const { applyLoopbackServerEffect } = typedLoopbackFixtureModule
|
|
const nativeFetch = globalThis.fetch
|
|
|
|
class FixtureServer extends EventEmitter {
|
|
readonly started = Promise.withResolvers<undefined>()
|
|
listening = false
|
|
closed = false
|
|
connectionsClosed = false
|
|
unreferenced = false
|
|
private listenCallback: (() => void) | undefined
|
|
private port = 0
|
|
|
|
listen(_port: number, _host: string, callback: () => void): this {
|
|
this.listenCallback = callback
|
|
this.started.resolve(undefined)
|
|
return this
|
|
}
|
|
|
|
finishListening(port = 54321): void {
|
|
this.port = port
|
|
this.listening = true
|
|
this.listenCallback?.()
|
|
}
|
|
|
|
address(): { address: string; family: string; port: number } | null {
|
|
return this.listening ? { address: '127.0.0.1', family: 'IPv4', port: this.port } : null
|
|
}
|
|
|
|
unref(): this {
|
|
this.unreferenced = true
|
|
return this
|
|
}
|
|
|
|
close(callback: (error?: Error) => void): this {
|
|
this.listening = false
|
|
this.closed = true
|
|
callback()
|
|
return this
|
|
}
|
|
|
|
closeAllConnections(): void {
|
|
this.connectionsClosed = true
|
|
}
|
|
}
|
|
|
|
function nextServer(): FixtureServer {
|
|
const server = new FixtureServer()
|
|
httpMock.createServer.mockReturnValueOnce(server)
|
|
return server
|
|
}
|
|
|
|
function captureErrors(ctx: Context): unknown[] {
|
|
const errors: unknown[] = []
|
|
ctx.logger.error = ((error: unknown) => { errors.push(error) }) as typeof ctx.logger.error
|
|
return errors
|
|
}
|
|
|
|
async function disposeWhileStarting(fiber: { dispose(): Promise<unknown> }, server: FixtureServer): Promise<void> {
|
|
await server.started.promise
|
|
const disposal = fiber.dispose()
|
|
const settled = vi.fn()
|
|
void disposal.then(settled)
|
|
await Promise.resolve()
|
|
expect(settled).not.toHaveBeenCalled()
|
|
server.finishListening()
|
|
await disposal
|
|
}
|
|
|
|
afterEach(() => {
|
|
globalThis.fetch = nativeFetch
|
|
httpMock.createServer.mockReset()
|
|
})
|
|
|
|
describe('snapshot HTTP fixture lifecycle', () => {
|
|
it('joins search listener setup and cleanup when disposal wins the startup race', async () => {
|
|
const server = nextServer()
|
|
const ctx = new Context()
|
|
const errors = captureErrors(ctx)
|
|
const fiber = ctx.plugin(searchFixture)
|
|
await disposeWhileStarting(fiber, server)
|
|
|
|
expect(server).toMatchObject({ closed: true, connectionsClosed: true, unreferenced: true })
|
|
expect(globalThis.fetch).toBe(nativeFetch)
|
|
expect(errors).toEqual([])
|
|
})
|
|
|
|
it('runs owner cleanup and closes the listener when disposal wins the startup race', async () => {
|
|
const server = nextServer()
|
|
const ctx = new Context()
|
|
const errors = captureErrors(ctx)
|
|
const onCleanup = vi.fn()
|
|
const onListening = vi.fn()
|
|
const fiber = ctx.plugin({
|
|
name: 'loopback-fixture-lifecycle-test',
|
|
apply: testCtx => applyLoopbackServerEffect(testCtx, {
|
|
label: 'loopback-fixture-lifecycle-test',
|
|
onCleanup,
|
|
onListening,
|
|
requestListener: () => {},
|
|
}),
|
|
})
|
|
await disposeWhileStarting(fiber, server)
|
|
|
|
expect(server).toMatchObject({ closed: true, connectionsClosed: true, unreferenced: true })
|
|
expect(onListening).toHaveBeenCalledWith(expect.objectContaining({ port: 54321 }))
|
|
expect(onCleanup).toHaveBeenCalledOnce()
|
|
expect(errors).toEqual([])
|
|
})
|
|
|
|
it('maps every fetch input form and rejects another path on the recorded authority', async () => {
|
|
const server = nextServer()
|
|
const fetchMock = vi.fn(async (_input: string | URL | Request, _init?: RequestInit) => new Response('{}'))
|
|
globalThis.fetch = fetchMock
|
|
const ctx = new Context()
|
|
const fiber = ctx.plugin(searchFixture)
|
|
await server.started.promise
|
|
server.finishListening(54322)
|
|
await fiber
|
|
|
|
try {
|
|
await globalThis.fetch(RECORDED_ENDPOINT)
|
|
expect(fetchMock.mock.calls.at(-1)?.[0]).toBe('http://127.0.0.1:54322/anthropic/v1/messages')
|
|
|
|
await globalThis.fetch(new URL(RECORDED_ENDPOINT))
|
|
expect(fetchMock.mock.calls.at(-1)?.[0]).toBe('http://127.0.0.1:54322/anthropic/v1/messages')
|
|
|
|
const request = new Request(RECORDED_ENDPOINT, { method: 'POST', headers: { 'x-fixture': 'request' } })
|
|
await globalThis.fetch(request)
|
|
const mappedRequest = fetchMock.mock.calls.at(-1)?.[0]
|
|
expect(mappedRequest).toBeInstanceOf(Request)
|
|
if (!(mappedRequest instanceof Request)) throw new TypeError('mapped fetch input must be a Request')
|
|
expect(mappedRequest.url).toBe('http://127.0.0.1:54322/anthropic/v1/messages')
|
|
expect(mappedRequest.method).toBe('POST')
|
|
expect(mappedRequest.headers.get('x-fixture')).toBe('request')
|
|
|
|
const unrelated = new URL('https://example.test/')
|
|
await globalThis.fetch(unrelated)
|
|
expect(fetchMock.mock.calls.at(-1)?.[0]).toBe(unrelated)
|
|
|
|
await expect(globalThis.fetch('http://127.0.0.1:43118/unexpected'))
|
|
.rejects.toThrow('web-search-error-fixture: unexpected URL for recorded authority')
|
|
} finally {
|
|
await fiber.dispose()
|
|
}
|
|
|
|
expect(globalThis.fetch).toBe(fetchMock)
|
|
expect(server.closed).toBe(true)
|
|
})
|
|
|
|
it('preserves a later fetch wrapper while still closing the listener and reporting the ownership error', async () => {
|
|
const server = nextServer()
|
|
const fetchMock = vi.fn(async (_input: string | URL | Request, _init?: RequestInit) => new Response('{}'))
|
|
globalThis.fetch = fetchMock
|
|
const ctx = new Context()
|
|
const errors = captureErrors(ctx)
|
|
const fiber = ctx.plugin(searchFixture)
|
|
await server.started.promise
|
|
server.finishListening()
|
|
await fiber
|
|
|
|
const fixtureFetch = globalThis.fetch
|
|
const laterFetch = vi.fn((input: string | URL | Request, init?: RequestInit) => fixtureFetch(input, init))
|
|
globalThis.fetch = laterFetch
|
|
await fiber.dispose()
|
|
|
|
expect(globalThis.fetch).toBe(laterFetch)
|
|
expect(server).toMatchObject({ closed: true, connectionsClosed: true })
|
|
expect(errors.map(String).join('\n')).toContain('web-search-error-fixture: global fetch owner changed before cleanup')
|
|
})
|
|
})
|