Skip to content

Commit 3e3da76

Browse files
gemini-cli-robotgsquared94galdawave
authored
fix(patch): cherry-pick ea48bd9 to release/v0.31.0-preview.1-pr-20577 [CONFLICTS] (#20592)
Co-authored-by: Gaurav <39389231+gsquared94@users.noreply.github.com> Co-authored-by: Gal Zahavi <38544478+galz10@users.noreply.github.com> Co-authored-by: galz10 <galzahavi@google.com>
1 parent f310915 commit 3e3da76

17 files changed

Lines changed: 672 additions & 19 deletions

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

Lines changed: 48 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@ vi.mock('@google/gemini-cli-core', async (importOriginal) => {
1717
await importOriginal<typeof import('@google/gemini-cli-core')>();
1818
return {
1919
...actual,
20-
getErrorMessage: (e: unknown) => (e as Error).message,
2120
};
2221
});
2322

@@ -32,7 +31,7 @@ describe('auth', () => {
3231

3332
it('should return null if authType is undefined', async () => {
3433
const result = await performInitialAuth(mockConfig, undefined);
35-
expect(result).toBeNull();
34+
expect(result).toEqual({ authError: null, accountSuspensionInfo: null });
3635
expect(mockConfig.refreshAuth).not.toHaveBeenCalled();
3736
});
3837

@@ -41,7 +40,7 @@ describe('auth', () => {
4140
mockConfig,
4241
AuthType.LOGIN_WITH_GOOGLE,
4342
);
44-
expect(result).toBeNull();
43+
expect(result).toEqual({ authError: null, accountSuspensionInfo: null });
4544
expect(mockConfig.refreshAuth).toHaveBeenCalledWith(
4645
AuthType.LOGIN_WITH_GOOGLE,
4746
);
@@ -54,7 +53,10 @@ describe('auth', () => {
5453
mockConfig,
5554
AuthType.LOGIN_WITH_GOOGLE,
5655
);
57-
expect(result).toBe('Failed to login. Message: Auth failed');
56+
expect(result).toEqual({
57+
authError: 'Failed to login. Message: Auth failed',
58+
accountSuspensionInfo: null,
59+
});
5860
expect(mockConfig.refreshAuth).toHaveBeenCalledWith(
5961
AuthType.LOGIN_WITH_GOOGLE,
6062
);
@@ -68,7 +70,48 @@ describe('auth', () => {
6870
mockConfig,
6971
AuthType.LOGIN_WITH_GOOGLE,
7072
);
71-
expect(result).toBeNull();
73+
expect(result).toEqual({ authError: null, accountSuspensionInfo: null });
74+
expect(mockConfig.refreshAuth).toHaveBeenCalledWith(
75+
AuthType.LOGIN_WITH_GOOGLE,
76+
);
77+
});
78+
79+
it('should return accountSuspensionInfo for 403 TOS_VIOLATION error', async () => {
80+
vi.mocked(mockConfig.refreshAuth).mockRejectedValue({
81+
response: {
82+
data: {
83+
error: {
84+
code: 403,
85+
message:
86+
'This service has been disabled for violation of Terms of Service.',
87+
details: [
88+
{
89+
'@type': 'type.googleapis.com/google.rpc.ErrorInfo',
90+
reason: 'TOS_VIOLATION',
91+
domain: 'example.googleapis.com',
92+
metadata: {
93+
appeal_url: 'https://example.com/appeal',
94+
appeal_url_link_text: 'Appeal Here',
95+
},
96+
},
97+
],
98+
},
99+
},
100+
},
101+
});
102+
const result = await performInitialAuth(
103+
mockConfig,
104+
AuthType.LOGIN_WITH_GOOGLE,
105+
);
106+
expect(result).toEqual({
107+
authError: null,
108+
accountSuspensionInfo: {
109+
message:
110+
'This service has been disabled for violation of Terms of Service.',
111+
appealUrl: 'https://example.com/appeal',
112+
appealLinkText: 'Appeal Here',
113+
},
114+
});
72115
expect(mockConfig.refreshAuth).toHaveBeenCalledWith(
73116
AuthType.LOGIN_WITH_GOOGLE,
74117
);

packages/cli/src/core/auth.ts

Lines changed: 28 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,20 +9,28 @@ import {
99
type Config,
1010
getErrorMessage,
1111
ValidationRequiredError,
12+
isAccountSuspendedError,
1213
} from '@google/gemini-cli-core';
1314

15+
import type { AccountSuspensionInfo } from '../ui/contexts/UIStateContext.js';
16+
17+
export interface InitialAuthResult {
18+
authError: string | null;
19+
accountSuspensionInfo: AccountSuspensionInfo | null;
20+
}
21+
1422
/**
1523
* Handles the initial authentication flow.
1624
* @param config The application config.
1725
* @param authType The selected auth type.
18-
* @returns An error message if authentication fails, otherwise null.
26+
* @returns The auth result with error message and account suspension status.
1927
*/
2028
export async function performInitialAuth(
2129
config: Config,
2230
authType: AuthType | undefined,
23-
): Promise<string | null> {
31+
): Promise<InitialAuthResult> {
2432
if (!authType) {
25-
return null;
33+
return { authError: null, accountSuspensionInfo: null };
2634
}
2735

2836
try {
@@ -33,10 +41,24 @@ export async function performInitialAuth(
3341
if (e instanceof ValidationRequiredError) {
3442
// Don't treat validation required as a fatal auth error during startup.
3543
// This allows the React UI to load and show the ValidationDialog.
36-
return null;
44+
return { authError: null, accountSuspensionInfo: null };
45+
}
46+
const suspendedError = isAccountSuspendedError(e);
47+
if (suspendedError) {
48+
return {
49+
authError: null,
50+
accountSuspensionInfo: {
51+
message: suspendedError.message,
52+
appealUrl: suspendedError.appealUrl,
53+
appealLinkText: suspendedError.appealLinkText,
54+
},
55+
};
3756
}
38-
return `Failed to login. Message: ${getErrorMessage(e)}`;
57+
return {
58+
authError: `Failed to login. Message: ${getErrorMessage(e)}`,
59+
accountSuspensionInfo: null,
60+
};
3961
}
4062

41-
return null;
63+
return { authError: null, accountSuspensionInfo: null };
4264
}

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

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,10 @@ describe('initializer', () => {
7272
vi.mocked(IdeClient.getInstance).mockResolvedValue(
7373
mockIdeClient as unknown as IdeClient,
7474
);
75-
vi.mocked(performInitialAuth).mockResolvedValue(null);
75+
vi.mocked(performInitialAuth).mockResolvedValue({
76+
authError: null,
77+
accountSuspensionInfo: null,
78+
});
7679
vi.mocked(validateTheme).mockReturnValue(null);
7780
});
7881

@@ -84,6 +87,7 @@ describe('initializer', () => {
8487

8588
expect(result).toEqual({
8689
authError: null,
90+
accountSuspensionInfo: null,
8791
themeError: null,
8892
shouldOpenAuthDialog: false,
8993
geminiMdFileCount: 5,
@@ -103,6 +107,7 @@ describe('initializer', () => {
103107

104108
expect(result).toEqual({
105109
authError: null,
110+
accountSuspensionInfo: null,
106111
themeError: null,
107112
shouldOpenAuthDialog: false,
108113
geminiMdFileCount: 5,
@@ -116,7 +121,10 @@ describe('initializer', () => {
116121
});
117122

118123
it('should handle auth error', async () => {
119-
vi.mocked(performInitialAuth).mockResolvedValue('Auth failed');
124+
vi.mocked(performInitialAuth).mockResolvedValue({
125+
authError: 'Auth failed',
126+
accountSuspensionInfo: null,
127+
});
120128
const result = await initializeApp(
121129
mockConfig as unknown as Config,
122130
mockSettings,

packages/cli/src/core/initializer.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,11 @@ import {
1717
import { type LoadedSettings } from '../config/settings.js';
1818
import { performInitialAuth } from './auth.js';
1919
import { validateTheme } from './theme.js';
20+
import type { AccountSuspensionInfo } from '../ui/contexts/UIStateContext.js';
2021

2122
export interface InitializationResult {
2223
authError: string | null;
24+
accountSuspensionInfo: AccountSuspensionInfo | null;
2325
themeError: string | null;
2426
shouldOpenAuthDialog: boolean;
2527
geminiMdFileCount: number;
@@ -37,7 +39,7 @@ export async function initializeApp(
3739
settings: LoadedSettings,
3840
): Promise<InitializationResult> {
3941
const authHandle = startupProfiler.start('authenticate');
40-
const authError = await performInitialAuth(
42+
const { authError, accountSuspensionInfo } = await performInitialAuth(
4143
config,
4244
settings.merged.security.auth.selectedType,
4345
);
@@ -60,6 +62,7 @@ export async function initializeApp(
6062

6163
return {
6264
authError,
65+
accountSuspensionInfo,
6366
themeError,
6467
shouldOpenAuthDialog,
6568
geminiMdFileCount: config.getGeminiMdFileCount(),

packages/cli/src/gemini.test.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1201,6 +1201,7 @@ describe('startInteractiveUI', () => {
12011201
const mockWorkspaceRoot = '/root';
12021202
const mockInitializationResult = {
12031203
authError: null,
1204+
accountSuspensionInfo: null,
12041205
themeError: null,
12051206
shouldOpenAuthDialog: false,
12061207
geminiMdFileCount: 0,

packages/cli/src/test-utils/AppRig.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,8 @@ vi.mock('../ui/auth/useAuth.js', () => ({
6363
onAuthError: vi.fn(),
6464
apiKeyDefaultValue: 'test-api-key',
6565
reloadApiKey: vi.fn().mockResolvedValue('test-api-key'),
66+
accountSuspensionInfo: null,
67+
setAccountSuspensionInfo: vi.fn(),
6668
}),
6769
validateAuthMethodWithSettings: () => null,
6870
}));
@@ -292,6 +294,7 @@ export class AppRig {
292294
version="test-version"
293295
initializationResult={{
294296
authError: null,
297+
accountSuspensionInfo: null,
295298
themeError: null,
296299
shouldOpenAuthDialog: false,
297300
geminiMdFileCount: 0,

packages/cli/src/test-utils/render.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -574,6 +574,8 @@ const mockUIActions: UIActions = {
574574
onHintSubmit: vi.fn(),
575575
handleRestart: vi.fn(),
576576
handleNewAgentsSelect: vi.fn(),
577+
getPreferredEditor: vi.fn(),
578+
clearAccountSuspension: vi.fn(),
577579
};
578580

579581
let capturedOverflowState: OverflowState | undefined;

packages/cli/src/ui/AppContainer.tsx

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -692,7 +692,14 @@ export const AppContainer = (props: AppContainerProps) => {
692692
onAuthError,
693693
apiKeyDefaultValue,
694694
reloadApiKey,
695-
} = useAuthCommand(settings, config, initializationResult.authError);
695+
accountSuspensionInfo,
696+
setAccountSuspensionInfo,
697+
} = useAuthCommand(
698+
settings,
699+
config,
700+
initializationResult.authError,
701+
initializationResult.accountSuspensionInfo,
702+
);
696703
const [authContext, setAuthContext] = useState<{ requiresRestart?: boolean }>(
697704
{},
698705
);
@@ -2223,6 +2230,7 @@ Logging in with Google... Restarting Gemini CLI to continue.
22232230
isAuthenticating,
22242231
isConfigInitialized,
22252232
authError,
2233+
accountSuspensionInfo,
22262234
isAuthDialogOpen,
22272235
isAwaitingApiKeyInput: authState === AuthState.AwaitingApiKeyInput,
22282236
apiKeyDefaultValue,
@@ -2351,6 +2359,7 @@ Logging in with Google... Restarting Gemini CLI to continue.
23512359
isAuthenticating,
23522360
isConfigInitialized,
23532361
authError,
2362+
accountSuspensionInfo,
23542363
isAuthDialogOpen,
23552364
editorError,
23562365
isEditorDialogOpen,
@@ -2554,6 +2563,11 @@ Logging in with Google... Restarting Gemini CLI to continue.
25542563
}
25552564
setNewAgents(null);
25562565
},
2566+
getPreferredEditor,
2567+
clearAccountSuspension: () => {
2568+
setAccountSuspensionInfo(null);
2569+
setAuthState(AuthState.Updating);
2570+
},
25572571
}),
25582572
[
25592573
handleThemeSelect,
@@ -2602,9 +2616,11 @@ Logging in with Google... Restarting Gemini CLI to continue.
26022616
setActiveBackgroundShellPid,
26032617
setIsBackgroundShellListOpen,
26042618
setAuthContext,
2619+
setAccountSuspensionInfo,
26052620
newAgents,
26062621
config,
26072622
historyManager,
2623+
getPreferredEditor,
26082624
],
26092625
);
26102626

0 commit comments

Comments
 (0)