|
|
@@ -0,0 +1,116 @@
|
|
|
+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<MuxFrame>[]
|
|
|
+ waitForQuestion(): Promise<RpcRequest<Extract<MuxFrame, { type: 'question/requested' }>>>
|
|
|
+} {
|
|
|
+ const envelopes: RpcRequest<MuxFrame>[] = []
|
|
|
+ let resolveQuestion!: (value: RpcRequest<Extract<MuxFrame, { type: 'question/requested' }>>) => void
|
|
|
+ const question = new Promise<RpcRequest<Extract<MuxFrame, { type: 'question/requested' }>>>((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<Extract<MuxFrame, { type: 'question/requested' }>>)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ })()
|
|
|
+ return { envelopes, waitForQuestion: () => question }
|
|
|
+}
|
|
|
+
|
|
|
+function answer(
|
|
|
+ envelope: RpcRequest<Extract<MuxFrame, { type: 'question/requested' }>>,
|
|
|
+ selected: string[],
|
|
|
+ custom?: string,
|
|
|
+): Parameters<ApiProxy['respond']>[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()
|
|
|
+ })
|
|
|
+})
|