Skip to content

Commit c048eb6

Browse files
committed
test(js/client): chatStream() handles non-streaming chat-response
Two cases against a real ws.WebSocketServer: - chat-response with text → chatStream yields the text and exits in <2s - chat-response with empty text → chatStream exits in <2s, zero yields Both pin elapsed < 2s while configuring timeoutPerChunkMs=60s, so a regression manifests as a multi-second hang (the symptom of the bug). Pre-fix both hang to the per-chunk timeout — the suite terminates instead of completing.
1 parent 555e505 commit c048eb6

1 file changed

Lines changed: 63 additions & 0 deletions

File tree

js/test/client_stream.test.ts

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,69 @@ describe('ZhubConnection.chatStream', () => {
113113
}
114114
});
115115

116+
it('forwards a single chat-response into the stream as text-delta + done', async () => {
117+
// Regression: a publisher that ignores stream:true (chat_handler returns
118+
// a plain string/{text}) replies with one chat-response, not chat-chunks.
119+
// Python's chat_stream() unpacks that into {delta:text, done:false} +
120+
// {done:true} (zhub/client.py:549-552); the JS port routed chat-response
121+
// only through `pending` so a chatStream() caller hung for the full
122+
// per-chunk timeout. Pre-fix this assertion never reached the elapsed
123+
// check — the for-await blocked on the next chunk that never came.
124+
const s = await startServer((ws, env) => {
125+
if (env.type !== 'chat-request') return;
126+
ws.send(JSON.stringify({
127+
type: 'chat-response',
128+
request_id: env.request_id,
129+
payload: { text: 'whole response', finish_reason: 'stop' },
130+
}));
131+
});
132+
try {
133+
const conn = connect({ aiName: 'test-ai', apiKey: 'zk_test', hubUrl: makeHubUrl(s.port) });
134+
await new Promise<void>((r) => setTimeout(r, 100));
135+
136+
const out: string[] = [];
137+
const start = Date.now();
138+
for await (const delta of conn.chatStream([{ role: 'user', content: 'hi' }], { timeoutPerChunkMs: 60_000 })) {
139+
out.push(delta);
140+
}
141+
const elapsed = Date.now() - start;
142+
assert.deepEqual(out, ['whole response']);
143+
assert(elapsed < 2_000, `expected immediate exit on chat-response, got ${elapsed}ms`);
144+
145+
await conn.stop();
146+
} finally {
147+
await s.close();
148+
}
149+
});
150+
151+
it('empty-text chat-response into stream exits cleanly with no yield', async () => {
152+
const s = await startServer((ws, env) => {
153+
if (env.type !== 'chat-request') return;
154+
ws.send(JSON.stringify({
155+
type: 'chat-response',
156+
request_id: env.request_id,
157+
payload: { text: '', finish_reason: 'stop' },
158+
}));
159+
});
160+
try {
161+
const conn = connect({ aiName: 'test-ai', apiKey: 'zk_test', hubUrl: makeHubUrl(s.port) });
162+
await new Promise<void>((r) => setTimeout(r, 100));
163+
164+
const out: string[] = [];
165+
const start = Date.now();
166+
for await (const delta of conn.chatStream([{ role: 'user', content: 'hi' }], { timeoutPerChunkMs: 60_000 })) {
167+
out.push(delta);
168+
}
169+
const elapsed = Date.now() - start;
170+
assert.deepEqual(out, []);
171+
assert(elapsed < 2_000, `expected immediate exit, got ${elapsed}ms`);
172+
173+
await conn.stop();
174+
} finally {
175+
await s.close();
176+
}
177+
});
178+
116179
it('sends stream:true on the chat-request envelope', async () => {
117180
let captured: Record<string, unknown> | null = null;
118181
const s = await startServer((ws, env) => {

0 commit comments

Comments
 (0)