← back to Dear Bubbe Nextjs
archived/tests/comprehensive-voice-test.js
526 lines
#!/usr/bin/env node
/**
* COMPREHENSIVE BUBBE.AI VOICE TESTING SYSTEM
*
* Tests every aspect of the voice chat functionality:
* 1. Server health and responsiveness
* 2. Button presence and visibility
* 3. ElevenLabs voice API functionality
* 4. Actual audio generation and playback
* 5. Full integration testing
*
* Runs every 10 minutes until 5:00 AM
*/
const fs = require('fs');
const { exec } = require('child_process');
const util = require('util');
const execPromise = util.promisify(exec);
const LOG_FILE = '/tmp/bubbe-monitoring.log';
const HOURLY_SUMMARY_FILE = '/tmp/bubbe-hourly-summary.log';
const AUDIO_TEST_DIR = '/tmp/bubbe-audio-tests';
const PORT = 3011;
const SERVER_IP = '45.61.58.125';
const BASE_URL = `http://${SERVER_IP}:${PORT}`;
// Test results storage
let testResults = {
timestamp: new Date().toISOString(),
serverHealth: null,
buttonPresence: null,
elevenLabsAPI: null,
audioGeneration: null,
integration: null,
errors: []
};
// Ensure directories exist
if (!fs.existsSync(AUDIO_TEST_DIR)) {
fs.mkdirSync(AUDIO_TEST_DIR, { recursive: true });
}
// Logging function
function log(message, level = 'INFO') {
const timestamp = new Date().toISOString();
const logMessage = `[${timestamp}] [${level}] ${message}\n`;
console.log(logMessage.trim());
fs.appendFileSync(LOG_FILE, logMessage);
}
// Test 1: Server Health Check
async function testServerHealth() {
log('=== TEST 1: SERVER HEALTH CHECK ===');
try {
// Check if port is responding
const { stdout: curlOutput } = await execPromise(`curl -s -o /dev/null -w "%{http_code}|%{time_total}" --max-time 5 ${BASE_URL}`);
const [statusCode, responseTime] = curlOutput.trim().split('|');
if (statusCode === '200') {
log(`✓ Server responding on port ${PORT} (HTTP ${statusCode}, ${parseFloat(responseTime).toFixed(3)}s)`);
testResults.serverHealth = {
status: 'PASS',
statusCode,
responseTime: parseFloat(responseTime),
url: BASE_URL
};
} else {
log(`✗ Server returned unexpected status: ${statusCode}`, 'ERROR');
testResults.serverHealth = {
status: 'FAIL',
statusCode,
error: `Unexpected HTTP status: ${statusCode}`
};
testResults.errors.push('Server health check failed');
return false;
}
// Check PM2 process status
const { stdout: pm2Status } = await execPromise(`pm2 jlist | jq -r '.[] | select(.name=="bubbe") | .pm2_env.status'`);
const processStatus = pm2Status.trim();
if (processStatus === 'online') {
log(`✓ PM2 process status: ${processStatus}`);
testResults.serverHealth.pm2Status = processStatus;
} else {
log(`✗ PM2 process not online: ${processStatus}`, 'ERROR');
testResults.serverHealth.pm2Status = processStatus;
testResults.errors.push(`PM2 process status: ${processStatus}`);
// Attempt auto-recovery
await attemptRecovery('pm2_offline');
return false;
}
// Check memory and CPU usage
const { stdout: pm2Details } = await execPromise(`pm2 jlist | jq -r '.[] | select(.name=="bubbe") | {cpu: .monit.cpu, memory: .monit.memory}'`);
const resourceUsage = JSON.parse(pm2Details);
log(`✓ Resource usage: CPU ${resourceUsage.cpu}%, Memory ${(resourceUsage.memory / 1024 / 1024).toFixed(2)}MB`);
testResults.serverHealth.resources = resourceUsage;
return true;
} catch (error) {
log(`✗ Server health check failed: ${error.message}`, 'ERROR');
testResults.serverHealth = {
status: 'FAIL',
error: error.message
};
testResults.errors.push(`Server health check exception: ${error.message}`);
// Attempt auto-recovery
await attemptRecovery('server_error');
return false;
}
}
// Test 2: Button Presence Check
async function testButtonPresence() {
log('=== TEST 2: BUTTON PRESENCE CHECK ===');
try {
// Fetch the HTML and check for button
const { stdout: htmlContent } = await execPromise(`curl -s ${BASE_URL}`);
const hasLetsChatButton = htmlContent.includes("Let's Chat") || htmlContent.includes("Let's Chat");
const hasVoiceButton = htmlContent.includes('voice') || htmlContent.includes('microphone');
if (hasLetsChatButton) {
log(`✓ "Let's Chat" button found in HTML`);
testResults.buttonPresence = {
status: 'PASS',
found: true,
buttonText: "Let's Chat"
};
} else if (hasVoiceButton) {
log(`✓ Voice/microphone button found in HTML`);
testResults.buttonPresence = {
status: 'PASS',
found: true,
buttonText: 'voice button'
};
} else {
log(`✗ No chat button found in HTML`, 'ERROR');
testResults.buttonPresence = {
status: 'FAIL',
found: false,
error: 'Button not present in HTML'
};
testResults.errors.push('Button not found in page HTML');
return false;
}
// Check for required JavaScript files
const hasVoiceScript = htmlContent.includes('bubbe-voice') || htmlContent.includes('voice') || htmlContent.includes('speech');
if (hasVoiceScript) {
log(`✓ Voice-related scripts found in HTML`);
testResults.buttonPresence.hasScripts = true;
} else {
log(`⚠ No obvious voice scripts detected`, 'WARN');
testResults.buttonPresence.hasScripts = false;
}
return true;
} catch (error) {
log(`✗ Button presence check failed: ${error.message}`, 'ERROR');
testResults.buttonPresence = {
status: 'FAIL',
error: error.message
};
testResults.errors.push(`Button check exception: ${error.message}`);
return false;
}
}
// Test 3: ElevenLabs Voice API Test
async function testElevenLabsAPI() {
log('=== TEST 3: ELEVENLABS VOICE API TEST ===');
try {
// Test with simple text
const testText = "Hello, this is a test of Bubbe's voice.";
const testPayload = JSON.stringify({ text: testText, mode: 'bubbe' });
log(`Testing voice generation with text: "${testText}"`);
const startTime = Date.now();
const { stdout: response, stderr } = await execPromise(
`curl -s -X POST ${BASE_URL}/api/bubbe-voice \
-H "Content-Type: application/json" \
-d '${testPayload}' \
-w "\\n%{http_code}|%{size_download}|%{time_total}" \
-o /tmp/test-audio-${Date.now()}.mp3 --max-time 15`,
{ maxBuffer: 10 * 1024 * 1024 }
);
const endTime = Date.now();
const lines = response.trim().split('\n');
const metrics = lines[lines.length - 1].split('|');
const [statusCode, audioSize, responseTime] = metrics;
log(`API Response: HTTP ${statusCode}, Size: ${audioSize} bytes, Time: ${responseTime}s`);
if (statusCode === '200') {
const audioSizeNum = parseInt(audioSize);
if (audioSizeNum > 1000) {
log(`✓ Voice API working correctly (${audioSizeNum} bytes audio generated)`);
testResults.elevenLabsAPI = {
status: 'PASS',
statusCode,
audioSize: audioSizeNum,
responseTime: parseFloat(responseTime),
duration: endTime - startTime
};
return true;
} else {
log(`✗ Voice API returned small audio file (${audioSizeNum} bytes)`, 'ERROR');
testResults.elevenLabsAPI = {
status: 'FAIL',
statusCode,
audioSize: audioSizeNum,
error: 'Audio file too small'
};
testResults.errors.push('ElevenLabs returned suspiciously small audio');
return false;
}
} else {
log(`✗ Voice API returned HTTP ${statusCode}`, 'ERROR');
testResults.elevenLabsAPI = {
status: 'FAIL',
statusCode,
error: `HTTP ${statusCode}`
};
testResults.errors.push(`Voice API HTTP ${statusCode}`);
return false;
}
} catch (error) {
log(`✗ ElevenLabs API test failed: ${error.message}`, 'ERROR');
testResults.elevenLabsAPI = {
status: 'FAIL',
error: error.message
};
testResults.errors.push(`ElevenLabs API exception: ${error.message}`);
return false;
}
}
// Test 4: Audio Generation and Validation
async function testAudioGeneration() {
log('=== TEST 4: AUDIO GENERATION AND VALIDATION ===');
try {
const testPhrases = [
{ text: "Shayna punim! How are you today?", mode: "bubbe" },
{ text: "Oy vey, what a day!", mode: "meshugana" },
{ text: "Let me tell you a story...", mode: "comedian" }
];
let allTestsPassed = true;
for (const [index, phrase] of testPhrases.entries()) {
log(`Testing phrase ${index + 1}: "${phrase.text}" (mode: ${phrase.mode})`);
const audioFileName = `${AUDIO_TEST_DIR}/test-${Date.now()}-${index}.mp3`;
const payload = JSON.stringify(phrase);
try {
const { stdout } = await execPromise(
`curl -s -X POST ${BASE_URL}/api/bubbe-voice \
-H "Content-Type: application/json" \
-d '${payload}' \
-w "\\n%{http_code}|%{size_download}" \
-o "${audioFileName}" --max-time 15`
);
const [statusCode, audioSize] = stdout.trim().split('\n').pop().split('|');
const audioSizeNum = parseInt(audioSize);
if (statusCode === '200' && audioSizeNum > 1000) {
// Verify the file exists and has content
const fileStats = fs.statSync(audioFileName);
if (fileStats.size > 1000) {
log(` ✓ Audio generated successfully (${fileStats.size} bytes)`);
// Try to validate MP3 header
const buffer = fs.readFileSync(audioFileName);
const hasMP3Header = buffer[0] === 0xFF && (buffer[1] & 0xE0) === 0xE0;
if (hasMP3Header) {
log(` ✓ Valid MP3 file format`);
} else {
log(` ⚠ File may not be valid MP3`, 'WARN');
allTestsPassed = false;
}
} else {
log(` ✗ Audio file too small (${fileStats.size} bytes)`, 'ERROR');
allTestsPassed = false;
}
} else {
log(` ✗ Failed to generate audio (HTTP ${statusCode}, ${audioSize} bytes)`, 'ERROR');
allTestsPassed = false;
}
} catch (error) {
log(` ✗ Error testing phrase: ${error.message}`, 'ERROR');
allTestsPassed = false;
}
}
testResults.audioGeneration = {
status: allTestsPassed ? 'PASS' : 'FAIL',
testsRun: testPhrases.length,
testDirectory: AUDIO_TEST_DIR
};
if (!allTestsPassed) {
testResults.errors.push('Some audio generation tests failed');
}
return allTestsPassed;
} catch (error) {
log(`✗ Audio generation test failed: ${error.message}`, 'ERROR');
testResults.audioGeneration = {
status: 'FAIL',
error: error.message
};
testResults.errors.push(`Audio generation exception: ${error.message}`);
return false;
}
}
// Test 5: Full Integration Test
async function testIntegration() {
log('=== TEST 5: FULL INTEGRATION TEST ===');
try {
// Test the complete flow
log('Simulating complete voice chat flow...');
// 1. Check TTS configuration endpoint
const { stdout: configCheck } = await execPromise(`curl -s ${BASE_URL}/api/bubbe-voice`);
const config = JSON.parse(configCheck);
if (config.configured) {
log('✓ TTS service is configured');
} else {
log('✗ TTS service not configured', 'ERROR');
testResults.integration = {
status: 'FAIL',
error: 'TTS not configured'
};
testResults.errors.push('TTS service not configured');
return false;
}
// 2. Test a realistic conversation flow
const conversationTest = "Bubbele, let me tell you about the time I made the best brisket in the neighborhood!";
const payload = JSON.stringify({ text: conversationTest, mode: 'bubbe' });
const { stdout: conversationResponse } = await execPromise(
`curl -s -X POST ${BASE_URL}/api/bubbe-voice \
-H "Content-Type: application/json" \
-d '${payload}' \
-w "\\n%{http_code}|%{size_download}|%{time_total}" \
-o /tmp/integration-test.mp3 --max-time 15`
);
const [statusCode, audioSize, responseTime] = conversationResponse.trim().split('\n').pop().split('|');
if (statusCode === '200' && parseInt(audioSize) > 5000) {
log(`✓ Integration test passed (${audioSize} bytes in ${responseTime}s)`);
testResults.integration = {
status: 'PASS',
audioSize: parseInt(audioSize),
responseTime: parseFloat(responseTime)
};
return true;
} else {
log(`✗ Integration test failed (HTTP ${statusCode}, ${audioSize} bytes)`, 'ERROR');
testResults.integration = {
status: 'FAIL',
statusCode,
audioSize: parseInt(audioSize),
error: 'Integration flow incomplete'
};
testResults.errors.push('Integration test failed');
return false;
}
} catch (error) {
log(`✗ Integration test failed: ${error.message}`, 'ERROR');
testResults.integration = {
status: 'FAIL',
error: error.message
};
testResults.errors.push(`Integration test exception: ${error.message}`);
return false;
}
}
// Auto-recovery function
async function attemptRecovery(reason) {
log(`=== ATTEMPTING AUTO-RECOVERY (Reason: ${reason}) ===`, 'WARN');
try {
// Step 1: Restart PM2 process
log('Step 1: Restarting PM2 process...');
await execPromise('pm2 restart bubbe');
await new Promise(resolve => setTimeout(resolve, 5000)); // Wait 5 seconds
// Check if restart worked
const { stdout: statusCheck } = await execPromise(`curl -s -o /dev/null -w "%{http_code}" --max-time 5 ${BASE_URL}`);
if (statusCheck.trim() === '200') {
log('✓ Recovery successful: PM2 restart fixed the issue');
return true;
}
// Step 2: If restart didn't work, try clearing cache and rebuilding
log('Step 2: PM2 restart insufficient, clearing cache...');
await execPromise('cd /root/Projects/dear-bubbe-nextjs && rm -rf .next');
log('Step 3: Rebuilding application...');
const { stdout: buildOutput } = await execPromise('cd /root/Projects/dear-bubbe-nextjs && npm run build', {
maxBuffer: 10 * 1024 * 1024
});
log('Step 4: Restarting after rebuild...');
await execPromise('pm2 restart bubbe');
await new Promise(resolve => setTimeout(resolve, 10000)); // Wait 10 seconds
// Final check
const { stdout: finalCheck } = await execPromise(`curl -s -o /dev/null -w "%{http_code}" --max-time 5 ${BASE_URL}`);
if (finalCheck.trim() === '200') {
log('✓ Recovery successful: Full rebuild fixed the issue');
return true;
} else {
log('✗ Recovery failed: Issue persists after rebuild', 'ERROR');
return false;
}
} catch (error) {
log(`✗ Recovery failed: ${error.message}`, 'ERROR');
return false;
}
}
// Generate test summary
function generateSummary() {
log('=== TEST SUMMARY ===');
const totalTests = 5;
const passedTests = Object.values(testResults).filter(t =>
t && typeof t === 'object' && t.status === 'PASS'
).length;
const passRate = ((passedTests / totalTests) * 100).toFixed(1);
log(`Tests Passed: ${passedTests}/${totalTests} (${passRate}%)`);
log(`Errors: ${testResults.errors.length}`);
if (testResults.errors.length > 0) {
log('Error Details:');
testResults.errors.forEach((error, index) => {
log(` ${index + 1}. ${error}`, 'ERROR');
});
}
// Write summary to hourly log
const hourlyEntry = {
timestamp: testResults.timestamp,
passRate: `${passedTests}/${totalTests}`,
serverHealth: testResults.serverHealth?.status || 'UNKNOWN',
elevenLabs: testResults.elevenLabsAPI?.status || 'UNKNOWN',
errors: testResults.errors.length,
details: testResults
};
fs.appendFileSync(HOURLY_SUMMARY_FILE, JSON.stringify(hourlyEntry, null, 2) + '\n---\n');
return passedTests === totalTests;
}
// Main test execution
async function runAllTests() {
log('╔═══════════════════════════════════════════════════════════════╗');
log('║ BUBBE.AI COMPREHENSIVE VOICE TESTING - STARTING ║');
log('╚═══════════════════════════════════════════════════════════════╝');
const startTime = Date.now();
// Run all tests in sequence
await testServerHealth();
await testButtonPresence();
await testElevenLabsAPI();
await testAudioGeneration();
await testIntegration();
// Generate summary
const allTestsPassed = generateSummary();
const duration = ((Date.now() - startTime) / 1000).toFixed(2);
log(`\nTotal test duration: ${duration}s`);
if (allTestsPassed) {
log('╔═══════════════════════════════════════════════════════════════╗');
log('║ ✓ ALL TESTS PASSED - SYSTEM HEALTHY ║');
log('╚═══════════════════════════════════════════════════════════════╝');
return 0;
} else {
log('╔═══════════════════════════════════════════════════════════════╗');
log('║ ✗ SOME TESTS FAILED - CHECK LOGS FOR DETAILS ║');
log('╚═══════════════════════════════════════════════════════════════╝');
return 1;
}
}
// Execute tests
runAllTests()
.then(exitCode => process.exit(exitCode))
.catch(error => {
log(`FATAL ERROR: ${error.message}`, 'ERROR');
console.error(error);
process.exit(1);
});