Skip to content

Commit 687ca40

Browse files
chrstnbscidomino
authored andcommitted
Fix race condition by awaiting scheduleToolCalls (#16759)
Co-authored-by: Tommaso Sciortino <sciortino@gmail.com>
1 parent def0977 commit 687ca40

3 files changed

Lines changed: 86 additions & 52 deletions

File tree

packages/cli/src/ui/hooks/useGeminiStream.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -444,7 +444,7 @@ export const useGeminiStream = (
444444
isClientInitiated: true,
445445
prompt_id,
446446
};
447-
scheduleToolCalls([toolCallRequest], abortSignal);
447+
await scheduleToolCalls([toolCallRequest], abortSignal);
448448
return { queryToSend: null, shouldProceed: false };
449449
}
450450
case 'submit_prompt': {
@@ -911,7 +911,7 @@ export const useGeminiStream = (
911911
}
912912
}
913913
if (toolCallRequests.length > 0) {
914-
scheduleToolCalls(toolCallRequests, signal);
914+
await scheduleToolCalls(toolCallRequests, signal);
915915
}
916916
return StreamProcessingStatus.Completed;
917917
},

packages/cli/src/ui/hooks/useReactToolScheduler.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ import { ToolCallStatus } from '../types.js';
3131
export type ScheduleFn = (
3232
request: ToolCallRequestInfo | ToolCallRequestInfo[],
3333
signal: AbortSignal,
34-
) => void;
34+
) => Promise<void>;
3535
export type MarkToolsAsSubmittedFn = (callIds: string[]) => void;
3636

3737
export type TrackedScheduledToolCall = ScheduledToolCall & {
@@ -180,7 +180,7 @@ export function useReactToolScheduler(
180180
signal: AbortSignal,
181181
) => {
182182
setToolCallsForDisplay([]);
183-
void scheduler.schedule(request, signal);
183+
return scheduler.schedule(request, signal);
184184
},
185185
[scheduler, setToolCallsForDisplay],
186186
);

packages/cli/src/ui/hooks/useToolScheduler.test.ts

Lines changed: 82 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -163,8 +163,8 @@ describe('useReactToolScheduler in YOLO Mode', () => {
163163
args: { data: 'any data' },
164164
} as any;
165165

166-
act(() => {
167-
schedule(request, new AbortController().signal);
166+
await act(async () => {
167+
await schedule(request, new AbortController().signal);
168168
});
169169

170170
await act(async () => {
@@ -220,11 +220,11 @@ describe('useReactToolScheduler', () => {
220220
schedule: (
221221
req: ToolCallRequestInfo | ToolCallRequestInfo[],
222222
signal: AbortSignal,
223-
) => void,
223+
) => Promise<void>,
224224
request: ToolCallRequestInfo | ToolCallRequestInfo[],
225225
) => {
226-
act(() => {
227-
schedule(request, new AbortController().signal);
226+
await act(async () => {
227+
await schedule(request, new AbortController().signal);
228228
});
229229

230230
await advanceAndSettle();
@@ -313,10 +313,13 @@ describe('useReactToolScheduler', () => {
313313

314314
it('should clear previous tool calls when scheduling new ones', async () => {
315315
mockToolRegistry.getTool.mockReturnValue(mockTool);
316-
(mockTool.execute as Mock).mockResolvedValue({
317-
llmContent: 'Tool output',
318-
returnDisplay: 'Formatted tool output',
319-
} as ToolResult);
316+
(mockTool.execute as Mock).mockImplementation(async () => {
317+
await new Promise((r) => setTimeout(r, 10));
318+
return {
319+
llmContent: 'Tool output',
320+
returnDisplay: 'Formatted tool output',
321+
};
322+
});
320323

321324
const { result } = renderScheduler();
322325
const schedule = result.current[1];
@@ -337,10 +340,13 @@ describe('useReactToolScheduler', () => {
337340
name: 'mockTool',
338341
args: {},
339342
} as any;
340-
act(() => {
341-
schedule(newRequest, new AbortController().signal);
343+
let schedulePromise: Promise<void>;
344+
await act(async () => {
345+
schedulePromise = schedule(newRequest, new AbortController().signal);
342346
});
343347

348+
await advanceAndSettle();
349+
344350
// After scheduling, the old call should be gone,
345351
// and the new one should be in the display in its initial state.
346352
expect(result.current[0].length).toBe(1);
@@ -349,14 +355,13 @@ describe('useReactToolScheduler', () => {
349355

350356
// Let the new call finish.
351357
await act(async () => {
352-
await vi.advanceTimersByTimeAsync(0);
358+
await vi.advanceTimersByTimeAsync(20);
353359
});
360+
354361
await act(async () => {
355-
await vi.advanceTimersByTimeAsync(0);
356-
});
357-
await act(async () => {
358-
await vi.advanceTimersByTimeAsync(0);
362+
await schedulePromise;
359363
});
364+
360365
expect(onComplete).toHaveBeenCalled();
361366
});
362367

@@ -379,16 +384,14 @@ describe('useReactToolScheduler', () => {
379384
args: {},
380385
} as any;
381386

382-
act(() => {
383-
schedule(request, new AbortController().signal);
384-
});
387+
let schedulePromise: Promise<void>;
385388
await act(async () => {
386-
await vi.advanceTimersByTimeAsync(0);
387-
}); // validation
388-
await act(async () => {
389-
await vi.advanceTimersByTimeAsync(0); // Process scheduling
389+
schedulePromise = schedule(request, new AbortController().signal);
390390
});
391391

392+
await advanceAndSettle(); // validation
393+
await advanceAndSettle(); // Process scheduling
394+
392395
// At this point, the tool is 'executing' and waiting on the promise.
393396
expect(result.current[0][0].status).toBe('executing');
394397

@@ -397,9 +400,7 @@ describe('useReactToolScheduler', () => {
397400
cancelAllToolCalls(cancelController.signal);
398401
});
399402

400-
await act(async () => {
401-
await vi.advanceTimersByTimeAsync(0);
402-
});
403+
await advanceAndSettle();
403404

404405
expect(onComplete).toHaveBeenCalledWith([
405406
expect.objectContaining({
@@ -412,6 +413,11 @@ describe('useReactToolScheduler', () => {
412413
await act(async () => {
413414
resolveExecute({ llmContent: 'output', returnDisplay: 'display' });
414415
});
416+
417+
// Now await the schedule promise
418+
await act(async () => {
419+
await schedulePromise;
420+
});
415421
});
416422

417423
it.each([
@@ -511,8 +517,9 @@ describe('useReactToolScheduler', () => {
511517
args: { data: 'sensitive' },
512518
} as any;
513519

514-
act(() => {
515-
schedule(request, new AbortController().signal);
520+
let schedulePromise: Promise<void>;
521+
await act(async () => {
522+
schedulePromise = schedule(request, new AbortController().signal);
516523
});
517524
await advanceAndSettle();
518525

@@ -525,10 +532,13 @@ describe('useReactToolScheduler', () => {
525532
await capturedOnConfirmForTest?.(ToolConfirmationOutcome.ProceedOnce);
526533
});
527534

528-
await advanceAndSettle();
529-
await advanceAndSettle();
530535
await advanceAndSettle();
531536

537+
// Now await the schedule promise as it should complete
538+
await act(async () => {
539+
await schedulePromise;
540+
});
541+
532542
expect(mockOnUserConfirmForToolConfirmation).toHaveBeenCalledWith(
533543
ToolConfirmationOutcome.ProceedOnce,
534544
);
@@ -558,8 +568,9 @@ describe('useReactToolScheduler', () => {
558568
args: {},
559569
} as any;
560570

561-
act(() => {
562-
schedule(request, new AbortController().signal);
571+
let schedulePromise: Promise<void>;
572+
await act(async () => {
573+
schedulePromise = schedule(request, new AbortController().signal);
563574
});
564575
await advanceAndSettle();
565576

@@ -571,8 +582,13 @@ describe('useReactToolScheduler', () => {
571582
await act(async () => {
572583
await capturedOnConfirmForTest?.(ToolConfirmationOutcome.Cancel);
573584
});
585+
574586
await advanceAndSettle();
575-
await advanceAndSettle();
587+
588+
// Now await the schedule promise
589+
await act(async () => {
590+
await schedulePromise;
591+
});
576592

577593
expect(mockOnUserConfirmForToolConfirmation).toHaveBeenCalledWith(
578594
ToolConfirmationOutcome.Cancel,
@@ -619,8 +635,12 @@ describe('useReactToolScheduler', () => {
619635
args: {},
620636
} as any;
621637

622-
act(() => {
623-
result.current[1](request, new AbortController().signal);
638+
let schedulePromise: Promise<void>;
639+
await act(async () => {
640+
schedulePromise = result.current[1](
641+
request,
642+
new AbortController().signal,
643+
);
624644
});
625645
await advanceAndSettle();
626646

@@ -644,7 +664,11 @@ describe('useReactToolScheduler', () => {
644664
} as ToolResult);
645665
});
646666
await advanceAndSettle();
647-
await advanceAndSettle();
667+
668+
// Now await schedule
669+
await act(async () => {
670+
await schedulePromise;
671+
});
648672

649673
const completedCalls = onComplete.mock.calls[0][0] as ToolCall[];
650674
expect(completedCalls[0].status).toBe('success');
@@ -690,8 +714,8 @@ describe('useReactToolScheduler', () => {
690714
{ callId: 'multi2', name: 'tool2', args: { p: 2 } } as any,
691715
];
692716

693-
act(() => {
694-
schedule(requests, new AbortController().signal);
717+
await act(async () => {
718+
await schedule(requests, new AbortController().signal);
695719
});
696720
await act(async () => {
697721
await vi.advanceTimersByTimeAsync(0);
@@ -782,38 +806,48 @@ describe('useReactToolScheduler', () => {
782806
args: {},
783807
} as any;
784808

785-
act(() => {
786-
schedule(request1, new AbortController().signal);
809+
let schedulePromise1: Promise<void>;
810+
let schedulePromise2: Promise<void>;
811+
812+
await act(async () => {
813+
schedulePromise1 = schedule(request1, new AbortController().signal);
787814
});
788815
await act(async () => {
789816
await vi.advanceTimersByTimeAsync(0);
790817
});
791818

792-
act(() => {
793-
schedule(request2, new AbortController().signal);
819+
await act(async () => {
820+
schedulePromise2 = schedule(request2, new AbortController().signal);
794821
});
795822

796823
await act(async () => {
797824
await vi.advanceTimersByTimeAsync(50);
798825
await vi.advanceTimersByTimeAsync(0);
799-
await act(async () => {
800-
await vi.advanceTimersByTimeAsync(0);
801-
});
802826
});
827+
828+
// Wait for first to complete
829+
await act(async () => {
830+
await schedulePromise1;
831+
});
832+
803833
expect(onComplete).toHaveBeenCalledWith([
804834
expect.objectContaining({
805835
status: 'success',
806836
request: request1,
807837
response: expect.objectContaining({ resultDisplay: 'done display' }),
808838
}),
809839
]);
840+
810841
await act(async () => {
811842
await vi.advanceTimersByTimeAsync(50);
812843
await vi.advanceTimersByTimeAsync(0);
813-
await act(async () => {
814-
await vi.advanceTimersByTimeAsync(0);
815-
});
816844
});
845+
846+
// Wait for second to complete
847+
await act(async () => {
848+
await schedulePromise2;
849+
});
850+
817851
expect(onComplete).toHaveBeenCalledWith([
818852
expect.objectContaining({
819853
status: 'success',

0 commit comments

Comments
 (0)