Skip to content

Commit bf3fae2

Browse files
committed
test(client): cover sync-generator dict/tool_call chunk serialization
Drives _handle_chat with sync-generator handlers: dict deltas extract real text (not repr) with done/finish_reason preserved, tool_call_delta forwarded verbatim, non-streaming accumulates text, and plain string chunks are unchanged.
1 parent b2fe21e commit bf3fae2

1 file changed

Lines changed: 103 additions & 0 deletions

File tree

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
"""Unit coverage for sync-generator chat handlers in _handle_chat.
2+
3+
A chat_handler may be a *sync* generator. The async-gen path serializes each
4+
yielded chunk through _serialize_stream_chunk / _chunk_to_text, which extract
5+
the delta (and preserve tool_call_delta / done / finish_reason) for dict- and
6+
ChatChunk-shaped chunks. The sync-gen path historically called str(chunk),
7+
which stringified a dict chunk into its Python repr (e.g. "{'delta': 'hi'}")
8+
as the literal delta and dropped done/finish_reason/tool_call_delta entirely.
9+
These tests pin the sync path to the same behavior as the async path.
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+
def _payloads(ws: _FakeWS) -> list[dict]:
34+
return [m["payload"] for m in ws.sent]
35+
36+
37+
@pytest.mark.asyncio
38+
async def test_sync_gen_streaming_dict_chunks_extract_delta():
39+
def handler(messages, options):
40+
yield {"delta": "the "}
41+
yield {"delta": "fox", "done": True, "finish_reason": "length"}
42+
43+
ws = _FakeWS()
44+
env = Envelope(type="chat-request", payload={"messages": [], "stream": True})
45+
await _handle_chat(_Pub(handler), ws, env)
46+
47+
payloads = _payloads(ws)
48+
# Deltas are the real text, not the dict repr.
49+
assert payloads[0]["delta"] == "the "
50+
assert payloads[1]["delta"] == "fox"
51+
# The chunk's own done/finish_reason survive serialization.
52+
assert payloads[1]["done"] is True
53+
assert payloads[1]["finish_reason"] == "length"
54+
# Trailing synthetic terminator closes the stream.
55+
assert payloads[-1] == {"delta": "", "done": True, "finish_reason": "stop"}
56+
57+
58+
@pytest.mark.asyncio
59+
async def test_sync_gen_streaming_forwards_tool_call_delta():
60+
def handler(messages, options):
61+
yield {"tool_call_delta": {"index": 0, "id": "call_x"}}
62+
yield {"done": True, "finish_reason": "tool_calls"}
63+
64+
ws = _FakeWS()
65+
env = Envelope(type="chat-request", payload={"messages": [], "stream": True})
66+
await _handle_chat(_Pub(handler), ws, env)
67+
68+
payloads = _payloads(ws)
69+
assert payloads[0]["tool_call_delta"] == {"index": 0, "id": "call_x"}
70+
# The tool_call delta must not have been collapsed into a string delta.
71+
assert "delta" not in payloads[0]
72+
assert payloads[1]["finish_reason"] == "tool_calls"
73+
74+
75+
@pytest.mark.asyncio
76+
async def test_sync_gen_nonstreaming_accumulates_text_not_repr():
77+
def handler(messages, options):
78+
yield {"delta": "the "}
79+
yield {"delta": "fox"}
80+
81+
ws = _FakeWS()
82+
env = Envelope(type="chat-request", payload={"messages": []})
83+
await _handle_chat(_Pub(handler), ws, env)
84+
85+
assert len(ws.sent) == 1
86+
assert ws.sent[0]["type"] == "chat-response"
87+
assert ws.sent[0]["payload"]["text"] == "the fox"
88+
89+
90+
@pytest.mark.asyncio
91+
async def test_sync_gen_string_chunks_unchanged():
92+
def handler(messages, options):
93+
yield "the "
94+
yield "fox"
95+
96+
ws = _FakeWS()
97+
env = Envelope(type="chat-request", payload={"messages": [], "stream": True})
98+
await _handle_chat(_Pub(handler), ws, env)
99+
100+
payloads = _payloads(ws)
101+
assert payloads[0]["delta"] == "the "
102+
assert payloads[1]["delta"] == "fox"
103+
assert payloads[-1] == {"delta": "", "done": True, "finish_reason": "stop"}

0 commit comments

Comments
 (0)