mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-03-12 15:10:59 -07:00
173 lines
5.4 KiB
TypeScript
173 lines
5.4 KiB
TypeScript
/**
|
|
* @license
|
|
* Copyright 2026 Google LLC
|
|
* SPDX-License-Identifier: Apache-2.0
|
|
*/
|
|
|
|
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
import { GeminiCliAgent } from './agent.js';
|
|
import * as path from 'node:path';
|
|
import { fileURLToPath } from 'node:url';
|
|
|
|
const __filename = fileURLToPath(import.meta.url);
|
|
const __dirname = path.dirname(__filename);
|
|
|
|
// Set this to true locally when you need to update snapshots
|
|
const RECORD_MODE = process.env['RECORD_NEW_RESPONSES'] === 'true';
|
|
|
|
const getGoldenPath = (name: string) =>
|
|
path.resolve(__dirname, '../test-data', `${name}.json`);
|
|
|
|
describe('GeminiCliAgent Integration', () => {
|
|
beforeEach(() => {
|
|
vi.stubEnv('GEMINI_API_KEY', 'test-api-key');
|
|
});
|
|
|
|
afterEach(() => {
|
|
vi.unstubAllEnvs();
|
|
});
|
|
it('handles static instructions', async () => {
|
|
const goldenFile = getGoldenPath('agent-static-instructions');
|
|
|
|
const agent = new GeminiCliAgent({
|
|
instructions: 'You are a pirate. Respond in pirate speak.',
|
|
model: 'gemini-2.0-flash',
|
|
recordResponses: RECORD_MODE ? goldenFile : undefined,
|
|
fakeResponses: RECORD_MODE ? undefined : goldenFile,
|
|
});
|
|
|
|
const session = agent.session();
|
|
expect(session.id).toMatch(
|
|
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i,
|
|
);
|
|
const events = [];
|
|
const stream = session.sendStream('Say hello.');
|
|
|
|
for await (const event of stream) {
|
|
events.push(event);
|
|
}
|
|
|
|
const textEvents = events.filter((e) => e.type === 'content');
|
|
const responseText = textEvents
|
|
.map((e) => (typeof e.value === 'string' ? e.value : ''))
|
|
.join('');
|
|
|
|
// Expect pirate speak
|
|
expect(responseText.toLowerCase()).toMatch(/ahoy|matey|arrr/);
|
|
}, 30000);
|
|
|
|
it('handles dynamic instructions', async () => {
|
|
const goldenFile = getGoldenPath('agent-dynamic-instructions');
|
|
|
|
let callCount = 0;
|
|
const agent = new GeminiCliAgent({
|
|
instructions: (_ctx) => {
|
|
callCount++;
|
|
return `You are a helpful assistant. The secret number is ${callCount}. Always mention the secret number when asked.`;
|
|
},
|
|
model: 'gemini-2.0-flash',
|
|
recordResponses: RECORD_MODE ? goldenFile : undefined,
|
|
fakeResponses: RECORD_MODE ? undefined : goldenFile,
|
|
});
|
|
|
|
const session = agent.session();
|
|
|
|
// First turn
|
|
const events1 = [];
|
|
const stream1 = session.sendStream('What is the secret number?');
|
|
for await (const event of stream1) {
|
|
events1.push(event);
|
|
}
|
|
const responseText1 = events1
|
|
.filter((e) => e.type === 'content')
|
|
.map((e) => (typeof e.value === 'string' ? e.value : ''))
|
|
.join('');
|
|
|
|
expect(responseText1).toContain('1');
|
|
|
|
// Second turn
|
|
const events2 = [];
|
|
const stream2 = session.sendStream('What is the secret number now?');
|
|
for await (const event of stream2) {
|
|
events2.push(event);
|
|
}
|
|
const responseText2 = events2
|
|
.filter((e) => e.type === 'content')
|
|
.map((e) => (typeof e.value === 'string' ? e.value : ''))
|
|
.join('');
|
|
|
|
expect(responseText2).toContain('2');
|
|
}, 30000);
|
|
|
|
it('resumes a session', async () => {
|
|
const goldenFile = getGoldenPath('agent-resume-session');
|
|
|
|
// Create initial session
|
|
const agent = new GeminiCliAgent({
|
|
instructions: 'You are a memory test. Remember the word "BANANA".',
|
|
model: 'gemini-2.0-flash',
|
|
recordResponses: RECORD_MODE ? goldenFile : undefined,
|
|
fakeResponses: RECORD_MODE ? undefined : goldenFile,
|
|
});
|
|
|
|
const session1 = agent.session({ sessionId: 'resume-test-fixed-id' });
|
|
const sessionId = session1.id;
|
|
const stream1 = session1.sendStream('What is the word?');
|
|
for await (const _ of stream1) {
|
|
// consume stream
|
|
}
|
|
|
|
// Resume session
|
|
// Allow some time for async writes if any
|
|
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
|
|
const session2 = await agent.resumeSession(sessionId);
|
|
expect(session2.id).toBe(sessionId);
|
|
|
|
const events2 = [];
|
|
const stream2 = session2.sendStream('What is the word again?');
|
|
for await (const event of stream2) {
|
|
events2.push(event);
|
|
}
|
|
|
|
const responseText = events2
|
|
.filter((e) => e.type === 'content')
|
|
.map((e) => (typeof e.value === 'string' ? e.value : ''))
|
|
.join('');
|
|
|
|
expect(responseText).toContain('BANANA');
|
|
}, 30000);
|
|
|
|
it('throws on invalid instructions', () => {
|
|
// Missing instructions should be fine
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
expect(() => new GeminiCliAgent({} as any).session()).not.toThrow();
|
|
|
|
expect(() =>
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
new GeminiCliAgent({ instructions: 123 as any }).session(),
|
|
).toThrow('Instructions must be a string or a function.');
|
|
});
|
|
|
|
it('propagates errors from dynamic instructions', async () => {
|
|
const goldenFile = getGoldenPath('agent-static-instructions');
|
|
const agent = new GeminiCliAgent({
|
|
instructions: () => {
|
|
throw new Error('Dynamic instruction failure');
|
|
},
|
|
model: 'gemini-2.0-flash',
|
|
recordResponses: RECORD_MODE ? goldenFile : undefined,
|
|
fakeResponses: RECORD_MODE ? undefined : goldenFile,
|
|
});
|
|
|
|
const session = agent.session();
|
|
const stream = session.sendStream('Say hello.');
|
|
|
|
await expect(async () => {
|
|
for await (const _event of stream) {
|
|
// Just consume the stream
|
|
}
|
|
}).rejects.toThrow('Dynamic instruction failure');
|
|
}, 30000);
|
|
});
|