mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-07-11 02:20:48 -07:00
First take at mocking out gemini cli responses in integration tests (#11156)
This commit is contained in:
@@ -283,6 +283,7 @@ export interface ConfigParameters {
|
||||
continueOnFailedApiCall?: boolean;
|
||||
retryFetchErrors?: boolean;
|
||||
enableShellOutputEfficiency?: boolean;
|
||||
fakeResponses?: string;
|
||||
ptyInfo?: string;
|
||||
disableYoloMode?: boolean;
|
||||
}
|
||||
@@ -381,6 +382,7 @@ export class Config {
|
||||
private readonly continueOnFailedApiCall: boolean;
|
||||
private readonly retryFetchErrors: boolean;
|
||||
private readonly enableShellOutputEfficiency: boolean;
|
||||
readonly fakeResponses?: string;
|
||||
private readonly disableYoloMode: boolean;
|
||||
|
||||
constructor(params: ConfigParameters) {
|
||||
@@ -489,6 +491,7 @@ export class Config {
|
||||
params.enableShellOutputEfficiency ?? true;
|
||||
this.extensionManagement = params.extensionManagement ?? true;
|
||||
this.storage = new Storage(this.targetDir);
|
||||
this.fakeResponses = params.fakeResponses;
|
||||
this.enablePromptCompletion = params.enablePromptCompletion ?? false;
|
||||
this.fileExclusions = new FileExclusions(this);
|
||||
this.eventEmitter = params.eventEmitter;
|
||||
|
||||
@@ -15,13 +15,36 @@ import { createCodeAssistContentGenerator } from '../code_assist/codeAssist.js';
|
||||
import { GoogleGenAI } from '@google/genai';
|
||||
import type { Config } from '../config/config.js';
|
||||
import { LoggingContentGenerator } from './loggingContentGenerator.js';
|
||||
import { FakeContentGenerator } from './fakeContentGenerator.js';
|
||||
|
||||
vi.mock('../code_assist/codeAssist.js');
|
||||
vi.mock('@google/genai');
|
||||
vi.mock('./fakeContentGenerator.js');
|
||||
|
||||
const mockConfig = {} as unknown as Config;
|
||||
|
||||
describe('createContentGenerator', () => {
|
||||
it('should create a FakeContentGenerator', async () => {
|
||||
const mockGenerator = {} as unknown as ContentGenerator;
|
||||
vi.mocked(FakeContentGenerator.fromFile).mockResolvedValue(
|
||||
mockGenerator as never,
|
||||
);
|
||||
const fakeResponsesFile = 'fake/responses.yaml';
|
||||
const mockConfigWithFake = {
|
||||
fakeResponses: fakeResponsesFile,
|
||||
} as unknown as Config;
|
||||
const generator = await createContentGenerator(
|
||||
{
|
||||
authType: AuthType.USE_GEMINI,
|
||||
},
|
||||
mockConfigWithFake,
|
||||
);
|
||||
expect(FakeContentGenerator.fromFile).toHaveBeenCalledWith(
|
||||
fakeResponsesFile,
|
||||
);
|
||||
expect(generator).toEqual(mockGenerator);
|
||||
});
|
||||
|
||||
it('should create a CodeAssistContentGenerator', async () => {
|
||||
const mockGenerator = {} as unknown as ContentGenerator;
|
||||
vi.mocked(createCodeAssistContentGenerator).mockResolvedValue(
|
||||
|
||||
@@ -19,6 +19,7 @@ import type { Config } from '../config/config.js';
|
||||
import type { UserTierId } from '../code_assist/types.js';
|
||||
import { LoggingContentGenerator } from './loggingContentGenerator.js';
|
||||
import { InstallationManager } from '../utils/installationManager.js';
|
||||
import { FakeContentGenerator } from './fakeContentGenerator.js';
|
||||
|
||||
/**
|
||||
* Interface abstracting the core functionalities for generating content and counting tokens.
|
||||
@@ -105,6 +106,10 @@ export async function createContentGenerator(
|
||||
gcConfig: Config,
|
||||
sessionId?: string,
|
||||
): Promise<ContentGenerator> {
|
||||
if (gcConfig.fakeResponses) {
|
||||
return FakeContentGenerator.fromFile(gcConfig.fakeResponses);
|
||||
}
|
||||
|
||||
const version = process.env['CLI_VERSION'] || process.version;
|
||||
const userAgent = `GeminiCLI/${version} (${process.platform}; ${process.arch})`;
|
||||
const baseHeaders: Record<string, string> = {
|
||||
|
||||
@@ -0,0 +1,205 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { FakeContentGenerator } from './fakeContentGenerator.js';
|
||||
import { promises } from 'node:fs';
|
||||
import type { FakeResponses } from './fakeContentGenerator.js';
|
||||
import type {
|
||||
GenerateContentResponse,
|
||||
CountTokensResponse,
|
||||
EmbedContentResponse,
|
||||
GenerateContentParameters,
|
||||
CountTokensParameters,
|
||||
EmbedContentParameters,
|
||||
} from '@google/genai';
|
||||
|
||||
vi.mock('node:fs', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('node:fs')>();
|
||||
return {
|
||||
...actual,
|
||||
promises: {
|
||||
...actual.promises,
|
||||
readFile: vi.fn(),
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
const mockReadFile = vi.mocked(promises.readFile);
|
||||
|
||||
describe('FakeContentGenerator', () => {
|
||||
const fakeResponses: FakeResponses = {
|
||||
generateContent: [
|
||||
{
|
||||
candidates: [
|
||||
{ content: { parts: [{ text: 'response1' }], role: 'model' } },
|
||||
],
|
||||
},
|
||||
] as GenerateContentResponse[],
|
||||
generateContentStream: [
|
||||
[
|
||||
{
|
||||
candidates: [
|
||||
{ content: { parts: [{ text: 'chunk1' }], role: 'model' } },
|
||||
],
|
||||
},
|
||||
{
|
||||
candidates: [
|
||||
{ content: { parts: [{ text: 'chunk2' }], role: 'model' } },
|
||||
],
|
||||
},
|
||||
],
|
||||
] as GenerateContentResponse[][],
|
||||
countTokens: [{ totalTokens: 10 }] as CountTokensResponse[],
|
||||
embedContent: [
|
||||
{ embeddings: [{ values: [1, 2, 3] }] },
|
||||
] as EmbedContentResponse[],
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
vi.resetAllMocks();
|
||||
});
|
||||
|
||||
it('should return responses for generateContent', async () => {
|
||||
const generator = new FakeContentGenerator(fakeResponses);
|
||||
const response = await generator.generateContent(
|
||||
{} as GenerateContentParameters,
|
||||
'id',
|
||||
);
|
||||
expect(response).toEqual(fakeResponses.generateContent[0]);
|
||||
});
|
||||
|
||||
it('should throw error when no more generateContent responses', async () => {
|
||||
const generator = new FakeContentGenerator({
|
||||
...fakeResponses,
|
||||
generateContent: [],
|
||||
});
|
||||
await expect(
|
||||
generator.generateContent({} as GenerateContentParameters, 'id'),
|
||||
).rejects.toThrowError('No more mock responses for generateContent');
|
||||
});
|
||||
|
||||
it('should return responses for generateContentStream', async () => {
|
||||
const generator = new FakeContentGenerator(fakeResponses);
|
||||
const stream = await generator.generateContentStream(
|
||||
{} as GenerateContentParameters,
|
||||
'id',
|
||||
);
|
||||
const responses = [];
|
||||
for await (const response of stream) {
|
||||
responses.push(response);
|
||||
}
|
||||
expect(responses).toEqual(fakeResponses.generateContentStream[0]);
|
||||
});
|
||||
|
||||
it('should throw error when no more generateContentStream responses', async () => {
|
||||
const generator = new FakeContentGenerator({
|
||||
...fakeResponses,
|
||||
generateContentStream: [],
|
||||
});
|
||||
await expect(
|
||||
generator.generateContentStream({} as GenerateContentParameters, 'id'),
|
||||
).rejects.toThrow('No more mock responses for generateContentStream');
|
||||
});
|
||||
|
||||
it('should return responses for countTokens', async () => {
|
||||
const generator = new FakeContentGenerator(fakeResponses);
|
||||
const response = await generator.countTokens({} as CountTokensParameters);
|
||||
expect(response).toEqual(fakeResponses.countTokens[0]);
|
||||
});
|
||||
|
||||
it('should throw error when no more countTokens responses', async () => {
|
||||
const generator = new FakeContentGenerator({
|
||||
...fakeResponses,
|
||||
countTokens: [],
|
||||
});
|
||||
await expect(
|
||||
generator.countTokens({} as CountTokensParameters),
|
||||
).rejects.toThrowError('No more mock responses for countTokens');
|
||||
});
|
||||
|
||||
it('should return responses for embedContent', async () => {
|
||||
const generator = new FakeContentGenerator(fakeResponses);
|
||||
const response = await generator.embedContent({} as EmbedContentParameters);
|
||||
expect(response).toEqual(fakeResponses.embedContent[0]);
|
||||
});
|
||||
|
||||
it('should throw error when no more embedContent responses', async () => {
|
||||
const generator = new FakeContentGenerator({
|
||||
...fakeResponses,
|
||||
embedContent: [],
|
||||
});
|
||||
await expect(
|
||||
generator.embedContent({} as EmbedContentParameters),
|
||||
).rejects.toThrowError('No more mock responses for embedContent');
|
||||
});
|
||||
|
||||
it('should handle multiple calls and exhaust responses', async () => {
|
||||
const generator = new FakeContentGenerator(fakeResponses);
|
||||
await generator.generateContent({} as GenerateContentParameters, 'id');
|
||||
await expect(
|
||||
generator.generateContent({} as GenerateContentParameters, 'id'),
|
||||
).rejects.toThrow();
|
||||
});
|
||||
|
||||
describe('fromFile', () => {
|
||||
it('should create a generator from a file', async () => {
|
||||
const fileContent = JSON.stringify(fakeResponses);
|
||||
mockReadFile.mockResolvedValue(fileContent);
|
||||
|
||||
const generator = await FakeContentGenerator.fromFile('fake-path.json');
|
||||
const response = await generator.generateContent(
|
||||
{} as GenerateContentParameters,
|
||||
'id',
|
||||
);
|
||||
expect(response).toEqual(fakeResponses.generateContent[0]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('constructor with partial responses', () => {
|
||||
it('should handle missing generateContent', async () => {
|
||||
const responses = { ...fakeResponses, generateContent: undefined };
|
||||
const generator = new FakeContentGenerator(
|
||||
responses as unknown as FakeResponses,
|
||||
);
|
||||
await expect(
|
||||
generator.generateContent({} as GenerateContentParameters, 'id'),
|
||||
).rejects.toThrowError('No more mock responses for generateContent');
|
||||
});
|
||||
|
||||
it('should handle missing generateContentStream', async () => {
|
||||
const responses = { ...fakeResponses, generateContentStream: undefined };
|
||||
const generator = new FakeContentGenerator(
|
||||
responses as unknown as FakeResponses,
|
||||
);
|
||||
await expect(
|
||||
generator.generateContentStream({} as GenerateContentParameters, 'id'),
|
||||
).rejects.toThrowError(
|
||||
'No more mock responses for generateContentStream',
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle missing countTokens', async () => {
|
||||
const responses = { ...fakeResponses, countTokens: undefined };
|
||||
const generator = new FakeContentGenerator(
|
||||
responses as unknown as FakeResponses,
|
||||
);
|
||||
await expect(
|
||||
generator.countTokens({} as CountTokensParameters),
|
||||
).rejects.toThrowError('No more mock responses for countTokens');
|
||||
});
|
||||
|
||||
it('should handle missing embedContent', async () => {
|
||||
const responses = { ...fakeResponses, embedContent: undefined };
|
||||
const generator = new FakeContentGenerator(
|
||||
responses as unknown as FakeResponses,
|
||||
);
|
||||
await expect(
|
||||
generator.embedContent({} as EmbedContentParameters),
|
||||
).rejects.toThrowError('No more mock responses for embedContent');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,101 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import type {
|
||||
CountTokensResponse,
|
||||
GenerateContentResponse,
|
||||
GenerateContentParameters,
|
||||
CountTokensParameters,
|
||||
EmbedContentResponse,
|
||||
EmbedContentParameters,
|
||||
} from '@google/genai';
|
||||
import { promises } from 'node:fs';
|
||||
import type { ContentGenerator } from './contentGenerator.js';
|
||||
import type { UserTierId } from '../code_assist/types.js';
|
||||
import { safeJsonStringify } from '../utils/safeJsonStringify.js';
|
||||
|
||||
export type FakeResponses = {
|
||||
generateContent: GenerateContentResponse[];
|
||||
generateContentStream: GenerateContentResponse[][];
|
||||
countTokens: CountTokensResponse[];
|
||||
embedContent: EmbedContentResponse[];
|
||||
};
|
||||
|
||||
// A ContentGenerator that responds with canned responses.
|
||||
//
|
||||
// Typically these would come from a file, provided by the `--fake-responses`
|
||||
// CLI argument.
|
||||
export class FakeContentGenerator implements ContentGenerator {
|
||||
private responses: FakeResponses;
|
||||
private callCounters = {
|
||||
generateContent: 0,
|
||||
generateContentStream: 0,
|
||||
countTokens: 0,
|
||||
embedContent: 0,
|
||||
};
|
||||
userTier?: UserTierId;
|
||||
|
||||
constructor(responses: FakeResponses) {
|
||||
this.responses = {
|
||||
generateContent: responses.generateContent ?? [],
|
||||
generateContentStream: responses.generateContentStream ?? [],
|
||||
countTokens: responses.countTokens ?? [],
|
||||
embedContent: responses.embedContent ?? [],
|
||||
};
|
||||
}
|
||||
|
||||
static async fromFile(filePath: string): Promise<FakeContentGenerator> {
|
||||
const fileContent = await promises.readFile(filePath, 'utf-8');
|
||||
const responses = JSON.parse(fileContent) as FakeResponses;
|
||||
return new FakeContentGenerator(responses);
|
||||
}
|
||||
|
||||
private getNextResponse<K extends keyof FakeResponses>(
|
||||
method: K,
|
||||
request: unknown,
|
||||
): FakeResponses[K][number] {
|
||||
const response = this.responses[method][this.callCounters[method]++];
|
||||
if (!response) {
|
||||
throw new Error(
|
||||
`No more mock responses for ${method}, got request:\n` +
|
||||
safeJsonStringify(request),
|
||||
);
|
||||
}
|
||||
return response;
|
||||
}
|
||||
|
||||
async generateContent(
|
||||
_request: GenerateContentParameters,
|
||||
_userPromptId: string,
|
||||
): Promise<GenerateContentResponse> {
|
||||
return this.getNextResponse('generateContent', _request);
|
||||
}
|
||||
|
||||
async generateContentStream(
|
||||
_request: GenerateContentParameters,
|
||||
_userPromptId: string,
|
||||
): Promise<AsyncGenerator<GenerateContentResponse>> {
|
||||
const responses = this.getNextResponse('generateContentStream', _request);
|
||||
async function* stream() {
|
||||
for (const response of responses) {
|
||||
yield response;
|
||||
}
|
||||
}
|
||||
return stream();
|
||||
}
|
||||
|
||||
async countTokens(
|
||||
_request: CountTokensParameters,
|
||||
): Promise<CountTokensResponse> {
|
||||
return this.getNextResponse('countTokens', _request);
|
||||
}
|
||||
|
||||
async embedContent(
|
||||
_request: EmbedContentParameters,
|
||||
): Promise<EmbedContentResponse> {
|
||||
return this.getNextResponse('embedContent', _request);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user