diff --git a/static/boot.js b/static/boot.js index 1aaeb7af..f778cba7 100644 --- a/static/boot.js +++ b/static/boot.js @@ -444,6 +444,10 @@ $('btnAttach').onclick=e=>{if(e&&e.preventDefault)e.preventDefault();$('fileInpu // Raw audio mode preference: send audio file instead of transcribing let _rawAudioMode = localStorage.getItem('hermes-raw-audio-mode') === 'true'; + // Capture backend pinned at recording start ('speech' | 'media' | null) so + // _stopMic / onstop act on the backend that actually started, even if the + // raw-audio toggle changes mid-recording (#3169 Codex review). + let _activeCaptureMode = null; const btn=$('btnMic'); const status=$('micStatus'); @@ -549,7 +553,11 @@ $('btnAttach').onclick=e=>{if(e&&e.preventDefault)e.preventDefault();$('fileInpu function _stopMic(){ if(!window._micActive) return; - if(recognition && !_rawAudioMode){ + // Stop the backend that was ACTIVE WHEN RECORDING STARTED — not whatever + // _rawAudioMode says now. The user can toggle Settings → Sound mid-recording, + // which would otherwise make us stop the wrong backend and orphan the other + // (#3169 Codex review). _activeCaptureMode is pinned at start. + if(recognition && _activeCaptureMode==='speech'){ recognition.stop(); return; } @@ -630,6 +638,7 @@ $('btnAttach').onclick=e=>{if(e&&e.preventDefault)e.preventDefault();$('fileInpu _finalText=''; _prefix=ta.value; if(recognition && !_forceMediaRecorder && !_rawAudioMode){ + _activeCaptureMode='speech'; recognition.start(); _setRecording(true); return; @@ -659,7 +668,7 @@ $('btnAttach').onclick=e=>{if(e&&e.preventDefault)e.preventDefault();$('fileInpu _setRecording(false); _stopTracks(); if(blob.size){ - if(_rawAudioMode){ + if(_activeCaptureMode==='media-raw'){ await _sendRawAudio(blob); }else{ await _transcribeBlob(blob); @@ -669,6 +678,7 @@ $('btnAttach').onclick=e=>{if(e&&e.preventDefault)e.preventDefault();$('fileInpu window._micPendingSend=false; } }; + _activeCaptureMode=_rawAudioMode?'media-raw':'media-transcribe'; mediaRecorder.start(); _setRecording(true); }catch(err){ diff --git a/tests/test_raw_audio_upload.py b/tests/test_raw_audio_upload.py index f5221aef..c50d7973 100644 --- a/tests/test_raw_audio_upload.py +++ b/tests/test_raw_audio_upload.py @@ -141,3 +141,36 @@ def test_raw_audio_upload_different_formats(): except KeyError: pass assert handler.status is not None, f"Failed for {filename} ({mime})" + + +# ── Front-end: raw-audio mic backend pinning (boot.js source invariants) ────── +# Regression guard for the #3169 Codex-review fix: _stopMic / onstop must act on +# the capture backend that was ACTIVE WHEN RECORDING STARTED (pinned in +# _activeCaptureMode), not whatever _rawAudioMode says now — otherwise toggling +# Settings → Sound mid-recording orphans the wrong backend. Also: an explicit +# Send click (_micPendingSend) must send even with text in the composer. +import pathlib as _pathlib + +_BOOT_JS = (_pathlib.Path(__file__).parent.parent / "static" / "boot.js").read_text(encoding="utf-8") + + +def test_stop_mic_uses_pinned_active_capture_mode_not_current_rawmode(): + assert "let _activeCaptureMode" in _BOOT_JS, "_activeCaptureMode must be declared" + # _stopMic decides backend from the pinned mode, not _rawAudioMode. + assert "_activeCaptureMode==='speech'" in _BOOT_JS + # onstop dispatches raw-vs-transcribe from the pinned mode too. + assert "_activeCaptureMode==='media-raw'" in _BOOT_JS + # the mode is pinned at both start branches. + assert "_activeCaptureMode='speech'" in _BOOT_JS + assert "_activeCaptureMode=_rawAudioMode?'media-raw':'media-transcribe'" in _BOOT_JS + + +def test_send_raw_audio_honors_explicit_pending_send(): + # An explicit Send-button click (sets _micPendingSend) must send the raw + # audio even when the composer already has text. + assert "if(window._micPendingSend){" in _BOOT_JS + # and it lives inside _sendRawAudio (before the empty-composer fallback). + idx = _BOOT_JS.index("async function _sendRawAudio") + end = _BOOT_JS.index("function _commitTranscript", idx) + body = _BOOT_JS[idx:end] + assert "window._micPendingSend" in body and "send()" in body