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);
});