Files
deepseek-harness/packages/client/connection/tests/connection.spec.ts
T
Hypatia May 4819210142 refactor(token-meter): make context occupancy durable projection state
Replace the transient `session/model-request` mux frame with ordinary durable
session state. Occupancy now rides two last-wins projection fields instead of a
non-replayable frame that needed removal tombstones and cross-stream fencing.

The frame was the only non-replayable class on the mux stream. Because host and
mux are independent SSE streams with no cross-stream order, a request emitted
before a removal could arrive after `host/session-removed`, and a legitimate
request for a new lifecycle reusing the same id could be fenced by a late
removal. Fixing that needed a lifecycle generation on every frame; the frame
itself was the problem.

Removed: the `session/model-request` frame and schema, the `agent/model-request`
core event, the ApiProxy measurement point, the client-side telemetry map and
removal tombstone, and the synthetic `cancelled` open error used to signal
reconnect through the error channel.

Added: `request/context`, a log-only session event recording the
registration-bound capacity of the route a request resolved to, appended beside
`request/header` from the lookup that already prepared the call and skipped when
the route is unchanged. Capacity stays out of `EpochHeader` because it is
adapter metadata about a route, not an input the request was built from, so it
must not join request reconstruction or header equality.

The `contextPressure` projection pairs the newest provider-reported prompt size
with the newest recorded capacity. The two are deliberately not one atomic
request observation: switching models can pair a fresh capacity with the prior
route's pressure until the next request reports usage. The figure is a
user-facing reference, and this matches how the TUI status line has always
computed occupancy.
2026-07-30 13:53:08 +08:00

239 lines
9.4 KiB
TypeScript

/**
* ConnectionController: stream pumping into sinks, the strict readiness
* handshake (describe + both streams' onOpen, timeout-guarded), generation
* abort on loss, backoff reconnection, state transitions, and sink-exception
* isolation. Real (short) timers — the timeout and backoff are configurable,
* so tests run them at millisecond scale.
*/
import { describe, expect, it, vi } from 'vitest'
import type { SessionId } from '../src/client/api.ts'
import type { ConnectionState } from '../src/client/connection.ts'
import { ConnectionController } from '../src/client/connection.ts'
import { FakeApiClient, deferred, ok } from './fake-api.ts'
const SID = 'fk-c1' as SessionId
const FAST = { backoffBaseMs: 10, backoffFactor: 1, backoffMaxMs: 10, streamOpenTimeoutMs: 500 }
function subscribedFrame(lastSeq = 0) {
return { type: 'session/subscribed', sessionId: SID, lastSeq } as const
}
describe('connection lifecycle', () => {
it('announces connected after describe + both streams open, then pumps frames to sinks', async () => {
const api = new FakeApiClient()
const muxSeen: string[] = []
let connected = 0
const controller = new ConnectionController(api, {
onMuxEnvelope: envelope => muxSeen.push(envelope.payload.type),
onConnected: () => { connected++ },
}, FAST)
controller.start()
try {
await vi.waitFor(() => { expect(connected).toBe(1) })
api.pushMux(subscribedFrame())
await vi.waitFor(() => { expect(muxSeen).toEqual(['session/subscribed']) })
expect(api.callsOf('host.describe')).toHaveLength(1)
} finally {
controller.stop()
}
})
it('reconnects with a fresh generation when a stream fails, and stop() ends the loop', async () => {
const api = new FakeApiClient()
let connected = 0
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => undefined)
const controller = new ConnectionController(api, { onConnected: () => { connected++ } }, FAST)
controller.start()
try {
await vi.waitFor(() => { expect(connected).toBe(1) })
api.failStreams(new Error('stream torn'))
await vi.waitFor(() => { expect(connected).toBe(2) }) // new generation after backoff
expect(api.openMuxCount).toBe(1) // the dead generation's stream is gone, exactly one live
} finally {
controller.stop()
warnSpy.mockRestore()
}
// stop() aborts the live generation (streams tear down) and no reconnect follows.
await vi.waitFor(() => { expect(api.openMuxCount).toBe(0) })
await new Promise(resolve => setTimeout(resolve, 40))
expect(api.openMuxCount).toBe(0)
})
it('treats describe failure as generation failure and retries', async () => {
const api = new FakeApiClient()
const gate = deferred<Awaited<ReturnType<FakeApiClient['onDescribe']>>>()
let describeCalls = 0
api.onDescribe = () => {
describeCalls++
return describeCalls === 1 ? Promise.reject(new Error('host down')) : gate.promise
}
let connected = 0
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => undefined)
const controller = new ConnectionController(api, { onConnected: () => { connected++ } }, FAST)
controller.start()
try {
await vi.waitFor(() => { expect(describeCalls).toBe(2) }) // retried after backoff
expect(connected).toBe(0) // never announced during the failed generation
gate.resolve(ok({ version: '0', cwd: '/f', attachedSessions: 0 }))
await vi.waitFor(() => { expect(connected).toBe(1) })
} finally {
controller.stop()
warnSpy.mockRestore()
}
})
it('converges stream/error frames into reconnect instead of dispatching them', async () => {
const api = new FakeApiClient()
const muxSeen: string[] = []
let connected = 0
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => undefined)
const controller = new ConnectionController(api, {
onMuxEnvelope: envelope => muxSeen.push(envelope.payload.type),
onConnected: () => { connected++ },
}, FAST)
controller.start()
try {
await vi.waitFor(() => { expect(connected).toBe(1) })
api.pushMux({ type: 'stream/error', error: { code: 'internal', message: 'impl broke', details: {} } })
await vi.waitFor(() => { expect(connected).toBe(2) }) // treated as loss → reconnect
expect(muxSeen).toEqual([]) // never forwarded to the business sink
} finally {
controller.stop()
warnSpy.mockRestore()
}
})
it('isolates sink exceptions from the pump', async () => {
const api = new FakeApiClient()
const seen: string[] = []
let connected = 0
const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => undefined)
const controller = new ConnectionController(api, {
onMuxEnvelope: (envelope) => {
seen.push(envelope.payload.type)
throw new Error('business layer bug')
},
onConnected: () => { connected++ },
}, FAST)
controller.start()
try {
await vi.waitFor(() => { expect(connected).toBe(1) })
api.pushMux(subscribedFrame(1))
api.pushMux(subscribedFrame(2))
await vi.waitFor(() => { expect(seen).toHaveLength(2) }) // second frame still pumped
expect(connected).toBe(1) // no reconnect triggered by the sink throw
} finally {
controller.stop()
errorSpy.mockRestore()
}
})
it('holds onConnected until both streams establish even after describe succeeds', async () => {
const api = new FakeApiClient()
api.holdStreamOpen = true // describe resolves immediately; stream establishment is in the case's hand
let connected = 0
const controller = new ConnectionController(api, { onConnected: () => { connected++ } }, FAST)
controller.start()
try {
await vi.waitFor(() => { expect(api.callsOf('host.describe')).toHaveLength(1) })
await new Promise(resolve => setTimeout(resolve, 30))
expect(connected).toBe(0) // describe alone must not announce
api.releaseStreamOpens()
await vi.waitFor(() => { expect(connected).toBe(1) })
} finally {
controller.stop()
}
})
it('proceeds as connected via the timeout guard when a carrier never fires onOpen', async () => {
const api = new FakeApiClient()
api.suppressStreamOpen = true // misbehaving carrier: streams open but onOpen never fires
let connected = 0
const controller = new ConnectionController(api, { onConnected: () => { connected++ } }, { ...FAST, streamOpenTimeoutMs: 20 })
controller.start()
try {
await vi.waitFor(() => { expect(connected).toBe(1) }) // handshake resolved by the guard, not wedged
} finally {
controller.stop()
}
})
it('emits deduplicated connected/reconnecting state transitions', async () => {
const api = new FakeApiClient()
const states: ConnectionState[] = []
let connected = 0
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => undefined)
const controller = new ConnectionController(api, {
onConnected: () => { connected++ },
onStateChange: state => states.push(state),
}, FAST)
controller.start()
try {
await vi.waitFor(() => { expect(connected).toBe(1) })
expect(states).toEqual(['connected'])
api.failStreams(new Error('torn'))
await vi.waitFor(() => { expect(connected).toBe(2) })
expect(states).toEqual(['connected', 'reconnecting', 'connected'])
} finally {
controller.stop()
warnSpy.mockRestore()
}
})
it('deduplicates consecutive reconnecting emissions across two straight failures', async () => {
const api = new FakeApiClient()
const gate = deferred<Awaited<ReturnType<FakeApiClient['onDescribe']>>>()
let describeCalls = 0
api.onDescribe = () => {
describeCalls++
return describeCalls <= 2 ? Promise.reject(new Error('down')) : gate.promise
}
const states: ConnectionState[] = []
let connected = 0
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => undefined)
const controller = new ConnectionController(api, {
onConnected: () => { connected++ },
onStateChange: state => states.push(state),
}, FAST)
controller.start()
try {
await vi.waitFor(() => { expect(describeCalls).toBe(3) })
gate.resolve(ok({ version: '0', cwd: '/f', attachedSessions: 0 }))
await vi.waitFor(() => { expect(connected).toBe(1) })
expect(states).toEqual(['reconnecting', 'connected']) // two failures, one reconnecting emission
} finally {
controller.stop()
warnSpy.mockRestore()
}
})
it('runs with no sinks at all (every callback slot optional)', async () => {
const api = new FakeApiClient()
const controller = new ConnectionController(api, {}, FAST)
controller.start()
try {
await vi.waitFor(() => { expect(api.callsOf('host.describe')).toHaveLength(1) })
api.pushMux(subscribedFrame()) // pumped with sink undefined: dropped silently
await new Promise(resolve => setTimeout(resolve, 20))
} finally {
controller.stop()
}
})
it('start() is idempotent (one loop, one stream set)', async () => {
const api = new FakeApiClient()
let connected = 0
const controller = new ConnectionController(api, { onConnected: () => { connected++ } }, FAST)
controller.start()
controller.start()
try {
await vi.waitFor(() => { expect(connected).toBe(1) })
expect(api.openMuxCount).toBe(1)
expect(api.callsOf('host.describe')).toHaveLength(1)
} finally {
controller.stop()
}
})
})