- 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>
351 lines
13 KiB
JavaScript
Executable File
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);
|