diff --git a/integration-tests/extensions-reload.test.ts b/integration-tests/extensions-reload.test.ts index ba9bec55e1..4a1250fd00 100644 --- a/integration-tests/extensions-reload.test.ts +++ b/integration-tests/extensions-reload.test.ts @@ -10,13 +10,9 @@ 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 { env } from 'node:process'; -import { platform } from 'node:os'; import stripAnsi from 'strip-ansi'; -const itIf = (condition: boolean) => (condition ? it : it.skip); - describe('extension reloading', () => { let rig: TestRig; @@ -26,141 +22,130 @@ describe('extension reloading', () => { afterEach(async () => await rig.cleanup()); - const sandboxEnv = env['GEMINI_SANDBOX']; - // Fails in linux non-sandbox e2e tests + // always fails // TODO(#14527): Re-enable this once fixed - // Fails in sandbox mode, can't check for local extension updates. - itIf( - (!sandboxEnv || sandboxEnv === 'false') && - platform() !== 'win32' && - platform() !== 'linux', - )( - '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`, - }, + 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(); + 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']); + }); });