feat: align conversation state, composer and scoped approvals
This commit is contained in:
1 parent
771ca540f7
commit
308b98df23
61 files changed
+1701
-292
No files matched your search
@@ -0,0 +1,69 @@
|
||||
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')
|
||||
Reference in new issue
Block a user