@@ -4,7 +4,7 @@ import { createAgent } from '@apeira/core'
44import { hitl } from '@apeira/plugin-hitl'
55import { describe , expect , it } from 'vitest'
66
7- import { createHitlDemoTools , createHitlReplayFetch , createHitlResumeInput } from '../../shared/hitl-demo'
7+ import { createHitlDemoTools , createHitlReplayFetch } from '../../shared/hitl-demo'
88
99const userInput = ( content : string ) : ItemParam => ( {
1010 content,
@@ -27,6 +27,29 @@ const readEvents = async (stream: ReadableStream<AgentEvent>) => {
2727 return events
2828}
2929
30+ const readEventsAndDecide = async (
31+ stream : ReadableStream < AgentEvent > ,
32+ decide : ( id : string ) => void ,
33+ ) => {
34+ const reader = stream . getReader ( )
35+ const events : AgentEvent [ ] = [ ]
36+ let decided = false
37+
38+ while ( true ) {
39+ const { done, value } = await reader . read ( )
40+ if ( done )
41+ break
42+
43+ events . push ( value )
44+ if ( ! decided && value . type === 'tool-interruption' ) {
45+ decided = true
46+ decide ( value . interruption . id )
47+ }
48+ }
49+
50+ return events
51+ }
52+
3053const createDemoSession = ( ) => {
3154 const controller = hitl ( { mode : 'ask' } )
3255 const replay = createHitlReplayFetch ( )
@@ -58,68 +81,78 @@ const text = (events: AgentEvent[]) =>
5881describe ( 'hitl demo runtime integration' , ( ) => {
5982 it ( 'asks again after a call-scope approval' , async ( ) => {
6083 const { controller, session } = createDemoSession ( )
61- const first = await readEvents ( session . run ( userInput ( 'hitl-demo once' ) ) )
84+ const first = await readEventsAndDecide (
85+ session . run ( userInput ( 'hitl-demo once' ) ) ,
86+ id => expect ( controller . approve ( id , 'call' ) ) . toBe ( true ) ,
87+ )
6288 const id = interruptions ( first ) [ 0 ] . interruption . id
6389
6490 expect ( id ) . toBeDefined ( )
65- expect ( controller . approve ( id , 'call' ) ) . toBe ( true )
66- await readEvents ( session . run ( userInput ( createHitlResumeInput ( id , 'approved' ) ) ) )
6791
68- const second = await readEvents ( session . run ( userInput ( 'hitl-demo once' ) ) )
92+ const second = await readEventsAndDecide (
93+ session . run ( userInput ( 'hitl-demo once' ) ) ,
94+ id => expect ( controller . reject ( id ) ) . toBe ( true ) ,
95+ )
6996 expect ( interruptions ( second ) ) . toHaveLength ( 1 )
7097 } )
7198
7299 it ( 'lets run-scope approval continue repeated same-key calls in one resumed turn only' , async ( ) => {
73100 const { controller, session } = createDemoSession ( )
74- const first = await readEvents ( session . run ( userInput ( 'hitl-demo turn' ) ) )
101+ const first = await readEventsAndDecide (
102+ session . run ( userInput ( 'hitl-demo turn' ) ) ,
103+ id => expect ( controller . approve ( id , 'run' ) ) . toBe ( true ) ,
104+ )
75105 const id = interruptions ( first ) [ 0 ] . interruption . id
76106
77107 expect ( id ) . toBeDefined ( )
78- expect ( controller . approve ( id , 'run' ) ) . toBe ( true )
79108
80- const resumed = await readEvents ( session . run ( userInput ( createHitlResumeInput ( id , 'approved' ) ) ) )
81- expect ( interruptions ( resumed ) ) . toHaveLength ( 0 )
82-
83- const nextTurn = await readEvents ( session . run ( userInput ( 'hitl-demo turn' ) ) )
109+ const nextTurn = await readEventsAndDecide (
110+ session . run ( userInput ( 'hitl-demo turn' ) ) ,
111+ id => expect ( controller . reject ( id ) ) . toBe ( true ) ,
112+ )
84113 expect ( interruptions ( nextTurn ) ) . toHaveLength ( 1 )
85114 } )
86115
87116 it ( 'remembers conversation-scope approvals by exact key' , async ( ) => {
88117 const { controller, session } = createDemoSession ( )
89- const first = await readEvents ( session . run ( userInput ( 'hitl-demo conversation' ) ) )
118+ const first = await readEventsAndDecide (
119+ session . run ( userInput ( 'hitl-demo conversation' ) ) ,
120+ id => expect ( controller . approve ( id , 'conversation' ) ) . toBe ( true ) ,
121+ )
90122 const id = interruptions ( first ) [ 0 ] . interruption . id
91123
92124 expect ( id ) . toBeDefined ( )
93- expect ( controller . approve ( id , 'conversation' ) ) . toBe ( true )
94- await readEvents ( session . run ( userInput ( createHitlResumeInput ( id , 'approved' ) ) ) )
95125
96126 const sameKey = await readEvents ( session . run ( userInput ( 'hitl-demo conversation' ) ) )
97127 expect ( interruptions ( sameKey ) ) . toHaveLength ( 0 )
98128 } )
99129
100130 it ( 'keeps approval-key exact after a conversation approval' , async ( ) => {
101131 const { controller, session } = createDemoSession ( )
102- const first = await readEvents ( session . run ( userInput ( 'hitl-demo approval-key' ) ) )
132+ const first = await readEventsAndDecide (
133+ session . run ( userInput ( 'hitl-demo approval-key' ) ) ,
134+ id => expect ( controller . approve ( id , 'conversation' ) ) . toBe ( true ) ,
135+ )
103136 const id = interruptions ( first ) [ 0 ] . interruption . id
104137
105138 expect ( id ) . toBeDefined ( )
106- expect ( controller . approve ( id , 'conversation' ) ) . toBe ( true )
107- await readEvents ( session . run ( userInput ( createHitlResumeInput ( id , 'approved' ) ) ) )
108139
109- const dangerous = await readEvents ( session . run ( userInput ( 'hitl-demo approval-key' ) ) )
140+ const dangerous = await readEventsAndDecide (
141+ session . run ( userInput ( 'hitl-demo approval-key' ) ) ,
142+ id => expect ( controller . reject ( id ) ) . toBe ( true ) ,
143+ )
110144 expect ( JSON . stringify ( dangerous ) ) . toContain ( 'rm -rf .' )
111145 expect ( interruptions ( dangerous ) ) . toHaveLength ( 1 )
112146 } )
113147
114148 it ( 'returns a model-visible rejection summary' , async ( ) => {
115149 const { controller, session } = createDemoSession ( )
116- const first = await readEvents ( session . run ( userInput ( 'hitl-demo reject' ) ) )
117- const id = interruptions ( first ) [ 0 ] . interruption . id
118-
119- expect ( id ) . toBeDefined ( )
120- expect ( controller . reject ( id , 'TOOL_HITL_REJECTED: denied in demo' ) ) . toBe ( true )
150+ const events = await readEventsAndDecide (
151+ session . run ( userInput ( 'hitl-demo reject' ) ) ,
152+ id => expect ( controller . reject ( id , 'TOOL_HITL_REJECTED: denied in demo' ) ) . toBe ( true ) ,
153+ )
121154
122- const resumed = await readEvents ( session . run ( userInput ( createHitlResumeInput ( id , 'rejected' ) ) ) )
123- expect ( text ( resumed ) ) . toContain ( '用户拒绝' )
155+ expect ( interruptions ( events ) [ 0 ] . interruption . id ) . toBeDefined ( )
156+ expect ( text ( events ) ) . toContain ( '用户拒绝' )
124157 } )
125158} )
0 commit comments