chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,174 @@
|
||||
"""
|
||||
Tests for Note.save()'s embed_note submission resilience
|
||||
(open_notebook/domain/notebook.py) and its one caller that needs the
|
||||
opposite behavior (api/routers/embedding.py).
|
||||
|
||||
Note.save()'s embed_note submission is now wrapped in try/except: the note
|
||||
itself is already durably saved by the time it's attempted (verified
|
||||
against a live embedded SurrealDB instance - see session notes), so a
|
||||
transient submission failure shouldn't turn an otherwise-successful save
|
||||
into a 500, unlike Source.vectorize()/add_insight() which are dedicated
|
||||
"submit this job" calls with nothing useful to fall back on.
|
||||
|
||||
api/routers/embedding.py's POST /embed endpoint is the one exception: for
|
||||
item_type=note it reuses Note.save() specifically *because* embedding
|
||||
submission is the explicit point of that call (mirroring
|
||||
Source.vectorize()) - so it now explicitly checks for a submission failure
|
||||
(content present, no command_id) and still surfaces it as a failure.
|
||||
"""
|
||||
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
import pytest
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from open_notebook.domain.notebook import Note
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def client():
|
||||
from api.main import app
|
||||
|
||||
return TestClient(app)
|
||||
|
||||
|
||||
class TestNoteSaveEmbedResilience:
|
||||
@pytest.mark.asyncio
|
||||
async def test_save_does_not_raise_when_submission_fails(self):
|
||||
note = Note(title="Test", content="some content")
|
||||
with (
|
||||
patch(
|
||||
"open_notebook.domain.base.ObjectModel.save",
|
||||
new=AsyncMock(),
|
||||
),
|
||||
patch(
|
||||
"open_notebook.domain.notebook.submit_command",
|
||||
side_effect=RuntimeError("job queue is down"),
|
||||
),
|
||||
):
|
||||
object.__setattr__(note, "id", "note:abc123")
|
||||
command_id = await note.save()
|
||||
|
||||
assert command_id is None
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_save_still_calls_super_save_before_attempting_submission(self):
|
||||
"""The note's core save must happen (and succeed) regardless of
|
||||
whether the embedding submission afterward fails."""
|
||||
note = Note(title="Test", content="some content")
|
||||
with (
|
||||
patch(
|
||||
"open_notebook.domain.base.ObjectModel.save",
|
||||
new=AsyncMock(),
|
||||
) as mock_super_save,
|
||||
patch(
|
||||
"open_notebook.domain.notebook.submit_command",
|
||||
side_effect=RuntimeError("job queue is down"),
|
||||
),
|
||||
):
|
||||
object.__setattr__(note, "id", "note:abc123")
|
||||
await note.save()
|
||||
|
||||
mock_super_save.assert_awaited_once()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_save_returns_command_id_on_success(self):
|
||||
note = Note(title="Test", content="some content")
|
||||
with (
|
||||
patch("open_notebook.domain.base.ObjectModel.save", new=AsyncMock()),
|
||||
patch(
|
||||
"open_notebook.domain.notebook.submit_command",
|
||||
return_value="command:xyz789",
|
||||
),
|
||||
):
|
||||
object.__setattr__(note, "id", "note:abc123")
|
||||
command_id = await note.save()
|
||||
|
||||
assert command_id == "command:xyz789"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_save_returns_none_when_no_content(self):
|
||||
note = Note(title="Test", content=None)
|
||||
with (
|
||||
patch("open_notebook.domain.base.ObjectModel.save", new=AsyncMock()),
|
||||
patch(
|
||||
"open_notebook.domain.notebook.submit_command"
|
||||
) as mock_submit,
|
||||
):
|
||||
object.__setattr__(note, "id", "note:abc123")
|
||||
command_id = await note.save()
|
||||
|
||||
assert command_id is None
|
||||
mock_submit.assert_not_called()
|
||||
|
||||
|
||||
class TestEmbeddingEndpointNoteBranch:
|
||||
"""api/routers/embedding.py: POST /api/embed with item_type=note."""
|
||||
|
||||
def test_returns_500_when_submission_fails_with_content(self, client):
|
||||
note = Note(title="Test", content="some content")
|
||||
object.__setattr__(note, "id", "note:abc123")
|
||||
|
||||
with (
|
||||
patch("api.routers.embedding.Note.get", new=AsyncMock(return_value=note)),
|
||||
patch.object(Note, "save", new=AsyncMock(return_value=None)),
|
||||
patch(
|
||||
"open_notebook.ai.models.model_manager.get_embedding_model",
|
||||
new=AsyncMock(return_value=object()),
|
||||
),
|
||||
):
|
||||
response = client.post(
|
||||
"/api/embed",
|
||||
json={"item_id": "note:abc123", "item_type": "note", "async_processing": False},
|
||||
)
|
||||
|
||||
assert response.status_code == 500
|
||||
assert response.json()["detail"] == "Failed to submit note embedding job"
|
||||
|
||||
def test_succeeds_when_submission_works(self, client):
|
||||
note = Note(title="Test", content="some content")
|
||||
object.__setattr__(note, "id", "note:abc123")
|
||||
|
||||
with (
|
||||
patch("api.routers.embedding.Note.get", new=AsyncMock(return_value=note)),
|
||||
patch.object(
|
||||
Note, "save", new=AsyncMock(return_value="command:abc123")
|
||||
),
|
||||
patch(
|
||||
"open_notebook.ai.models.model_manager.get_embedding_model",
|
||||
new=AsyncMock(return_value=object()),
|
||||
),
|
||||
):
|
||||
response = client.post(
|
||||
"/api/embed",
|
||||
json={"item_id": "note:abc123", "item_type": "note", "async_processing": False},
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
body = response.json()
|
||||
assert body["success"] is True
|
||||
assert body["command_id"] == "command:abc123"
|
||||
|
||||
def test_succeeds_with_no_command_id_when_note_has_no_content(self, client):
|
||||
"""A content-less note has nothing to embed - save() correctly
|
||||
returns None, and that must NOT be treated as a submission failure."""
|
||||
note = Note(title="Test", content=None)
|
||||
object.__setattr__(note, "id", "note:abc123")
|
||||
|
||||
with (
|
||||
patch("api.routers.embedding.Note.get", new=AsyncMock(return_value=note)),
|
||||
patch.object(Note, "save", new=AsyncMock(return_value=None)),
|
||||
patch(
|
||||
"open_notebook.ai.models.model_manager.get_embedding_model",
|
||||
new=AsyncMock(return_value=object()),
|
||||
),
|
||||
):
|
||||
response = client.post(
|
||||
"/api/embed",
|
||||
json={"item_id": "note:abc123", "item_type": "note", "async_processing": False},
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
body = response.json()
|
||||
assert body["success"] is True
|
||||
assert body["command_id"] is None
|
||||
Reference in New Issue
Block a user