Files
ycode/scripts/claude-approval-policy-regression.ts
T

36 lines
3.4 KiB
TypeScript

/** Host policy unit tests only: no query(), CLI process or model request. */
import assert from 'node:assert/strict'
import { mkdtemp, rm } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { ClaudeKernel } from '../src/main/agent/claude/claude-kernel'
import { clearSessionGrants, sessionGrantRules } from '../src/main/agent/claude/session-grants'
import type { ClaudeKernelOptions } from '../src/main/agent/claude/claude-kernel'
const temp = await mkdtemp(join(tmpdir(),'approval-unit-'))
process.env.CLAUDE_TEST_USER_DATA=temp
const create = (id:string, mode:'craft'|'ask'|'plan'='craft', permissionMode:'default-sandbox'|'full-access'='default-sandbox') => new ClaudeKernel({sessionId:id,workspaceRoot:temp,model:{} as ClaudeKernelOptions['model'],permissionMode,agentMode:mode,onSessionId:()=>{}})
const suggestions=[{type:'addRules',behavior:'allow',destination:'projectSettings',rules:[{toolName:'Bash',ruleContent:'echo safe:*'},{toolName:'Read',ruleContent:'/tmp/owned/**'}]}]
let calls=0
const setup = (kernel:ClaudeKernel, choose='allow-always') => {
const controller = new AbortController()
const raw=kernel as any
raw.instance={applyFlagSettings:async()=>{},close:()=>{}}
raw.active={settled:false,request:{runId:'r',signal:controller.signal,emit:()=>{},interactions:{approval:async (...args:any[])=>{calls++;assert.equal(args[5].toolName,'Bash');const decision={kind:'approval',optionId:choose};await args[6]?.(decision);return decision}}}}
return { raw, controller, invoke:(extra:object={})=>raw.canUseTool('Bash',{command:'echo safe'}, {signal:controller.signal,toolUseID:'t',suggestions,...extra}) }
}
try {
const a=setup(create('a')); const result=await a.invoke()
assert.equal(result.behavior,'allow'); assert.equal(result.updatedPermissions.length,1)
assert.equal(result.updatedPermissions[0].rules.length,2); assert.equal(result.updatedPermissions[0].destination,'session')
assert.equal(sessionGrantRules('a',temp).length,2);assert.equal(sessionGrantRules('b',temp).length,0)
const once=setup(create('once'),'allow-once'); assert.equal((await once.invoke()).updatedPermissions,undefined);assert.equal(sessionGrantRules('once',temp).length,0)
const denied=setup(create('denied'),'reject-once');assert.equal((await denied.invoke()).behavior,'deny')
for(const mode of ['ask','plan'] as const) {const old=calls;assert.equal((await setup(create(mode,mode,'full-access')).invoke()).behavior,'deny');assert.equal(calls,old)}
const full=setup(create('full','craft','full-access'));const old=calls;assert.equal((await full.invoke()).behavior,'allow');assert.equal(calls,old)
await full.invoke({matchedAskRule:{source:'projectSettings',toolName:'Bash',ruleContent:'echo safe:*'}});assert.equal(calls,old+1)
const failed=setup(create('failed'));failed.raw.instance.applyFlagSettings=async()=>{throw new Error('apply failed')};assert.equal((await failed.invoke()).behavior,'deny');assert.equal(sessionGrantRules('failed',temp).length,0)
const stale=setup(create('stale'));stale.raw.active.request.interactions.approval=async()=>{stale.controller.abort();return {kind:'approval',optionId:'allow-always'}}
assert.equal((await stale.invoke()).behavior,'deny');assert.equal(sessionGrantRules('stale',temp).length,0)
console.log('Claude host approval policy unit checks passed (no SDK process/application/matching verification)')
} finally {clearSessionGrants();await rm(temp,{recursive:true,force:true})}