#!/usr/bin/env node /** * Approval Flow Test Suite * * Tests the fix for AI-conversational approval flow where clicking "Approve" * now sends the response as a WebSocket command message instead of an * approval-response message. */ const fs = require('fs'); const path = require('path'); // ANSI color codes const colors = { reset: '\x1b[0m', bright: '\x1b[1m', red: '\x1b[31m', green: '\x1b[32m', yellow: '\x1b[33m', blue: '\x1b[34m', cyan: '\x1b[36m', }; let testResults = { passed: 0, failed: 0, warnings: 0, tests: [] }; function log(message, color = 'reset') { console.log(`${colors[color]}${message}${colors.reset}`); } function test(name, fn) { try { fn(); testResults.passed++; testResults.tests.push({ name, status: 'PASS' }); log(`✓ ${name}`, 'green'); } catch (error) { testResults.failed++; testResults.tests.push({ name, status: 'FAIL', error: error.message }); log(`✗ ${name}`, 'red'); log(` ${error.message}`, 'red'); } } function warn(message) { testResults.warnings++; log(`⚠ ${message}`, 'yellow'); } function assert(condition, message) { if (!condition) { throw new Error(message || 'Assertion failed'); } } function assertContains(haystack, needle, message) { if (!haystack.includes(needle)) { throw new Error(message || `Expected "${haystack}" to contain "${needle}"`); } } // ============================================================================= // Test Suite 1: Frontend Syntax and Structure // ============================================================================= log('\n=== Test Suite 1: Frontend Syntax and Structure ===\n', 'cyan'); test('approval-card.js: File exists', () => { const filePath = path.join(__dirname, 'public/claude-ide/components/approval-card.js'); assert(fs.existsSync(filePath), 'approval-card.js file does not exist'); }); test('approval-card.js: Valid JavaScript syntax', () => { const filePath = path.join(__dirname, 'public/claude-ide/components/approval-card.js'); const content = fs.readFileSync(filePath, 'utf-8'); // Check for syntax errors by attempting to parse try { // Basic syntax checks assert(!content.includes('if (typeof appendSystemMessage === \'function\') && approved)'), 'Found incorrect syntax with extra closing parenthesis on line 222'); assert(content.includes('if (typeof appendSystemMessage === \'function\' && approved)'), 'Missing corrected syntax on line 222'); } catch (error) { throw new Error(`Syntax check failed: ${error.message}`); } }); test('approval-card.js: Exports ApprovalCard to window', () => { const filePath = path.join(__dirname, 'public/claude-ide/components/approval-card.js'); const content = fs.readFileSync(filePath, 'utf-8'); assertContains(content, 'window.ApprovalCard = {', 'ApprovalCard not exported to window object'); }); test('approval-card.js: Has required methods', () => { const filePath = path.join(__dirname, 'public/claude-ide/components/approval-card.js'); const content = fs.readFileSync(filePath, 'utf-8'); const requiredMethods = [ 'handleApprove', 'handleReject', 'handleCustom', 'executeCustom', 'sendApprovalResponse', 'handleExpired' ]; requiredMethods.forEach(method => { assertContains(content, method, `Missing required method: ${method}`); }); }); // ============================================================================= // Test Suite 2: Approval Response Implementation // ============================================================================= log('\n=== Test Suite 2: Approval Response Implementation ===\n', 'cyan'); test('sendApprovalResponse: Checks for AI-conversational approval', () => { const filePath = path.join(__dirname, 'public/claude-ide/components/approval-card.js'); const content = fs.readFileSync(filePath, 'utf-8'); assertContains(content, 'const pendingApproval = window._pendingApprovals && window._pendingApprovals[approvalId]', 'Missing check for AI-conversational approval in window._pendingApprovals'); }); test('sendApprovalResponse: Sends as WebSocket command for AI approvals', () => { const filePath = path.join(__dirname, 'public/claude-ide/components/approval-card.js'); const content = fs.readFileSync(filePath, 'utf-8'); assertContains(content, "type: 'command'", 'AI-conversational approval should be sent as type: "command"'); }); test('sendApprovalResponse: Includes isApprovalResponse metadata', () => { const filePath = path.join(__dirname, 'public/claude-ide/components/approval-card.js'); const content = fs.readFileSync(filePath, 'utf-8'); assertContains(content, 'isApprovalResponse: true', 'Missing isApprovalResponse metadata flag'); }); test('sendApprovalResponse: Sends "yes" for approve', () => { const filePath = path.join(__dirname, 'public/claude-ide/components/approval-card.js'); const content = fs.readFileSync(filePath, 'utf-8'); // Check that approved sends "yes" assertContains(content, "responseMessage = 'yes'", 'Approved response should send "yes" message'); }); test('sendApprovalResponse: Sends "no" for reject', () => { const filePath = path.join(__dirname, 'public/claude-ide/components/approval-card.js'); const content = fs.readFileSync(filePath, 'utf-8'); assertContains(content, "responseMessage = 'no'", 'Rejected response should send "no" message'); }); test('sendApprovalResponse: Supports custom commands', () => { const filePath = path.join(__dirname, 'public/claude-ide/components/approval-card.js'); const content = fs.readFileSync(filePath, 'utf-8'); assertContains(content, 'if (customCommand)', 'Missing custom command handling'); assertContains(content, 'responseMessage = customCommand', 'Custom command not used as response message'); }); test('sendApprovalResponse: Cleans up pending approval', () => { const filePath = path.join(__dirname, 'public/claude-ide/components/approval-card.js'); const content = fs.readFileSync(filePath, 'utf-8'); assertContains(content, 'delete window._pendingApprovals[approvalId]', 'Pending approval not cleaned up after response'); }); test('sendApprovalResponse: Shows feedback on approval', () => { const filePath = path.join(__dirname, 'public/claude-ide/components/approval-card.js'); const content = fs.readFileSync(filePath, 'utf-8'); assertContains(content, "appendSystemMessage('✅ Approval sent - continuing execution...')", 'Missing success feedback message'); }); // ============================================================================= // Test Suite 3: Server-Side Command Handling // ============================================================================= log('\n=== Test Suite 3: Server-Side Command Handling ===\n', 'cyan'); test('server.js: File exists', () => { const filePath = path.join(__dirname, 'server.js'); assert(fs.existsSync(filePath), 'server.js file does not exist'); }); test('server.js: Handles WebSocket command messages', () => { const filePath = path.join(__dirname, 'server.js'); const content = fs.readFileSync(filePath, 'utf-8'); assertContains(content, "if (data.type === 'command')", 'Server missing WebSocket command type handler'); }); test('server.js: Extracts sessionId and command from message', () => { const filePath = path.join(__dirname, 'server.js'); const content = fs.readFileSync(filePath, 'utf-8'); assertContains(content, "const { sessionId, command } = data", 'Server not extracting sessionId and command from WebSocket message'); }); test('server.js: Sends command to Claude service', () => { const filePath = path.join(__dirname, 'server.js'); const content = fs.readFileSync(filePath, 'utf-8'); assertContains(content, 'claudeService.sendCommand(sessionId, command)', 'Server not sending command to Claude service'); }); test('server.js: Has PendingApprovalsManager', () => { const filePath = path.join(__dirname, 'server.js'); const content = fs.readFileSync(filePath, 'utf-8'); assertContains(content, 'class PendingApprovalsManager', 'Server missing PendingApprovalsManager class'); assertContains(content, 'createApproval(sessionId, command, explanation)', 'PendingApprovalsManager missing createApproval method'); }); // ============================================================================= // Test Suite 4: HTML Integration // ============================================================================= log('\n=== Test Suite 4: HTML Integration ===\n', 'cyan'); test('index.html: Loads approval-card.js', () => { const filePath = path.join(__dirname, 'public/claude-ide/index.html'); const content = fs.readFileSync(filePath, 'utf-8'); assertContains(content, 'approval-card.js', 'index.html does not load approval-card.js'); }); test('index.html: Loads approval-card.css', () => { const filePath = path.join(__dirname, 'public/claude-ide/index.html'); const content = fs.readFileSync(filePath, 'utf-8'); assertContains(content, 'approval-card.css', 'index.html does not load approval-card.css'); }); // ============================================================================= // Test Suite 5: IDE Integration // ============================================================================= log('\n=== Test Suite 5: IDE Integration ===\n', 'cyan'); test('ide.js: Creates window._pendingApprovals', () => { const filePath = path.join(__dirname, 'public/claude-ide/ide.js'); const content = fs.readFileSync(filePath, 'utf-8'); assertContains(content, 'window._pendingApprovals = window._pendingApprovals || {}', 'ide.js does not create window._pendingApprovals'); }); test('ide.js: Stores approval data with sessionId', () => { const filePath = path.join(__dirname, 'public/claude-ide/ide.js'); const content = fs.readFileSync(filePath, 'utf-8'); assertContains(content, 'sessionId: data.sessionId', 'ide.js does not store sessionId in pending approval'); }); test('ide.js: Stores original command', () => { const filePath = path.join(__dirname, 'public/claude-ide/ide.js'); const content = fs.readFileSync(filePath, 'utf-8'); assertContains(content, 'command: approvalRequest.command', 'ide.js does not store original command in pending approval'); }); // ============================================================================= // Test Suite 6: Edge Cases // ============================================================================= log('\n=== Test Suite 6: Edge Cases and Error Handling ===\n', 'cyan'); test('approval-card.js: Checks WebSocket connection before sending', () => { const filePath = path.join(__dirname, 'public/claude-ide/components/approval-card.js'); const content = fs.readFileSync(filePath, 'utf-8'); assertContains(content, "window.ws.readyState === WebSocket.OPEN", 'Missing WebSocket connection check before sending'); }); test('approval-card.js: Validates custom command input', () => { const filePath = path.join(__dirname, 'public/claude-ide/components/approval-card.js'); const content = fs.readFileSync(filePath, 'utf-8'); assertContains(content, 'if (!customCommand)', 'Missing validation for empty custom command'); }); test('approval-card.js: Handles expired approvals', () => { const filePath = path.join(__dirname, 'public/claude-ide/components/approval-card.js'); const content = fs.readFileSync(filePath, 'utf-8'); assertContains(content, 'function handleExpired', 'Missing handleExpired function'); }); test('approval-card.js: Prevents XSS with escapeHtml', () => { const filePath = path.join(__dirname, 'public/claude-ide/components/approval-card.js'); const content = fs.readFileSync(filePath, 'utf-8'); assertContains(content, 'function escapeHtml', 'Missing escapeHtml function for XSS prevention'); assertContains(content, 'escapeHtml(approvalData.command)', 'Command not escaped before rendering'); }); // ============================================================================= // Summary // ============================================================================= log('\n=== Test Summary ===\n', 'cyan'); const totalTests = testResults.tests.length; const passRate = ((testResults.passed / totalTests) * 100).toFixed(1); log(`Total Tests: ${totalTests}`, 'bright'); log(`Passed: ${testResults.passed}`, 'green'); log(`Failed: ${testResults.failed}`, testResults.failed > 0 ? 'red' : 'green'); log(`Warnings: ${testResults.warnings}`, 'yellow'); log(`Pass Rate: ${passRate}%`, passRate === '100.0' ? 'green' : 'yellow'); if (testResults.failed > 0) { log('\n=== Failed Tests ===\n', 'red'); testResults.tests .filter(t => t.status === 'FAIL') .forEach(t => { log(`✗ ${t.name}`, 'red'); log(` ${t.error}`, 'red'); }); } // Exit with appropriate code process.exit(testResults.failed > 0 ? 1 : 0);