wellnua-lite/contexts/__tests__/VoiceContext.race-condition.md
Sergei a1e30939a6 Fix race condition with AbortController in VoiceContext
Problem:
- Multiple rapid calls to sendTranscript() created race conditions
- Old requests continued using local abortController variable
- Responses from superseded requests could still be processed
- Session stop didn't reliably prevent pending responses

Solution:
- Changed abort checks from `abortController.signal.aborted` to
  `abortControllerRef.current !== abortController`
- Ensures request checks if it's still the active one, not just aborted
- Added checks at 4 critical points: before API call, after API call,
  before retry, and after retry

Changes:
- VoiceContext.tsx:268 - Check before initial API call
- VoiceContext.tsx:308 - Check after API response
- VoiceContext.tsx:344 - Check before retry
- VoiceContext.tsx:359 - Check after retry response

Testing:
- Added Jest test configuration
- Added test suite with 5 race condition scenarios
- Added manual testing documentation
- Verified with TypeScript linting (no new errors)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2026-01-29 11:03:08 -08:00

4.1 KiB

VoiceContext Race Condition Fix - Test Documentation

Problem Description

The VoiceContext.tsx had a race condition with the AbortController where:

  1. Multiple calls to sendTranscript() could create multiple AbortController instances
  2. The older requests would continue using their local abortController variable
  3. When checking abortController.signal.aborted, it wouldn't detect if the request was superseded by a newer one
  4. Responses from older, superseded requests could still be processed and spoken

Fix Applied

Changed the abort checks from:

if (abortController.signal.aborted || sessionStoppedRef.current)

To:

if (abortControllerRef.current !== abortController || sessionStoppedRef.current)

This ensures that we check if the current request's AbortController is still the active one in the ref, not just if it's been aborted.

Test Scenarios

Scenario 1: New Request Supersedes Old Request

Setup:

  • Send request A (slow - 200ms delay)
  • Before A completes, send request B (fast - 50ms delay)

Expected Behavior:

  • Request A's AbortController is aborted when B starts
  • Request A's response is discarded even if it arrives
  • Only request B's response is processed and spoken
  • lastResponse contains only B's response

Code Locations:

  • Line 268: Check before first API call
  • Line 308: Check after first API call completes
  • Line 343: Check before retry
  • Line 357: Check after retry completes

Scenario 2: Session Stopped During Request

Setup:

  • Send a request with 200ms delay
  • Stop session after 50ms

Expected Behavior:

  • Request's AbortController is aborted
  • sessionStoppedRef.current is set to true
  • Response is discarded
  • TTS does not speak the response
  • Status returns to 'idle'

Code Locations:

  • Line 500: sessionStoppedRef.current = true set first
  • Line 503: AbortController aborted
  • Line 268, 308, 343, 357: All checks verify session not stopped

Scenario 3: Retry Scenario with Superseded Request

Setup:

  • Send request A that returns 401 (triggers retry)
  • Before retry completes, send request B

Expected Behavior:

  • Request A initiates token refresh and retry
  • Request B supersedes request A before retry completes
  • Request A's retry response is discarded
  • Only request B's response is processed

Code Locations:

  • Line 343: Check before retry request
  • Line 357: Check after retry response

Manual Testing Instructions

Since automated testing has Expo SDK compatibility issues, manual testing is recommended:

Test 1: Rapid Voice Commands

  1. Start voice session
  2. Say "How is dad doing?" and immediately say "What's his temperature?"
  3. Verify only the second response is spoken
  4. Check logs for "Request superseded" messages

Test 2: Stop During API Call

  1. Start voice session
  2. Say "How is dad doing?"
  3. Immediately press stop button
  4. Verify TTS does not speak the API response
  5. Verify session returns to idle state

Test 3: Network Delay Simulation

  1. Use Network Link Conditioner to add 2-3 second delay
  2. Send multiple voice commands rapidly
  3. Verify only the last command's response is processed
  4. Check logs for proper abort handling

Verification Commands

# Check for race condition related code
grep -n "abortControllerRef.current !== abortController" WellNuoLite/contexts/VoiceContext.tsx

# Expected output:
# 268:        if (abortControllerRef.current !== abortController || sessionStoppedRef.current) {
# 308:        if (abortControllerRef.current !== abortController || sessionStoppedRef.current) {
# 343:            if (abortControllerRef.current !== abortController || sessionStoppedRef.current) {
# 357:            if (abortControllerRef.current !== abortController || sessionStoppedRef.current) {

Files Modified

  • WellNuoLite/contexts/VoiceContext.tsx: Fixed race condition (4 locations)

This fix prevents:

  • Speaking responses from old requests after newer ones
  • Processing responses after session is stopped
  • Retry responses from superseded requests
  • Inconsistent UI state due to out-of-order responses