personal memory agent
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()