1+ /**
2+ * GeminiCLI unit tests
3+ *
4+ * Testing library note:
5+ * - These tests are compatible with Jest or Vitest (they use describe/it/expect globals only).
6+ * - We avoid jest/vi-specific APIs and monkeypatch console/process manually.
7+ * - If your repository uses Jest, no changes are required.
8+ * - If your repository uses Vitest, no changes are required.
9+ */
10+
11+ describe ( 'GeminiCLI' , ( ) => {
12+ // Dynamically locate and load GeminiCLI from common paths so tests don't depend on a specific layout.
13+
14+ let GeminiCLI : any ;
15+
16+ const candidateModules = [
17+ // Typical source locations
18+ '../../../src/cli/gemini-cli' ,
19+ '../../../src/cli/gemini' ,
20+ '../../../src/gemini-cli' ,
21+ // Built output locations
22+ '../../../lib/cli/gemini-cli' ,
23+ '../../../dist/cli/gemini-cli' ,
24+ '../../../build/cli/gemini-cli' ,
25+ ] ;
26+
27+ const req : any = ( global as any ) . require ? ( global as any ) . require : undefined ;
28+
29+ async function loadGeminiCLI ( ) {
30+ for ( const p of candidateModules ) {
31+ try {
32+ const mod = req ( p ) ;
33+ const C = mod . GeminiCLI ?? mod . default ?? mod ;
34+ if ( typeof C === 'function' ) return C ;
35+ } catch ( err ) {
36+ // fall through and try dynamic import next
37+ try {
38+ const mod = await import ( p ) ;
39+ const C = ( mod as any ) . GeminiCLI ?? ( mod as any ) . default ?? mod ;
40+ if ( typeof C === 'function' ) return C ;
41+ } catch ( _e ) {
42+ // try next path
43+ }
44+ }
45+ }
46+ throw new Error ( 'Unable to resolve GeminiCLI from known locations. ' +
47+ 'Please adjust candidateModules in tests/unit/cli/gemini-cli.test.ts to match project structure.' ) ;
48+ }
49+
50+ beforeAll ( async ( ) => {
51+ GeminiCLI = await loadGeminiCLI ( ) ;
52+ } ) ;
53+
54+ async function runCLI ( args : string [ ] , options : { env ?: Record < string , string | undefined > } = { } ) {
55+ const logs : string [ ] = [ ] ;
56+ const original = {
57+ argv : process . argv . slice ( ) ,
58+ log : console . log ,
59+ error : console . error ,
60+ exit : process . exit ,
61+ env : { ...process . env } ,
62+ } ;
63+
64+ // Patch console to capture output
65+ console . log = ( ...a : any [ ] ) => {
66+ try {
67+ logs . push ( a . map ( x => ( typeof x === 'string' ? x : JSON . stringify ( x ) ) ) . join ( ' ' ) ) ;
68+ } catch {
69+ logs . push ( String ( a ) ) ;
70+ }
71+ } ;
72+ console . error = ( ...a : any [ ] ) => {
73+ try {
74+ logs . push ( a . map ( x => ( typeof x === 'string' ? x : JSON . stringify ( x ) ) ) . join ( ' ' ) ) ;
75+ } catch {
76+ logs . push ( String ( a ) ) ;
77+ }
78+ } ;
79+
80+ // Patch exit to prevent terminating the test runner
81+ let exitCode : number | undefined ;
82+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
83+ ( process as any ) . exit = ( ( code ?: number ) => {
84+ exitCode = code ?? 0 ;
85+ return undefined as never ;
86+ } ) as any ;
87+
88+ // Configure argv and env
89+ process . argv = [ 'node' , 'gemini-flow' , ...args ] ;
90+
91+ if ( options . env ) {
92+ for ( const [ k , v ] of Object . entries ( options . env ) ) {
93+ if ( typeof v === 'undefined' ) {
94+ delete ( process . env as any ) [ k ] ;
95+ } else {
96+ process . env [ k ] = String ( v ) ;
97+ }
98+ }
99+ }
100+
101+ try {
102+ const cli = new GeminiCLI ( ) ;
103+ await cli . run ( ) ;
104+ } finally {
105+ // Restore env
106+ const backup = original . env ;
107+ // Remove keys not in backup
108+ for ( const key of Object . keys ( process . env ) ) {
109+ if ( ! ( key in backup ) ) {
110+ delete ( process . env as any ) [ key ] ;
111+ }
112+ }
113+ // Restore backup values
114+ for ( const [ k , v ] of Object . entries ( backup ) ) {
115+ process . env [ k ] = v as string ;
116+ }
117+
118+ // Restore process and console
119+ process . argv = original . argv ;
120+ console . log = original . log ;
121+ console . error = original . error ;
122+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
123+ ( process as any ) . exit = original . exit as any ;
124+ }
125+
126+ return { logs, exitCode } ;
127+ }
128+
129+ // Helper to check presence of multiple substrings
130+ function expectLogsToContainAll ( logs : string [ ] , substrings : string [ ] ) {
131+ const joined = logs . join ( '\n' ) ;
132+ for ( const s of substrings ) {
133+ expect ( joined ) . toContain ( s ) ;
134+ }
135+ }
136+
137+ it ( 'shows help when no args are provided' , async ( ) => {
138+ const { logs, exitCode } = await runCLI ( [ ] ) ;
139+ expect ( exitCode ) . toBeUndefined ( ) ;
140+ expectLogsToContainAll ( logs , [
141+ 'Gemini-Flow CLI' ,
142+ 'Usage:' ,
143+ 'Commands:' ,
144+ 'chat, c' ,
145+ 'generate, g' ,
146+ 'list-models, models' ,
147+ 'auth'
148+ ] ) ;
149+ } ) ;
150+
151+ it ( 'shows help for --help and -h flags' , async ( ) => {
152+ const r1 = await runCLI ( [ '--help' ] ) ;
153+ expectLogsToContainAll ( r1 . logs , [ 'Usage:' , 'Commands:' ] ) ;
154+ expect ( r1 . exitCode ) . toBeUndefined ( ) ;
155+
156+ const r2 = await runCLI ( [ '-h' ] ) ;
157+ expectLogsToContainAll ( r2 . logs , [ 'Usage:' , 'Commands:' ] ) ;
158+ expect ( r2 . exitCode ) . toBeUndefined ( ) ;
159+ } ) ;
160+
161+ it ( 'prints error and exits with code 1 for unknown command' , async ( ) => {
162+ const { logs, exitCode } = await runCLI ( [ 'unknown-cmd' ] ) ;
163+ expect ( exitCode ) . toBe ( 1 ) ;
164+ expect ( logs . join ( '\n' ) ) . toContain ( 'Unknown command: unknown-cmd' ) ;
165+ expect ( logs . join ( '\n' ) ) . toContain ( 'Usage:' ) ;
166+ } ) ;
167+
168+ describe ( 'chat command' , ( ) => {
169+ it ( 'shows chat header without prompt' , async ( ) => {
170+ const { logs, exitCode } = await runCLI ( [ 'chat' ] ) ;
171+ expect ( exitCode ) . toBeUndefined ( ) ;
172+ expectLogsToContainAll ( logs , [
173+ '🤖 Gemini Chat Mode' ,
174+ '(Basic implementation - install dependencies for full functionality)' ,
175+ 'Use Ctrl+C to exit'
176+ ] ) ;
177+ } ) ;
178+
179+ it ( 'shows chat header and echoes prompt when args provided' , async ( ) => {
180+ const { logs } = await runCLI ( [ 'chat' , 'Hello' , 'Gemini' ] ) ;
181+ expectLogsToContainAll ( logs , [
182+ '🤖 Gemini Chat Mode' ,
183+ 'You: Hello Gemini' ,
184+ 'Assistant: Hello! This is a basic CLI implementation.'
185+ ] ) ;
186+ } ) ;
187+
188+ it ( 'supports alias "c"' , async ( ) => {
189+ const { logs } = await runCLI ( [ 'c' , 'Hi' ] ) ;
190+ expect ( logs . join ( '\n' ) ) . toContain ( 'You: Hi' ) ;
191+ } ) ;
192+ } ) ;
193+
194+ describe ( 'generate command' , ( ) => {
195+ it ( 'errors when no prompt is provided' , async ( ) => {
196+ const { logs, exitCode } = await runCLI ( [ 'generate' ] ) ;
197+ expect ( exitCode ) . toBeUndefined ( ) ;
198+ expect ( logs . join ( '\n' ) ) . toContain ( 'Error: Please provide a prompt for generation' ) ;
199+ } ) ;
200+
201+ it ( 'generates output and shows note for provided prompt' , async ( ) => {
202+ const { logs } = await runCLI ( [ 'generate' , 'Write' , 'a' , 'haiku' ] ) ;
203+ const out = logs . join ( '\n' ) ;
204+ expect ( out ) . toContain ( 'Generating response for: "Write a haiku"' ) ;
205+ expect ( out ) . toContain ( 'Note: This is a basic CLI implementation.' ) ;
206+ } ) ;
207+
208+ it ( 'supports alias "g"' , async ( ) => {
209+ const { logs } = await runCLI ( [ 'g' , 'test' ] ) ;
210+ expect ( logs . join ( '\n' ) ) . toContain ( 'Generating response for: "test"' ) ;
211+ } ) ;
212+ } ) ;
213+
214+ describe ( 'list-models command' , ( ) => {
215+ it ( 'prints available models' , async ( ) => {
216+ const { logs } = await runCLI ( [ 'list-models' ] ) ;
217+ const out = logs . join ( '\n' ) ;
218+ expect ( out ) . toContain ( 'Available models:' ) ;
219+ expect ( out ) . toContain ( '- gemini-1.5-flash' ) ;
220+ expect ( out ) . toContain ( '- gemini-1.5-pro' ) ;
221+ } ) ;
222+
223+ it ( 'supports alias "models"' , async ( ) => {
224+ const { logs } = await runCLI ( [ 'models' ] ) ;
225+ const out = logs . join ( '\n' ) ;
226+ expect ( out ) . toContain ( 'Available models:' ) ;
227+ } ) ;
228+ } ) ;
229+
230+ describe ( 'auth command' , ( ) => {
231+ it ( 'configures API key when --key is provided with value' , async ( ) => {
232+ const { logs } = await runCLI ( [ 'auth' , '--key' , 'dummy-key' ] ) ;
233+ const out = logs . join ( '\n' ) ;
234+ expect ( out ) . toContain ( '✅ API key configured (basic implementation)' ) ;
235+ expect ( out ) . toContain ( 'Note: In full version, this would save your API key securely.' ) ;
236+ } ) ;
237+
238+ it ( 'errors when --key is missing value' , async ( ) => {
239+ const { logs } = await runCLI ( [ 'auth' , '--key' ] ) ;
240+ const out = logs . join ( '\n' ) ;
241+ expect ( out ) . toContain ( '❌ Error: Please provide an API key' ) ;
242+ expect ( out ) . toContain ( 'Usage: gemini-flow auth --key YOUR_API_KEY' ) ;
243+ } ) ;
244+
245+ it ( 'status shows Found when GEMINI_API_KEY is set' , async ( ) => {
246+ const { logs } = await runCLI ( [ 'auth' , '--status' ] , { env : { GEMINI_API_KEY : 'present' , GOOGLE_AI_API_KEY : undefined } } ) ;
247+ const out = logs . join ( '\n' ) ;
248+ expect ( out ) . toContain ( 'Authentication Status:' ) ;
249+ expect ( out ) . toContain ( 'API Key in Environment: ✅ Found' ) ;
250+ } ) ;
251+
252+ it ( 'status shows Not found and guidance when no env keys are set' , async ( ) => {
253+ const { logs } = await runCLI ( [ 'auth' , '--status' ] , { env : { GEMINI_API_KEY : undefined , GOOGLE_AI_API_KEY : undefined } } ) ;
254+ const out = logs . join ( '\n' ) ;
255+ expect ( out ) . toContain ( 'API Key in Environment: ❌ Not found' ) ;
256+ expect ( out ) . toContain ( 'To set your API key:' ) ;
257+ expect ( out ) . toContain ( 'export GEMINI_API_KEY="your-key-here"' ) ;
258+ } ) ;
259+
260+ it ( 'test flag prints testing message' , async ( ) => {
261+ const { logs } = await runCLI ( [ 'auth' , '--test' ] ) ;
262+ expect ( logs . join ( '\n' ) ) . toContain ( '🔧 Testing API key...' ) ;
263+ } ) ;
264+
265+ it ( 'clear flag prints cleared message' , async ( ) => {
266+ const { logs } = await runCLI ( [ 'auth' , '--clear' ] ) ;
267+ expect ( logs . join ( '\n' ) ) . toContain ( '🧹 API key cleared (basic implementation)' ) ;
268+ } ) ;
269+
270+ it ( 'no flags prints auth help' , async ( ) => {
271+ const { logs } = await runCLI ( [ 'auth' ] ) ;
272+ const out = logs . join ( '\n' ) ;
273+ expect ( out ) . toContain ( 'Auth Commands:' ) ;
274+ expect ( out ) . toContain ( '--key <key>' ) ;
275+ expect ( out ) . toContain ( '--status' ) ;
276+ expect ( out ) . toContain ( '--test' ) ;
277+ expect ( out ) . toContain ( '--clear' ) ;
278+ } ) ;
279+ } ) ;
280+ } ) ;
0 commit comments