Skip to content

Commit 96f3df3

Browse files
committed
test(client): pin _handle_chat None handler result → empty text
3 tests over the single-shot else branch: sync handler with no return, async handler returning None, and an int-return guard so the fix doesn't over-narrow the else path (only None is coerced; ints/floats/ lists keep the existing `str(result)` behavior). Mutation-verified — reverting the fix fails both None cases and the guard still passes.
1 parent 5fe2b70 commit 96f3df3

1 file changed

Lines changed: 76 additions & 0 deletions

File tree

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
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

Comments
 (0)