2026-07-09 16:41:03 +08:00
import { describe , expect , it , vi } from 'vitest'
2026-07-09 15:25:18 +08:00
import { Context } from 'cordis'
2026-07-19 22:13:50 +08:00
import { agentEvents , type Agent } from '@deepseek-ai/dsh-agent'
2026-07-09 15:25:18 +08:00
import { CallId } from '@deepseek-ai/dsh-llm'
2026-07-12 22:39:01 +08:00
import { carrierKeyOf , createScope } from '@deepseek-ai/dsh-scope'
import type { Scope } from '@deepseek-ai/dsh-scope'
2026-07-12 00:31:46 +08:00
import SessionStore , { Session , SessionId } from '@deepseek-ai/dsh-session'
2026-07-09 15:25:18 +08:00
import type { SessionEvent } from '@deepseek-ai/dsh-session'
2026-07-09 16:41:03 +08:00
import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
2026-07-11 21:37:38 +08:00
import ApprovalService , { ApprovalOutcome , ApprovalRequest , effectiveApprovalPolicy , setApprovalPolicy } from '@deepseek-ai/dsh-user-approval'
2026-07-09 15:25:18 +08:00
/**
* A minimal Agent stand-in — the service only reaches `agent.session.append`
* and folds `.events`. Seeded inside an open turn by default (request()'s
* turn-enclosure precondition); pass `seed` to stage idle/closed logs.
* Returns the recorded audit appends alongside the fake.
*/
function fakeAgent ( seed : Array < { type : string } > = [ { type : 'turn/start' } , { type : 'user/message' } ] ) : { agent : Agent ; appended : Array < { type : string ; data : Record < string , unknown > } > } {
const appended : Array < { type : string ; data : Record < string , unknown > } > = [ ]
const agent = {
session : {
events : seed ,
append : ( type : string , data : Record < string , unknown > ) = > {
appended . push ( { type , data } )
return { type , data } as unknown as SessionEvent
} ,
} ,
} as unknown as Agent
return { agent , appended }
}
async function mounted ( ) : Promise < Context > {
const ctx = new Context ( )
await ctx . plugin ( ApprovalService )
return ctx
}
function requestOf ( agent : Agent , overrides : Partial < ApprovalRequest > = { } ) : ApprovalRequest {
return { agent , toolName : 'echo' , . . . overrides }
}
describe ( 'ApprovalService.request' , ( ) = > {
it ( 'throws before appending anything when no turn has ever opened (idle ask)' , async ( ) = > {
const ctx = await mounted ( )
const { agent , appended } = fakeAgent ( [ ] )
await expect ( ctx . approval . request ( requestOf ( agent ) ) ) . rejects . toThrow ( /outside an open turn/ )
expect ( appended ) . toHaveLength ( 0 )
} )
it ( 'throws between turns — a closed turn does not satisfy the enclosure precondition' , async ( ) = > {
const ctx = await mounted ( )
const { agent , appended } = fakeAgent ( [ { type : 'turn/start' } , { type : 'turn/end' } ] )
await expect ( ctx . approval . request ( requestOf ( agent ) ) ) . rejects . toThrow ( /outside an open turn/ )
expect ( appended ) . toHaveLength ( 0 )
} )
it ( 'fails closed to unavailable when nobody listens, auditing the asked/decided pair' , async ( ) = > {
const ctx = await mounted ( )
const { agent , appended } = fakeAgent ( )
const outcome = await ctx . approval . request ( requestOf ( agent , { callId : CallId ( 'call-1' ) , reason : 'hook says ask' } ) )
expect ( outcome ) . toBe ( 'unavailable' )
expect ( appended . map ( e = > e . type ) ) . toEqual ( [ 'approval/asked' , 'approval/decided' ] )
const [ asked , decided ] = appended
expect ( asked ? . data ) . toMatchObject ( { toolName : 'echo' , callId : 'call-1' , reason : 'hook says ask' } )
expect ( decided ? . data ) . toMatchObject ( { outcome : 'unavailable' } )
expect ( decided ? . data [ 'id' ] ) . toBe ( asked ? . data [ 'id' ] )
} )
it ( 'omits absent optional fields from the asked audit event' , async ( ) = > {
const ctx = await mounted ( )
const { agent , appended } = fakeAgent ( )
await ctx . approval . request ( requestOf ( agent ) )
expect ( Object . keys ( appended [ 0 ] ? . data ? ? { } ) . sort ( ) ) . toEqual ( [ 'id' , 'toolName' ] )
} )
2026-07-12 22:39:01 +08:00
it ( 'borrows the exact readonly request for scoped dispatch and audit' , async ( ) = > {
2026-07-12 00:31:46 +08:00
const ctx = await mounted ( )
2026-07-12 22:39:01 +08:00
const { agent , appended } = fakeAgent ( )
let scope ! : Scope
const scopeFiber = await ctx . plugin ( Object . assign ( ( inner : Context ) = > {
scope = createScope ( inner , agent )
} , { inject : [ 'approval' ] } ) )
2026-07-12 00:31:46 +08:00
let received : ApprovalRequest | undefined
let carrier : unknown
2026-07-12 22:39:01 +08:00
scope . ctx . on ( 'approval/request' , function ( req ) {
2026-07-12 00:31:46 +08:00
received = req
carrier = carrierKeyOf ( this )
2026-07-12 22:39:01 +08:00
return Promise . resolve < ApprovalOutcome > ( 'allowed-once' )
2026-07-12 00:31:46 +08:00
} )
2026-07-12 22:39:01 +08:00
const request = requestOf ( agent , {
toolName : 'scoped-tool' ,
callId : CallId ( 'scoped-call' ) ,
reason : 'scoped reason' ,
2026-07-12 00:31:46 +08:00
} )
2026-07-12 22:39:01 +08:00
await expect ( ctx . approval . request ( request ) ) . resolves . toBe ( 'allowed-once' )
expect ( carrier ) . toBe ( agent )
expect ( received ) . toBe ( request )
expect ( appended ) . toHaveLength ( 2 )
expect ( appended [ 0 ] ? . data ) . toMatchObject ( {
toolName : 'scoped-tool' ,
callId : 'scoped-call' ,
reason : 'scoped reason' ,
2026-07-12 00:31:46 +08:00
} )
2026-07-12 22:39:01 +08:00
expect ( appended [ 1 ] ? . data ) . toMatchObject ( { outcome : 'allowed-once' } )
expect ( appended [ 1 ] ? . data [ 'id' ] ) . toBe ( appended [ 0 ] ? . data [ 'id' ] )
await scopeFiber . dispose ( )
2026-07-12 00:31:46 +08:00
} )
it ( 'contains an approval/asked observer throw after append and still completes the pair' , async ( ) = > {
const ctx = new Context ( )
await ctx . plugin ( SessionStore )
await ctx . plugin ( ApprovalService )
const session = ctx . sessions . create ( SessionId ( 'asked-observer-throw' ) )
session . append ( 'turn/start' , { turn : 1 , trigger : { kind : 'message' , source : { kind : 'user' } } } )
const agent = { session } as unknown as Agent
const warn = vi . spyOn ( ctx . logger , 'warn' ) . mockImplementation ( ( ) = > { } )
ctx . on ( 'session/event' , ( _session , event ) = > {
if ( event . type === 'approval/asked' ) throw new Error ( 'observer failed after asked append' )
} )
ctx . on ( 'approval/request' , ( ) = > Promise . resolve < ApprovalOutcome > ( 'allowed-once' ) )
await expect ( ctx . approval . request ( requestOf ( agent ) ) ) . resolves . toBe ( 'allowed-once' )
const audit = session . events . filter ( event = > event . type . startsWith ( 'approval/' ) )
const asked = session . events . find ( ( event ) : event is SessionEvent < 'approval/asked' > = > event . type === 'approval/asked' )
const decided = session . events . find ( ( event ) : event is SessionEvent < 'approval/decided' > = > event . type === 'approval/decided' )
expect ( audit . map ( event = > event . type ) ) . toEqual ( [ 'approval/asked' , 'approval/decided' ] )
expect ( decided ? . data . id ) . toBe ( asked ? . data . id )
2026-07-12 18:57:42 +08:00
expect ( warn ) . toHaveBeenCalledWith ( expect . stringContaining ( 'session/event listener threw: Error: observer failed after asked append' ) )
2026-07-12 00:31:46 +08:00
} )
it ( 'contains an approval/decided observer throw after append and still resolves' , async ( ) = > {
const ctx = new Context ( )
await ctx . plugin ( SessionStore )
await ctx . plugin ( ApprovalService )
const session = ctx . sessions . create ( SessionId ( 'decided-observer-throw' ) )
session . append ( 'turn/start' , { turn : 1 , trigger : { kind : 'message' , source : { kind : 'user' } } } )
const agent = { session } as unknown as Agent
const warn = vi . spyOn ( ctx . logger , 'warn' ) . mockImplementation ( ( ) = > { } )
ctx . on ( 'session/event' , ( _session , event ) = > {
if ( event . type === 'approval/decided' ) throw new Error ( 'observer failed after decided append' )
} )
ctx . on ( 'approval/request' , ( ) = > Promise . resolve < ApprovalOutcome > ( 'rejected' ) )
await expect ( ctx . approval . request ( requestOf ( agent ) ) ) . resolves . toBe ( 'rejected' )
const audit = session . events . filter ( event = > event . type . startsWith ( 'approval/' ) )
const asked = session . events . find ( ( event ) : event is SessionEvent < 'approval/asked' > = > event . type === 'approval/asked' )
const decided = session . events . find ( ( event ) : event is SessionEvent < 'approval/decided' > = > event . type === 'approval/decided' )
expect ( audit . map ( event = > event . type ) ) . toEqual ( [ 'approval/asked' , 'approval/decided' ] )
expect ( decided ? . data ) . toMatchObject ( { id : asked?.data.id , outcome : 'rejected' } )
2026-07-12 18:57:42 +08:00
expect ( warn ) . toHaveBeenCalledWith ( expect . stringContaining ( 'session/event listener threw: Error: observer failed after decided append' ) )
2026-07-12 00:31:46 +08:00
} )
2026-07-12 18:57:42 +08:00
it ( 'propagates an append failure that prevented audit log growth' , async ( ) = > {
2026-07-12 00:31:46 +08:00
const ctx = await mounted ( )
const failure = new Error ( 'append failed before log growth' )
const agent = {
session : {
events : [ { type : 'turn/start' } ] ,
append : ( ) = > { throw failure } ,
} ,
} as unknown as Agent
await expect ( ctx . approval . request ( requestOf ( agent ) ) ) . rejects . toBe ( failure )
} )
2026-07-09 15:25:18 +08:00
it ( 'returns the first answering listener outcome (single decision slot)' , async ( ) = > {
const ctx = await mounted ( )
const { agent } = fakeAgent ( )
let secondRan = false
ctx . on ( 'approval/request' , ( ) = > Promise . resolve < ApprovalOutcome > ( 'allowed-once' ) )
ctx . on ( 'approval/request' , ( ) = > {
secondRan = true
return Promise . resolve < ApprovalOutcome > ( 'rejected' )
} )
await expect ( ctx . approval . request ( requestOf ( agent ) ) ) . resolves . toBe ( 'allowed-once' )
expect ( secondRan ) . toBe ( false )
} )
it ( 'lets a non-owning listener delegate via next() down to the fail-closed default' , async ( ) = > {
const ctx = await mounted ( )
const { agent } = fakeAgent ( )
ctx . on ( 'approval/request' , ( _req , next ) = > next ( ) )
await expect ( ctx . approval . request ( requestOf ( agent ) ) ) . resolves . toBe ( 'unavailable' )
} )
2026-07-11 23:14:09 +08:00
it ( 'dispatches to global and matching agent-scoped listeners, never a foreign scope' , async ( ) = > {
const ctx = await mounted ( )
const { agent : agentA } = fakeAgent ( )
const { agent : agentB } = fakeAgent ( )
2026-07-12 22:39:01 +08:00
let scopeA ! : Scope
let scopeB ! : Scope
const scopesFiber = await ctx . plugin ( Object . assign ( ( inner : Context ) = > {
scopeA = createScope ( inner , agentA )
scopeB = createScope ( inner , agentB )
} , { inject : [ 'approval' ] } ) )
2026-07-11 23:14:09 +08:00
const heard : string [ ] = [ ]
ctx . on ( 'approval/request' , ( req , next ) = > {
heard . push ( req . agent === agentA ? 'global:A' : 'global:B' )
return next ( )
} )
scopeA . ctx . on ( 'approval/request' , ( _req , next ) = > {
heard . push ( 'scoped:A' )
return next ( )
} )
scopeB . ctx . on ( 'approval/request' , ( _req , next ) = > {
heard . push ( 'scoped:B' )
return next ( )
} )
await expect ( ctx . approval . request ( requestOf ( agentA ) ) ) . resolves . toBe ( 'unavailable' )
await expect ( ctx . approval . request ( requestOf ( agentB ) ) ) . resolves . toBe ( 'unavailable' )
expect ( heard ) . toEqual ( [ 'global:A' , 'scoped:A' , 'global:B' , 'scoped:B' ] )
2026-07-12 22:39:01 +08:00
await scopesFiber . dispose ( )
2026-07-11 23:14:09 +08:00
} )
it ( 'keys the scoped dispatch carrier to the exact request agent' , async ( ) = > {
const ctx = await mounted ( )
const { agent } = fakeAgent ( )
2026-07-12 22:39:01 +08:00
let scope ! : Scope
const scopeFiber = await ctx . plugin ( Object . assign ( ( inner : Context ) = > {
scope = createScope ( inner , agent )
} , { inject : [ 'approval' ] } ) )
2026-07-11 23:14:09 +08:00
let seenKey : object | undefined
scope . ctx . on ( 'approval/request' , function ( req , next ) {
seenKey = carrierKeyOf ( this )
expect ( req . agent ) . toBe ( agent )
return next ( )
} )
await expect ( ctx . approval . request ( requestOf ( agent ) ) ) . resolves . toBe ( 'unavailable' )
expect ( seenKey ) . toBe ( agent )
2026-07-12 22:39:01 +08:00
await scopeFiber . dispose ( )
2026-07-11 23:14:09 +08:00
} )
2026-07-09 15:25:18 +08:00
it ( 'contains a throwing answerer as unavailable' , async ( ) = > {
const ctx = await mounted ( )
const { agent , appended } = fakeAgent ( )
ctx . on ( 'approval/request' , ( ) = > Promise . reject ( new Error ( 'transport died' ) ) )
await expect ( ctx . approval . request ( requestOf ( agent ) ) ) . resolves . toBe ( 'unavailable' )
expect ( appended [ 1 ] ? . data ) . toMatchObject ( { outcome : 'unavailable' } )
} )
it ( 'normalizes a rogue non-vocabulary answer to unavailable' , async ( ) = > {
const ctx = await mounted ( )
const { agent } = fakeAgent ( )
// A JS answerer can return anything; the seam must not leak it into
// callers' closed-union switches.
ctx . on ( 'approval/request' , ( ) = > Promise . resolve ( 'yolo' as ApprovalOutcome ) )
await expect ( ctx . approval . request ( requestOf ( agent ) ) ) . resolves . toBe ( 'unavailable' )
} )
it ( 'settles cancelled immediately on an already-aborted signal without asking anyone' , async ( ) = > {
const ctx = await mounted ( )
const { agent , appended } = fakeAgent ( )
let asked = false
ctx . on ( 'approval/request' , ( ) = > {
asked = true
return Promise . resolve < ApprovalOutcome > ( 'allowed-once' )
} )
const outcome = await ctx . approval . request ( requestOf ( agent , { signal : AbortSignal.abort ( ) } ) )
expect ( outcome ) . toBe ( 'cancelled' )
expect ( asked ) . toBe ( false )
expect ( appended . map ( e = > e . type ) ) . toEqual ( [ 'approval/asked' , 'approval/decided' ] )
expect ( appended [ 1 ] ? . data ) . toMatchObject ( { outcome : 'cancelled' } )
} )
it ( 'resolves cancelled when the signal aborts mid-question and discards the late answer' , async ( ) = > {
const ctx = await mounted ( )
const { agent , appended } = fakeAgent ( )
let settleLate : ( ( outcome : ApprovalOutcome ) = > void ) | undefined
ctx . on ( 'approval/request' , ( ) = > new Promise < ApprovalOutcome > ( ( resolve ) = > { settleLate = resolve } ) )
const controller = new AbortController ( )
const pending = ctx . approval . request ( requestOf ( agent , { signal : controller.signal } ) )
controller . abort ( )
await expect ( pending ) . resolves . toBe ( 'cancelled' )
// The answerer settles after the fact: no second decided event appears.
settleLate ? . ( 'allowed-once' )
await Promise . resolve ( )
expect ( appended . filter ( e = > e . type === 'approval/decided' ) ) . toHaveLength ( 1 )
expect ( appended [ 1 ] ? . data ) . toMatchObject ( { outcome : 'cancelled' } )
} )
it ( 'discards a late REJECTION after abort without an unhandled rejection' , async ( ) = > {
const ctx = await mounted ( )
const { agent } = fakeAgent ( )
let rejectLate : ( ( error : Error ) = > void ) | undefined
ctx . on ( 'approval/request' , ( ) = > new Promise < ApprovalOutcome > ( ( _resolve , reject ) = > { rejectLate = reject } ) )
const controller = new AbortController ( )
const pending = ctx . approval . request ( requestOf ( agent , { signal : controller.signal } ) )
controller . abort ( )
await expect ( pending ) . resolves . toBe ( 'cancelled' )
rejectLate ? . ( new Error ( 'answered too late' ) )
// Drain microtasks: the contained rejection must not escape the seam.
await new Promise ( ( resolve ) = > { setTimeout ( resolve , 0 ) } )
} )
it ( 'resolves the answer when the signal never aborts' , async ( ) = > {
const ctx = await mounted ( )
const { agent } = fakeAgent ( )
ctx . on ( 'approval/request' , ( ) = > Promise . resolve < ApprovalOutcome > ( 'rejected' ) )
const controller = new AbortController ( )
await expect ( ctx . approval . request ( requestOf ( agent , { signal : controller.signal } ) ) ) . resolves . toBe ( 'rejected' )
} )
it ( 'issues a fresh id per request' , async ( ) = > {
const ctx = await mounted ( )
const { agent , appended } = fakeAgent ( )
await ctx . approval . request ( requestOf ( agent ) )
await ctx . approval . request ( requestOf ( agent ) )
const ids = appended . filter ( e = > e . type === 'approval/asked' ) . map ( e = > e . data [ 'id' ] )
expect ( ids ) . toHaveLength ( 2 )
expect ( ids [ 0 ] ) . not . toBe ( ids [ 1 ] )
} )
it ( 'drops a disposed plugin listener from the chain (HMR safety)' , async ( ) = > {
const ctx = await mounted ( )
const { agent } = fakeAgent ( )
const fiber = await ctx . plugin ( ( inner : Context ) = > {
inner . on ( 'approval/request' , ( ) = > Promise . resolve < ApprovalOutcome > ( 'allowed-once' ) )
} )
await expect ( ctx . approval . request ( requestOf ( agent ) ) ) . resolves . toBe ( 'allowed-once' )
await fiber . dispose ( )
await expect ( ctx . approval . request ( requestOf ( agent ) ) ) . resolves . toBe ( 'unavailable' )
} )
} )
2026-07-09 16:41:03 +08:00
describe ( 'approval policy (the approval/policy fold)' , ( ) = > {
const NEVER_SENTENCE = 'Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`).'
2026-07-11 21:37:38 +08:00
const ASK_MARKER = '<!-- dsh-user-approval-policy:ask -->'
const NEVER_MARKER = '<!-- dsh-user-approval-policy:never -->'
2026-07-09 16:41:03 +08:00
/**
* An agent stand-in over a REAL Session — gate, section, and narrator fold
* real events; the opened turn satisfies request()'s enclosure precondition.
*/
function sessionAgent ( id : string ) : { agent : Agent ; session : Session ; injected : string [ ] } {
const session = new Session ( SessionId ( id ) )
session . append ( 'turn/start' , { turn : 1 , trigger : { kind : 'message' , source : { kind : 'user' } } } )
const injected : string [ ] = [ ]
const agent = {
id ,
session ,
inject : ( content : { type : string ; text : string } [ ] ) = > { injected . push ( content [ 0 ] ? . text ? ? '' ) } ,
} as unknown as Agent
return { agent , session , injected }
}
const preStep = ( ctx : Context , agent : Agent ) : Promise < void > = >
2026-07-19 22:13:50 +08:00
agentEvents ( ctx , agent ) . serial ( 'agent/pre-step' , 1 , 1 , new AbortController ( ) . signal )
2026-07-09 16:41:03 +08:00
/** Append a `request/header` snapshot whose system text is exactly `system`. */
function appendHeader ( session : Session , system : string ) : void {
2026-07-14 21:57:52 +08:00
session . append ( 'request/header' , { header : { config : { provider : 'mock' , model : 'mock' } , system } , reason : 'initial' } )
2026-07-09 16:41:03 +08:00
}
it ( 'folds to the last event, or undefined without one' , ( ) = > {
const { session } = sessionAgent ( 'sess-fold' )
expect ( effectiveApprovalPolicy ( session . events ) ) . toBeUndefined ( )
setApprovalPolicy ( session , 'never' )
setApprovalPolicy ( session , 'ask' )
expect ( effectiveApprovalPolicy ( session . events ) ) . toBe ( 'ask' )
expect ( session . events . at ( - 1 ) ) . toMatchObject ( { type : 'approval/policy' , data : { policy : 'ask' } } )
} )
2026-07-12 05:13:17 +08:00
it ( 'rejects a policy outside the closed vocabulary before appending' , ( ) = > {
const append = vi . fn ( )
const session = { append } as unknown as Session
expect ( ( ) = > { setApprovalPolicy ( session , 'sometimes' as Parameters < typeof setApprovalPolicy > [ 1 ] ) } )
. toThrow ( 'approval policy must be one of "ask" or "never"' )
expect ( append ) . not . toHaveBeenCalled ( )
} )
2026-07-09 16:41:03 +08:00
it ( 'defaults a schema-less construction to ask (the ?? narrows the optional TYPE)' , async ( ) = > {
// Direct construction bypasses the plugin schema (the SystemPrompt-test
// precedent for covering a defaulted Config field's type-narrowing ??).
const ctx = new Context ( )
const service = new ApprovalService ( ctx , { } )
const { agent } = sessionAgent ( 'sess-bare-config' )
ctx . on ( 'approval/request' , ( ) = > Promise . resolve < ApprovalOutcome > ( 'allowed-once' ) )
await expect ( service . request ( { agent , toolName : 'echo' } ) ) . resolves . toBe ( 'allowed-once' )
} )
it ( 'contains an answerer that throws SYNCHRONOUSLY as unavailable' , async ( ) = > {
const ctx = new Context ( )
await ctx . plugin ( ApprovalService )
const { agent } = sessionAgent ( 'sess-syncthrow' )
ctx . on ( 'approval/request' , ( ) = > { throw new Error ( 'sync bug' ) } )
await expect ( ctx . approval . request ( { agent , toolName : 'echo' } ) ) . resolves . toBe ( 'unavailable' )
} )
it ( 'a never config rejects deterministically without consulting any answerer' , async ( ) = > {
const ctx = new Context ( )
await ctx . plugin ( ApprovalService , { policy : 'never' } )
const consulted = vi . fn ( )
ctx . on ( 'approval/request' , ( _req , next ) = > { consulted ( ) ; return next ( ) } )
const { agent , session } = sessionAgent ( 'sess-gate-1' )
await expect ( ctx . approval . request ( { agent , toolName : 'bash' } ) ) . resolves . toBe ( 'rejected' )
expect ( consulted ) . not . toHaveBeenCalled ( )
// The audit pair still lands on the session log.
expect ( session . events . filter ( e = > e . type === 'approval/asked' ) ) . toHaveLength ( 1 )
expect ( session . events . filter ( e = > e . type === 'approval/decided' ) ) . toHaveLength ( 1 )
} )
it ( 'the gate decides FIRST even against an answerer registered before the service (prepend)' , async ( ) = > {
const ctx = new Context ( )
ctx . on ( 'approval/request' , ( ) = > Promise . resolve < ApprovalOutcome > ( 'allowed-once' ) )
await ctx . plugin ( ApprovalService , { policy : 'never' } )
const { agent } = sessionAgent ( 'sess-gate-2' )
await expect ( ctx . approval . request ( { agent , toolName : 'bash' } ) ) . resolves . toBe ( 'rejected' )
} )
it ( 'never is unbypassable even by an answerer PREPENDED after the service mounts' , async ( ) = > {
2026-07-12 03:36:43 +08:00
// Cordis prepend unshifts ahead of every existing listener, including any gate LISTENER the
// service could register — which is exactly why the 'never' decision lives inside request()
2026-07-13 23:27:00 +08:00
// instead. This eager grant would bypass a listener-based gate and therefore must never run.
2026-07-09 16:41:03 +08:00
const ctx = new Context ( )
await ctx . plugin ( ApprovalService , { policy : 'never' } )
const consulted = vi . fn ( )
ctx . on ( 'approval/request' , ( ) = > { consulted ( ) ; return Promise . resolve < ApprovalOutcome > ( 'allowed-once' ) } , { prepend : true } )
const { agent , appended } = fakeAgent ( )
await expect ( ctx . approval . request ( requestOf ( agent ) ) ) . resolves . toBe ( 'rejected' )
expect ( consulted ) . not . toHaveBeenCalled ( )
expect ( appended . map ( e = > e . type ) ) . toEqual ( [ 'approval/asked' , 'approval/decided' ] )
} )
it ( 'a session override outranks the configured default, in both directions' , async ( ) = > {
const ctx = new Context ( )
await ctx . plugin ( ApprovalService , { policy : 'never' } )
ctx . on ( 'approval/request' , ( ) = > Promise . resolve < ApprovalOutcome > ( 'allowed-once' ) )
const { agent , session } = sessionAgent ( 'sess-gate-3' )
setApprovalPolicy ( session , 'ask' )
await expect ( ctx . approval . request ( { agent , toolName : 'bash' } ) ) . resolves . toBe ( 'allowed-once' )
setApprovalPolicy ( session , 'never' )
await expect ( ctx . approval . request ( { agent , toolName : 'bash' } ) ) . resolves . toBe ( 'rejected' )
} )
2026-07-11 21:37:38 +08:00
it ( 'states never (and only never) in prose while recording either policy with a source-owned marker' , async ( ) = > {
2026-07-09 16:41:03 +08:00
const ctx = new Context ( )
await ctx . plugin ( SystemPrompt )
await ctx . plugin ( ApprovalService )
const askAgent = sessionAgent ( 'sess-sect-ask' ) . agent
const { agent : neverAgent , session } = sessionAgent ( 'sess-sect-never' )
setApprovalPolicy ( session , 'never' )
const sectionFor = async ( context : object ) = >
( await ctx . systemPrompt . assemble ( context ) ) . sections . find ( s = > s . name === 'approval:policy' ) ? . text
2026-07-11 21:37:38 +08:00
expect ( await sectionFor ( { agent : askAgent } ) ) . toBe ( ASK_MARKER )
expect ( await sectionFor ( { agent : neverAgent } ) ) . toBe ( ` ${ NEVER_SENTENCE } \ n ${ NEVER_MARKER } ` )
2026-07-09 16:41:03 +08:00
// A bare assemble (no agent) has no session to state.
expect ( await sectionFor ( { } ) ) . toBe ( '' )
} )
it ( 'narrates nothing cold, once per coalesced switch (user wording), and idempotently' , async ( ) = > {
const ctx = new Context ( )
await ctx . plugin ( ApprovalService )
const { agent , session , injected } = sessionAgent ( 'sess-narr-1' )
await preStep ( ctx , agent )
expect ( injected ) . toEqual ( [ ] )
setApprovalPolicy ( session , 'never' )
setApprovalPolicy ( session , 'ask' )
setApprovalPolicy ( session , 'never' )
await preStep ( ctx , agent )
expect ( injected ) . toEqual ( [ 'The approval policy changed from "ask" to "never" (changed by the user).' ] )
await preStep ( ctx , agent )
expect ( injected ) . toHaveLength ( 1 )
setApprovalPolicy ( session , 'ask' )
setApprovalPolicy ( session , 'never' )
await preStep ( ctx , agent )
expect ( injected ) . toHaveLength ( 1 )
} )
it ( 'reads what the model was told back from the folded header text after a restart' , async ( ) = > {
// A session whose last request carried the never sentence resumes under
// an ask default: the narrator attributes the change to the operator.
const ctx = new Context ( )
await ctx . plugin ( ApprovalService )
const { agent , session , injected } = sessionAgent ( 'sess-narr-2' )
2026-07-11 21:37:38 +08:00
appendHeader ( session , ` persona \ n \ n ${ NEVER_SENTENCE } \ n ${ NEVER_MARKER } ` )
2026-07-09 16:41:03 +08:00
await preStep ( ctx , agent )
expect ( injected ) . toEqual ( [ 'The approval policy changed from "never" to "ask" (changed by the operator/config).' ] )
} )
2026-07-11 21:37:38 +08:00
it ( 'narrates a config default drift from the logged ask marker' , async ( ) = > {
2026-07-09 16:41:03 +08:00
const ctx = new Context ( )
await ctx . plugin ( ApprovalService , { policy : 'never' } )
const { agent , session , injected } = sessionAgent ( 'sess-narr-3' )
2026-07-11 21:37:38 +08:00
appendHeader ( session , ` persona only \ n ${ ASK_MARKER } ` )
2026-07-09 16:41:03 +08:00
await preStep ( ctx , agent )
expect ( injected ) . toEqual ( [ 'The approval policy changed from "ask" to "never" (changed by the operator/config).' ] )
} )
it ( 'a pinned override survives a default change silently' , async ( ) = > {
const ctx = new Context ( )
await ctx . plugin ( ApprovalService , { policy : 'never' } )
const { agent , session , injected } = sessionAgent ( 'sess-narr-4' )
2026-07-11 21:37:38 +08:00
appendHeader ( session , ` persona only \ n ${ ASK_MARKER } ` )
2026-07-09 16:41:03 +08:00
setApprovalPolicy ( session , 'ask' )
2026-07-11 21:37:38 +08:00
appendHeader ( session , ` persona only \ n ${ ASK_MARKER } ` )
2026-07-09 16:41:03 +08:00
await preStep ( ctx , agent )
expect ( injected ) . toEqual ( [ ] )
} )
2026-07-11 21:37:38 +08:00
it ( 'does not infer never from deployment prose that quotes the never sentence' , async ( ) = > {
const ctx = new Context ( )
await ctx . plugin ( ApprovalService )
const { agent , session , injected } = sessionAgent ( 'sess-narr-spoof-prose' )
appendHeader ( session , ` persona quotes this warning: ${ NEVER_SENTENCE } \ n ${ ASK_MARKER } ` )
await preStep ( ctx , agent )
expect ( injected ) . toEqual ( [ ] )
} )
it ( 'treats a legacy header with no source-owned marker as untold' , async ( ) = > {
const ctx = new Context ( )
await ctx . plugin ( ApprovalService , { policy : 'never' } )
const { agent , session , injected } = sessionAgent ( 'sess-narr-unmarked-header' )
appendHeader ( session , 'legacy persona-only header' )
await preStep ( ctx , agent )
expect ( injected ) . toEqual ( [ ] )
} )
it ( 'uses the service marker after an earlier persona marker' , async ( ) = > {
const ctx = new Context ( )
await ctx . plugin ( ApprovalService )
const { agent , session , injected } = sessionAgent ( 'sess-narr-spoof-marker' )
appendHeader ( session , ` persona quotes ${ NEVER_MARKER } \ n ${ ASK_MARKER } ` )
await preStep ( ctx , agent )
expect ( injected ) . toEqual ( [ ] )
} )
it ( 'disposes the service prompt section and pre-step narrator together (HMR safety)' , async ( ) = > {
const ctx = new Context ( )
await ctx . plugin ( SystemPrompt )
const fiber = await ctx . plugin ( ApprovalService )
const live = sessionAgent ( 'sess-hmr-service-live' )
const afterDispose = sessionAgent ( 'sess-hmr-service-disposed' )
const sectionFor = async ( ) = >
( await ctx . systemPrompt . assemble ( { agent : live.agent } ) ) . sections . find ( section = > section . name === 'approval:policy' )
expect ( await sectionFor ( ) ) . toBeDefined ( )
appendHeader ( live . session , ` persona \ n ${ ASK_MARKER } ` )
setApprovalPolicy ( live . session , 'never' )
await preStep ( ctx , live . agent )
expect ( live . injected ) . toEqual ( [ 'The approval policy changed from "ask" to "never" (changed by the user).' ] )
appendHeader ( afterDispose . session , ` persona \ n ${ ASK_MARKER } ` )
setApprovalPolicy ( afterDispose . session , 'never' )
await fiber . dispose ( )
expect ( await sectionFor ( ) ) . toBeUndefined ( )
await preStep ( ctx , afterDispose . agent )
expect ( afterDispose . injected ) . toEqual ( [ ] )
} )
2026-07-09 16:41:03 +08:00
} )