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>
This commit is contained in:
591
test-hybrid-approach.js
Executable file
591
test-hybrid-approach.js
Executable file
@@ -0,0 +1,591 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Comprehensive QA Test Suite for Hybrid Approach Implementation
|
||||
*
|
||||
* Tests the session attachment race condition fix using:
|
||||
* - Route-based URLs (/claude/ide/session/:sessionId)
|
||||
* - REST API for commands (POST /api/session/:sessionId/prompt)
|
||||
* - SSE for responses (GET /api/session/:sessionId/events)
|
||||
* - EventBus for event coordination
|
||||
*/
|
||||
|
||||
const http = require('http');
|
||||
const https = require('https');
|
||||
const { URL } = require('url');
|
||||
|
||||
// Configuration
|
||||
const BASE_URL = 'http://localhost:3010';
|
||||
const AUTH_COOKIE = 'connect.sid=s%3AWAhScwZ3V-yPWqFv7tFYOgNJtXh3UUNu.pXQ3v0%2Fh1K2JqxHCwxDEZ8VaNw7NbdhGu1OyKJvYq5QY'; // Update with valid session
|
||||
const TEST_SESSION_ID = 'session-1769029589191-gjqeg0i0i';
|
||||
|
||||
// Colors for terminal output
|
||||
const colors = {
|
||||
reset: '\x1b[0m',
|
||||
green: '\x1b[32m',
|
||||
red: '\x1b[31m',
|
||||
yellow: '\x1b[33m',
|
||||
blue: '\x1b[34m',
|
||||
cyan: '\x1b[36m',
|
||||
magenta: '\x1b[35m'
|
||||
};
|
||||
|
||||
// Test results tracking
|
||||
const results = {
|
||||
passed: 0,
|
||||
failed: 0,
|
||||
skipped: 0,
|
||||
tests: []
|
||||
};
|
||||
|
||||
/**
|
||||
* Make HTTP request with proper error handling
|
||||
*/
|
||||
function request(options) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const url = new URL(options.url || BASE_URL + options.path);
|
||||
|
||||
const reqOptions = {
|
||||
hostname: url.hostname,
|
||||
port: url.port || 3010,
|
||||
path: url.pathname + url.search,
|
||||
method: options.method || 'GET',
|
||||
headers: {
|
||||
'Cookie': AUTH_COOKIE,
|
||||
...(options.headers || {})
|
||||
}
|
||||
};
|
||||
|
||||
if (options.body) {
|
||||
reqOptions.headers['Content-Type'] = 'application/json';
|
||||
reqOptions.headers['Content-Length'] = Buffer.byteLength(JSON.stringify(options.body));
|
||||
}
|
||||
|
||||
const protocol = url.protocol === 'https:' ? https : http;
|
||||
const req = protocol.request(reqOptions, (res) => {
|
||||
let data = '';
|
||||
|
||||
res.on('data', chunk => {
|
||||
data += chunk;
|
||||
});
|
||||
|
||||
res.on('end', () => {
|
||||
try {
|
||||
const body = data ? JSON.parse(data) : null;
|
||||
resolve({
|
||||
statusCode: res.statusCode,
|
||||
headers: res.headers,
|
||||
body
|
||||
});
|
||||
} catch (e) {
|
||||
resolve({
|
||||
statusCode: res.statusCode,
|
||||
headers: res.headers,
|
||||
body: data
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
req.on('error', reject);
|
||||
|
||||
if (options.body) {
|
||||
req.write(JSON.stringify(options.body));
|
||||
}
|
||||
|
||||
req.end();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Log test result
|
||||
*/
|
||||
function logTest(name, status, message, details = null) {
|
||||
const timestamp = new Date().toISOString();
|
||||
const result = { name, status, message, details, timestamp };
|
||||
|
||||
results.tests.push(result);
|
||||
|
||||
let color;
|
||||
let symbol;
|
||||
|
||||
switch (status) {
|
||||
case 'PASS':
|
||||
color = colors.green;
|
||||
symbol = '✓';
|
||||
results.passed++;
|
||||
break;
|
||||
case 'FAIL':
|
||||
color = colors.red;
|
||||
symbol = '✗';
|
||||
results.failed++;
|
||||
break;
|
||||
case 'SKIP':
|
||||
color = colors.yellow;
|
||||
symbol = '⊘';
|
||||
results.skipped++;
|
||||
break;
|
||||
default:
|
||||
color = colors.blue;
|
||||
symbol = '○';
|
||||
}
|
||||
|
||||
console.log(`${color}${symbol} [${status}]${colors.reset} ${name}`);
|
||||
if (message) {
|
||||
console.log(` ${colors.cyan}→${colors.reset} ${message}`);
|
||||
}
|
||||
if (details) {
|
||||
console.log(` ${colors.gray}${JSON.stringify(details, null, 2)}${colors.reset}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test Suite 1: URL Routing
|
||||
*/
|
||||
async function testURLRouting() {
|
||||
console.log(`\n${colors.magenta}═══════════════════════════════════════════════════${colors.reset}`);
|
||||
console.log(`${colors.magenta}TEST SUITE 1: URL Routing${colors.reset}`);
|
||||
console.log(`${colors.magenta}═══════════════════════════════════════════════════${colors.reset}\n`);
|
||||
|
||||
// Test 1.1: Visit /claude/ide - should show landing page
|
||||
try {
|
||||
const response = await request({
|
||||
path: '/claude/ide',
|
||||
method: 'GET'
|
||||
});
|
||||
|
||||
if (response.statusCode === 200 && response.body.includes('claude-ide')) {
|
||||
logTest('1.1 Landing Page', 'PASS', 'Landing page loads successfully');
|
||||
} else if (response.statusCode === 302 || response.statusCode === 301) {
|
||||
logTest('1.1 Landing Page', 'PASS', 'Landing page redirects', { location: response.headers.location });
|
||||
} else {
|
||||
logTest('1.1 Landing Page', 'FAIL', 'Unexpected status code', { statusCode: response.statusCode });
|
||||
}
|
||||
} catch (error) {
|
||||
logTest('1.1 Landing Page', 'FAIL', error.message);
|
||||
}
|
||||
|
||||
// Test 1.2: Visit /claude/ide?session=XXX - should redirect to route-based URL
|
||||
try {
|
||||
const response = await request({
|
||||
path: '/claude/ide?session=test-session-123',
|
||||
method: 'GET',
|
||||
redirect: 'manual' // Don't follow redirects automatically
|
||||
});
|
||||
|
||||
if (response.statusCode === 302 || response.statusCode === 301) {
|
||||
const redirectUrl = response.headers.location;
|
||||
if (redirectUrl.includes('/claude/ide/session/')) {
|
||||
logTest('1.2 Query Param Redirect', 'PASS', 'Redirects to route-based URL', { redirectUrl });
|
||||
} else {
|
||||
logTest('1.2 Query Param Redirect', 'FAIL', 'Redirect URL incorrect', { redirectUrl });
|
||||
}
|
||||
} else {
|
||||
logTest('1.2 Query Param Redirect', 'FAIL', 'No redirect occurred', { statusCode: response.statusCode });
|
||||
}
|
||||
} catch (error) {
|
||||
logTest('1.2 Query Param Redirect', 'FAIL', error.message);
|
||||
}
|
||||
|
||||
// Test 1.3: Visit /claude/ide/session/session-XXX - should load session directly
|
||||
try {
|
||||
const response = await request({
|
||||
path: `/claude/ide/session/${TEST_SESSION_ID}`,
|
||||
method: 'GET'
|
||||
});
|
||||
|
||||
if (response.statusCode === 200) {
|
||||
logTest('1.3 Route-based Session URL', 'PASS', 'Session page loads successfully', {
|
||||
sessionId: TEST_SESSION_ID
|
||||
});
|
||||
} else if (response.statusCode === 404) {
|
||||
logTest('1.3 Route-based Session URL', 'SKIP', 'Session not found (may not exist yet)', {
|
||||
sessionId: TEST_SESSION_ID
|
||||
});
|
||||
} else {
|
||||
logTest('1.3 Route-based Session URL', 'FAIL', 'Unexpected status code', { statusCode: response.statusCode });
|
||||
}
|
||||
} catch (error) {
|
||||
logTest('1.3 Route-based Session URL', 'FAIL', error.message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test Suite 2: Session API Endpoints
|
||||
*/
|
||||
async function testSessionAPI() {
|
||||
console.log(`\n${colors.magenta}═══════════════════════════════════════════════════${colors.reset}`);
|
||||
console.log(`${colors.magenta}TEST SUITE 2: Session API Endpoints${colors.reset}`);
|
||||
console.log(`${colors.magenta}═══════════════════════════════════════════════════${colors.reset}\n`);
|
||||
|
||||
// Test 2.1: GET /api/session/:sessionId/status
|
||||
try {
|
||||
const response = await request({
|
||||
path: `/api/session/${TEST_SESSION_ID}/status`,
|
||||
method: 'GET'
|
||||
});
|
||||
|
||||
if (response.statusCode === 200 && response.body) {
|
||||
logTest('2.1 Session Status', 'PASS', 'Session status retrieved', {
|
||||
sessionId: response.body.sessionId,
|
||||
status: response.body.status
|
||||
});
|
||||
} else if (response.statusCode === 404) {
|
||||
logTest('2.1 Session Status', 'SKIP', 'Session not found', { sessionId: TEST_SESSION_ID });
|
||||
} else {
|
||||
logTest('2.1 Session Status', 'FAIL', 'Failed to get status', {
|
||||
statusCode: response.statusCode,
|
||||
body: response.body
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
logTest('2.1 Session Status', 'FAIL', error.message);
|
||||
}
|
||||
|
||||
// Test 2.2: POST /api/session/:sessionId/prompt
|
||||
try {
|
||||
const response = await request({
|
||||
path: `/api/session/${TEST_SESSION_ID}/prompt`,
|
||||
method: 'POST',
|
||||
body: {
|
||||
command: 'echo "Hello from test"'
|
||||
}
|
||||
});
|
||||
|
||||
if (response.statusCode === 200 && response.body && response.body.success) {
|
||||
logTest('2.2 Send Prompt', 'PASS', 'Command sent successfully', {
|
||||
sessionId: response.body.sessionId,
|
||||
timestamp: response.body.timestamp
|
||||
});
|
||||
} else if (response.statusCode === 404) {
|
||||
logTest('2.2 Send Prompt', 'SKIP', 'Session not found', { sessionId: TEST_SESSION_ID });
|
||||
} else {
|
||||
logTest('2.2 Send Prompt', 'FAIL', 'Failed to send command', {
|
||||
statusCode: response.statusCode,
|
||||
body: response.body
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
logTest('2.2 Send Prompt', 'FAIL', error.message);
|
||||
}
|
||||
|
||||
// Test 2.3: Invalid session ID format
|
||||
try {
|
||||
const response = await request({
|
||||
path: '/api/session/invalid-session-id/status',
|
||||
method: 'GET'
|
||||
});
|
||||
|
||||
if (response.statusCode === 400) {
|
||||
logTest('2.3 Invalid Session ID', 'PASS', 'Properly rejects invalid session ID', {
|
||||
error: response.body?.error
|
||||
});
|
||||
} else {
|
||||
logTest('2.3 Invalid Session ID', 'FAIL', 'Should reject invalid session ID', {
|
||||
statusCode: response.statusCode
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
logTest('2.3 Invalid Session ID', 'FAIL', error.message);
|
||||
}
|
||||
|
||||
// Test 2.4: Empty command
|
||||
try {
|
||||
const response = await request({
|
||||
path: `/api/session/${TEST_SESSION_ID}/prompt`,
|
||||
method: 'POST',
|
||||
body: {
|
||||
command: ''
|
||||
}
|
||||
});
|
||||
|
||||
if (response.statusCode === 400) {
|
||||
logTest('2.4 Empty Command', 'PASS', 'Properly rejects empty command', {
|
||||
error: response.body?.error
|
||||
});
|
||||
} else if (response.statusCode === 404) {
|
||||
logTest('2.4 Empty Command', 'SKIP', 'Session not found');
|
||||
} else {
|
||||
logTest('2.4 Empty Command', 'FAIL', 'Should reject empty command', {
|
||||
statusCode: response.statusCode
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
logTest('2.4 Empty Command', 'FAIL', error.message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test Suite 3: SSE Connection
|
||||
*/
|
||||
async function testSSEConnection() {
|
||||
console.log(`\n${colors.magenta}═══════════════════════════════════════════════════${colors.reset}`);
|
||||
console.log(`${colors.magenta}TEST SUITE 3: SSE Connection${colors.reset}`);
|
||||
console.log(`${colors.magenta}═══════════════════════════════════════════════════${colors.reset}\n`);
|
||||
|
||||
// Test 3.1: SSE connection establishment
|
||||
try {
|
||||
const url = new URL(`${BASE_URL}/api/session/${TEST_SESSION_ID}/events`);
|
||||
const reqOptions = {
|
||||
hostname: url.hostname,
|
||||
port: url.port || 3010,
|
||||
path: url.pathname,
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Cookie': AUTH_COOKIE,
|
||||
'Accept': 'text/event-stream'
|
||||
}
|
||||
};
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
const req = http.request(reqOptions, (res) => {
|
||||
const contentType = res.headers['content-type'];
|
||||
|
||||
// Check if it's an SSE stream
|
||||
if (contentType && contentType.includes('text/event-stream')) {
|
||||
logTest('3.1 SSE Connection', 'PASS', 'SSE connection established', {
|
||||
contentType,
|
||||
cacheControl: res.headers['cache-control']
|
||||
});
|
||||
} else {
|
||||
logTest('3.1 SSE Connection', 'FAIL', 'Wrong content type', { contentType });
|
||||
}
|
||||
|
||||
// Don't wait for events, just check connection
|
||||
res.on('data', () => {});
|
||||
setTimeout(() => {
|
||||
req.destroy();
|
||||
resolve();
|
||||
}, 1000);
|
||||
});
|
||||
|
||||
req.on('error', (error) => {
|
||||
if (error.code === 'ECONNRESET') {
|
||||
// Connection was closed by us, that's OK
|
||||
resolve();
|
||||
} else {
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
|
||||
req.setTimeout(5000, () => {
|
||||
req.destroy();
|
||||
resolve();
|
||||
});
|
||||
|
||||
req.end();
|
||||
});
|
||||
} catch (error) {
|
||||
if (error.message.includes('404') || error.message.includes('Not Found')) {
|
||||
logTest('3.1 SSE Connection', 'SKIP', 'Session not found', { sessionId: TEST_SESSION_ID });
|
||||
} else {
|
||||
logTest('3.1 SSE Connection', 'FAIL', error.message);
|
||||
}
|
||||
}
|
||||
|
||||
// Test 3.2: SSE connection status endpoint
|
||||
try {
|
||||
const response = await request({
|
||||
path: `/api/session/${TEST_SESSION_ID}/events/status`,
|
||||
method: 'GET'
|
||||
});
|
||||
|
||||
if (response.statusCode === 200 && response.body) {
|
||||
logTest('3.2 SSE Status', 'PASS', 'SSE status retrieved', {
|
||||
activeConnections: response.body.activeConnections
|
||||
});
|
||||
} else {
|
||||
logTest('3.2 SSE Status', 'FAIL', 'Failed to get SSE status', {
|
||||
statusCode: response.statusCode
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
logTest('3.2 SSE Status', 'FAIL', error.message);
|
||||
}
|
||||
|
||||
// Test 3.3: SSE global stats
|
||||
try {
|
||||
const response = await request({
|
||||
path: '/api/sse/stats',
|
||||
method: 'GET'
|
||||
});
|
||||
|
||||
if (response.statusCode === 200 && response.body) {
|
||||
logTest('3.3 SSE Global Stats', 'PASS', 'SSE stats retrieved', {
|
||||
totalSessions: response.body.totalSessions,
|
||||
totalConnections: response.body.totalConnections
|
||||
});
|
||||
} else {
|
||||
logTest('3.3 SSE Global Stats', 'FAIL', 'Failed to get SSE stats', {
|
||||
statusCode: response.statusCode
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
logTest('3.3 SSE Global Stats', 'FAIL', error.message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test Suite 4: EventBus Integration
|
||||
*/
|
||||
async function testEventBusIntegration() {
|
||||
console.log(`\n${colors.magenta}═══════════════════════════════════════════════════${colors.reset}`);
|
||||
console.log(`${colors.magenta}TEST SUITE 4: EventBus Integration${colors.reset}`);
|
||||
console.log(`${colors.magenta}═══════════════════════════════════════════════════${colors.reset}\n`);
|
||||
|
||||
// Note: EventBus is server-side, so we can't directly test it
|
||||
// But we can verify events are being logged by checking server logs
|
||||
|
||||
logTest('4.1 EventBus Integration', 'INFO',
|
||||
'EventBus integration verified through server logs',
|
||||
{ note: 'Check server logs for [EventBus] entries' }
|
||||
);
|
||||
|
||||
logTest('4.2 Event Types', 'INFO',
|
||||
'Expected event types: session-output, session-error, command-sent, approval-request',
|
||||
{ note: 'Verify in server logs' }
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test Suite 5: Session ID Validation
|
||||
*/
|
||||
async function testSessionIDValidation() {
|
||||
console.log(`\n${colors.magenta}═══════════════════════════════════════════════════${colors.reset}`);
|
||||
console.log(`${colors.magenta}TEST SUITE 5: Session ID Validation${colors.reset}`);
|
||||
console.log(`${colors.magenta}═══════════════════════════════════════════════════${colors.reset}\n`);
|
||||
|
||||
const testCases = [
|
||||
{ sessionId: 'abc', shouldFail: true, name: 'Too short' },
|
||||
{ sessionId: 'a'.repeat(65), shouldFail: true, name: 'Too long' },
|
||||
{ sessionId: 'session-123', shouldFail: true, name: 'Too short (timestamp format)' },
|
||||
{ sessionId: 'session-1769029589191-gjqeg0i0i', shouldFail: false, name: 'Valid timestamp format' },
|
||||
{ sessionId: 'my-session-12345', shouldFail: false, name: 'Valid custom format' },
|
||||
{ sessionId: 'session@123', shouldFail: true, name: 'Invalid character (@)' },
|
||||
{ sessionId: 'session 123', shouldFail: true, name: 'Invalid character (space)' }
|
||||
];
|
||||
|
||||
for (const testCase of testCases) {
|
||||
try {
|
||||
const response = await request({
|
||||
path: `/api/session/${testCase.sessionId}/status`,
|
||||
method: 'GET'
|
||||
});
|
||||
|
||||
const passed = testCase.shouldFail
|
||||
? (response.statusCode === 400)
|
||||
: (response.statusCode === 200 || response.statusCode === 404);
|
||||
|
||||
if (passed) {
|
||||
logTest(`5.${testCases.indexOf(testCase) + 1} ${testCase.name}`, 'PASS',
|
||||
testCase.shouldFail ? 'Correctly rejected' : 'Correctly accepted',
|
||||
{ sessionId: testCase.sessionId, statusCode: response.statusCode }
|
||||
);
|
||||
} else {
|
||||
logTest(`5.${testCases.indexOf(testCase) + 1} ${testCase.name}`, 'FAIL',
|
||||
testCase.shouldFail ? 'Should reject but accepted' : 'Should accept but rejected',
|
||||
{ sessionId: testCase.sessionId, statusCode: response.statusCode }
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
logTest(`5.${testCases.indexOf(testCase) + 1} ${testCase.name}`, 'FAIL', error.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test Suite 6: Race Condition Prevention
|
||||
*/
|
||||
async function testRaceConditionPrevention() {
|
||||
console.log(`\n${colors.magenta}═══════════════════════════════════════════════════${colors.reset}`);
|
||||
console.log(`${colors.magenta}TEST SUITE 6: Race Condition Prevention${colors.reset}`);
|
||||
console.log(`${colors.magenta}═══════════════════════════════════════════════════${colors.reset}\n`);
|
||||
|
||||
// Test that session ID is available from URL path immediately
|
||||
logTest('6.1 URL Path Context', 'INFO',
|
||||
'Session ID available from URL path on page load',
|
||||
{
|
||||
url: `/claude/ide/session/${TEST_SESSION_ID}`,
|
||||
note: 'No race condition - session ID from URL, not async load'
|
||||
}
|
||||
);
|
||||
|
||||
// Test that REST API is used for commands
|
||||
logTest('6.2 REST API Commands', 'INFO',
|
||||
'Commands sent via POST /api/session/:sessionId/prompt',
|
||||
{
|
||||
method: 'POST',
|
||||
endpoint: '/api/session/:sessionId/prompt',
|
||||
note: 'No WebSocket race condition'
|
||||
}
|
||||
);
|
||||
|
||||
// Test that SSE receives events
|
||||
logTest('6.3 SSE Event Delivery', 'INFO',
|
||||
'AI responses arrive via SSE, not WebSocket',
|
||||
{
|
||||
endpoint: '/api/session/:sessionId/events',
|
||||
protocol: 'Server-Sent Events',
|
||||
note: 'Unidirectional, no race condition'
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Main test runner
|
||||
*/
|
||||
async function runTests() {
|
||||
console.log(`\n${colors.cyan}╔═══════════════════════════════════════════════════╗${colors.reset}`);
|
||||
console.log(`${colors.cyan}║ HYBRID APPROACH QA TEST SUITE ║${colors.reset}`);
|
||||
console.log(`${colors.cyan}║ Session Attachment Race Condition Fix ║${colors.reset}`);
|
||||
console.log(`${colors.cyan}╚═══════════════════════════════════════════════════╝${colors.reset}`);
|
||||
|
||||
console.log(`\n${colors.blue}Configuration:${colors.reset}`);
|
||||
console.log(` Base URL: ${BASE_URL}`);
|
||||
console.log(` Test Session: ${TEST_SESSION_ID}`);
|
||||
console.log(` Auth Cookie: ${AUTH_COOKIE.substring(0, 50)}...`);
|
||||
|
||||
try {
|
||||
await testURLRouting();
|
||||
await testSessionAPI();
|
||||
await testSSEConnection();
|
||||
await testEventBusIntegration();
|
||||
await testSessionIDValidation();
|
||||
await testRaceConditionPrevention();
|
||||
} catch (error) {
|
||||
console.error(`${colors.red}Fatal error during testing:${colors.reset}`, error);
|
||||
}
|
||||
|
||||
// Print summary
|
||||
console.log(`\n${colors.cyan}═══════════════════════════════════════════════════${colors.reset}`);
|
||||
console.log(`${colors.cyan}TEST SUMMARY${colors.reset}`);
|
||||
console.log(`${colors.cyan}═══════════════════════════════════════════════════${colors.reset}\n`);
|
||||
|
||||
const total = results.passed + results.failed + results.skipped;
|
||||
const passRate = total > 0 ? ((results.passed / total) * 100).toFixed(1) : 0;
|
||||
|
||||
console.log(` Total Tests: ${total}`);
|
||||
console.log(`${colors.green} Passed: ${results.passed}${colors.reset}`);
|
||||
console.log(`${colors.red} Failed: ${results.failed}${colors.reset}`);
|
||||
console.log(`${colors.yellow} Skipped: ${results.skipped}${colors.reset}`);
|
||||
console.log(` Pass Rate: ${passRate}%`);
|
||||
|
||||
if (results.failed > 0) {
|
||||
console.log(`\n${colors.red}Failed Tests:${colors.reset}`);
|
||||
results.tests
|
||||
.filter(t => t.status === 'FAIL')
|
||||
.forEach(t => {
|
||||
console.log(` ${colors.red}✗${colors.reset} ${t.name}: ${t.message}`);
|
||||
});
|
||||
}
|
||||
|
||||
console.log(`\n${colors.cyan}═══════════════════════════════════════════════════${colors.reset}\n`);
|
||||
|
||||
// Exit with appropriate code
|
||||
process.exit(results.failed > 0 ? 1 : 0);
|
||||
}
|
||||
|
||||
// Run tests
|
||||
runTests().catch(error => {
|
||||
console.error('Test runner error:', error);
|
||||
process.exit(1);
|
||||
});
|
||||
Reference in New Issue
Block a user