import { describe, expect, it } from 'vitest' import { Context } from 'cordis' import type { Agent } from '@deepseek-ai/dsh-agent' import SessionStore from '@deepseek-ai/dsh-session' import UserInteractionService from '@deepseek-ai/dsh-user-interaction' import type { ApiProxy, MuxFrame, RpcRequest } from '@deepseek-ai/dsh-host-apiproxy/api' import { RpcId } from '@deepseek-ai/dsh-host-apiproxy/api/rpc' import { createApiProxy } from '../src/api-proxy.ts' async function harness(): Promise<{ ctx: Context; api: ApiProxy }> { const ctx = new Context() await ctx.plugin(SessionStore) await ctx.plugin(UserInteractionService) return { ctx, api: createApiProxy(ctx, { provider: 'p', model: 'm', cwd: '/tmp', workspaceRoot: '/tmp' }), } } function agent(id: string): Agent { return { id } as unknown as Agent } function openMux(api: ApiProxy, abort: AbortController): { envelopes: RpcRequest[] waitForQuestion(): Promise>> } { const envelopes: RpcRequest[] = [] let resolveQuestion!: (value: RpcRequest>) => void const question = new Promise>>((resolve) => { resolveQuestion = resolve }) void (async () => { for await (const envelope of api.events.mux({ rpcId: RpcId('question-mux'), payload: {} }, abort.signal)) { envelopes.push(envelope) if (envelope.payload.type === 'question/requested') { resolveQuestion(envelope as RpcRequest>) } } })() return { envelopes, waitForQuestion: () => question } } function answer( envelope: RpcRequest>, selected: string[], custom?: string, ): Parameters[0] { return { type: 'client-response', rpcId: envelope.rpcId, result: { ok: true, value: { sessionId: envelope.payload.sessionId, answer: { answers: [{ id: envelope.payload.questions[0]?.id, selected, ...custom === undefined ? {} : { custom }, }], }, }, }, } } describe('question response validation', () => { it('accepts selected options with custom text for multi-select questions', async () => { const { ctx, api } = await harness() const abort = new AbortController() const mux = openMux(api, abort) const asked = ctx.userInteraction.ask({ agent: agent('session-multi'), questions: [{ id: 'targets', question: 'Choose targets and add another', multiSelect: true, options: [{ label: 'Code' }, { label: 'Docs' }], }], }) const envelope = await mux.waitForQuestion() expect(await api.respond(answer(envelope, ['Code', 'Docs'], 'Release notes'))) .toEqual({ accepted: true }) await expect(asked).resolves.toEqual({ answers: [{ id: 'targets', selected: ['Code', 'Docs'], custom: 'Release notes' }], }) expect(mux.envelopes.some(item => item.payload.type === 'question/resolved')).toBe(true) abort.abort() }) it('keeps selected options and custom text mutually exclusive for single-select questions', async () => { const { ctx, api } = await harness() const abort = new AbortController() const mux = openMux(api, abort) const asked = ctx.userInteraction.ask({ agent: agent('session-single'), questions: [{ id: 'target', question: 'Choose one target', options: [{ label: 'Code' }, { label: 'Docs' }], }], }) const envelope = await mux.waitForQuestion() expect(await api.respond(answer(envelope, ['Code'], 'Release notes'))) .toEqual({ accepted: false, reason: 'bad-response' }) expect(await api.respond(answer(envelope, [], 'Release notes'))) .toEqual({ accepted: true }) await expect(asked).resolves.toEqual({ answers: [{ id: 'target', selected: [], custom: 'Release notes' }], }) abort.abort() }) })