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:
@@ -16,6 +16,16 @@
|
||||
* - `FAKE_MALFORMED`: `initialize` returns `{}` (no serverInfo); `prompt` returns `{}` (no accepted).
|
||||
* - `FAKE_MALFORMED_PROMPT`: `initialize` is normal; only `prompt` returns `{}` (no accepted).
|
||||
* - `FAKE_INIT_ERROR`: `initialize` answers a JSON-RPC error response with code 7.
|
||||
* - `FAKE_INIT_ERROR_ONCE_FILE`: fail `initialize` (code 7) only when this
|
||||
* marker file does NOT exist yet, creating it — so the first runtime
|
||||
* process fails the handshake and a respawned one succeeds (retry probe).
|
||||
* - `FAKE_ECHO_CWD_IN_INIT`: reply `serverInfo.version` = this process's cwd
|
||||
* (wire-visible spawn-cwd probe).
|
||||
* - `FAKE_MALFORMED_EVENT`: the turn's `session.event` carries a number as
|
||||
* the event; `FAKE_MALFORMED_MESSAGE`: assistant/message content is not an
|
||||
* array; `FAKE_MESSAGE_WITHOUT_DATA`: assistant/message with no data
|
||||
* member; `FAKE_MALFORMED_REASON`: `session.finished` reason is a bare
|
||||
* string (wire-validation probes).
|
||||
* - `FAKE_HANG_INIT`: never answer `initialize` (mid-handshake cancel probe).
|
||||
* - `FAKE_INIT_READY` + `FAKE_INIT_GO`: touch the READY file when `initialize`
|
||||
* arrives, then poll for the GO file before answering (deterministic
|
||||
@@ -78,8 +88,20 @@ function assistantText(): string {
|
||||
|
||||
function runTurn(sessionId: string): void {
|
||||
const text = assistantText()
|
||||
if (env.FAKE_MALFORMED_EVENT !== undefined) {
|
||||
notify('session.event', { sessionId, event: 42 })
|
||||
return
|
||||
}
|
||||
event(sessionId, 'turn/start', { turn: 0 })
|
||||
event(sessionId, 'assistant/chunk', { turn: 0, step: 0, chunk: { type: 'text-delta', index: 0, text } })
|
||||
if (env.FAKE_MALFORMED_MESSAGE !== undefined) {
|
||||
event(sessionId, 'assistant/message', { turn: 0, step: 0, content: 'not-an-array' })
|
||||
return
|
||||
}
|
||||
if (env.FAKE_MESSAGE_WITHOUT_DATA !== undefined) {
|
||||
notify('session.event', { sessionId, event: { type: 'assistant/message', seq: seq++, time: 0 } })
|
||||
return
|
||||
}
|
||||
event(sessionId, 'assistant/message', {
|
||||
turn: 0,
|
||||
step: 0,
|
||||
@@ -110,7 +132,9 @@ function runTurn(sessionId: string): void {
|
||||
notify('session.finished', {
|
||||
sessionId,
|
||||
status: env.FAKE_STATUS ?? 'ok',
|
||||
...(reasonKind === 'none' ? {} : { reason: { kind: reasonKind } }),
|
||||
...(env.FAKE_MALFORMED_REASON !== undefined
|
||||
? { reason: 'not-a-record' }
|
||||
: reasonKind === 'none' ? {} : { reason: { kind: reasonKind } }),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -144,10 +168,19 @@ reader.on('line', (line) => {
|
||||
write({ jsonrpc: '2.0', id: frame.id, error: { code: 7, message: 'scripted init failure', data: { hint: 'fake' } } })
|
||||
return
|
||||
}
|
||||
if (env.FAKE_INIT_ERROR_ONCE_FILE !== undefined && !existsSync(env.FAKE_INIT_ERROR_ONCE_FILE)) {
|
||||
writeFileSync(env.FAKE_INIT_ERROR_ONCE_FILE, 'failed-once\n')
|
||||
write({ jsonrpc: '2.0', id: frame.id, error: { code: 7, message: 'scripted first-boot failure' } })
|
||||
return
|
||||
}
|
||||
if (env.FAKE_MALFORMED !== undefined) {
|
||||
respond({})
|
||||
return
|
||||
}
|
||||
if (env.FAKE_ECHO_CWD_IN_INIT !== undefined) {
|
||||
respond({ serverInfo: { name: 'deepseek-harness-sdk-runtime', version: process.cwd() } })
|
||||
return
|
||||
}
|
||||
respond({ serverInfo: { name: 'deepseek-harness-sdk-runtime', version: '0.0.1' } })
|
||||
return
|
||||
case 'session/prompt': {
|
||||
|
||||
Reference in New Issue
Block a user