personal memory agent
1# SPDX-License-Identifier: AGPL-3.0-only
2# Copyright (c) 2026 sol pbc
3
4from __future__ import annotations
5
6import json
7from unittest.mock import Mock, patch
8
9import pytest
10
11import think.providers
12import think.providers.anthropic
13import think.providers.google
14import think.providers.openai
15from convey import create_app
16from tests.conftest import copytree_tracked
17from think.providers import validate_key
18
19
20@pytest.fixture
21def settings_client(journal_copy):
22 app = create_app(str(journal_copy))
23 app.config["TESTING"] = True
24 return app.test_client(), journal_copy
25
26
27def test_validate_key_anthropic_success():
28 client = Mock()
29 client.models.list.return_value = [Mock()]
30
31 with patch("anthropic.Anthropic", return_value=client) as mock_cls:
32 result = think.providers.anthropic.validate_key("test-key")
33
34 assert result == {"valid": True}
35 mock_cls.assert_called_once_with(api_key="test-key", timeout=10)
36
37
38def test_validate_key_anthropic_auth_error():
39 client = Mock()
40 client.models.list.side_effect = Exception("invalid x-api-key")
41
42 with patch("anthropic.Anthropic", return_value=client):
43 result = think.providers.anthropic.validate_key("bad-key")
44
45 assert result["valid"] is False
46 assert "invalid x-api-key" in result["error"]
47
48
49def test_validate_key_openai_success():
50 client = Mock()
51 client.models.list.return_value = [Mock()]
52
53 with patch("openai.OpenAI", return_value=client) as mock_cls:
54 result = think.providers.openai.validate_key("test-key")
55
56 assert result == {"valid": True}
57 mock_cls.assert_called_once_with(api_key="test-key", timeout=10)
58
59
60def test_validate_key_openai_auth_error():
61 client = Mock()
62 client.models.list.side_effect = Exception("Incorrect API key")
63
64 with patch("openai.OpenAI", return_value=client):
65 result = think.providers.openai.validate_key("bad-key")
66
67 assert result["valid"] is False
68 assert "Incorrect API key" in result["error"]
69
70
71def test_validate_key_google_success():
72 client = Mock()
73 client.models.list.return_value = [Mock()]
74
75 with patch("think.providers.google.genai.Client", return_value=client) as mock_cls:
76 result = think.providers.google.validate_key("test-key")
77
78 assert result == {"valid": True}
79 mock_cls.assert_called_once()
80 assert mock_cls.call_args.kwargs["api_key"] == "test-key"
81
82
83def test_validate_key_google_auth_error():
84 client = Mock()
85 client.models.list.side_effect = Exception("API key not valid")
86
87 with patch("think.providers.google.genai.Client", return_value=client):
88 result = think.providers.google.validate_key("bad-key")
89
90 assert result["valid"] is False
91 assert "API key not valid" in result["error"]
92
93
94def test_validate_key_dispatcher_success():
95 with patch("think.providers.google.validate_key", return_value={"valid": True}):
96 result = validate_key("google", "test-key")
97
98 assert result == {"valid": True}
99
100
101def test_validate_key_dispatcher_unknown_provider():
102 with pytest.raises(ValueError, match="Unknown provider"):
103 validate_key("bogus", "test-key")
104
105
106def test_validate_key_timeout():
107 """Validate that timeout exceptions are caught and reported."""
108 client = Mock()
109 client.models.list.side_effect = TimeoutError("Connection timed out")
110
111 with patch("openai.OpenAI", return_value=client):
112 result = think.providers.openai.validate_key("test-key")
113
114 assert result["valid"] is False
115 assert "timed out" in result["error"]
116
117
118def test_update_config_saves_key_validation(settings_client):
119 client, journal = settings_client
120
121 with patch(
122 "think.providers.validate_key",
123 return_value={"valid": False, "error": "bad key"},
124 ):
125 response = client.put(
126 "/app/settings/api/config",
127 json={"section": "env", "data": {"GOOGLE_API_KEY": "bad-key"}},
128 )
129
130 assert response.status_code == 200
131 payload = response.get_json()
132 assert payload["success"] is True
133 assert payload["key_validation"]["google"]["valid"] is False
134 assert payload["key_validation"]["google"]["error"] == "bad key"
135 assert "timestamp" in payload["key_validation"]["google"]
136
137 config = json.loads((journal / "config" / "journal.json").read_text())
138 assert config["providers"]["auth"]["google"] == "api_key"
139 assert config["providers"]["key_validation"]["google"]["valid"] is False
140
141
142def test_update_config_clears_key_validation(settings_client):
143 client, journal = settings_client
144 config_path = journal / "config" / "journal.json"
145 config = json.loads(config_path.read_text())
146 config.setdefault("env", {})["GOOGLE_API_KEY"] = "existing-key"
147 config.setdefault("providers", {}).setdefault("auth", {})["google"] = "api_key"
148 config["providers"]["key_validation"] = {
149 "google": {"valid": True, "timestamp": "2026-01-01T00:00:00+00:00"}
150 }
151 config_path.write_text(json.dumps(config, indent=2) + "\n", encoding="utf-8")
152
153 response = client.put(
154 "/app/settings/api/config",
155 json={"section": "env", "data": {"GOOGLE_API_KEY": ""}},
156 )
157
158 assert response.status_code == 200
159 payload = response.get_json()
160 assert payload["success"] is True
161 assert "google" not in payload["key_validation"]
162
163 saved = json.loads(config_path.read_text())
164 assert saved["providers"]["auth"]["google"] == "platform"
165 assert "google" not in saved["providers"]["key_validation"]
166
167
168def test_get_providers_includes_key_validation(settings_client):
169 client, journal = settings_client
170 config_path = journal / "config" / "journal.json"
171 config = json.loads(config_path.read_text())
172 config.setdefault("providers", {})["key_validation"] = {
173 "openai": {"valid": True, "timestamp": "2026-01-01T00:00:00+00:00"}
174 }
175 config_path.write_text(json.dumps(config, indent=2) + "\n", encoding="utf-8")
176
177 response = client.get("/app/settings/api/providers")
178
179 assert response.status_code == 200
180 payload = response.get_json()
181 assert payload["key_validation"]["openai"]["valid"] is True
182
183
184def test_validate_all_keys_endpoint(settings_client):
185 client, journal = settings_client
186 config_path = journal / "config" / "journal.json"
187 config = json.loads(config_path.read_text())
188 config.setdefault("env", {})["GOOGLE_API_KEY"] = "google-key"
189 config["env"]["OPENAI_API_KEY"] = "openai-key"
190 config_path.write_text(json.dumps(config, indent=2) + "\n", encoding="utf-8")
191
192 def fake_validate(provider: str, api_key: str) -> dict:
193 return {
194 "valid": provider == "google",
195 "error": "" if provider == "google" else "bad key",
196 }
197
198 with patch("think.providers.validate_key", side_effect=fake_validate):
199 response = client.post("/app/settings/api/validate-keys")
200
201 assert response.status_code == 200
202 payload = response.get_json()
203 assert payload["success"] is True
204 assert payload["key_validation"]["google"]["valid"] is True
205 assert payload["key_validation"]["openai"]["valid"] is False
206 assert "timestamp" in payload["key_validation"]["google"]
207
208 saved = json.loads(config_path.read_text())
209 assert set(saved["providers"]["key_validation"]) == {"google", "openai"}