diff --git a/observe/vad.py b/observe/vad.py index 23c1a88d0..f48c1b015 100644 --- a/observe/vad.py +++ b/observe/vad.py @@ -26,6 +26,7 @@ import time from dataclasses import dataclass, field import numpy as np +from faster_whisper.vad import VadOptions, get_speech_timestamps from observe.utils import SAMPLE_RATE @@ -295,8 +296,6 @@ def run_vad( VadResult with duration info, has_speech flag, speech segment boundaries, and non-speech RMS level for noise detection """ - from faster_whisper.vad import VadOptions, get_speech_timestamps - logging.info("Running VAD...") t0 = time.perf_counter() diff --git a/tests/conftest.py b/tests/conftest.py index b180d665a..9fcb2963b 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -273,14 +273,6 @@ def add_module_stubs(request, monkeypatch): cv2_mod.cvtColor = cvtColor sys.modules["cv2"] = cv2_mod - if "soundfile" not in sys.modules: - sf_mod = types.ModuleType("soundfile") - - def write(buf, data, samplerate, format=None): - buf.write(b"fLaCfake") - - sf_mod.write = write - sys.modules["soundfile"] = sf_mod for name in [ "noisereduce", ]: diff --git a/tests/test_sense.py b/tests/test_sense.py index baddd346e..9a7bd373a 100644 --- a/tests/test_sense.py +++ b/tests/test_sense.py @@ -373,8 +373,14 @@ def test_file_sensor_spawn_handler(mock_popen, mock_day, mock_journal, tmp_path) assert len(log_files) == 1, f"Expected 1 echo log file, found {len(log_files)}" -def test_file_sensor_spawn_handler_duplicate(tmp_path, mock_callosum): +@patch("think.runner._current_day") +def test_file_sensor_spawn_handler_duplicate( + mock_day, tmp_path, monkeypatch, mock_callosum +): """Test that duplicate file processing is prevented.""" + monkeypatch.setenv("_SOLSTONE_JOURNAL_OVERRIDE", str(tmp_path)) + mock_day.return_value = "20250101" + # Create journal/day structure day_dir = tmp_path / "chronicle" / "20250101" day_dir.mkdir(parents=True) @@ -399,14 +405,12 @@ def test_file_sensor_spawn_handler_duplicate(tmp_path, mock_callosum): mock_popen.assert_not_called() -@patch("think.runner._get_journal_path") @patch("think.runner._current_day") def test_file_sensor_spawn_handler_real_process( - mock_day, mock_journal, tmp_path, mock_callosum + mock_day, tmp_path, monkeypatch, mock_callosum ): """Test spawning a real process and monitoring completion.""" - # Mock runner functions to use tmp_path - mock_journal.return_value = tmp_path + monkeypatch.setenv("_SOLSTONE_JOURNAL_OVERRIDE", str(tmp_path)) mock_day.return_value = "20241101" sensor = FileSensor(tmp_path) @@ -434,8 +438,12 @@ def test_file_sensor_spawn_handler_real_process( assert "[echo:stdout]" in log_content -def test_file_sensor_spawn_handler_failing_process(tmp_path): +@patch("think.runner._current_day") +def test_file_sensor_spawn_handler_failing_process(mock_day, tmp_path, monkeypatch): """Test handling of failing process.""" + monkeypatch.setenv("_SOLSTONE_JOURNAL_OVERRIDE", str(tmp_path)) + mock_day.return_value = "20241101" + sensor = FileSensor(tmp_path) test_file = tmp_path / "test.txt" @@ -451,8 +459,12 @@ def test_file_sensor_spawn_handler_failing_process(tmp_path): assert test_file not in sensor.running -def test_file_sensor_failing_process_notifies(tmp_path): +@patch("think.runner._current_day") +def test_file_sensor_failing_process_notifies(mock_day, tmp_path, monkeypatch): """Test that a failing handler process emits a notification event.""" + monkeypatch.setenv("_SOLSTONE_JOURNAL_OVERRIDE", str(tmp_path)) + mock_day.return_value = "20241101" + sensor = FileSensor(tmp_path) # Mock callosum on sensor to capture emitted events sensor.callosum = MagicMock() @@ -613,10 +625,16 @@ def test_file_sensor_handle_callosum_message_invalid_event(tmp_path): mock_handle.assert_not_called() -def test_file_sensor_segment_observed_includes_day(tmp_path, mock_callosum): +@patch("think.runner._current_day") +def test_file_sensor_segment_observed_includes_day( + mock_day, tmp_path, monkeypatch, mock_callosum +): """Test that observe.observed event includes day field.""" from think.callosum import CallosumConnection + monkeypatch.setenv("_SOLSTONE_JOURNAL_OVERRIDE", str(tmp_path)) + mock_day.return_value = "20250101" + # Create journal/day/stream/segment structure day_dir = tmp_path / "chronicle" / "20250101" segment_dir = day_dir / "default" / "143022_300" @@ -662,7 +680,7 @@ def test_file_sensor_segment_observed_includes_day(tmp_path, mock_callosum): assert observed_events[0].get("segment") == "143022_300" -def test_file_sensor_segment_observed_no_handlers(tmp_path, mock_callosum): +def test_file_sensor_segment_observed_no_handlers(tmp_path, monkeypatch, mock_callosum): """Test that observe.observed is emitted immediately for segments with no matching handlers. This covers the case of tmux-only segments where files like .jsonl don't match @@ -670,6 +688,8 @@ def test_file_sensor_segment_observed_no_handlers(tmp_path, mock_callosum): """ from think.callosum import CallosumConnection + monkeypatch.setenv("_SOLSTONE_JOURNAL_OVERRIDE", str(tmp_path)) + # Create journal/day/stream/segment structure day_dir = tmp_path / "chronicle" / "20250101" segment_dir = day_dir / "default" / "143022_300" diff --git a/tests/test_supervisor_schedule.py b/tests/test_supervisor_schedule.py index 4aaf7fc60..7a8b5c558 100644 --- a/tests/test_supervisor_schedule.py +++ b/tests/test_supervisor_schedule.py @@ -132,7 +132,7 @@ def test_advances_state_with_no_updated_days( assert mod._daily_state["last_day"] == date(2025, 1, 2) -def test_excludes_today(mock_callosum, monkeypatch, set_today): +def test_excludes_today(mock_callosum, monkeypatch, submit_mock, set_today): mod._daily_state["last_day"] = date(2025, 1, 1) set_today(date(2025, 1, 2)) updated_days = MagicMock(return_value=["20250101"]) diff --git a/tests/test_vad.py b/tests/test_vad.py index 697a1ac6d..f5fcecb15 100644 --- a/tests/test_vad.py +++ b/tests/test_vad.py @@ -297,7 +297,7 @@ class TestComputeNonspeechRms: class TestRunVad: """Test run_vad function.""" - @patch("faster_whisper.vad.get_speech_timestamps") + @patch("observe.vad.get_speech_timestamps") def test_silent_audio_returns_no_speech(self, mock_get_timestamps): """Silent audio should return has_speech=False.""" audio = np.zeros(5 * SAMPLE_RATE, dtype=np.float32) @@ -309,7 +309,7 @@ class TestRunVad: assert result.speech_duration == 0.0 assert result.has_speech is False - @patch("faster_whisper.vad.get_speech_timestamps") + @patch("observe.vad.get_speech_timestamps") def test_speech_audio_returns_has_speech(self, mock_get_timestamps): """Audio with speech should return has_speech=True.""" audio = np.zeros(5 * SAMPLE_RATE, dtype=np.float32) @@ -324,7 +324,7 @@ class TestRunVad: # Speech segments should be converted to seconds assert result.speech_segments == [(1.0, 3.0)] - @patch("faster_whisper.vad.get_speech_timestamps") + @patch("observe.vad.get_speech_timestamps") def test_speech_below_threshold(self, mock_get_timestamps): """Speech below threshold should return has_speech=False.""" audio = np.zeros(5 * SAMPLE_RATE, dtype=np.float32) @@ -337,7 +337,7 @@ class TestRunVad: assert result.speech_duration == 0.5 assert result.has_speech is False - @patch("faster_whisper.vad.get_speech_timestamps") + @patch("observe.vad.get_speech_timestamps") def test_custom_min_speech_threshold(self, mock_get_timestamps): """Custom min_speech_seconds threshold should be respected.""" audio = np.zeros(5 * SAMPLE_RATE, dtype=np.float32) @@ -352,7 +352,7 @@ class TestRunVad: result = run_vad(audio, min_speech_seconds=1.0) assert result.has_speech is False - @patch("faster_whisper.vad.get_speech_timestamps") + @patch("observe.vad.get_speech_timestamps") def test_multiple_speech_chunks(self, mock_get_timestamps): """Multiple speech chunks should be summed correctly.""" audio = np.zeros(5 * SAMPLE_RATE, dtype=np.float32) @@ -368,7 +368,7 @@ class TestRunVad: assert result.speech_duration == 2.0 assert result.has_speech is True - @patch("faster_whisper.vad.get_speech_timestamps") + @patch("observe.vad.get_speech_timestamps") def test_returns_rms_for_silent_background(self, mock_get_timestamps): """run_vad should return low RMS for silent non-speech regions.""" # Silent audio (zeros) @@ -382,7 +382,7 @@ class TestRunVad: assert result.noisy_rms < 0.001 # Effectively zero assert result.noisy_s == 3.0 # 1s leading + 2s trailing - @patch("faster_whisper.vad.get_speech_timestamps") + @patch("observe.vad.get_speech_timestamps") def test_returns_rms_for_noisy_background(self, mock_get_timestamps): """run_vad should return measurable RMS for noisy non-speech regions.""" # Noisy audio @@ -397,7 +397,7 @@ class TestRunVad: assert result.noisy_rms > 0.01 # Noisy threshold assert result.noisy_s == 3.0 - @patch("faster_whisper.vad.get_speech_timestamps") + @patch("observe.vad.get_speech_timestamps") def test_returns_none_rms_when_no_qualifying_segments(self, mock_get_timestamps): """run_vad should return None RMS when no qualifying non-speech segments.""" audio = np.zeros(2 * SAMPLE_RATE, dtype=np.float32)