from __future__ import annotations import asyncio import pytest from yuxi.channels.policy.debounce import QueueDebounce class TestQueueDebounce: @pytest.fixture def debouncer(self) -> QueueDebounce: return QueueDebounce(debounce_ms=100) @pytest.mark.asyncio async def test_single_message_flushed(self, debouncer): results: list[list[str]] = [] async def handler(msgs: list[str]) -> None: results.append(msgs) await debouncer.enqueue("session-1", "hello", handler) await asyncio.sleep(0.3) assert len(results) == 1 assert results[0] == ["hello"] @pytest.mark.asyncio async def test_multiple_messages_merged(self, debouncer): results: list[list[str]] = [] async def handler(msgs: list[str]) -> None: results.append(msgs) for msg in ["m1", "m2", "m3"]: await debouncer.enqueue("session-1", msg, handler) await asyncio.sleep(0.02) await asyncio.sleep(0.3) assert len(results) == 1 assert results[0] == ["m1", "m2", "m3"] @pytest.mark.asyncio async def test_different_sessions_not_merged(self, debouncer): results: list[tuple[str, list[str]]] = [] async def make_handler(sid: str): async def handler(msgs: list[str]) -> None: results.append((sid, msgs)) return handler await debouncer.enqueue("session-a", "a1", await make_handler("session-a")) await debouncer.enqueue("session-b", "b1", await make_handler("session-b")) await debouncer.enqueue("session-a", "a2", await make_handler("session-a")) await asyncio.sleep(0.3) session_a_msgs = [m for s, m in results if s == "session-a"] session_b_msgs = [m for s, m in results if s == "session-b"] assert len(session_a_msgs) == 1 assert session_a_msgs[0] == ["a1", "a2"] assert len(session_b_msgs) == 1 assert session_b_msgs[0] == ["b1"] @pytest.mark.asyncio async def test_cancel_prevents_flush(self, debouncer): results: list[list[str]] = [] async def handler(msgs: list[str]) -> None: results.append(msgs) await debouncer.enqueue("session-1", "hello", handler) await debouncer.cancel("session-1") await asyncio.sleep(0.3) assert len(results) == 0 @pytest.mark.asyncio async def test_pending_count_reflects_active_sessions(self, debouncer): results: list[list[str]] = [] async def handler(msgs: list[str]) -> None: results.append(msgs) assert debouncer.pending_count == 0 await debouncer.enqueue("session-1", "m1", handler) await debouncer.enqueue("session-2", "m2", handler) assert debouncer.pending_count == 2 await asyncio.sleep(0.3) assert debouncer.pending_count == 0 @pytest.mark.asyncio async def test_handler_exception_does_not_crash(self, debouncer): async def failing_handler(msgs: list[str]) -> None: raise RuntimeError("handler error") await debouncer.enqueue("session-1", "m1", failing_handler) await asyncio.sleep(0.3) assert debouncer.pending_count == 0 @pytest.mark.asyncio async def test_consecutive_enqueues_reset_timer(self, debouncer): debouncer_long = QueueDebounce(debounce_ms=300) results: list[list[str]] = [] async def handler(msgs: list[str]) -> None: results.append(msgs) await debouncer_long.enqueue("s1", "first", handler) await asyncio.sleep(0.1) await debouncer_long.enqueue("s1", "second", handler) await asyncio.sleep(0.1) await debouncer_long.enqueue("s1", "third", handler) # After the first 0.3s (from first enqueue), it should NOT have flushed yet await asyncio.sleep(0.15) assert len(results) == 0 # Wait for the full timer from the last enqueue await asyncio.sleep(0.3) assert len(results) == 1 assert results[0] == ["first", "second", "third"] @pytest.mark.asyncio async def test_cancel_nonexistent_session_no_error(self, debouncer): await debouncer.cancel("nonexistent") assert debouncer.pending_count == 0