fix(apiproxy): reject empty question batches at the wire boundary

ask() already fails EMPTY_QUESTIONS before a request exists, so a
question/requested frame with zero items is host breakage; muxFrameSchema
now refuses it instead of letting an undefined first question reach the
composer (review r3635427102).
This commit is contained in:
imccyu
2026-07-23 20:23:54 +08:00
parent 0f63666522
commit 38af17a2f5
2 changed files with 8 additions and 1 deletions

View File

@@ -27,7 +27,10 @@ export const muxFrameSchema = z.discriminatedUnion('type', [
z.object({ type: z.literal('session/subscribed'), sessionId: sessionIdSchema, lastSeq: z.number().int() }),
z.object({ type: z.literal('approval/requested'), sessionId: sessionIdSchema, approvalId: approvalRequestIdSchema, toolName: z.string(), callId: z.string().optional(), reason: z.string().optional() }),
z.object({ type: z.literal('approval/resolved'), sessionId: sessionIdSchema, approvalId: approvalRequestIdSchema, outcome: z.union([z.literal('allowed-once'), z.literal('rejected'), z.literal('cancelled'), z.literal('unavailable')]) }),
z.object({ type: z.literal('question/requested'), sessionId: sessionIdSchema, questions: z.array(askUserQuestionItemSchema) }),
// Non-empty by wire contract: the user-interaction service rejects empty
// batches at ask() (EMPTY_QUESTIONS), so an empty frame is host breakage
// and must fail loud here, not reach the composer.
z.object({ type: z.literal('question/requested'), sessionId: sessionIdSchema, questions: z.array(askUserQuestionItemSchema).min(1) }),
z.object({ type: z.literal('question/resolved'), sessionId: sessionIdSchema, questionRpcId: rpcIdSchema, outcome: z.union([z.literal('answered'), z.literal('cancelled')]) }),
z.object({ type: z.literal('stream/error'), error: rpcErrorSchema }),
]) as unknown as z.ZodType<MuxFrame>

View File

@@ -135,6 +135,10 @@ describe('events frame schemas', () => {
expect(askUserQuestionItemSchema.parse({ id: 'q', question: 'Q?' }).id).toBe('q')
})
it('rejects an empty question batch (ask() guarantees at least one, so an empty frame is host breakage)', () => {
expect(() => muxFrameSchema.parse({ type: 'question/requested', sessionId: 's', questions: [] })).toThrow()
})
it('accepts every host frame branch', () => {
const frames = [
{ type: 'host/session-added', sessionId: 's', parentSessionId: 'p' },