| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113 |
- import { afterEach, beforeEach, describe, expect, it } from 'vitest'
- import { mkdtemp, rm } from 'node:fs/promises'
- import { tmpdir } from 'node:os'
- import { join } from 'node:path'
- import { PROTOCOL_VERSION } from '@agentclientprotocol/sdk'
- import { CallId } from '@deepseek-ai/dsh-llm'
- import { type Agent } from '@deepseek-ai/dsh-agent'
- import ApprovalService, { type ApprovalRequest } from '@deepseek-ai/dsh-user-approval'
- import { makeBridgeHarness, type BridgeHarness } from './harness.ts'
- import { SessionId } from '@deepseek-ai/dsh-session'
- /**
- * The bridge's `approval/request` answerer: an ask for an agent the bridge
- * owns becomes a `session/request_permission` prompt attached to the tool
- * call; foreign or call-less requests delegate down to the fail-closed
- * default. Driven through `ctx.approval` — the same path dsh-tools' ask
- * routing takes — against the harness's scriptable client.
- */
- describe('acp bridge — approval answerer', () => {
- let storageDir: string
- let harness: BridgeHarness | undefined
- beforeEach(async () => { storageDir = await mkdtemp(join(tmpdir(), 'acp-approval-')) })
- afterEach(async () => {
- await harness?.dispose()
- harness = undefined
- await rm(storageDir, { recursive: true, force: true })
- })
- async function ownedAgentRequest(
- h: BridgeHarness, overrides: Partial<ApprovalRequest> = {},
- ): Promise<{ agent: Agent; request: ApprovalRequest }> {
- await h.client.initialize({ protocolVersion: PROTOCOL_VERSION, clientCapabilities: {} })
- const { sessionId } = await h.client.newSession({ cwd: process.cwd(), mcpServers: [] })
- const agent = h.ctx.agents.get(SessionId(sessionId))
- if (agent === undefined) throw new Error('newSession created no agent')
- // In production an ask always fires mid-turn (tool execution); open one so
- // request()'s turn-enclosure precondition holds for the direct drive below.
- agent.session.append('turn/start', { turn: 1, trigger: { kind: 'message', source: { kind: 'user' } } })
- return { agent, request: { agent, toolName: 'echo', callId: CallId('call-9'), ...overrides } }
- }
- it('prompts the editor for an owned agent and maps allow-once → allowed-once', async () => {
- harness = await makeBridgeHarness({ storageDir })
- await harness.ctx.plugin(ApprovalService)
- harness.onPermission = () => ({ outcome: { outcome: 'selected', optionId: 'allow-once' } })
- const { request } = await ownedAgentRequest(harness)
- await expect(harness.ctx.approval.request(request)).resolves.toBe('allowed-once')
- expect(harness.permissionRequests).toHaveLength(1)
- const wire = harness.permissionRequests[0]
- expect(wire?.toolCall).toEqual({ toolCallId: 'call-9' })
- expect(wire?.options.map(o => ({ optionId: o.optionId, kind: o.kind }))).toEqual([
- { optionId: 'allow-once', kind: 'allow_once' },
- { optionId: 'reject-once', kind: 'reject_once' },
- ])
- })
- it('maps reject-once → rejected', async () => {
- harness = await makeBridgeHarness({ storageDir })
- await harness.ctx.plugin(ApprovalService)
- harness.onPermission = () => ({ outcome: { outcome: 'selected', optionId: 'reject-once' } })
- const { request } = await ownedAgentRequest(harness)
- await expect(harness.ctx.approval.request(request)).resolves.toBe('rejected')
- })
- it('maps a client cancellation → cancelled', async () => {
- harness = await makeBridgeHarness({ storageDir })
- await harness.ctx.plugin(ApprovalService)
- harness.onPermission = () => ({ outcome: { outcome: 'cancelled' } })
- const { request } = await ownedAgentRequest(harness)
- await expect(harness.ctx.approval.request(request)).resolves.toBe('cancelled')
- })
- it('treats an unknown optionId from a non-conforming client as a rejection, never a grant', async () => {
- harness = await makeBridgeHarness({ storageDir })
- await harness.ctx.plugin(ApprovalService)
- harness.onPermission = () => ({ outcome: { outcome: 'selected', optionId: 'allow-always-i-insist' } })
- const { request } = await ownedAgentRequest(harness)
- await expect(harness.ctx.approval.request(request)).resolves.toBe('rejected')
- })
- it('delegates a foreign agent down to the fail-closed default', async () => {
- harness = await makeBridgeHarness({ storageDir })
- await harness.ctx.plugin(ApprovalService)
- harness.onPermission = () => ({ outcome: { outcome: 'selected', optionId: 'allow-once' } })
- const { agent } = await ownedAgentRequest(harness)
- // Even an impostor that claims the bridge-owned session id must delegate:
- // ownership requires the exact Agent object stored in the session record.
- const foreign = {
- session: { id: agent.session.id, events: [{ type: 'turn/start' }], append: () => ({}) },
- } as unknown as Agent
- await expect(harness.ctx.approval.request({ agent: foreign, toolName: 'echo', callId: CallId('c') }))
- .resolves.toBe('unavailable')
- expect(harness.permissionRequests).toHaveLength(0)
- })
- it('delegates a call-less request — the protocol prompt must attach to a tool call', async () => {
- harness = await makeBridgeHarness({ storageDir })
- await harness.ctx.plugin(ApprovalService)
- harness.onPermission = () => ({ outcome: { outcome: 'selected', optionId: 'allow-once' } })
- const { agent } = await ownedAgentRequest(harness)
- await expect(harness.ctx.approval.request({ agent, toolName: 'echo' })).resolves.toBe('unavailable')
- expect(harness.permissionRequests).toHaveLength(0)
- })
- })
|