-
Notifications
You must be signed in to change notification settings - Fork 188
Expand file tree
/
Copy pathtest_tool_edit_note.py
More file actions
831 lines (694 loc) · 28.2 KB
/
test_tool_edit_note.py
File metadata and controls
831 lines (694 loc) · 28.2 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
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
"""Tests for the edit_note MCP tool."""
from unittest.mock import patch
import pytest
from basic_memory.mcp.tools.edit_note import edit_note
from basic_memory.mcp.tools.read_note import read_note
from basic_memory.mcp.tools.write_note import write_note
@pytest.mark.asyncio
async def test_edit_note_append_operation(client, test_project):
"""Test appending content to an existing note."""
# Create initial note
await write_note(
project=test_project.name,
title="Test Note",
directory="test",
content="# Test Note\nOriginal content here.",
)
# Append content
result = await edit_note(
project=test_project.name,
identifier="test/test-note",
operation="append",
content="\n## New Section\nAppended content here.",
)
assert isinstance(result, str)
assert "Edited note (append)" in result
assert f"project: {test_project.name}" in result
assert "file_path: test/Test Note.md" in result
assert f"permalink: {test_project.name}/test/test-note" in result
assert "Added 3 lines to end of note" in result
assert f"[Session: Using project '{test_project.name}']" in result
@pytest.mark.asyncio
async def test_edit_note_prepend_operation(client, test_project):
"""Test prepending content to an existing note."""
# Create initial note
await write_note(
project=test_project.name,
title="Meeting Notes",
directory="meetings",
content="# Meeting Notes\nExisting content.",
)
# Prepend content
result = await edit_note(
project=test_project.name,
identifier="meetings/meeting-notes",
operation="prepend",
content="## 2025-05-25 Update\nNew meeting notes.\n",
)
assert isinstance(result, str)
assert "Edited note (prepend)" in result
assert f"project: {test_project.name}" in result
assert "file_path: meetings/Meeting Notes.md" in result
assert f"permalink: {test_project.name}/meetings/meeting-notes" in result
assert "Added 3 lines to beginning of note" in result
assert f"[Session: Using project '{test_project.name}']" in result
@pytest.mark.asyncio
async def test_edit_note_find_replace_operation(client, test_project):
"""Test find and replace operation."""
# Create initial note with version info
await write_note(
project=test_project.name,
title="Config Document",
directory="config",
content="# Configuration\nVersion: v0.12.0\nSettings for v0.12.0 release.",
)
# Replace version - expecting 2 replacements
result = await edit_note(
project=test_project.name,
identifier="config/config-document",
operation="find_replace",
content="v0.13.0",
find_text="v0.12.0",
expected_replacements=2,
)
assert isinstance(result, str)
assert "Edited note (find_replace)" in result
assert f"project: {test_project.name}" in result
assert "file_path: config/Config Document.md" in result
assert "operation: Find and replace operation completed" in result
assert f"[Session: Using project '{test_project.name}']" in result
@pytest.mark.asyncio
async def test_edit_note_replace_section_operation(client, test_project):
"""Test replacing content under a specific section."""
# Create initial note with sections
await write_note(
project=test_project.name,
title="API Specification",
directory="specs",
content="# API Spec\n\n## Overview\nAPI overview here.\n\n## Implementation\nOld implementation details.\n\n## Testing\nTest info here.",
)
# Replace implementation section
result = await edit_note(
project=test_project.name,
identifier="specs/api-specification",
operation="replace_section",
content="New implementation approach using FastAPI.\nImproved error handling.\n",
section="## Implementation",
)
assert isinstance(result, str)
assert "Edited note (replace_section)" in result
assert f"project: {test_project.name}" in result
assert "file_path: specs/API Specification.md" in result
assert "Replaced content under section '## Implementation'" in result
assert f"[Session: Using project '{test_project.name}']" in result
@pytest.mark.asyncio
async def test_edit_note_nonexistent_note_find_replace(client, test_project):
"""Test find_replace on a note that doesn't exist - should return helpful guidance."""
result = await edit_note(
project=test_project.name,
identifier="nonexistent/note",
operation="find_replace",
content="replacement",
find_text="old text",
)
assert isinstance(result, str)
assert "# Edit Failed" in result
assert "search_notes" in result # Should suggest searching
assert "append" in result # Should suggest using append/prepend instead
@pytest.mark.asyncio
async def test_edit_note_nonexistent_note_replace_section(client, test_project):
"""Test replace_section on a note that doesn't exist - should return helpful guidance."""
result = await edit_note(
project=test_project.name,
identifier="nonexistent/note",
operation="replace_section",
content="new section content",
section="## Missing Section",
)
assert isinstance(result, str)
assert "# Edit Failed" in result
assert "search_notes" in result # Should suggest searching
@pytest.mark.asyncio
async def test_edit_note_append_creates_note_if_not_found(client, test_project):
"""append to a non-existent note should create it automatically."""
result = await edit_note(
project=test_project.name,
identifier="auto-created-note",
operation="append",
content="# New Note\n\nCreated via append.",
)
assert isinstance(result, str)
assert "Created note (append)" in result
assert "fileCreated: true" in result
assert f"project: {test_project.name}" in result
@pytest.mark.asyncio
async def test_edit_note_prepend_creates_note_if_not_found(client, test_project):
"""prepend to a non-existent note should create it automatically."""
result = await edit_note(
project=test_project.name,
identifier="auto-created-prepend",
operation="prepend",
content="# Prepended Note\n\nCreated via prepend.",
)
assert isinstance(result, str)
assert "Created note (prepend)" in result
assert "fileCreated: true" in result
assert f"project: {test_project.name}" in result
@pytest.mark.asyncio
async def test_edit_note_append_creates_with_directory_from_identifier(client, test_project):
"""Identifier 'conversations/my-note' should create in conversations/ directory."""
result = await edit_note(
project=test_project.name,
identifier="conversations/my-note",
operation="append",
content="# My Note\n\nCreated in conversations directory.",
)
assert isinstance(result, str)
assert "Created note (append)" in result
assert "fileCreated: true" in result
assert "conversations/" in result
@pytest.mark.asyncio
async def test_edit_note_append_creates_at_root_when_no_directory(client, test_project):
"""Identifier 'my-note' (no slash) should create at project root."""
result = await edit_note(
project=test_project.name,
identifier="root-level-note",
operation="append",
content="# Root Note\n\nCreated at root.",
)
assert isinstance(result, str)
assert "Created note (append)" in result
assert "fileCreated: true" in result
@pytest.mark.asyncio
async def test_edit_note_append_creates_json_format(client, test_project):
"""JSON output should include fileCreated: true when note is auto-created."""
result = await edit_note(
project=test_project.name,
identifier="json-auto-create",
operation="append",
content="# JSON Test\n\nAuto-created.",
output_format="json",
)
assert isinstance(result, dict)
assert result["fileCreated"] is True
assert result["title"] is not None
assert result["operation"] == "append"
@pytest.mark.asyncio
async def test_edit_note_existing_note_json_includes_file_created_false(client, test_project):
"""JSON output for editing an existing note should include fileCreated: false."""
# Create the note first
await write_note(
project=test_project.name,
title="Existing JSON Note",
directory="test",
content="# Existing Note\nOriginal content.",
)
result = await edit_note(
project=test_project.name,
identifier="test/existing-json-note",
operation="append",
content="\nAppended content.",
output_format="json",
)
assert isinstance(result, dict)
assert result["fileCreated"] is False
assert result["title"] == "Existing JSON Note"
assert result["operation"] == "append"
@pytest.mark.asyncio
async def test_edit_note_invalid_operation(client, test_project):
"""Test using an invalid operation."""
# Create a note first
await write_note(
project=test_project.name,
title="Test Note",
directory="test",
content="# Test\nContent here.",
)
with pytest.raises(ValueError) as exc_info:
await edit_note(
project=test_project.name,
identifier="test/test-note",
operation="invalid_op",
content="Some content",
)
assert "Invalid operation 'invalid_op'" in str(exc_info.value)
@pytest.mark.asyncio
async def test_edit_note_find_replace_missing_find_text(client, test_project):
"""Test find_replace operation without find_text parameter."""
# Create a note first
await write_note(
project=test_project.name,
title="Test Note",
directory="test",
content="# Test\nContent here.",
)
with pytest.raises(ValueError) as exc_info:
await edit_note(
project=test_project.name,
identifier="test/test-note",
operation="find_replace",
content="replacement",
)
assert "find_text parameter is required for find_replace operation" in str(exc_info.value)
@pytest.mark.asyncio
async def test_edit_note_replace_section_missing_section(client, test_project):
"""Test replace_section operation without section parameter."""
# Create a note first
await write_note(
project=test_project.name,
title="Test Note",
directory="test",
content="# Test\nContent here.",
)
with pytest.raises(ValueError) as exc_info:
await edit_note(
project=test_project.name,
identifier="test/test-note",
operation="replace_section",
content="new content",
)
assert "section parameter is required for section-based operations" in str(exc_info.value)
@pytest.mark.asyncio
async def test_edit_note_replace_section_nonexistent_section(client, test_project):
"""Test replacing a section that doesn't exist - should append it."""
# Create initial note without the target section
await write_note(
project=test_project.name,
title="Document",
directory="docs",
content="# Document\n\n## Existing Section\nSome content here.",
)
# Try to replace non-existent section
result = await edit_note(
project=test_project.name,
identifier="docs/document",
operation="replace_section",
content="New section content here.\n",
section="## New Section",
)
assert isinstance(result, str)
assert "Edited note (replace_section)" in result
assert f"project: {test_project.name}" in result
assert "file_path: docs/Document.md" in result
assert f"[Session: Using project '{test_project.name}']" in result
# Should succeed - the section gets appended if it doesn't exist
@pytest.mark.asyncio
async def test_edit_note_with_observations_and_relations(client, test_project):
"""Test editing a note that contains observations and relations."""
# Create note with semantic content
await write_note(
project=test_project.name,
title="Feature Spec",
directory="features",
content="# Feature Spec\n\n- [design] Initial design thoughts #architecture\n- implements [[Base System]]\n\nOriginal content.",
)
# Append more semantic content
result = await edit_note(
project=test_project.name,
identifier="features/feature-spec",
operation="append",
content="\n## Updates\n\n- [implementation] Added new feature #development\n- relates_to [[User Guide]]",
)
assert isinstance(result, str)
assert "Edited note (append)" in result
assert "## Observations" in result
assert "## Relations" in result
@pytest.mark.asyncio
async def test_edit_note_identifier_variations(client, test_project):
"""Test that various identifier formats work."""
# Create a note
await write_note(
project=test_project.name,
title="Test Document",
directory="docs",
content="# Test Document\nOriginal content.",
)
# Test different identifier formats
identifiers_to_test = [
"docs/test-document", # permalink
"Test Document", # title
"docs/Test Document", # folder/title
]
for identifier in identifiers_to_test:
result = await edit_note(
project=test_project.name,
identifier=identifier,
operation="append",
content=f"\n## Update via {identifier}",
)
assert isinstance(result, str)
assert "Edited note (append)" in result
assert f"project: {test_project.name}" in result
assert "file_path: docs/Test Document.md" in result
@pytest.mark.asyncio
async def test_edit_note_find_replace_no_matches(client, test_project):
"""Test find_replace when the find_text doesn't exist - should return error."""
# Create initial note
await write_note(
project=test_project.name,
title="Test Note",
directory="test",
content="# Test Note\nSome content here.",
)
# Try to replace text that doesn't exist - should fail with default expected_replacements=1
result = await edit_note(
project=test_project.name,
identifier="test/test-note",
operation="find_replace",
content="replacement",
find_text="nonexistent_text",
)
assert isinstance(result, str)
assert "# Edit Failed - Text Not Found" in result
assert "read_note" in result # Should suggest reading the note first
assert "Alternative approaches" in result # Should suggest alternatives
@pytest.mark.asyncio
async def test_edit_note_empty_content_operations(client, test_project):
"""Test operations with empty content."""
# Create initial note
await write_note(
project=test_project.name,
title="Test Note",
directory="test",
content="# Test Note\nOriginal content.",
)
# Test append with empty content
result = await edit_note(
project=test_project.name, identifier="test/test-note", operation="append", content=""
)
assert isinstance(result, str)
assert "Edited note (append)" in result
# Should still work, just adding empty content
@pytest.mark.asyncio
async def test_edit_note_find_replace_wrong_count(client, test_project):
"""Test find_replace when replacement count doesn't match expected."""
# Create initial note with version info
await write_note(
project=test_project.name,
title="Config Document",
directory="config",
content="# Configuration\nVersion: v0.12.0\nSettings for v0.12.0 release.",
)
# Try to replace expecting 1 occurrence, but there are actually 2
result = await edit_note(
project=test_project.name,
identifier="config/config-document",
operation="find_replace",
content="v0.13.0",
find_text="v0.12.0",
expected_replacements=1, # Wrong! There are actually 2 occurrences
)
assert isinstance(result, str)
assert "# Edit Failed - Wrong Replacement Count" in result
assert "Expected 1 occurrences" in result
assert "but found 2" in result
assert "Update expected_replacements" in result # Should suggest the fix
assert "expected_replacements=2" in result # Should suggest the exact fix
@pytest.mark.asyncio
async def test_edit_note_replace_section_multiple_sections(client, test_project):
"""Test replace_section with multiple sections having same header - should return helpful error."""
# Create note with duplicate section headers
await write_note(
project=test_project.name,
title="Sample Note",
directory="docs",
content="# Main Title\n\n## Section 1\nFirst instance\n\n## Section 2\nSome content\n\n## Section 1\nSecond instance",
)
# Try to replace section when multiple exist
result = await edit_note(
project=test_project.name,
identifier="docs/sample-note",
operation="replace_section",
content="New content",
section="## Section 1",
)
assert isinstance(result, str)
assert "# Edit Failed - Duplicate Section Headers" in result
assert "Multiple sections found" in result
assert "read_note" in result # Should suggest reading the note first
assert "Make headers unique" in result # Should suggest making headers unique
@pytest.mark.asyncio
async def test_edit_note_find_replace_empty_find_text(client, test_project):
"""Test find_replace with empty/whitespace find_text - should return helpful error."""
# Create initial note
await write_note(
project=test_project.name,
title="Test Note",
directory="test",
content="# Test Note\nSome content here.",
)
# Try with whitespace-only find_text - this should be caught by service validation
result = await edit_note(
project=test_project.name,
identifier="test/test-note",
operation="find_replace",
content="replacement",
find_text=" ", # whitespace only
)
assert isinstance(result, str)
assert "# Edit Failed" in result
# Should contain helpful guidance about the error
@pytest.mark.asyncio
async def test_edit_note_append_with_null_optional_fields(client, test_project):
"""Regression test: MCP clients may send explicit null for unused optional fields.
When an MCP client sends find_text=None, section=None, expected_replacements=None
for an append operation, the tool should accept them without validation errors.
"""
# Create initial note
await write_note(
project=test_project.name,
title="Null Fields Test",
directory="test",
content="# Null Fields Test\nOriginal content.",
)
# Call edit_note with explicit None for all optional fields (simulates MCP null)
result = await edit_note(
project=test_project.name,
identifier="test/null-fields-test",
operation="append",
content="\nAppended content.",
find_text=None,
section=None,
expected_replacements=None,
)
assert isinstance(result, str)
assert "Edited note (append)" in result
assert f"project: {test_project.name}" in result
assert "file_path: test/Null Fields Test.md" in result
assert f"[Session: Using project '{test_project.name}']" in result
@pytest.mark.asyncio
async def test_edit_note_preserves_permalink_when_frontmatter_missing(client, test_project):
"""Test that editing a note preserves the permalink when frontmatter doesn't contain one.
This is a regression test for issue #170 where edit_note would fail with a validation error
because the permalink was being set to None when the markdown file didn't have a permalink
in its frontmatter.
"""
# Create initial note
await write_note(
project=test_project.name,
title="Test Note",
directory="test",
content="# Test Note\nOriginal content here.",
)
# Verify the note was created with a permalink
first_result = await edit_note(
project=test_project.name,
identifier="test/test-note",
operation="append",
content="\nFirst edit.",
)
assert isinstance(first_result, str)
assert f"permalink: {test_project.name}/test/test-note" in first_result
# Perform another edit - this should preserve the permalink even if the
# file doesn't have a permalink in its frontmatter
second_result = await edit_note(
project=test_project.name,
identifier="test/test-note",
operation="append",
content="\nSecond edit.",
)
assert isinstance(second_result, str)
assert "Edited note (append)" in second_result
assert f"project: {test_project.name}" in second_result
assert f"permalink: {test_project.name}/test/test-note" in second_result
assert f"[Session: Using project '{test_project.name}']" in second_result
# The edit should succeed without validation errors
@pytest.mark.asyncio
async def test_edit_note_find_replace_rejects_fuzzy_match(client, test_project):
"""find_replace must reject nonexistent identifiers, not fuzzy-match to a similar note."""
# Create two notes that could be fuzzy-matched
await write_note(
project=test_project.name,
title="Routing Test A",
directory="test",
content="# Routing Test A\nContent A.",
)
await write_note(
project=test_project.name,
title="Routing Test B",
directory="test",
content="# Routing Test B\nContent B.",
)
# Attempt to edit a nonexistent note — should error, not silently edit A or B
result = await edit_note(
project=test_project.name,
identifier="Routing Test NONEXISTENT",
operation="find_replace",
content="replaced",
find_text="Content",
)
assert isinstance(result, str)
assert "# Edit Failed" in result
# Verify neither A nor B was modified
content_a = await read_note("Routing Test A", project=test_project.name)
assert "Content A" in content_a
content_b = await read_note("Routing Test B", project=test_project.name)
assert "Content B" in content_b
@pytest.mark.asyncio
async def test_edit_note_append_autocreate_not_fuzzy_match(client, test_project):
"""append to a nonexistent note should auto-create it, not fuzzy-match an existing note."""
await write_note(
project=test_project.name,
title="Existing Note Alpha",
directory="test",
content="# Existing Note Alpha\nOriginal content.",
)
# Append to a nonexistent note — should create a new note, not edit "Existing Note Alpha"
result = await edit_note(
project=test_project.name,
identifier="Existing Note ZZZZZ",
operation="append",
content="# New Note\nBrand new content.",
)
assert isinstance(result, str)
assert "Created note (append)" in result
assert "fileCreated: true" in result
# Verify original note was NOT modified
content = await read_note("Existing Note Alpha", project=test_project.name)
assert "Original content" in content
assert "Brand new content" not in content
@pytest.mark.asyncio
async def test_edit_note_insert_before_section_operation(client, test_project):
"""Test inserting content before a section heading."""
# Create initial note with sections
await write_note(
project=test_project.name,
title="Insert Before Doc",
directory="docs",
content="# Doc\n\n## Overview\nOverview content.\n\n## Details\nDetail content.",
)
result = await edit_note(
project=test_project.name,
identifier="docs/insert-before-doc",
operation="insert_before_section",
content="--- inserted divider ---",
section="## Details",
)
assert isinstance(result, str)
assert "Edited note (insert_before_section)" in result
assert f"project: {test_project.name}" in result
assert "Inserted content before section '## Details'" in result
assert f"[Session: Using project '{test_project.name}']" in result
@pytest.mark.asyncio
async def test_edit_note_insert_after_section_operation(client, test_project):
"""Test inserting content after a section heading."""
# Create initial note with sections
await write_note(
project=test_project.name,
title="Insert After Doc",
directory="docs",
content="# Doc\n\n## Overview\nOverview content.\n\n## Details\nDetail content.",
)
result = await edit_note(
project=test_project.name,
identifier="docs/insert-after-doc",
operation="insert_after_section",
content="Inserted after overview heading",
section="## Overview",
)
assert isinstance(result, str)
assert "Edited note (insert_after_section)" in result
assert f"project: {test_project.name}" in result
assert "Inserted content after section '## Overview'" in result
assert f"[Session: Using project '{test_project.name}']" in result
@pytest.mark.asyncio
async def test_edit_note_insert_before_section_missing_section(client, test_project):
"""Test insert_before_section without section parameter raises ValueError."""
await write_note(
project=test_project.name,
title="Test Note",
directory="test",
content="# Test\nContent here.",
)
with pytest.raises(ValueError, match="section parameter is required"):
await edit_note(
project=test_project.name,
identifier="test/test-note",
operation="insert_before_section",
content="new content",
)
@pytest.mark.asyncio
async def test_edit_note_insert_before_section_not_found(client, test_project):
"""Test insert_before_section when section doesn't exist returns error."""
await write_note(
project=test_project.name,
title="Test Note",
directory="test",
content="# Test\n\n## Existing\nContent here.",
)
result = await edit_note(
project=test_project.name,
identifier="test/test-note",
operation="insert_before_section",
content="new content",
section="## Nonexistent",
)
assert isinstance(result, str)
assert "# Edit Failed" in result
@pytest.mark.asyncio
async def test_edit_note_detects_project_from_memory_url(client, test_project):
"""edit_note should detect project from memory:// URL prefix when project=None."""
# Create a note first
await write_note(
project=test_project.name,
title="URL Detection Note",
directory="test",
content="# URL Detection Note\nOriginal content.",
)
# Edit using memory:// URL with project=None — should auto-detect project
# The memory URL uses the permalink (which includes project prefix)
result = await edit_note(
identifier=f"memory://{test_project.name}/test/url-detection-note",
operation="append",
content="\nAppended via memory URL.",
project=None,
)
assert isinstance(result, str)
# Should route to the correct project and succeed (either edit or create)
assert f"project: {test_project.name}" in result
@pytest.mark.asyncio
async def test_edit_note_skips_detection_for_plain_path(client, test_project):
"""edit_note should NOT call detect_project_from_url_prefix for plain path identifiers.
A plain path like 'research/note' should not be misrouted to a project
named 'research' — the 'research' segment is a directory, not a project.
"""
with patch("basic_memory.mcp.tools.edit_note.detect_project_from_url_prefix") as mock_detect:
# Use a plain path (no memory:// prefix) — detection should not be called
await edit_note(
identifier="test/some-note",
operation="append",
content="content",
project=None,
)
mock_detect.assert_not_called()
@pytest.mark.asyncio
async def test_edit_note_skips_detection_when_project_provided(client, test_project):
"""edit_note should skip URL detection when project is explicitly provided."""
with patch("basic_memory.mcp.tools.edit_note.detect_project_from_url_prefix") as mock_detect:
await edit_note(
identifier=f"memory://{test_project.name}/test/some-note",
operation="append",
content="content",
project=test_project.name,
)
mock_detect.assert_not_called()