1
+ import { describe , it , expect , vi , beforeEach } from 'vitest' ;
2
+ import { shellStartTool } from './shellStart' ;
3
+ import { ShellStatus , ShellTracker } from './ShellTracker' ;
4
+
5
+ import type { ToolContext } from '../../core/types' ;
6
+
7
+ /**
8
+ * Tests for the shellStart bug fix where shellId wasn't being properly
9
+ * tracked for shell status updates.
10
+ */
11
+ describe ( 'shellStart bug fix' , ( ) => {
12
+ // Create a mock ShellTracker with the real implementation
13
+ const shellTracker = new ShellTracker ( 'test-agent' ) ;
14
+
15
+ // Spy on the real methods
16
+ const registerShellSpy = vi . spyOn ( shellTracker , 'registerShell' ) ;
17
+ const updateShellStatusSpy = vi . spyOn ( shellTracker , 'updateShellStatus' ) ;
18
+
19
+ // Create a mock process that allows us to trigger events
20
+ const mockProcess = {
21
+ on : vi . fn ( ( event , handler ) => {
22
+ mockProcess [ `${ event } Handler` ] = handler ;
23
+ return mockProcess ;
24
+ } ) ,
25
+ stdout : {
26
+ on : vi . fn ( ( event , handler ) => {
27
+ mockProcess [ `stdout${ event } Handler` ] = handler ;
28
+ return mockProcess . stdout ;
29
+ } )
30
+ } ,
31
+ stderr : {
32
+ on : vi . fn ( ( event , handler ) => {
33
+ mockProcess [ `stderr${ event } Handler` ] = handler ;
34
+ return mockProcess . stderr ;
35
+ } )
36
+ } ,
37
+ // Trigger an exit event
38
+ triggerExit : ( code : number , signal : string | null ) => {
39
+ mockProcess [ `exitHandler` ] ?.( code , signal ) ;
40
+ } ,
41
+ // Trigger an error event
42
+ triggerError : ( error : Error ) => {
43
+ mockProcess [ `errorHandler` ] ?.( error ) ;
44
+ }
45
+ } ;
46
+
47
+ // Mock child_process.spawn
48
+ vi . mock ( 'child_process' , ( ) => ( {
49
+ spawn : vi . fn ( ( ) => mockProcess )
50
+ } ) ) ;
51
+
52
+ // Create mock logger
53
+ const mockLogger = {
54
+ log : vi . fn ( ) ,
55
+ debug : vi . fn ( ) ,
56
+ error : vi . fn ( ) ,
57
+ warn : vi . fn ( ) ,
58
+ info : vi . fn ( ) ,
59
+ } ;
60
+
61
+ // Create mock context
62
+ const mockContext : ToolContext = {
63
+ logger : mockLogger as any ,
64
+ workingDirectory : '/test' ,
65
+ headless : false ,
66
+ userSession : false ,
67
+ tokenTracker : { trackTokens : vi . fn ( ) } as any ,
68
+ githubMode : false ,
69
+ provider : 'anthropic' ,
70
+ maxTokens : 4000 ,
71
+ temperature : 0 ,
72
+ agentTracker : { registerAgent : vi . fn ( ) } as any ,
73
+ shellTracker : shellTracker as any ,
74
+ browserTracker : { registerSession : vi . fn ( ) } as any ,
75
+ } ;
76
+
77
+ beforeEach ( ( ) => {
78
+ vi . clearAllMocks ( ) ;
79
+ shellTracker [ 'shells' ] = new Map ( ) ;
80
+ shellTracker . processStates . clear ( ) ;
81
+ } ) ;
82
+
83
+ it ( 'should use the shellId returned from registerShell when updating status' , async ( ) => {
84
+ // Start the shell command
85
+ const commandPromise = shellStartTool . execute (
86
+ { command : 'test command' , description : 'Test' , timeout : 5000 } ,
87
+ mockContext
88
+ ) ;
89
+
90
+ // Verify registerShell was called with the correct command
91
+ expect ( registerShellSpy ) . toHaveBeenCalledWith ( 'test command' ) ;
92
+
93
+ // Get the shellId that was generated
94
+ const shellId = registerShellSpy . mock . results [ 0 ] . value ;
95
+
96
+ // Verify the shell is registered as running
97
+ const runningShells = shellTracker . getShells ( ShellStatus . RUNNING ) ;
98
+ expect ( runningShells . length ) . toBe ( 1 ) ;
99
+ expect ( runningShells [ 0 ] . shellId ) . toBe ( shellId ) ;
100
+
101
+ // Trigger the process to complete
102
+ mockProcess . triggerExit ( 0 , null ) ;
103
+
104
+ // Await the command to complete
105
+ const result = await commandPromise ;
106
+
107
+ // Verify we got a sync response
108
+ expect ( result . mode ) . toBe ( 'sync' ) ;
109
+
110
+ // Verify updateShellStatus was called with the correct shellId
111
+ expect ( updateShellStatusSpy ) . toHaveBeenCalledWith (
112
+ shellId ,
113
+ ShellStatus . COMPLETED ,
114
+ expect . objectContaining ( { exitCode : 0 } )
115
+ ) ;
116
+
117
+ // Verify the shell is now marked as completed
118
+ const completedShells = shellTracker . getShells ( ShellStatus . COMPLETED ) ;
119
+ expect ( completedShells . length ) . toBe ( 1 ) ;
120
+ expect ( completedShells [ 0 ] . shellId ) . toBe ( shellId ) ;
121
+
122
+ // Verify no shells are left in running state
123
+ expect ( shellTracker . getShells ( ShellStatus . RUNNING ) . length ) . toBe ( 0 ) ;
124
+ } ) ;
125
+
126
+ it ( 'should properly update status when process fails' , async ( ) => {
127
+ // Start the shell command
128
+ const commandPromise = shellStartTool . execute (
129
+ { command : 'failing command' , description : 'Test failure' , timeout : 5000 } ,
130
+ mockContext
131
+ ) ;
132
+
133
+ // Get the shellId that was generated
134
+ const shellId = registerShellSpy . mock . results [ 0 ] . value ;
135
+
136
+ // Trigger the process to fail
137
+ mockProcess . triggerExit ( 1 , null ) ;
138
+
139
+ // Await the command to complete
140
+ const result = await commandPromise ;
141
+
142
+ // Verify we got a sync response with error
143
+ expect ( result . mode ) . toBe ( 'sync' ) ;
144
+ expect ( result . exitCode ) . toBe ( 1 ) ;
145
+
146
+ // Verify updateShellStatus was called with the correct shellId and ERROR status
147
+ expect ( updateShellStatusSpy ) . toHaveBeenCalledWith (
148
+ shellId ,
149
+ ShellStatus . ERROR ,
150
+ expect . objectContaining ( { exitCode : 1 } )
151
+ ) ;
152
+
153
+ // Verify the shell is now marked as error
154
+ const errorShells = shellTracker . getShells ( ShellStatus . ERROR ) ;
155
+ expect ( errorShells . length ) . toBe ( 1 ) ;
156
+ expect ( errorShells [ 0 ] . shellId ) . toBe ( shellId ) ;
157
+
158
+ // Verify no shells are left in running state
159
+ expect ( shellTracker . getShells ( ShellStatus . RUNNING ) . length ) . toBe ( 0 ) ;
160
+ } ) ;
161
+
162
+ it ( 'should properly update status in async mode' , async ( ) => {
163
+ // Start the shell command with very short timeout to force async mode
164
+ const commandPromise = shellStartTool . execute (
165
+ { command : 'long command' , description : 'Test async' , timeout : 0 } ,
166
+ mockContext
167
+ ) ;
168
+
169
+ // Get the shellId that was generated
170
+ const shellId = registerShellSpy . mock . results [ 0 ] . value ;
171
+
172
+ // Await the command (which should return in async mode due to timeout=0)
173
+ const result = await commandPromise ;
174
+
175
+ // Verify we got an async response
176
+ expect ( result . mode ) . toBe ( 'async' ) ;
177
+ expect ( result . shellId ) . toBe ( shellId ) ;
178
+
179
+ // Shell should still be running
180
+ expect ( shellTracker . getShells ( ShellStatus . RUNNING ) . length ) . toBe ( 1 ) ;
181
+
182
+ // Now trigger the process to complete
183
+ mockProcess . triggerExit ( 0 , null ) ;
184
+
185
+ // Verify updateShellStatus was called with the correct shellId
186
+ expect ( updateShellStatusSpy ) . toHaveBeenCalledWith (
187
+ shellId ,
188
+ ShellStatus . COMPLETED ,
189
+ expect . objectContaining ( { exitCode : 0 } )
190
+ ) ;
191
+
192
+ // Verify the shell is now marked as completed
193
+ const completedShells = shellTracker . getShells ( ShellStatus . COMPLETED ) ;
194
+ expect ( completedShells . length ) . toBe ( 1 ) ;
195
+ expect ( completedShells [ 0 ] . shellId ) . toBe ( shellId ) ;
196
+
197
+ // Verify no shells are left in running state
198
+ expect ( shellTracker . getShells ( ShellStatus . RUNNING ) . length ) . toBe ( 0 ) ;
199
+ } ) ;
200
+ } ) ;
0 commit comments