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