70 lines
5.9 KiB
TypeScript
70 lines
5.9 KiB
TypeScript
import assert from 'node:assert/strict'
|
|
import { sessionGrantProposal, sessionGrantRules, rememberSessionGrant, clearSessionGrants } from '../src/main/agent/claude/session-grants'
|
|
import { applyAssistantEvent } from '../src/shared/message-parts'
|
|
import { createInteractionBroker, respondToInteraction } from '../src/main/interaction-broker'
|
|
import type { ChatEvent, ChatMessage } from '../src/shared/types'
|
|
|
|
const proposal = sessionGrantProposal([{type:'addRules',behavior:'allow',destination:'projectSettings',rules:[{toolName:'Bash',ruleContent:'echo safe:*'},{toolName:'Read',ruleContent:'/tmp/owned/**'}]}])!
|
|
assert.equal(proposal.updates[0].destination, 'session')
|
|
assert.deepEqual(proposal.rules, ['Bash(echo safe:*)','Read(/tmp/owned/**)'])
|
|
assert.equal(sessionGrantProposal([{type:'setMode',mode:'bypassPermissions',destination:'session'}]), undefined)
|
|
assert.equal(sessionGrantProposal([{type:'addRules',behavior:'allow',destination:'session',rules:[{toolName:'Bash'}]}]), undefined)
|
|
assert.equal(sessionGrantProposal([...proposal.updates,{type:'addDirectories',directories:['/'],destination:'session'}]), undefined)
|
|
rememberSessionGrant('a', proposal); rememberSessionGrant('a',proposal)
|
|
assert.equal(sessionGrantRules('a').length, 2)
|
|
assert.equal(sessionGrantRules('b').length, 0)
|
|
const copy = sessionGrantRules('a'); copy.push('Write(*)'); assert.equal(sessionGrantRules('a').length, 2)
|
|
rememberSessionGrant('a',proposal,'/workspace/a'); assert.equal(sessionGrantRules('a','/workspace/b').length,0)
|
|
clearSessionGrants('a'); assert.equal(sessionGrantRules('a').length,0)
|
|
const msg: ChatMessage = {id:'m',role:'assistant',content:'',createdAt:1,requestId:'r',status:'streaming'}
|
|
const event: ChatEvent = {runId:'r',type:'progress',progress:{source:'agent',phase:'tool',toolName:'Read'}}
|
|
const live = applyAssistantEvent(msg,event)
|
|
assert.equal(live.runtimeProgress?.toolName,'Read')
|
|
assert.equal(applyAssistantEvent(msg,{...event,runId:'other'}),msg)
|
|
for(const terminal of [{runId:'r',type:'done',content:'',tools:[]},{runId:'r',type:'cancelled'},{runId:'r',type:'error',message:'fail'}] as ChatEvent[]) {
|
|
const done = applyAssistantEvent(live,{...terminal,turnFinishedAt:100,turnDurationMs:99})
|
|
assert.equal(done.runtimeProgress,undefined); assert.equal(done.turnFinishedAt,100)
|
|
assert.equal(applyAssistantEvent(done,event),done)
|
|
}
|
|
const events: ChatEvent[]=[]
|
|
const controller = new AbortController()
|
|
const broker = createInteractionBroker('r',controller.signal,event=>events.push(event))
|
|
const response = broker.approval('tool','Run', [{optionId:'once',name:'Once',kind:'allow_once'}],undefined,undefined,{requestKind:'permission',toolName:'Bash',allowScope:'once'})
|
|
const pending = events.find(event=>event.type==='interaction')!
|
|
assert.equal(pending.type,'interaction')
|
|
if(pending.type!=='interaction') throw new Error('missing')
|
|
assert.equal(respondToInteraction('other',pending.interaction.id,{kind:'approval',optionId:'once'},event=>events.push(event)),false)
|
|
assert.throws(()=>respondToInteraction('r',pending.interaction.id,{kind:'approval',optionId:'bad'},event=>events.push(event)))
|
|
assert.equal(respondToInteraction('r',pending.interaction.id,{kind:'approval',optionId:'once'},event=>events.push(event)),true)
|
|
assert.equal(respondToInteraction('r',pending.interaction.id,{kind:'approval',optionId:'once'},event=>events.push(event)),false)
|
|
await response
|
|
const cancelled = broker.approval('tool2','Run', [{optionId:'once',name:'Once',kind:'allow_once'}])
|
|
const last = events.at(-1)!
|
|
controller.abort()
|
|
await assert.rejects(cancelled,/取消/)
|
|
if(last.type==='interaction') assert.equal(respondToInteraction('r',last.interaction.id,{kind:'approval',optionId:'once'},event=>events.push(event)),false)
|
|
const retryController=new AbortController()
|
|
const retryEvents:ChatEvent[]=[]
|
|
const retryBroker=createInteractionBroker('retry',retryController.signal,event=>retryEvents.push(event))
|
|
let failApply=true
|
|
const waiting=retryBroker.approval('tool','Run',[{optionId:'session',name:'Session',kind:'allow_always'}],undefined,undefined,undefined,()=>{if(failApply)throw new Error('apply rejected');return Promise.resolve()})
|
|
const retryEvent=retryEvents[0];if(retryEvent.type!=='interaction')throw new Error('missing')
|
|
await assert.rejects(Promise.resolve(respondToInteraction('retry',retryEvent.interaction.id,{kind:'approval',optionId:'session'},event=>retryEvents.push(event))),/apply rejected/)
|
|
assert.equal(retryEvents.some(event=>event.type==='interaction'&&event.interaction.status==='answered'),false)
|
|
assert.equal(retryEvents.at(-1)?.type==='interaction'&&(retryEvents.at(-1) as any).interaction.status,'error')
|
|
failApply=false
|
|
assert.equal(await respondToInteraction('retry',retryEvent.interaction.id,{kind:'approval',optionId:'session'},event=>retryEvents.push(event)),true)
|
|
await waiting
|
|
const abortApply=new AbortController();const abortEvents:ChatEvent[]=[]
|
|
const abortBroker=createInteractionBroker('abort-apply',abortApply.signal,event=>abortEvents.push(event))
|
|
let unblock!:()=>void
|
|
const abortedWait=abortBroker.approval('tool','Run',[{optionId:'session',name:'Session',kind:'allow_always'}],undefined,undefined,undefined,()=>new Promise<void>(resolve=>{unblock=resolve}))
|
|
const catchAbort=assert.rejects(abortedWait,/取消/)
|
|
const abortEvent=abortEvents[0];if(abortEvent.type!=='interaction')throw new Error('missing')
|
|
const applying=respondToInteraction('abort-apply',abortEvent.interaction.id,{kind:'approval',optionId:'session'},event=>abortEvents.push(event))
|
|
assert.equal(respondToInteraction('abort-apply',abortEvent.interaction.id,{kind:'approval',optionId:'session'},event=>abortEvents.push(event)),false)
|
|
await Promise.resolve() // Let the guarded async application begin before cancelling it.
|
|
abortApply.abort();unblock();assert.equal(await applying,false);await catchAbort
|
|
assert.equal(abortEvents.some(event=>event.type==='interaction'&&event.interaction.status==='answered'),false)
|
|
console.log('Runtime progress, session-grant scope and approval lifecycle regressions passed')
|