personal memory agent
at main 165 lines 5.8 kB view raw
1# SPDX-License-Identifier: AGPL-3.0-only 2# Copyright (c) 2026 sol pbc 3 4"""Unit tests for sol transcribe CLI (M3, M8, M9).""" 5 6import argparse 7from pathlib import Path 8from unittest.mock import MagicMock, patch 9 10import pytest 11 12 13def test_main_accepts_journal_relative_path(tmp_path, monkeypatch): 14 """main() resolves audio_path relative to journal when absolute path fails.""" 15 seg_dir = tmp_path / "20260201" / "default" / "090000_300" 16 seg_dir.mkdir(parents=True) 17 audio_file = seg_dir / "audio.wav" 18 audio_file.touch() 19 20 monkeypatch.setenv("_SOLSTONE_JOURNAL_OVERRIDE", str(tmp_path)) 21 monkeypatch.setattr( 22 "sys.argv", ["sol transcribe", "20260201/default/090000_300/audio.wav"] 23 ) 24 25 mock_load = MagicMock(return_value=MagicMock()) 26 mock_vad_result = MagicMock() 27 mock_vad_result.has_speech = False 28 mock_vad_result.speech_duration = 0.0 29 mock_vad_result.duration = 5.0 30 mock_vad = MagicMock(return_value=mock_vad_result) 31 32 with ( 33 patch("observe.transcribe.main.load_audio", mock_load), 34 patch("observe.transcribe.main.run_vad", mock_vad), 35 patch("observe.transcribe.main.callosum_send"), 36 patch("observe.transcribe.main.get_segment_key", return_value="090000_300"), 37 patch("observe.transcribe.main._build_base_event", return_value={}), 38 patch("think.entities.load_recent_entity_names", return_value=[]), 39 ): 40 from observe.transcribe.main import main 41 42 main() 43 44 mock_load.assert_called_once() 45 46 47def test_main_errors_on_nonexistent_absolute_path(tmp_path, monkeypatch, capsys): 48 """main() errors clearly when path doesn't exist as absolute or journal-relative.""" 49 monkeypatch.setenv("_SOLSTONE_JOURNAL_OVERRIDE", str(tmp_path)) 50 monkeypatch.setattr("sys.argv", ["sol transcribe", "/nonexistent/path/audio.wav"]) 51 52 from observe.transcribe.main import main 53 54 with pytest.raises(SystemExit): 55 main() 56 57 captured = capsys.readouterr() 58 assert "Tried absolute" in captured.err or "not found" in captured.err.lower() 59 60 61def test_setup_cli_no_message_on_project_journal(tmp_path, monkeypatch, capsys): 62 """setup_cli() prints no informational message — journal path is always deterministic.""" 63 monkeypatch.setenv("_SOLSTONE_JOURNAL_OVERRIDE", str(tmp_path)) 64 65 with ( 66 patch("think.utils.get_journal", return_value=str(tmp_path)), 67 patch("think.utils.get_config", return_value={}), 68 ): 69 from think.utils import setup_cli 70 71 parser = argparse.ArgumentParser() 72 monkeypatch.setattr("sys.argv", ["test"]) 73 setup_cli(parser) 74 75 captured = capsys.readouterr() 76 assert "docs/INSTALL.md" not in captured.err 77 78 79def _make_batch_journal(tmp_path: Path) -> Path: 80 """Create a minimal temp journal with three segments for batch testing.""" 81 seg1 = tmp_path / "20260101" / "default" / "090000_300" 82 seg1.mkdir(parents=True) 83 (seg1 / "audio.flac").touch() 84 85 seg2 = tmp_path / "20260101" / "default" / "140000_300" 86 seg2.mkdir(parents=True) 87 (seg2 / "audio.flac").touch() 88 (seg2 / "audio.jsonl").touch() 89 90 seg3 = tmp_path / "20260101" / "default" / "180000_300" 91 seg3.mkdir(parents=True) 92 (seg3 / "screen.png").touch() 93 94 return tmp_path 95 96 97def test_all_batch_processes_unprocessed_skips_transcribed( 98 tmp_path, monkeypatch, capsys 99): 100 """--all processes unprocessed audio, skips already-transcribed, ignores non-audio.""" 101 journal = _make_batch_journal(tmp_path) 102 monkeypatch.setenv("_SOLSTONE_JOURNAL_OVERRIDE", str(journal)) 103 monkeypatch.setattr("sys.argv", ["sol transcribe", "--all"]) 104 105 mock_process_one = MagicMock() 106 107 with ( 108 patch("observe.transcribe.main._process_one", mock_process_one), 109 patch("think.entities.load_recent_entity_names", return_value=[]), 110 ): 111 from observe.transcribe.main import main 112 113 main() 114 115 assert mock_process_one.call_count == 1 116 called_path = mock_process_one.call_args[0][0] 117 assert called_path.name == "audio.flac" 118 assert "090000_300" in str(called_path) 119 120 captured = capsys.readouterr() 121 assert "1 processed" in captured.out 122 assert "1 skipped" in captured.out 123 124 125def test_all_redo_reprocesses_transcribed(tmp_path, monkeypatch): 126 """--all --redo reprocesses even segments that already have .jsonl.""" 127 journal = _make_batch_journal(tmp_path) 128 monkeypatch.setenv("_SOLSTONE_JOURNAL_OVERRIDE", str(journal)) 129 monkeypatch.setattr("sys.argv", ["sol transcribe", "--all", "--redo"]) 130 131 mock_process_one = MagicMock() 132 133 with ( 134 patch("observe.transcribe.main._process_one", mock_process_one), 135 patch("think.entities.load_recent_entity_names", return_value=[]), 136 ): 137 from observe.transcribe.main import main 138 139 main() 140 141 assert mock_process_one.call_count == 2 142 143 144def test_all_and_audio_path_mutually_exclusive(tmp_path, monkeypatch): 145 """Providing both --all and audio_path produces a clear error.""" 146 monkeypatch.setenv("_SOLSTONE_JOURNAL_OVERRIDE", str(tmp_path)) 147 monkeypatch.setattr("sys.argv", ["sol transcribe", "--all", "some/audio.wav"]) 148 149 with patch("think.entities.load_recent_entity_names", return_value=[]): 150 from observe.transcribe.main import main 151 152 with pytest.raises(SystemExit): 153 main() 154 155 156def test_neither_all_nor_audio_path_errors(tmp_path, monkeypatch): 157 """Providing neither --all nor audio_path produces a clear error.""" 158 monkeypatch.setenv("_SOLSTONE_JOURNAL_OVERRIDE", str(tmp_path)) 159 monkeypatch.setattr("sys.argv", ["sol transcribe"]) 160 161 with patch("think.entities.load_recent_entity_names", return_value=[]): 162 from observe.transcribe.main import main 163 164 with pytest.raises(SystemExit): 165 main()