🐺 Initial commit - Lupul Augmentat MCP Server
- 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
This commit is contained in:
176
tests/tools/FileListTool.test.ts
Normal file
176
tests/tools/FileListTool.test.ts
Normal file
@@ -0,0 +1,176 @@
|
||||
import { FileListTool } from '../../src/tools/builtin/FileListTool';
|
||||
import { ToolContext } from '../../src/tools/base/ToolHandler';
|
||||
import { promises as fs } from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
// Mock fs module
|
||||
jest.mock('fs', () => ({
|
||||
promises: {
|
||||
readdir: jest.fn(),
|
||||
stat: jest.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
describe('FileListTool', () => {
|
||||
let tool: FileListTool;
|
||||
|
||||
beforeEach(() => {
|
||||
tool = new FileListTool();
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
const createContext = (permissions: string[] = ['file:read']): ToolContext => ({
|
||||
requestId: 'test-request',
|
||||
permissions,
|
||||
});
|
||||
|
||||
describe('execute', () => {
|
||||
it('should list files in a directory', async () => {
|
||||
const testPath = './test/dir';
|
||||
|
||||
(fs.readdir as jest.Mock).mockResolvedValue(['file1.txt', 'file2.js', 'subdir']);
|
||||
(fs.stat as jest.Mock)
|
||||
.mockResolvedValueOnce({ isDirectory: () => true })
|
||||
.mockResolvedValueOnce({
|
||||
isDirectory: () => false,
|
||||
isFile: () => true,
|
||||
size: 100,
|
||||
mtime: new Date('2025-01-01'),
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
isDirectory: () => false,
|
||||
isFile: () => true,
|
||||
size: 200,
|
||||
mtime: new Date('2025-01-02'),
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
isDirectory: () => true,
|
||||
isFile: () => false,
|
||||
size: 0,
|
||||
mtime: new Date('2025-01-03'),
|
||||
});
|
||||
|
||||
const result = await tool.execute(
|
||||
{ path: testPath },
|
||||
createContext(),
|
||||
);
|
||||
|
||||
expect(result.path).toBe(path.resolve(testPath));
|
||||
const files = result.entries.filter(e => e.type === 'file');
|
||||
const dirs = result.entries.filter(e => e.type === 'directory');
|
||||
expect(files).toHaveLength(2);
|
||||
expect(dirs).toHaveLength(1);
|
||||
expect(files[0]).toMatchObject({
|
||||
name: 'file1.txt',
|
||||
size: 100,
|
||||
});
|
||||
});
|
||||
|
||||
it('should list files recursively', async () => {
|
||||
const testPath = './test/dir';
|
||||
|
||||
// Mock for root directory
|
||||
(fs.readdir as jest.Mock)
|
||||
.mockResolvedValueOnce(['file.txt', 'subdir'])
|
||||
.mockResolvedValueOnce(['nested.txt']);
|
||||
|
||||
(fs.stat as jest.Mock)
|
||||
.mockResolvedValueOnce({ isDirectory: () => true }) // root dir
|
||||
.mockResolvedValueOnce({
|
||||
isDirectory: () => false,
|
||||
isFile: () => true,
|
||||
size: 100,
|
||||
mtime: new Date(),
|
||||
}) // file.txt
|
||||
.mockResolvedValueOnce({
|
||||
isDirectory: () => true,
|
||||
isFile: () => false,
|
||||
size: 0,
|
||||
mtime: new Date(),
|
||||
}) // subdir
|
||||
.mockResolvedValueOnce({
|
||||
isDirectory: () => false,
|
||||
isFile: () => true,
|
||||
size: 50,
|
||||
mtime: new Date(),
|
||||
}); // nested.txt
|
||||
|
||||
const result = await tool.execute(
|
||||
{ path: testPath, recursive: true },
|
||||
createContext(),
|
||||
);
|
||||
|
||||
const files = result.entries.filter(e => e.type === 'file');
|
||||
expect(files).toHaveLength(2);
|
||||
expect(files.map(f => f.path)).toContain(path.resolve(testPath, 'file.txt'));
|
||||
expect(files.map(f => f.path)).toContain(path.resolve(testPath, 'subdir', 'nested.txt'));
|
||||
});
|
||||
|
||||
it('should filter by pattern', async () => {
|
||||
const testPath = './test/dir';
|
||||
|
||||
(fs.readdir as jest.Mock).mockResolvedValue(['file1.txt', 'file2.js', 'test.txt']);
|
||||
(fs.stat as jest.Mock)
|
||||
.mockResolvedValueOnce({ isDirectory: () => true })
|
||||
.mockResolvedValueOnce({
|
||||
isDirectory: () => false,
|
||||
isFile: () => true,
|
||||
size: 100,
|
||||
mtime: new Date(),
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
isDirectory: () => false,
|
||||
isFile: () => true,
|
||||
size: 200,
|
||||
mtime: new Date(),
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
isDirectory: () => false,
|
||||
isFile: () => true,
|
||||
size: 150,
|
||||
mtime: new Date(),
|
||||
});
|
||||
|
||||
const result = await tool.execute(
|
||||
{ path: testPath, pattern: '*.txt' },
|
||||
createContext(),
|
||||
);
|
||||
|
||||
const files = result.entries.filter(e => e.type === 'file');
|
||||
expect(files).toHaveLength(2);
|
||||
expect(files.every(f => f.name.endsWith('.txt'))).toBe(true);
|
||||
});
|
||||
|
||||
it('should prevent directory traversal', async () => {
|
||||
await expect(
|
||||
tool.execute(
|
||||
{ path: '../../../etc' },
|
||||
createContext(),
|
||||
),
|
||||
).rejects.toThrow('Invalid path: directory traversal not allowed');
|
||||
});
|
||||
|
||||
it('should require file:read permission', async () => {
|
||||
await expect(
|
||||
tool.execute(
|
||||
{ path: './test' },
|
||||
createContext([]),
|
||||
),
|
||||
).rejects.toThrow('Permission denied: file:read required');
|
||||
});
|
||||
|
||||
it('should handle non-existent directory', async () => {
|
||||
(fs.stat as jest.Mock).mockResolvedValue({
|
||||
isDirectory: () => false,
|
||||
isFile: () => false
|
||||
});
|
||||
|
||||
await expect(
|
||||
tool.execute(
|
||||
{ path: './non/existent' },
|
||||
createContext(),
|
||||
),
|
||||
).rejects.toThrow('Path is not a directory');
|
||||
});
|
||||
});
|
||||
});
|
||||
92
tests/tools/FileReadTool.test.ts
Normal file
92
tests/tools/FileReadTool.test.ts
Normal file
@@ -0,0 +1,92 @@
|
||||
import { FileReadTool } from '../../src/tools/builtin/FileReadTool';
|
||||
import { ToolContext } from '../../src/tools/base/ToolHandler';
|
||||
import * as fs from 'fs/promises';
|
||||
import * as path from 'path';
|
||||
import * as os from 'os';
|
||||
|
||||
describe('FileReadTool', () => {
|
||||
let tool: FileReadTool;
|
||||
let testDir: string;
|
||||
let testFile: string;
|
||||
const testContent = 'Hello, World!\nThis is a test file.';
|
||||
|
||||
beforeAll(async () => {
|
||||
tool = new FileReadTool();
|
||||
testDir = await fs.mkdtemp(path.join(os.tmpdir(), 'test-'));
|
||||
testFile = path.join(testDir, 'test.txt');
|
||||
await fs.writeFile(testFile, testContent);
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await fs.rm(testDir, { recursive: true });
|
||||
});
|
||||
|
||||
const createContext = (permissions: string[] = ['file:read']): ToolContext => ({
|
||||
requestId: 'test-request',
|
||||
permissions,
|
||||
});
|
||||
|
||||
describe('execute', () => {
|
||||
it('should read file content successfully', async () => {
|
||||
const result = await tool.execute(
|
||||
{ path: testFile },
|
||||
createContext(),
|
||||
);
|
||||
|
||||
expect(result).toEqual({
|
||||
content: testContent,
|
||||
size: Buffer.from(testContent).length,
|
||||
path: testFile,
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle different encodings', async () => {
|
||||
const binaryFile = path.join(testDir, 'binary.bin');
|
||||
const binaryContent = Buffer.from([0x00, 0x01, 0x02, 0x03]);
|
||||
await fs.writeFile(binaryFile, binaryContent);
|
||||
|
||||
const result = await tool.execute(
|
||||
{ path: binaryFile, encoding: 'binary' },
|
||||
createContext(),
|
||||
);
|
||||
|
||||
expect(result.content).toBe(binaryContent.toString('binary'));
|
||||
});
|
||||
|
||||
it('should throw error for non-existent file', async () => {
|
||||
await expect(
|
||||
tool.execute(
|
||||
{ path: './non/existent/file.txt' },
|
||||
createContext(),
|
||||
),
|
||||
).rejects.toThrow('File not found');
|
||||
});
|
||||
|
||||
it('should throw error for directory', async () => {
|
||||
await expect(
|
||||
tool.execute(
|
||||
{ path: testDir },
|
||||
createContext(),
|
||||
),
|
||||
).rejects.toThrow('Path is not a file');
|
||||
});
|
||||
|
||||
it('should throw error without permission', async () => {
|
||||
await expect(
|
||||
tool.execute(
|
||||
{ path: testFile },
|
||||
createContext([]),
|
||||
),
|
||||
).rejects.toThrow('Permission denied: file:read required');
|
||||
});
|
||||
|
||||
it('should prevent directory traversal', async () => {
|
||||
await expect(
|
||||
tool.execute(
|
||||
{ path: '../../../etc/passwd' },
|
||||
createContext(),
|
||||
),
|
||||
).rejects.toThrow('Invalid path: directory traversal not allowed');
|
||||
});
|
||||
});
|
||||
});
|
||||
113
tests/tools/FileWriteTool.test.ts
Normal file
113
tests/tools/FileWriteTool.test.ts
Normal file
@@ -0,0 +1,113 @@
|
||||
import { FileWriteTool } from '../../src/tools/builtin/FileWriteTool';
|
||||
import { ToolContext } from '../../src/tools/base/ToolHandler';
|
||||
import { promises as fs } from 'fs';
|
||||
import * as path from 'path';
|
||||
import * as os from 'os';
|
||||
|
||||
// Mock fs module
|
||||
jest.mock('fs', () => ({
|
||||
promises: {
|
||||
mkdir: jest.fn(),
|
||||
writeFile: jest.fn(),
|
||||
stat: jest.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
describe('FileWriteTool', () => {
|
||||
let tool: FileWriteTool;
|
||||
let tempDir: string;
|
||||
|
||||
beforeEach(() => {
|
||||
tool = new FileWriteTool();
|
||||
tempDir = path.join(os.tmpdir(), 'mcp-test');
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
const createContext = (permissions: string[] = ['file:write']): ToolContext => ({
|
||||
requestId: 'test-request',
|
||||
permissions,
|
||||
});
|
||||
|
||||
describe('execute', () => {
|
||||
it('should write content to a file', async () => {
|
||||
const testPath = path.join(tempDir, 'test.txt');
|
||||
const content = 'Hello, World!';
|
||||
|
||||
(fs.mkdir as jest.Mock).mockResolvedValue(undefined);
|
||||
(fs.writeFile as jest.Mock).mockResolvedValue(undefined);
|
||||
(fs.stat as jest.Mock).mockResolvedValue({ size: content.length });
|
||||
|
||||
const result = await tool.execute(
|
||||
{ path: testPath, content },
|
||||
createContext(),
|
||||
);
|
||||
|
||||
expect(result).toEqual({
|
||||
path: testPath,
|
||||
size: content.length,
|
||||
mode: 'overwrite',
|
||||
});
|
||||
|
||||
expect(fs.mkdir).toHaveBeenCalledWith(tempDir, { recursive: true });
|
||||
expect(fs.writeFile).toHaveBeenCalledWith(testPath, content, 'utf8');
|
||||
});
|
||||
|
||||
it('should overwrite existing file', async () => {
|
||||
const testPath = path.join(tempDir, 'existing.txt');
|
||||
const content = 'New content';
|
||||
|
||||
(fs.mkdir as jest.Mock).mockResolvedValue(undefined);
|
||||
(fs.writeFile as jest.Mock).mockResolvedValue(undefined);
|
||||
(fs.stat as jest.Mock).mockResolvedValue({
|
||||
isFile: () => true,
|
||||
size: content.length
|
||||
});
|
||||
|
||||
const result = await tool.execute(
|
||||
{ path: testPath, content },
|
||||
createContext(),
|
||||
);
|
||||
|
||||
expect(result.mode).toBe('overwrite');
|
||||
expect(fs.writeFile).toHaveBeenCalledWith(testPath, content, 'utf8');
|
||||
});
|
||||
|
||||
it('should prevent directory traversal', async () => {
|
||||
await expect(
|
||||
tool.execute(
|
||||
{ path: '../../../etc/passwd', content: 'malicious' },
|
||||
createContext(),
|
||||
),
|
||||
).rejects.toThrow('Invalid path: directory traversal not allowed');
|
||||
});
|
||||
|
||||
it('should require file:write permission', async () => {
|
||||
await expect(
|
||||
tool.execute(
|
||||
{ path: 'test.txt', content: 'test' },
|
||||
createContext([]),
|
||||
),
|
||||
).rejects.toThrow('Permission denied: file:write required');
|
||||
});
|
||||
|
||||
it('should handle binary content', async () => {
|
||||
const testPath = path.join(tempDir, 'binary.bin');
|
||||
const binaryContent = Buffer.from([0x89, 0x50, 0x4e, 0x47]).toString('base64');
|
||||
|
||||
(fs.mkdir as jest.Mock).mockResolvedValue(undefined);
|
||||
(fs.writeFile as jest.Mock).mockResolvedValue(undefined);
|
||||
(fs.stat as jest.Mock).mockResolvedValue({ size: 4 }); // binary data size
|
||||
|
||||
await tool.execute(
|
||||
{ path: testPath, content: binaryContent, encoding: 'base64' },
|
||||
createContext(),
|
||||
);
|
||||
|
||||
expect(fs.writeFile).toHaveBeenCalledWith(
|
||||
testPath,
|
||||
expect.any(Buffer),
|
||||
undefined,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
130
tests/tools/HttpRequestTool.test.ts
Normal file
130
tests/tools/HttpRequestTool.test.ts
Normal file
@@ -0,0 +1,130 @@
|
||||
import { HttpRequestTool } from '../../src/tools/builtin/HttpRequestTool';
|
||||
import { ToolContext } from '../../src/tools/base/ToolHandler';
|
||||
|
||||
// Mock fetch
|
||||
global.fetch = jest.fn();
|
||||
|
||||
describe('HttpRequestTool', () => {
|
||||
let tool: HttpRequestTool;
|
||||
|
||||
beforeEach(() => {
|
||||
tool = new HttpRequestTool();
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
const createContext = (permissions: string[] = ['network:http']): ToolContext => ({
|
||||
requestId: 'test-request',
|
||||
permissions,
|
||||
});
|
||||
|
||||
describe('execute', () => {
|
||||
it('should make GET request successfully', async () => {
|
||||
const mockResponse = {
|
||||
status: 200,
|
||||
statusText: 'OK',
|
||||
headers: new Headers({ 'content-type': 'application/json' }),
|
||||
json: async () => ({ data: 'test' }),
|
||||
};
|
||||
|
||||
(global.fetch as jest.Mock).mockResolvedValue(mockResponse);
|
||||
|
||||
const result = await tool.execute(
|
||||
{ url: 'https://api.example.com/data' },
|
||||
createContext(),
|
||||
);
|
||||
|
||||
expect(result).toMatchObject({
|
||||
status: 200,
|
||||
statusText: 'OK',
|
||||
body: { data: 'test' },
|
||||
});
|
||||
|
||||
expect(global.fetch).toHaveBeenCalledWith(
|
||||
'https://api.example.com/data',
|
||||
expect.objectContaining({
|
||||
method: 'GET',
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should make POST request with JSON body', async () => {
|
||||
const mockResponse = {
|
||||
status: 201,
|
||||
statusText: 'Created',
|
||||
headers: new Headers({ 'content-type': 'application/json' }),
|
||||
json: async () => ({ id: 123 }),
|
||||
};
|
||||
|
||||
(global.fetch as jest.Mock).mockResolvedValue(mockResponse);
|
||||
|
||||
const result = await tool.execute(
|
||||
{
|
||||
url: 'https://api.example.com/users',
|
||||
method: 'POST',
|
||||
body: { name: 'John Doe' },
|
||||
},
|
||||
createContext(),
|
||||
);
|
||||
|
||||
expect(result.status).toBe(201);
|
||||
expect(global.fetch).toHaveBeenCalledWith(
|
||||
'https://api.example.com/users',
|
||||
expect.objectContaining({
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ name: 'John Doe' }),
|
||||
headers: expect.objectContaining({
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw error for internal URLs', async () => {
|
||||
await expect(
|
||||
tool.execute(
|
||||
{ url: 'http://localhost:8080/internal' },
|
||||
createContext(),
|
||||
),
|
||||
).rejects.toThrow('Requests to internal networks are not allowed');
|
||||
|
||||
await expect(
|
||||
tool.execute(
|
||||
{ url: 'http://192.168.1.1/admin' },
|
||||
createContext(),
|
||||
),
|
||||
).rejects.toThrow('Requests to internal networks are not allowed');
|
||||
});
|
||||
|
||||
it('should throw error without permission', async () => {
|
||||
await expect(
|
||||
tool.execute(
|
||||
{ url: 'https://api.example.com' },
|
||||
createContext([]),
|
||||
),
|
||||
).rejects.toThrow('Permission denied: network:http required');
|
||||
});
|
||||
|
||||
it('should handle timeout', async () => {
|
||||
(global.fetch as jest.Mock).mockImplementation(
|
||||
(_url, options) => new Promise((_resolve, reject) => {
|
||||
// Simulate AbortController behavior
|
||||
if (options?.signal) {
|
||||
options.signal.addEventListener('abort', () => {
|
||||
const error = new Error('The operation was aborted');
|
||||
error.name = 'AbortError';
|
||||
reject(error);
|
||||
});
|
||||
}
|
||||
// Never resolve to simulate timeout
|
||||
}),
|
||||
);
|
||||
|
||||
await expect(
|
||||
tool.execute(
|
||||
{ url: 'https://api.example.com', timeout: 100 },
|
||||
createContext(),
|
||||
),
|
||||
).rejects.toThrow('Request timeout after 100ms');
|
||||
});
|
||||
});
|
||||
});
|
||||
201
tests/tools/SystemCommandTool.test.ts
Normal file
201
tests/tools/SystemCommandTool.test.ts
Normal file
@@ -0,0 +1,201 @@
|
||||
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();
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user