Merge remote-tracking branch 'origin/master' into worktree/custom-deepseek-models
# Conflicts: # packages/host/apiproxy/README.i18n.yaml
This commit is contained in:
116
packages/host/apiproxy/tests/api-proxy-question.spec.ts
Normal file
116
packages/host/apiproxy/tests/api-proxy-question.spec.ts
Normal file
@@ -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()
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user