From 362ead7f0de6aa2ca460bf4eed49910515dc9529 Mon Sep 17 00:00:00 2001 From: Bryan Van Deusen Date: Wed, 13 May 2026 12:11:03 -0400 Subject: [PATCH] feat(notes): accept and return description field through service and routes create_note service accepts a new description kwarg and forwards it to the Note constructor. PUT/PATCH/POST routes include description in the field whitelist. update_note already passed **fields through setattr, so the new column is reachable without touching that signature. --- src/fabledassistant/routes/notes.py | 5 +- src/fabledassistant/services/notes.py | 2 + tests/test_notes_description_field.py | 74 +++++++++++++++++++++++++++ 3 files changed, 79 insertions(+), 2 deletions(-) create mode 100644 tests/test_notes_description_field.py diff --git a/src/fabledassistant/routes/notes.py b/src/fabledassistant/routes/notes.py index 7b7a5b1..a515969 100644 --- a/src/fabledassistant/routes/notes.py +++ b/src/fabledassistant/routes/notes.py @@ -112,6 +112,7 @@ async def create_note_route(): uid, title=data.get("title", ""), body=body, + description=data.get("description"), tags=tags, parent_id=data.get("parent_id"), project_id=project_id, @@ -215,7 +216,7 @@ async def update_note_route(note_id: int): uid = get_current_user_id() data = await request.get_json() fields = {} - for key in ("title", "body", "parent_id", "project_id", "milestone_id", "status", "priority", "note_type"): + for key in ("title", "body", "description", "parent_id", "project_id", "milestone_id", "status", "priority", "note_type"): if key in data: fields[key] = data[key] if "metadata" in data: @@ -250,7 +251,7 @@ async def patch_note_route(note_id: int): uid = get_current_user_id() data = await request.get_json() fields = {} - for key in ("title", "body", "parent_id", "project_id", "milestone_id", "status", "priority", "note_type"): + for key in ("title", "body", "description", "parent_id", "project_id", "milestone_id", "status", "priority", "note_type"): if key in data: fields[key] = data[key] if "metadata" in data: diff --git a/src/fabledassistant/services/notes.py b/src/fabledassistant/services/notes.py index 0d46d96..0b64a41 100644 --- a/src/fabledassistant/services/notes.py +++ b/src/fabledassistant/services/notes.py @@ -76,6 +76,7 @@ async def create_note( user_id: int, title: str = "", body: str = "", + description: str | None = None, tags: list[str] | None = None, parent_id: int | None = None, project_id: int | None = None, @@ -103,6 +104,7 @@ async def create_note( user_id=user_id, title=title, body=body, + description=description, tags=_normalize_tags(tags or []), parent_id=parent_id, project_id=project_id, diff --git a/tests/test_notes_description_field.py b/tests/test_notes_description_field.py new file mode 100644 index 0000000..2167d83 --- /dev/null +++ b/tests/test_notes_description_field.py @@ -0,0 +1,74 @@ +"""Tests for the description-field roundtrip through the notes service. + +The description field is the user-stated goal/context on a task; distinct +from `body` which (post-Task-as-Durable-Record) becomes the LLM-maintained +consolidation summary. +""" +from unittest.mock import AsyncMock, MagicMock, patch + + +def _mock_session_for_update(mock_note): + mock_session = AsyncMock() + mock_result = MagicMock() + mock_result.scalars.return_value.first.return_value = mock_note + mock_session.execute = AsyncMock(return_value=mock_result) + mock_session.commit = AsyncMock() + mock_session.refresh = AsyncMock() + mock_session.__aenter__ = AsyncMock(return_value=mock_session) + mock_session.__aexit__ = AsyncMock(return_value=False) + return mock_session + + +async def test_update_note_persists_description(): + """update_note(description=...) should assign to note.description. + + update_note already accepts **fields and uses setattr, so this exercises + that the new model column is reachable through the existing dynamic-fields + path (no service-layer change required beyond the model column). + """ + mock_note = MagicMock() + # Pre-existing state — None description, no recurrence, status untouched. + mock_note.description = None + mock_note.status = None + mock_note.recurrence_rule = None + mock_note.project_id = None + + with patch( + "fabledassistant.services.notes.async_session", + return_value=_mock_session_for_update(mock_note), + ): + from fabledassistant.services.notes import update_note + await update_note(1, 1, description="the goal text") + + assert mock_note.description == "the goal text" + + +async def test_create_note_forwards_description_to_model(): + """create_note(description=...) should pass description into the Note + constructor so it gets persisted alongside title/body/etc.""" + captured: dict = {} + + class FakeNote: + def __init__(self, **kw): + captured.update(kw) + self.id = 1 + self.project_id = kw.get("project_id") + for k, v in kw.items(): + setattr(self, k, v) + + mock_session = AsyncMock() + mock_session.add = MagicMock() + mock_session.commit = AsyncMock() + mock_session.refresh = AsyncMock() + mock_session.__aenter__ = AsyncMock(return_value=mock_session) + mock_session.__aexit__ = AsyncMock(return_value=False) + + with patch( + "fabledassistant.services.notes.async_session", return_value=mock_session + ), patch("fabledassistant.services.notes.Note", FakeNote): + from fabledassistant.services.notes import create_note + await create_note( + user_id=1, title="renew cert", description="the goal text", + ) + + assert captured.get("description") == "the goal text"