personal memory agent
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

test: harden test infra — drop soundfile stub, hoist VAD imports, fix fixture leaks

Three independent test-infra hardening changes bundled into one pass, all
surfaced by the install-service load diagnosis (req_4655ikra):

- Delete the order-dependent soundfile stub in tests/conftest.py. The real
library is already a top-level dep; no test relied on the fake b"fLaCfake"
payload.
- Hoist `from faster_whisper.vad import VadOptions, get_speech_timestamps`
to module scope in observe/vad.py, eliminating the partial-module cascade
risk under timeout. Retarget the 8 @patch decorators in tests/test_vad.py
to observe.vad.get_speech_timestamps accordingly. sol --help is unaffected
(transcribe is lazy-loaded via the COMMANDS registry).
- Fix 5 tests in tests/test_sense.py that leaked into tests/fixtures/journal
via observe/sense.day_path() and post-test subprocess races. Each test now
overrides _SOLSTONE_JOURNAL_OVERRIDE to tmp_path via monkeypatch (covers
both think.runner._get_journal_path and observe.sense.day_path code paths).
- Add submit_mock to test_supervisor_schedule.py::test_excludes_today to
prevent it from spawning a real background `sol think` task that keeps
writing to the fixture journal after the test returns.

Validated: two concurrent `pytest tests/ --ignore=tests/integration` runs
complete green with no FileNotFoundError races; `make test` leaves
tests/fixtures/journal/ byte-for-byte unchanged (empty find-snapshot diff).

+39 -28
+1 -2
observe/vad.py
··· 26 26 from dataclasses import dataclass, field 27 27 28 28 import numpy as np 29 + from faster_whisper.vad import VadOptions, get_speech_timestamps 29 30 30 31 from observe.utils import SAMPLE_RATE 31 32 ··· 295 296 VadResult with duration info, has_speech flag, speech segment boundaries, 296 297 and non-speech RMS level for noise detection 297 298 """ 298 - from faster_whisper.vad import VadOptions, get_speech_timestamps 299 - 300 299 logging.info("Running VAD...") 301 300 t0 = time.perf_counter() 302 301
-8
tests/conftest.py
··· 273 273 274 274 cv2_mod.cvtColor = cvtColor 275 275 sys.modules["cv2"] = cv2_mod 276 - if "soundfile" not in sys.modules: 277 - sf_mod = types.ModuleType("soundfile") 278 - 279 - def write(buf, data, samplerate, format=None): 280 - buf.write(b"fLaCfake") 281 - 282 - sf_mod.write = write 283 - sys.modules["soundfile"] = sf_mod 284 276 for name in [ 285 277 "noisereduce", 286 278 ]:
+29 -9
tests/test_sense.py
··· 373 373 assert len(log_files) == 1, f"Expected 1 echo log file, found {len(log_files)}" 374 374 375 375 376 - def test_file_sensor_spawn_handler_duplicate(tmp_path, mock_callosum): 376 + @patch("think.runner._current_day") 377 + def test_file_sensor_spawn_handler_duplicate( 378 + mock_day, tmp_path, monkeypatch, mock_callosum 379 + ): 377 380 """Test that duplicate file processing is prevented.""" 381 + monkeypatch.setenv("_SOLSTONE_JOURNAL_OVERRIDE", str(tmp_path)) 382 + mock_day.return_value = "20250101" 383 + 378 384 # Create journal/day structure 379 385 day_dir = tmp_path / "chronicle" / "20250101" 380 386 day_dir.mkdir(parents=True) ··· 399 405 mock_popen.assert_not_called() 400 406 401 407 402 - @patch("think.runner._get_journal_path") 403 408 @patch("think.runner._current_day") 404 409 def test_file_sensor_spawn_handler_real_process( 405 - mock_day, mock_journal, tmp_path, mock_callosum 410 + mock_day, tmp_path, monkeypatch, mock_callosum 406 411 ): 407 412 """Test spawning a real process and monitoring completion.""" 408 - # Mock runner functions to use tmp_path 409 - mock_journal.return_value = tmp_path 413 + monkeypatch.setenv("_SOLSTONE_JOURNAL_OVERRIDE", str(tmp_path)) 410 414 mock_day.return_value = "20241101" 411 415 412 416 sensor = FileSensor(tmp_path) ··· 434 438 assert "[echo:stdout]" in log_content 435 439 436 440 437 - def test_file_sensor_spawn_handler_failing_process(tmp_path): 441 + @patch("think.runner._current_day") 442 + def test_file_sensor_spawn_handler_failing_process(mock_day, tmp_path, monkeypatch): 438 443 """Test handling of failing process.""" 444 + monkeypatch.setenv("_SOLSTONE_JOURNAL_OVERRIDE", str(tmp_path)) 445 + mock_day.return_value = "20241101" 446 + 439 447 sensor = FileSensor(tmp_path) 440 448 441 449 test_file = tmp_path / "test.txt" ··· 451 459 assert test_file not in sensor.running 452 460 453 461 454 - def test_file_sensor_failing_process_notifies(tmp_path): 462 + @patch("think.runner._current_day") 463 + def test_file_sensor_failing_process_notifies(mock_day, tmp_path, monkeypatch): 455 464 """Test that a failing handler process emits a notification event.""" 465 + monkeypatch.setenv("_SOLSTONE_JOURNAL_OVERRIDE", str(tmp_path)) 466 + mock_day.return_value = "20241101" 467 + 456 468 sensor = FileSensor(tmp_path) 457 469 # Mock callosum on sensor to capture emitted events 458 470 sensor.callosum = MagicMock() ··· 613 625 mock_handle.assert_not_called() 614 626 615 627 616 - def test_file_sensor_segment_observed_includes_day(tmp_path, mock_callosum): 628 + @patch("think.runner._current_day") 629 + def test_file_sensor_segment_observed_includes_day( 630 + mock_day, tmp_path, monkeypatch, mock_callosum 631 + ): 617 632 """Test that observe.observed event includes day field.""" 618 633 from think.callosum import CallosumConnection 634 + 635 + monkeypatch.setenv("_SOLSTONE_JOURNAL_OVERRIDE", str(tmp_path)) 636 + mock_day.return_value = "20250101" 619 637 620 638 # Create journal/day/stream/segment structure 621 639 day_dir = tmp_path / "chronicle" / "20250101" ··· 662 680 assert observed_events[0].get("segment") == "143022_300" 663 681 664 682 665 - def test_file_sensor_segment_observed_no_handlers(tmp_path, mock_callosum): 683 + def test_file_sensor_segment_observed_no_handlers(tmp_path, monkeypatch, mock_callosum): 666 684 """Test that observe.observed is emitted immediately for segments with no matching handlers. 667 685 668 686 This covers the case of tmux-only segments where files like .jsonl don't match 669 687 any registered patterns (*.flac, *.webm, etc.). 670 688 """ 671 689 from think.callosum import CallosumConnection 690 + 691 + monkeypatch.setenv("_SOLSTONE_JOURNAL_OVERRIDE", str(tmp_path)) 672 692 673 693 # Create journal/day/stream/segment structure 674 694 day_dir = tmp_path / "chronicle" / "20250101"
+1 -1
tests/test_supervisor_schedule.py
··· 132 132 assert mod._daily_state["last_day"] == date(2025, 1, 2) 133 133 134 134 135 - def test_excludes_today(mock_callosum, monkeypatch, set_today): 135 + def test_excludes_today(mock_callosum, monkeypatch, submit_mock, set_today): 136 136 mod._daily_state["last_day"] = date(2025, 1, 1) 137 137 set_today(date(2025, 1, 2)) 138 138 updated_days = MagicMock(return_value=["20250101"])
+8 -8
tests/test_vad.py
··· 297 297 class TestRunVad: 298 298 """Test run_vad function.""" 299 299 300 - @patch("faster_whisper.vad.get_speech_timestamps") 300 + @patch("observe.vad.get_speech_timestamps") 301 301 def test_silent_audio_returns_no_speech(self, mock_get_timestamps): 302 302 """Silent audio should return has_speech=False.""" 303 303 audio = np.zeros(5 * SAMPLE_RATE, dtype=np.float32) ··· 309 309 assert result.speech_duration == 0.0 310 310 assert result.has_speech is False 311 311 312 - @patch("faster_whisper.vad.get_speech_timestamps") 312 + @patch("observe.vad.get_speech_timestamps") 313 313 def test_speech_audio_returns_has_speech(self, mock_get_timestamps): 314 314 """Audio with speech should return has_speech=True.""" 315 315 audio = np.zeros(5 * SAMPLE_RATE, dtype=np.float32) ··· 324 324 # Speech segments should be converted to seconds 325 325 assert result.speech_segments == [(1.0, 3.0)] 326 326 327 - @patch("faster_whisper.vad.get_speech_timestamps") 327 + @patch("observe.vad.get_speech_timestamps") 328 328 def test_speech_below_threshold(self, mock_get_timestamps): 329 329 """Speech below threshold should return has_speech=False.""" 330 330 audio = np.zeros(5 * SAMPLE_RATE, dtype=np.float32) ··· 337 337 assert result.speech_duration == 0.5 338 338 assert result.has_speech is False 339 339 340 - @patch("faster_whisper.vad.get_speech_timestamps") 340 + @patch("observe.vad.get_speech_timestamps") 341 341 def test_custom_min_speech_threshold(self, mock_get_timestamps): 342 342 """Custom min_speech_seconds threshold should be respected.""" 343 343 audio = np.zeros(5 * SAMPLE_RATE, dtype=np.float32) ··· 352 352 result = run_vad(audio, min_speech_seconds=1.0) 353 353 assert result.has_speech is False 354 354 355 - @patch("faster_whisper.vad.get_speech_timestamps") 355 + @patch("observe.vad.get_speech_timestamps") 356 356 def test_multiple_speech_chunks(self, mock_get_timestamps): 357 357 """Multiple speech chunks should be summed correctly.""" 358 358 audio = np.zeros(5 * SAMPLE_RATE, dtype=np.float32) ··· 368 368 assert result.speech_duration == 2.0 369 369 assert result.has_speech is True 370 370 371 - @patch("faster_whisper.vad.get_speech_timestamps") 371 + @patch("observe.vad.get_speech_timestamps") 372 372 def test_returns_rms_for_silent_background(self, mock_get_timestamps): 373 373 """run_vad should return low RMS for silent non-speech regions.""" 374 374 # Silent audio (zeros) ··· 382 382 assert result.noisy_rms < 0.001 # Effectively zero 383 383 assert result.noisy_s == 3.0 # 1s leading + 2s trailing 384 384 385 - @patch("faster_whisper.vad.get_speech_timestamps") 385 + @patch("observe.vad.get_speech_timestamps") 386 386 def test_returns_rms_for_noisy_background(self, mock_get_timestamps): 387 387 """run_vad should return measurable RMS for noisy non-speech regions.""" 388 388 # Noisy audio ··· 397 397 assert result.noisy_rms > 0.01 # Noisy threshold 398 398 assert result.noisy_s == 3.0 399 399 400 - @patch("faster_whisper.vad.get_speech_timestamps") 400 + @patch("observe.vad.get_speech_timestamps") 401 401 def test_returns_none_rms_when_no_qualifying_segments(self, mock_get_timestamps): 402 402 """run_vad should return None RMS when no qualifying non-speech segments.""" 403 403 audio = np.zeros(2 * SAMPLE_RATE, dtype=np.float32)