Skip to content
This repository was archived by the owner on Jan 29, 2026. It is now read-only.

Commit cb2fe32

Browse files
CodeRabbit Generated Unit Tests: Add TypeScript CLI tests for GeminiCLI, index entry, simple mode
1 parent 3483500 commit cb2fe32

File tree

3 files changed

+521
-0
lines changed

3 files changed

+521
-0
lines changed

tests/unit/cli/gemini-cli.test.ts

Lines changed: 280 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,280 @@
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

Comments
 (0)