Skip to content
Merged
42 changes: 42 additions & 0 deletions packages/cli/src/nonInteractiveCli.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,20 @@ vi.mock('./ui/hooks/atCommandProcessor.js');
vi.mock('@google/gemini-cli-core', async (importOriginal) => {
const original =
await importOriginal<typeof import('@google/gemini-cli-core')>();

class MockChatRecordingService {
initialize = vi.fn();
recordMessage = vi.fn();
recordMessageTokens = vi.fn();
recordToolCalls = vi.fn();
}

return {
...original,
executeToolCall: vi.fn(),
shutdownTelemetry: vi.fn(),
isTelemetrySdkInitialized: vi.fn().mockReturnValue(true),
ChatRecordingService: MockChatRecordingService,
};
});

Expand All @@ -41,6 +50,7 @@ describe('runNonInteractive', () => {
let processStdoutSpy: vi.SpyInstance;
let mockGeminiClient: {
sendMessageStream: vi.Mock;
getChatRecordingService: vi.Mock;
};

beforeEach(async () => {
Expand All @@ -59,13 +69,24 @@ describe('runNonInteractive', () => {

mockGeminiClient = {
sendMessageStream: vi.fn(),
getChatRecordingService: vi.fn(() => ({
initialize: vi.fn(),
recordMessage: vi.fn(),
recordMessageTokens: vi.fn(),
recordToolCalls: vi.fn(),
})),
};

mockConfig = {
initialize: vi.fn().mockResolvedValue(undefined),
getGeminiClient: vi.fn().mockReturnValue(mockGeminiClient),
getToolRegistry: vi.fn().mockReturnValue(mockToolRegistry),
getMaxSessionTurns: vi.fn().mockReturnValue(10),
getSessionId: vi.fn().mockReturnValue('test-session-id'),
getProjectRoot: vi.fn().mockReturnValue('/test/project'),
storage: {
getProjectTempDir: vi.fn().mockReturnValue('/test/project/.gemini/tmp'),
},
getIdeMode: vi.fn().mockReturnValue(false),
getFullContext: vi.fn().mockReturnValue(false),
getContentGeneratorConfig: vi.fn().mockReturnValue({}),
Expand Down Expand Up @@ -97,6 +118,10 @@ describe('runNonInteractive', () => {
const events: ServerGeminiStreamEvent[] = [
{ type: GeminiEventType.Content, value: 'Hello' },
{ type: GeminiEventType.Content, value: ' World' },
{
type: GeminiEventType.Finished,
value: { reason: undefined, usageMetadata: { totalTokenCount: 10 } },
},
];
mockGeminiClient.sendMessageStream.mockReturnValue(
createStreamFromEvents(events),
Expand Down Expand Up @@ -132,6 +157,10 @@ describe('runNonInteractive', () => {
const firstCallEvents: ServerGeminiStreamEvent[] = [toolCallEvent];
const secondCallEvents: ServerGeminiStreamEvent[] = [
{ type: GeminiEventType.Content, value: 'Final answer' },
{
type: GeminiEventType.Finished,
value: { reason: undefined, usageMetadata: { totalTokenCount: 10 } },
},
];

mockGeminiClient.sendMessageStream
Expand Down Expand Up @@ -187,6 +216,10 @@ describe('runNonInteractive', () => {
type: GeminiEventType.Content,
value: 'Sorry, let me try again.',
},
{
type: GeminiEventType.Finished,
value: { reason: undefined, usageMetadata: { totalTokenCount: 10 } },
},
];
mockGeminiClient.sendMessageStream
.mockReturnValueOnce(createStreamFromEvents([toolCallEvent]))
Expand Down Expand Up @@ -242,12 +275,17 @@ describe('runNonInteractive', () => {
mockCoreExecuteToolCall.mockResolvedValue({
error: new Error('Tool "nonexistentTool" not found in registry.'),
resultDisplay: 'Tool "nonexistentTool" not found in registry.',
responseParts: [],
});
const finalResponse: ServerGeminiStreamEvent[] = [
{
type: GeminiEventType.Content,
value: "Sorry, I can't find that tool.",
},
{
type: GeminiEventType.Finished,
value: { reason: undefined, usageMetadata: { totalTokenCount: 10 } },
},
];

mockGeminiClient.sendMessageStream
Expand Down Expand Up @@ -304,6 +342,10 @@ describe('runNonInteractive', () => {
// Mock a simple stream response from the Gemini client
const events: ServerGeminiStreamEvent[] = [
{ type: GeminiEventType.Content, value: 'Summary complete.' },
{
type: GeminiEventType.Finished,
value: { reason: undefined, usageMetadata: { totalTokenCount: 10 } },
},
];
mockGeminiClient.sendMessageStream.mockReturnValue(
createStreamFromEvents(events),
Expand Down
15 changes: 15 additions & 0 deletions packages/cli/src/ui/App.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,22 @@ vi.mock('@google/gemini-cli-core', async (importOriginal) => {
getProjectRoot: vi.fn(() => opts.targetDir),
getEnablePromptCompletion: vi.fn(() => false),
getGeminiClient: vi.fn(() => ({
isInitialized: vi.fn(() => true),
getUserTier: vi.fn(),
getChatRecordingService: vi.fn(() => ({
initialize: vi.fn(),
recordMessage: vi.fn(),
recordMessageTokens: vi.fn(),
recordToolCalls: vi.fn(),
})),
getChat: vi.fn(() => ({
getChatRecordingService: vi.fn(() => ({
initialize: vi.fn(),
recordMessage: vi.fn(),
recordMessageTokens: vi.fn(),
recordToolCalls: vi.fn(),
})),
})),
})),
getCheckpointingEnabled: vi.fn(() => opts.checkpointing ?? true),
getAllGeminiMdFilenames: vi.fn(() => ['GEMINI.md']),
Expand Down
38 changes: 32 additions & 6 deletions packages/cli/src/ui/hooks/useGeminiStream.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,14 @@ const MockedGeminiClientClass = vi.hoisted(() =>
this.startChat = mockStartChat;
this.sendMessageStream = mockSendMessageStream;
this.addHistory = vi.fn();
this.getChatRecordingService = vi.fn().mockReturnValue({
recordThought: vi.fn(),
initialize: vi.fn(),
recordMessage: vi.fn(),
recordMessageTokens: vi.fn(),
recordToolCalls: vi.fn(),
getConversationFile: vi.fn(),
});
}),
);

Expand Down Expand Up @@ -1275,7 +1283,10 @@ describe('useGeminiStream', () => {
type: ServerGeminiEventType.Content,
value: 'This is a truncated response...',
};
yield { type: ServerGeminiEventType.Finished, value: 'MAX_TOKENS' };
yield {
type: ServerGeminiEventType.Finished,
value: { reason: 'MAX_TOKENS', usageMetadata: undefined },
};
})(),
);

Expand Down Expand Up @@ -1324,7 +1335,10 @@ describe('useGeminiStream', () => {
type: ServerGeminiEventType.Content,
value: 'Complete response',
};
yield { type: ServerGeminiEventType.Finished, value: 'STOP' };
yield {
type: ServerGeminiEventType.Finished,
value: { reason: 'STOP', usageMetadata: undefined },
};
})(),
);

Expand Down Expand Up @@ -1373,7 +1387,10 @@ describe('useGeminiStream', () => {
};
yield {
type: ServerGeminiEventType.Finished,
value: 'FINISH_REASON_UNSPECIFIED',
value: {
reason: 'FINISH_REASON_UNSPECIFIED',
usageMetadata: undefined,
},
};
})(),
);
Expand Down Expand Up @@ -1464,7 +1481,10 @@ describe('useGeminiStream', () => {
type: ServerGeminiEventType.Content,
value: `Response for ${reason}`,
};
yield { type: ServerGeminiEventType.Finished, value: reason };
yield {
type: ServerGeminiEventType.Finished,
value: { reason, usageMetadata: undefined },
};
})(),
);

Expand Down Expand Up @@ -1579,7 +1599,10 @@ describe('useGeminiStream', () => {
type: ServerGeminiEventType.Content,
value: 'Some response content',
};
yield { type: ServerGeminiEventType.Finished, value: 'STOP' };
yield {
type: ServerGeminiEventType.Finished,
value: { reason: 'STOP', usageMetadata: undefined },
};
})(),
);

Expand Down Expand Up @@ -1626,7 +1649,10 @@ describe('useGeminiStream', () => {
type: ServerGeminiEventType.Content,
value: 'New response content',
};
yield { type: ServerGeminiEventType.Finished, value: 'STOP' };
yield {
type: ServerGeminiEventType.Finished,
value: { reason: 'STOP', usageMetadata: undefined },
};
})(),
);

Expand Down
5 changes: 4 additions & 1 deletion packages/cli/src/ui/hooks/useGeminiStream.ts
Original file line number Diff line number Diff line change
Expand Up @@ -504,7 +504,10 @@ export const useGeminiStream = (

const handleFinishedEvent = useCallback(
(event: ServerGeminiFinishedEvent, userMessageTimestamp: number) => {
const finishReason = event.value;
const finishReason = event.value.reason;
if (!finishReason) {
return;
}

const finishReasonMessages: Record<FinishReason, string | undefined> = {
[FinishReason.FINISH_REASON_UNSPECIFIED]: undefined,
Expand Down
1 change: 1 addition & 0 deletions packages/cli/src/ui/hooks/useToolScheduler.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ const mockConfig = {
model: 'test-model',
authType: 'oauth-personal',
}),
getGeminiClient: () => null, // No client needed for these tests
} as unknown as Config;

const mockTool = new MockTool({
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/code_assist/converter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -204,7 +204,7 @@ function toContent(content: ContentUnion): Content {
};
}

function toParts(parts: PartUnion[]): Part[] {
export function toParts(parts: PartUnion[]): Part[] {
return parts.map(toPart);
}

Expand Down
30 changes: 30 additions & 0 deletions packages/core/src/core/client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,32 @@ import { tokenLimit } from './tokenLimits.js';
import { ideContext } from '../ide/ideContext.js';
import { ClearcutLogger } from '../telemetry/clearcut-logger/clearcut-logger.js';

// Mock fs module to prevent actual file system operations during tests
const mockFileSystem = new Map<string, string>();

vi.mock('node:fs', () => {
const fsModule = {
mkdirSync: vi.fn(),
writeFileSync: vi.fn((path: string, data: string) => {
mockFileSystem.set(path, data);
}),
readFileSync: vi.fn((path: string) => {
if (mockFileSystem.has(path)) {
return mockFileSystem.get(path);
}
throw Object.assign(new Error('ENOENT: no such file or directory'), {
code: 'ENOENT',
});
}),
existsSync: vi.fn((path: string) => mockFileSystem.has(path)),
};

return {
default: fsModule,
...fsModule,
};
});

// --- Mocks ---
const mockChatCreateFn = vi.fn();
const mockGenerateContentFn = vi.fn();
Expand Down Expand Up @@ -278,6 +304,10 @@ describe('Gemini Client (client.ts)', () => {
setFallbackMode: vi.fn(),
getChatCompression: vi.fn().mockReturnValue(undefined),
getSkipNextSpeakerCheck: vi.fn().mockReturnValue(false),
getProjectRoot: vi.fn().mockReturnValue('/test/project/root'),
storage: {
getProjectTempDir: vi.fn().mockReturnValue('/test/temp'),
},
};
const MockedConfig = vi.mocked(Config, true);
MockedConfig.mockImplementation(
Expand Down
5 changes: 5 additions & 0 deletions packages/core/src/core/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import { retryWithBackoff } from '../utils/retry.js';
import { getErrorMessage } from '../utils/errors.js';
import { isFunctionResponse } from '../utils/messageInspectors.js';
import { tokenLimit } from './tokenLimits.js';
import type { ChatRecordingService } from '../services/chatRecordingService.js';
import type {
ContentGenerator,
ContentGeneratorConfig,
Expand Down Expand Up @@ -222,6 +223,10 @@ export class GeminiClient {
this.chat = await this.startChat();
}

getChatRecordingService(): ChatRecordingService | undefined {
return this.chat?.getChatRecordingService();
Comment thread
bl-ue marked this conversation as resolved.
}

async addDirectoryContext(): Promise<void> {
if (!this.chat) {
return;
Expand Down
9 changes: 9 additions & 0 deletions packages/core/src/core/coreToolScheduler.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,7 @@ describe('CoreToolScheduler', () => {
authType: 'oauth-personal',
}),
getToolRegistry: () => mockToolRegistry,
getGeminiClient: () => null, // No client needed for these tests
} as unknown as Config;

const scheduler = new CoreToolScheduler({
Expand Down Expand Up @@ -201,6 +202,7 @@ describe('CoreToolScheduler', () => {
// Create mocked tool registry
const mockConfig = {
getToolRegistry: () => mockToolRegistry,
getGeminiClient: () => null, // No client needed for these tests
} as unknown as Config;
const mockToolRegistry = {
getAllToolNames: () => ['list_files', 'read_file', 'write_file'],
Expand Down Expand Up @@ -265,6 +267,7 @@ describe('CoreToolScheduler with payload', () => {
authType: 'oauth-personal',
}),
getToolRegistry: () => mockToolRegistry,
getGeminiClient: () => null, // No client needed for these tests
} as unknown as Config;

const scheduler = new CoreToolScheduler({
Expand Down Expand Up @@ -571,6 +574,7 @@ describe('CoreToolScheduler edit cancellation', () => {
authType: 'oauth-personal',
}),
getToolRegistry: () => mockToolRegistry,
getGeminiClient: () => null, // No client needed for these tests
} as unknown as Config;

const scheduler = new CoreToolScheduler({
Expand Down Expand Up @@ -662,6 +666,7 @@ describe('CoreToolScheduler YOLO mode', () => {
authType: 'oauth-personal',
}),
getToolRegistry: () => mockToolRegistry,
getGeminiClient: () => null, // No client needed for these tests
} as unknown as Config;

const scheduler = new CoreToolScheduler({
Expand Down Expand Up @@ -752,6 +757,7 @@ describe('CoreToolScheduler request queueing', () => {
authType: 'oauth-personal',
}),
getToolRegistry: () => mockToolRegistry,
getGeminiClient: () => null, // No client needed for these tests
} as unknown as Config;

const scheduler = new CoreToolScheduler({
Expand Down Expand Up @@ -868,6 +874,7 @@ describe('CoreToolScheduler request queueing', () => {
model: 'test-model',
authType: 'oauth-personal',
}),
getGeminiClient: () => null, // No client needed for these tests
} as unknown as Config;

const scheduler = new CoreToolScheduler({
Expand Down Expand Up @@ -948,6 +955,7 @@ describe('CoreToolScheduler request queueing', () => {
authType: 'oauth-personal',
}),
getToolRegistry: () => mockToolRegistry,
getGeminiClient: () => null, // No client needed for these tests
} as unknown as Config;

const scheduler = new CoreToolScheduler({
Expand Down Expand Up @@ -1007,6 +1015,7 @@ describe('CoreToolScheduler request queueing', () => {
setApprovalMode: (mode: ApprovalMode) => {
approvalMode = mode;
},
getGeminiClient: () => null, // No client needed for these tests
} as unknown as Config;

const testTool = new TestApprovalTool(mockConfig);
Expand Down
Loading
Loading