File size: 5,620 Bytes
7421850 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 | /**
* @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) => ('value' in 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) => ('value' in 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) => ('value' in 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) => ('value' in 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);
});
|