"""Unit tests for Signal stream buffer, coalescer, and formatting.""" import asyncio import time from unittest.mock import AsyncMock, MagicMock import pytest from yuxi.channels.adapters.signal.streaming import ( StreamBuffer, StreamCoalescer, ReasoningLaneFormatter, ) from yuxi.channels.adapters.signal.channel import SignalChannel from yuxi.channels.models import DeliveryResult class TestStreamBuffer: def test_set_and_get(self): buf = StreamBuffer() buf.set("chat1", "hello") assert buf.get("chat1") == "hello" def test_get_nonexistent_returns_empty(self): buf = StreamBuffer() assert buf.get("nonexistent") == "" def test_pop_returns_and_removes(self): buf = StreamBuffer() buf.set("chat1", "hello") result = buf.pop("chat1") assert result == "hello" assert buf.get("chat1") == "" def test_pop_nonexistent_returns_none(self): buf = StreamBuffer() assert buf.pop("nonexistent") is None def test_clear_removes_all(self): buf = StreamBuffer() buf.set("chat1", "a") buf.set("chat2", "b") buf.clear() assert buf.get("chat1") == "" assert buf.get("chat2") == "" def test_idle_ms_returns_zero_when_empty(self): buf = StreamBuffer() assert buf.idle_ms("chat1") == 0 def test_idle_ms_returns_positive_after_delay(self): buf = StreamBuffer() buf.set("chat1", "hello") time.sleep(0.05) ms = buf.idle_ms("chat1") assert ms > 0 def test_prune_removes_expired_entries(self): buf = StreamBuffer(ttl=0.01) buf.set("chat1", "hello") time.sleep(0.02) buf.prune() assert buf.get("chat1") == "" def test_prune_removes_excess_entries(self): buf = StreamBuffer(max_entries=2) for i in range(5): buf.set(f"chat{i}", f"msg{i}") buf.prune() count = sum(1 for i in range(5) if buf.get(f"chat{i}")) assert count <= 2 class TestStreamCoalescer: def test_should_flush_on_idle_below_threshold(self): buf = StreamBuffer() buf.set("chat1", "hello") coalescer = StreamCoalescer(buf, idle_ms=1000) assert coalescer.should_flush_on_idle("chat1") is False def test_should_flush_on_idle_empty_buffer(self): buf = StreamBuffer() coalescer = StreamCoalescer(buf, idle_ms=100) assert coalescer.should_flush_on_idle("empty") is False def test_should_flush_on_size_below_threshold(self): buf = StreamBuffer() buf.set("chat1", "short") coalescer = StreamCoalescer(buf, min_chars=1500) assert coalescer.should_flush_on_size("chat1") is False def test_should_flush_on_size_above_threshold(self): buf = StreamBuffer() buf.set("chat1", "x" * 1600) coalescer = StreamCoalescer(buf, min_chars=1500) assert coalescer.should_flush_on_size("chat1") is True def test_is_reasoning_chunk_true(self): assert StreamCoalescer.is_reasoning_chunk("lane:reasoning:hello") is True def test_is_reasoning_chunk_false(self): assert StreamCoalescer.is_reasoning_chunk("normal message") is False def test_format_reasoning(self): coalescer = StreamCoalescer(StreamBuffer(), lane_separator="---") result = coalescer.format_reasoning("lane:reasoning:hello world") assert "(thinking)" in result assert "hello world" in result assert "---" in result class TestReasoningLaneFormatter: def test_format(self): fmt = ReasoningLaneFormatter("~~~") result = fmt.format("lane:reasoning:test") assert "(thinking)" in result assert "test" in result assert "~~~" in result def test_is_reasoning_true(self): assert ReasoningLaneFormatter.is_reasoning("lane:reasoning:x") is True def test_is_reasoning_false(self): assert ReasoningLaneFormatter.is_reasoning("normal") is False class TestStreamCoalesceIntegration: def test_coalesce_below_threshold_not_sent(self): channel = SignalChannel({"block_streaming": True}) channel._sender = MagicMock() channel._rpc_client = MagicMock() channel._sender.send_text = AsyncMock(return_value=DeliveryResult(success=True, message_id="1")) async def _test(): result = await channel.send_stream_chunk("+1234", "1", "short", False) assert result.success is True asyncio.run(_test()) def test_coalesce_idle_timeout_sends_buffered(self): channel = SignalChannel( {"block_streaming": True, "block_streaming_coalesce": {"min_chars": 5000, "idle_ms": 0}} ) channel._sender = MagicMock() channel._rpc_client = MagicMock() channel._sender.send_text = AsyncMock(return_value=DeliveryResult(success=True, message_id="1")) async def _test(): channel._stream_buffer.set("+1234", "buffered_content") channel._stream_coalesce_idle_ms = 0 result = await channel.send_stream_chunk("+1234", "1", " new part", False) assert result.success is True asyncio.run(_test()) def test_coalesce_finished_sends_all(self): channel = SignalChannel({"block_streaming": True}) channel._sender = MagicMock() channel._rpc_client = MagicMock() channel._sender.send_text = AsyncMock(return_value=DeliveryResult(success=True, message_id="final")) async def _test(): result = await channel.send_stream_chunk("+1234", "1", "final message", True) assert result.success is True asyncio.run(_test()) def test_block_streaming_mode_sends_immediately(self): channel = SignalChannel({"block_streaming": True}) channel._sender = MagicMock() channel._rpc_client = MagicMock() channel._sender.send_text = AsyncMock(return_value=DeliveryResult(success=True, message_id="1")) async def _test(): result = await channel.send_stream_chunk("+1234", None, "immediate", False) assert result.success is True asyncio.run(_test()) def test_progress_streaming_sends_each_chunk(self): channel = SignalChannel({"block_streaming": False}) channel._sender = MagicMock() channel._rpc_client = MagicMock() channel._sender.send_text = AsyncMock(return_value=DeliveryResult(success=True, message_id="1")) async def _test(): result = await channel.send_stream_chunk("+1234", None, "chunk1", False) assert result.success is True asyncio.run(_test()) class TestMarkdownFullConversion: def test_bold_italic_strikethrough(self): from yuxi.channels.adapters.signal.format import markdown_to_signal_styles result = markdown_to_signal_styles("**bold** *italic* ~~strike~~") assert result is not None assert "bold" in result.body assert "italic" in result.body assert "strike" in result.body style_names = {s.style for s in result.styles} assert "BOLD" in style_names assert "ITALIC" in style_names assert "STRIKETHROUGH" in style_names def test_code_and_spoiler(self): from yuxi.channels.adapters.signal.format import markdown_to_signal_styles result = markdown_to_signal_styles("`code` ||spoiler||") assert result is not None assert "code" in result.body assert "spoiler" in result.body style_names = {s.style for s in result.styles} assert "MONOSPACE" in style_names assert "SPOILER" in style_names def test_h1_to_bold(self): from yuxi.channels.adapters.signal.format import markdown_to_signal_styles result = markdown_to_signal_styles("# Heading", heading_style="bold") assert result is not None assert "Heading" in result.body def test_blockquote_prefix(self): from yuxi.channels.adapters.signal.format import markdown_to_signal_styles result = markdown_to_signal_styles("> quote text", blockquote_prefix="| ") assert result is not None assert "| " in result.body or "quote" in result.body def test_tables_to_bullets(self): from yuxi.channels.adapters.signal.format import markdown_to_signal_styles result = markdown_to_signal_styles( "| A | B |\n| --- | --- |\n| 1 | 2 |", table_mode="bullets", ) assert result is not None assert "A" in result.body or "B" in result.body def test_link_extraction(self): from yuxi.channels.adapters.signal.format import markdown_to_signal_styles result = markdown_to_signal_styles("[click me](https://example.com)") assert result is not None assert "https://example.com" in result.body or "click me" in result.body def test_nested_styles(self): from yuxi.channels.adapters.signal.format import markdown_to_signal_styles result = markdown_to_signal_styles("**bold *and italic***") assert result is not None assert "bold" in result.body def test_overlapping_styles_merge(self): from yuxi.channels.adapters.signal.format import markdown_to_signal_styles result = markdown_to_signal_styles("**same** **style**") assert result is not None assert "same" in result.body assert "style" in result.body