mirror of
https://github.com/deepseek-ai/deepseek-harness.git
synced 2026-08-29 04:26:38 +00:00
test(client): cover reconnect and question scope paths
This commit is contained in:
+2
-2
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-08-18-session-history-and-event-transport.md
|
||||
2026-08-18-session-history-and-event-transport.md: 3d2b6737bcff1a4f929fc5b878f76e1be804a69d
|
||||
2026-08-18-session-history-and-event-transport.zh.md: 95d271256468e05800b67b3b6989d8909be1ac84
|
||||
2026-08-18-session-history-and-event-transport.md: 206d3d13d1f183b97b02b89644b022367be2ebfd
|
||||
2026-08-18-session-history-and-event-transport.zh.md: 3d0b5d4ab768ecd3877bfde86822245d0b63ac49
|
||||
|
||||
+3
-3
@@ -258,11 +258,11 @@ Returning waterfalls currently support Agent scope only. The event signature mus
|
||||
|
||||
The Host projects only top-level `agent` and `signal` fields from the request: `agent` becomes top-level `agentId` in the frame, `signal` becomes the delivery lifetime, and all remaining fields must be lossless JSON as a whole.
|
||||
|
||||
The Client synchronously resolves `agentId` to an existing Agent Context, restores the current delivery signal into the request's direct `signal` field, and invokes Cordis `waterfall()` on the target Context's private key.
|
||||
The Client synchronously resolves or materializes an Agent Context from `agentId`, restores the current delivery signal into the request's direct `signal` field, and invokes Cordis `waterfall()` on the target Context's private key. Before the first successful Session-list baseline, the Session-backed adapter lets transport materialize a scope; after that baseline, the list lifecycle owns scope liveness.
|
||||
|
||||
The system does not scan arbitrarily deep objects, transmit path arrays or placeholders, deep-clone/restore Context and AbortSignal, or wait for a future Agent Context.
|
||||
|
||||
When no Client adapter is registered, the Agent Context is absent, or that Context has been disposed, that Client immediately returns `next`. It does not subscribe to a registry, recheck races after resolution, or create a temporary Fiber for one delivery.
|
||||
When no Client adapter is registered, its resolver returns no Context, or resolution throws, that Client immediately returns `next`. It does not subscribe to a registry, recheck races after resolution, or create a temporary Fiber for one delivery.
|
||||
|
||||
Gateway Host retains `eventId`, the Host continuation, and delivered Client generations for every unfinished waterfall. A new Client generation receives a replay of the same pending event.
|
||||
|
||||
@@ -310,7 +310,7 @@ API Proxy carries only independent business APIs it owns. Session, Workspace, Re
|
||||
|
||||
**Project Agent scope through arbitrary object depth.** Recursive Context and AbortSignal scans need path, placeholder, clone, and restore protocols and turn incidental object structure into a wire promise. Top-level `agent` and `signal` cover current waterfalls.
|
||||
|
||||
**Wait for a Client Agent Context or adapter before dispatching.** Registry waiters, post-resolution race checks, and temporary delivery Fibers add lifecycle to a Client that can delegate immediately. Returning `next` when the target is absent preserves Cordis waterfall semantics.
|
||||
**Wait for a Client Agent Context or adapter before dispatching.** Registry waiters, post-resolution race checks, and temporary delivery Fibers add lifecycle to a Client that can synchronously resolve or materialize its target. Returning `next` when the resolver cannot provide a target immediately preserves Cordis waterfall semantics.
|
||||
|
||||
**Use an independent physical WebSocket or duplex stream for Remote Event.** Gateway mux already provides authenticated upgrade, multiplexing, cancellation, error mapping, and reconnect. Downlink `$events` plus HTTP `$events/result` expresses request/response without a third connection.
|
||||
|
||||
|
||||
@@ -209,7 +209,6 @@ export function apply(ctx: Context): void {
|
||||
const controller = new ConnectionController(api, source, {
|
||||
...sinks,
|
||||
onConnected: (next) => {
|
||||
if (!ownsGeneration()) return
|
||||
publishDescription(next)
|
||||
// A description subscriber may synchronously stop the loop. In that
|
||||
// case publishDescription(undefined) has already retracted this
|
||||
@@ -219,7 +218,6 @@ export function apply(ctx: Context): void {
|
||||
sinks.onConnected?.(next)
|
||||
},
|
||||
onStateChange: (state) => {
|
||||
if (!ownsGeneration()) return
|
||||
if (state === 'reconnecting') publishDescription(undefined)
|
||||
if (!ownsGeneration()) return
|
||||
sinks.onStateChange?.(state)
|
||||
|
||||
@@ -213,6 +213,39 @@ describe('connection client apply', () => {
|
||||
}
|
||||
})
|
||||
|
||||
it('does not announce reconnecting after a description subscriber stops the loop', async () => {
|
||||
;(globalThis as Win).location = { hostname: 'localhost', search: '?fixture' }
|
||||
const handle = await mount()
|
||||
const generation = installGeneration(handle)
|
||||
const owner: { loop?: ReturnType<ConnectionHandle['start']> } = {}
|
||||
let stoppedOnRetraction = false
|
||||
const stopDescription = handle.hostDescription.subscribe(() => {
|
||||
if (handle.hostDescription.getSnapshot() !== undefined || owner.loop === undefined) return
|
||||
stoppedOnRetraction = true
|
||||
owner.loop.stop()
|
||||
})
|
||||
const states: string[] = []
|
||||
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => undefined)
|
||||
const loop = handle.start({
|
||||
onStateChange: (state) => { states.push(state) },
|
||||
}, { backoffBaseMs: 10, backoffFactor: 1, backoffMaxMs: 10, generationReadyTimeoutMs: 500 })
|
||||
owner.loop = loop
|
||||
try {
|
||||
await vi.waitFor(() => {
|
||||
expect(handle.hostDescription.getSnapshot()?.canOpenPath).toBe(true)
|
||||
})
|
||||
generation.end()
|
||||
|
||||
await vi.waitFor(() => { expect(stoppedOnRetraction).toBe(true) })
|
||||
expect(handle.hostDescription.getSnapshot()).toBeUndefined()
|
||||
expect(states).toEqual(['connected'])
|
||||
} finally {
|
||||
stopDescription()
|
||||
loop.stop()
|
||||
warnSpy.mockRestore()
|
||||
}
|
||||
})
|
||||
|
||||
it('WebApiClient keeps unary calls on globalThis.fetch', async () => {
|
||||
;(globalThis as Win).location = { hostname: 'localhost', search: '' }
|
||||
const handle = await mount()
|
||||
|
||||
@@ -79,6 +79,7 @@ describe('scoped-dispatch invariants', () => {
|
||||
['tools/post-execute', [{ callId: 'c', name: 't', arguments: {}, agent }, { content: [], isError: false }, () => Promise.resolve({ kind: 'accept' })]],
|
||||
['tools/pre-execute', [{ callId: 'c', name: 't', arguments: {}, agent }, () => Promise.resolve({ kind: 'allow' })]],
|
||||
['tools/result', [{ callId: 'c', name: 't', arguments: {}, agent }, { content: [], isError: false }]],
|
||||
['user-questions/request', [{ agent, questions: [] }, () => Promise.resolve({ answers: [] })]],
|
||||
]
|
||||
|
||||
for (const [event, args] of rows) {
|
||||
|
||||
Reference in New Issue
Block a user