test: add more tests for AskUser (#17720)

This commit is contained in:
Jack Wotherspoon
2026-01-28 11:57:16 -05:00
committed by GitHub
parent 65accca296
commit 25ae1a1b54
4 changed files with 198 additions and 51 deletions

View File

@@ -29,7 +29,13 @@ import {
type ResumedSessionData,
AuthType,
type AgentDefinition,
MessageBusType,
QuestionType,
} from '@google/gemini-cli-core';
import {
AskUserActionsContext,
type AskUserState,
} from './contexts/AskUserActionsContext.js';
// Mock coreEvents
const mockCoreEvents = vi.hoisted(() => ({
@@ -107,9 +113,11 @@ vi.mock('ink', async (importOriginal) => {
// so we can assert against them in our tests.
let capturedUIState: UIState;
let capturedUIActions: UIActions;
let capturedAskUserRequest: AskUserState | null;
function TestContextConsumer() {
capturedUIState = useContext(UIStateContext)!;
capturedUIActions = useContext(UIActionsContext)!;
capturedAskUserRequest = useContext(AskUserActionsContext)?.request ?? null;
return null;
}
@@ -259,6 +267,7 @@ describe('AppContainer State Management', () => {
mocks.mockStdout.write.mockClear();
capturedUIState = null!;
capturedAskUserRequest = null;
// **Provide a default return value for EVERY mocked hook.**
mockedUseQuotaAndFallback.mockReturnValue({
@@ -2498,6 +2507,41 @@ describe('AppContainer State Management', () => {
unmount!();
});
it('should show ask user dialog when request is received', async () => {
let unmount: () => void;
await act(async () => {
const result = renderAppContainer();
unmount = result.unmount;
});
const questions = [
{
question: 'What is your favorite color?',
header: 'Color Preference',
type: QuestionType.TEXT,
},
];
await act(async () => {
await mockConfig.getMessageBus().publish({
type: MessageBusType.ASK_USER_REQUEST,
questions,
correlationId: 'test-id',
});
});
await waitFor(
() => {
expect(capturedAskUserRequest).not.toBeNull();
expect(capturedAskUserRequest?.questions).toEqual(questions);
expect(capturedAskUserRequest?.correlationId).toBe('test-id');
},
{ timeout: 2000 },
);
unmount!();
});
});
describe('Regression Tests', () => {

View File

@@ -8,14 +8,19 @@ import {
renderWithProviders,
createMockSettings,
} from '../../../test-utils/render.js';
import { describe, it, expect, vi } from 'vitest';
import { describe, it, expect, vi, afterEach } from 'vitest';
import { ToolGroupMessage } from './ToolGroupMessage.js';
import type { IndividualToolCallDisplay } from '../../types.js';
import { ToolCallStatus } from '../../types.js';
import { Scrollable } from '../shared/Scrollable.js';
import type { Config } from '@google/gemini-cli-core';
import { ASK_USER_DISPLAY_NAME, makeFakeConfig } from '@google/gemini-cli-core';
import os from 'node:os';
describe('<ToolGroupMessage />', () => {
afterEach(() => {
vi.restoreAllMocks();
});
const createToolCall = (
overrides: Partial<IndividualToolCallDisplay> = {},
): IndividualToolCallDisplay => ({
@@ -35,16 +40,16 @@ describe('<ToolGroupMessage />', () => {
isFocused: true,
};
const baseMockConfig = {
getModel: () => 'gemini-pro',
getTargetDir: () => '/test',
getDebugMode: () => false,
isTrustedFolder: () => true,
getIdeMode: () => false,
getEnableInteractiveShell: () => true,
getPreviewFeatures: () => false,
isEventDrivenSchedulerEnabled: () => true,
} as unknown as Config;
const baseMockConfig = makeFakeConfig({
model: 'gemini-pro',
targetDir: os.tmpdir(),
debugMode: false,
folderTrust: false,
ideMode: false,
enableInteractiveShell: true,
previewFeatures: false,
enableEventDrivenScheduler: true,
});
describe('Golden Snapshots', () => {
it('renders single successful tool call', () => {
@@ -83,10 +88,11 @@ describe('<ToolGroupMessage />', () => {
status: ToolCallStatus.Error,
}),
];
const mockConfig = {
...baseMockConfig,
isEventDrivenSchedulerEnabled: () => false,
} as unknown as Config;
const mockConfig = makeFakeConfig({
model: 'gemini-pro',
targetDir: os.tmpdir(),
enableEventDrivenScheduler: false,
});
const { lastFrame, unmount } = renderWithProviders(
<ToolGroupMessage {...baseProps} toolCalls={toolCalls} />,
@@ -116,10 +122,11 @@ describe('<ToolGroupMessage />', () => {
},
}),
];
const mockConfig = {
...baseMockConfig,
isEventDrivenSchedulerEnabled: () => false,
} as unknown as Config;
const mockConfig = makeFakeConfig({
model: 'gemini-pro',
targetDir: os.tmpdir(),
enableEventDrivenScheduler: false,
});
const { lastFrame, unmount } = renderWithProviders(
<ToolGroupMessage {...baseProps} toolCalls={toolCalls} />,
@@ -177,10 +184,11 @@ describe('<ToolGroupMessage />', () => {
status: ToolCallStatus.Pending,
}),
];
const mockConfig = {
...baseMockConfig,
isEventDrivenSchedulerEnabled: () => false,
} as unknown as Config;
const mockConfig = makeFakeConfig({
model: 'gemini-pro',
targetDir: os.tmpdir(),
enableEventDrivenScheduler: false,
});
const { lastFrame, unmount } = renderWithProviders(
<ToolGroupMessage {...baseProps} toolCalls={toolCalls} />,
@@ -381,10 +389,11 @@ describe('<ToolGroupMessage />', () => {
describe('Border Color Logic', () => {
it('uses yellow border when tools are pending', () => {
const toolCalls = [createToolCall({ status: ToolCallStatus.Pending })];
const mockConfig = {
...baseMockConfig,
isEventDrivenSchedulerEnabled: () => false,
} as unknown as Config;
const mockConfig = makeFakeConfig({
model: 'gemini-pro',
targetDir: os.tmpdir(),
enableEventDrivenScheduler: false,
});
const { lastFrame, unmount } = renderWithProviders(
<ToolGroupMessage {...baseProps} toolCalls={toolCalls} />,
@@ -503,10 +512,11 @@ describe('<ToolGroupMessage />', () => {
},
}),
];
const mockConfig = {
...baseMockConfig,
isEventDrivenSchedulerEnabled: () => false,
} as unknown as Config;
const mockConfig = makeFakeConfig({
model: 'gemini-pro',
targetDir: os.tmpdir(),
enableEventDrivenScheduler: false,
});
const { lastFrame, unmount } = renderWithProviders(
<ToolGroupMessage {...baseProps} toolCalls={toolCalls} />,
@@ -539,10 +549,11 @@ describe('<ToolGroupMessage />', () => {
const settings = createMockSettings({
security: { enablePermanentToolApproval: true },
});
const mockConfig = {
...baseMockConfig,
isEventDrivenSchedulerEnabled: () => false,
} as unknown as Config;
const mockConfig = makeFakeConfig({
model: 'gemini-pro',
targetDir: os.tmpdir(),
enableEventDrivenScheduler: false,
});
const { lastFrame, unmount } = renderWithProviders(
<ToolGroupMessage {...baseProps} toolCalls={toolCalls} />,
@@ -574,10 +585,11 @@ describe('<ToolGroupMessage />', () => {
}),
];
const mockConfig = {
...baseMockConfig,
isEventDrivenSchedulerEnabled: () => false,
} as unknown as Config;
const mockConfig = makeFakeConfig({
model: 'gemini-pro',
targetDir: os.tmpdir(),
enableEventDrivenScheduler: false,
});
const { lastFrame, unmount } = renderWithProviders(
<ToolGroupMessage {...baseProps} toolCalls={toolCalls} />,
@@ -604,10 +616,7 @@ describe('<ToolGroupMessage />', () => {
}),
];
const mockConfig = {
...baseMockConfig,
isEventDrivenSchedulerEnabled: () => true,
} as unknown as Config;
const mockConfig = baseMockConfig;
const { lastFrame, unmount } = renderWithProviders(
<ToolGroupMessage {...baseProps} toolCalls={toolCalls} />,
@@ -640,10 +649,7 @@ describe('<ToolGroupMessage />', () => {
}),
];
const mockConfig = {
...baseMockConfig,
isEventDrivenSchedulerEnabled: () => true,
} as unknown as Config;
const mockConfig = baseMockConfig;
const { lastFrame, unmount } = renderWithProviders(
<ToolGroupMessage {...baseProps} toolCalls={toolCalls} />,
@@ -658,4 +664,72 @@ describe('<ToolGroupMessage />', () => {
unmount();
});
});
describe('Ask User Filtering', () => {
it.each([
ToolCallStatus.Pending,
ToolCallStatus.Executing,
ToolCallStatus.Confirming,
])('filters out ask_user when status is %s', (status) => {
const toolCalls = [
createToolCall({
callId: `ask-user-${status}`,
name: ASK_USER_DISPLAY_NAME,
status,
}),
];
const { lastFrame, unmount } = renderWithProviders(
<ToolGroupMessage {...baseProps} toolCalls={toolCalls} />,
{ config: baseMockConfig },
);
expect(lastFrame()).toMatchSnapshot();
unmount();
});
it.each([ToolCallStatus.Success, ToolCallStatus.Error])(
'does NOT filter out ask_user when status is %s',
(status) => {
const toolCalls = [
createToolCall({
callId: `ask-user-${status}`,
name: ASK_USER_DISPLAY_NAME,
status,
}),
];
const { lastFrame, unmount } = renderWithProviders(
<ToolGroupMessage {...baseProps} toolCalls={toolCalls} />,
{ config: baseMockConfig },
);
expect(lastFrame()).toMatchSnapshot();
unmount();
},
);
it('shows other tools when ask_user is filtered out', () => {
const toolCalls = [
createToolCall({
callId: 'other-tool',
name: 'other-tool',
status: ToolCallStatus.Success,
}),
createToolCall({
callId: 'ask-user-pending',
name: ASK_USER_DISPLAY_NAME,
status: ToolCallStatus.Pending,
}),
];
const { lastFrame, unmount } = renderWithProviders(
<ToolGroupMessage {...baseProps} toolCalls={toolCalls} />,
{ config: baseMockConfig },
);
expect(lastFrame()).toMatchSnapshot();
unmount();
});
});
});

View File

@@ -1,5 +1,35 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`<ToolGroupMessage /> > Ask User Filtering > does NOT filter out ask_user when status is Error 1`] = `
"╭──────────────────────────────────────────────────────────────────────────────╮
│ x Ask User │
│ │
│ Test result │
╰──────────────────────────────────────────────────────────────────────────────╯"
`;
exports[`<ToolGroupMessage /> > Ask User Filtering > does NOT filter out ask_user when status is Success 1`] = `
"╭──────────────────────────────────────────────────────────────────────────────╮
│ ✓ Ask User │
│ │
│ Test result │
╰──────────────────────────────────────────────────────────────────────────────╯"
`;
exports[`<ToolGroupMessage /> > Ask User Filtering > filters out ask_user when status is Confirming 1`] = `""`;
exports[`<ToolGroupMessage /> > Ask User Filtering > filters out ask_user when status is Executing 1`] = `""`;
exports[`<ToolGroupMessage /> > Ask User Filtering > filters out ask_user when status is Pending 1`] = `""`;
exports[`<ToolGroupMessage /> > Ask User Filtering > shows other tools when ask_user is filtered out 1`] = `
"╭──────────────────────────────────────────────────────────────────────────────╮
│ ✓ other-tool A tool for testing │
│ │
│ Test result │
╰──────────────────────────────────────────────────────────────────────────────╯"
`;
exports[`<ToolGroupMessage /> > Border Color Logic > uses gray border when all tools are successful and no shell commands 1`] = `
"╭──────────────────────────────────────────────────────────────────────────────╮
│ ✓ test-tool A tool for testing │

View File

@@ -24,9 +24,8 @@ interface AskUserActionsContextValue {
cancel: () => void;
}
const AskUserActionsContext = createContext<AskUserActionsContextValue | null>(
null,
);
export const AskUserActionsContext =
createContext<AskUserActionsContextValue | null>(null);
export const useAskUserActions = () => {
const context = useContext(AskUserActionsContext);