personal memory agent
1# SPDX-License-Identifier: AGPL-3.0-only
2# Copyright (c) 2026 sol pbc
3
4"""Tests for think.help_cli."""
5
6import io
7import json
8import subprocess
9import sys
10from unittest.mock import MagicMock, patch
11
12import pytest
13
14from think.help_cli import main
15
16
17@pytest.fixture(autouse=True)
18def _set_journal_path(monkeypatch):
19 monkeypatch.setenv("_SOLSTONE_JOURNAL_OVERRIDE", "tests/fixtures/journal")
20
21
22def _make_popen(stdout_lines, *, returncode=0):
23 """Build a mock Popen whose stdout yields *stdout_lines*."""
24 proc = MagicMock()
25 proc.stdin = MagicMock()
26 proc.stdout = io.StringIO("\n".join(stdout_lines) + "\n")
27 proc.stderr = MagicMock()
28 proc.stderr.read.return_value = ""
29 proc.returncode = returncode
30 proc.wait.return_value = returncode
31 return proc
32
33
34def test_help_no_question_shows_static_help(monkeypatch):
35 monkeypatch.setattr(sys, "argv", ["sol help"])
36
37 with patch("sol.print_help") as mock_print_help:
38 main()
39
40 mock_print_help.assert_called_once()
41
42
43def test_help_parses_question(monkeypatch):
44 monkeypatch.setattr(sys, "argv", ["sol help", "how", "do", "I", "search"])
45 mock_proc = _make_popen(
46 ['{"event":"finish","result":"Use sol call journal search"}'],
47 )
48
49 with patch("think.help_cli.subprocess.Popen", return_value=mock_proc) as mock_cls:
50 main()
51
52 call_args = mock_cls.call_args
53 assert call_args[0][0] == ["sol", "agents"]
54 assert call_args[1]["stdin"] == subprocess.PIPE
55 assert call_args[1]["stdout"] == subprocess.PIPE
56 assert call_args[1]["text"] is True
57
58 written = mock_proc.stdin.write.call_args[0][0]
59 payload = json.loads(written.strip())
60 assert payload["prompt"] == "how do I search"
61 mock_proc.stdin.close.assert_called_once()
62
63
64def test_help_ndjson_config(monkeypatch):
65 monkeypatch.setattr(sys, "argv", ["sol help", "show", "todo", "commands"])
66 mock_proc = _make_popen(['{"event":"finish","result":"ok"}'])
67
68 with patch("think.help_cli.subprocess.Popen", return_value=mock_proc):
69 main()
70
71 written = mock_proc.stdin.write.call_args[0][0]
72 payload = json.loads(written.strip())
73 assert payload == {"name": "unified", "prompt": "show todo commands"}
74
75
76def test_help_parses_finish_event(monkeypatch, capsys):
77 monkeypatch.setattr(sys, "argv", ["sol help", "how", "to", "search"])
78 mock_proc = _make_popen(
79 [
80 '{"event":"start","ts":1}',
81 '{"event":"thinking","ts":2,"summary":"..."}',
82 '{"event":"finish","ts":3,"result":"Use `sol call journal search`."}',
83 ]
84 )
85
86 with patch("think.help_cli.subprocess.Popen", return_value=mock_proc):
87 main()
88
89 captured = capsys.readouterr()
90 assert "Use `sol call journal search`." in captured.out
91
92
93def test_help_uses_last_finish_event(monkeypatch, capsys):
94 monkeypatch.setattr(sys, "argv", ["sol help", "search"])
95 mock_proc = _make_popen(
96 [
97 '{"event":"finish","ts":1,"result":"old result"}',
98 '{"event":"finish","ts":2,"result":"new result"}',
99 ]
100 )
101
102 with patch("think.help_cli.subprocess.Popen", return_value=mock_proc):
103 main()
104
105 captured = capsys.readouterr()
106 assert "new result" in captured.out
107 assert "old result" not in captured.out
108
109
110def test_help_handles_error_event(monkeypatch, capsys):
111 monkeypatch.setattr(sys, "argv", ["sol help", "bad", "request"])
112 mock_proc = _make_popen(
113 ['{"event":"error","error":"provider unavailable"}'],
114 returncode=1,
115 )
116
117 with patch("think.help_cli.subprocess.Popen", return_value=mock_proc):
118 with pytest.raises(SystemExit) as exc_info:
119 main()
120
121 assert exc_info.value.code == 1
122 captured = capsys.readouterr()
123 assert "provider unavailable" in captured.err
124
125
126def test_help_handles_empty_finish_result(monkeypatch, capsys):
127 monkeypatch.setattr(sys, "argv", ["sol help", "empty"])
128 mock_proc = _make_popen(['{"event":"finish","result":""}'])
129
130 with patch("think.help_cli.subprocess.Popen", return_value=mock_proc):
131 with pytest.raises(SystemExit) as exc_info:
132 main()
133
134 assert exc_info.value.code == 1
135 captured = capsys.readouterr()
136 assert "empty result" in captured.err.lower()
137
138
139def test_help_handles_timeout(monkeypatch, capsys):
140 monkeypatch.setattr(sys, "argv", ["sol help", "slow", "question"])
141 mock_proc = _make_popen([])
142 mock_proc.wait.side_effect = subprocess.TimeoutExpired(
143 cmd=["sol", "agents"], timeout=120
144 )
145
146 with patch("think.help_cli.subprocess.Popen", return_value=mock_proc):
147 with pytest.raises(SystemExit) as exc_info:
148 main()
149
150 assert exc_info.value.code == 1
151 mock_proc.kill.assert_called_once()
152 captured = capsys.readouterr()
153 assert "timed out" in captured.err.lower()