@@ -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