fix(cli): honor argv @path in interactive sessions (quoted + unquoted) (#5952)

Co-authored-by: Allen Hutchison <adh@google.com>
This commit is contained in:
Tayyab3245
2025-09-30 21:18:04 -04:00
committed by GitHub
parent f2aa9d283a
commit c913ce3c0b
3 changed files with 191 additions and 23 deletions

View File

@@ -207,6 +207,136 @@ describe('parseArguments', () => {
expect(argv.prompt).toBeUndefined();
});
it('should convert positional query argument to prompt by default', async () => {
process.argv = ['node', 'script.js', 'Hi Gemini'];
const argv = await parseArguments({} as Settings);
expect(argv.query).toBe('Hi Gemini');
expect(argv.prompt).toBe('Hi Gemini');
expect(argv.promptInteractive).toBeUndefined();
});
it('should map @path to prompt (one-shot) when it starts with @', async () => {
process.argv = ['node', 'script.js', '@path ./file.md'];
const argv = await parseArguments({} as Settings);
expect(argv.query).toBe('@path ./file.md');
expect(argv.prompt).toBe('@path ./file.md');
expect(argv.promptInteractive).toBeUndefined();
});
it('should map @path to prompt even when config flags are present', async () => {
// @path queries should now go to one-shot mode regardless of other flags
process.argv = [
'node',
'script.js',
'@path',
'./file.md',
'--model',
'gemini-1.5-pro',
];
const argv = await parseArguments({} as Settings);
expect(argv.query).toBe('@path ./file.md');
expect(argv.prompt).toBe('@path ./file.md'); // Should map to one-shot
expect(argv.promptInteractive).toBeUndefined();
expect(argv.model).toBe('gemini-1.5-pro');
});
it('maps unquoted positional @path + arg to prompt (one-shot)', async () => {
// Simulate: gemini @path ./file.md
process.argv = ['node', 'script.js', '@path', './file.md'];
const argv = await parseArguments({} as Settings);
// After normalization, query is a single string
expect(argv.query).toBe('@path ./file.md');
// And it's mapped to one-shot prompt when no -p/-i flags are set
expect(argv.prompt).toBe('@path ./file.md');
expect(argv.promptInteractive).toBeUndefined();
});
it('should handle multiple @path arguments in a single command (one-shot)', async () => {
// Simulate: gemini @path ./file1.md @path ./file2.md
process.argv = [
'node',
'script.js',
'@path',
'./file1.md',
'@path',
'./file2.md',
];
const argv = await parseArguments({} as Settings);
// After normalization, all arguments are joined with spaces
expect(argv.query).toBe('@path ./file1.md @path ./file2.md');
// And it's mapped to one-shot prompt
expect(argv.prompt).toBe('@path ./file1.md @path ./file2.md');
expect(argv.promptInteractive).toBeUndefined();
});
it('should handle mixed quoted and unquoted @path arguments (one-shot)', async () => {
// Simulate: gemini "@path ./file1.md" @path ./file2.md "additional text"
process.argv = [
'node',
'script.js',
'@path ./file1.md',
'@path',
'./file2.md',
'additional text',
];
const argv = await parseArguments({} as Settings);
// After normalization, all arguments are joined with spaces
expect(argv.query).toBe(
'@path ./file1.md @path ./file2.md additional text',
);
// And it's mapped to one-shot prompt
expect(argv.prompt).toBe(
'@path ./file1.md @path ./file2.md additional text',
);
expect(argv.promptInteractive).toBeUndefined();
});
it('should map @path to prompt with ambient flags (debug, telemetry)', async () => {
// Ambient flags like debug, telemetry should NOT affect routing
process.argv = [
'node',
'script.js',
'@path',
'./file.md',
'--debug',
'--telemetry',
];
const argv = await parseArguments({} as Settings);
expect(argv.query).toBe('@path ./file.md');
expect(argv.prompt).toBe('@path ./file.md'); // Should map to one-shot
expect(argv.promptInteractive).toBeUndefined();
expect(argv.debug).toBe(true);
expect(argv.telemetry).toBe(true);
});
it('should map any @command to prompt (one-shot)', async () => {
// Test that all @commands now go to one-shot mode
const testCases = [
'@path ./file.md',
'@include src/',
'@search pattern',
'@web query',
'@git status',
];
for (const testQuery of testCases) {
process.argv = ['node', 'script.js', testQuery];
const argv = await parseArguments({} as Settings);
expect(argv.query).toBe(testQuery);
expect(argv.prompt).toBe(testQuery);
expect(argv.promptInteractive).toBeUndefined();
}
});
it('should handle @command with leading whitespace', async () => {
// Test that trim() + routing handles leading whitespace correctly
process.argv = ['node', 'script.js', ' @path ./file.md'];
const argv = await parseArguments({} as Settings);
expect(argv.query).toBe(' @path ./file.md');
expect(argv.prompt).toBe(' @path ./file.md');
expect(argv.promptInteractive).toBeUndefined();
});
it('should throw an error when both --yolo and --approval-mode are used together', async () => {
process.argv = [
'node',
@@ -2702,7 +2832,7 @@ describe('loadCliConfig interactive', () => {
'script.js',
'--model',
'gemini-1.5-pro',
'--sandbox',
'--yolo',
'Hello world',
];
const argv = await parseArguments({} as Settings);
@@ -2717,6 +2847,9 @@ describe('loadCliConfig interactive', () => {
argv,
);
expect(config.isInteractive()).toBe(false);
// Verify the question is preserved for one-shot execution
expect(argv.prompt).toBe('Hello world');
expect(argv.promptInteractive).toBeUndefined();
});
it('should be interactive if no positional prompt words are provided with flags', async () => {
@@ -3155,10 +3288,13 @@ describe('parseArguments with positional prompt', () => {
mockConsoleError.mockRestore();
});
it('should correctly parse a positional prompt', async () => {
it('should correctly parse a positional prompt to query field', async () => {
process.argv = ['node', 'script.js', 'positional', 'prompt'];
const argv = await parseArguments({} as Settings);
expect(argv.promptWords).toEqual(['positional', 'prompt']);
expect(argv.query).toBe('positional prompt');
// Since no explicit prompt flags are set and query doesn't start with @, should map to prompt (one-shot)
expect(argv.prompt).toBe('positional prompt');
expect(argv.promptInteractive).toBeUndefined();
});
it('should correctly parse a prompt from the --prompt flag', async () => {