Skip to content

Commit ba58abb

Browse files
committed
Fix shellStart bug with incorrect shellId tracking
1 parent 310f984 commit ba58abb

File tree

2 files changed

+202
-5
lines changed

2 files changed

+202
-5
lines changed

packages/agent/src/tools/shell/shellStart.ts

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -103,11 +103,8 @@ export const shellStartTool: Tool<Parameters, ReturnType> = {
103103

104104
return new Promise((resolve) => {
105105
try {
106-
// Generate a unique ID for this process
107-
const shellId = uuidv4();
108-
109-
// Register this shell process with the shell tracker
110-
shellTracker.registerShell(command);
106+
// Register this shell process with the shell tracker and get the shellId
107+
const shellId = shellTracker.registerShell(command);
111108

112109
let hasResolved = false;
113110

Lines changed: 200 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
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

Comments
 (0)