/**
* Terminal Module Tests
*
* Tests for the terminal module functionality including:
* - Session management
* - Command execution
* - Output retrieval
* - Session cleanup
*/
import { describe, it, expect, beforeEach, afterEach } from '@jest/globals';
import {
handleConnect,
handleExecute,
handleGetOutput,
handleDisconnect,
cleanupExpiredSessions,
getActiveSessionCount,
getSessionInfo,
terminalTools,
TERMINAL_TOOL_COUNT,
} from '../src/modules/terminal';
describe('Terminal Module', () => {
let testSessionId: string;
beforeEach(() => {
// Reset session state before each test
testSessionId = '';
});
afterEach(async () => {
// Clean up any sessions created during tests
if (testSessionId) {
await handleDisconnect({ session_id: testSessionId });
}
cleanupExpiredSessions(0); // Clean up all sessions
});
describe('Tool Definitions', () => {
it('should export correct number of tools', () => {
expect(terminalTools).toHaveLength(TERMINAL_TOOL_COUNT);
expect(TERMINAL_TOOL_COUNT).toBe(4);
});
it('should have correct tool names', () => {
const toolNames = terminalTools.map(tool => tool.name);
expect(toolNames).toContain('komodo_terminal_Connect');
expect(toolNames).toContain('komodo_terminal_Execute');
expect(toolNames).toContain('komodo_terminal_GetOutput');
expect(toolNames).toContain('komodo_terminal_Disconnect');
});
it('should have valid input schemas', () => {
terminalTools.forEach(tool => {
expect(tool.inputSchema).toBeDefined();
expect(tool.inputSchema.type).toBe('object');
expect(tool.inputSchema.properties).toBeDefined();
expect(tool.inputSchema.required).toBeDefined();
});
});
});
describe('handleConnect', () => {
it('should create a new terminal session', async () => {
const result = await handleConnect({
server_id: 'test-server-123',
});
expect(result.success).toBe(true);
expect(result.sessionId).toBeDefined();
expect(result.status).toBe('connected');
expect(result.message).toContain('Terminal session established');
testSessionId = result.sessionId!;
});
it('should accept custom shell option', async () => {
const result = await handleConnect({
server_id: 'test-server-123',
options: {
shell: 'bash',
cwd: '/home/user',
},
});
expect(result.success).toBe(true);
expect(result.sessionId).toBeDefined();
testSessionId = result.sessionId!;
});
it('should reuse existing session for same server', async () => {
const result1 = await handleConnect({
server_id: 'test-server-123',
});
testSessionId = result1.sessionId!;
const result2 = await handleConnect({
server_id: 'test-server-123',
});
// Should return the same session or a new one
expect(result2.success).toBe(true);
expect(result2.sessionId).toBeDefined();
});
});
describe('handleExecute', () => {
beforeEach(async () => {
// Create a session for execute tests
const result = await handleConnect({
server_id: 'test-server-123',
});
testSessionId = result.sessionId!;
});
it('should execute command with existing session', async () => {
const result = await handleExecute({
server_id: 'test-server-123',
command: 'ls -la',
session_id: testSessionId,
});
expect(result.success).toBe(true);
expect(result.executionId).toBeDefined();
expect(result.sessionId).toBe(testSessionId);
expect(result.status).toBe('running');
});
it('should execute command without session (creates new)', async () => {
const result = await handleExecute({
server_id: 'test-server-456',
command: 'echo "Hello World"',
});
expect(result.success).toBe(true);
expect(result.executionId).toBeDefined();
expect(result.sessionId).toBeDefined();
// Clean up the auto-created session
if (result.sessionId) {
await handleDisconnect({ session_id: result.sessionId });
}
});
it('should accept execution options', async () => {
const result = await handleExecute({
server_id: 'test-server-123',
command: 'pwd',
session_id: testSessionId,
options: {
cwd: '/tmp',
timeout: 60,
captureOutput: true,
},
});
expect(result.success).toBe(true);
expect(result.executionId).toBeDefined();
});
it('should handle commands with stdin', async () => {
const result = await handleExecute({
server_id: 'test-server-123',
command: 'cat',
session_id: testSessionId,
options: {
stdin: 'test input data',
},
});
expect(result.success).toBe(true);
});
});
describe('handleGetOutput', () => {
let executionSessionId: string;
beforeEach(async () => {
// Create session and execute command
const connectResult = await handleConnect({
server_id: 'test-server-123',
});
executionSessionId = connectResult.sessionId!;
await handleExecute({
server_id: 'test-server-123',
command: 'echo "test output"',
session_id: executionSessionId,
});
testSessionId = executionSessionId;
});
it('should retrieve output for valid session', async () => {
const result = await handleGetOutput({
session_id: executionSessionId,
});
expect(result.success).toBe(true);
expect(result.output).toBeDefined();
expect(result.output?.stdout).toBeDefined();
expect(result.output?.stderr).toBeDefined();
expect(result.output?.isComplete).toBeDefined();
});
it('should fail for non-existent session', async () => {
const result = await handleGetOutput({
session_id: 'non-existent-session-id',
});
expect(result.success).toBe(false);
expect(result.error).toBe('Session not found');
});
it('should support output options', async () => {
const result = await handleGetOutput({
session_id: executionSessionId,
options: {
lines: 100,
tail: true,
},
});
expect(result.success).toBe(true);
});
});
describe('handleDisconnect', () => {
beforeEach(async () => {
const result = await handleConnect({
server_id: 'test-server-123',
});
testSessionId = result.sessionId!;
});
it('should disconnect existing session', async () => {
const result = await handleDisconnect({
session_id: testSessionId,
});
expect(result.success).toBe(true);
expect(result.sessionId).toBe(testSessionId);
expect(result.status).toBe('disconnected');
testSessionId = ''; // Mark as cleaned up
});
it('should fail for non-existent session', async () => {
const result = await handleDisconnect({
session_id: 'non-existent-session-id',
});
expect(result.success).toBe(false);
expect(result.error).toBe('Session not found');
});
it('should support disconnect options', async () => {
const result = await handleDisconnect({
session_id: testSessionId,
options: {
force: true,
killProcesses: true,
timeout: 5,
},
});
expect(result.success).toBe(true);
testSessionId = '';
});
it('should clean up resources on disconnect', async () => {
const initialCount = getActiveSessionCount();
const result = await handleDisconnect({
session_id: testSessionId,
});
expect(result.success).toBe(true);
const finalCount = getActiveSessionCount();
expect(finalCount).toBeLessThan(initialCount);
testSessionId = '';
});
});
describe('Session Management', () => {
it('should track active sessions', async () => {
const initialCount = getActiveSessionCount();
const result = await handleConnect({
server_id: 'test-server-123',
});
testSessionId = result.sessionId!;
const newCount = getActiveSessionCount();
expect(newCount).toBeGreaterThan(initialCount);
});
it('should provide session info', async () => {
const result = await handleConnect({
server_id: 'test-server-123',
});
testSessionId = result.sessionId!;
const sessionInfo = getSessionInfo(testSessionId);
expect(sessionInfo).toBeDefined();
expect(sessionInfo?.sessionId).toBe(testSessionId);
expect(sessionInfo?.serverId).toBe('test-server-123');
});
it('should clean up expired sessions', async () => {
const result = await handleConnect({
server_id: 'test-server-123',
});
testSessionId = result.sessionId!;
// Clean up sessions older than 0ms (all sessions)
const cleanedCount = cleanupExpiredSessions(0);
expect(cleanedCount).toBeGreaterThanOrEqual(1);
expect(getSessionInfo(testSessionId)).toBeUndefined();
testSessionId = ''; // Mark as cleaned up
});
it('should not clean up recent sessions', async () => {
const result = await handleConnect({
server_id: 'test-server-123',
});
testSessionId = result.sessionId!;
// Clean up sessions older than 1 hour
const cleanedCount = cleanupExpiredSessions(3600000);
expect(cleanedCount).toBe(0);
expect(getSessionInfo(testSessionId)).toBeDefined();
});
});
describe('Error Handling', () => {
it('should handle connect errors gracefully', async () => {
// Test with invalid server ID format
const result = await handleConnect({
server_id: '',
});
// Should still return a response (may succeed or fail depending on implementation)
expect(result).toBeDefined();
expect(result.success).toBeDefined();
});
it('should handle execute errors gracefully', async () => {
const result = await handleExecute({
server_id: 'test-server-123',
command: '',
});
expect(result).toBeDefined();
expect(result.success).toBeDefined();
});
it('should handle getOutput errors gracefully', async () => {
const result = await handleGetOutput({
session_id: 'invalid-session',
});
expect(result.success).toBe(false);
expect(result.error).toBeDefined();
});
it('should handle disconnect errors gracefully', async () => {
const result = await handleDisconnect({
session_id: 'invalid-session',
});
expect(result.success).toBe(false);
expect(result.error).toBeDefined();
});
});
describe('Integration Flow', () => {
it('should support complete terminal workflow', async () => {
// 1. Connect
const connectResult = await handleConnect({
server_id: 'test-server-123',
options: {
shell: 'bash',
},
});
expect(connectResult.success).toBe(true);
testSessionId = connectResult.sessionId!;
// 2. Execute command
const executeResult = await handleExecute({
server_id: 'test-server-123',
command: 'echo "Hello World"',
session_id: testSessionId,
});
expect(executeResult.success).toBe(true);
// 3. Get output
const outputResult = await handleGetOutput({
session_id: testSessionId,
});
expect(outputResult.success).toBe(true);
// 4. Disconnect
const disconnectResult = await handleDisconnect({
session_id: testSessionId,
});
expect(disconnectResult.success).toBe(true);
testSessionId = '';
});
it('should support multiple concurrent sessions', async () => {
const sessions: string[] = [];
try {
// Create multiple sessions
for (let i = 0; i < 3; i++) {
const result = await handleConnect({
server_id: `test-server-${i}`,
});
expect(result.success).toBe(true);
sessions.push(result.sessionId!);
}
// Verify all sessions are active
expect(sessions).toHaveLength(3);
// Execute commands on all sessions
for (const sessionId of sessions) {
const result = await handleExecute({
server_id: sessionId.split('_')[1],
command: 'ls',
session_id: sessionId,
});
expect(result.success).toBe(true);
}
} finally {
// Clean up all sessions
for (const sessionId of sessions) {
await handleDisconnect({ session_id: sessionId });
}
}
});
});
});