Add MongoDB-backed NotesTool with CRUD actions and unit tests (#1982)

* Add MongoDB-backed NotesTool with CRUD actions and unit tests

* NotesTool: enforce single note per user, require decoded_token['sub'] user_id, fix tests

* chore: remove accidentally committed results.txt and ignore it

* fix: remove results.txt, enforce single note per user, add tests

* refactor: update NotesTool actions and tests for clarity and consistency

* refactor: update NotesTool docstring for clarity

* refactor: simplify MCPTool docstring and remove redundant import in test_notes_tool

* lint: fix test

* refactor: remove unused import from test_notes_tool.py

---------

Co-authored-by: Alex <a@tushynski.me>
This commit is contained in:
Dhairya Parikh
2025-10-06 12:24:03 -04:00
committed by GitHub
parent 7cfc230316
commit 4c31e9a8b1
5 changed files with 323 additions and 2 deletions

133
tests/test_notes_tool.py Normal file
View File

@@ -0,0 +1,133 @@
import pytest
from application.agents.tools.notes import NotesTool
from application.core.settings import settings
@pytest.fixture
def notes_tool(monkeypatch) -> NotesTool:
"""Provide a NotesTool with a fake Mongo collection and fixed user_id."""
class FakeCollection:
def __init__(self) -> None:
self.doc = None # single note per user
def update_one(self, q, u, upsert=False):
# emulate single-note storage with optional upsert
if self.doc is None and not upsert:
return type("res", (), {"modified_count": 0})
if self.doc is None and upsert:
self.doc = {"user_id": q["user_id"], "note": ""}
if "$set" in u and "note" in u["$set"]:
self.doc["note"] = u["$set"]["note"]
return type("res", (), {"modified_count": 1})
def find_one(self, q):
if self.doc and self.doc.get("user_id") == q.get("user_id"):
return self.doc
return None
def delete_one(self, q):
if self.doc and self.doc.get("user_id") == q.get("user_id"):
self.doc = None
return type("res", (), {"deleted_count": 1})
return type("res", (), {"deleted_count": 0})
fake_collection = FakeCollection()
fake_db = {"notes": fake_collection}
fake_client = {settings.MONGO_DB_NAME: fake_db}
# Patch MongoDB client globally for the tool
monkeypatch.setattr("application.core.mongo_db.MongoDB.get_client", lambda: fake_client)
# ToolManager will pass user_id in production; in tests we pass it directly
return NotesTool({}, user_id="test_user")
def test_view(notes_tool: NotesTool) -> None:
# Manually insert a note to test retrieval
notes_tool.collection.update_one(
{"user_id": "test_user"},
{"$set": {"note": "hello"}},
upsert=True
)
assert "hello" in notes_tool.execute_action("view")
def test_overwrite_and_delete(notes_tool: NotesTool) -> None:
# Overwrite creates a new note
assert "saved" in notes_tool.execute_action("overwrite", text="first").lower()
assert "first" in notes_tool.execute_action("view")
# Overwrite replaces existing note
assert "saved" in notes_tool.execute_action("overwrite", text="second").lower()
assert "second" in notes_tool.execute_action("view")
assert "deleted" in notes_tool.execute_action("delete").lower()
assert "no note" in notes_tool.execute_action("view").lower()
def test_init_without_user_id(monkeypatch):
"""Should fail gracefully if no user_id is provided."""
notes_tool = NotesTool(tool_config={})
result = notes_tool.execute_action("view")
assert "user_id" in str(result).lower()
def test_view_not_found(notes_tool: NotesTool) -> None:
"""Should return 'No note found.' when no note exists"""
result = notes_tool.execute_action("view")
assert "no note found" in result.lower()
def test_str_replace(notes_tool: NotesTool) -> None:
"""Test string replacement in note"""
# Create a note
notes_tool.execute_action("overwrite", text="Hello world, hello universe")
# Replace text
result = notes_tool.execute_action("str_replace", old_str="hello", new_str="hi")
assert "updated" in result.lower()
# Verify replacement
note = notes_tool.execute_action("view")
assert "hi world, hi universe" in note.lower()
def test_str_replace_not_found(notes_tool: NotesTool) -> None:
"""Test string replacement when string not found"""
notes_tool.execute_action("overwrite", text="Hello world")
result = notes_tool.execute_action("str_replace", old_str="goodbye", new_str="hi")
assert "not found" in result.lower()
def test_insert_line(notes_tool: NotesTool) -> None:
"""Test inserting text at a line number"""
# Create a multiline note
notes_tool.execute_action("overwrite", text="Line 1\nLine 2\nLine 3")
# Insert at line 2
result = notes_tool.execute_action("insert", line_number=2, text="Inserted line")
assert "inserted" in result.lower()
# Verify insertion
note = notes_tool.execute_action("view")
lines = note.split("\n")
assert lines[1] == "Inserted line"
assert lines[2] == "Line 2"
def test_delete_nonexistent_note(monkeypatch):
class FakeResult:
deleted_count = 0
class FakeCollection:
def delete_one(self, *args, **kwargs):
return FakeResult()
monkeypatch.setattr(
"application.core.mongo_db.MongoDB.get_client",
lambda: {"docsgpt": {"notes": FakeCollection()}}
)
notes_tool = NotesTool(tool_config={}, user_id="user123")
result = notes_tool.execute_action("delete")
assert "no note found" in result.lower()