|
|
@@ -1282,6 +1282,96 @@ it('supports unlimited rounds beyond the finite limit and still permits stopping
|
|
|
expect(()=>store.updateConversation(owner,g.id,{maxReplyRounds:0})).toThrow()
|
|
|
})
|
|
|
|
|
|
+it('applies independent Bot approval choices on the same Profile and does not replay automatic answers', async () => {
|
|
|
+ const ask = agent('手动审批')
|
|
|
+ const { approvalPolicy: _policy, ...legacyAsk } = ask
|
|
|
+ store.put(owner, 'agent', ask.id, legacyAsk)
|
|
|
+ const allow = store.updateAgent(owner, agent('允许审批').id, { approvalPolicy: 'allow' })
|
|
|
+ const deny = store.updateAgent(owner, agent('拒绝审批').id, { approvalPolicy: 'deny' })
|
|
|
+ const bots = [ask, allow, deny]
|
|
|
+ expect(bots.every(bot => bot.profile === ask.profile)).toBe(true)
|
|
|
+ const notified: string[] = []
|
|
|
+ runtime.onNotify = (_owner, _conversation, _run, _message, interaction) => { if (interaction) notified.push(interaction.agentId) }
|
|
|
+ server.on('connection', socket => socket.on('message', raw => {
|
|
|
+ const frame = JSON.parse(String(raw))
|
|
|
+ if (frame.method === 'approval.respond') completeReply(socket, frame.params, '审批已处理')
|
|
|
+ }))
|
|
|
+ reply = (socket, params) => {
|
|
|
+ const frame = JSON.stringify({ method: 'event', params: { type: 'approval.request', session_id: params.session_id, payload: { request_id: 'same-id', message: '是否执行?' } } })
|
|
|
+ socket.send(frame); socket.send(frame)
|
|
|
+ }
|
|
|
+ const runs = bots.map(bot => runtime.send(owner, direct(bot.id).id, { requestId: randomUUID(), content: '执行任务' }))
|
|
|
+ await finished(runs[1]!.id); await finished(runs[2]!.id)
|
|
|
+ const interactions = store.list<WorkspaceInteraction & { answer?: string }>(owner, 'interaction')
|
|
|
+ expect(interactions).toHaveLength(3)
|
|
|
+ expect(interactions.find(item => item.agentId === ask.id)).toMatchObject({ resolved: false })
|
|
|
+ expect(interactions.find(item => item.agentId === allow.id)).toMatchObject({ resolved: true, answer: 'once' })
|
|
|
+ expect(interactions.find(item => item.agentId === deny.id)).toMatchObject({ resolved: true, answer: 'deny' })
|
|
|
+ expect(requests.filter(frame => frame.method === 'approval.respond')).toHaveLength(2)
|
|
|
+ expect(notified).toEqual([ask.id])
|
|
|
+ await runtime.respond(owner, interactions.find(item => item.agentId === ask.id)!.id, 'deny')
|
|
|
+ await finished(runs[0]!.id)
|
|
|
+})
|
|
|
+
|
|
|
+it('leaves clarifications and already pending approvals for the user after a policy change', async () => {
|
|
|
+ const bot = agent('更改审批')
|
|
|
+ let kind = 'approval'
|
|
|
+ server.on('connection', socket => socket.on('message', raw => {
|
|
|
+ const frame = JSON.parse(String(raw))
|
|
|
+ if (['approval.respond', 'clarify.respond'].includes(frame.method)) completeReply(socket, frame.params, '已答复')
|
|
|
+ }))
|
|
|
+ reply = (socket, params) => socket.send(JSON.stringify({ method: 'event', params: { type: `${kind}.request`, session_id: params.session_id, payload: { request_id: 'pending', message: '请确认' } } }))
|
|
|
+ for (kind of ['approval', 'clarify']) {
|
|
|
+ const run = runtime.send(owner, direct(bot.id).id, { requestId: randomUUID(), content: '继续任务' })
|
|
|
+ await vi.waitFor(() => expect(store.require<WorkspaceRun>(owner, 'run', run.id).status).toBe('waiting'))
|
|
|
+ store.updateAgent(owner, bot.id, { approvalPolicy: 'allow' })
|
|
|
+ const interaction = store.list<WorkspaceInteraction>(owner, 'interaction').find(item => !item.resolved)!
|
|
|
+ expect(requests.filter(frame => ['approval.respond', 'clarify.respond'].includes(frame.method))).toHaveLength(kind === 'approval' ? 0 : 1)
|
|
|
+ await runtime.respond(owner, interaction.id, kind === 'approval' ? 'once' : '下一步')
|
|
|
+ await finished(run.id)
|
|
|
+ }
|
|
|
+})
|
|
|
+
|
|
|
+it('defaults legacy and newly created Bots to manual approval and prevents autonomous grants', () => {
|
|
|
+ const parent = agent('创建者')
|
|
|
+ expect(parent.approvalPolicy).toBe('ask')
|
|
|
+ const child = store.createAgent(owner, { name: '子成员', profile: parent.profile, approvalPolicy: 'allow' }, { createdByAgentId: parent.id, createdFromRunId: randomUUID() })
|
|
|
+ expect(child.approvalPolicy).toBe('ask')
|
|
|
+ expect(() => store.updateAgent(owner, child.id, { approvalPolicy: 'always' })).toThrow('approvalPolicy')
|
|
|
+ store.put(owner, 'agent', child.id, { ...child, remoteAgentId: randomUUID() })
|
|
|
+ expect(store.updateAgent(owner, child.id, { approvalPolicy: 'deny' }).approvalPolicy).toBe('deny')
|
|
|
+ expect(() => store.updateAgent(owner, child.id, { name: '不能改远端资料' })).toThrow('远端管理')
|
|
|
+})
|
|
|
+
|
|
|
+it('keeps a failed automatic approval pending and does not retry a replayed request', async () => {
|
|
|
+ const bot = store.updateAgent(owner, agent('审批失败').id, { approvalPolicy: 'allow' })
|
|
|
+ const rpc = WorkspaceGateway.prototype.rpc
|
|
|
+ let attempts = 0
|
|
|
+ const spy = vi.spyOn(WorkspaceGateway.prototype, 'rpc').mockImplementation(function (this: WorkspaceGateway, method, params) {
|
|
|
+ if (method === 'approval.respond' && attempts++ === 0) return Promise.reject(new HttpError(409, '暂时拒绝答复', 'gateway_rejected'))
|
|
|
+ return rpc.call(this, method, params)
|
|
|
+ })
|
|
|
+ try {
|
|
|
+ const notify = vi.fn()
|
|
|
+ runtime.onNotify = notify
|
|
|
+ reply = (socket, params) => {
|
|
|
+ const frame = JSON.stringify({ method: 'event', params: { type: 'approval.request', session_id: params.session_id, payload: { request_id: 'failure', message: '确认操作' } } })
|
|
|
+ socket.send(frame); socket.send(frame)
|
|
|
+ }
|
|
|
+ server.on('connection', socket => socket.on('message', raw => {
|
|
|
+ const frame = JSON.parse(String(raw))
|
|
|
+ if (frame.method === 'approval.respond') completeReply(socket, frame.params, '手动处理完成')
|
|
|
+ }))
|
|
|
+ const run = runtime.send(owner, direct(bot.id).id, { requestId: randomUUID(), content: '继续' })
|
|
|
+ await vi.waitFor(() => expect(notify.mock.calls.some(call => call[4]?.kind === 'approval')).toBe(true))
|
|
|
+ expect(attempts).toBe(1)
|
|
|
+ const interaction = store.list<WorkspaceInteraction>(owner, 'interaction').find(item => !item.resolved)!
|
|
|
+ expect(store.require<WorkspaceRun>(owner, 'run', run.id).status).toBe('waiting')
|
|
|
+ await runtime.respond(owner, interaction.id, 'deny')
|
|
|
+ await finished(run.id)
|
|
|
+ } finally { spy.mockRestore() }
|
|
|
+})
|
|
|
+
|
|
|
it('keeps parallel approval cards scoped to their member and deduplicates replayed requests and answers', async () => {
|
|
|
const a=agent('审批管理员'),b=agent('审批乙'),c=agent('审批丙'),g=store.createGroup(owner,{name:'并发审批',memberIds:[a.id,b.id,c.id],administratorId:a.id})
|
|
|
let admin=0
|