-
Notifications
You must be signed in to change notification settings - Fork 196
Expand file tree
/
Copy pathtest_tool_read_note.py
More file actions
291 lines (231 loc) · 9.22 KB
/
test_tool_read_note.py
File metadata and controls
291 lines (231 loc) · 9.22 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
"""Tests for note tools that exercise the full stack with SQLite."""
from textwrap import dedent
import pytest
from basic_memory.mcp.tools import write_note, read_note
import pytest_asyncio
from unittest.mock import MagicMock, patch
from basic_memory.schemas.search import SearchResponse, SearchItemType
@pytest_asyncio.fixture
async def mock_call_get():
"""Mock for call_get to simulate different responses."""
with patch("basic_memory.mcp.tools.read_note.call_get") as mock:
# Default to 404 - not found
mock_response = MagicMock()
mock_response.status_code = 404
mock.return_value = mock_response
yield mock
@pytest_asyncio.fixture
async def mock_search():
"""Mock for search tool."""
with patch("basic_memory.mcp.tools.read_note.search_notes") as mock:
# Default to empty results
mock.return_value = SearchResponse(results=[], current_page=1, page_size=1)
yield mock
@pytest.mark.asyncio
async def test_read_note_by_title(app):
"""Test reading a note by its title."""
# First create a note
await write_note(title="Special Note", folder="test", content="Note content here")
# Should be able to read it by title
content = await read_note("Special Note")
assert "Note content here" in content
@pytest.mark.asyncio
async def test_note_unicode_content(app):
"""Test handling of unicode content in"""
content = "# Test 🚀\nThis note has emoji 🎉 and unicode ♠♣♥♦"
result = await write_note(title="Unicode Test", folder="test", content=content)
assert (
dedent("""
# Created test/Unicode Test.md (272389cd)
permalink: test/unicode-test
""").strip()
in result
)
# Read back should preserve unicode
result = await read_note("test/unicode-test")
assert content in result
@pytest.mark.asyncio
async def test_multiple_notes(app):
"""Test creating and managing multiple"""
# Create several notes
notes_data = [
("test/note-1", "Note 1", "test", "Content 1", ["tag1"]),
("test/note-2", "Note 2", "test", "Content 2", ["tag1", "tag2"]),
("test/note-3", "Note 3", "test", "Content 3", []),
]
for _, title, folder, content, tags in notes_data:
await write_note(title=title, folder=folder, content=content, tags=tags)
# Should be able to read each one
for permalink, title, folder, content, _ in notes_data:
note = await read_note(permalink)
assert content in note
# read multiple notes at once
result = await read_note("test/*")
# note we can't compare times
assert "--- memory://test/note-1" in result
assert "Content 1" in result
assert "--- memory://test/note-2" in result
assert "Content 2" in result
assert "--- memory://test/note-3" in result
assert "Content 3" in result
@pytest.mark.asyncio
async def test_multiple_notes_pagination(app):
"""Test creating and managing multiple"""
# Create several notes
notes_data = [
("test/note-1", "Note 1", "test", "Content 1", ["tag1"]),
("test/note-2", "Note 2", "test", "Content 2", ["tag1", "tag2"]),
("test/note-3", "Note 3", "test", "Content 3", []),
]
for _, title, folder, content, tags in notes_data:
await write_note(title=title, folder=folder, content=content, tags=tags)
# Should be able to read each one
for permalink, title, folder, content, _ in notes_data:
note = await read_note(permalink)
assert content in note
# read multiple notes at once with pagination
result = await read_note("test/*", page=1, page_size=2)
# note we can't compare times
assert "--- memory://test/note-1" in result
assert "Content 1" in result
assert "--- memory://test/note-2" in result
assert "Content 2" in result
@pytest.mark.asyncio
async def test_read_note_memory_url(app):
"""Test reading a note using a memory:// URL.
Should:
- Handle memory:// URLs correctly
- Normalize the URL before resolving
- Return the note content
"""
# First create a note
result = await write_note(
title="Memory URL Test",
folder="test",
content="Testing memory:// URL handling",
)
assert result
# Should be able to read it with a memory:// URL
memory_url = "memory://test/memory-url-test"
content = await read_note(memory_url)
assert "Testing memory:// URL handling" in content
@pytest.mark.asyncio
async def test_read_note_direct_success(mock_call_get):
"""Test read_note with successful direct permalink lookup."""
# Setup mock for successful response
mock_response = MagicMock()
mock_response.status_code = 200
mock_response.text = "# Test Note\n\nThis is a test note."
mock_call_get.return_value = mock_response
# Call the function
result = await read_note("test/test-note")
# Verify direct lookup was used
mock_call_get.assert_called_once()
assert "test/test-note" in mock_call_get.call_args[0][1]
# Verify result
assert "# Test Note" in result
assert "This is a test note." in result
@pytest.mark.asyncio
async def test_read_note_title_search_fallback(mock_call_get, mock_search):
"""Test read_note falls back to title search when direct lookup fails."""
# Setup mock for failed direct lookup
mock_call_get.side_effect = [
# First call fails (direct lookup)
MagicMock(status_code=404),
# Second call succeeds (after title search)
MagicMock(status_code=200, text="# Test Note\n\nThis is a test note."),
]
# Setup mock for successful title search
mock_search.return_value = SearchResponse(
results=[
{
"id": 1,
"entity": "test/test-note",
"title": "Test Note",
"type": SearchItemType.ENTITY,
"permalink": "test/test-note",
"file_path": "test/test-note.md",
"score": 1.0,
}
],
current_page=1,
page_size=1,
)
# Call the function
result = await read_note("Test Note")
# Verify title search was used
mock_search.assert_called_once()
assert mock_search.call_args[0][0].title == "Test Note"
# Verify second lookup was used
assert mock_call_get.call_count == 2
assert "test/test-note" in mock_call_get.call_args[0][1]
# Verify result
assert "# Test Note" in result
assert "This is a test note." in result
@pytest.mark.asyncio
async def test_read_note_text_search_fallback(mock_call_get, mock_search):
"""Test read_note falls back to text search and returns related results."""
# Setup mock for failed direct and title lookups
mock_call_get.return_value = MagicMock(status_code=404)
# Setup mock for failed title search but successful text search
mock_search.side_effect = [
# First call (title search) returns no results
SearchResponse(results=[], current_page=1, page_size=1),
# Second call (text search) returns results
SearchResponse(
results=[
{
"id": 1,
"title": "Related Note 1",
"entity": "notes/related-note-1",
"type": SearchItemType.ENTITY,
"permalink": "notes/related-note-1",
"file_path": "notes/related-note-1.md",
"score": 0.8,
},
{
"id": 2,
"title": "Related Note 2",
"entity": "notes/related-note-2",
"type": SearchItemType.ENTITY,
"permalink": "notes/related-note-2",
"file_path": "notes/related-note-2.md",
"score": 0.7,
},
],
current_page=1,
page_size=1,
),
]
# Call the function
result = await read_note("some query")
# Verify both search types were used
assert mock_search.call_count == 2
assert mock_search.call_args_list[0][0][0].title == "some query" # Title search
assert mock_search.call_args_list[1][0][0].text == "some query" # Text search
# Verify result contains helpful information
assert "Note Not Found" in result
assert "Related Note 1" in result
assert "Related Note 2" in result
assert 'read_note("notes/related-note-1")' in result
assert "search(query=" in result
assert "write_note(" in result
@pytest.mark.asyncio
async def test_read_note_complete_fallback(mock_call_get, mock_search):
"""Test read_note with all lookups failing."""
# Setup mock for failed direct lookup
mock_call_get.return_value = MagicMock(status_code=404)
# Setup mock for failed searches
mock_search.return_value = SearchResponse(results=[], current_page=1, page_size=1)
# Call the function
result = await read_note("nonexistent")
# Verify search was used
assert mock_search.call_count == 2
# Verify result contains helpful guidance
assert "Note Not Found" in result
assert "nonexistent" in result
assert "Check Identifier Type" in result
assert "Search Instead" in result
assert "Recent Activity" in result
assert "Create New Note" in result
assert "write_note(" in result