@@ -12,10 +12,16 @@ const { isSimExecuted, executeTool, ensureHandlersRegistered } = vi.hoisted(() =
1212 ensureHandlersRegistered : vi . fn ( ) ,
1313} ) )
1414
15- const { upsertAsyncToolCall, markAsyncToolRunning, completeAsyncToolCall } = vi . hoisted ( ( ) => ( {
16- upsertAsyncToolCall : vi . fn ( ) ,
17- markAsyncToolRunning : vi . fn ( ) ,
18- completeAsyncToolCall : vi . fn ( ) ,
15+ const { upsertAsyncToolCall, markAsyncToolRunning, completeAsyncToolCall, markAsyncToolDelivered } =
16+ vi . hoisted ( ( ) => ( {
17+ upsertAsyncToolCall : vi . fn ( ) ,
18+ markAsyncToolRunning : vi . fn ( ) ,
19+ completeAsyncToolCall : vi . fn ( ) ,
20+ markAsyncToolDelivered : vi . fn ( ) ,
21+ } ) )
22+
23+ const { waitForToolCompletion } = vi . hoisted ( ( ) => ( {
24+ waitForToolCompletion : vi . fn ( ) ,
1925} ) )
2026
2127vi . mock ( '@/lib/copilot/tool-executor' , ( ) => ( {
@@ -34,16 +40,20 @@ vi.mock('@/lib/copilot/async-runs/repository', () => ({
3440 createRunCheckpoint : vi . fn ( ) ,
3541 getAsyncToolCall : vi . fn ( ) ,
3642 markAsyncToolStatus : vi . fn ( ) ,
37- markAsyncToolDelivered : vi . fn ( ) ,
3843 listAsyncToolCallsForRun : vi . fn ( ) ,
3944 getAsyncToolCalls : vi . fn ( ) ,
4045 claimCompletedAsyncToolCall : vi . fn ( ) ,
4146 releaseCompletedAsyncToolClaim : vi . fn ( ) ,
4247 upsertAsyncToolCall,
4348 markAsyncToolRunning,
49+ markAsyncToolDelivered,
4450 completeAsyncToolCall,
4551} ) )
4652
53+ vi . mock ( '@/lib/copilot/request/tools/client' , ( ) => ( {
54+ waitForToolCompletion,
55+ } ) )
56+
4757import {
4858 MothershipStreamV1AsyncToolRecordStatus ,
4959 MothershipStreamV1EventType ,
@@ -68,6 +78,8 @@ describe('sse-handlers tool lifecycle', () => {
6878 upsertAsyncToolCall . mockResolvedValue ( null )
6979 markAsyncToolRunning . mockResolvedValue ( null )
7080 completeAsyncToolCall . mockResolvedValue ( null )
81+ markAsyncToolDelivered . mockResolvedValue ( null )
82+ waitForToolCompletion . mockResolvedValue ( null )
7183 context = {
7284 chatId : undefined ,
7385 messageId : 'msg-1' ,
@@ -236,6 +248,51 @@ describe('sse-handlers tool lifecycle', () => {
236248 expect ( updated ?. result ?. output ) . toBe ( 'done' )
237249 } )
238250
251+ it ( 'marks background client workflow tools delivered after synthetic result emission' , async ( ) => {
252+ waitForToolCompletion . mockResolvedValueOnce ( {
253+ status : 'background' ,
254+ data : { detached : true } ,
255+ } )
256+ const onEvent = vi . fn ( )
257+
258+ await sseHandlers . tool (
259+ {
260+ type : MothershipStreamV1EventType . tool ,
261+ payload : {
262+ toolCallId : 'tool-background' ,
263+ toolName : 'run_workflow' ,
264+ arguments : { workflowId : 'workflow-1' } ,
265+ executor : MothershipStreamV1ToolExecutor . client ,
266+ mode : MothershipStreamV1ToolMode . async ,
267+ phase : MothershipStreamV1ToolPhase . call ,
268+ } ,
269+ } satisfies StreamEvent ,
270+ context ,
271+ execContext ,
272+ { onEvent, interactive : true , timeout : 1000 }
273+ )
274+
275+ await sleep ( 0 )
276+ await Promise . allSettled ( context . pendingToolPromises . values ( ) )
277+
278+ expect ( markAsyncToolDelivered ) . toHaveBeenCalledWith ( 'tool-background' )
279+ expect ( onEvent ) . toHaveBeenCalledWith (
280+ expect . objectContaining ( {
281+ type : MothershipStreamV1EventType . tool ,
282+ payload : expect . objectContaining ( {
283+ toolCallId : 'tool-background' ,
284+ phase : MothershipStreamV1ToolPhase . result ,
285+ status : MothershipStreamV1ToolOutcome . skipped ,
286+ success : true ,
287+ output : { detached : true } ,
288+ } ) ,
289+ } )
290+ )
291+ expect ( context . toolCalls . get ( 'tool-background' ) ?. status ) . toBe (
292+ MothershipStreamV1ToolOutcome . skipped
293+ )
294+ } )
295+
239296 it ( 'does not add hidden tool calls to content blocks' , async ( ) => {
240297 executeTool . mockResolvedValueOnce ( { success : true , output : { skill : 'ok' } } )
241298
0 commit comments