mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-07-05 15:46:47 -07:00
Merge branch 'main' into mk-teleport
This commit is contained in:
@@ -1,2 +1,4 @@
|
||||
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"text":"I'll open https://example.com and check the page title for you."},{"functionCall":{"name":"browser_agent","args":{"task":"Open https://example.com and get the page title"}}}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":100,"candidatesTokenCount":35,"totalTokenCount":135}}]}
|
||||
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"text":"The page title of https://example.com is \"Example Domain\". The browser session has been completed and cleaned up successfully."}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":200,"candidatesTokenCount":30,"totalTokenCount":230}}]}
|
||||
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"text":"I have opened the page and the title is 'Example Domain'."}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":200,"candidatesTokenCount":30,"totalTokenCount":230}}]}
|
||||
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"text":"The task is complete. The page title is 'Example Domain'."}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":300,"candidatesTokenCount":20,"totalTokenCount":320}}]}
|
||||
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"text":"Done."}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":400,"candidatesTokenCount":5,"totalTokenCount":405}}]}
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"text":"I'll help you with that."},{"functionCall":{"name":"browser_agent","args":{"task":"Open https://example.com and check if there is a heading"}}}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":100,"candidatesTokenCount":50,"totalTokenCount":150}}]}
|
||||
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"functionCall":{"name":"new_page","args":{"url":"https://example.com"}}}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":100,"candidatesTokenCount":50,"totalTokenCount":150}}]}
|
||||
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"functionCall":{"name":"take_snapshot","args":{}}}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":100,"candidatesTokenCount":50,"totalTokenCount":150}}]}
|
||||
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"functionCall":{"name":"complete_task","args":{"success":true,"summary":"SUCCESS_POLICY_TEST_COMPLETED"}}}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":100,"candidatesTokenCount":50,"totalTokenCount":150}}]}
|
||||
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"text":"Task completed successfully. The page has the heading \"Example Domain\"."}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":200,"candidatesTokenCount":50,"totalTokenCount":250}}]}
|
||||
@@ -0,0 +1,210 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2026 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
||||
import { TestRig, poll } from './test-helper.js';
|
||||
import { dirname, join } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { execSync } from 'node:child_process';
|
||||
import { existsSync, writeFileSync, readFileSync, mkdirSync } from 'node:fs';
|
||||
import stripAnsi from 'strip-ansi';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
|
||||
const chromeAvailable = (() => {
|
||||
try {
|
||||
if (process.platform === 'darwin') {
|
||||
execSync(
|
||||
'test -d "/Applications/Google Chrome.app" || test -d "/Applications/Chromium.app"',
|
||||
{
|
||||
stdio: 'ignore',
|
||||
},
|
||||
);
|
||||
} else if (process.platform === 'linux') {
|
||||
execSync(
|
||||
'which google-chrome || which chromium-browser || which chromium',
|
||||
{ stdio: 'ignore' },
|
||||
);
|
||||
} else if (process.platform === 'win32') {
|
||||
const chromePaths = [
|
||||
'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe',
|
||||
'C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe',
|
||||
`${process.env['LOCALAPPDATA'] ?? ''}\\Google\\Chrome\\Application\\chrome.exe`,
|
||||
];
|
||||
const found = chromePaths.some((p) => existsSync(p));
|
||||
if (!found) {
|
||||
execSync('where chrome || where chromium', { stdio: 'ignore' });
|
||||
}
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
})();
|
||||
|
||||
describe.skipIf(!chromeAvailable)('browser-policy', () => {
|
||||
let rig: TestRig;
|
||||
|
||||
beforeEach(() => {
|
||||
rig = new TestRig();
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await rig.cleanup();
|
||||
});
|
||||
|
||||
it('should skip confirmation when "Allow all server tools for this session" is chosen', async () => {
|
||||
rig.setup('browser-policy-skip-confirmation', {
|
||||
fakeResponsesPath: join(__dirname, 'browser-policy.responses'),
|
||||
settings: {
|
||||
agents: {
|
||||
overrides: {
|
||||
browser_agent: {
|
||||
enabled: true,
|
||||
},
|
||||
},
|
||||
browser: {
|
||||
headless: true,
|
||||
sessionMode: 'isolated',
|
||||
allowedDomains: ['example.com'],
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Manually trust the folder to avoid the dialog and enable option 3
|
||||
const geminiDir = join(rig.homeDir!, '.gemini');
|
||||
mkdirSync(geminiDir, { recursive: true });
|
||||
|
||||
// Write to trustedFolders.json
|
||||
const trustedFoldersPath = join(geminiDir, 'trustedFolders.json');
|
||||
const trustedFolders = {
|
||||
[rig.testDir!]: 'TRUST_FOLDER',
|
||||
};
|
||||
writeFileSync(trustedFoldersPath, JSON.stringify(trustedFolders, null, 2));
|
||||
|
||||
// Force confirmation for browser agent.
|
||||
// NOTE: We don't force confirm browser tools here because "Allow all server tools"
|
||||
// adds a rule with ALWAYS_ALLOW_PRIORITY (3.9x) which would be overshadowed by
|
||||
// a rule in the user tier (4.x) like the one from this TOML.
|
||||
// By removing the explicit mcp rule, the first MCP tool will still prompt
|
||||
// due to default approvalMode = 'default', and then "Allow all" will correctly
|
||||
// bypass subsequent tools.
|
||||
const policyFile = join(rig.testDir!, 'force-confirm.toml');
|
||||
writeFileSync(
|
||||
policyFile,
|
||||
`
|
||||
[[rule]]
|
||||
name = "Force confirm browser_agent"
|
||||
toolName = "browser_agent"
|
||||
decision = "ask_user"
|
||||
priority = 200
|
||||
`,
|
||||
);
|
||||
|
||||
// Update settings.json in both project and home directories to point to the policy file
|
||||
for (const baseDir of [rig.testDir!, rig.homeDir!]) {
|
||||
const settingsPath = join(baseDir, '.gemini', 'settings.json');
|
||||
if (existsSync(settingsPath)) {
|
||||
const settings = JSON.parse(readFileSync(settingsPath, 'utf-8'));
|
||||
settings.policyPaths = [policyFile];
|
||||
// Ensure folder trust is enabled
|
||||
settings.security = settings.security || {};
|
||||
settings.security.folderTrust = settings.security.folderTrust || {};
|
||||
settings.security.folderTrust.enabled = true;
|
||||
writeFileSync(settingsPath, JSON.stringify(settings, null, 2));
|
||||
}
|
||||
}
|
||||
|
||||
const run = await rig.runInteractive({
|
||||
approvalMode: 'default',
|
||||
env: {
|
||||
GEMINI_CLI_INTEGRATION_TEST: 'true',
|
||||
},
|
||||
});
|
||||
|
||||
await run.sendKeys(
|
||||
'Open https://example.com and check if there is a heading\r',
|
||||
);
|
||||
await run.sendKeys('\r');
|
||||
|
||||
// Handle confirmations.
|
||||
// 1. Initial browser_agent delegation (likely only 3 options, so use option 1: Allow once)
|
||||
await poll(
|
||||
() => stripAnsi(run.output).toLowerCase().includes('action required'),
|
||||
60000,
|
||||
1000,
|
||||
);
|
||||
await run.sendKeys('1\r');
|
||||
await new Promise((r) => setTimeout(r, 2000));
|
||||
|
||||
// Handle privacy notice
|
||||
await poll(
|
||||
() => stripAnsi(run.output).toLowerCase().includes('privacy notice'),
|
||||
5000,
|
||||
100,
|
||||
);
|
||||
await run.sendKeys('1\r');
|
||||
await new Promise((r) => setTimeout(r, 5000));
|
||||
|
||||
// new_page (MCP tool, should have 4 options, use option 3: Allow all server tools)
|
||||
await poll(
|
||||
() => {
|
||||
const stripped = stripAnsi(run.output).toLowerCase();
|
||||
return (
|
||||
stripped.includes('new_page') &&
|
||||
stripped.includes('allow all server tools for this session')
|
||||
);
|
||||
},
|
||||
60000,
|
||||
1000,
|
||||
);
|
||||
|
||||
// Select "Allow all server tools for this session" (option 3)
|
||||
await run.sendKeys('3\r');
|
||||
await new Promise((r) => setTimeout(r, 30000));
|
||||
|
||||
const output = stripAnsi(run.output).toLowerCase();
|
||||
|
||||
expect(output).toContain('browser_agent');
|
||||
expect(output).toContain('completed successfully');
|
||||
});
|
||||
|
||||
it('should show the visible warning when browser agent starts in existing session mode', async () => {
|
||||
rig.setup('browser-session-warning', {
|
||||
fakeResponsesPath: join(__dirname, 'browser-agent.cleanup.responses'),
|
||||
settings: {
|
||||
general: {
|
||||
enableAutoUpdateNotification: false,
|
||||
},
|
||||
agents: {
|
||||
overrides: {
|
||||
browser_agent: {
|
||||
enabled: true,
|
||||
},
|
||||
},
|
||||
browser: {
|
||||
sessionMode: 'existing',
|
||||
headless: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const stdout = await rig.runCommand(['Open https://example.com'], {
|
||||
env: {
|
||||
GEMINI_API_KEY: 'fake-key',
|
||||
GEMINI_TELEMETRY_DISABLED: 'true',
|
||||
DEV: 'true',
|
||||
},
|
||||
});
|
||||
|
||||
expect(stdout).toContain('saved logins will be visible');
|
||||
});
|
||||
});
|
||||
@@ -42,11 +42,10 @@ describe('extension install', () => {
|
||||
const listResult = await rig.runCommand(['extensions', 'list']);
|
||||
expect(listResult).toContain('test-extension-install');
|
||||
writeFileSync(testServerPath, extensionUpdate);
|
||||
const updateResult = await rig.runCommand([
|
||||
'extensions',
|
||||
'update',
|
||||
`test-extension-install`,
|
||||
]);
|
||||
const updateResult = await rig.runCommand(
|
||||
['extensions', 'update', `test-extension-install`],
|
||||
{ stdin: 'y\n' },
|
||||
);
|
||||
expect(updateResult).toContain('0.0.2');
|
||||
} finally {
|
||||
await rig.runCommand([
|
||||
|
||||
+2196
-1958
File diff suppressed because it is too large
Load Diff
@@ -183,11 +183,17 @@ describe('Policy Engine Headless Mode', () => {
|
||||
responsesFile: 'policy-headless-shell-denied.responses',
|
||||
promptCommand: ECHO_PROMPT,
|
||||
policyContent: `
|
||||
[[rule]]
|
||||
toolName = "run_shell_command"
|
||||
commandPrefix = "echo"
|
||||
decision = "deny"
|
||||
priority = 100
|
||||
|
||||
[[rule]]
|
||||
toolName = "run_shell_command"
|
||||
commandPrefix = "node"
|
||||
decision = "allow"
|
||||
priority = 100
|
||||
priority = 90
|
||||
`,
|
||||
expectAllowed: false,
|
||||
expectedDenialString: 'Tool execution denied by policy',
|
||||
|
||||
@@ -58,12 +58,18 @@ function getDisallowedFileReadCommand(testFile: string): {
|
||||
const quotedPath = `"${testFile}"`;
|
||||
switch (shell) {
|
||||
case 'powershell':
|
||||
return { command: `Get-Content ${quotedPath}`, tool: 'Get-Content' };
|
||||
return {
|
||||
command: `powershell -Command "Get-Content ${quotedPath}"`,
|
||||
tool: 'powershell',
|
||||
};
|
||||
case 'cmd':
|
||||
return { command: `type ${quotedPath}`, tool: 'type' };
|
||||
return { command: `cmd /c type ${quotedPath}`, tool: 'cmd' };
|
||||
case 'bash':
|
||||
default:
|
||||
return { command: `cat ${quotedPath}`, tool: 'cat' };
|
||||
return {
|
||||
command: `node -e "console.log(require('fs').readFileSync('${testFile}', 'utf8'))"`,
|
||||
tool: 'node',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
*/
|
||||
|
||||
import { describe, expect, it, beforeEach, afterEach } from 'vitest';
|
||||
import { TestRig, InteractiveRun } from './test-helper.js';
|
||||
import { TestRig, InteractiveRun, skipFlaky } from './test-helper.js';
|
||||
import * as fs from 'node:fs';
|
||||
import * as os from 'node:os';
|
||||
import {
|
||||
@@ -33,104 +33,107 @@ const otherExtension = `{
|
||||
"version": "6.6.6"
|
||||
}`;
|
||||
|
||||
describe('extension symlink install spoofing protection', () => {
|
||||
let rig: TestRig;
|
||||
describe.skipIf(skipFlaky)(
|
||||
'extension symlink install spoofing protection',
|
||||
() => {
|
||||
let rig: TestRig;
|
||||
|
||||
beforeEach(() => {
|
||||
rig = new TestRig();
|
||||
});
|
||||
|
||||
afterEach(async () => await rig.cleanup());
|
||||
|
||||
it('canonicalizes the trust path and prevents symlink spoofing', async () => {
|
||||
// Enable folder trust for this test
|
||||
rig.setup('symlink spoofing test', {
|
||||
settings: {
|
||||
security: {
|
||||
folderTrust: {
|
||||
enabled: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
beforeEach(() => {
|
||||
rig = new TestRig();
|
||||
});
|
||||
|
||||
const realExtPath = join(rig.testDir!, 'real-extension');
|
||||
mkdirSync(realExtPath);
|
||||
writeFileSync(join(realExtPath, 'gemini-extension.json'), extension);
|
||||
afterEach(async () => await rig.cleanup());
|
||||
|
||||
const maliciousExtPath = join(
|
||||
os.tmpdir(),
|
||||
`malicious-extension-${Date.now()}`,
|
||||
);
|
||||
mkdirSync(maliciousExtPath);
|
||||
writeFileSync(
|
||||
join(maliciousExtPath, 'gemini-extension.json'),
|
||||
otherExtension,
|
||||
);
|
||||
|
||||
const symlinkPath = join(rig.testDir!, 'symlink-extension');
|
||||
symlinkSync(realExtPath, symlinkPath);
|
||||
|
||||
// Function to run a command with a PTY to avoid headless mode
|
||||
const runPty = (args: string[]) => {
|
||||
const ptyProcess = pty.spawn(process.execPath, [BUNDLE_PATH, ...args], {
|
||||
name: 'xterm-color',
|
||||
cols: 80,
|
||||
rows: 80,
|
||||
cwd: rig.testDir!,
|
||||
env: {
|
||||
...process.env,
|
||||
GEMINI_CLI_HOME: rig.homeDir!,
|
||||
GEMINI_CLI_INTEGRATION_TEST: 'true',
|
||||
GEMINI_PTY_INFO: 'node-pty',
|
||||
it('canonicalizes the trust path and prevents symlink spoofing', async () => {
|
||||
// Enable folder trust for this test
|
||||
rig.setup('symlink spoofing test', {
|
||||
settings: {
|
||||
security: {
|
||||
folderTrust: {
|
||||
enabled: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
return new InteractiveRun(ptyProcess);
|
||||
};
|
||||
|
||||
// 1. Install via symlink, trust it
|
||||
const run1 = runPty(['extensions', 'install', symlinkPath]);
|
||||
await run1.expectText('Do you want to trust this folder', 30000);
|
||||
await run1.type('y\r');
|
||||
await run1.expectText('trust this workspace', 30000);
|
||||
await run1.type('y\r');
|
||||
await run1.expectText('Do you want to continue', 30000);
|
||||
await run1.type('y\r');
|
||||
await run1.expectText('installed successfully', 30000);
|
||||
await run1.kill();
|
||||
const realExtPath = join(rig.testDir!, 'real-extension');
|
||||
mkdirSync(realExtPath);
|
||||
writeFileSync(join(realExtPath, 'gemini-extension.json'), extension);
|
||||
|
||||
// 2. Verify trustedFolders.json contains the REAL path, not the symlink path
|
||||
const trustedFoldersPath = join(
|
||||
rig.homeDir!,
|
||||
GEMINI_DIR,
|
||||
'trustedFolders.json',
|
||||
);
|
||||
// Wait for file to be written
|
||||
let attempts = 0;
|
||||
while (!fs.existsSync(trustedFoldersPath) && attempts < 50) {
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
attempts++;
|
||||
}
|
||||
const maliciousExtPath = join(
|
||||
os.tmpdir(),
|
||||
`malicious-extension-${Date.now()}`,
|
||||
);
|
||||
mkdirSync(maliciousExtPath);
|
||||
writeFileSync(
|
||||
join(maliciousExtPath, 'gemini-extension.json'),
|
||||
otherExtension,
|
||||
);
|
||||
|
||||
const trustedFolders = JSON.parse(
|
||||
readFileSync(trustedFoldersPath, 'utf-8'),
|
||||
);
|
||||
const trustedPaths = Object.keys(trustedFolders);
|
||||
const canonicalRealExtPath = fs.realpathSync(realExtPath);
|
||||
const symlinkPath = join(rig.testDir!, 'symlink-extension');
|
||||
symlinkSync(realExtPath, symlinkPath);
|
||||
|
||||
expect(trustedPaths).toContain(canonicalRealExtPath);
|
||||
expect(trustedPaths).not.toContain(symlinkPath);
|
||||
// Function to run a command with a PTY to avoid headless mode
|
||||
const runPty = (args: string[]) => {
|
||||
const ptyProcess = pty.spawn(process.execPath, [BUNDLE_PATH, ...args], {
|
||||
name: 'xterm-color',
|
||||
cols: 80,
|
||||
rows: 80,
|
||||
cwd: rig.testDir!,
|
||||
env: {
|
||||
...process.env,
|
||||
GEMINI_CLI_HOME: rig.homeDir!,
|
||||
GEMINI_CLI_INTEGRATION_TEST: 'true',
|
||||
GEMINI_PTY_INFO: 'node-pty',
|
||||
},
|
||||
});
|
||||
return new InteractiveRun(ptyProcess);
|
||||
};
|
||||
|
||||
// 3. Swap the symlink to point to the malicious extension
|
||||
unlinkSync(symlinkPath);
|
||||
symlinkSync(maliciousExtPath, symlinkPath);
|
||||
// 1. Install via symlink, trust it
|
||||
const run1 = runPty(['extensions', 'install', symlinkPath]);
|
||||
await run1.expectText('Do you want to trust this folder', 30000);
|
||||
await run1.type('y\r');
|
||||
await run1.expectText('trust this workspace', 30000);
|
||||
await run1.type('y\r');
|
||||
await run1.expectText('Do you want to continue', 30000);
|
||||
await run1.type('y\r');
|
||||
await run1.expectText('installed successfully', 30000);
|
||||
await run1.kill();
|
||||
|
||||
// 4. Try to install again via the same symlink path.
|
||||
// It should NOT be trusted because the real path changed.
|
||||
const run2 = runPty(['extensions', 'install', symlinkPath]);
|
||||
await run2.expectText('Do you want to trust this folder', 30000);
|
||||
await run2.type('n\r');
|
||||
await run2.expectText('Installation aborted', 30000);
|
||||
await run2.kill();
|
||||
}, 60000);
|
||||
});
|
||||
// 2. Verify trustedFolders.json contains the REAL path, not the symlink path
|
||||
const trustedFoldersPath = join(
|
||||
rig.homeDir!,
|
||||
GEMINI_DIR,
|
||||
'trustedFolders.json',
|
||||
);
|
||||
// Wait for file to be written
|
||||
let attempts = 0;
|
||||
while (!fs.existsSync(trustedFoldersPath) && attempts < 50) {
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
attempts++;
|
||||
}
|
||||
|
||||
const trustedFolders = JSON.parse(
|
||||
readFileSync(trustedFoldersPath, 'utf-8'),
|
||||
);
|
||||
const trustedPaths = Object.keys(trustedFolders);
|
||||
const canonicalRealExtPath = fs.realpathSync(realExtPath);
|
||||
|
||||
expect(trustedPaths).toContain(canonicalRealExtPath);
|
||||
expect(trustedPaths).not.toContain(symlinkPath);
|
||||
|
||||
// 3. Swap the symlink to point to the malicious extension
|
||||
unlinkSync(symlinkPath);
|
||||
symlinkSync(maliciousExtPath, symlinkPath);
|
||||
|
||||
// 4. Try to install again via the same symlink path.
|
||||
// It should NOT be trusted because the real path changed.
|
||||
const run2 = runPty(['extensions', 'install', symlinkPath]);
|
||||
await run2.expectText('Do you want to trust this folder', 30000);
|
||||
await run2.type('n\r');
|
||||
await run2.expectText('Installation aborted', 30000);
|
||||
await run2.kill();
|
||||
}, 60000);
|
||||
},
|
||||
);
|
||||
|
||||
@@ -6,3 +6,5 @@
|
||||
|
||||
export * from '@google/gemini-cli-test-utils';
|
||||
export { normalizePath } from '@google/gemini-cli-test-utils';
|
||||
|
||||
export const skipFlaky = !process.env['RUN_FLAKY_INTEGRATION'];
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"functionCall":{"name":"mcp_weather-server_get_weather","args":{"location":"London"}}}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":10,"candidatesTokenCount":10,"totalTokenCount":20}}]}
|
||||
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"text":"The weather in London is rainy."}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":10,"candidatesTokenCount":10,"totalTokenCount":20}}]}
|
||||
@@ -0,0 +1,75 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2026 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
||||
import {
|
||||
TestRig,
|
||||
assertModelHasOutput,
|
||||
TestMcpServerBuilder,
|
||||
} from './test-helper.js';
|
||||
import { join, dirname } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import fs from 'node:fs';
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
describe('test-mcp-support', () => {
|
||||
let rig: TestRig;
|
||||
|
||||
beforeEach(() => {
|
||||
rig = new TestRig();
|
||||
});
|
||||
|
||||
afterEach(async () => await rig.cleanup());
|
||||
|
||||
it('should discover and call a tool on the test server', async () => {
|
||||
await rig.setup('test-mcp-test', {
|
||||
settings: {
|
||||
tools: { core: [] }, // disable core tools to force using MCP
|
||||
model: {
|
||||
name: 'gemini-3-flash-preview',
|
||||
},
|
||||
},
|
||||
fakeResponsesPath: join(__dirname, 'test-mcp-support.responses'),
|
||||
});
|
||||
|
||||
// Workaround for ProjectRegistry save issue
|
||||
const userGeminiDir = join(rig.homeDir!, '.gemini');
|
||||
fs.writeFileSync(join(userGeminiDir, 'projects.json'), '{"projects":{}}');
|
||||
|
||||
const builder = new TestMcpServerBuilder('weather-server').addTool(
|
||||
'get_weather',
|
||||
'Get the weather for a location',
|
||||
'The weather in London is always rainy.',
|
||||
{
|
||||
type: 'object',
|
||||
properties: {
|
||||
location: { type: 'string' },
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
rig.addTestMcpServer('weather-server', builder.build());
|
||||
|
||||
// Run the CLI asking for weather
|
||||
const output = await rig.run({
|
||||
args: 'What is the weather in London? Answer with the raw tool response snippet.',
|
||||
env: { GEMINI_API_KEY: 'dummy' },
|
||||
});
|
||||
|
||||
// Assert tool call
|
||||
const foundToolCall = await rig.waitForToolCall(
|
||||
'mcp_weather-server_get_weather',
|
||||
);
|
||||
expect(
|
||||
foundToolCall,
|
||||
'Expected to find a get_weather tool call',
|
||||
).toBeTruthy();
|
||||
|
||||
assertModelHasOutput(output);
|
||||
expect(output.toLowerCase()).toContain('rainy');
|
||||
}, 30000);
|
||||
});
|
||||
Reference in New Issue
Block a user