From 7efac77503364566f5fa0c8d5291eb8401f9e74f Mon Sep 17 00:00:00 2001 From: Richard Solomou Date: Sat, 14 Feb 2026 23:11:23 +0200 Subject: [PATCH] feat: expose embed command_id in note API responses (#545) * feat: expose embed command_id in note API responses Note.save() already returns the command_id from the embed_note background job, but the API routes discarded it. This surfaces the command_id in NoteResponse for both POST and PUT endpoints, enabling callers to poll GET /api/commands/jobs/{command_id} to know when embedding has completed. * Add tests for note API command_id response --- api/models.py | 1 + api/routers/notes.py | 6 ++- tests/test_notes_api.py | 115 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 120 insertions(+), 2 deletions(-) create mode 100644 tests/test_notes_api.py diff --git a/api/models.py b/api/models.py index b7c5ac8..7ea8967 100644 --- a/api/models.py +++ b/api/models.py @@ -193,6 +193,7 @@ class NoteResponse(BaseModel): note_type: Optional[str] created: str updated: str + command_id: Optional[str] = None # Embedding API models diff --git a/api/routers/notes.py b/api/routers/notes.py index d07cd34..4935057 100644 --- a/api/routers/notes.py +++ b/api/routers/notes.py @@ -78,7 +78,7 @@ async def create_note(note_data: NoteCreate): content=note_data.content, note_type=note_type, ) - await new_note.save() + command_id = await new_note.save() # Add to notebook if specified if note_data.notebook_id: @@ -96,6 +96,7 @@ async def create_note(note_data: NoteCreate): note_type=new_note.note_type, created=str(new_note.created), updated=str(new_note.updated), + command_id=command_id, ) except HTTPException: raise @@ -150,7 +151,7 @@ async def update_note(note_id: str, note_update: NoteUpdate): status_code=400, detail="note_type must be 'human' or 'ai'" ) - await note.save() + command_id = await note.save() return NoteResponse( id=note.id or "", @@ -159,6 +160,7 @@ async def update_note(note_id: str, note_update: NoteUpdate): note_type=note.note_type, created=str(note.created), updated=str(note.updated), + command_id=command_id, ) except HTTPException: raise diff --git a/tests/test_notes_api.py b/tests/test_notes_api.py new file mode 100644 index 0000000..eb11721 --- /dev/null +++ b/tests/test_notes_api.py @@ -0,0 +1,115 @@ +from unittest.mock import AsyncMock, patch + +import pytest +from fastapi.testclient import TestClient + + +@pytest.fixture +def client(): + """Create test client after environment variables have been cleared by conftest.""" + from api.main import app + + return TestClient(app) + + +class TestNoteCreation: + """Test suite for Note API endpoints.""" + + @patch("api.routers.notes.Note") + def test_create_note_returns_command_id(self, mock_note_cls, client): + """Test that creating a note returns the embed command_id.""" + mock_note = AsyncMock() + mock_note.id = "note:abc123" + mock_note.title = "Test Note" + mock_note.content = "Some content" + mock_note.note_type = "human" + mock_note.created = "2026-01-01T00:00:00Z" + mock_note.updated = "2026-01-01T00:00:00Z" + mock_note.save.return_value = "command:embed123" + mock_note.add_to_notebook = AsyncMock() + mock_note_cls.return_value = mock_note + + response = client.post( + "/api/notes", + json={"content": "Some content", "note_type": "human"}, + ) + + assert response.status_code == 200 + data = response.json() + assert data["command_id"] == "command:embed123" + assert data["id"] == "note:abc123" + + @patch("api.routers.notes.Note") + def test_create_note_command_id_none_when_no_content_embedding( + self, mock_note_cls, client + ): + """Test that command_id is None when save returns None (no embedding).""" + mock_note = AsyncMock() + mock_note.id = "note:abc456" + mock_note.title = "Empty Note" + mock_note.content = "Some content" + mock_note.note_type = "human" + mock_note.created = "2026-01-01T00:00:00Z" + mock_note.updated = "2026-01-01T00:00:00Z" + mock_note.save.return_value = None + mock_note.add_to_notebook = AsyncMock() + mock_note_cls.return_value = mock_note + + response = client.post( + "/api/notes", + json={"content": "Some content", "note_type": "human"}, + ) + + assert response.status_code == 200 + data = response.json() + assert data["command_id"] is None + + +class TestNoteUpdate: + """Test suite for Note update endpoint.""" + + @patch("api.routers.notes.Note") + def test_update_note_returns_command_id(self, mock_note_cls, client): + """Test that updating a note returns the embed command_id.""" + mock_note = AsyncMock() + mock_note.id = "note:abc123" + mock_note.title = "Test Note" + mock_note.content = "Original content" + mock_note.note_type = "human" + mock_note.created = "2026-01-01T00:00:00Z" + mock_note.updated = "2026-01-01T00:00:00Z" + mock_note.save.return_value = "command:embed789" + mock_note_cls.get = AsyncMock(return_value=mock_note) + + response = client.put( + "/api/notes/note:abc123", + json={"content": "Updated content"}, + ) + + assert response.status_code == 200 + data = response.json() + assert data["command_id"] == "command:embed789" + + @patch("api.routers.notes.Note") + def test_update_note_command_id_none_when_no_embedding( + self, mock_note_cls, client + ): + """Test that command_id is None on update when no embedding is triggered.""" + mock_note = AsyncMock() + mock_note.id = "note:abc123" + mock_note.title = "Test Note" + mock_note.content = "Some content" + mock_note.note_type = "human" + mock_note.created = "2026-01-01T00:00:00Z" + mock_note.updated = "2026-01-01T00:00:00Z" + mock_note.save.return_value = None + mock_note_cls.get = AsyncMock(return_value=mock_note) + + response = client.put( + "/api/notes/note:abc123", + json={"title": "Updated Title"}, + ) + + assert response.status_code == 200 + data = response.json() + assert data["command_id"] is None