mirror of
https://github.com/rennf93/roboco.git
synced 2026-08-03 07:23:24 +02:00
113 lines
3.7 KiB
Python
113 lines
3.7 KiB
Python
"""#96/#97: the periodic flush must (a) actually remove a ready buffer after
|
|||
|
|
notifying callbacks (``get_ready_buffers`` only peeks — without a flush the
|
||
|
|
buffer map grows unbounded), and (b) offload each sync callback to a thread so
|
||
|
|
a slow callback can't block the flush task / event loop.
|
||
|
|
"""
|
||
|
|
|
||
|
|
from __future__ import annotations
|
||
|
|
|
||
|
|
import threading
|
||
|
|
from uuid import UUID, uuid4
|
||
|
|
|
||
|
|
import pytest
|
||
|
|
from roboco.models.transcription import TranscriptionConfig
|
||
|
|
from roboco.services.transcription import TranscriptionService
|
||
|
|
|
||
|
|
|
||
|
|
def _svc() -> TranscriptionService:
|
||
|
|
# Tiny intervals so a single flush tick is fast and deterministic.
|
||
|
|
return TranscriptionService(
|
||
|
|
TranscriptionConfig(
|
||
|
|
min_chars_for_extraction=2,
|
||
|
|
idle_threshold_seconds=0.0,
|
||
|
|
flush_interval_seconds=0.01,
|
||
|
|
)
|
||
|
|
)
|
||
|
|
|
||
|
|
|
||
|
|
def _ready_buffer(svc: TranscriptionService, content: str) -> tuple[UUID, UUID]:
|
||
|
|
"""Create a buffer for a fresh agent/session, append content, mark it
|
||
|
|
complete (ready for extraction). Returns the (agent_id, session_id)."""
|
||
|
|
agent_id, session_id, channel_id = uuid4(), uuid4(), uuid4()
|
||
|
|
buffer = svc.get_buffer(
|
||
|
|
agent_id=agent_id, session_id=session_id, channel_id=channel_id
|
||
|
|
)
|
||
|
|
buffer.append(content)
|
||
|
|
buffer.is_complete = True
|
||
|
|
return agent_id, session_id
|
||
|
|
|
||
|
|
|
||
|
|
@pytest.mark.asyncio
|
||
|
|
async def test_periodic_flush_removes_ready_buffer() -> None:
|
||
|
|
"""#96: after a flush tick, a ready buffer is popped from ``_buffers``
|
||
|
|
(not re-yielded forever). Without the flush call the map grew unbounded."""
|
||
|
|
svc = _svc()
|
||
|
|
agent_id, session_id = _ready_buffer(svc, "some content")
|
||
|
|
assert agent_id in svc._buffers
|
||
|
|
|
||
|
|
await svc._flush_ready_buffers()
|
||
|
|
|
||
|
|
# The ready buffer was flushed (removed), not left to accumulate.
|
||
|
|
assert agent_id not in svc._buffers or session_id not in svc._buffers[agent_id]
|
||
|
|
|
||
|
|
|
||
|
|
@pytest.mark.asyncio
|
||
|
|
async def test_periodic_flush_invokes_callbacks() -> None:
|
||
|
|
"""#96: callbacks are still notified on the flush tick."""
|
||
|
|
svc = _svc()
|
||
|
|
_ready_buffer(svc, "content for callback")
|
||
|
|
|
||
|
|
seen: list[object] = []
|
||
|
|
svc.register_callback(seen.append)
|
||
|
|
|
||
|
|
await svc._flush_ready_buffers()
|
||
|
|
|
||
|
|
assert len(seen) == 1
|
||
|
|
|
||
|
|
|
||
|
|
@pytest.mark.asyncio
|
||
|
|
async def test_periodic_flush_offloads_sync_callback_to_thread() -> None:
|
||
|
|
"""#97: a sync callback runs off the event loop (in a worker thread) so a
|
||
|
|
slow callback can't block the flush task. Assert the callback executes in a
|
||
|
|
thread that is NOT the running event loop's main thread."""
|
||
|
|
svc = _svc()
|
||
|
|
_ready_buffer(svc, "content for offload")
|
||
|
|
|
||
|
|
callback_thread: list[object] = []
|
||
|
|
|
||
|
|
def _slow_callback(_b: object) -> None:
|
||
|
|
# Record the thread id; a sleep here would block the loop if not offloaded.
|
||
|
|
callback_thread.append(threading.get_ident())
|
||
|
|
|
||
|
|
svc.register_callback(_slow_callback)
|
||
|
|
|
||
|
|
await svc._flush_ready_buffers()
|
||
|
|
|
||
|
|
assert len(callback_thread) == 1
|
||
|
|
# The callback ran in a worker thread, not on the event loop's main thread.
|
||
|
|
assert callback_thread[0] != threading.get_ident()
|
||
|
|
|
||
|
|
|
||
|
|
@pytest.mark.asyncio
|
||
|
|
async def test_periodic_flush_callback_error_does_not_abort_flush() -> None:
|
||
|
|
"""A failing callback must not abort the flush (the buffer is still removed
|
||
|
|
and a sibling callback still runs)."""
|
||
|
|
svc = _svc()
|
||
|
|
agent_id, session_id = _ready_buffer(svc, "content with bad callback")
|
||
|
|
|
||
|
|
sibling_seen: list[object] = []
|
||
|
|
|
||
|
|
def _bad(_b: object) -> None:
|
||
|
|
raise RuntimeError("boom")
|
||
|
|
|
||
|
|
def _good(b: object) -> None:
|
||
|
|
sibling_seen.append(b)
|
||
|
|
|
||
|
|
svc.register_callback(_bad)
|
||
|
|
svc.register_callback(_good)
|
||
|
|
|
||
|
|
await svc._flush_ready_buffers() # must not raise
|
||
|
|
|
||
|
|
assert len(sibling_seen) == 1
|
||
|
|
assert agent_id not in svc._buffers or session_id not in svc._buffers[agent_id]
|