feat: align conversation state, composer and scoped approvals

This commit is contained in:
dot committed 2026-10-21 03:46:53 +09:00
1 parent 771ca540f7
commit 308b98df23
61 files changed
+1701 -292

No files matched your search

+50
View File
@@ -0,0 +1,50 @@
/** Mock SDK transport: tests host replay lifecycle, NOT SDK permission matching. */
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 { rememberSessionGrant, sessionGrantProposal, clearSessionGrants } from '../src/main/agent/claude/session-grants'
const temp=await mkdtemp(join(tmpdir(),'grant-replay-unit-'));process.env.CLAUDE_TEST_USER_DATA=temp
const instances:any[]=[]
let fail=true
;(globalThis as any).__SDK_QUERY_FIXTURE=(input:any)=>{
let end!:()=>void;const done=new Promise<void>(resolve=>{end=resolve})
const instance={input,closed:false,settings:[] as any[],async *[Symbol.asyncIterator](){await done},close(){this.closed=true;end()},async applyFlagSettings(value:any){this.settings.push(value);if(fail)throw new Error('fixture replay failure')},async setPermissionMode(){},async getContextUsage(){return {categories:[]}}}
instances.push(instance);return instance
}
const model={profile:{id:'fixture',model:'fixture',authMode:'api-key'},config:{model:'fixture',baseUrl:'http://127.0.0.1:1',apiKey:'not-used',authMode:'api-key'}}
const kernel=new ClaudeKernel({sessionId:'a',workspaceRoot:temp,model:model as any,permissionMode:'default-sandbox',agentMode:'craft',onSessionId:()=>{}})
try{
const grant=sessionGrantProposal([{type:'addRules',behavior:'allow',destination:'session',rules:[{toolName:'Bash',ruleContent:'echo safe:*'}]}])!
rememberSessionGrant('a',grant,temp)
await assert.rejects(kernel.ensureStarted(),/恢复本会话授权失败/)
assert.equal(instances.length,1);assert.equal(instances[0].closed,true)
assert.equal((kernel as any).instance,null);assert.equal((kernel as any).input,null)
// Complete retired iterator cleanup before attempting a second kernel start.
await (kernel as any).draining
await assert.rejects(kernel.ensureStarted(),/恢复本会话授权失败/)
assert.equal(instances.length,2,'failure never silently reuses poisoned query')
await (kernel as any).draining
fail=false;await kernel.ensureStarted()
assert.equal(instances.length,3)
assert.equal((kernel as any).draining,null,'retired consumer draining lock cleared')
assert.deepEqual(await kernel.getContextSummary(),{categories:[]},'context queries recover after failed replay')
const raw=kernel as any
raw.active={settled:false,appends:new Map(),groupIds:['original'],request:{signal:new AbortController().signal,onSegmentStart:async()=>{},onSegmentComplete:async()=>{}}}
const append=kernel.appendInput({userMessageId:'append',prompt:'fixture'})
assert.equal(raw.active.appends.size,1,'append accepted after retry, no stale draining lock')
raw.active.appends.get('append').resolve();await append
raw.active=null
assert.deepEqual(instances[2].settings,[{permissions:{allow:['Bash(echo safe:*)']}}])
const hook=instances[2].input.options.hooks.PreToolUse[0].hooks[0]
;(kernel as any).active={request:{signal:new AbortController().signal}}
await kernel.setPolicy('full-access','plan')
const denied=await hook({hook_event_name:'PreToolUse',tool_name:'Bash',tool_input:{command:'echo safe'}})
assert.equal(denied.hookSpecificOutput.permissionDecision,'deny','plan denies even matching replay grant')
;(kernel as any).active=null
const other=new ClaudeKernel({sessionId:'other',workspaceRoot:temp,model:model as any,permissionMode:'default-sandbox',agentMode:'craft',onSessionId:()=>{}})
await other.ensureStarted();assert.equal(instances.at(-1).settings.length,0,'other session receives no grants')
await other.close()
console.log('Mock SDK replay lifecycle passed: fail closed, retry, session isolation, independent mode guard')
}finally{(kernel as any).active=null;await kernel.close();clearSessionGrants();await rm(temp,{recursive:true,force:true})}