fix(sdk-client): address ds-review-bot findings
- api: resolve a relative workspace cwd to absolute before the handshake — the child spawns relative to the parent cwd, but the wire cwd is resolved again inside the child, so a relative value double-resolved (worker -> worker/worker). - api: make the documented handshake retry real — HarnessClient.close() is permanent, so a failed initialize now reaps the runtime and swaps in a fresh client; DeepSeekHarness.close() is terminal and stops the respawns. - api: validate session.event envelopes, assistant/message content, and session.finished reasons at the wire boundary — a malformed runtime surfaces as SdkProtocolError instead of type-invalid TurnResult data or a TypeError out of finalResponse. - client: a throwing subscribe() filter fails and detaches only its own subscription (normalized to Error); sibling fan-out and the transport read loop are undisturbed. - client: NotificationSubscription.close() drops its queued notifications, matching its documented contract; runtime-death fail() still leaves already-delivered items drainable. - client: subscribe() after close()/runtime death returns a born-failed subscription so next() rejects instead of parking forever. - client/transport: bounded requests abandon via AbortSignal — the transport drops the pending entry at timeout, so repeated bounded calls against a hung method retain no per-call state. One test per finding; per-file coverage stays 100% on both packages.
This commit is contained in:
@@ -109,15 +109,47 @@ export class JsonRpcLineTransport implements JsonRpcTransportPeer {
|
||||
this.notificationHandler = handler
|
||||
}
|
||||
|
||||
request(method: string, params: object): Promise<unknown> {
|
||||
/**
|
||||
* Send a request and await its response.
|
||||
* @param method - the JSON-RPC method name.
|
||||
* @param params - the request parameters object.
|
||||
* @param signal - optional abandonment signal: aborting removes the pending
|
||||
* entry (no state is retained for a response that may never come) and
|
||||
* rejects with the signal's reason.
|
||||
* @returns the result; rejects per {@link JsonRpcTransportPeer.request}.
|
||||
*/
|
||||
request(method: string, params: object, signal?: AbortSignal): Promise<unknown> {
|
||||
const id = `req_${randomUUID().replaceAll('-', '')}`
|
||||
const message = { jsonrpc: '2.0', id, method, params }
|
||||
return new Promise((resolve, reject) => {
|
||||
this.pending.set(id, { resolve, reject })
|
||||
let detach = (): void => {}
|
||||
if (signal !== undefined) {
|
||||
if (signal.aborted) {
|
||||
reject(abortError(signal.reason))
|
||||
return
|
||||
}
|
||||
const onAbort = (): void => {
|
||||
this.pending.delete(id)
|
||||
reject(abortError(signal.reason))
|
||||
}
|
||||
signal.addEventListener('abort', onAbort, { once: true })
|
||||
detach = () => { signal.removeEventListener('abort', onAbort) }
|
||||
}
|
||||
this.pending.set(id, {
|
||||
resolve: (value) => {
|
||||
detach()
|
||||
resolve(value)
|
||||
},
|
||||
reject: (error) => {
|
||||
detach()
|
||||
reject(error)
|
||||
},
|
||||
})
|
||||
try {
|
||||
this.write(message)
|
||||
} catch (error) {
|
||||
this.pending.delete(id)
|
||||
detach()
|
||||
reject(error instanceof Error ? error : new Error(String(error)))
|
||||
}
|
||||
})
|
||||
@@ -240,3 +272,8 @@ export class JsonRpcLineTransport implements JsonRpcTransportPeer {
|
||||
function objectParams(params: unknown): Record<string, unknown> {
|
||||
return params && typeof params === 'object' && !Array.isArray(params) ? params as Record<string, unknown> : {}
|
||||
}
|
||||
|
||||
/** Normalize an abort reason into the rejection Error (a non-Error reason is stringified). */
|
||||
function abortError(reason: unknown): Error {
|
||||
return reason instanceof Error ? reason : new Error(`JSON-RPC request aborted: ${String(reason)}`)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user