Skip to content

Commit 45ff3a0

Browse files
megha1188mboshernitsan
authored andcommitted
Improve code coverage for cli package (#13724)
1 parent 081bcb5 commit 45ff3a0

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

47 files changed

+5126
-500
lines changed

packages/cli/src/__snapshots__/nonInteractiveCli.test.ts.snap

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,32 @@
11
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
22

3+
exports[`runNonInteractive > should emit appropriate error event in streaming JSON mode: 'loop detected' 1`] = `
4+
"{"type":"init","timestamp":"<TIMESTAMP>","session_id":"test-session-id","model":"test-model"}
5+
{"type":"message","timestamp":"<TIMESTAMP>","role":"user","content":"Loop test"}
6+
{"type":"error","timestamp":"<TIMESTAMP>","severity":"warning","message":"Loop detected, stopping execution"}
7+
{"type":"result","timestamp":"<TIMESTAMP>","status":"success","stats":{"total_tokens":0,"input_tokens":0,"output_tokens":0,"duration_ms":<DURATION>,"tool_calls":0}}
8+
"
9+
`;
10+
11+
exports[`runNonInteractive > should emit appropriate error event in streaming JSON mode: 'max session turns' 1`] = `
12+
"{"type":"init","timestamp":"<TIMESTAMP>","session_id":"test-session-id","model":"test-model"}
13+
{"type":"message","timestamp":"<TIMESTAMP>","role":"user","content":"Max turns test"}
14+
{"type":"error","timestamp":"<TIMESTAMP>","severity":"error","message":"Maximum session turns exceeded"}
15+
{"type":"result","timestamp":"<TIMESTAMP>","status":"success","stats":{"total_tokens":0,"input_tokens":0,"output_tokens":0,"duration_ms":<DURATION>,"tool_calls":0}}
16+
"
17+
`;
18+
19+
exports[`runNonInteractive > should emit appropriate events for streaming JSON output 1`] = `
20+
"{"type":"init","timestamp":"<TIMESTAMP>","session_id":"test-session-id","model":"test-model"}
21+
{"type":"message","timestamp":"<TIMESTAMP>","role":"user","content":"Stream test"}
22+
{"type":"message","timestamp":"<TIMESTAMP>","role":"assistant","content":"Thinking...","delta":true}
23+
{"type":"tool_use","timestamp":"<TIMESTAMP>","tool_name":"testTool","tool_id":"tool-1","parameters":{"arg1":"value1"}}
24+
{"type":"tool_result","timestamp":"<TIMESTAMP>","tool_id":"tool-1","status":"success","output":"Tool executed successfully"}
25+
{"type":"message","timestamp":"<TIMESTAMP>","role":"assistant","content":"Final answer","delta":true}
26+
{"type":"result","timestamp":"<TIMESTAMP>","status":"success","stats":{"total_tokens":0,"input_tokens":0,"output_tokens":0,"duration_ms":<DURATION>,"tool_calls":0}}
27+
"
28+
`;
29+
330
exports[`runNonInteractive > should write a single newline between sequential text outputs from the model 1`] = `
431
"Use mock tool
532
Use mock tool again
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
/**
2+
* @license
3+
* Copyright 2025 Google LLC
4+
* SPDX-License-Identifier: Apache-2.0
5+
*/
6+
7+
import { describe, it, expect, vi } from 'vitest';
8+
import { extensionsCommand } from './extensions.js';
9+
10+
// Mock subcommands
11+
vi.mock('./extensions/install.js', () => ({
12+
installCommand: { command: 'install' },
13+
}));
14+
vi.mock('./extensions/uninstall.js', () => ({
15+
uninstallCommand: { command: 'uninstall' },
16+
}));
17+
vi.mock('./extensions/list.js', () => ({ listCommand: { command: 'list' } }));
18+
vi.mock('./extensions/update.js', () => ({
19+
updateCommand: { command: 'update' },
20+
}));
21+
vi.mock('./extensions/disable.js', () => ({
22+
disableCommand: { command: 'disable' },
23+
}));
24+
vi.mock('./extensions/enable.js', () => ({
25+
enableCommand: { command: 'enable' },
26+
}));
27+
vi.mock('./extensions/link.js', () => ({ linkCommand: { command: 'link' } }));
28+
vi.mock('./extensions/new.js', () => ({ newCommand: { command: 'new' } }));
29+
vi.mock('./extensions/validate.js', () => ({
30+
validateCommand: { command: 'validate' },
31+
}));
32+
33+
// Mock gemini.js
34+
vi.mock('../gemini.js', () => ({
35+
initializeOutputListenersAndFlush: vi.fn(),
36+
}));
37+
38+
describe('extensionsCommand', () => {
39+
it('should have correct command and aliases', () => {
40+
expect(extensionsCommand.command).toBe('extensions <command>');
41+
expect(extensionsCommand.aliases).toEqual(['extension']);
42+
expect(extensionsCommand.describe).toBe('Manage Gemini CLI extensions.');
43+
});
44+
45+
it('should register all subcommands in builder', () => {
46+
const mockYargs = {
47+
middleware: vi.fn().mockReturnThis(),
48+
command: vi.fn().mockReturnThis(),
49+
demandCommand: vi.fn().mockReturnThis(),
50+
version: vi.fn().mockReturnThis(),
51+
};
52+
53+
// @ts-expect-error - Mocking yargs
54+
extensionsCommand.builder(mockYargs);
55+
56+
expect(mockYargs.middleware).toHaveBeenCalled();
57+
expect(mockYargs.command).toHaveBeenCalledWith({ command: 'install' });
58+
expect(mockYargs.command).toHaveBeenCalledWith({ command: 'uninstall' });
59+
expect(mockYargs.command).toHaveBeenCalledWith({ command: 'list' });
60+
expect(mockYargs.command).toHaveBeenCalledWith({ command: 'update' });
61+
expect(mockYargs.command).toHaveBeenCalledWith({ command: 'disable' });
62+
expect(mockYargs.command).toHaveBeenCalledWith({ command: 'enable' });
63+
expect(mockYargs.command).toHaveBeenCalledWith({ command: 'link' });
64+
expect(mockYargs.command).toHaveBeenCalledWith({ command: 'new' });
65+
expect(mockYargs.command).toHaveBeenCalledWith({ command: 'validate' });
66+
expect(mockYargs.demandCommand).toHaveBeenCalledWith(1, expect.any(String));
67+
expect(mockYargs.version).toHaveBeenCalledWith(false);
68+
});
69+
70+
it('should have a handler that does nothing', () => {
71+
// @ts-expect-error - Handler doesn't take arguments in this case
72+
expect(extensionsCommand.handler()).toBeUndefined();
73+
});
74+
});
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
/**
2+
* @license
3+
* Copyright 2025 Google LLC
4+
* SPDX-License-Identifier: Apache-2.0
5+
*/
6+
7+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
8+
import { exitCli } from './utils.js';
9+
import { runExitCleanup } from '../utils/cleanup.js';
10+
11+
vi.mock('../utils/cleanup.js', () => ({
12+
runExitCleanup: vi.fn(),
13+
}));
14+
15+
describe('utils', () => {
16+
const originalProcessExit = process.exit;
17+
18+
beforeEach(() => {
19+
// @ts-expect-error - Mocking process.exit
20+
process.exit = vi.fn();
21+
});
22+
23+
afterEach(() => {
24+
process.exit = originalProcessExit;
25+
vi.clearAllMocks();
26+
});
27+
28+
describe('exitCli', () => {
29+
it('should call runExitCleanup and process.exit with default exit code 0', async () => {
30+
await exitCli();
31+
expect(runExitCleanup).toHaveBeenCalled();
32+
expect(process.exit).toHaveBeenCalledWith(0);
33+
});
34+
35+
it('should call runExitCleanup and process.exit with specified exit code', async () => {
36+
await exitCli(1);
37+
expect(runExitCleanup).toHaveBeenCalled();
38+
expect(process.exit).toHaveBeenCalledWith(1);
39+
});
40+
});
41+
});

packages/cli/src/core/auth.test.ts

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
/**
2+
* @license
3+
* Copyright 2025 Google LLC
4+
* SPDX-License-Identifier: Apache-2.0
5+
*/
6+
7+
import { describe, it, expect, vi, beforeEach } from 'vitest';
8+
import { performInitialAuth } from './auth.js';
9+
import { type Config } from '@google/gemini-cli-core';
10+
11+
vi.mock('@google/gemini-cli-core', () => ({
12+
AuthType: {
13+
OAUTH: 'oauth',
14+
},
15+
getErrorMessage: (e: unknown) => (e as Error).message,
16+
}));
17+
18+
const AuthType = {
19+
OAUTH: 'oauth',
20+
} as const;
21+
22+
describe('auth', () => {
23+
let mockConfig: Config;
24+
25+
beforeEach(() => {
26+
mockConfig = {
27+
refreshAuth: vi.fn(),
28+
} as unknown as Config;
29+
});
30+
31+
it('should return null if authType is undefined', async () => {
32+
const result = await performInitialAuth(mockConfig, undefined);
33+
expect(result).toBeNull();
34+
expect(mockConfig.refreshAuth).not.toHaveBeenCalled();
35+
});
36+
37+
it('should return null on successful auth', async () => {
38+
const result = await performInitialAuth(
39+
mockConfig,
40+
AuthType.OAUTH as unknown as Parameters<typeof performInitialAuth>[1],
41+
);
42+
expect(result).toBeNull();
43+
expect(mockConfig.refreshAuth).toHaveBeenCalledWith(AuthType.OAUTH);
44+
});
45+
46+
it('should return error message on failed auth', async () => {
47+
const error = new Error('Auth failed');
48+
vi.mocked(mockConfig.refreshAuth).mockRejectedValue(error);
49+
const result = await performInitialAuth(
50+
mockConfig,
51+
AuthType.OAUTH as unknown as Parameters<typeof performInitialAuth>[1],
52+
);
53+
expect(result).toBe('Failed to login. Message: Auth failed');
54+
expect(mockConfig.refreshAuth).toHaveBeenCalledWith(AuthType.OAUTH);
55+
});
56+
});
Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
/**
2+
* @license
3+
* Copyright 2025 Google LLC
4+
* SPDX-License-Identifier: Apache-2.0
5+
*/
6+
7+
import { describe, it, expect, vi, beforeEach } from 'vitest';
8+
import { initializeApp } from './initializer.js';
9+
import {
10+
IdeClient,
11+
logIdeConnection,
12+
logCliConfiguration,
13+
type Config,
14+
} from '@google/gemini-cli-core';
15+
import { performInitialAuth } from './auth.js';
16+
import { validateTheme } from './theme.js';
17+
import { type LoadedSettings } from '../config/settings.js';
18+
19+
vi.mock('@google/gemini-cli-core', async (importOriginal) => {
20+
const actual =
21+
await importOriginal<typeof import('@google/gemini-cli-core')>();
22+
return {
23+
...actual,
24+
IdeClient: {
25+
getInstance: vi.fn(),
26+
},
27+
logIdeConnection: vi.fn(),
28+
logCliConfiguration: vi.fn(),
29+
StartSessionEvent: vi.fn(),
30+
IdeConnectionEvent: vi.fn(),
31+
};
32+
});
33+
34+
vi.mock('./auth.js', () => ({
35+
performInitialAuth: vi.fn(),
36+
}));
37+
38+
vi.mock('./theme.js', () => ({
39+
validateTheme: vi.fn(),
40+
}));
41+
42+
describe('initializer', () => {
43+
let mockConfig: {
44+
getToolRegistry: ReturnType<typeof vi.fn>;
45+
getIdeMode: ReturnType<typeof vi.fn>;
46+
getGeminiMdFileCount: ReturnType<typeof vi.fn>;
47+
};
48+
let mockSettings: LoadedSettings;
49+
let mockIdeClient: {
50+
connect: ReturnType<typeof vi.fn>;
51+
};
52+
53+
beforeEach(() => {
54+
vi.clearAllMocks();
55+
mockConfig = {
56+
getToolRegistry: vi.fn(),
57+
getIdeMode: vi.fn().mockReturnValue(false),
58+
getGeminiMdFileCount: vi.fn().mockReturnValue(5),
59+
};
60+
mockSettings = {
61+
merged: {
62+
security: {
63+
auth: {
64+
selectedType: 'oauth',
65+
},
66+
},
67+
},
68+
} as unknown as LoadedSettings;
69+
mockIdeClient = {
70+
connect: vi.fn(),
71+
};
72+
vi.mocked(IdeClient.getInstance).mockResolvedValue(
73+
mockIdeClient as unknown as IdeClient,
74+
);
75+
vi.mocked(performInitialAuth).mockResolvedValue(null);
76+
vi.mocked(validateTheme).mockReturnValue(null);
77+
});
78+
79+
it('should initialize correctly in non-IDE mode', async () => {
80+
const result = await initializeApp(
81+
mockConfig as unknown as Config,
82+
mockSettings,
83+
);
84+
85+
expect(result).toEqual({
86+
authError: null,
87+
themeError: null,
88+
shouldOpenAuthDialog: false,
89+
geminiMdFileCount: 5,
90+
});
91+
expect(performInitialAuth).toHaveBeenCalledWith(mockConfig, 'oauth');
92+
expect(validateTheme).toHaveBeenCalledWith(mockSettings);
93+
expect(logCliConfiguration).toHaveBeenCalled();
94+
expect(IdeClient.getInstance).not.toHaveBeenCalled();
95+
});
96+
97+
it('should initialize correctly in IDE mode', async () => {
98+
mockConfig.getIdeMode.mockReturnValue(true);
99+
const result = await initializeApp(
100+
mockConfig as unknown as Config,
101+
mockSettings,
102+
);
103+
104+
expect(result).toEqual({
105+
authError: null,
106+
themeError: null,
107+
shouldOpenAuthDialog: false,
108+
geminiMdFileCount: 5,
109+
});
110+
expect(IdeClient.getInstance).toHaveBeenCalled();
111+
expect(mockIdeClient.connect).toHaveBeenCalled();
112+
expect(logIdeConnection).toHaveBeenCalledWith(
113+
mockConfig as unknown as Config,
114+
expect.any(Object),
115+
);
116+
});
117+
118+
it('should handle auth error', async () => {
119+
vi.mocked(performInitialAuth).mockResolvedValue('Auth failed');
120+
const result = await initializeApp(
121+
mockConfig as unknown as Config,
122+
mockSettings,
123+
);
124+
125+
expect(result.authError).toBe('Auth failed');
126+
expect(result.shouldOpenAuthDialog).toBe(true);
127+
});
128+
129+
it('should handle undefined auth type', async () => {
130+
mockSettings.merged.security!.auth!.selectedType = undefined;
131+
const result = await initializeApp(
132+
mockConfig as unknown as Config,
133+
mockSettings,
134+
);
135+
136+
expect(result.shouldOpenAuthDialog).toBe(true);
137+
});
138+
139+
it('should handle theme error', async () => {
140+
vi.mocked(validateTheme).mockReturnValue('Theme not found');
141+
const result = await initializeApp(
142+
mockConfig as unknown as Config,
143+
mockSettings,
144+
);
145+
146+
expect(result.themeError).toBe('Theme not found');
147+
});
148+
});

0 commit comments

Comments
 (0)