Skip to content

Commit 6aff6fb

Browse files
committed
Add tests for EntryManager handling of separate diary and planner paths
1 parent e596dd2 commit 6aff6fb

File tree

2 files changed

+264
-1
lines changed

2 files changed

+264
-1
lines changed

tests/brain_cli/test_cli.py

Lines changed: 190 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
"""Integration tests for CLI commands."""
22
import pytest
3-
from unittest.mock import Mock, patch
3+
from unittest.mock import Mock, patch, MagicMock
4+
from datetime import date, timedelta
45
from typer.testing import CliRunner
56

67
from brain_cli.main import app as brain_app
8+
from brain_cli.plan_commands import parse_date_arg, extract_tasks_from_diary
79

810

911
runner = CliRunner()
@@ -109,6 +111,193 @@ def test_plan_already_exists(self, mock_manager, mock_config, mock_env):
109111

110112
assert "already exists" in result.stdout.lower()
111113

114+
@patch('brain_cli.plan_commands.get_config')
115+
@patch('brain_cli.plan_commands.get_llm_client')
116+
@patch('brain_cli.plan_commands.EntryManager')
117+
def test_plan_with_diary_tasks(self, mock_manager, mock_llm, mock_config, mock_env):
118+
"""Test plan create with LLM task extraction from diary."""
119+
from brain_core.entry_manager import DiaryEntry
120+
121+
# Setup mocks
122+
mock_config.return_value = Mock(
123+
diary_path=mock_env["diary_path"],
124+
planner_path=mock_env["planner_path"]
125+
)
126+
127+
# Mock LLM to return tasks
128+
mock_llm_client = Mock()
129+
mock_llm_client.generate_sync.return_value = """1. Follow up with Sarah about project
130+
2. Review pull request #42
131+
3. Prepare slides for presentation"""
132+
mock_llm.return_value = mock_llm_client
133+
134+
# Mock diary entry with substantial content
135+
yesterday_diary = DiaryEntry(
136+
date.today() - timedelta(days=1),
137+
"## Brain Dump\n" + "A" * 100,
138+
entry_type="reflection"
139+
)
140+
yesterday_diary._has_substantial_content = True
141+
142+
mock_manager_instance = Mock()
143+
mock_manager_instance.entry_exists.return_value = False
144+
mock_manager_instance.read_entry.side_effect = lambda d, entry_type: (
145+
yesterday_diary if entry_type == "reflection" else None
146+
)
147+
mock_manager.return_value = mock_manager_instance
148+
149+
result = runner.invoke(brain_app, ["plan", "create", "today"])
150+
151+
assert result.exit_code == 0
152+
assert "Created plan" in result.stdout
153+
# Should show extracted tasks
154+
assert "extracted from diary" in result.stdout
155+
156+
@patch('brain_cli.plan_commands.get_config')
157+
@patch('brain_cli.plan_commands.EntryManager')
158+
def test_plan_with_pending_todos(self, mock_manager, mock_config, mock_env):
159+
"""Test plan create carries forward unchecked todos."""
160+
from brain_core.entry_manager import DiaryEntry
161+
162+
mock_config.return_value = Mock(
163+
diary_path=mock_env["diary_path"],
164+
planner_path=mock_env["planner_path"]
165+
)
166+
167+
# Mock yesterday's plan with unchecked todos
168+
yesterday_plan = DiaryEntry(
169+
date.today() - timedelta(days=1),
170+
"""## Action Items
171+
- [ ] Finish project documentation
172+
- [x] Completed task
173+
- [ ] Review team feedback""",
174+
entry_type="plan"
175+
)
176+
177+
mock_manager_instance = Mock()
178+
mock_manager_instance.entry_exists.return_value = False
179+
mock_manager_instance.read_entry.side_effect = lambda d, entry_type: (
180+
yesterday_plan if entry_type == "plan" else None
181+
)
182+
mock_manager.return_value = mock_manager_instance
183+
184+
result = runner.invoke(brain_app, ["plan", "create", "today"])
185+
186+
assert result.exit_code == 0
187+
assert "pending from plan" in result.stdout
188+
189+
190+
class TestPlanHelpers:
191+
"""Tests for plan command helper functions."""
192+
193+
def test_parse_date_today(self):
194+
"""Test parsing 'today' argument."""
195+
result = parse_date_arg("today")
196+
assert result == date.today()
197+
198+
def test_parse_date_tomorrow(self):
199+
"""Test parsing 'tomorrow' argument."""
200+
result = parse_date_arg("tomorrow")
201+
assert result == date.today() + timedelta(days=1)
202+
203+
def test_parse_date_iso(self):
204+
"""Test parsing ISO date string."""
205+
result = parse_date_arg("2025-10-12")
206+
assert result == date(2025, 10, 12)
207+
208+
def test_extract_tasks_from_empty_diary(self):
209+
"""Test task extraction with empty diary content."""
210+
mock_llm = Mock()
211+
tasks = extract_tasks_from_diary("", "2025-10-11", mock_llm)
212+
assert tasks == []
213+
# LLM should not be called for empty content
214+
mock_llm.generate_sync.assert_not_called()
215+
216+
def test_extract_tasks_from_short_diary(self):
217+
"""Test task extraction with short diary content."""
218+
mock_llm = Mock()
219+
tasks = extract_tasks_from_diary("Too short", "2025-10-11", mock_llm)
220+
assert tasks == []
221+
# LLM should not be called for short content
222+
mock_llm.generate_sync.assert_not_called()
223+
224+
def test_extract_tasks_success(self):
225+
"""Test successful task extraction from diary."""
226+
mock_llm = Mock()
227+
mock_llm.generate_sync.return_value = """Here are the tasks:
228+
1. Follow up with Sarah about the project proposal
229+
2. Review pull request #42 before EOD
230+
3. Prepare slides for Thursday presentation
231+
4. Send meeting notes to team"""
232+
233+
diary_content = "A" * 100 # Substantial content
234+
tasks = extract_tasks_from_diary(diary_content, "2025-10-11", mock_llm)
235+
236+
assert len(tasks) == 4
237+
assert "Follow up with Sarah about the project proposal" in tasks
238+
assert "Review pull request #42 before EOD" in tasks
239+
assert "Prepare slides for Thursday presentation" in tasks
240+
assert "Send meeting notes to team" in tasks
241+
242+
# Verify LLM was called with correct parameters
243+
mock_llm.generate_sync.assert_called_once()
244+
call_kwargs = mock_llm.generate_sync.call_args.kwargs
245+
assert "prompt" in call_kwargs
246+
assert "system" in call_kwargs
247+
assert call_kwargs["temperature"] == 0.4
248+
assert call_kwargs["max_tokens"] == 300
249+
250+
def test_extract_tasks_no_tasks_found(self):
251+
"""Test task extraction when LLM finds no tasks."""
252+
mock_llm = Mock()
253+
mock_llm.generate_sync.return_value = "NO_TASKS"
254+
255+
diary_content = "A" * 100
256+
tasks = extract_tasks_from_diary(diary_content, "2025-10-11", mock_llm)
257+
258+
assert tasks == []
259+
260+
def test_extract_tasks_with_parentheses_format(self):
261+
"""Test task extraction with parentheses numbering."""
262+
mock_llm = Mock()
263+
mock_llm.generate_sync.return_value = """1) First task
264+
2) Second task
265+
3) Third task"""
266+
267+
diary_content = "A" * 100
268+
tasks = extract_tasks_from_diary(diary_content, "2025-10-11", mock_llm)
269+
270+
assert len(tasks) == 3
271+
assert "First task" in tasks
272+
assert "Second task" in tasks
273+
274+
def test_extract_tasks_filters_short_tasks(self):
275+
"""Test that very short tasks are filtered out."""
276+
mock_llm = Mock()
277+
mock_llm.generate_sync.return_value = """1. OK
278+
2. This is a proper task description
279+
3. No
280+
4. Another valid task here"""
281+
282+
diary_content = "A" * 100
283+
tasks = extract_tasks_from_diary(diary_content, "2025-10-11", mock_llm)
284+
285+
# Only tasks with >5 characters should be included
286+
assert len(tasks) == 2
287+
assert "This is a proper task description" in tasks
288+
assert "Another valid task here" in tasks
289+
290+
def test_extract_tasks_handles_llm_error(self):
291+
"""Test task extraction gracefully handles LLM errors."""
292+
mock_llm = Mock()
293+
mock_llm.generate_sync.side_effect = Exception("LLM API error")
294+
295+
diary_content = "A" * 100
296+
tasks = extract_tasks_from_diary(diary_content, "2025-10-11", mock_llm)
297+
298+
# Should return empty list on error, not crash
299+
assert tasks == []
300+
112301

113302
class TestNotesCommands:
114303
"""Tests for notes commands."""

tests/brain_core/test_entry_manager.py

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,80 @@ def test_get_entry_path_plan(self, temp_dir):
110110
path = manager.get_entry_path(date(2025, 10, 12), entry_type="plan")
111111
assert path == temp_dir / "2025-10-12-plan.md"
112112

113+
def test_separate_planner_path(self, temp_dir):
114+
"""Test EntryManager with separate planner_path."""
115+
diary_dir = temp_dir / "diary"
116+
planner_dir = temp_dir / "planner"
117+
diary_dir.mkdir()
118+
planner_dir.mkdir()
119+
120+
manager = EntryManager(diary_dir, planner_dir)
121+
122+
# Reflection should go to diary_path
123+
reflection_path = manager.get_entry_path(date(2025, 10, 12), entry_type="reflection")
124+
assert reflection_path == diary_dir / "2025-10-12.md"
125+
126+
# Plan should go to planner_path
127+
plan_path = manager.get_entry_path(date(2025, 10, 12), entry_type="plan")
128+
assert plan_path == planner_dir / "2025-10-12-plan.md"
129+
130+
def test_planner_path_defaults_to_diary_path(self, temp_dir):
131+
"""Test that planner_path defaults to diary_path if not specified."""
132+
manager = EntryManager(temp_dir)
133+
134+
# Both should use the same path
135+
reflection_path = manager.get_entry_path(date(2025, 10, 12), entry_type="reflection")
136+
plan_path = manager.get_entry_path(date(2025, 10, 12), entry_type="plan")
137+
138+
assert reflection_path.parent == plan_path.parent
139+
assert reflection_path.parent == temp_dir
140+
141+
def test_write_entry_to_separate_paths(self, temp_dir):
142+
"""Test writing entries to separate diary and planner paths."""
143+
diary_dir = temp_dir / "diary"
144+
planner_dir = temp_dir / "planner"
145+
diary_dir.mkdir()
146+
planner_dir.mkdir()
147+
148+
manager = EntryManager(diary_dir, planner_dir)
149+
test_date = date(2025, 10, 12)
150+
151+
# Write reflection entry
152+
reflection = DiaryEntry(test_date, "Reflection content", entry_type="reflection")
153+
manager.write_entry(reflection)
154+
assert (diary_dir / "2025-10-12.md").exists()
155+
assert not (planner_dir / "2025-10-12.md").exists()
156+
157+
# Write plan entry
158+
plan = DiaryEntry(test_date, "Plan content", entry_type="plan")
159+
manager.write_entry(plan)
160+
assert (planner_dir / "2025-10-12-plan.md").exists()
161+
assert not (diary_dir / "2025-10-12-plan.md").exists()
162+
163+
def test_read_entry_from_separate_paths(self, temp_dir):
164+
"""Test reading entries from separate diary and planner paths."""
165+
diary_dir = temp_dir / "diary"
166+
planner_dir = temp_dir / "planner"
167+
diary_dir.mkdir()
168+
planner_dir.mkdir()
169+
170+
manager = EntryManager(diary_dir, planner_dir)
171+
test_date = date(2025, 10, 12)
172+
173+
# Write entries to separate directories
174+
(diary_dir / "2025-10-12.md").write_text("Reflection content")
175+
(planner_dir / "2025-10-12-plan.md").write_text("Plan content")
176+
177+
# Read reflection from diary_path
178+
reflection = manager.read_entry(test_date, entry_type="reflection")
179+
assert reflection is not None
180+
assert reflection.content == "Reflection content"
181+
182+
# Read plan from planner_path
183+
plan = manager.read_entry(test_date, entry_type="plan")
184+
assert plan is not None
185+
assert plan.content == "Plan content"
186+
113187
def test_entry_exists_reflection(self, temp_dir):
114188
"""Test entry_exists for reflection entry."""
115189
manager = EntryManager(temp_dir)

0 commit comments

Comments
 (0)