11// npx vitest src/core/assistant-message/__tests__/presentAssistantMessage-custom-tool.spec.ts
22
3+ import { DispatchState } from "../../task/dispatchState"
34import { describe , it , expect , beforeEach , vi } from "vitest"
45import { presentAssistantMessage } from "../presentAssistantMessage"
56import { validateToolUse } from "../../tools/validateToolUse"
@@ -67,8 +68,8 @@ describe("presentAssistantMessage - Custom Tool Recording", () => {
6768 taskId : "test-task-id" ,
6869 instanceId : "test-instance" ,
6970 abort : false ,
70- presentAssistantMessageLocked : false ,
71- presentAssistantMessageHasPendingUpdates : false ,
71+ dispatchState : DispatchState . IDLE ,
72+ pendingWork : null ,
7273 currentStreamingContentIndex : 0 ,
7374 assistantMessageContent : [ ] ,
7475 userMessageContent : [ ] ,
@@ -224,17 +225,17 @@ describe("presentAssistantMessage - Custom Tool Recording", () => {
224225 } )
225226 } )
226227
227- describe ( "Presentation lock ownership" , ( ) => {
228+ describe ( "Dispatch state ownership" , ( ) => {
228229 it ( "drains an update queued during the helper-to-wrapper handoff" , async ( ) => {
229230 mockTask . assistantMessageContent = [ { type : "text" , content : "initial" , partial : true } ]
230231 let queuedCall : Promise < void > | undefined
231- let lockedDuringHandoff = false
232+ let busyDuringHandoff = false
232233 mockTask . say . mockImplementationOnce ( async ( ) => {
233234 // The first microtask precedes the helper's await continuation.
234235 // The second follows that continuation but precedes the wrapper's.
235236 queueMicrotask ( ( ) => {
236237 queueMicrotask ( ( ) => {
237- lockedDuringHandoff = mockTask . presentAssistantMessageLocked
238+ busyDuringHandoff = mockTask . dispatchState !== DispatchState . IDLE
238239 mockTask . assistantMessageContent [ 0 ] = { type : "text" , content : "final" , partial : false }
239240 mockTask . didCompleteReadingStream = true
240241 queuedCall = presentAssistantMessage ( mockTask , baseSnapshot )
@@ -245,15 +246,15 @@ describe("presentAssistantMessage - Custom Tool Recording", () => {
245246 await presentAssistantMessage ( mockTask , baseSnapshot )
246247 await queuedCall
247248
248- expect ( lockedDuringHandoff ) . toBe ( true )
249+ expect ( busyDuringHandoff ) . toBe ( true )
249250 expect ( mockTask . say . mock . calls ) . toEqual ( [
250251 [ "text" , "initial" , undefined , true ] ,
251252 [ "text" , "final" , undefined , false ] ,
252253 ] )
253254 expect ( mockTask . currentStreamingContentIndex ) . toBe ( 1 )
254255 expect ( mockTask . userMessageContentReady ) . toBe ( true )
255- expect ( mockTask . presentAssistantMessageHasPendingUpdates ) . toBe ( false )
256- expect ( mockTask . presentAssistantMessageLocked ) . toBe ( false )
256+ expect ( mockTask . pendingWork ) . toBeNull ( )
257+ expect ( mockTask . dispatchState ) . toBe ( DispatchState . IDLE )
257258 } )
258259
259260 it ( "stops draining an update queued during the handoff once the task aborts" , async ( ) => {
@@ -279,7 +280,7 @@ describe("presentAssistantMessage - Custom Tool Recording", () => {
279280 mockTask . assistantMessageContent [ 0 ] = { type : "text" , content : "final" , partial : false }
280281 mockTask . didCompleteReadingStream = true
281282 queuedCall = presentAssistantMessage ( mockTask , baseSnapshot )
282- pendingDuringHandoff = mockTask . presentAssistantMessageHasPendingUpdates
283+ pendingDuringHandoff = mockTask . pendingWork !== null
283284 aborted = true
284285 } )
285286 } )
@@ -291,30 +292,30 @@ describe("presentAssistantMessage - Custom Tool Recording", () => {
291292 expect ( pendingDuringHandoff ) . toBe ( true )
292293 expect ( mockTask . say ) . toHaveBeenCalledExactlyOnceWith ( "text" , "initial" , undefined , true )
293294 expect ( mockTask . currentStreamingContentIndex ) . toBe ( 0 )
294- expect ( mockTask . presentAssistantMessageLocked ) . toBe ( false )
295+ expect ( mockTask . dispatchState ) . toBe ( DispatchState . IDLE )
295296 } )
296297
297- it ( "holds one lock across consecutive blocks and queues overlapping calls" , async ( ) => {
298+ it ( "holds SERIAL across consecutive blocks and queues overlapping calls" , async ( ) => {
298299 mockTask . assistantMessageContent = [
299300 { type : "text" , content : "first" , partial : false } ,
300301 { type : "text" , content : "second" , partial : false } ,
301302 ]
302303 mockTask . didCompleteReadingStream = true
303- const lockChanges : boolean [ ] = [ ]
304- let locked = false
305- Object . defineProperty ( mockTask , "presentAssistantMessageLocked " , {
304+ const stateChanges : DispatchState [ ] = [ ]
305+ let state = DispatchState . IDLE
306+ Object . defineProperty ( mockTask , "dispatchState " , {
306307 configurable : true ,
307- get : ( ) => locked ,
308- set : ( value : boolean ) => {
309- locked = value
310- lockChanges . push ( value )
308+ get : ( ) => state ,
309+ set : ( value : DispatchState ) => {
310+ state = value
311+ stateChanges . push ( value )
311312 } ,
312313 } )
313314 mockTask . say . mockImplementation ( async ( _type : string , content : string ) => {
314- expect ( mockTask . presentAssistantMessageLocked ) . toBe ( true )
315+ expect ( mockTask . dispatchState ) . toBe ( DispatchState . SERIAL )
315316 if ( content === "second" ) {
316317 await presentAssistantMessage ( mockTask , baseSnapshot )
317- expect ( mockTask . presentAssistantMessageHasPendingUpdates ) . toBe ( true )
318+ expect ( mockTask . pendingWork ) . toEqual ( { mode : "serial" } )
318319 }
319320 } )
320321
@@ -326,8 +327,8 @@ describe("presentAssistantMessage - Custom Tool Recording", () => {
326327 ] )
327328 expect ( mockTask . currentStreamingContentIndex ) . toBe ( 2 )
328329 expect ( mockTask . userMessageContentReady ) . toBe ( true )
329- expect ( mockTask . presentAssistantMessageHasPendingUpdates ) . toBe ( false )
330- expect ( lockChanges ) . toEqual ( [ true , false ] )
330+ expect ( mockTask . pendingWork ) . toBeNull ( )
331+ expect ( stateChanges ) . toEqual ( [ DispatchState . SERIAL , DispatchState . IDLE ] )
331332 } )
332333
333334 it ( "consumes a pending update for a partial block without repeating it indefinitely" , async ( ) => {
@@ -336,11 +337,11 @@ describe("presentAssistantMessage - Custom Tool Recording", () => {
336337 . mockImplementationOnce ( async ( ) => {
337338 mockTask . assistantMessageContent [ 0 ] = { type : "text" , content : "updated" , partial : true }
338339 await presentAssistantMessage ( mockTask , baseSnapshot )
339- expect ( mockTask . presentAssistantMessageHasPendingUpdates ) . toBe ( true )
340+ expect ( mockTask . pendingWork ) . toEqual ( { mode : "serial" } )
340341 } )
341342 . mockImplementationOnce ( async ( ) => {
342- expect ( mockTask . presentAssistantMessageLocked ) . toBe ( true )
343- expect ( mockTask . presentAssistantMessageHasPendingUpdates ) . toBe ( false )
343+ expect ( mockTask . dispatchState ) . toBe ( DispatchState . SERIAL )
344+ expect ( mockTask . pendingWork ) . toBeNull ( )
344345 } )
345346
346347 await presentAssistantMessage ( mockTask , baseSnapshot )
@@ -350,8 +351,8 @@ describe("presentAssistantMessage - Custom Tool Recording", () => {
350351 [ "text" , "updated" , undefined , true ] ,
351352 ] )
352353 expect ( mockTask . currentStreamingContentIndex ) . toBe ( 0 )
353- expect ( mockTask . presentAssistantMessageHasPendingUpdates ) . toBe ( false )
354- expect ( mockTask . presentAssistantMessageLocked ) . toBe ( false )
354+ expect ( mockTask . pendingWork ) . toBeNull ( )
355+ expect ( mockTask . dispatchState ) . toBe ( DispatchState . IDLE )
355356 } )
356357
357358 it ( "stops internal continuation when the task aborts between blocks" , async ( ) => {
@@ -367,10 +368,10 @@ describe("presentAssistantMessage - Custom Tool Recording", () => {
367368
368369 expect ( mockTask . say ) . toHaveBeenCalledExactlyOnceWith ( "text" , "first" , undefined , false )
369370 expect ( mockTask . currentStreamingContentIndex ) . toBe ( 1 )
370- expect ( mockTask . presentAssistantMessageLocked ) . toBe ( false )
371+ expect ( mockTask . dispatchState ) . toBe ( DispatchState . IDLE )
371372 } )
372373
373- it ( "releases the presentation lock when a later block throws" , async ( ) => {
374+ it ( "returns to IDLE when a later block throws" , async ( ) => {
374375 mockTask . assistantMessageContent = [
375376 { type : "text" , content : "first" , partial : false } ,
376377 { type : "text" , content : "second" , partial : false } ,
@@ -381,12 +382,12 @@ describe("presentAssistantMessage - Custom Tool Recording", () => {
381382
382383 expect ( mockTask . say ) . toHaveBeenCalledTimes ( 2 )
383384 expect ( mockTask . currentStreamingContentIndex ) . toBe ( 1 )
384- expect ( mockTask . presentAssistantMessageLocked ) . toBe ( false )
385+ expect ( mockTask . dispatchState ) . toBe ( DispatchState . IDLE )
385386 } )
386387 } )
387388
388389 describe ( "Custom tool error recording" , ( ) => {
389- it ( "releases the presentation lock when dispatch throws" , async ( ) => {
390+ it ( "returns to IDLE when dispatch throws" , async ( ) => {
390391 mockTask . assistantMessageContent = [
391392 {
392393 type : "tool_use" ,
@@ -404,7 +405,7 @@ describe("presentAssistantMessage - Custom Tool Recording", () => {
404405
405406 try {
406407 await expect ( presentAssistantMessage ( mockTask , baseSnapshot ) ) . rejects . toThrow ( "dispatch failed" )
407- expect ( mockTask . presentAssistantMessageLocked ) . toBe ( false )
408+ expect ( mockTask . dispatchState ) . toBe ( DispatchState . IDLE )
408409 } finally {
409410 handleSpy . mockRestore ( )
410411 }
0 commit comments