personal memory agent
1# SPDX-License-Identifier: AGPL-3.0-only
2# Copyright (c) 2026 sol pbc
3
4"""Tests for cogitate coder mode: write flag, coder agent."""
5
6import asyncio
7import importlib
8from unittest.mock import AsyncMock, patch
9
10# ---------------------------------------------------------------------------
11# Write flag — Anthropic provider
12# ---------------------------------------------------------------------------
13
14
15class TestAnthropicWriteFlag:
16 """Verify --allowedTools is controlled by config write flag."""
17
18 def _provider(self):
19 return importlib.import_module("think.providers.anthropic")
20
21 @patch("think.providers.anthropic.check_cli_binary")
22 @patch("think.providers.anthropic.CLIRunner")
23 def test_no_write_restricts_tools(self, mock_runner_cls, mock_check):
24 """Without write flag, --allowedTools restricts to sol."""
25 provider = self._provider()
26 mock_instance = AsyncMock()
27 mock_instance.run = AsyncMock(return_value="result")
28 mock_instance.cli_session_id = None
29 mock_runner_cls.return_value = mock_instance
30
31 config = {"prompt": "test", "model": "claude-sonnet-4-20250514"}
32 asyncio.run(provider.run_cogitate(config))
33
34 cmd = mock_runner_cls.call_args.kwargs["cmd"]
35 assert "--allowedTools" in cmd
36 assert "Bash(sol *)" in cmd
37
38 @patch("think.providers.anthropic.check_cli_binary")
39 @patch("think.providers.anthropic.CLIRunner")
40 def test_write_true_grants_full_access(self, mock_runner_cls, mock_check):
41 """With write=True, --allowedTools is omitted for full tool access."""
42 provider = self._provider()
43 mock_instance = AsyncMock()
44 mock_instance.run = AsyncMock(return_value="result")
45 mock_instance.cli_session_id = None
46 mock_runner_cls.return_value = mock_instance
47
48 config = {"prompt": "test", "model": "claude-sonnet-4-20250514", "write": True}
49 asyncio.run(provider.run_cogitate(config))
50
51 cmd = mock_runner_cls.call_args.kwargs["cmd"]
52 assert "--allowedTools" not in cmd
53
54 @patch("think.providers.anthropic.check_cli_binary")
55 @patch("think.providers.anthropic.CLIRunner")
56 def test_write_false_restricts_tools(self, mock_runner_cls, mock_check):
57 """Explicit write=False keeps restriction."""
58 provider = self._provider()
59 mock_instance = AsyncMock()
60 mock_instance.run = AsyncMock(return_value="result")
61 mock_instance.cli_session_id = None
62 mock_runner_cls.return_value = mock_instance
63
64 config = {"prompt": "test", "model": "claude-sonnet-4-20250514", "write": False}
65 asyncio.run(provider.run_cogitate(config))
66
67 cmd = mock_runner_cls.call_args.kwargs["cmd"]
68 assert "--allowedTools" in cmd
69
70
71# ---------------------------------------------------------------------------
72# Write flag — OpenAI provider
73# ---------------------------------------------------------------------------
74
75
76class TestOpenAIWriteFlag:
77 """Verify sandbox mode is controlled by config write flag."""
78
79 def _provider(self):
80 return importlib.import_module("think.providers.openai")
81
82 @patch("think.providers.openai.CLIRunner")
83 def test_no_write_uses_readonly_sandbox(self, mock_runner_cls):
84 """Without write flag, sandbox is read-only."""
85 provider = self._provider()
86 mock_instance = AsyncMock()
87 mock_instance.run = AsyncMock(return_value="result")
88 mock_instance.cli_session_id = None
89 mock_runner_cls.return_value = mock_instance
90
91 config = {"prompt": "test", "model": "gpt-5.2"}
92 asyncio.run(provider.run_cogitate(config))
93
94 cmd = mock_runner_cls.call_args.kwargs["cmd"]
95 # Find the -s flag and its value
96 s_idx = cmd.index("-s")
97 assert cmd[s_idx + 1] == "read-only"
98
99 @patch("think.providers.openai.CLIRunner")
100 def test_write_true_uses_write_sandbox(self, mock_runner_cls):
101 """With write=True, sandbox is write."""
102 provider = self._provider()
103 mock_instance = AsyncMock()
104 mock_instance.run = AsyncMock(return_value="result")
105 mock_instance.cli_session_id = None
106 mock_runner_cls.return_value = mock_instance
107
108 config = {"prompt": "test", "model": "gpt-5.2", "write": True}
109 asyncio.run(provider.run_cogitate(config))
110
111 cmd = mock_runner_cls.call_args.kwargs["cmd"]
112 s_idx = cmd.index("-s")
113 assert cmd[s_idx + 1] == "workspace-write"
114
115 @patch("think.providers.openai.CLIRunner")
116 def test_write_true_with_session_resume(self, mock_runner_cls):
117 """Write flag works correctly with session resume path."""
118 provider = self._provider()
119 mock_instance = AsyncMock()
120 mock_instance.run = AsyncMock(return_value="result")
121 mock_instance.cli_session_id = None
122 mock_runner_cls.return_value = mock_instance
123
124 config = {
125 "prompt": "test",
126 "model": "gpt-5.2",
127 "write": True,
128 "session_id": "sess-123",
129 }
130 asyncio.run(provider.run_cogitate(config))
131
132 cmd = mock_runner_cls.call_args.kwargs["cmd"]
133 s_idx = cmd.index("-s")
134 assert cmd[s_idx + 1] == "workspace-write"
135 assert "resume" in cmd
136
137
138# ---------------------------------------------------------------------------
139# Write flag — Google provider
140# ---------------------------------------------------------------------------
141
142
143class TestGoogleWriteFlag:
144 """Verify --allowed-tools is controlled by config write flag."""
145
146 def _provider(self):
147 return importlib.import_module("think.providers.google")
148
149 @patch("think.providers.google.CLIRunner")
150 def test_no_write_restricts_tools(self, mock_runner_cls):
151 """Without write flag, --allowed-tools restricts to sol."""
152 provider = self._provider()
153 mock_instance = AsyncMock()
154 mock_instance.run = AsyncMock(return_value="result")
155 mock_instance.cli_session_id = None
156 mock_runner_cls.return_value = mock_instance
157
158 config = {"prompt": "test", "model": "gemini-2.5-flash"}
159 asyncio.run(provider.run_cogitate(config))
160
161 cmd = mock_runner_cls.call_args.kwargs["cmd"]
162 assert "--allowed-tools" in cmd
163 assert "run_shell_command(sol)" in cmd
164
165 @patch("think.providers.google.CLIRunner")
166 def test_write_true_grants_full_access(self, mock_runner_cls):
167 """With write=True, --allowed-tools is omitted."""
168 provider = self._provider()
169 mock_instance = AsyncMock()
170 mock_instance.run = AsyncMock(return_value="result")
171 mock_instance.cli_session_id = None
172 mock_runner_cls.return_value = mock_instance
173
174 config = {"prompt": "test", "model": "gemini-2.5-flash", "write": True}
175 asyncio.run(provider.run_cogitate(config))
176
177 cmd = mock_runner_cls.call_args.kwargs["cmd"]
178 assert "--allowed-tools" not in cmd
179
180
181# ---------------------------------------------------------------------------
182# talent/coder.md existence and frontmatter
183# ---------------------------------------------------------------------------
184
185
186class TestCoderAgent:
187 """Verify talent/coder.md exists with correct frontmatter."""
188
189 def test_coder_md_exists(self):
190 """talent/coder.md must exist in the repo."""
191 from pathlib import Path
192
193 coder_path = Path(__file__).parent.parent / "talent" / "coder.md"
194 assert coder_path.exists(), "talent/coder.md not found"
195
196 def test_coder_frontmatter(self):
197 """coder.md must have write: true and type: cogitate."""
198 from pathlib import Path
199
200 import frontmatter
201
202 coder_path = Path(__file__).parent.parent / "talent" / "coder.md"
203 post = frontmatter.load(coder_path)
204
205 assert post.metadata.get("type") == "cogitate"
206 assert post.metadata.get("write") is True
207 assert post.metadata.get("title") == "Coder"
208 assert "description" in post.metadata
209
210 def test_coder_references_coding_skill(self):
211 """coder.md must reference the coding skill instead of inlining guidelines."""
212 from pathlib import Path
213
214 coder_path = Path(__file__).parent.parent / "talent" / "coder.md"
215 content = coder_path.read_text(encoding="utf-8")
216
217 # Should reference the coding skill, not inline dev guidelines
218 assert "coding" in content.lower()
219 assert "single source of truth" in content
220
221 # The coding skill must exist with reference files
222 coding_skill = Path(__file__).parent.parent / "talent" / "coding" / "SKILL.md"
223 assert coding_skill.exists(), "talent/coding/SKILL.md not found"
224
225 coding_refs = Path(__file__).parent.parent / "talent" / "coding" / "reference"
226 assert (coding_refs / "coding-standards.md").exists()
227 assert (coding_refs / "project-structure.md").exists()
228 assert (coding_refs / "testing.md").exists()
229 assert (coding_refs / "environment.md").exists()