11import { Text } from 'ink' ;
22import { render } from 'ink-testing-library' ;
33
4- import { MODE } from '../../constants' ;
5- import { ollama , tools } from '../../utils' ;
6- import { tick } from '../../utils/test' ;
4+ import { DECISION , MODE } from '../../constants' ;
5+ import { ollama , test , tools } from '../../utils' ;
76
87const mockState = vi . hoisted ( ( ) => ( {
9- handlers : [ ] as ( ( value : string ) => void ) [ ] ,
8+ handler : undefined as ( ( value : string ) => void ) | undefined ,
109 testInput : '' ,
1110 shouldReset : false ,
1211 clear ( ) {
13- this . handlers . length = 0 ;
12+ this . handler = undefined ;
1413 this . testInput = '' ;
1514 this . shouldReset = true ;
1615 } ,
@@ -23,19 +22,35 @@ const planApprovalState = vi.hoisted(() => ({
2322 } ,
2423} ) ) ;
2524
25+ const toolApprovalState = vi . hoisted ( ( ) => ( {
26+ onChange : undefined as ( ( value : DECISION . Decision ) => void ) | undefined ,
27+ clear ( ) {
28+ this . onChange = undefined ;
29+ } ,
30+ } ) ) ;
31+
2632vi . mock ( '@inkjs/ui' , async ( ) => {
27- const actual = await vi . importActual < typeof import ( '@inkjs/ui' ) > ( '@inkjs/ui' ) ;
33+ const actual = await vi . importActual ( '@inkjs/ui' ) ;
2834 const { Text } = await import ( 'ink' ) ;
2935 return {
3036 ...actual ,
3137 Select : ( {
3238 options,
3339 onChange,
3440 } : {
35- options : { label : string ; value : MODE . Name } [ ] ;
36- onChange ?: ( value : MODE . Name ) => void ;
41+ options : { label : string ; value : string } [ ] ;
42+ onChange ?: ( value : string ) => void ;
3743 } ) => {
38- planApprovalState . onChange = onChange ;
44+ const isPlanApproval = options . some ( ( { value } ) =>
45+ Object . values ( MODE . NAME ) . includes ( value as MODE . Name ) ,
46+ ) ;
47+
48+ if ( isPlanApproval ) {
49+ planApprovalState . onChange = onChange ;
50+ } else {
51+ toolApprovalState . onChange = onChange ;
52+ }
53+
3954 return (
4055 < >
4156 { options . map ( ( { value, label } ) => (
@@ -47,33 +62,29 @@ vi.mock('@inkjs/ui', async () => {
4762 } ;
4863} ) ;
4964
50- vi . mock ( '../../utils' , async ( ) => {
51- const actual =
52- await vi . importActual < typeof import ( '../../utils' ) > ( '../../utils' ) ;
53- return {
54- ...actual ,
55- ollama : {
56- streamChat : vi . fn ( ) . mockImplementation ( function * ( ) {
57- yield { type : 'content' , content : 'Mocked' } ;
58- yield { type : 'content' , content : ' response' } ;
59- } ) ,
60- } ,
61- tools : {
62- TOOLS : [ ] ,
63- READ_ONLY_TOOLS : new Set ( ) ,
64- DANGEROUS_TOOLS : new Set ( ) ,
65- executeTool : vi . fn ( ) ,
66- } ,
67- } ;
68- } ) ;
65+ vi . mock ( '../../utils' , async ( ) => ( {
66+ ...( await vi . importActual ( '../../utils' ) ) ,
67+ ollama : {
68+ streamChat : vi . fn ( ) . mockImplementation ( function * ( ) {
69+ yield { type : 'content' , content : 'Mocked' } ;
70+ yield { type : 'content' , content : ' response' } ;
71+ } ) ,
72+ } ,
73+ tools : {
74+ TOOLS : [ ] ,
75+ READ_ONLY_TOOLS : new Set ( ) ,
76+ DANGEROUS_TOOLS : new Set ( ) ,
77+ executeTool : vi . fn ( ) ,
78+ } ,
79+ } ) ) ;
6980
7081vi . mock ( './Input' , ( ) => ( {
7182 Input : ( props : {
7283 onSubmit ?: ( value : string ) => void ;
7384 isDisabled ?: boolean ;
7485 } ) => {
7586 if ( props . onSubmit ) {
76- mockState . handlers . push ( props . onSubmit ) ;
87+ mockState . handler = props . onSubmit ;
7788 }
7889
7990 if ( props . isDisabled ) {
@@ -102,30 +113,33 @@ async function typeText(
102113) {
103114 mockState . testInput = text ;
104115 rerender ( tree ) ;
105- await tick ( ) ;
116+ await test . tick ( ) ;
106117}
107118
108119function submitInput ( value : string ) {
109- for ( const handler of mockState . handlers ) {
110- handler ( value ) ;
111- }
120+ mockState . handler ?.( value ) ;
112121 mockState . clear ( ) ;
113122}
114123
115124function choosePlanMode ( mode : MODE . Name ) {
116125 planApprovalState . onChange ?.( mode ) ;
117126}
118127
128+ function chooseToolDecision ( decision : DECISION . Decision ) {
129+ toolApprovalState . onChange ?.( decision ) ;
130+ }
131+
119132async function waitForStream ( ) {
120133 // Allow time for async generator to yield values
121- await tick ( 10 ) ;
134+ await test . tick ( 10 ) ;
122135}
123136
124137function resetChatMocks ( ) {
125138 vi . restoreAllMocks ( ) ;
126139 vi . clearAllMocks ( ) ;
127140 mockState . clear ( ) ;
128141 planApprovalState . clear ( ) ;
142+ toolApprovalState . clear ( ) ;
129143 tools . TOOLS . splice ( 0 , tools . TOOLS . length ) ;
130144 vi . mocked ( ollama . streamChat ) . mockImplementation ( async function * ( ) {
131145 await Promise . resolve ( ) ;
@@ -151,7 +165,7 @@ describe('Chat', () => {
151165 onModeChange = { onModeChange }
152166 /> ,
153167 ) ;
154- await tick ( ) ;
168+ await test . tick ( ) ;
155169 const frame = lastFrame ( ) ?? '' ;
156170 expect ( frame ) . not . toContain ( 'coding assistant' ) ;
157171 expect ( frame ) . toContain ( '>' ) ;
@@ -167,7 +181,7 @@ describe('Chat', () => {
167181 />
168182 ) ;
169183 const { lastFrame, rerender } = render ( chat ) ;
170- await tick ( ) ;
184+ await test . tick ( ) ;
171185 await typeText ( rerender , 'hello' , chat ) ;
172186 submitInput ( 'hello' ) ;
173187 rerender ( chat ) ;
@@ -186,7 +200,7 @@ describe('Chat', () => {
186200 />
187201 ) ;
188202 const { lastFrame, rerender } = render ( chat ) ;
189- await tick ( ) ;
203+ await test . tick ( ) ;
190204 await typeText ( rerender , 'hello' , chat ) ;
191205 submitInput ( 'hello' ) ;
192206 rerender ( chat ) ;
@@ -206,13 +220,13 @@ describe('Chat', () => {
206220 />
207221 ) ;
208222 const { lastFrame, rerender } = render ( chat ) ;
209- await tick ( ) ;
223+ await test . tick ( ) ;
210224 const beforeFrame = lastFrame ( ) ?? '' ;
211225 const systemLineCount = beforeFrame . split ( '\n' ) . length ;
212226 await typeText ( rerender , ' ' , chat ) ;
213227 submitInput ( ' ' ) ;
214228 rerender ( chat ) ;
215- await tick ( ) ;
229+ await test . tick ( ) ;
216230 const afterFrame = lastFrame ( ) ?? '' ;
217231 const afterLineCount = afterFrame . split ( '\n' ) . length ;
218232 // After submitting blank input, line count should not increase
@@ -231,7 +245,7 @@ describe('Chat', () => {
231245 />
232246 ) ;
233247 const { lastFrame, rerender } = render ( chat ) ;
234- await tick ( ) ;
248+ await test . tick ( ) ;
235249 await typeText ( rerender , 'first' , chat ) ;
236250 submitInput ( 'first' ) ;
237251 rerender ( chat ) ;
@@ -260,7 +274,7 @@ describe('Chat', () => {
260274 const { rerender } = render ( chat ) ;
261275 submitInput ( '/model' ) ;
262276 rerender ( chat ) ;
263- await tick ( ) ;
277+ await test . tick ( ) ;
264278 expect ( onCommand ) . toHaveBeenCalledWith ( '/model' ) ;
265279 } ) ;
266280
@@ -715,7 +729,7 @@ describe('Chat with tool calls', () => {
715729 expect ( lastFrame ( ) ) . toContain ( 'Plan Generated' ) ;
716730
717731 choosePlanMode ( MODE . NAME . PLAN ) ;
718- await tick ( ) ;
732+ await test . tick ( ) ;
719733 rerender ( chat ) ;
720734
721735 expect ( onModeChange ) . toHaveBeenCalledWith ( MODE . NAME . PLAN ) ;
@@ -724,7 +738,7 @@ describe('Chat with tool calls', () => {
724738 ) ;
725739
726740 choosePlanMode ( MODE . NAME . AUTO ) ;
727- await tick ( ) ;
741+ await test . tick ( ) ;
728742 } ) ;
729743
730744 it ( 'executes an approved plan immediately in auto mode' , async ( ) => {
@@ -867,7 +881,7 @@ describe('Chat with tool calls', () => {
867881 onModeChange = { vi . fn ( ) }
868882 />
869883 ) ;
870- const { lastFrame, rerender, stdin } = render ( chat ) ;
884+ const { lastFrame, rerender } = render ( chat ) ;
871885
872886 await typeText ( rerender , 'write a file' , chat ) ;
873887 submitInput ( 'write a file' ) ;
@@ -878,11 +892,8 @@ describe('Chat with tool calls', () => {
878892 // Verify approval prompt is shown
879893 expect ( lastFrame ( ) ) . toContain ( 'Tool requires approval' ) ;
880894
881- // Reject the tool (move to No with right arrow, then Enter)
882- stdin . write ( '\x1B[C' ) ; // Right arrow
883- await tick ( ) ;
884- stdin . write ( '\r' ) ; // Enter
885- await tick ( ) ;
895+ chooseToolDecision ( DECISION . REJECT ) ;
896+ await waitForStream ( ) ;
886897 rerender ( chat ) ;
887898
888899 // Should show rejection message
@@ -928,7 +939,7 @@ describe('Chat with tool calls', () => {
928939 onModeChange = { vi . fn ( ) }
929940 />
930941 ) ;
931- const { lastFrame, rerender, stdin } = render ( chat ) ;
942+ const { lastFrame, rerender } = render ( chat ) ;
932943
933944 await typeText ( rerender , 'write a file' , chat ) ;
934945 submitInput ( 'write a file' ) ;
@@ -939,9 +950,8 @@ describe('Chat with tool calls', () => {
939950 // Verify approval prompt is shown
940951 expect ( lastFrame ( ) ) . toContain ( 'Tool requires approval' ) ;
941952
942- // Approve the tool by pressing Enter (yes is default)
943- stdin . write ( '\r' ) ; // Enter
944- await tick ( ) ;
953+ chooseToolDecision ( DECISION . APPROVE ) ;
954+ await waitForStream ( ) ;
945955 rerender ( chat ) ;
946956
947957 // Should have called executeTool
@@ -991,17 +1001,16 @@ describe('Chat with tool calls', () => {
9911001 onModeChange = { vi . fn ( ) }
9921002 />
9931003 ) ;
994- const { rerender, stdin } = render ( chat ) ;
1004+ const { rerender } = render ( chat ) ;
9951005
9961006 await typeText ( rerender , 'write a file' , chat ) ;
9971007 submitInput ( 'write a file' ) ;
9981008 rerender ( chat ) ;
9991009 await waitForStream ( ) ;
10001010 rerender ( chat ) ;
10011011
1002- // Approve the tool by pressing Enter
1003- stdin . write ( '\r' ) ;
1004- await tick ( ) ;
1012+ chooseToolDecision ( DECISION . APPROVE ) ;
1013+ await waitForStream ( ) ;
10051014 rerender ( chat ) ;
10061015
10071016 // Should have called executeTool
0 commit comments