-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathrun_long_command.test.ts
More file actions
162 lines (132 loc) · 4.9 KB
/
run_long_command.test.ts
File metadata and controls
162 lines (132 loc) · 4.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
/**
* @license
* Copyright 2026 Steven A. Thompson (steve@stevenathompson.com)
* SPDX-License-Identifier: MIT
*/
import {
describe,
it,
expect,
vi,
beforeEach,
afterEach,
type Mock,
} from 'vitest';
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { spawn, execSync } from 'child_process';
import { EventEmitter } from 'events';
// Mock the MCP server and transport
const mockRegisterTool = vi.fn();
const mockConnect = vi.fn();
vi.mock('@modelcontextprotocol/sdk/server/mcp.js', () => ({
McpServer: vi.fn().mockImplementation(() => ({
registerTool: mockRegisterTool,
connect: mockConnect,
})),
}));
vi.mock('@modelcontextprotocol/sdk/server/stdio.js', () => ({
StdioServerTransport: vi.fn(),
}));
// Mock child_process
vi.mock('child_process', () => ({
spawn: vi.fn(),
execSync: vi.fn(),
}));
describe('run_long_command MCP Server', () => {
let toolFn: Function;
beforeEach(async () => {
vi.clearAllMocks();
// Dynamically import to trigger tool registration
await import('./run_long_command.js');
toolFn = (mockRegisterTool as Mock).mock.calls[0][2];
});
afterEach(() => {
vi.resetModules();
});
it('should register the "run_long_command" tool', () => {
expect(mockRegisterTool).toHaveBeenCalledWith(
'run_long_command',
expect.objectContaining({
description: expect.stringContaining('Executes a long-running shell command'),
}),
expect.any(Function),
);
});
it('should fail if not in the correct tmux session', async () => {
(execSync as Mock).mockImplementation(() => {
throw new Error('session not found');
});
const result = await toolFn({ command: 'ls' });
expect(result.isError).toBe(true);
expect(result.content[0].text).toContain("Error: Not running inside tmux session 'gemini-cli'");
});
it('should spawn the command and return immediately if in tmux session', async () => {
(execSync as Mock).mockReturnValue(Buffer.from('')); // has-session succeeds
const mockChild = new EventEmitter() as any;
mockChild.unref = vi.fn();
mockChild.stdout = new EventEmitter();
mockChild.stderr = new EventEmitter();
(spawn as Mock).mockReturnValue(mockChild);
const result = await toolFn({ command: 'sleep 10' });
expect(spawn).toHaveBeenCalledWith('sleep 10', expect.objectContaining({
detached: true,
stdio: ['ignore', 'pipe', 'pipe'],
}));
// expect(mockChild.unref).toHaveBeenCalled(); // Removed
expect(result.content[0].text).toContain('started in the background');
});
it('should notify Gemini when the command completes', async () => {
vi.useFakeTimers();
(execSync as Mock).mockReturnValue(Buffer.from('')); // All execSync calls succeed
const mockChild = new EventEmitter() as any;
mockChild.unref = vi.fn();
mockChild.stdout = new EventEmitter();
mockChild.stderr = new EventEmitter();
(spawn as Mock).mockReturnValue(mockChild);
await toolFn({ command: 'echo hello' });
// Simulate process completion
mockChild.emit('close', 0);
// Fast-forward through delays in notifyGemini
await vi.runAllTimersAsync();
// Check if tmux send-keys was called
expect(execSync).toHaveBeenCalledWith(expect.stringContaining('tmux send-keys -t gemini-cli:0.0 Escape'));
// Reconstruct the message from individual send-keys calls
const sendKeyCalls = (execSync as Mock).mock.calls.map(call => call[0]);
const typedChars = sendKeyCalls
.filter(call => call.includes("tmux send-keys -t gemini-cli:0.0 '"))
.map(call => {
const match = call.match(/'(.*)'$/);
return match ? match[1] : '';
})
.join('')
.replace(/'\''/g, "'");
expect(typedChars).toContain('Cmd: "echo hello" (0) Out: []');
vi.useRealTimers();
});
it('should notify Gemini when the command fails to start', async () => {
vi.useFakeTimers();
(execSync as Mock).mockReturnValue(Buffer.from(''));
const mockChild = new EventEmitter() as any;
mockChild.unref = vi.fn();
mockChild.stdout = new EventEmitter();
mockChild.stderr = new EventEmitter();
(spawn as Mock).mockReturnValue(mockChild);
await toolFn({ command: 'invalid-command' });
// Simulate error
mockChild.emit('error', new Error('spawn ENOENT'));
await vi.runAllTimersAsync();
// Reconstruct the message from individual send-keys calls
const sendKeyCalls = (execSync as Mock).mock.calls.map(call => call[0]);
const typedChars = sendKeyCalls
.filter(call => call.includes("tmux send-keys -t gemini-cli:0.0 '"))
.map(call => {
const match = call.match(/'(.*)'$/);
return match ? match[1] : '';
})
.join('')
.replace(/'\\''/g, "'");
expect(typedChars).toContain('Err: "invalid-command" (spawn ENOENT)');
vi.useRealTimers();
});
});