@@ -66,6 +66,7 @@ describe("useAIChat", () => {
6666 expect ( client . chatCompletionStream ) . toHaveBeenCalledWith ( {
6767 model : "gpt-5-mini" ,
6868 messages : [ { role : "user" , content : "Hi" } ] ,
69+ stream : true ,
6970 signal : expect . any ( AbortSignal ) ,
7071 } ) ;
7172
@@ -131,6 +132,78 @@ describe("useAIChat", () => {
131132 } ) ;
132133 } ) ;
133134
135+ it ( "ignores stale chunks after stop and allows the next send" , async ( ) => {
136+ let releaseStoppedRequest ! : ( ) => void ;
137+ const stoppedRequestGate = new Promise < void > ( ( resolve ) => {
138+ releaseStoppedRequest = resolve ;
139+ } ) ;
140+
141+ const client = {
142+ chatCompletionStream : vi . fn ( async function * ( { messages } ) {
143+ const text = messages . at ( - 1 ) ?. content ;
144+
145+ if ( text === "First" ) {
146+ yield "Partial" ;
147+ await stoppedRequestGate ;
148+ yield " stale" ;
149+ return ;
150+ }
151+
152+ yield "Fresh" ;
153+ } ) ,
154+ } satisfies AIGatewayClient ;
155+
156+ const { result } = renderHook ( ( ) => useAIChat ( { client, model : "gpt-5-mini" } ) ) ;
157+
158+ let firstSendPromise : Promise < boolean > | undefined ;
159+ await act ( async ( ) => {
160+ firstSendPromise = result . current . sendMessage ( "First" ) ;
161+ } ) ;
162+
163+ await waitFor ( ( ) => {
164+ expect ( result . current . status ) . toBe ( "streaming" ) ;
165+ expect ( result . current . messages ) . toEqual ( [
166+ { id : "id-1" , role : "user" , content : "First" } ,
167+ { id : "id-2" , role : "assistant" , content : "Partial" } ,
168+ ] ) ;
169+ } ) ;
170+
171+ act ( ( ) => {
172+ result . current . stop ( ) ;
173+ } ) ;
174+
175+ await waitFor ( ( ) => {
176+ expect ( result . current . status ) . toBe ( "ready" ) ;
177+ } ) ;
178+
179+ await act ( async ( ) => {
180+ await expect ( result . current . sendMessage ( "Second" ) ) . resolves . toBe ( true ) ;
181+ } ) ;
182+
183+ await waitFor ( ( ) => {
184+ expect ( result . current . messages ) . toEqual ( [
185+ { id : "id-1" , role : "user" , content : "First" } ,
186+ { id : "id-2" , role : "assistant" , content : "Partial" } ,
187+ { id : "id-3" , role : "user" , content : "Second" } ,
188+ { id : "id-4" , role : "assistant" , content : "Fresh" } ,
189+ ] ) ;
190+ } ) ;
191+
192+ releaseStoppedRequest ( ) ;
193+ await act ( async ( ) => {
194+ await expect ( firstSendPromise ) . resolves . toBe ( false ) ;
195+ } ) ;
196+
197+ await waitFor ( ( ) => {
198+ expect ( result . current . messages ) . toEqual ( [
199+ { id : "id-1" , role : "user" , content : "First" } ,
200+ { id : "id-2" , role : "assistant" , content : "Partial" } ,
201+ { id : "id-3" , role : "user" , content : "Second" } ,
202+ { id : "id-4" , role : "assistant" , content : "Fresh" } ,
203+ ] ) ;
204+ } ) ;
205+ } ) ;
206+
134207 it ( "sets error state and recovers on the next successful send" , async ( ) => {
135208 let shouldFail = true ;
136209 const client = {
0 commit comments