Skip to content

Commit c45fdca

Browse files
committed
test(client): pin _handle_chat dict-result no-mutation
Three direct unit tests against _handle_chat: a tool_calls-only template keeps its single key after the call; a text-bearing template doesn't gain finish_reason; reusing the same template across two calls leaves its key set clean. Each test would fail under the previous `payload = result` reference.
1 parent f540bbb commit c45fdca

1 file changed

Lines changed: 92 additions & 0 deletions

File tree

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
"""Regression: _handle_chat single-shot dict path used `payload = result` and
2+
then `setdefault`'d defaults onto the handler-supplied dict, mutating a
3+
caller's template / cached response (silently adding `text: ""` and
4+
`finish_reason: "stop"`). A handler returning a class-attribute or a memoised
5+
response dict therefore saw the SAME dict grow extra keys across calls.
6+
7+
Fix copies first (`payload = dict(result)`); these tests pin that behavior.
8+
"""
9+
10+
import json
11+
12+
import pytest
13+
14+
from zhub.client import _handle_chat
15+
from zhub.protocol import Envelope
16+
17+
18+
class _FakeWS:
19+
def __init__(self) -> None:
20+
self.sent: list[dict] = []
21+
22+
async def send(self, text: str) -> None:
23+
self.sent.append(json.loads(text))
24+
25+
26+
class _Pub:
27+
def __init__(self, handler) -> None:
28+
self.chat_handler = handler
29+
30+
31+
@pytest.mark.asyncio
32+
async def test_dict_handler_result_is_not_mutated_with_defaults():
33+
template = {"tool_calls": [{"id": "c1", "function": {"name": "lookup"}}]}
34+
35+
def handler(messages, options):
36+
return template
37+
38+
ws = _FakeWS()
39+
env = Envelope(type="chat-request", payload={"messages": []})
40+
await _handle_chat(_Pub(handler), ws, env)
41+
42+
# Wire payload still carries the defaults the hub expects.
43+
payload = ws.sent[0]["payload"]
44+
assert payload["text"] == ""
45+
assert payload["finish_reason"] == "stop"
46+
assert payload["tool_calls"] == [{"id": "c1", "function": {"name": "lookup"}}]
47+
48+
# But the caller's template is untouched — same keys it walked in with.
49+
assert set(template.keys()) == {"tool_calls"}
50+
51+
52+
@pytest.mark.asyncio
53+
async def test_dict_handler_text_present_no_default_inserted():
54+
template = {"text": "hi", "usage": {"prompt_tokens": 7}}
55+
56+
def handler(messages, options):
57+
return template
58+
59+
ws = _FakeWS()
60+
env = Envelope(type="chat-request", payload={"messages": []})
61+
await _handle_chat(_Pub(handler), ws, env)
62+
63+
payload = ws.sent[0]["payload"]
64+
assert payload["text"] == "hi"
65+
assert payload["finish_reason"] == "stop"
66+
assert payload["usage"] == {"prompt_tokens": 7}
67+
68+
# Caller's template unchanged across the call.
69+
assert set(template.keys()) == {"text", "usage"}
70+
71+
72+
@pytest.mark.asyncio
73+
async def test_dict_handler_reused_across_calls_keeps_clean_keys():
74+
"""Two calls reusing the same dict — second call must not see the first
75+
call's defaults baked into the template."""
76+
template = {"tool_calls": []}
77+
78+
def handler(messages, options):
79+
return template
80+
81+
pub = _Pub(handler)
82+
ws1 = _FakeWS()
83+
ws2 = _FakeWS()
84+
await _handle_chat(pub, ws1, Envelope(type="chat-request", payload={"messages": []}))
85+
snapshot_after_first = set(template.keys())
86+
await _handle_chat(pub, ws2, Envelope(type="chat-request", payload={"messages": []}))
87+
88+
assert snapshot_after_first == {"tool_calls"}
89+
assert set(template.keys()) == {"tool_calls"}
90+
# Both wire payloads still carry the defaults.
91+
assert ws1.sent[0]["payload"]["text"] == ""
92+
assert ws2.sent[0]["payload"]["text"] == ""

0 commit comments

Comments
 (0)