mirror of
https://github.com/deepseek-ai/deepseek-harness.git
synced 2026-08-29 04:26:38 +00:00
128 lines
3.9 KiB
TypeScript
128 lines
3.9 KiB
TypeScript
// Test-local programmable IApiClient fake (NOT the fixture: fixture is a demo
|
|
// data source on a real clock; behavior tests need per-case responses and
|
|
// deferred-controlled timing). The generation source is a hand pump.
|
|
import type { IApiClient, RpcResponse } from '../src/client/api.ts'
|
|
import type { ConnectionGenerationSource } from '../src/client/connection.ts'
|
|
import { RpcId } from '../src/client/api.ts'
|
|
|
|
export interface Deferred<T> {
|
|
promise: Promise<T>
|
|
resolve(value: T): void
|
|
reject(error: unknown): void
|
|
}
|
|
|
|
/** Test-held settlement: the case decides when an RPC lands (history-pending injections etc.). */
|
|
export function deferred<T>(): Deferred<T> {
|
|
let resolve!: (value: T) => void
|
|
let reject!: (error: unknown) => void
|
|
const promise = new Promise<T>((res, rej) => {
|
|
resolve = res
|
|
reject = rej
|
|
})
|
|
return { promise, resolve, reject }
|
|
}
|
|
|
|
let nextRpc = 0
|
|
|
|
export function ok<T>(value: T): RpcResponse<T> {
|
|
return { rpcId: RpcId(`fake-${nextRpc++}`), result: { ok: true, value } }
|
|
}
|
|
|
|
|
|
type StreamItem = { kind: 'end' } | { kind: 'fail'; error: unknown }
|
|
|
|
interface StreamConn {
|
|
feed(item: StreamItem): void
|
|
}
|
|
|
|
export class FakeApiClient implements IApiClient {
|
|
/** Chronological call record: [method, payload]. */
|
|
readonly calls: { method: string; payload: unknown }[] = []
|
|
|
|
// Programmable slots (defaults answer OK-empty); reassign per case.
|
|
onDescribe: (payload: unknown) => Promise<RpcResponse<{
|
|
version: string
|
|
cwd: string
|
|
attachedSessions: number
|
|
home: string
|
|
canOpenPath: boolean
|
|
}>> =
|
|
() => Promise.resolve(ok({
|
|
version: '0-fake', cwd: '/f', attachedSessions: 0, home: '/h', canOpenPath: true,
|
|
}))
|
|
|
|
private readonly generationConns: StreamConn[] = []
|
|
|
|
readonly host: IApiClient['host'] = {
|
|
describe: payload => this.record('host.describe', payload, this.onDescribe(payload)),
|
|
}
|
|
|
|
/** When true, the source never reports ready. */
|
|
suppressGenerationReady = false
|
|
|
|
/** When true, ready callbacks remain parked until the test releases them. */
|
|
holdGenerationReady = false
|
|
private heldOpens: (() => void)[] = []
|
|
|
|
releaseGenerationReady(): void {
|
|
const held = this.heldOpens
|
|
this.heldOpens = []
|
|
for (const fire of held) fire()
|
|
}
|
|
|
|
readonly generation: ConnectionGenerationSource = (signal, ready) =>
|
|
this.openGeneration(signal, ready)
|
|
|
|
/** End (clean close) or fail (throw) every open stream — reconnect-path material. */
|
|
endStreams(): void {
|
|
for (const conn of [...this.generationConns]) conn.feed({ kind: 'end' })
|
|
}
|
|
|
|
failStreams(error: unknown): void {
|
|
for (const conn of [...this.generationConns]) conn.feed({ kind: 'fail', error })
|
|
}
|
|
|
|
get openGenerationCount(): number {
|
|
return this.generationConns.length
|
|
}
|
|
|
|
callsOf(method: string): unknown[] {
|
|
return this.calls.filter(c => c.method === method).map(c => c.payload)
|
|
}
|
|
|
|
private record<T>(method: string, payload: unknown, response: Promise<T>): Promise<T> {
|
|
this.calls.push({ method, payload })
|
|
return response
|
|
}
|
|
|
|
private async openGeneration(signal: AbortSignal, onOpen: () => void): Promise<void> {
|
|
const inbox: StreamItem[] = []
|
|
let wake: (() => void) | null = null
|
|
const conn: StreamConn = {
|
|
feed: (item) => {
|
|
inbox.push(item)
|
|
wake?.()
|
|
},
|
|
}
|
|
this.generationConns.push(conn)
|
|
if (this.holdGenerationReady) this.heldOpens.push(onOpen)
|
|
else if (!this.suppressGenerationReady) onOpen()
|
|
try {
|
|
while (!signal.aborted) {
|
|
while (inbox.length > 0) {
|
|
const item = inbox.shift() as StreamItem
|
|
if (item.kind === 'end') return
|
|
if (item.kind === 'fail') throw item.error
|
|
}
|
|
await new Promise<void>((resolve) => {
|
|
wake = resolve
|
|
signal.addEventListener('abort', () => { resolve() }, { once: true })
|
|
})
|
|
wake = null
|
|
}
|
|
} finally {
|
|
this.generationConns.splice(this.generationConns.indexOf(conn), 1)
|
|
}
|
|
}
|
|
}
|