diff --git a/backend/app/services/focus_service.py b/backend/app/services/focus_service.py index 906ce0b6c..87eec7135 100644 --- a/backend/app/services/focus_service.py +++ b/backend/app/services/focus_service.py @@ -361,6 +361,10 @@ async def _upsert_focus_item_impl( await db.refresh(item) else: await db.flush() + # The caller may serialize this item before committing its outer + # transaction. Load database-generated timestamps first so async ORM + # attribute access cannot trigger an implicit lazy-load. + await db.refresh(item) return _serialize_focus_item(item) diff --git a/backend/tests/test_focus_service.py b/backend/tests/test_focus_service.py new file mode 100644 index 000000000..8bff9de21 --- /dev/null +++ b/backend/tests/test_focus_service.py @@ -0,0 +1,60 @@ +from types import SimpleNamespace +from unittest.mock import AsyncMock +import uuid + +import pytest + +from app.services import focus_service + + +class _Result: + def __init__(self, item): + self.item = item + + def scalar_one_or_none(self): + return self.item + + +@pytest.mark.asyncio +async def test_upsert_with_caller_session_refreshes_before_serializing(monkeypatch): + """Server-generated timestamps must be loaded inside the async context.""" + item = SimpleNamespace( + title=None, + description="Previous description", + status="in_progress", + kind="normal", + source="user", + item_metadata={}, + completed_at=None, + ) + events: list[str] = [] + + async def flush(): + events.append("flush") + + async def refresh(value): + assert value is item + events.append("refresh") + + session = SimpleNamespace( + execute=AsyncMock(return_value=_Result(item)), + flush=flush, + refresh=refresh, + ) + monkeypatch.setattr(focus_service, "_serialize_focus_item", lambda value: {"key": "system:okr_reports"}) + + result = await focus_service._upsert_focus_item_impl( + session, + uuid.uuid4(), + "system:okr_reports", + None, + "OKR reports", + "in_progress", + "system", + "trigger", + None, + should_commit=False, + ) + + assert result == {"key": "system:okr_reports"} + assert events == ["flush", "refresh"]