Skip to main content
Glama
story5-terminal-state-synchronization-unit.test.ts15.8 kB
/** * Story 5: Terminal State Synchronization - Unit Tests * * These tests validate the core terminal state synchronization functionality * following strict TDD practices. All tests should initially fail until * the implementation is complete. */ import { WebServerManager } from '../src/web-server-manager.js'; import { SSHConnectionManager } from '../src/ssh-connection-manager.js'; import WebSocket from 'ws'; describe('Story 5: Terminal State Synchronization - Unit Tests', () => { let webServerManager: WebServerManager; let sshManager: SSHConnectionManager; let testSessionName: string; beforeEach(() => { sshManager = new SSHConnectionManager(); webServerManager = new WebServerManager(sshManager); testSessionName = 'test-session-sync'; }); afterEach(async () => { if (webServerManager) { await webServerManager.stop(); } if (sshManager) { await sshManager.cleanup(); } }); // AC5.1: Source-Based Terminal Unlocking describe('AC5.1: Source-Based Terminal Unlocking', () => { test('should track terminal lock state per user command', async () => { // This test should fail initially - we need to implement source-based locking await webServerManager.start(); const port = await webServerManager.getPort(); // Create SSH session for testing await sshManager.createConnection({ name: testSessionName, host: 'localhost', username: 'testuser', password: 'testpass' }); // Connect WebSocket client const ws = new WebSocket(`ws://localhost:${port}/ws/session/${testSessionName}`); await new Promise(resolve => ws.on('open', resolve)); let lockState: any = null; let unlockReceived = false; ws.on('message', (data: any) => { const message = JSON.parse(data.toString()); if (message.type === 'terminal_lock_state') { lockState = message; if (!message.isLocked) { unlockReceived = true; } } }); // Send user-initiated command const userCommand = { type: 'terminal_input', sessionName: testSessionName, command: 'echo "user test"', commandId: 'user-cmd-1', source: 'user' }; ws.send(JSON.stringify(userCommand)); // Wait for lock state updates await new Promise(resolve => setTimeout(resolve, 100)); // Should have received lock state message indicating terminal is locked for user command expect(lockState).toBeTruthy(); expect(lockState.isLocked).toBe(true); expect(lockState.commandId).toBe('user-cmd-1'); expect(lockState.source).toBe('user'); ws.close(); // This test will fail initially because source-based locking isn't implemented expect(unlockReceived).toBe(false); // Should stay locked until user command completes }); test('should not lock terminal for Claude Code commands', async () => { // This test should fail initially - Claude Code commands shouldn't affect lock state await webServerManager.start(); const port = await webServerManager.getPort(); await sshManager.createConnection({ name: testSessionName, host: 'localhost', username: 'testuser', password: 'testpass' }); const ws = new WebSocket(`ws://localhost:${port}/ws/session/${testSessionName}`); await new Promise(resolve => ws.on('open', resolve)); let lockStateReceived = false; ws.on('message', (data: any) => { const message = JSON.parse(data.toString()); if (message.type === 'terminal_lock_state') { lockStateReceived = true; } }); // Simulate Claude Code command execution await sshManager.executeCommand(testSessionName, 'echo "claude code test"', { source: 'claude' }); // Wait to see if any lock state changes occurred await new Promise(resolve => setTimeout(resolve, 100)); // Terminal should not lock for Claude Code commands expect(lockStateReceived).toBe(false); ws.close(); }); test('should unlock terminal only when user-initiated command completes', async () => { // This test should fail initially - we need proper completion detection await webServerManager.start(); const port = await webServerManager.getPort(); await sshManager.createConnection({ name: testSessionName, host: 'localhost', username: 'testuser', password: 'testpass' }); const ws = new WebSocket(`ws://localhost:${port}/ws/session/${testSessionName}`); await new Promise(resolve => ws.on('open', resolve)); let finalLockState: any = null; const lockStates: any[] = []; ws.on('message', (data: any) => { const message = JSON.parse(data.toString()); if (message.type === 'terminal_lock_state') { lockStates.push(message); finalLockState = message; } }); // Send user command that should complete const userCommand = { type: 'terminal_input', sessionName: testSessionName, command: 'echo "test" && sleep 0.1', commandId: 'user-cmd-complete', source: 'user' }; ws.send(JSON.stringify(userCommand)); // Wait for command completion await new Promise(resolve => setTimeout(resolve, 200)); // Should have received lock/unlock sequence expect(lockStates.length).toBeGreaterThanOrEqual(2); expect(lockStates[0].isLocked).toBe(true); expect(finalLockState.isLocked).toBe(false); expect(finalLockState.source).toBe('user'); ws.close(); }); }); // AC5.2: Visual State Indicators describe('AC5.2: Visual State Indicators', () => { test('should provide different visual indicators for user vs Claude Code execution', async () => { // This test should fail initially - visual indicators not implemented await webServerManager.start(); const port = await webServerManager.getPort(); await sshManager.createConnection({ name: testSessionName, host: 'localhost', username: 'testuser', password: 'testpass' }); const ws = new WebSocket(`ws://localhost:${port}/ws/session/${testSessionName}`); await new Promise(resolve => ws.on('open', resolve)); let visualIndicators: any[] = []; ws.on('message', (data: any) => { const message = JSON.parse(data.toString()); if (message.type === 'visual_state_indicator') { visualIndicators.push(message); } }); // Send user command const userCommand = { type: 'terminal_input', sessionName: testSessionName, command: 'echo "user"', commandId: 'visual-test-user', source: 'user' }; ws.send(JSON.stringify(userCommand)); // Execute Claude Code command await sshManager.executeCommand(testSessionName, 'echo "claude"', { source: 'claude' }); await new Promise(resolve => setTimeout(resolve, 150)); // Should have received different visual indicators const userIndicators = visualIndicators.filter(v => v.source === 'user'); const claudeIndicators = visualIndicators.filter(v => v.source === 'claude'); expect(userIndicators.length).toBeGreaterThan(0); expect(claudeIndicators.length).toBeGreaterThan(0); expect(userIndicators[0].indicatorType).not.toBe(claudeIndicators[0].indicatorType); ws.close(); }); test('should show clear processing state during command execution', async () => { // This test should fail initially - processing indicators not implemented await webServerManager.start(); const port = await webServerManager.getPort(); await sshManager.createConnection({ name: testSessionName, host: 'localhost', username: 'testuser', password: 'testpass' }); const ws = new WebSocket(`ws://localhost:${port}/ws/session/${testSessionName}`); await new Promise(resolve => ws.on('open', resolve)); let processingStates: any[] = []; ws.on('message', (data: any) => { const message = JSON.parse(data.toString()); if (message.type === 'processing_state') { processingStates.push(message); } }); // Send command that takes some time const command = { type: 'terminal_input', sessionName: testSessionName, command: 'sleep 0.2 && echo "done"', commandId: 'processing-test', source: 'user' }; ws.send(JSON.stringify(command)); await new Promise(resolve => setTimeout(resolve, 300)); // Should show processing states: started -> executing -> completed expect(processingStates.length).toBeGreaterThanOrEqual(2); expect(processingStates[0].state).toBe('executing'); expect(processingStates[processingStates.length - 1].state).toBe('completed'); ws.close(); }); }); // AC5.3: Multiple Client Synchronization describe('AC5.3: Multiple Client Synchronization', () => { test('should synchronize terminal state across multiple browser clients', async () => { // This test should fail initially - multi-client sync not implemented await webServerManager.start(); const port = await webServerManager.getPort(); await sshManager.createConnection({ name: testSessionName, host: 'localhost', username: 'testuser', password: 'testpass' }); // Connect two WebSocket clients const ws1 = new WebSocket(`ws://localhost:${port}/ws/session/${testSessionName}`); const ws2 = new WebSocket(`ws://localhost:${port}/ws/session/${testSessionName}`); await Promise.all([ new Promise(resolve => ws1.on('open', resolve)), new Promise(resolve => ws2.on('open', resolve)) ]); let client1States: any[] = []; let client2States: any[] = []; ws1.on('message', (data: any) => { const message = JSON.parse(data.toString()); if (message.type === 'terminal_lock_state') { client1States.push(message); } }); ws2.on('message', (data: any) => { const message = JSON.parse(data.toString()); if (message.type === 'terminal_lock_state') { client2States.push(message); } }); // Send command from first client const command = { type: 'terminal_input', sessionName: testSessionName, command: 'echo "sync test"', commandId: 'sync-cmd-1', source: 'user' }; ws1.send(JSON.stringify(command)); await new Promise(resolve => setTimeout(resolve, 150)); // Both clients should receive the same state updates expect(client1States.length).toBeGreaterThan(0); expect(client2States.length).toBeGreaterThan(0); expect(client1States.length).toBe(client2States.length); // States should be identical for (let i = 0; i < client1States.length; i++) { expect(client1States[i].isLocked).toBe(client2States[i].isLocked); expect(client1States[i].commandId).toBe(client2States[i].commandId); } ws1.close(); ws2.close(); }); test('should show same terminal output to all connected clients', async () => { // This test should fail initially - synchronized output not guaranteed await webServerManager.start(); const port = await webServerManager.getPort(); await sshManager.createConnection({ name: testSessionName, host: 'localhost', username: 'testuser', password: 'testpass' }); const ws1 = new WebSocket(`ws://localhost:${port}/ws/session/${testSessionName}`); const ws2 = new WebSocket(`ws://localhost:${port}/ws/session/${testSessionName}`); const ws3 = new WebSocket(`ws://localhost:${port}/ws/session/${testSessionName}`); await Promise.all([ new Promise(resolve => ws1.on('open', resolve)), new Promise(resolve => ws2.on('open', resolve)), new Promise(resolve => ws3.on('open', resolve)) ]); let client1Output: any[] = []; let client2Output: any[] = []; let client3Output: any[] = []; ws1.on('message', (data: any) => { const message = JSON.parse(data.toString()); if (message.type === 'terminal_output') { client1Output.push(message.data); } }); ws2.on('message', (data: any) => { const message = JSON.parse(data.toString()); if (message.type === 'terminal_output') { client2Output.push(message.data); } }); ws3.on('message', (data: any) => { const message = JSON.parse(data.toString()); if (message.type === 'terminal_output') { client3Output.push(message.data); } }); // Execute command await sshManager.executeCommand(testSessionName, 'echo "multi-client test"', { source: 'user' }); await new Promise(resolve => setTimeout(resolve, 100)); // All clients should receive identical output expect(client1Output.length).toBeGreaterThan(0); expect(client2Output.length).toBe(client1Output.length); expect(client3Output.length).toBe(client1Output.length); for (let i = 0; i < client1Output.length; i++) { expect(client1Output[i]).toBe(client2Output[i]); expect(client1Output[i]).toBe(client3Output[i]); } ws1.close(); ws2.close(); ws3.close(); }); test('should unlock all clients simultaneously when command completes', async () => { // This test should fail initially - synchronized unlocking not implemented await webServerManager.start(); const port = await webServerManager.getPort(); await sshManager.createConnection({ name: testSessionName, host: 'localhost', username: 'testuser', password: 'testpass' }); const ws1 = new WebSocket(`ws://localhost:${port}/ws/session/${testSessionName}`); const ws2 = new WebSocket(`ws://localhost:${port}/ws/session/${testSessionName}`); await Promise.all([ new Promise(resolve => ws1.on('open', resolve)), new Promise(resolve => ws2.on('open', resolve)) ]); let client1UnlockTime: number | null = null; let client2UnlockTime: number | null = null; ws1.on('message', (data: any) => { const message = JSON.parse(data.toString()); if (message.type === 'terminal_lock_state' && !message.isLocked) { client1UnlockTime = Date.now(); } }); ws2.on('message', (data: any) => { const message = JSON.parse(data.toString()); if (message.type === 'terminal_lock_state' && !message.isLocked) { client2UnlockTime = Date.now(); } }); // Send command from client 1 const command = { type: 'terminal_input', sessionName: testSessionName, command: 'echo "unlock test"', commandId: 'unlock-cmd-1', source: 'user' }; ws1.send(JSON.stringify(command)); await new Promise(resolve => setTimeout(resolve, 150)); // Both clients should unlock at nearly the same time expect(client1UnlockTime).not.toBeNull(); expect(client2UnlockTime).not.toBeNull(); expect(Math.abs(client1UnlockTime! - client2UnlockTime!)).toBeLessThan(50); // Within 50ms ws1.close(); ws2.close(); }); }); });

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/LightspeedDMS/ssh-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server