File size: 4,648 Bytes
c44600e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
/**
 * @license
 * Copyright 2025 Google LLC
 * SPDX-License-Identifier: Apache-2.0
 */

import { expect, it, describe, beforeEach, afterEach } from 'vitest';
import { TestRig } from './test-helper.js';
import { TestMcpServer } from './test-mcp-server.js';
import { writeFileSync } from 'node:fs';
import { join } from 'node:path';
import { safeJsonStringify } from '@google/gemini-cli-core/src/utils/safeJsonStringify.js';

import stripAnsi from 'strip-ansi';

describe('extension reloading', () => {
  let rig: TestRig;

  beforeEach(() => {
    rig = new TestRig();
  });

  afterEach(async () => await rig.cleanup());

  // always fails
  // TODO(#14527): Re-enable this once fixed
  it.skip('installs a local extension, updates it, checks it was reloaded properly', async () => {
    const serverA = new TestMcpServer();
    const portA = await serverA.start({
      hello: () => ({ content: [{ type: 'text', text: 'world' }] }),
    });
    const extension = {
      name: 'test-extension',
      version: '0.0.1',
      mcpServers: {
        'test-server': {
          httpUrl: `http://localhost:${portA}/mcp`,
        },
      },
    };

    rig.setup('extension reload test', {
      settings: {
        experimental: { extensionReloading: true },
      },
    });
    const testServerPath = join(rig.testDir!, 'gemini-extension.json');
    writeFileSync(testServerPath, safeJsonStringify(extension, 2));
    // defensive cleanup from previous tests.
    try {
      await rig.runCommand(['extensions', 'uninstall', 'test-extension']);
    } catch {
      /* empty */
    }

    const result = await rig.runCommand(
      ['--debug', 'extensions', 'install', `${rig.testDir!}`],
      { stdin: 'y\n' },
    );
    expect(result).toContain('test-extension');

    // Now create the update, but its not installed yet
    const serverB = new TestMcpServer();
    const portB = await serverB.start({
      goodbye: () => ({ content: [{ type: 'text', text: 'world' }] }),
    });
    extension.version = '0.0.2';
    extension.mcpServers['test-server'].httpUrl =
      `http://localhost:${portB}/mcp`;
    writeFileSync(testServerPath, safeJsonStringify(extension, 2));

    // Start the CLI.
    const run = await rig.runInteractive({ args: '--debug' });
    await run.expectText('You have 1 extension with an update available');
    // See the outdated extension
    await run.sendText('/extensions list');
    await run.type('\r');
    await run.expectText('test-extension (v0.0.1) - active (update available)');
    // Wait for the UI to settle and retry the command until we see the update
    await new Promise((resolve) => setTimeout(resolve, 1000));

    // Poll for the updated list
    await rig.pollCommand(
      async () => {
        await run.sendText('/mcp list');
        await run.type('\r');
      },
      () => {
        const output = stripAnsi(run.output);
        return (
          output.includes(
            'test-server (from test-extension) - Ready (1 tool)',
          ) && output.includes('- mcp_test-server_hello')
        );
      },
      30000, // 30s timeout
    );

    // Update the extension, expect the list to update, and mcp servers as well.
    await run.sendKeys('\u0015/extensions update test-extension');
    await run.expectText('/extensions update test-extension');
    await run.type('\r');
    await new Promise((resolve) => setTimeout(resolve, 500));
    await run.type('\r');
    await run.expectText(
      ` * test-server (remote): http://localhost:${portB}/mcp`,
    );
    await run.type('\r'); // consent
    await run.expectText(
      'Extension "test-extension" successfully updated: 0.0.1 → 0.0.2',
    );

    // Poll for the updated extension version
    await rig.pollCommand(
      async () => {
        await run.sendText('/extensions list');
        await run.type('\r');
      },
      () =>
        stripAnsi(run.output).includes(
          'test-extension (v0.0.2) - active (updated)',
        ),
      30000,
    );

    // Poll for the updated mcp tool
    await rig.pollCommand(
      async () => {
        await run.sendText('/mcp list');
        await run.type('\r');
      },
      () => {
        const output = stripAnsi(run.output);
        return (
          output.includes(
            'test-server (from test-extension) - Ready (1 tool)',
          ) && output.includes('- mcp_test-server_goodbye')
        );
      },
      30000,
    );

    await run.sendText('/quit');
    await run.type('\r');

    // Clean things up.
    await serverA.stop();
    await serverB.stop();
    await rig.runCommand(['extensions', 'uninstall', 'test-extension']);
  });
});