← 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);
  });