Sync upstream Gemini-CLI v0.8.2 (#838)

This commit is contained in:
tanzhenxin
2025-10-23 09:27:04 +08:00
committed by GitHub
parent 096fabb5d6
commit eb95c131be
644 changed files with 70389 additions and 23709 deletions

View File

@@ -4,19 +4,12 @@
* SPDX-License-Identifier: Apache-2.0
*/
import {
describe,
it,
expect,
vi,
beforeEach,
afterEach,
type MockedFunction,
} from 'vitest';
import type { NonInteractiveConfig } from './validateNonInterActiveAuth.js';
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { validateNonInteractiveAuth } from './validateNonInterActiveAuth.js';
import { AuthType } from '@qwen-code/qwen-code-core';
import { AuthType, OutputFormat } from '@qwen-code/qwen-code-core';
import type { Config } from '@qwen-code/qwen-code-core';
import * as auth from './config/auth.js';
import { type LoadedSettings } from './config/settings.js';
describe('validateNonInterActiveAuth', () => {
let originalEnvGeminiApiKey: string | undefined;
@@ -25,7 +18,8 @@ describe('validateNonInterActiveAuth', () => {
let originalEnvOpenAiApiKey: string | undefined;
let consoleErrorSpy: ReturnType<typeof vi.spyOn>;
let processExitSpy: ReturnType<typeof vi.spyOn>;
let refreshAuthMock: MockedFunction<(authType: AuthType) => Promise<unknown>>;
let refreshAuthMock: vi.Mock;
let mockSettings: LoadedSettings;
beforeEach(() => {
originalEnvGeminiApiKey = process.env['GEMINI_API_KEY'];
@@ -41,6 +35,25 @@ describe('validateNonInterActiveAuth', () => {
throw new Error(`process.exit(${code}) called`);
});
refreshAuthMock = vi.fn().mockResolvedValue('refreshed');
mockSettings = {
system: { path: '', settings: {} },
systemDefaults: { path: '', settings: {} },
user: { path: '', settings: {} },
workspace: { path: '', settings: {} },
errors: [],
setValue: vi.fn(),
merged: {
security: {
auth: {
enforcedType: undefined,
},
},
},
isTrusted: true,
migratedInMemorScopes: new Set(),
forScope: vi.fn(),
computeMergedSettings: vi.fn(),
} as unknown as LoadedSettings;
});
afterEach(() => {
@@ -68,14 +81,19 @@ describe('validateNonInterActiveAuth', () => {
});
it('exits if no auth type is configured or env vars set', async () => {
const nonInteractiveConfig: NonInteractiveConfig = {
const nonInteractiveConfig = {
refreshAuth: refreshAuthMock,
};
getOutputFormat: vi.fn().mockReturnValue(OutputFormat.TEXT),
getContentGeneratorConfig: vi
.fn()
.mockReturnValue({ authType: undefined }),
} as unknown as Config;
try {
await validateNonInteractiveAuth(
undefined,
undefined,
nonInteractiveConfig,
mockSettings,
);
expect.fail('Should have exited');
} catch (e) {
@@ -89,51 +107,63 @@ describe('validateNonInterActiveAuth', () => {
it('uses LOGIN_WITH_GOOGLE if GOOGLE_GENAI_USE_GCA is set', async () => {
process.env['GOOGLE_GENAI_USE_GCA'] = 'true';
const nonInteractiveConfig: NonInteractiveConfig = {
const nonInteractiveConfig = {
refreshAuth: refreshAuthMock,
};
} as unknown as Config;
await validateNonInteractiveAuth(
undefined,
undefined,
nonInteractiveConfig,
mockSettings,
);
expect(refreshAuthMock).toHaveBeenCalledWith(AuthType.LOGIN_WITH_GOOGLE);
});
it('uses USE_GEMINI if GEMINI_API_KEY is set', async () => {
process.env['GEMINI_API_KEY'] = 'fake-key';
const nonInteractiveConfig: NonInteractiveConfig = {
const nonInteractiveConfig = {
refreshAuth: refreshAuthMock,
};
} as unknown as Config;
await validateNonInteractiveAuth(
undefined,
undefined,
nonInteractiveConfig,
mockSettings,
);
expect(refreshAuthMock).toHaveBeenCalledWith(AuthType.USE_GEMINI);
});
it('uses USE_OPENAI if OPENAI_API_KEY is set', async () => {
process.env['OPENAI_API_KEY'] = 'fake-openai-key';
const nonInteractiveConfig: NonInteractiveConfig = {
const nonInteractiveConfig = {
refreshAuth: refreshAuthMock,
};
getOutputFormat: vi.fn().mockReturnValue(OutputFormat.TEXT),
getContentGeneratorConfig: vi
.fn()
.mockReturnValue({ authType: undefined }),
} as unknown as Config;
await validateNonInteractiveAuth(
undefined,
undefined,
nonInteractiveConfig,
mockSettings,
);
expect(refreshAuthMock).toHaveBeenCalledWith(AuthType.USE_OPENAI);
});
it('uses configured QWEN_OAUTH if provided', async () => {
const nonInteractiveConfig: NonInteractiveConfig = {
const nonInteractiveConfig = {
refreshAuth: refreshAuthMock,
};
getOutputFormat: vi.fn().mockReturnValue(OutputFormat.TEXT),
getContentGeneratorConfig: vi
.fn()
.mockReturnValue({ authType: undefined }),
} as unknown as Config;
await validateNonInteractiveAuth(
AuthType.QWEN_OAUTH,
undefined,
nonInteractiveConfig,
mockSettings,
);
expect(refreshAuthMock).toHaveBeenCalledWith(AuthType.QWEN_OAUTH);
});
@@ -142,13 +172,14 @@ describe('validateNonInterActiveAuth', () => {
process.env['GOOGLE_GENAI_USE_VERTEXAI'] = 'true';
process.env['GOOGLE_CLOUD_PROJECT'] = 'test-project';
process.env['GOOGLE_CLOUD_LOCATION'] = 'us-central1';
const nonInteractiveConfig: NonInteractiveConfig = {
const nonInteractiveConfig = {
refreshAuth: refreshAuthMock,
};
} as unknown as Config;
await validateNonInteractiveAuth(
undefined,
undefined,
nonInteractiveConfig,
mockSettings,
);
expect(refreshAuthMock).toHaveBeenCalledWith(AuthType.USE_VERTEX_AI);
});
@@ -156,13 +187,14 @@ describe('validateNonInterActiveAuth', () => {
it('uses USE_VERTEX_AI if GOOGLE_GENAI_USE_VERTEXAI is true and GOOGLE_API_KEY is set', async () => {
process.env['GOOGLE_GENAI_USE_VERTEXAI'] = 'true';
process.env['GOOGLE_API_KEY'] = 'vertex-api-key';
const nonInteractiveConfig: NonInteractiveConfig = {
const nonInteractiveConfig = {
refreshAuth: refreshAuthMock,
};
} as unknown as Config;
await validateNonInteractiveAuth(
undefined,
undefined,
nonInteractiveConfig,
mockSettings,
);
expect(refreshAuthMock).toHaveBeenCalledWith(AuthType.USE_VERTEX_AI);
});
@@ -173,13 +205,14 @@ describe('validateNonInterActiveAuth', () => {
process.env['GOOGLE_GENAI_USE_VERTEXAI'] = 'true';
process.env['GOOGLE_CLOUD_PROJECT'] = 'test-project';
process.env['GOOGLE_CLOUD_LOCATION'] = 'us-central1';
const nonInteractiveConfig: NonInteractiveConfig = {
const nonInteractiveConfig = {
refreshAuth: refreshAuthMock,
};
} as unknown as Config;
await validateNonInteractiveAuth(
undefined,
undefined,
nonInteractiveConfig,
mockSettings,
);
expect(refreshAuthMock).toHaveBeenCalledWith(AuthType.LOGIN_WITH_GOOGLE);
});
@@ -189,13 +222,14 @@ describe('validateNonInterActiveAuth', () => {
process.env['GOOGLE_GENAI_USE_VERTEXAI'] = 'true';
process.env['GOOGLE_CLOUD_PROJECT'] = 'test-project';
process.env['GOOGLE_CLOUD_LOCATION'] = 'us-central1';
const nonInteractiveConfig: NonInteractiveConfig = {
const nonInteractiveConfig = {
refreshAuth: refreshAuthMock,
};
} as unknown as Config;
await validateNonInteractiveAuth(
undefined,
undefined,
nonInteractiveConfig,
mockSettings,
);
expect(refreshAuthMock).toHaveBeenCalledWith(AuthType.USE_VERTEX_AI);
});
@@ -205,13 +239,14 @@ describe('validateNonInterActiveAuth', () => {
process.env['GEMINI_API_KEY'] = 'fake-key';
process.env['GOOGLE_CLOUD_PROJECT'] = 'test-project';
process.env['GOOGLE_CLOUD_LOCATION'] = 'us-central1';
const nonInteractiveConfig: NonInteractiveConfig = {
const nonInteractiveConfig = {
refreshAuth: refreshAuthMock,
};
} as unknown as Config;
await validateNonInteractiveAuth(
undefined,
undefined,
nonInteractiveConfig,
mockSettings,
);
expect(refreshAuthMock).toHaveBeenCalledWith(AuthType.USE_GEMINI);
});
@@ -219,13 +254,14 @@ describe('validateNonInterActiveAuth', () => {
it('uses configuredAuthType if provided', async () => {
// Set required env var for USE_GEMINI
process.env['GEMINI_API_KEY'] = 'fake-key';
const nonInteractiveConfig: NonInteractiveConfig = {
const nonInteractiveConfig = {
refreshAuth: refreshAuthMock,
};
} as unknown as Config;
await validateNonInteractiveAuth(
AuthType.USE_GEMINI,
undefined,
nonInteractiveConfig,
mockSettings,
);
expect(refreshAuthMock).toHaveBeenCalledWith(AuthType.USE_GEMINI);
});
@@ -233,14 +269,19 @@ describe('validateNonInterActiveAuth', () => {
it('exits if validateAuthMethod returns error', async () => {
// Mock validateAuthMethod to return error
vi.spyOn(auth, 'validateAuthMethod').mockReturnValue('Auth error!');
const nonInteractiveConfig: NonInteractiveConfig = {
const nonInteractiveConfig = {
refreshAuth: refreshAuthMock,
};
getOutputFormat: vi.fn().mockReturnValue(OutputFormat.TEXT),
getContentGeneratorConfig: vi
.fn()
.mockReturnValue({ authType: undefined }),
} as unknown as Config;
try {
await validateNonInteractiveAuth(
AuthType.USE_GEMINI,
undefined,
nonInteractiveConfig,
mockSettings,
);
expect.fail('Should have exited');
} catch (e) {
@@ -255,9 +296,9 @@ describe('validateNonInterActiveAuth', () => {
const validateAuthMethodSpy = vi
.spyOn(auth, 'validateAuthMethod')
.mockReturnValue('Auth error!');
const nonInteractiveConfig: NonInteractiveConfig = {
const nonInteractiveConfig = {
refreshAuth: refreshAuthMock,
};
} as unknown as Config;
// Even with an invalid auth type, it should not exit
// because validation is skipped.
@@ -265,6 +306,7 @@ describe('validateNonInterActiveAuth', () => {
'invalid-auth-type' as AuthType,
true, // useExternalAuth = true
nonInteractiveConfig,
mockSettings,
);
expect(validateAuthMethodSpy).not.toHaveBeenCalled();
@@ -273,4 +315,152 @@ describe('validateNonInterActiveAuth', () => {
// We still expect refreshAuth to be called with the (invalid) type
expect(refreshAuthMock).toHaveBeenCalledWith('invalid-auth-type');
});
it('uses enforcedAuthType if provided', async () => {
mockSettings.merged.security!.auth!.enforcedType = AuthType.USE_GEMINI;
mockSettings.merged.security!.auth!.selectedType = AuthType.USE_GEMINI;
// Set required env var for USE_GEMINI to ensure enforcedAuthType takes precedence
process.env['GEMINI_API_KEY'] = 'fake-key';
const nonInteractiveConfig = {
refreshAuth: refreshAuthMock,
} as unknown as Config;
await validateNonInteractiveAuth(
AuthType.USE_GEMINI,
undefined,
nonInteractiveConfig,
mockSettings,
);
expect(refreshAuthMock).toHaveBeenCalledWith(AuthType.USE_GEMINI);
});
it('exits if currentAuthType does not match enforcedAuthType', async () => {
mockSettings.merged.security!.auth!.enforcedType =
AuthType.LOGIN_WITH_GOOGLE;
process.env['GOOGLE_GENAI_USE_VERTEXAI'] = 'true';
const nonInteractiveConfig = {
refreshAuth: refreshAuthMock,
getOutputFormat: vi.fn().mockReturnValue(OutputFormat.TEXT),
getContentGeneratorConfig: vi
.fn()
.mockReturnValue({ authType: undefined }),
} as unknown as Config;
try {
await validateNonInteractiveAuth(
AuthType.USE_GEMINI,
undefined,
nonInteractiveConfig,
mockSettings,
);
expect.fail('Should have exited');
} catch (e) {
expect((e as Error).message).toContain('process.exit(1) called');
}
expect(consoleErrorSpy).toHaveBeenCalledWith(
'The configured auth type is oauth-personal, but the current auth type is vertex-ai. Please re-authenticate with the correct type.',
);
expect(processExitSpy).toHaveBeenCalledWith(1);
});
describe('JSON output mode', () => {
it('prints JSON error when no auth is configured and exits with code 1', async () => {
const nonInteractiveConfig = {
refreshAuth: refreshAuthMock,
getOutputFormat: vi.fn().mockReturnValue(OutputFormat.JSON),
getContentGeneratorConfig: vi
.fn()
.mockReturnValue({ authType: undefined }),
} as unknown as Config;
let thrown: Error | undefined;
try {
await validateNonInteractiveAuth(
undefined,
undefined,
nonInteractiveConfig,
mockSettings,
);
} catch (e) {
thrown = e as Error;
}
expect(thrown?.message).toBe('process.exit(1) called');
const errorArg = consoleErrorSpy.mock.calls[0]?.[0] as string;
const payload = JSON.parse(errorArg);
expect(payload.error.type).toBe('Error');
expect(payload.error.code).toBe(1);
expect(payload.error.message).toContain(
'Please set an Auth method in your',
);
});
it('prints JSON error when enforced auth mismatches current auth and exits with code 1', async () => {
mockSettings.merged.security!.auth!.enforcedType = AuthType.USE_GEMINI;
process.env['GOOGLE_GENAI_USE_GCA'] = 'true';
const nonInteractiveConfig = {
refreshAuth: refreshAuthMock,
getOutputFormat: vi.fn().mockReturnValue(OutputFormat.JSON),
getContentGeneratorConfig: vi
.fn()
.mockReturnValue({ authType: undefined }),
} as unknown as Config;
let thrown: Error | undefined;
try {
await validateNonInteractiveAuth(
undefined,
undefined,
nonInteractiveConfig,
mockSettings,
);
} catch (e) {
thrown = e as Error;
}
expect(thrown?.message).toBe('process.exit(1) called');
{
const errorArg = consoleErrorSpy.mock.calls[0]?.[0] as string;
const payload = JSON.parse(errorArg);
expect(payload.error.type).toBe('Error');
expect(payload.error.code).toBe(1);
expect(payload.error.message).toContain(
'The configured auth type is gemini-api-key, but the current auth type is oauth-personal.',
);
}
});
it('prints JSON error when validateAuthMethod fails and exits with code 1', async () => {
vi.spyOn(auth, 'validateAuthMethod').mockReturnValue('Auth error!');
process.env['GEMINI_API_KEY'] = 'fake-key';
const nonInteractiveConfig = {
refreshAuth: refreshAuthMock,
getOutputFormat: vi.fn().mockReturnValue(OutputFormat.JSON),
getContentGeneratorConfig: vi
.fn()
.mockReturnValue({ authType: undefined }),
} as unknown as Config;
let thrown: Error | undefined;
try {
await validateNonInteractiveAuth(
AuthType.USE_GEMINI,
undefined,
nonInteractiveConfig,
mockSettings,
);
} catch (e) {
thrown = e as Error;
}
expect(thrown?.message).toBe('process.exit(1) called');
{
const errorArg = consoleErrorSpy.mock.calls[0]?.[0] as string;
const payload = JSON.parse(errorArg);
expect(payload.error.type).toBe('Error');
expect(payload.error.code).toBe(1);
expect(payload.error.message).toBe('Auth error!');
}
});
});
});