MCP Command Proxy
by Hormold
- src
- utils
import { CommandRunner, ProcessStatus } from './command-runner';
import * as pty from 'node-pty';
// Mock node-pty
jest.mock('node-pty', () => ({
spawn: jest.fn(() => ({
onData: jest.fn(),
onExit: jest.fn(),
write: jest.fn(),
kill: jest.fn(),
})),
}));
describe('CommandRunner', () => {
let runner: CommandRunner;
let mockProcess: jest.Mocked<pty.IPty>;
beforeEach(() => {
jest.clearAllMocks();
mockProcess = {
onData: jest.fn(),
onExit: jest.fn(),
write: jest.fn(),
kill: jest.fn(),
} as unknown as jest.Mocked<pty.IPty>;
(pty.spawn as jest.Mock).mockReturnValue(mockProcess);
});
describe('constructor', () => {
it('should initialize with default options', () => {
runner = new CommandRunner({ command: 'test-cmd' });
expect(runner.getStatus()).toBe(ProcessStatus.STOPPED);
expect(runner.getLogs()).toEqual([]);
});
it('should parse command and arguments correctly', () => {
runner = new CommandRunner({
command: 'test-cmd arg1 arg2',
args: ['arg3']
});
runner.start();
expect(pty.spawn).toHaveBeenCalledWith(
'test-cmd',
['arg1', 'arg2', 'arg3'],
expect.any(Object)
);
});
it('should use provided options', () => {
const cwd = '/test/dir';
const env = { TEST_ENV: 'value' };
runner = new CommandRunner({
command: 'test-cmd',
cwd,
env,
logBufferSize: 100
});
runner.start();
expect(pty.spawn).toHaveBeenCalledWith(
'test-cmd',
[],
expect.objectContaining({
cwd,
env: expect.objectContaining(env)
})
);
});
});
describe('process lifecycle', () => {
beforeEach(() => {
runner = new CommandRunner({ command: 'test-cmd' });
});
it('should start process and update status', () => {
const statusListener = jest.fn();
runner.on('statusChange', statusListener);
runner.start();
expect(runner.getStatus()).toBe(ProcessStatus.RUNNING);
expect(statusListener).toHaveBeenCalledWith(ProcessStatus.RUNNING);
expect(pty.spawn).toHaveBeenCalled();
});
it('should handle process exit', () => {
const exitListener = jest.fn();
const statusListener = jest.fn();
runner.on('exit', exitListener);
runner.on('statusChange', statusListener);
runner.start();
const exitCallback = (mockProcess.onExit as jest.Mock).mock.calls[0][0];
exitCallback({ exitCode: 0, signal: null });
expect(runner.getStatus()).toBe(ProcessStatus.STOPPED);
expect(exitListener).toHaveBeenCalledWith(0, null);
expect(statusListener).toHaveBeenCalledWith(ProcessStatus.STOPPED);
});
it('should stop process', () => {
runner.start();
runner.stop();
expect(mockProcess.kill).toHaveBeenCalled();
});
});
describe('log management', () => {
beforeEach(() => {
runner = new CommandRunner({ command: 'test-cmd' });
});
it('should capture stdout', () => {
const logListener = jest.fn();
runner.on('log', logListener);
runner.start();
const dataCallback = (mockProcess.onData as jest.Mock).mock.calls[0][0];
dataCallback('test output');
const logs = runner.getLogs();
expect(logs).toHaveLength(2); // Including start command log
expect(logs[1].content).toBe('test output');
expect(logs[1].type).toBe('stdout');
expect(logListener).toHaveBeenCalledWith(expect.objectContaining({
content: 'test output',
type: 'stdout'
}));
});
it('should respect log buffer size', () => {
runner = new CommandRunner({
command: 'test-cmd',
logBufferSize: 2
});
runner.start();
const dataCallback = (mockProcess.onData as jest.Mock).mock.calls[0][0];
dataCallback('output1');
dataCallback('output2');
dataCallback('output3');
const logs = runner.getLogs();
expect(logs).toHaveLength(2);
expect(logs.map(l => l.content)).toEqual(['output2', 'output3']);
});
});
describe('error handling', () => {
it('should handle spawn errors', () => {
const error = new Error('Spawn error');
(pty.spawn as jest.Mock).mockImplementation(() => {
throw error;
});
const errorListener = jest.fn();
runner = new CommandRunner({ command: 'test-cmd' });
runner.on('error', errorListener);
runner.start();
expect(runner.getStatus()).toBe(ProcessStatus.ERROR);
expect(errorListener).toHaveBeenCalledWith(error);
});
it('should handle write errors', () => {
runner = new CommandRunner({ command: 'test-cmd' });
runner.start();
mockProcess.write.mockImplementation(() => {
throw new Error('Write error');
});
runner.write('test');
const logs = runner.getLogs();
expect(logs.some(log =>
log.type === 'system' &&
log.content.includes('Failed to write key')
)).toBe(true);
});
});
describe('write', () => {
beforeEach(() => {
runner = new CommandRunner({ command: 'test-cmd' });
});
it('should write to process when running', () => {
runner.start();
runner.write('test input');
expect(mockProcess.write).toHaveBeenCalledWith('test input');
});
it('should not write when process is stopped', () => {
runner.write('test input');
expect(mockProcess.write).not.toHaveBeenCalled();
const logs = runner.getLogs();
expect(logs.some(log =>
log.type === 'system' &&
log.content.includes('Cannot write to process: not running')
)).toBe(true);
});
});
});