mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-04-24 12:04:56 -07:00
fix(plan): Fix AskUser evals (#22074)
This commit is contained in:
+72
-33
@@ -5,31 +5,62 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { describe, expect } from 'vitest';
|
import { describe, expect } from 'vitest';
|
||||||
import { evalTest } from './test-helper.js';
|
import { appEvalTest, AppEvalCase } from './app-test-helper.js';
|
||||||
|
import { EvalPolicy } from './test-helper.js';
|
||||||
|
|
||||||
|
function askUserEvalTest(policy: EvalPolicy, evalCase: AppEvalCase) {
|
||||||
|
return appEvalTest(policy, {
|
||||||
|
...evalCase,
|
||||||
|
configOverrides: {
|
||||||
|
...evalCase.configOverrides,
|
||||||
|
general: {
|
||||||
|
...evalCase.configOverrides?.general,
|
||||||
|
approvalMode: 'default',
|
||||||
|
enableAutoUpdate: false,
|
||||||
|
enableAutoUpdateNotification: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
files: {
|
||||||
|
...evalCase.files,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
describe('ask_user', () => {
|
describe('ask_user', () => {
|
||||||
evalTest('USUALLY_PASSES', {
|
askUserEvalTest('USUALLY_PASSES', {
|
||||||
name: 'Agent uses AskUser tool to present multiple choice options',
|
name: 'Agent uses AskUser tool to present multiple choice options',
|
||||||
prompt: `Use the ask_user tool to ask me what my favorite color is. Provide 3 options: red, green, or blue.`,
|
prompt: `Use the ask_user tool to ask me what my favorite color is. Provide 3 options: red, green, or blue.`,
|
||||||
|
setup: async (rig) => {
|
||||||
|
rig.setBreakpoint(['ask_user']);
|
||||||
|
},
|
||||||
assert: async (rig) => {
|
assert: async (rig) => {
|
||||||
const wasToolCalled = await rig.waitForToolCall('ask_user');
|
const confirmation = await rig.waitForPendingConfirmation('ask_user');
|
||||||
expect(wasToolCalled, 'Expected ask_user tool to be called').toBe(true);
|
expect(
|
||||||
|
confirmation,
|
||||||
|
'Expected a pending confirmation for ask_user tool',
|
||||||
|
).toBeDefined();
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
evalTest('USUALLY_PASSES', {
|
askUserEvalTest('USUALLY_PASSES', {
|
||||||
name: 'Agent uses AskUser tool to clarify ambiguous requirements',
|
name: 'Agent uses AskUser tool to clarify ambiguous requirements',
|
||||||
files: {
|
files: {
|
||||||
'package.json': JSON.stringify({ name: 'my-app', version: '1.0.0' }),
|
'package.json': JSON.stringify({ name: 'my-app', version: '1.0.0' }),
|
||||||
},
|
},
|
||||||
prompt: `I want to build a new feature in this app. Ask me questions to clarify the requirements before proceeding.`,
|
prompt: `I want to build a new feature in this app. Ask me questions to clarify the requirements before proceeding.`,
|
||||||
|
setup: async (rig) => {
|
||||||
|
rig.setBreakpoint(['ask_user']);
|
||||||
|
},
|
||||||
assert: async (rig) => {
|
assert: async (rig) => {
|
||||||
const wasToolCalled = await rig.waitForToolCall('ask_user');
|
const confirmation = await rig.waitForPendingConfirmation('ask_user');
|
||||||
expect(wasToolCalled, 'Expected ask_user tool to be called').toBe(true);
|
expect(
|
||||||
|
confirmation,
|
||||||
|
'Expected a pending confirmation for ask_user tool',
|
||||||
|
).toBeDefined();
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
evalTest('USUALLY_PASSES', {
|
askUserEvalTest('USUALLY_PASSES', {
|
||||||
name: 'Agent uses AskUser tool before performing significant ambiguous rework',
|
name: 'Agent uses AskUser tool before performing significant ambiguous rework',
|
||||||
files: {
|
files: {
|
||||||
'packages/core/src/index.ts': '// index\nexport const version = "1.0.0";',
|
'packages/core/src/index.ts': '// index\nexport const version = "1.0.0";',
|
||||||
@@ -39,28 +70,37 @@ describe('ask_user', () => {
|
|||||||
}),
|
}),
|
||||||
'README.md': '# Gemini CLI',
|
'README.md': '# Gemini CLI',
|
||||||
},
|
},
|
||||||
prompt: `Refactor the entire core package to be better.`,
|
prompt: `I want to completely rewrite the core package to support the upcoming V2 architecture, but I haven't decided what that looks like yet. We need to figure out the requirements first. Can you ask me some questions to help nail down the design?`,
|
||||||
|
setup: async (rig) => {
|
||||||
|
rig.setBreakpoint(['enter_plan_mode', 'ask_user']);
|
||||||
|
},
|
||||||
assert: async (rig) => {
|
assert: async (rig) => {
|
||||||
const wasPlanModeCalled = await rig.waitForToolCall('enter_plan_mode');
|
// It might call enter_plan_mode first.
|
||||||
expect(wasPlanModeCalled, 'Expected enter_plan_mode to be called').toBe(
|
let confirmation = await rig.waitForPendingConfirmation([
|
||||||
true,
|
'enter_plan_mode',
|
||||||
);
|
'ask_user',
|
||||||
|
]);
|
||||||
|
expect(confirmation, 'Expected a tool call confirmation').toBeDefined();
|
||||||
|
|
||||||
|
if (confirmation?.name === 'enter_plan_mode') {
|
||||||
|
rig.acceptConfirmation('enter_plan_mode');
|
||||||
|
confirmation = await rig.waitForPendingConfirmation('ask_user');
|
||||||
|
}
|
||||||
|
|
||||||
const wasAskUserCalled = await rig.waitForToolCall('ask_user');
|
|
||||||
expect(
|
expect(
|
||||||
wasAskUserCalled,
|
confirmation?.toolName,
|
||||||
'Expected ask_user tool to be called to clarify the significant rework',
|
'Expected ask_user to be called to clarify the significant rework',
|
||||||
).toBe(true);
|
).toBe('ask_user');
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// --- Regression Tests for Recent Fixes ---
|
// --- Regression Tests for Recent Fixes ---
|
||||||
|
|
||||||
// Regression test for issue #20177: Ensure the agent does not use `ask_user` to
|
// Regression test for issue #20177: Ensure the agent does not use \`ask_user\` to
|
||||||
// confirm shell commands. Fixed via prompt refinements and tool definition
|
// confirm shell commands. Fixed via prompt refinements and tool definition
|
||||||
// updates to clarify that shell command confirmation is handled by the UI.
|
// updates to clarify that shell command confirmation is handled by the UI.
|
||||||
// See fix: https://github.com/google-gemini/gemini-cli/pull/20504
|
// See fix: https://github.com/google-gemini/gemini-cli/pull/20504
|
||||||
evalTest('USUALLY_PASSES', {
|
askUserEvalTest('USUALLY_PASSES', {
|
||||||
name: 'Agent does NOT use AskUser to confirm shell commands',
|
name: 'Agent does NOT use AskUser to confirm shell commands',
|
||||||
files: {
|
files: {
|
||||||
'package.json': JSON.stringify({
|
'package.json': JSON.stringify({
|
||||||
@@ -68,25 +108,24 @@ describe('ask_user', () => {
|
|||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
prompt: `Run 'npm run build' in the current directory.`,
|
prompt: `Run 'npm run build' in the current directory.`,
|
||||||
|
setup: async (rig) => {
|
||||||
|
rig.setBreakpoint(['run_shell_command', 'ask_user']);
|
||||||
|
},
|
||||||
assert: async (rig) => {
|
assert: async (rig) => {
|
||||||
await rig.waitForTelemetryReady();
|
const confirmation = await rig.waitForPendingConfirmation([
|
||||||
|
'run_shell_command',
|
||||||
const toolLogs = rig.readToolLogs();
|
'ask_user',
|
||||||
const wasShellCalled = toolLogs.some(
|
]);
|
||||||
(log) => log.toolRequest.name === 'run_shell_command',
|
|
||||||
);
|
|
||||||
const wasAskUserCalled = toolLogs.some(
|
|
||||||
(log) => log.toolRequest.name === 'ask_user',
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
wasShellCalled,
|
confirmation,
|
||||||
'Expected run_shell_command tool to be called',
|
'Expected a pending confirmation for a tool',
|
||||||
).toBe(true);
|
).toBeDefined();
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
wasAskUserCalled,
|
confirmation?.toolName,
|
||||||
'ask_user should not be called to confirm shell commands',
|
'ask_user should not be called to confirm shell commands',
|
||||||
).toBe(false);
|
).toBe('run_shell_command');
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -487,7 +487,7 @@ export class AppRig {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async waitForPendingConfirmation(
|
async waitForPendingConfirmation(
|
||||||
toolNameOrDisplayName?: string | RegExp,
|
toolNameOrDisplayName?: string | RegExp | string[],
|
||||||
timeout = 30000,
|
timeout = 30000,
|
||||||
): Promise<PendingConfirmation> {
|
): Promise<PendingConfirmation> {
|
||||||
const matches = (p: PendingConfirmation) => {
|
const matches = (p: PendingConfirmation) => {
|
||||||
@@ -498,6 +498,12 @@ export class AppRig {
|
|||||||
p.toolDisplayName === toolNameOrDisplayName
|
p.toolDisplayName === toolNameOrDisplayName
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
if (Array.isArray(toolNameOrDisplayName)) {
|
||||||
|
return (
|
||||||
|
toolNameOrDisplayName.includes(p.toolName) ||
|
||||||
|
toolNameOrDisplayName.includes(p.toolDisplayName || '')
|
||||||
|
);
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
toolNameOrDisplayName.test(p.toolName) ||
|
toolNameOrDisplayName.test(p.toolName) ||
|
||||||
toolNameOrDisplayName.test(p.toolDisplayName || '')
|
toolNameOrDisplayName.test(p.toolDisplayName || '')
|
||||||
|
|||||||
@@ -353,6 +353,7 @@ export class TestRig {
|
|||||||
testName: string,
|
testName: string,
|
||||||
options: {
|
options: {
|
||||||
settings?: Record<string, unknown>;
|
settings?: Record<string, unknown>;
|
||||||
|
state?: Record<string, unknown>;
|
||||||
fakeResponsesPath?: string;
|
fakeResponsesPath?: string;
|
||||||
} = {},
|
} = {},
|
||||||
) {
|
) {
|
||||||
@@ -382,6 +383,9 @@ export class TestRig {
|
|||||||
|
|
||||||
// Create a settings file to point the CLI to the local collector
|
// Create a settings file to point the CLI to the local collector
|
||||||
this._createSettingsFile(options.settings);
|
this._createSettingsFile(options.settings);
|
||||||
|
|
||||||
|
// Create persistent state file
|
||||||
|
this._createStateFile(options.state);
|
||||||
}
|
}
|
||||||
|
|
||||||
private _cleanDir(dir: string) {
|
private _cleanDir(dir: string) {
|
||||||
@@ -473,6 +477,24 @@ export class TestRig {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private _createStateFile(overrideState?: Record<string, unknown>) {
|
||||||
|
if (!this.homeDir) throw new Error('TestRig homeDir is not initialized');
|
||||||
|
const userGeminiDir = join(this.homeDir, GEMINI_DIR);
|
||||||
|
mkdirSync(userGeminiDir, { recursive: true });
|
||||||
|
|
||||||
|
const state = deepMerge(
|
||||||
|
{
|
||||||
|
terminalSetupPromptShown: true, // Default to true in tests to avoid blocking prompts
|
||||||
|
},
|
||||||
|
overrideState ?? {},
|
||||||
|
);
|
||||||
|
|
||||||
|
writeFileSync(
|
||||||
|
join(userGeminiDir, 'state.json'),
|
||||||
|
JSON.stringify(state, null, 2),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
createFile(fileName: string, content: string) {
|
createFile(fileName: string, content: string) {
|
||||||
const filePath = join(this.testDir!, fileName);
|
const filePath = join(this.testDir!, fileName);
|
||||||
writeFileSync(filePath, content);
|
writeFileSync(filePath, content);
|
||||||
|
|||||||
Reference in New Issue
Block a user