- MCP server cu stdio transport pentru performanță maximă
- Tool-uri pentru file operations, HTTP requests, system commands
- Suport NATS pentru comunicare inter-module
- Configurare nginx cu API key auth și SSL
- Arhitectură modulară și extensibilă
🤖 Generated with Claude Code
201 lines
5.3 KiB
TypeScript
201 lines
5.3 KiB
TypeScript
import { SystemCommandTool } from '../../src/tools/builtin/SystemCommandTool';
|
|
import { ToolContext } from '../../src/tools/base/ToolHandler';
|
|
import { spawn } from 'child_process';
|
|
import { EventEmitter } from 'events';
|
|
|
|
// Mock child_process
|
|
jest.mock('child_process');
|
|
|
|
class MockChildProcess extends EventEmitter {
|
|
stdout = new EventEmitter();
|
|
stderr = new EventEmitter();
|
|
stdin = {
|
|
write: jest.fn(),
|
|
end: jest.fn(),
|
|
};
|
|
kill = jest.fn();
|
|
}
|
|
|
|
describe('SystemCommandTool', () => {
|
|
jest.setTimeout(10000); // Increase timeout for these tests
|
|
let tool: SystemCommandTool;
|
|
let mockSpawn: jest.MockedFunction<typeof spawn>;
|
|
|
|
beforeEach(() => {
|
|
tool = new SystemCommandTool();
|
|
mockSpawn = spawn as jest.MockedFunction<typeof spawn>;
|
|
jest.clearAllMocks();
|
|
});
|
|
|
|
const createContext = (permissions: string[] = ['system:exec']): ToolContext => ({
|
|
requestId: 'test-request',
|
|
permissions,
|
|
});
|
|
|
|
describe('execute', () => {
|
|
it('should execute allowed commands', async () => {
|
|
const mockProcess = new MockChildProcess();
|
|
mockSpawn.mockReturnValue(mockProcess as any);
|
|
|
|
const resultPromise = tool.execute(
|
|
{ command: 'ls', args: ['-la'] },
|
|
createContext(),
|
|
);
|
|
|
|
// Simulate command output
|
|
setImmediate(() => {
|
|
mockProcess.stdout.emit('data', Buffer.from('file1.txt\nfile2.txt'));
|
|
mockProcess.emit('close', 0);
|
|
});
|
|
|
|
const result = await resultPromise;
|
|
|
|
expect(result).toMatchObject({
|
|
stdout: 'file1.txt\nfile2.txt',
|
|
stderr: '',
|
|
exitCode: 0,
|
|
});
|
|
|
|
expect(mockSpawn).toHaveBeenCalledWith('ls', ['-la'], expect.any(Object));
|
|
});
|
|
|
|
it('should handle command with environment variables', async () => {
|
|
const mockProcess = new MockChildProcess();
|
|
mockSpawn.mockReturnValue(mockProcess as any);
|
|
|
|
const resultPromise = tool.execute(
|
|
{
|
|
command: 'echo',
|
|
args: ['test'],
|
|
env: { TEST_VAR: 'test-value' },
|
|
},
|
|
createContext(),
|
|
);
|
|
|
|
setImmediate(() => {
|
|
mockProcess.stdout.emit('data', Buffer.from('test'));
|
|
mockProcess.emit('close', 0);
|
|
});
|
|
|
|
await resultPromise;
|
|
|
|
expect(mockSpawn).toHaveBeenCalledWith(
|
|
'echo',
|
|
['test'],
|
|
expect.objectContaining({
|
|
env: expect.objectContaining({
|
|
TEST_VAR: 'test-value',
|
|
}),
|
|
}),
|
|
);
|
|
});
|
|
|
|
it('should reject non-whitelisted commands', async () => {
|
|
await expect(
|
|
tool.execute(
|
|
{ command: 'rm', args: ['-rf', '/'] },
|
|
createContext(),
|
|
),
|
|
).rejects.toThrow('Command not allowed: rm');
|
|
});
|
|
|
|
it('should prevent command injection', async () => {
|
|
await expect(
|
|
tool.execute(
|
|
{ command: 'ls', args: ['; rm -rf /'] },
|
|
createContext(),
|
|
),
|
|
).rejects.toThrow('Shell characters not allowed in');
|
|
});
|
|
|
|
it('should handle command timeout', async () => {
|
|
const mockProcess = new MockChildProcess();
|
|
mockSpawn.mockReturnValue(mockProcess as any);
|
|
|
|
const resultPromise = tool.execute(
|
|
{ command: 'ls', timeout: 100 },
|
|
createContext(),
|
|
);
|
|
|
|
// Don't emit close event to simulate timeout
|
|
await expect(resultPromise).rejects.toThrow('Command timed out after 100ms');
|
|
});
|
|
|
|
it('should handle command failure', async () => {
|
|
const mockProcess = new MockChildProcess();
|
|
mockSpawn.mockReturnValue(mockProcess as any);
|
|
|
|
const resultPromise = tool.execute(
|
|
{ command: 'ls' },
|
|
createContext(),
|
|
);
|
|
|
|
setImmediate(() => {
|
|
mockProcess.stderr.emit('data', Buffer.from('Command not found'));
|
|
mockProcess.emit('close', 127);
|
|
});
|
|
|
|
const result = await resultPromise;
|
|
|
|
expect(result).toEqual({
|
|
stdout: '',
|
|
stderr: 'Command not found',
|
|
exitCode: 127,
|
|
duration: expect.any(Number),
|
|
});
|
|
});
|
|
|
|
it('should require system:exec permission', async () => {
|
|
await expect(
|
|
tool.execute(
|
|
{ command: 'ls' },
|
|
createContext([]),
|
|
),
|
|
).rejects.toThrow('Permission denied: system:exec required');
|
|
});
|
|
|
|
it('should respect working directory', async () => {
|
|
const mockProcess = new MockChildProcess();
|
|
mockSpawn.mockReturnValue(mockProcess as any);
|
|
|
|
const resultPromise = tool.execute(
|
|
{ command: 'ls', cwd: '/tmp' },
|
|
createContext(),
|
|
);
|
|
|
|
setImmediate(() => {
|
|
mockProcess.emit('close', 0);
|
|
});
|
|
|
|
await resultPromise;
|
|
|
|
expect(mockSpawn).toHaveBeenCalledWith(
|
|
'ls',
|
|
[],
|
|
expect.objectContaining({
|
|
cwd: '/tmp',
|
|
}),
|
|
);
|
|
});
|
|
|
|
it('should handle stdin input', async () => {
|
|
const mockProcess = new MockChildProcess();
|
|
mockSpawn.mockReturnValue(mockProcess as any);
|
|
|
|
const resultPromise = tool.execute(
|
|
{ command: 'cat', stdin: 'Hello, World!' },
|
|
createContext(),
|
|
);
|
|
|
|
setImmediate(() => {
|
|
mockProcess.stdout.emit('data', Buffer.from('Hello, World!'));
|
|
mockProcess.emit('close', 0);
|
|
});
|
|
|
|
await resultPromise;
|
|
|
|
expect(mockProcess.stdin.write).toHaveBeenCalledWith('Hello, World!');
|
|
expect(mockProcess.stdin.end).toHaveBeenCalled();
|
|
});
|
|
});
|
|
}); |