mirror of
https://github.com/QwenLM/qwen-code.git
synced 2025-12-19 09:33:53 +00:00
This commit is contained in:
committed by
GitHub
parent
d820c2335b
commit
925d747b9d
@@ -16,18 +16,6 @@ export const validateAuthMethod = (authMethod: string): string | null => {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (authMethod === AuthType.LOGIN_WITH_GOOGLE_GCA) {
|
|
||||||
if (!process.env['GOOGLE_CLOUD_PROJECT']) {
|
|
||||||
return (
|
|
||||||
'[Error] GOOGLE_CLOUD_PROJECT is not set.\n' +
|
|
||||||
'Please set it using:\n' +
|
|
||||||
' export GOOGLE_CLOUD_PROJECT=<your-project-id>\n' +
|
|
||||||
'and try again.'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (authMethod === AuthType.USE_GEMINI) {
|
if (authMethod === AuthType.USE_GEMINI) {
|
||||||
if (!process.env['GEMINI_API_KEY']) {
|
if (!process.env['GEMINI_API_KEY']) {
|
||||||
return 'GEMINI_API_KEY environment variable not found. Add that to your environment and try again (no reload needed if using .env)!';
|
return 'GEMINI_API_KEY environment variable not found. Add that to your environment and try again (no reload needed if using .env)!';
|
||||||
|
|||||||
@@ -6,12 +6,12 @@
|
|||||||
|
|
||||||
import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest';
|
import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest';
|
||||||
import { aboutCommand } from './aboutCommand.js';
|
import { aboutCommand } from './aboutCommand.js';
|
||||||
import type { CommandContext } from './types.js';
|
import { type CommandContext } from './types.js';
|
||||||
import { createMockCommandContext } from '../../test-utils/mockCommandContext.js';
|
import { createMockCommandContext } from '../../test-utils/mockCommandContext.js';
|
||||||
import * as versionUtils from '../../utils/version.js';
|
import * as versionUtils from '../../utils/version.js';
|
||||||
import { MessageType } from '../types.js';
|
import { MessageType } from '../types.js';
|
||||||
|
|
||||||
import type { IdeClient } from '@google/gemini-cli-core';
|
import type { IdeClient } from '../../../../core/src/ide/ide-client.js';
|
||||||
|
|
||||||
vi.mock('../../utils/version.js', () => ({
|
vi.mock('../../utils/version.js', () => ({
|
||||||
getCliVersion: vi.fn(),
|
getCliVersion: vi.fn(),
|
||||||
@@ -29,11 +29,10 @@ describe('aboutCommand', () => {
|
|||||||
getModel: vi.fn(),
|
getModel: vi.fn(),
|
||||||
getIdeClient: vi.fn(),
|
getIdeClient: vi.fn(),
|
||||||
getIdeMode: vi.fn().mockReturnValue(true),
|
getIdeMode: vi.fn().mockReturnValue(true),
|
||||||
getGeminiClient: vi.fn(),
|
|
||||||
},
|
},
|
||||||
settings: {
|
settings: {
|
||||||
merged: {
|
merged: {
|
||||||
selectedAuthType: 'oauth-gca',
|
selectedAuthType: 'test-auth',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -53,11 +52,6 @@ describe('aboutCommand', () => {
|
|||||||
vi.spyOn(mockContext.services.config!, 'getIdeClient').mockReturnValue({
|
vi.spyOn(mockContext.services.config!, 'getIdeClient').mockReturnValue({
|
||||||
getDetectedIdeDisplayName: vi.fn().mockReturnValue('test-ide'),
|
getDetectedIdeDisplayName: vi.fn().mockReturnValue('test-ide'),
|
||||||
} as Partial<IdeClient> as IdeClient);
|
} as Partial<IdeClient> as IdeClient);
|
||||||
vi.spyOn(mockContext.services.config!, 'getGeminiClient').mockReturnValue({
|
|
||||||
getUserTier: vi.fn().mockReturnValue(undefined),
|
|
||||||
} as unknown as ReturnType<
|
|
||||||
NonNullable<typeof mockContext.services.config>['getGeminiClient']
|
|
||||||
>);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
@@ -89,10 +83,9 @@ describe('aboutCommand', () => {
|
|||||||
osVersion: 'test-os',
|
osVersion: 'test-os',
|
||||||
sandboxEnv: 'no sandbox',
|
sandboxEnv: 'no sandbox',
|
||||||
modelVersion: 'test-model',
|
modelVersion: 'test-model',
|
||||||
selectedAuthType: 'oauth-gca',
|
selectedAuthType: 'test-auth',
|
||||||
gcpProject: 'test-gcp-project',
|
gcpProject: 'test-gcp-project',
|
||||||
ideClient: 'test-ide',
|
ideClient: 'test-ide',
|
||||||
userTier: undefined,
|
|
||||||
},
|
},
|
||||||
expect.any(Number),
|
expect.any(Number),
|
||||||
);
|
);
|
||||||
@@ -132,14 +125,11 @@ describe('aboutCommand', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should not show ide client when it is not detected', async () => {
|
it('should not show ide client when it is not detected', async () => {
|
||||||
// Change to oauth type that doesn't use GCP project
|
|
||||||
mockContext.services.settings.merged.selectedAuthType = 'oauth';
|
|
||||||
|
|
||||||
vi.spyOn(mockContext.services.config!, 'getIdeClient').mockReturnValue({
|
vi.spyOn(mockContext.services.config!, 'getIdeClient').mockReturnValue({
|
||||||
getDetectedIdeDisplayName: vi.fn().mockReturnValue(undefined),
|
getDetectedIdeDisplayName: vi.fn().mockReturnValue(undefined),
|
||||||
} as Partial<IdeClient> as IdeClient);
|
} as Partial<IdeClient> as IdeClient);
|
||||||
|
|
||||||
process.env['SANDBOX'] = '';
|
process.env.SANDBOX = '';
|
||||||
if (!aboutCommand.action) {
|
if (!aboutCommand.action) {
|
||||||
throw new Error('The about command must have an action.');
|
throw new Error('The about command must have an action.');
|
||||||
}
|
}
|
||||||
@@ -153,8 +143,8 @@ describe('aboutCommand', () => {
|
|||||||
osVersion: 'test-os',
|
osVersion: 'test-os',
|
||||||
sandboxEnv: 'no sandbox',
|
sandboxEnv: 'no sandbox',
|
||||||
modelVersion: 'test-model',
|
modelVersion: 'test-model',
|
||||||
selectedAuthType: 'oauth',
|
selectedAuthType: 'test-auth',
|
||||||
gcpProject: '',
|
gcpProject: 'test-gcp-project',
|
||||||
ideClient: '',
|
ideClient: '',
|
||||||
}),
|
}),
|
||||||
expect.any(Number),
|
expect.any(Number),
|
||||||
|
|||||||
@@ -28,18 +28,11 @@ export const aboutCommand: SlashCommand = {
|
|||||||
const cliVersion = await getCliVersion();
|
const cliVersion = await getCliVersion();
|
||||||
const selectedAuthType =
|
const selectedAuthType =
|
||||||
context.services.settings.merged.selectedAuthType || '';
|
context.services.settings.merged.selectedAuthType || '';
|
||||||
// Only show GCP Project for auth types that actually use it
|
const gcpProject = process.env['GOOGLE_CLOUD_PROJECT'] || '';
|
||||||
const gcpProject =
|
|
||||||
selectedAuthType === 'oauth-gca' ||
|
|
||||||
selectedAuthType === 'vertex-ai' ||
|
|
||||||
selectedAuthType === 'cloud-shell'
|
|
||||||
? process.env['GOOGLE_CLOUD_PROJECT'] || ''
|
|
||||||
: '';
|
|
||||||
const ideClient =
|
const ideClient =
|
||||||
(context.services.config?.getIdeMode() &&
|
(context.services.config?.getIdeMode() &&
|
||||||
context.services.config?.getIdeClient()?.getDetectedIdeDisplayName()) ||
|
context.services.config?.getIdeClient()?.getDetectedIdeDisplayName()) ||
|
||||||
'';
|
'';
|
||||||
const userTier = context.services.config?.getGeminiClient()?.getUserTier();
|
|
||||||
|
|
||||||
const aboutItem: Omit<HistoryItemAbout, 'id'> = {
|
const aboutItem: Omit<HistoryItemAbout, 'id'> = {
|
||||||
type: MessageType.ABOUT,
|
type: MessageType.ABOUT,
|
||||||
@@ -50,7 +43,6 @@ export const aboutCommand: SlashCommand = {
|
|||||||
selectedAuthType,
|
selectedAuthType,
|
||||||
gcpProject,
|
gcpProject,
|
||||||
ideClient,
|
ideClient,
|
||||||
userTier,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
context.ui.addItem(aboutItem, Date.now());
|
context.ui.addItem(aboutItem, Date.now());
|
||||||
|
|||||||
@@ -8,8 +8,6 @@ import type React from 'react';
|
|||||||
import { Box, Text } from 'ink';
|
import { Box, Text } from 'ink';
|
||||||
import { Colors } from '../colors.js';
|
import { Colors } from '../colors.js';
|
||||||
import { GIT_COMMIT_INFO } from '../../generated/git-commit.js';
|
import { GIT_COMMIT_INFO } from '../../generated/git-commit.js';
|
||||||
import type { UserTierId } from '@google/gemini-cli-core';
|
|
||||||
import { getLicenseDisplay } from '../../utils/license.js';
|
|
||||||
|
|
||||||
interface AboutBoxProps {
|
interface AboutBoxProps {
|
||||||
cliVersion: string;
|
cliVersion: string;
|
||||||
@@ -19,7 +17,6 @@ interface AboutBoxProps {
|
|||||||
selectedAuthType: string;
|
selectedAuthType: string;
|
||||||
gcpProject: string;
|
gcpProject: string;
|
||||||
ideClient: string;
|
ideClient: string;
|
||||||
userTier?: UserTierId;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const AboutBox: React.FC<AboutBoxProps> = ({
|
export const AboutBox: React.FC<AboutBoxProps> = ({
|
||||||
@@ -30,7 +27,6 @@ export const AboutBox: React.FC<AboutBoxProps> = ({
|
|||||||
selectedAuthType,
|
selectedAuthType,
|
||||||
gcpProject,
|
gcpProject,
|
||||||
ideClient,
|
ideClient,
|
||||||
userTier,
|
|
||||||
}) => (
|
}) => (
|
||||||
<Box
|
<Box
|
||||||
borderStyle="round"
|
borderStyle="round"
|
||||||
@@ -109,16 +105,6 @@ export const AboutBox: React.FC<AboutBoxProps> = ({
|
|||||||
</Text>
|
</Text>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
<Box flexDirection="row">
|
|
||||||
<Box width="35%">
|
|
||||||
<Text bold color={Colors.LightBlue}>
|
|
||||||
License
|
|
||||||
</Text>
|
|
||||||
</Box>
|
|
||||||
<Box>
|
|
||||||
<Text>{getLicenseDisplay(selectedAuthType, userTier)}</Text>
|
|
||||||
</Box>
|
|
||||||
</Box>
|
|
||||||
{gcpProject && (
|
{gcpProject && (
|
||||||
<Box flexDirection="row">
|
<Box flexDirection="row">
|
||||||
<Box width="35%">
|
<Box width="35%">
|
||||||
|
|||||||
@@ -64,14 +64,9 @@ export function AuthDialog({
|
|||||||
});
|
});
|
||||||
const items = [
|
const items = [
|
||||||
{
|
{
|
||||||
label: 'Login with Google - Free Tier',
|
label: 'Login with Google',
|
||||||
value: AuthType.LOGIN_WITH_GOOGLE,
|
value: AuthType.LOGIN_WITH_GOOGLE,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
label:
|
|
||||||
'Login with Google - Gemini Code Assist (Requires GOOGLE_CLOUD_PROJECT)',
|
|
||||||
value: AuthType.LOGIN_WITH_GOOGLE_GCA,
|
|
||||||
},
|
|
||||||
...(process.env['CLOUD_SHELL'] === 'true'
|
...(process.env['CLOUD_SHELL'] === 'true'
|
||||||
? [
|
? [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -74,7 +74,6 @@ export const HistoryItemDisplay: React.FC<HistoryItemDisplayProps> = ({
|
|||||||
selectedAuthType={item.selectedAuthType}
|
selectedAuthType={item.selectedAuthType}
|
||||||
gcpProject={item.gcpProject}
|
gcpProject={item.gcpProject}
|
||||||
ideClient={item.ideClient}
|
ideClient={item.ideClient}
|
||||||
userTier={item.userTier}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{item.type === 'help' && commands && <Help commands={commands} />}
|
{item.type === 'help' && commands && <Help commands={commands} />}
|
||||||
|
|||||||
@@ -7,7 +7,6 @@
|
|||||||
import type {
|
import type {
|
||||||
ToolCallConfirmationDetails,
|
ToolCallConfirmationDetails,
|
||||||
ToolResultDisplay,
|
ToolResultDisplay,
|
||||||
UserTierId,
|
|
||||||
} from '@google/gemini-cli-core';
|
} from '@google/gemini-cli-core';
|
||||||
|
|
||||||
// Only defining the state enum needed by the UI
|
// Only defining the state enum needed by the UI
|
||||||
@@ -97,7 +96,6 @@ export type HistoryItemAbout = HistoryItemBase & {
|
|||||||
selectedAuthType: string;
|
selectedAuthType: string;
|
||||||
gcpProject: string;
|
gcpProject: string;
|
||||||
ideClient: string;
|
ideClient: string;
|
||||||
userTier?: UserTierId;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export type HistoryItemHelp = HistoryItemBase & {
|
export type HistoryItemHelp = HistoryItemBase & {
|
||||||
|
|||||||
@@ -1,119 +0,0 @@
|
|||||||
/**
|
|
||||||
* @license
|
|
||||||
* Copyright 2025 Google LLC
|
|
||||||
* SPDX-License-Identifier: Apache-2.0
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { describe, it, expect } from 'vitest';
|
|
||||||
import { getLicenseDisplay } from './license.js';
|
|
||||||
import { AuthType, UserTierId } from '@google/gemini-cli-core';
|
|
||||||
|
|
||||||
describe('getLicenseDisplay', () => {
|
|
||||||
describe('Free Tier (Login with Google)', () => {
|
|
||||||
it('should return Free Tier for LOGIN_WITH_GOOGLE', () => {
|
|
||||||
expect(getLicenseDisplay(AuthType.LOGIN_WITH_GOOGLE)).toBe(
|
|
||||||
'Free Tier (Login with Google)',
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should ignore userTier for LOGIN_WITH_GOOGLE', () => {
|
|
||||||
expect(
|
|
||||||
getLicenseDisplay(AuthType.LOGIN_WITH_GOOGLE, UserTierId.STANDARD),
|
|
||||||
).toBe('Free Tier (Login with Google)');
|
|
||||||
expect(
|
|
||||||
getLicenseDisplay(AuthType.LOGIN_WITH_GOOGLE, UserTierId.LEGACY),
|
|
||||||
).toBe('Free Tier (Login with Google)');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('Gemini Code Assist (Google Workspace)', () => {
|
|
||||||
it('should return GCA Standard for LOGIN_WITH_GOOGLE_GCA with STANDARD tier', () => {
|
|
||||||
expect(
|
|
||||||
getLicenseDisplay(AuthType.LOGIN_WITH_GOOGLE_GCA, UserTierId.STANDARD),
|
|
||||||
).toBe('Gemini Code Assist Standard (Google Workspace)');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return GCA Enterprise for LOGIN_WITH_GOOGLE_GCA with LEGACY tier', () => {
|
|
||||||
expect(
|
|
||||||
getLicenseDisplay(AuthType.LOGIN_WITH_GOOGLE_GCA, UserTierId.LEGACY),
|
|
||||||
).toBe('Gemini Code Assist Enterprise (Google Workspace)');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return generic GCA for LOGIN_WITH_GOOGLE_GCA without tier', () => {
|
|
||||||
expect(getLicenseDisplay(AuthType.LOGIN_WITH_GOOGLE_GCA)).toBe(
|
|
||||||
'Gemini Code Assist (Google Workspace)',
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return generic GCA for LOGIN_WITH_GOOGLE_GCA with unknown tier', () => {
|
|
||||||
expect(
|
|
||||||
getLicenseDisplay(
|
|
||||||
AuthType.LOGIN_WITH_GOOGLE_GCA,
|
|
||||||
'unknown-tier' as UserTierId,
|
|
||||||
),
|
|
||||||
).toBe('Gemini Code Assist (Google Workspace)');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return generic GCA for LOGIN_WITH_GOOGLE_GCA with FREE tier', () => {
|
|
||||||
expect(
|
|
||||||
getLicenseDisplay(AuthType.LOGIN_WITH_GOOGLE_GCA, UserTierId.FREE),
|
|
||||||
).toBe('Gemini Code Assist (Google Workspace)');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('Gemini API Key', () => {
|
|
||||||
it('should return Gemini API Key for USE_GEMINI', () => {
|
|
||||||
expect(getLicenseDisplay(AuthType.USE_GEMINI)).toBe('Gemini API Key');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should ignore userTier for USE_GEMINI', () => {
|
|
||||||
expect(getLicenseDisplay(AuthType.USE_GEMINI, UserTierId.STANDARD)).toBe(
|
|
||||||
'Gemini API Key',
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('Vertex AI', () => {
|
|
||||||
it('should return Vertex AI for USE_VERTEX_AI', () => {
|
|
||||||
expect(getLicenseDisplay(AuthType.USE_VERTEX_AI)).toBe('Vertex AI');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should ignore userTier for USE_VERTEX_AI', () => {
|
|
||||||
expect(getLicenseDisplay(AuthType.USE_VERTEX_AI, UserTierId.LEGACY)).toBe(
|
|
||||||
'Vertex AI',
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('Cloud Shell', () => {
|
|
||||||
it('should return Cloud Shell for CLOUD_SHELL', () => {
|
|
||||||
expect(getLicenseDisplay(AuthType.CLOUD_SHELL)).toBe('Cloud Shell');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should ignore userTier for CLOUD_SHELL', () => {
|
|
||||||
expect(getLicenseDisplay(AuthType.CLOUD_SHELL, UserTierId.STANDARD)).toBe(
|
|
||||||
'Cloud Shell',
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('Unknown auth types', () => {
|
|
||||||
it('should return the auth type as-is for unknown values', () => {
|
|
||||||
expect(getLicenseDisplay('custom-auth-type')).toBe('custom-auth-type');
|
|
||||||
expect(getLicenseDisplay('oauth')).toBe('oauth');
|
|
||||||
expect(getLicenseDisplay('unknown-auth')).toBe('unknown-auth');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should handle undefined gracefully', () => {
|
|
||||||
expect(getLicenseDisplay(undefined as unknown as string)).toBe(undefined);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should handle null gracefully', () => {
|
|
||||||
expect(getLicenseDisplay(null as unknown as string)).toBe(null);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should handle empty string', () => {
|
|
||||||
expect(getLicenseDisplay('')).toBe('');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
/**
|
|
||||||
* @license
|
|
||||||
* Copyright 2025 Google LLC
|
|
||||||
* SPDX-License-Identifier: Apache-2.0
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { AuthType, UserTierId } from '@google/gemini-cli-core';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get human-readable license display text based on auth type and user tier.
|
|
||||||
* @param selectedAuthType - The authentication type selected by the user
|
|
||||||
* @param userTier - Optional user tier information from the server
|
|
||||||
* @returns Human-readable license information
|
|
||||||
*/
|
|
||||||
export function getLicenseDisplay(
|
|
||||||
selectedAuthType: string,
|
|
||||||
userTier?: UserTierId,
|
|
||||||
): string {
|
|
||||||
switch (selectedAuthType) {
|
|
||||||
case AuthType.LOGIN_WITH_GOOGLE:
|
|
||||||
return 'Free Tier (Login with Google)';
|
|
||||||
|
|
||||||
case AuthType.LOGIN_WITH_GOOGLE_GCA:
|
|
||||||
if (userTier === UserTierId.STANDARD) {
|
|
||||||
return 'Gemini Code Assist Standard (Google Workspace)';
|
|
||||||
} else if (userTier === UserTierId.LEGACY) {
|
|
||||||
return 'Gemini Code Assist Enterprise (Google Workspace)';
|
|
||||||
}
|
|
||||||
return 'Gemini Code Assist (Google Workspace)';
|
|
||||||
|
|
||||||
case AuthType.USE_GEMINI:
|
|
||||||
return 'Gemini API Key';
|
|
||||||
|
|
||||||
case AuthType.USE_VERTEX_AI:
|
|
||||||
return 'Vertex AI';
|
|
||||||
|
|
||||||
case AuthType.CLOUD_SHELL:
|
|
||||||
return 'Cloud Shell';
|
|
||||||
|
|
||||||
default:
|
|
||||||
return selectedAuthType;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -20,11 +20,10 @@ export async function createCodeAssistContentGenerator(
|
|||||||
): Promise<ContentGenerator> {
|
): Promise<ContentGenerator> {
|
||||||
if (
|
if (
|
||||||
authType === AuthType.LOGIN_WITH_GOOGLE ||
|
authType === AuthType.LOGIN_WITH_GOOGLE ||
|
||||||
authType === AuthType.LOGIN_WITH_GOOGLE_GCA ||
|
|
||||||
authType === AuthType.CLOUD_SHELL
|
authType === AuthType.CLOUD_SHELL
|
||||||
) {
|
) {
|
||||||
const authClient = await getOauthClient(authType, config);
|
const authClient = await getOauthClient(authType, config);
|
||||||
const userData = await setupUser(authClient, authType);
|
const userData = await setupUser(authClient);
|
||||||
return new CodeAssistServer(
|
return new CodeAssistServer(
|
||||||
authClient,
|
authClient,
|
||||||
userData.projectId,
|
userData.projectId,
|
||||||
|
|||||||
@@ -5,16 +5,11 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||||
import {
|
import { setupUser, ProjectIdRequiredError } from './setup.js';
|
||||||
setupUser,
|
|
||||||
ProjectIdRequiredError,
|
|
||||||
ProjectAccessError,
|
|
||||||
} from './setup.js';
|
|
||||||
import { CodeAssistServer } from '../code_assist/server.js';
|
import { CodeAssistServer } from '../code_assist/server.js';
|
||||||
import type { OAuth2Client } from 'google-auth-library';
|
import type { OAuth2Client } from 'google-auth-library';
|
||||||
import type { GeminiUserTier } from './types.js';
|
import type { GeminiUserTier } from './types.js';
|
||||||
import { UserTierId } from './types.js';
|
import { UserTierId } from './types.js';
|
||||||
import { AuthType } from '../core/contentGenerator.js';
|
|
||||||
|
|
||||||
vi.mock('../code_assist/server.js');
|
vi.mock('../code_assist/server.js');
|
||||||
|
|
||||||
@@ -64,9 +59,8 @@ describe('setupUser for existing user', () => {
|
|||||||
vi.stubEnv('GOOGLE_CLOUD_PROJECT', 'test-project');
|
vi.stubEnv('GOOGLE_CLOUD_PROJECT', 'test-project');
|
||||||
mockLoad.mockResolvedValue({
|
mockLoad.mockResolvedValue({
|
||||||
currentTier: mockPaidTier,
|
currentTier: mockPaidTier,
|
||||||
cloudaicompanionProject: 'test-project',
|
|
||||||
});
|
});
|
||||||
await setupUser({} as OAuth2Client, AuthType.LOGIN_WITH_GOOGLE_GCA);
|
await setupUser({} as OAuth2Client);
|
||||||
expect(CodeAssistServer).toHaveBeenCalledWith(
|
expect(CodeAssistServer).toHaveBeenCalledWith(
|
||||||
{},
|
{},
|
||||||
'test-project',
|
'test-project',
|
||||||
@@ -82,10 +76,7 @@ describe('setupUser for existing user', () => {
|
|||||||
cloudaicompanionProject: 'server-project',
|
cloudaicompanionProject: 'server-project',
|
||||||
currentTier: mockPaidTier,
|
currentTier: mockPaidTier,
|
||||||
});
|
});
|
||||||
const projectId = await setupUser(
|
const projectId = await setupUser({} as OAuth2Client);
|
||||||
{} as OAuth2Client,
|
|
||||||
AuthType.LOGIN_WITH_GOOGLE_GCA,
|
|
||||||
);
|
|
||||||
expect(CodeAssistServer).toHaveBeenCalledWith(
|
expect(CodeAssistServer).toHaveBeenCalledWith(
|
||||||
{},
|
{},
|
||||||
'test-project',
|
'test-project',
|
||||||
@@ -106,9 +97,9 @@ describe('setupUser for existing user', () => {
|
|||||||
throw new ProjectIdRequiredError();
|
throw new ProjectIdRequiredError();
|
||||||
});
|
});
|
||||||
|
|
||||||
await expect(
|
await expect(setupUser({} as OAuth2Client)).rejects.toThrow(
|
||||||
setupUser({} as OAuth2Client, AuthType.LOGIN_WITH_GOOGLE_GCA),
|
ProjectIdRequiredError,
|
||||||
).rejects.toThrow(ProjectIdRequiredError);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -145,10 +136,7 @@ describe('setupUser for new user', () => {
|
|||||||
mockLoad.mockResolvedValue({
|
mockLoad.mockResolvedValue({
|
||||||
allowedTiers: [mockPaidTier],
|
allowedTiers: [mockPaidTier],
|
||||||
});
|
});
|
||||||
const userData = await setupUser(
|
const userData = await setupUser({} as OAuth2Client);
|
||||||
{} as OAuth2Client,
|
|
||||||
AuthType.LOGIN_WITH_GOOGLE_GCA,
|
|
||||||
);
|
|
||||||
expect(CodeAssistServer).toHaveBeenCalledWith(
|
expect(CodeAssistServer).toHaveBeenCalledWith(
|
||||||
{},
|
{},
|
||||||
'test-project',
|
'test-project',
|
||||||
@@ -178,10 +166,7 @@ describe('setupUser for new user', () => {
|
|||||||
mockLoad.mockResolvedValue({
|
mockLoad.mockResolvedValue({
|
||||||
allowedTiers: [mockFreeTier],
|
allowedTiers: [mockFreeTier],
|
||||||
});
|
});
|
||||||
const userData = await setupUser(
|
const userData = await setupUser({} as OAuth2Client);
|
||||||
{} as OAuth2Client,
|
|
||||||
AuthType.LOGIN_WITH_GOOGLE,
|
|
||||||
);
|
|
||||||
expect(CodeAssistServer).toHaveBeenCalledWith(
|
expect(CodeAssistServer).toHaveBeenCalledWith(
|
||||||
{},
|
{},
|
||||||
undefined,
|
undefined,
|
||||||
@@ -205,7 +190,7 @@ describe('setupUser for new user', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should throw ProjectAccessError when LOGIN_WITH_GOOGLE_GCA onboard response has no project ID', async () => {
|
it('should use GOOGLE_CLOUD_PROJECT when onboard response has no project ID', async () => {
|
||||||
vi.stubEnv('GOOGLE_CLOUD_PROJECT', 'test-project');
|
vi.stubEnv('GOOGLE_CLOUD_PROJECT', 'test-project');
|
||||||
mockLoad.mockResolvedValue({
|
mockLoad.mockResolvedValue({
|
||||||
allowedTiers: [mockPaidTier],
|
allowedTiers: [mockPaidTier],
|
||||||
@@ -216,9 +201,11 @@ describe('setupUser for new user', () => {
|
|||||||
cloudaicompanionProject: undefined,
|
cloudaicompanionProject: undefined,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
await expect(
|
const userData = await setupUser({} as OAuth2Client);
|
||||||
setupUser({} as OAuth2Client, AuthType.LOGIN_WITH_GOOGLE_GCA),
|
expect(userData).toEqual({
|
||||||
).rejects.toThrow(ProjectAccessError);
|
projectId: 'test-project',
|
||||||
|
userTier: 'standard-tier',
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should throw ProjectIdRequiredError when no project ID is available', async () => {
|
it('should throw ProjectIdRequiredError when no project ID is available', async () => {
|
||||||
@@ -230,8 +217,8 @@ describe('setupUser for new user', () => {
|
|||||||
done: true,
|
done: true,
|
||||||
response: {},
|
response: {},
|
||||||
});
|
});
|
||||||
await expect(
|
await expect(setupUser({} as OAuth2Client)).rejects.toThrow(
|
||||||
setupUser({} as OAuth2Client, AuthType.LOGIN_WITH_GOOGLE_GCA),
|
ProjectIdRequiredError,
|
||||||
).rejects.toThrow(ProjectIdRequiredError);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ import type {
|
|||||||
import { UserTierId } from './types.js';
|
import { UserTierId } from './types.js';
|
||||||
import { CodeAssistServer } from './server.js';
|
import { CodeAssistServer } from './server.js';
|
||||||
import type { OAuth2Client } from 'google-auth-library';
|
import type { OAuth2Client } from 'google-auth-library';
|
||||||
import { AuthType } from '../core/contentGenerator.js';
|
|
||||||
|
|
||||||
export class ProjectIdRequiredError extends Error {
|
export class ProjectIdRequiredError extends Error {
|
||||||
constructor() {
|
constructor() {
|
||||||
@@ -23,41 +22,6 @@ export class ProjectIdRequiredError extends Error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ProjectAccessError extends Error {
|
|
||||||
constructor(projectId: string, details?: string) {
|
|
||||||
super(
|
|
||||||
`Failed to access GCP project "${projectId}" for Gemini Code Assist.\n` +
|
|
||||||
`${details || ''}\n` +
|
|
||||||
`Please verify:\n` +
|
|
||||||
`1. The project ID is correct\n` +
|
|
||||||
`2. You have the necessary permissions for this project\n` +
|
|
||||||
`3. The Gemini for Cloud API is enabled for this project\n` +
|
|
||||||
`\n` +
|
|
||||||
`To use a different project:\n` +
|
|
||||||
` export GOOGLE_CLOUD_PROJECT=<your-project-id>\n` +
|
|
||||||
`\n` +
|
|
||||||
`To use Free Tier instead, run /auth and select "Login with Google - Free Tier"`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class LicenseMismatchError extends Error {
|
|
||||||
constructor(expected: string, actual: string) {
|
|
||||||
super(
|
|
||||||
`License type mismatch detected.\n` +
|
|
||||||
`You selected: ${expected}\n` +
|
|
||||||
`But the server returned: ${actual}\n` +
|
|
||||||
`\n` +
|
|
||||||
`This may indicate:\n` +
|
|
||||||
`1. The project doesn't have a valid GCA license\n` +
|
|
||||||
`2. You don't have access to the specified project\n` +
|
|
||||||
`3. The project configuration is incorrect\n` +
|
|
||||||
`\n` +
|
|
||||||
`Please verify your project settings or contact your administrator.`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface UserData {
|
export interface UserData {
|
||||||
projectId: string;
|
projectId: string;
|
||||||
userTier: UserTierId;
|
userTier: UserTierId;
|
||||||
@@ -65,20 +29,11 @@ export interface UserData {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @param client OAuth2 client
|
* @param projectId the user's project id, if any
|
||||||
* @param authType the authentication type being used
|
* @returns the user's actual project id
|
||||||
* @returns the user's actual project id and tier
|
|
||||||
*/
|
*/
|
||||||
export async function setupUser(
|
export async function setupUser(client: OAuth2Client): Promise<UserData> {
|
||||||
client: OAuth2Client,
|
const projectId = process.env['GOOGLE_CLOUD_PROJECT'] || undefined;
|
||||||
authType: AuthType,
|
|
||||||
): Promise<UserData> {
|
|
||||||
// Only use GOOGLE_CLOUD_PROJECT for GCA login or Cloud Shell
|
|
||||||
const projectId =
|
|
||||||
authType === AuthType.LOGIN_WITH_GOOGLE_GCA ||
|
|
||||||
authType === AuthType.CLOUD_SHELL
|
|
||||||
? process.env['GOOGLE_CLOUD_PROJECT'] || undefined
|
|
||||||
: undefined;
|
|
||||||
const caServer = new CodeAssistServer(client, projectId, {}, '', undefined);
|
const caServer = new CodeAssistServer(client, projectId, {}, '', undefined);
|
||||||
const coreClientMetadata: ClientMetadata = {
|
const coreClientMetadata: ClientMetadata = {
|
||||||
ideType: 'IDE_UNSPECIFIED',
|
ideType: 'IDE_UNSPECIFIED',
|
||||||
@@ -86,56 +41,22 @@ export async function setupUser(
|
|||||||
pluginType: 'GEMINI',
|
pluginType: 'GEMINI',
|
||||||
};
|
};
|
||||||
|
|
||||||
let loadRes: LoadCodeAssistResponse;
|
const loadRes = await caServer.loadCodeAssist({
|
||||||
try {
|
|
||||||
loadRes = await caServer.loadCodeAssist({
|
|
||||||
cloudaicompanionProject: projectId,
|
cloudaicompanionProject: projectId,
|
||||||
metadata: {
|
metadata: {
|
||||||
...coreClientMetadata,
|
...coreClientMetadata,
|
||||||
duetProject: projectId,
|
duetProject: projectId,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
} catch (error) {
|
|
||||||
// If GCA login failed with a project, throw a clear error
|
|
||||||
if (authType === AuthType.LOGIN_WITH_GOOGLE_GCA && projectId) {
|
|
||||||
throw new ProjectAccessError(
|
|
||||||
projectId,
|
|
||||||
error instanceof Error ? error.message : 'Authentication failed',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (loadRes.currentTier) {
|
if (loadRes.currentTier) {
|
||||||
// Check for license mismatch - GCA selected but Free Tier returned
|
|
||||||
if (
|
|
||||||
authType === AuthType.LOGIN_WITH_GOOGLE_GCA &&
|
|
||||||
loadRes.currentTier.id === UserTierId.FREE
|
|
||||||
) {
|
|
||||||
throw new LicenseMismatchError('Gemini Code Assist (GCA)', 'Free Tier');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!loadRes.cloudaicompanionProject) {
|
if (!loadRes.cloudaicompanionProject) {
|
||||||
if (projectId) {
|
if (projectId) {
|
||||||
// GCA with project but no cloudaicompanionProject means project access issue
|
|
||||||
if (authType === AuthType.LOGIN_WITH_GOOGLE_GCA) {
|
|
||||||
throw new ProjectAccessError(
|
|
||||||
projectId,
|
|
||||||
'The project exists but is not configured for Gemini Code Assist',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return {
|
return {
|
||||||
projectId,
|
projectId,
|
||||||
userTier: loadRes.currentTier.id,
|
userTier: loadRes.currentTier.id,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
// For Free Tier login, don't require project ID
|
|
||||||
if (authType === AuthType.LOGIN_WITH_GOOGLE) {
|
|
||||||
return {
|
|
||||||
projectId: '',
|
|
||||||
userTier: loadRes.currentTier.id,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
throw new ProjectIdRequiredError();
|
throw new ProjectIdRequiredError();
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
@@ -146,16 +67,8 @@ export async function setupUser(
|
|||||||
|
|
||||||
const tier = getOnboardTier(loadRes);
|
const tier = getOnboardTier(loadRes);
|
||||||
|
|
||||||
// Check for license mismatch during onboarding
|
|
||||||
if (
|
|
||||||
authType === AuthType.LOGIN_WITH_GOOGLE_GCA &&
|
|
||||||
tier.id === UserTierId.FREE
|
|
||||||
) {
|
|
||||||
throw new LicenseMismatchError('Gemini Code Assist (GCA)', 'Free Tier');
|
|
||||||
}
|
|
||||||
|
|
||||||
let onboardReq: OnboardUserRequest;
|
let onboardReq: OnboardUserRequest;
|
||||||
if (tier.id === UserTierId.FREE || authType === AuthType.LOGIN_WITH_GOOGLE) {
|
if (tier.id === UserTierId.FREE) {
|
||||||
// The free tier uses a managed google cloud project. Setting a project in the `onboardUser` request causes a `Precondition Failed` error.
|
// The free tier uses a managed google cloud project. Setting a project in the `onboardUser` request causes a `Precondition Failed` error.
|
||||||
onboardReq = {
|
onboardReq = {
|
||||||
tierId: tier.id,
|
tierId: tier.id,
|
||||||
@@ -182,42 +95,18 @@ export async function setupUser(
|
|||||||
|
|
||||||
if (!lroRes.response?.cloudaicompanionProject?.id) {
|
if (!lroRes.response?.cloudaicompanionProject?.id) {
|
||||||
if (projectId) {
|
if (projectId) {
|
||||||
// GCA with project but onboarding didn't return a project
|
|
||||||
if (authType === AuthType.LOGIN_WITH_GOOGLE_GCA) {
|
|
||||||
throw new ProjectAccessError(
|
|
||||||
projectId,
|
|
||||||
'Failed to onboard to Gemini Code Assist with this project',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return {
|
return {
|
||||||
projectId,
|
projectId,
|
||||||
userTier: tier.id,
|
userTier: tier.id,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
// For Free Tier login, don't require project ID
|
|
||||||
if (authType === AuthType.LOGIN_WITH_GOOGLE) {
|
|
||||||
return {
|
|
||||||
projectId: '',
|
|
||||||
userTier: tier.id,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
throw new ProjectIdRequiredError();
|
throw new ProjectIdRequiredError();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Final validation: ensure GCA users don't get Free Tier
|
return {
|
||||||
const finalUserData = {
|
|
||||||
projectId: lroRes.response.cloudaicompanionProject.id,
|
projectId: lroRes.response.cloudaicompanionProject.id,
|
||||||
userTier: tier.id,
|
userTier: tier.id,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (
|
|
||||||
authType === AuthType.LOGIN_WITH_GOOGLE_GCA &&
|
|
||||||
finalUserData.userTier === UserTierId.FREE
|
|
||||||
) {
|
|
||||||
throw new LicenseMismatchError('Gemini Code Assist (GCA)', 'Free Tier');
|
|
||||||
}
|
|
||||||
|
|
||||||
return finalUserData;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function getOnboardTier(res: LoadCodeAssistResponse): GeminiUserTier {
|
function getOnboardTier(res: LoadCodeAssistResponse): GeminiUserTier {
|
||||||
|
|||||||
@@ -44,7 +44,6 @@ export interface ContentGenerator {
|
|||||||
|
|
||||||
export enum AuthType {
|
export enum AuthType {
|
||||||
LOGIN_WITH_GOOGLE = 'oauth-personal',
|
LOGIN_WITH_GOOGLE = 'oauth-personal',
|
||||||
LOGIN_WITH_GOOGLE_GCA = 'oauth-gca',
|
|
||||||
USE_GEMINI = 'gemini-api-key',
|
USE_GEMINI = 'gemini-api-key',
|
||||||
USE_VERTEX_AI = 'vertex-ai',
|
USE_VERTEX_AI = 'vertex-ai',
|
||||||
CLOUD_SHELL = 'cloud-shell',
|
CLOUD_SHELL = 'cloud-shell',
|
||||||
@@ -79,7 +78,6 @@ export function createContentGeneratorConfig(
|
|||||||
// If we are using Google auth or we are in Cloud Shell, there is nothing else to validate for now
|
// If we are using Google auth or we are in Cloud Shell, there is nothing else to validate for now
|
||||||
if (
|
if (
|
||||||
authType === AuthType.LOGIN_WITH_GOOGLE ||
|
authType === AuthType.LOGIN_WITH_GOOGLE ||
|
||||||
authType === AuthType.LOGIN_WITH_GOOGLE_GCA ||
|
|
||||||
authType === AuthType.CLOUD_SHELL
|
authType === AuthType.CLOUD_SHELL
|
||||||
) {
|
) {
|
||||||
return contentGeneratorConfig;
|
return contentGeneratorConfig;
|
||||||
@@ -118,7 +116,6 @@ export async function createContentGenerator(
|
|||||||
|
|
||||||
if (
|
if (
|
||||||
config.authType === AuthType.LOGIN_WITH_GOOGLE ||
|
config.authType === AuthType.LOGIN_WITH_GOOGLE ||
|
||||||
config.authType === AuthType.LOGIN_WITH_GOOGLE_GCA ||
|
|
||||||
config.authType === AuthType.CLOUD_SHELL
|
config.authType === AuthType.CLOUD_SHELL
|
||||||
) {
|
) {
|
||||||
const httpOptions = { headers: baseHeaders };
|
const httpOptions = { headers: baseHeaders };
|
||||||
|
|||||||
Reference in New Issue
Block a user