Files
SuperCharged-Claude-Code-Up…/test-approval-flow.js
uroma 55aafbae9a Fix project isolation: Make loadChatHistory respect active project sessions
- Modified loadChatHistory() to check for active project before fetching all sessions
- When active project exists, use project.sessions instead of fetching from API
- Added detailed console logging to debug session filtering
- This prevents ALL sessions from appearing in every project's sidebar

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-22 14:43:05 +00:00

351 lines
13 KiB
JavaScript
Executable File

#!/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);