From 1962b51d8d3b971d820eef288d9d4f3346d3a1a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=84=B8=EC=9D=80?= <139741006+seeun0210@users.noreply.github.com> Date: Thu, 9 Oct 2025 05:32:05 +0900 Subject: [PATCH] fix: ensure positional prompt arguments work with extensions flag (#10077) Co-authored-by: Allen Hutchison --- packages/cli/src/config/config.test.ts | 125 ++++++++++++++++++ packages/cli/src/config/config.ts | 1 + packages/core/src/ide/detect-ide.test.ts | 11 ++ .../clearcut-logger/clearcut-logger.test.ts | 5 + 4 files changed, 142 insertions(+) diff --git a/packages/cli/src/config/config.test.ts b/packages/cli/src/config/config.test.ts index e439a162f3..5ffce15c9a 100644 --- a/packages/cli/src/config/config.test.ts +++ b/packages/cli/src/config/config.test.ts @@ -2975,6 +2975,121 @@ describe('loadCliConfig interactive', () => { expect(argv.promptInteractive).toBeUndefined(); }); + it('should not be interactive if positional prompt words are provided with extensions flag', async () => { + process.stdin.isTTY = true; + process.argv = ['node', 'script.js', '-e', 'none', 'hello']; + const argv = await parseArguments({} as Settings); + const config = await loadCliConfig( + {}, + [], + new ExtensionEnablementManager( + ExtensionStorage.getUserExtensionsDir(), + argv.extensions, + ), + 'test-session', + argv, + ); + expect(config.isInteractive()).toBe(false); + expect(argv.query).toBe('hello'); + expect(argv.extensions).toEqual(['none']); + }); + + it('should handle multiple positional words correctly', async () => { + process.stdin.isTTY = true; + process.argv = ['node', 'script.js', 'hello world how are you']; + const argv = await parseArguments({} as Settings); + const config = await loadCliConfig( + {}, + [], + new ExtensionEnablementManager( + ExtensionStorage.getUserExtensionsDir(), + argv.extensions, + ), + 'test-session', + argv, + ); + expect(config.isInteractive()).toBe(false); + expect(argv.query).toBe('hello world how are you'); + expect(argv.prompt).toBe('hello world how are you'); + }); + + it('should handle multiple positional words with flags', async () => { + process.stdin.isTTY = true; + process.argv = [ + 'node', + 'script.js', + '--model', + 'gemini-1.5-pro', + 'write', + 'a', + 'function', + 'to', + 'sort', + 'array', + ]; + const argv = await parseArguments({} as Settings); + const config = await loadCliConfig( + {}, + [], + new ExtensionEnablementManager( + ExtensionStorage.getUserExtensionsDir(), + argv.extensions, + ), + 'test-session', + argv, + ); + expect(config.isInteractive()).toBe(false); + expect(argv.query).toBe('write a function to sort array'); + expect(argv.model).toBe('gemini-1.5-pro'); + }); + + it('should handle empty positional arguments', async () => { + process.stdin.isTTY = true; + process.argv = ['node', 'script.js', '']; + const argv = await parseArguments({} as Settings); + const config = await loadCliConfig( + {}, + [], + new ExtensionEnablementManager( + ExtensionStorage.getUserExtensionsDir(), + argv.extensions, + ), + 'test-session', + argv, + ); + expect(config.isInteractive()).toBe(true); + expect(argv.query).toBeUndefined(); + }); + + it('should handle extensions flag with positional arguments correctly', async () => { + process.stdin.isTTY = true; + process.argv = [ + 'node', + 'script.js', + '-e', + 'none', + 'hello', + 'world', + 'how', + 'are', + 'you', + ]; + const argv = await parseArguments({} as Settings); + const config = await loadCliConfig( + {}, + [], + new ExtensionEnablementManager( + ExtensionStorage.getUserExtensionsDir(), + argv.extensions, + ), + 'test-session', + argv, + ); + expect(config.isInteractive()).toBe(false); + expect(argv.query).toBe('hello world how are you'); + expect(argv.extensions).toEqual(['none']); + }); + it('should be interactive if no positional prompt words are provided with flags', async () => { process.stdin.isTTY = true; process.argv = ['node', 'script.js', '--model', 'gemini-1.5-pro']; @@ -3420,6 +3535,16 @@ describe('parseArguments with positional prompt', () => { expect(argv.promptInteractive).toBeUndefined(); }); + it('should have correct positional argument description', async () => { + // Test that the positional argument has the expected description + const yargsInstance = await import('./config.js'); + // This test verifies that the positional 'query' argument is properly configured + // with the description: "Positional prompt. Defaults to one-shot; use -i/--prompt-interactive for interactive." + process.argv = ['node', 'script.js', 'test', 'query']; + const argv = await yargsInstance.parseArguments({} as Settings); + expect(argv.query).toBe('test query'); + }); + it('should correctly parse a prompt from the --prompt flag', async () => { process.argv = ['node', 'script.js', '--prompt', 'test prompt']; const argv = await parseArguments({} as Settings); diff --git a/packages/cli/src/config/config.ts b/packages/cli/src/config/config.ts index d0b884a5b4..a11bb3148f 100755 --- a/packages/cli/src/config/config.ts +++ b/packages/cli/src/config/config.ts @@ -255,6 +255,7 @@ export async function parseArguments(settings: Settings): Promise { alias: 'e', type: 'array', string: true, + nargs: 1, description: 'A list of extensions to use. If not provided, all extensions are used.', coerce: (extensions: string[]) => diff --git a/packages/core/src/ide/detect-ide.test.ts b/packages/core/src/ide/detect-ide.test.ts index 07a02bcdec..66993314c5 100644 --- a/packages/core/src/ide/detect-ide.test.ts +++ b/packages/core/src/ide/detect-ide.test.ts @@ -13,6 +13,8 @@ describe('detectIde', () => { afterEach(() => { vi.unstubAllEnvs(); + // Clear Cursor-specific environment variables that might interfere with tests + delete process.env['CURSOR_TRACE_ID']; }); it('should return undefined if TERM_PROGRAM is not vscode', () => { @@ -41,42 +43,49 @@ describe('detectIde', () => { it('should detect Codespaces', () => { vi.stubEnv('TERM_PROGRAM', 'vscode'); vi.stubEnv('CODESPACES', 'true'); + vi.stubEnv('CURSOR_TRACE_ID', ''); expect(detectIde(ideProcessInfo)).toBe(IDE_DEFINITIONS.codespaces); }); it('should detect Cloud Shell via EDITOR_IN_CLOUD_SHELL', () => { vi.stubEnv('TERM_PROGRAM', 'vscode'); vi.stubEnv('EDITOR_IN_CLOUD_SHELL', 'true'); + vi.stubEnv('CURSOR_TRACE_ID', ''); expect(detectIde(ideProcessInfo)).toBe(IDE_DEFINITIONS.cloudshell); }); it('should detect Cloud Shell via CLOUD_SHELL', () => { vi.stubEnv('TERM_PROGRAM', 'vscode'); vi.stubEnv('CLOUD_SHELL', 'true'); + vi.stubEnv('CURSOR_TRACE_ID', ''); expect(detectIde(ideProcessInfo)).toBe(IDE_DEFINITIONS.cloudshell); }); it('should detect Trae', () => { vi.stubEnv('TERM_PROGRAM', 'vscode'); vi.stubEnv('TERM_PRODUCT', 'Trae'); + vi.stubEnv('CURSOR_TRACE_ID', ''); expect(detectIde(ideProcessInfo)).toBe(IDE_DEFINITIONS.trae); }); it('should detect Firebase Studio via MONOSPACE_ENV', () => { vi.stubEnv('TERM_PROGRAM', 'vscode'); vi.stubEnv('MONOSPACE_ENV', 'true'); + vi.stubEnv('CURSOR_TRACE_ID', ''); expect(detectIde(ideProcessInfo)).toBe(IDE_DEFINITIONS.firebasestudio); }); it('should detect VSCode when no other IDE is detected and command includes "code"', () => { vi.stubEnv('TERM_PROGRAM', 'vscode'); vi.stubEnv('MONOSPACE_ENV', ''); + vi.stubEnv('CURSOR_TRACE_ID', ''); expect(detectIde(ideProcessInfo)).toBe(IDE_DEFINITIONS.vscode); }); it('should detect VSCodeFork when no other IDE is detected and command does not include "code"', () => { vi.stubEnv('TERM_PROGRAM', 'vscode'); vi.stubEnv('MONOSPACE_ENV', ''); + vi.stubEnv('CURSOR_TRACE_ID', ''); expect(detectIde(ideProcessInfoNoCode)).toBe(IDE_DEFINITIONS.vscodefork); }); }); @@ -99,6 +108,7 @@ describe('detectIde with ideInfoFromFile', () => { it('should fall back to env detection if name is missing', () => { const ideInfoFromFile = { displayName: 'Custom IDE' }; vi.stubEnv('TERM_PROGRAM', 'vscode'); + vi.stubEnv('CURSOR_TRACE_ID', ''); expect(detectIde(ideProcessInfo, ideInfoFromFile)).toBe( IDE_DEFINITIONS.vscode, ); @@ -107,6 +117,7 @@ describe('detectIde with ideInfoFromFile', () => { it('should fall back to env detection if displayName is missing', () => { const ideInfoFromFile = { name: 'custom-ide' }; vi.stubEnv('TERM_PROGRAM', 'vscode'); + vi.stubEnv('CURSOR_TRACE_ID', ''); expect(detectIde(ideProcessInfo, ideInfoFromFile)).toBe( IDE_DEFINITIONS.vscode, ); diff --git a/packages/core/src/telemetry/clearcut-logger/clearcut-logger.test.ts b/packages/core/src/telemetry/clearcut-logger/clearcut-logger.test.ts index 28ce2576fb..a4873c1f26 100644 --- a/packages/core/src/telemetry/clearcut-logger/clearcut-logger.test.ts +++ b/packages/core/src/telemetry/clearcut-logger/clearcut-logger.test.ts @@ -413,6 +413,11 @@ describe('ClearcutLogger', () => { for (const [key, value] of Object.entries(env)) { vi.stubEnv(key, value); } + // Clear Cursor-specific environment variables that might interfere with tests + // Only clear if not explicitly testing Cursor detection + if (!env.CURSOR_TRACE_ID) { + vi.stubEnv('CURSOR_TRACE_ID', ''); + } const event = logger?.createLogEvent(EventNames.API_ERROR, []); expect(event?.event_metadata[0][3]).toEqual({ gemini_cli_key: EventMetadataKey.GEMINI_CLI_SURFACE,