refactor(acp): make bridge SDK message handling type-safe (#1265)

* refactor(acp): make bridge SDK message handling type-safe

- Add BridgeSDKMessage type alias to eliminate 14 type errors from void-leaked IteratorResult
- Replace 18 scattered as-casts with a single uniform as BridgeSDKMessage
- Add 68 lines of unit tests covering bridge message handling
- Fixes docstring coverage to pass CI threshold

* fix(acp): restore IteratorResult return type to nextSdkMessageOrAbort

The simplified SDKMessage | undefined return type collapsed two distinct
states: generator truly done vs generator yielding undefined. This broke
forwardSessionUpdates which needs to distinguish the two — when the
generator yields null/undefined it should continue (calling next() again),
not break out of the loop.

Restored the original IteratorResult<SDKMessage, void> return type so
done and yielded-null are distinct again.
This commit is contained in:
James F
2026-06-09 21:49:05 +08:00
committed by GitHub
parent 4d930eb4eb
commit bee711f431
2 changed files with 249 additions and 54 deletions

View File

@@ -4,6 +4,7 @@ import {
toolUpdateFromToolResult,
toolUpdateFromEditToolResponse,
forwardSessionUpdates,
nextSdkMessageOrAbort,
} from '../bridge.js'
import { promptToQueryInput } from '../promptConversion.js'
import { markdownEscape, toDisplayPath } from '../utils.js'
@@ -30,6 +31,10 @@ async function* makeStream(
for (const m of msgs) yield m
}
async function* makeWaitingStream(): AsyncGenerator<SDKMessage, void, unknown> {
await new Promise<never>(() => {})
}
// ── toolInfoFromToolUse ────────────────────────────────────────────
describe('toolInfoFromToolUse', () => {
@@ -692,6 +697,47 @@ describe('toDisplayPath', () => {
// ── forwardSessionUpdates ─────────────────────────────────────────
describe('nextSdkMessageOrAbort', () => {
test('returns done:true when aborted while waiting for next message', async () => {
const ac = new AbortController()
const pending = nextSdkMessageOrAbort(makeWaitingStream(), ac.signal)
ac.abort()
const result = await Promise.race([
pending,
new Promise<'timeout'>(resolve => setTimeout(resolve, 100, 'timeout')),
])
expect(result).toEqual({ done: true, value: undefined })
})
test('returns done:true when stream is done', async () => {
const result = await nextSdkMessageOrAbort(
makeStream([]),
new AbortController().signal,
)
expect(result).toEqual({ done: true, value: undefined })
})
test('returns a valid SDKMessage via IteratorResult', async () => {
const msg = {
type: 'assistant',
message: {
role: 'assistant',
content: [{ type: 'text', text: 'hello' }],
},
} as unknown as SDKMessage
const result = await nextSdkMessageOrAbort(
makeStream([msg]),
new AbortController().signal,
)
expect(result).toEqual({ done: false, value: msg })
})
})
describe('forwardSessionUpdates', () => {
test('returns end_turn when stream is empty', async () => {
const conn = makeConn()
@@ -1077,6 +1123,28 @@ describe('forwardSessionUpdates', () => {
).toBe(0)
})
test('ignores unknown message types without crashing', async () => {
const conn = makeConn()
const debug = console.debug
const debugMock = mock(() => {})
console.debug = debugMock as typeof console.debug
try {
const result = await forwardSessionUpdates(
's1',
makeStream([{ type: 'future_message' } as unknown as SDKMessage]),
conn,
new AbortController().signal,
{},
)
expect(result.stopReason).toBe('end_turn')
expect(debugMock).toHaveBeenCalled()
} finally {
console.debug = debug
}
})
test('re-throws unexpected errors from stream', async () => {
const conn = makeConn()
async function* errorStream(): AsyncGenerator<