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
This commit is contained in:
parent
db094da10a
commit
7efac77503
3 changed files with 120 additions and 2 deletions
|
|
@ -193,6 +193,7 @@ class NoteResponse(BaseModel):
|
||||||
note_type: Optional[str]
|
note_type: Optional[str]
|
||||||
created: str
|
created: str
|
||||||
updated: str
|
updated: str
|
||||||
|
command_id: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
# Embedding API models
|
# Embedding API models
|
||||||
|
|
|
||||||
|
|
@ -78,7 +78,7 @@ async def create_note(note_data: NoteCreate):
|
||||||
content=note_data.content,
|
content=note_data.content,
|
||||||
note_type=note_type,
|
note_type=note_type,
|
||||||
)
|
)
|
||||||
await new_note.save()
|
command_id = await new_note.save()
|
||||||
|
|
||||||
# Add to notebook if specified
|
# Add to notebook if specified
|
||||||
if note_data.notebook_id:
|
if note_data.notebook_id:
|
||||||
|
|
@ -96,6 +96,7 @@ async def create_note(note_data: NoteCreate):
|
||||||
note_type=new_note.note_type,
|
note_type=new_note.note_type,
|
||||||
created=str(new_note.created),
|
created=str(new_note.created),
|
||||||
updated=str(new_note.updated),
|
updated=str(new_note.updated),
|
||||||
|
command_id=command_id,
|
||||||
)
|
)
|
||||||
except HTTPException:
|
except HTTPException:
|
||||||
raise
|
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'"
|
status_code=400, detail="note_type must be 'human' or 'ai'"
|
||||||
)
|
)
|
||||||
|
|
||||||
await note.save()
|
command_id = await note.save()
|
||||||
|
|
||||||
return NoteResponse(
|
return NoteResponse(
|
||||||
id=note.id or "",
|
id=note.id or "",
|
||||||
|
|
@ -159,6 +160,7 @@ async def update_note(note_id: str, note_update: NoteUpdate):
|
||||||
note_type=note.note_type,
|
note_type=note.note_type,
|
||||||
created=str(note.created),
|
created=str(note.created),
|
||||||
updated=str(note.updated),
|
updated=str(note.updated),
|
||||||
|
command_id=command_id,
|
||||||
)
|
)
|
||||||
except HTTPException:
|
except HTTPException:
|
||||||
raise
|
raise
|
||||||
|
|
|
||||||
115
tests/test_notes_api.py
Normal file
115
tests/test_notes_api.py
Normal file
|
|
@ -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
|
||||||
Loading…
Reference in a new issue