fix(sdks): keep session activity cache current
This commit is contained in:
parent
ba71600d6f
commit
90e05f67e1
|
|
@ -1256,10 +1256,12 @@ class SessionAio(AsyncMetadataConfigMixin):
|
|||
routes.messages(self._session.workspace_id, self._session.id),
|
||||
body={"messages": messages_data},
|
||||
)
|
||||
return [
|
||||
created_messages = [
|
||||
Message.from_api_response(MessageResponse.model_validate(msg))
|
||||
for msg in data
|
||||
]
|
||||
self._session._update_last_message_at_from_messages(created_messages)
|
||||
return created_messages
|
||||
|
||||
async def messages(
|
||||
self,
|
||||
|
|
@ -1559,10 +1561,12 @@ class SessionAio(AsyncMetadataConfigMixin):
|
|||
data=data_dict,
|
||||
)
|
||||
|
||||
return [
|
||||
created_messages = [
|
||||
Message.from_api_response(MessageResponse.model_validate(msg))
|
||||
for msg in response
|
||||
]
|
||||
self._session._update_last_message_at_from_messages(created_messages)
|
||||
return created_messages
|
||||
|
||||
@validate_call(config=ConfigDict(arbitrary_types_allowed=True))
|
||||
async def representation(
|
||||
|
|
|
|||
|
|
@ -127,6 +127,17 @@ class Session(SessionBase, MetadataConfigMixin):
|
|||
self._last_message_at = session.last_message_at
|
||||
self._is_active = session.is_active
|
||||
|
||||
def _update_last_message_at_from_messages(
|
||||
self, messages: Sequence[Message]
|
||||
) -> None:
|
||||
"""Advance the cached activity timestamp from locally written messages."""
|
||||
if not messages:
|
||||
return
|
||||
|
||||
newest_message_at = max(message.created_at for message in messages)
|
||||
if self._last_message_at is None or newest_message_at > self._last_message_at:
|
||||
self._last_message_at = newest_message_at
|
||||
|
||||
def get_metadata(self) -> dict[str, object]:
|
||||
"""
|
||||
Get metadata from the server and update the cache.
|
||||
|
|
@ -451,10 +462,12 @@ class Session(SessionBase, MetadataConfigMixin):
|
|||
routes.messages(self.workspace_id, self.id),
|
||||
body={"messages": messages_data},
|
||||
)
|
||||
return [
|
||||
created_messages = [
|
||||
Message.from_api_response(MessageResponse.model_validate(msg))
|
||||
for msg in data
|
||||
]
|
||||
self._update_last_message_at_from_messages(created_messages)
|
||||
return created_messages
|
||||
|
||||
@validate_call
|
||||
def messages(
|
||||
|
|
@ -906,10 +919,12 @@ class Session(SessionBase, MetadataConfigMixin):
|
|||
data=data_dict,
|
||||
)
|
||||
|
||||
return [
|
||||
created_messages = [
|
||||
Message.from_api_response(MessageResponse.model_validate(msg))
|
||||
for msg in response
|
||||
]
|
||||
self._update_last_message_at_from_messages(created_messages)
|
||||
return created_messages
|
||||
|
||||
@validate_call(config=ConfigDict(arbitrary_types_allowed=True))
|
||||
def representation(
|
||||
|
|
|
|||
|
|
@ -222,7 +222,7 @@ describe('Peer', () => {
|
|||
const sessionIds = sessions.items.map((s) => s.id)
|
||||
expect(sessionIds).toContain('peer-sessions-test')
|
||||
const returned = sessions.items.find((item) => item.id === session.id)
|
||||
expect(returned?.lastMessageAt).toBeDefined()
|
||||
expect(typeof returned?.lastMessageAt).toBe('string')
|
||||
})
|
||||
|
||||
test('sessions returns empty for peer in no sessions', async () => {
|
||||
|
|
|
|||
|
|
@ -126,7 +126,7 @@ export interface SessionResponse {
|
|||
metadata: Record<string, unknown>
|
||||
configuration: SessionConfigApi
|
||||
created_at: string
|
||||
last_message_at?: string | null
|
||||
last_message_at: string | null
|
||||
}
|
||||
|
||||
export interface SessionCreateParams {
|
||||
|
|
|
|||
|
|
@ -48,6 +48,7 @@ async def test_session_upload_file(
|
|||
assert text_content in messages[0].content
|
||||
assert messages[0].peer_id == user.id
|
||||
assert messages[0].session_id == session.id
|
||||
assert session.last_message_at == max(message.created_at for message in messages)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
|
|
|
|||
|
|
@ -1,15 +1,104 @@
|
|||
import datetime
|
||||
from types import SimpleNamespace
|
||||
from typing import cast
|
||||
from unittest.mock import AsyncMock, MagicMock
|
||||
|
||||
import pytest
|
||||
|
||||
from sdks.python.src.honcho.api_types import QueueStatusResponse
|
||||
from sdks.python.src.honcho.api_types import MessageCreateParams, QueueStatusResponse
|
||||
from sdks.python.src.honcho.client import Honcho
|
||||
from sdks.python.src.honcho.message import Message
|
||||
from sdks.python.src.honcho.peer import Peer
|
||||
from sdks.python.src.honcho.session import Session, SessionPeerConfig
|
||||
|
||||
|
||||
def _message_response(
|
||||
message_id: str, created_at: datetime.datetime
|
||||
) -> dict[str, object]:
|
||||
"""Build a complete message response fixture for SDK boundary tests."""
|
||||
return {
|
||||
"id": message_id,
|
||||
"content": message_id,
|
||||
"peer_id": "peer-1",
|
||||
"session_id": "session-1",
|
||||
"workspace_id": "workspace-1",
|
||||
"metadata": {},
|
||||
"created_at": created_at,
|
||||
"token_count": 1,
|
||||
}
|
||||
|
||||
|
||||
def _session_write_honcho() -> (
|
||||
tuple[SimpleNamespace, datetime.datetime, datetime.datetime]
|
||||
):
|
||||
"""Build sync and async HTTP stubs for local session write tests."""
|
||||
newest_time = datetime.datetime(2026, 1, 10, 12, 0, tzinfo=datetime.timezone.utc)
|
||||
older_time = datetime.datetime(2026, 1, 5, 12, 0, tzinfo=datetime.timezone.utc)
|
||||
older_response = [_message_response("backdated activity", older_time)]
|
||||
newest_response = [_message_response("newest activity", newest_time)]
|
||||
sync_http = SimpleNamespace(
|
||||
post=MagicMock(side_effect=[older_response, older_response]),
|
||||
upload=MagicMock(side_effect=[newest_response, older_response]),
|
||||
)
|
||||
async_http = SimpleNamespace(
|
||||
post=AsyncMock(side_effect=[older_response, older_response]),
|
||||
upload=AsyncMock(side_effect=[newest_response, older_response]),
|
||||
)
|
||||
honcho = SimpleNamespace(
|
||||
workspace_id="workspace-1",
|
||||
_http=sync_http,
|
||||
_async_http_client=async_http,
|
||||
_ensure_workspace=MagicMock(return_value=None),
|
||||
_ensure_workspace_async=AsyncMock(return_value=None),
|
||||
)
|
||||
return honcho, older_time, newest_time
|
||||
|
||||
|
||||
def test_session_sync_writes_update_last_message_at_monotonically() -> None:
|
||||
"""Sync message and file writes keep the newest cached activity timestamp."""
|
||||
honcho, older_time, newest_time = _session_write_honcho()
|
||||
session = Session("session-1", honcho)
|
||||
|
||||
session.add_messages(MessageCreateParams(content="activity", peer_id="peer-1"))
|
||||
assert session.last_message_at == older_time
|
||||
|
||||
session.upload_file(("activity.txt", b"activity", "text/plain"), peer="peer-1")
|
||||
assert session.last_message_at == newest_time
|
||||
|
||||
session.add_messages(MessageCreateParams(content="activity", peer_id="peer-1"))
|
||||
assert session.last_message_at == newest_time
|
||||
|
||||
session.upload_file(("activity.txt", b"activity", "text/plain"), peer="peer-1")
|
||||
assert session.last_message_at == newest_time
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_session_async_writes_update_last_message_at_monotonically() -> None:
|
||||
"""Async message and file writes keep the newest cached activity timestamp."""
|
||||
honcho, older_time, newest_time = _session_write_honcho()
|
||||
session = Session("session-1", honcho)
|
||||
|
||||
await session.aio.add_messages(
|
||||
MessageCreateParams(content="activity", peer_id="peer-1")
|
||||
)
|
||||
assert session.last_message_at == older_time
|
||||
|
||||
await session.aio.upload_file(
|
||||
("activity.txt", b"activity", "text/plain"), peer="peer-1"
|
||||
)
|
||||
assert session.last_message_at == newest_time
|
||||
|
||||
await session.aio.add_messages(
|
||||
MessageCreateParams(content="activity", peer_id="peer-1")
|
||||
)
|
||||
assert session.last_message_at == newest_time
|
||||
|
||||
await session.aio.upload_file(
|
||||
("activity.txt", b"activity", "text/plain"), peer="peer-1"
|
||||
)
|
||||
assert session.last_message_at == newest_time
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_session_metadata(client_fixture: tuple[Honcho, str]):
|
||||
"""
|
||||
|
|
@ -729,12 +818,14 @@ async def test_session_clone(client_fixture: tuple[Honcho, str]):
|
|||
cloned = await session.aio.clone()
|
||||
assert isinstance(cloned, Session)
|
||||
assert cloned.id != session.id # Should have a different ID
|
||||
assert cloned.last_message_at is not None
|
||||
|
||||
# Verify cloned session has the same messages
|
||||
cloned_messages_page = await cloned.aio.messages()
|
||||
cloned_messages = cloned_messages_page.items
|
||||
assert len(cloned_messages) == 2
|
||||
assert cloned.last_message_at == max(
|
||||
message.created_at for message in cloned_messages
|
||||
)
|
||||
|
||||
# Verify original session still has messages
|
||||
original_messages_page = await session.aio.messages()
|
||||
|
|
@ -758,12 +849,14 @@ async def test_session_clone(client_fixture: tuple[Honcho, str]):
|
|||
cloned = session.clone()
|
||||
assert isinstance(cloned, Session)
|
||||
assert cloned.id != session.id # Should have a different ID
|
||||
assert cloned.last_message_at is not None
|
||||
|
||||
# Verify cloned session has the same messages
|
||||
cloned_messages_page = cloned.messages()
|
||||
cloned_messages = list(cloned_messages_page)
|
||||
assert len(cloned_messages) == 2
|
||||
assert cloned.last_message_at == max(
|
||||
message.created_at for message in cloned_messages
|
||||
)
|
||||
|
||||
# Verify original session still has messages
|
||||
original_messages_page = session.messages()
|
||||
|
|
|
|||
Loading…
Reference in New Issue