|
| 1 | +"""Regression: a chat_handler that forgets ``return`` (returns None from |
| 2 | +falling off the end of the function body) shipped the literal three-character |
| 3 | +string ``"None"`` as the assistant reply text — the single-shot else branch |
| 4 | +called ``str(result)`` on the None value. |
| 5 | +
|
| 6 | +This is a common publisher-side mistake: ``def handler(msgs, opts): print(msgs)`` |
| 7 | +runs cleanly, returns None. Before the fix the caller saw ``{"text": "None"}`` |
| 8 | +on the wire; after the fix an empty response goes out, matching the JS port's |
| 9 | +``String(awaited ?? '')`` behavior. |
| 10 | +""" |
| 11 | + |
| 12 | +import json |
| 13 | + |
| 14 | +import pytest |
| 15 | + |
| 16 | +from zhub.client import _handle_chat |
| 17 | +from zhub.protocol import Envelope |
| 18 | + |
| 19 | + |
| 20 | +class _FakeWS: |
| 21 | + def __init__(self) -> None: |
| 22 | + self.sent: list[dict] = [] |
| 23 | + |
| 24 | + async def send(self, text: str) -> None: |
| 25 | + self.sent.append(json.loads(text)) |
| 26 | + |
| 27 | + |
| 28 | +class _Pub: |
| 29 | + def __init__(self, handler) -> None: |
| 30 | + self.chat_handler = handler |
| 31 | + |
| 32 | + |
| 33 | +@pytest.mark.asyncio |
| 34 | +async def test_sync_handler_returning_none_emits_empty_text(): |
| 35 | + def handler(messages, options): |
| 36 | + # Common mistake: forgot the return statement. Body runs, function |
| 37 | + # falls off the end, returns None. |
| 38 | + pass |
| 39 | + |
| 40 | + ws = _FakeWS() |
| 41 | + env = Envelope(type="chat-request", request_id="r1", payload={"messages": []}) |
| 42 | + await _handle_chat(_Pub(handler), ws, env) |
| 43 | + |
| 44 | + assert len(ws.sent) == 1 |
| 45 | + payload = ws.sent[0]["payload"] |
| 46 | + assert payload["text"] == "" # pre-fix: "None" |
| 47 | + assert payload["finish_reason"] == "stop" |
| 48 | + |
| 49 | + |
| 50 | +@pytest.mark.asyncio |
| 51 | +async def test_async_handler_returning_none_emits_empty_text(): |
| 52 | + async def handler(messages, options): |
| 53 | + return None |
| 54 | + |
| 55 | + ws = _FakeWS() |
| 56 | + env = Envelope(type="chat-request", request_id="r2", payload={"messages": []}) |
| 57 | + await _handle_chat(_Pub(handler), ws, env) |
| 58 | + |
| 59 | + assert ws.sent[-1]["payload"]["text"] == "" |
| 60 | + |
| 61 | + |
| 62 | +@pytest.mark.asyncio |
| 63 | +async def test_int_result_still_stringified(): |
| 64 | + """Guard: the fix only special-cases None. A handler returning a non-None |
| 65 | + non-str non-dict value (int, float, list …) is still stringified — this |
| 66 | + keeps existing behavior for the two-year-stable else branch, and only |
| 67 | + the ``str(None) == 'None'`` embarrassment is corrected.""" |
| 68 | + |
| 69 | + def handler(messages, options): |
| 70 | + return 42 |
| 71 | + |
| 72 | + ws = _FakeWS() |
| 73 | + env = Envelope(type="chat-request", request_id="r3", payload={"messages": []}) |
| 74 | + await _handle_chat(_Pub(handler), ws, env) |
| 75 | + |
| 76 | + assert ws.sent[-1]["payload"]["text"] == "42" |
0 commit comments