mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-08-08 16:11:58 -07:00
174 lines
5.1 KiB
TypeScript
174 lines
5.1 KiB
TypeScript
/**
|
|
* @license
|
|
* Copyright 2026 Google LLC
|
|
* SPDX-License-Identifier: Apache-2.0
|
|
*/
|
|
|
|
import { describe, it, expect, vi, beforeEach, type Mock } from 'vitest';
|
|
import { Task } from './task.js';
|
|
import {
|
|
MessageBusType,
|
|
CoreToolCallStatus,
|
|
type Config,
|
|
type MessageBus,
|
|
} from '@google/gemini-cli-core';
|
|
import { createMockConfig } from '../utils/testing_utils.js';
|
|
import type { RequestContext } from '@a2a-js/sdk/server';
|
|
|
|
describe('Task Race Condition', () => {
|
|
let mockConfig: Config;
|
|
let messageBus: MessageBus;
|
|
|
|
beforeEach(() => {
|
|
messageBus = {
|
|
subscribe: vi.fn(),
|
|
unsubscribe: vi.fn(),
|
|
publish: vi.fn(),
|
|
} as unknown as MessageBus;
|
|
mockConfig = createMockConfig({
|
|
messageBus,
|
|
}) as Config;
|
|
});
|
|
|
|
it('should not hang when multiple tool confirmations are processed while waiting', async () => {
|
|
// @ts-expect-error - private constructor
|
|
const task = new Task('task-id', 'context-id', mockConfig);
|
|
|
|
// 1. Register two tools as scheduled
|
|
task['_registerToolCall']('tool-1', 'scheduled');
|
|
task['_registerToolCall']('tool-2', 'scheduled');
|
|
|
|
// 2. Both transition to awaiting_approval
|
|
const updateHandler = (messageBus.subscribe as Mock).mock.calls.find(
|
|
(c: unknown[]) => c[0] === MessageBusType.TOOL_CALLS_UPDATE,
|
|
)?.[1];
|
|
|
|
updateHandler({
|
|
type: MessageBusType.TOOL_CALLS_UPDATE,
|
|
schedulerId: 'task-id',
|
|
toolCalls: [
|
|
{
|
|
request: { callId: 'tool-1', name: 't1' },
|
|
status: CoreToolCallStatus.AwaitingApproval,
|
|
correlationId: 'corr-1',
|
|
confirmationDetails: { type: 'info' },
|
|
},
|
|
{
|
|
request: { callId: 'tool-2', name: 't2' },
|
|
status: CoreToolCallStatus.AwaitingApproval,
|
|
correlationId: 'corr-2',
|
|
confirmationDetails: { type: 'info' },
|
|
},
|
|
],
|
|
});
|
|
|
|
// 3. Confirm Tool 1. This makes isAwaitingApprovalOnly() return false.
|
|
for await (const _ of task.acceptUserMessage(
|
|
{
|
|
userMessage: {
|
|
parts: [
|
|
{
|
|
kind: 'data',
|
|
data: { callId: 'tool-1', outcome: 'proceed_once' },
|
|
},
|
|
],
|
|
},
|
|
} as unknown as RequestContext,
|
|
new AbortController().signal,
|
|
)) {
|
|
// consume generator
|
|
}
|
|
|
|
// 4. Start waiting. This should now block because Tool 1 is confirmed (so we are waiting for its execution).
|
|
const waitPromise = task.waitForPendingTools();
|
|
|
|
// 5. Confirm Tool 2 while waiting.
|
|
for await (const _ of task.acceptUserMessage(
|
|
{
|
|
userMessage: {
|
|
parts: [
|
|
{
|
|
kind: 'data',
|
|
data: { callId: 'tool-2', outcome: 'proceed_once' },
|
|
},
|
|
],
|
|
},
|
|
} as unknown as RequestContext,
|
|
new AbortController().signal,
|
|
)) {
|
|
// consume generator
|
|
}
|
|
|
|
// 6. Both tools complete successfully
|
|
updateHandler({
|
|
type: MessageBusType.TOOL_CALLS_UPDATE,
|
|
schedulerId: 'task-id',
|
|
toolCalls: [
|
|
{
|
|
request: { callId: 'tool-1', name: 't1' },
|
|
status: CoreToolCallStatus.Success,
|
|
response: { responseParts: [] },
|
|
},
|
|
{
|
|
request: { callId: 'tool-2', name: 't2' },
|
|
status: CoreToolCallStatus.Success,
|
|
response: { responseParts: [] },
|
|
},
|
|
],
|
|
});
|
|
|
|
// 7. Verify that the original waitPromise resolves.
|
|
await expect(waitPromise).resolves.toBeUndefined();
|
|
});
|
|
|
|
it('should reject waitForPendingTools when tools are cancelled', async () => {
|
|
// @ts-expect-error - private constructor
|
|
const task = new Task('task-id', 'context-id', mockConfig);
|
|
|
|
// 1. Register a tool
|
|
task['_registerToolCall']('tool-1', 'scheduled');
|
|
|
|
// 2. Start waiting
|
|
const waitPromise = task.waitForPendingTools();
|
|
|
|
// 3. Cancel pending tools
|
|
task.cancelPendingTools('User requested cancellation');
|
|
|
|
// 4. Verify waitPromise rejects with the reason
|
|
await expect(waitPromise).rejects.toThrow('User requested cancellation');
|
|
});
|
|
|
|
it('should handle concurrent tool scheduling correctly', async () => {
|
|
// @ts-expect-error - private constructor
|
|
const task = new Task('task-id', 'context-id', mockConfig);
|
|
|
|
// 1. Register a tool and start waiting
|
|
task['_registerToolCall']('tool-1', 'scheduled');
|
|
const waitPromise = task.waitForPendingTools();
|
|
|
|
// 2. Schedule another tool concurrently (e.g. from a secondary user message)
|
|
// This should NOT resolve the current waitPromise until both are done
|
|
await task.scheduleToolCalls(
|
|
[{ callId: 'tool-2', name: 't2', args: {} }],
|
|
new AbortController().signal,
|
|
);
|
|
|
|
expect(task['pendingToolCalls'].size).toBe(2);
|
|
|
|
// 3. Resolve tool 1
|
|
task['_resolveToolCall']('tool-1');
|
|
|
|
// 4. Verify waitPromise is still pending
|
|
let resolved = false;
|
|
waitPromise.then(() => (resolved = true));
|
|
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
expect(resolved).toBe(false);
|
|
|
|
// 5. Resolve tool 2
|
|
task['_resolveToolCall']('tool-2');
|
|
|
|
// 6. Now it should resolve
|
|
await expect(waitPromise).resolves.toBeUndefined();
|
|
});
|
|
});
|