Fix(noninteractive) - Add message when user uses deprecated flag (#11682)

Co-authored-by: gemini-cli-robot <gemini-cli-robot@google.com>
This commit is contained in:
shishu314
2025-10-29 17:54:40 -04:00
committed by GitHub
parent 82c10421a0
commit 99f75f3218
5 changed files with 275 additions and 227 deletions

View File

@@ -386,13 +386,13 @@ export class TestRig {
};
if (typeof promptOrOptions === 'string') {
commandArgs.push('--prompt', promptOrOptions);
commandArgs.push(promptOrOptions);
} else if (
typeof promptOrOptions === 'object' &&
promptOrOptions !== null
) {
if (promptOrOptions.prompt) {
commandArgs.push('--prompt', promptOrOptions.prompt);
commandArgs.push(promptOrOptions.prompt);
}
if (promptOrOptions.stdin) {
execOptions.input = promptOrOptions.stdin;

View File

@@ -439,6 +439,7 @@ describe('startInteractiveUI', () => {
vi.mock('./utils/cleanup.js', () => ({
cleanupCheckpoints: vi.fn(() => Promise.resolve()),
registerCleanup: vi.fn(),
runExitCleanup: vi.fn(),
}));
vi.mock('ink', () => ({

View File

@@ -477,7 +477,16 @@ export async function main() {
debugLogger.log('Session ID: %s', sessionId);
}
await runNonInteractive(nonInteractiveConfig, settings, input, prompt_id);
const hasDeprecatedPromptArg = process.argv.some((arg) =>
arg.startsWith('--prompt'),
);
await runNonInteractive({
config: nonInteractiveConfig,
settings,
input,
prompt_id,
hasDeprecatedPromptArg,
});
// Call cleanup before process.exit, which causes cleanup to not run
await runExitCleanup();
process.exit(0);

View File

@@ -95,6 +95,26 @@ describe('runNonInteractive', () => {
sendMessageStream: Mock;
getChatRecordingService: Mock;
};
const MOCK_SESSION_METRICS: SessionMetrics = {
models: {},
tools: {
totalCalls: 0,
totalSuccess: 0,
totalFail: 0,
totalDurationMs: 0,
totalDecisions: {
accept: 0,
reject: 0,
modify: 0,
auto_accept: 0,
},
byName: {},
},
files: {
totalLinesAdded: 0,
totalLinesRemoved: 0,
},
};
beforeEach(async () => {
mockCoreExecuteToolCall = vi.mocked(executeToolCall);
@@ -206,12 +226,12 @@ describe('runNonInteractive', () => {
createStreamFromEvents(events),
);
await runNonInteractive(
mockConfig,
mockSettings,
'Test input',
'prompt-id-1',
);
await runNonInteractive({
config: mockConfig,
settings: mockSettings,
input: 'Test input',
prompt_id: 'prompt-id-1',
});
expect(mockGeminiClient.sendMessageStream).toHaveBeenCalledWith(
[{ text: 'Test input' }],
@@ -267,12 +287,12 @@ describe('runNonInteractive', () => {
.mockReturnValueOnce(createStreamFromEvents(firstCallEvents))
.mockReturnValueOnce(createStreamFromEvents(secondCallEvents));
await runNonInteractive(
mockConfig,
mockSettings,
'Use a tool',
'prompt-id-2',
);
await runNonInteractive({
config: mockConfig,
settings: mockSettings,
input: 'Use a tool',
prompt_id: 'prompt-id-2',
});
expect(mockGeminiClient.sendMessageStream).toHaveBeenCalledTimes(2);
expect(mockCoreExecuteToolCall).toHaveBeenCalledWith(
@@ -343,12 +363,12 @@ describe('runNonInteractive', () => {
.mockReturnValueOnce(createStreamFromEvents(modelTurn3));
// 4. Run the command.
await runNonInteractive(
mockConfig,
mockSettings,
'Use mock tool multiple times',
'prompt-id-multi',
);
await runNonInteractive({
config: mockConfig,
settings: mockSettings,
input: 'Use mock tool multiple times',
prompt_id: 'prompt-id-multi',
});
// 5. Verify the output.
// The rendered output should contain the text from each turn, separated by a
@@ -412,12 +432,12 @@ describe('runNonInteractive', () => {
.mockReturnValueOnce(createStreamFromEvents([toolCallEvent]))
.mockReturnValueOnce(createStreamFromEvents(finalResponse));
await runNonInteractive(
mockConfig,
mockSettings,
'Trigger tool error',
'prompt-id-3',
);
await runNonInteractive({
config: mockConfig,
settings: mockSettings,
input: 'Trigger tool error',
prompt_id: 'prompt-id-3',
});
expect(mockCoreExecuteToolCall).toHaveBeenCalled();
expect(consoleErrorSpy).toHaveBeenCalledWith(
@@ -449,12 +469,12 @@ describe('runNonInteractive', () => {
});
await expect(
runNonInteractive(
mockConfig,
mockSettings,
'Initial fail',
'prompt-id-4',
),
runNonInteractive({
config: mockConfig,
settings: mockSettings,
input: 'Initial fail',
prompt_id: 'prompt-id-4',
}),
).rejects.toThrow(apiError);
});
@@ -502,12 +522,12 @@ describe('runNonInteractive', () => {
.mockReturnValueOnce(createStreamFromEvents([toolCallEvent]))
.mockReturnValueOnce(createStreamFromEvents(finalResponse));
await runNonInteractive(
mockConfig,
mockSettings,
'Trigger tool not found',
'prompt-id-5',
);
await runNonInteractive({
config: mockConfig,
settings: mockSettings,
input: 'Trigger tool not found',
prompt_id: 'prompt-id-5',
});
expect(mockCoreExecuteToolCall).toHaveBeenCalled();
expect(consoleErrorSpy).toHaveBeenCalledWith(
@@ -520,12 +540,12 @@ describe('runNonInteractive', () => {
it('should exit when max session turns are exceeded', async () => {
vi.mocked(mockConfig.getMaxSessionTurns).mockReturnValue(0);
await expect(
runNonInteractive(
mockConfig,
mockSettings,
'Trigger loop',
'prompt-id-6',
),
runNonInteractive({
config: mockConfig,
settings: mockSettings,
input: 'Trigger loop',
prompt_id: 'prompt-id-6',
}),
).rejects.toThrow('process.exit(53) called');
});
@@ -564,7 +584,12 @@ describe('runNonInteractive', () => {
);
// 4. Run the non-interactive mode with the raw input
await runNonInteractive(mockConfig, mockSettings, rawInput, 'prompt-id-7');
await runNonInteractive({
config: mockConfig,
settings: mockSettings,
input: rawInput,
prompt_id: 'prompt-id-7',
});
// 5. Assert that sendMessageStream was called with the PROCESSED parts, not the raw input
expect(mockGeminiClient.sendMessageStream).toHaveBeenCalledWith(
@@ -589,42 +614,28 @@ describe('runNonInteractive', () => {
createStreamFromEvents(events),
);
vi.mocked(mockConfig.getOutputFormat).mockReturnValue(OutputFormat.JSON);
const mockMetrics: SessionMetrics = {
models: {},
tools: {
totalCalls: 0,
totalSuccess: 0,
totalFail: 0,
totalDurationMs: 0,
totalDecisions: {
accept: 0,
reject: 0,
modify: 0,
auto_accept: 0,
},
byName: {},
},
files: {
totalLinesAdded: 0,
totalLinesRemoved: 0,
},
};
vi.mocked(uiTelemetryService.getMetrics).mockReturnValue(mockMetrics);
await runNonInteractive(
mockConfig,
mockSettings,
'Test input',
'prompt-id-1',
vi.mocked(uiTelemetryService.getMetrics).mockReturnValue(
MOCK_SESSION_METRICS,
);
await runNonInteractive({
config: mockConfig,
settings: mockSettings,
input: 'Test input',
prompt_id: 'prompt-id-1',
});
expect(mockGeminiClient.sendMessageStream).toHaveBeenCalledWith(
[{ text: 'Test input' }],
expect.any(AbortSignal),
'prompt-id-1',
);
expect(processStdoutSpy).toHaveBeenCalledWith(
JSON.stringify({ response: 'Hello World', stats: mockMetrics }, null, 2),
JSON.stringify(
{ response: 'Hello World', stats: MOCK_SESSION_METRICS },
null,
2,
),
);
});
@@ -684,48 +695,17 @@ describe('runNonInteractive', () => {
.mockReturnValueOnce(createStreamFromEvents(secondCallEvents));
vi.mocked(mockConfig.getOutputFormat).mockReturnValue(OutputFormat.JSON);
const mockMetrics: SessionMetrics = {
models: {},
tools: {
totalCalls: 1,
totalSuccess: 1,
totalFail: 0,
totalDurationMs: 100,
totalDecisions: {
accept: 1,
reject: 0,
modify: 0,
auto_accept: 0,
},
byName: {
testTool: {
count: 1,
success: 1,
fail: 0,
durationMs: 100,
decisions: {
accept: 1,
reject: 0,
modify: 0,
auto_accept: 0,
},
},
},
},
files: {
totalLinesAdded: 0,
totalLinesRemoved: 0,
},
};
vi.mocked(uiTelemetryService.getMetrics).mockReturnValue(mockMetrics);
await runNonInteractive(
mockConfig,
mockSettings,
'Execute tool only',
'prompt-id-tool-only',
vi.mocked(uiTelemetryService.getMetrics).mockReturnValue(
MOCK_SESSION_METRICS,
);
await runNonInteractive({
config: mockConfig,
settings: mockSettings,
input: 'Execute tool only',
prompt_id: 'prompt-id-tool-only',
});
expect(mockGeminiClient.sendMessageStream).toHaveBeenCalledTimes(2);
expect(mockCoreExecuteToolCall).toHaveBeenCalledWith(
mockConfig,
@@ -735,7 +715,7 @@ describe('runNonInteractive', () => {
// This should output JSON with empty response but include stats
expect(processStdoutSpy).toHaveBeenCalledWith(
JSON.stringify({ response: '', stats: mockMetrics }, null, 2),
JSON.stringify({ response: '', stats: MOCK_SESSION_METRICS }, null, 2),
);
});
@@ -751,35 +731,17 @@ describe('runNonInteractive', () => {
createStreamFromEvents(events),
);
vi.mocked(mockConfig.getOutputFormat).mockReturnValue(OutputFormat.JSON);
const mockMetrics: SessionMetrics = {
models: {},
tools: {
totalCalls: 0,
totalSuccess: 0,
totalFail: 0,
totalDurationMs: 0,
totalDecisions: {
accept: 0,
reject: 0,
modify: 0,
auto_accept: 0,
},
byName: {},
},
files: {
totalLinesAdded: 0,
totalLinesRemoved: 0,
},
};
vi.mocked(uiTelemetryService.getMetrics).mockReturnValue(mockMetrics);
await runNonInteractive(
mockConfig,
mockSettings,
'Empty response test',
'prompt-id-empty',
vi.mocked(uiTelemetryService.getMetrics).mockReturnValue(
MOCK_SESSION_METRICS,
);
await runNonInteractive({
config: mockConfig,
settings: mockSettings,
input: 'Empty response test',
prompt_id: 'prompt-id-empty',
});
expect(mockGeminiClient.sendMessageStream).toHaveBeenCalledWith(
[{ text: 'Empty response test' }],
expect.any(AbortSignal),
@@ -788,7 +750,7 @@ describe('runNonInteractive', () => {
// This should output JSON with empty response but include stats
expect(processStdoutSpy).toHaveBeenCalledWith(
JSON.stringify({ response: '', stats: mockMetrics }, null, 2),
JSON.stringify({ response: '', stats: MOCK_SESSION_METRICS }, null, 2),
);
});
@@ -807,12 +769,12 @@ describe('runNonInteractive', () => {
let thrownError: Error | null = null;
try {
await runNonInteractive(
mockConfig,
mockSettings,
'Test input',
'prompt-id-error',
);
await runNonInteractive({
config: mockConfig,
settings: mockSettings,
input: 'Test input',
prompt_id: 'prompt-id-error',
});
// Should not reach here
expect.fail('Expected process.exit to be called');
} catch (error) {
@@ -852,12 +814,12 @@ describe('runNonInteractive', () => {
let thrownError: Error | null = null;
try {
await runNonInteractive(
mockConfig,
mockSettings,
'Invalid syntax',
'prompt-id-fatal',
);
await runNonInteractive({
config: mockConfig,
settings: mockSettings,
input: 'Invalid syntax',
prompt_id: 'prompt-id-fatal',
});
// Should not reach here
expect.fail('Expected process.exit to be called');
} catch (error) {
@@ -904,12 +866,12 @@ describe('runNonInteractive', () => {
createStreamFromEvents(events),
);
await runNonInteractive(
mockConfig,
mockSettings,
'/testcommand',
'prompt-id-slash',
);
await runNonInteractive({
config: mockConfig,
settings: mockSettings,
input: '/testcommand',
prompt_id: 'prompt-id-slash',
});
// Ensure the prompt sent to the model is from the command, not the raw input
expect(mockGeminiClient.sendMessageStream).toHaveBeenCalledWith(
@@ -933,12 +895,12 @@ describe('runNonInteractive', () => {
mockGetCommands.mockReturnValue([mockCommand]);
await expect(
runNonInteractive(
mockConfig,
mockSettings,
'/confirm',
'prompt-id-confirm',
),
runNonInteractive({
config: mockConfig,
settings: mockSettings,
input: '/confirm',
prompt_id: 'prompt-id-confirm',
}),
).rejects.toThrow(
'Exiting due to a confirmation prompt requested by the command.',
);
@@ -959,12 +921,12 @@ describe('runNonInteractive', () => {
createStreamFromEvents(events),
);
await runNonInteractive(
mockConfig,
mockSettings,
'/unknowncommand',
'prompt-id-unknown',
);
await runNonInteractive({
config: mockConfig,
settings: mockSettings,
input: '/unknowncommand',
prompt_id: 'prompt-id-unknown',
});
// Ensure the raw input is sent to the model
expect(mockGeminiClient.sendMessageStream).toHaveBeenCalledWith(
@@ -987,12 +949,12 @@ describe('runNonInteractive', () => {
mockGetCommands.mockReturnValue([mockCommand]);
await expect(
runNonInteractive(
mockConfig,
mockSettings,
'/noaction',
'prompt-id-unhandled',
),
runNonInteractive({
config: mockConfig,
settings: mockSettings,
input: '/noaction',
prompt_id: 'prompt-id-unhandled',
}),
).rejects.toThrow(
'Exiting due to command result that is not supported in non-interactive mode.',
);
@@ -1021,12 +983,12 @@ describe('runNonInteractive', () => {
createStreamFromEvents(events),
);
await runNonInteractive(
mockConfig,
mockSettings,
'/testargs arg1 arg2',
'prompt-id-args',
);
await runNonInteractive({
config: mockConfig,
settings: mockSettings,
input: '/testargs arg1 arg2',
prompt_id: 'prompt-id-args',
});
expect(mockAction).toHaveBeenCalledWith(expect.any(Object), 'arg1 arg2');
@@ -1052,12 +1014,12 @@ describe('runNonInteractive', () => {
createStreamFromEvents(events),
);
await runNonInteractive(
mockConfig,
mockSettings,
'/mycommand',
'prompt-id-loaders',
);
await runNonInteractive({
config: mockConfig,
settings: mockSettings,
input: '/mycommand',
prompt_id: 'prompt-id-loaders',
});
// Check that loaders were instantiated with the config
expect(FileCommandLoader).toHaveBeenCalledTimes(1);
@@ -1129,12 +1091,12 @@ describe('runNonInteractive', () => {
.mockReturnValueOnce(createStreamFromEvents(firstCallEvents))
.mockReturnValueOnce(createStreamFromEvents(secondCallEvents));
await runNonInteractive(
mockConfig,
mockSettings,
'List the files',
'prompt-id-allowed',
);
await runNonInteractive({
config: mockConfig,
settings: mockSettings,
input: 'List the files',
prompt_id: 'prompt-id-allowed',
});
expect(mockCoreExecuteToolCall).toHaveBeenCalledWith(
mockConfig,
@@ -1156,12 +1118,12 @@ describe('runNonInteractive', () => {
createStreamFromEvents(events),
);
await runNonInteractive(
mockConfig,
mockSettings,
'test',
'prompt-id-events',
);
await runNonInteractive({
config: mockConfig,
settings: mockSettings,
input: 'test',
prompt_id: 'prompt-id-events',
});
expect(mockCoreEvents.on).toHaveBeenCalledWith(
CoreEvent.UserFeedback,
@@ -1181,12 +1143,12 @@ describe('runNonInteractive', () => {
createStreamFromEvents(events),
);
await runNonInteractive(
mockConfig,
mockSettings,
'test',
'prompt-id-events',
);
await runNonInteractive({
config: mockConfig,
settings: mockSettings,
input: 'test',
prompt_id: 'prompt-id-events',
});
expect(mockCoreEvents.off).toHaveBeenCalledWith(
CoreEvent.UserFeedback,
@@ -1205,12 +1167,12 @@ describe('runNonInteractive', () => {
createStreamFromEvents(events),
);
await runNonInteractive(
mockConfig,
mockSettings,
'test',
'prompt-id-events',
);
await runNonInteractive({
config: mockConfig,
settings: mockSettings,
input: 'test',
prompt_id: 'prompt-id-events',
});
// Get the registered handler
const handler = mockCoreEvents.on.mock.calls.find(
@@ -1242,12 +1204,12 @@ describe('runNonInteractive', () => {
createStreamFromEvents(events),
);
await runNonInteractive(
mockConfig,
mockSettings,
'test',
'prompt-id-events',
);
await runNonInteractive({
config: mockConfig,
settings: mockSettings,
input: 'test',
prompt_id: 'prompt-id-events',
});
// Get the registered handler
const handler = mockCoreEvents.on.mock.calls.find(
@@ -1274,4 +1236,56 @@ describe('runNonInteractive', () => {
);
});
});
it('should display a deprecation warning if hasDeprecatedPromptArg is true', async () => {
const events: ServerGeminiStreamEvent[] = [
{ type: GeminiEventType.Content, value: 'Final Answer' },
{
type: GeminiEventType.Finished,
value: { reason: undefined, usageMetadata: { totalTokenCount: 10 } },
},
];
mockGeminiClient.sendMessageStream.mockReturnValue(
createStreamFromEvents(events),
);
await runNonInteractive({
config: mockConfig,
settings: mockSettings,
input: 'Test input',
prompt_id: 'prompt-id-deprecated',
hasDeprecatedPromptArg: true,
});
expect(processStderrSpy).toHaveBeenCalledWith(
'The --prompt (-p) flag has been deprecated and will be removed in a future version. Please use a positional argument for your prompt. See gemini --help for more information.\n',
);
expect(processStdoutSpy).toHaveBeenCalledWith('Final Answer');
});
it('should display a deprecation warning for JSON format', async () => {
const events: ServerGeminiStreamEvent[] = [
{ type: GeminiEventType.Content, value: 'Final Answer' },
{
type: GeminiEventType.Finished,
value: { reason: undefined, usageMetadata: { totalTokenCount: 10 } },
},
];
mockGeminiClient.sendMessageStream.mockReturnValue(
createStreamFromEvents(events),
);
vi.mocked(mockConfig.getOutputFormat).mockReturnValue(OutputFormat.JSON);
await runNonInteractive({
config: mockConfig,
settings: mockSettings,
input: 'Test input',
prompt_id: 'prompt-id-deprecated-json',
hasDeprecatedPromptArg: true,
});
const deprecateText =
'The --prompt (-p) flag has been deprecated and will be removed in a future version. Please use a positional argument for your prompt. See gemini --help for more information.\n';
expect(processStderrSpy).toHaveBeenCalledWith(deprecateText);
});
});

View File

@@ -42,12 +42,21 @@ import {
} from './utils/errors.js';
import { TextOutput } from './ui/utils/textOutput.js';
export async function runNonInteractive(
config: Config,
settings: LoadedSettings,
input: string,
prompt_id: string,
): Promise<void> {
interface RunNonInteractiveParams {
config: Config;
settings: LoadedSettings;
input: string;
prompt_id: string;
hasDeprecatedPromptArg?: boolean;
}
export async function runNonInteractive({
config,
settings,
input,
prompt_id,
hasDeprecatedPromptArg,
}: RunNonInteractiveParams): Promise<void> {
return promptIdContext.run(prompt_id, async () => {
const consolePatcher = new ConsolePatcher({
stderr: true,
@@ -151,6 +160,21 @@ export async function runNonInteractive(
let currentMessages: Content[] = [{ role: 'user', parts: query }];
let turnCount = 0;
const deprecateText =
'The --prompt (-p) flag has been deprecated and will be removed in a future version. Please use a positional argument for your prompt. See gemini --help for more information.\n';
if (hasDeprecatedPromptArg) {
if (streamFormatter) {
streamFormatter.emitEvent({
type: JsonStreamEventType.MESSAGE,
timestamp: new Date().toISOString(),
role: 'assistant',
content: deprecateText,
delta: true,
});
} else {
process.stderr.write(deprecateText);
}
}
while (true) {
turnCount++;
if (