mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-06-13 12:57:12 -07:00
Merge branch 'main' into mk-bundling-no-npmrc
This commit is contained in:
@@ -24,13 +24,147 @@ concurrency:
|
||||
${{ github.ref != 'refs/heads/main' && !startsWith(github.ref, 'refs/heads/release/') }}
|
||||
|
||||
jobs:
|
||||
deflake:
|
||||
name: 'Deflake'
|
||||
deflake_e2e_linux:
|
||||
name: 'E2E Test (Linux) - ${{ matrix.sandbox }}'
|
||||
runs-on: 'gemini-cli-ubuntu-16-core'
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
sandbox:
|
||||
- 'sandbox:none'
|
||||
- 'sandbox:docker'
|
||||
node-version:
|
||||
- '20.x'
|
||||
|
||||
steps:
|
||||
- name: 'Deflake'
|
||||
- name: 'Checkout'
|
||||
uses: 'actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955' # ratchet:actions/checkout@v5
|
||||
with:
|
||||
ref: '${{ github.event.pull_request.head.sha }}'
|
||||
repository: '${{ github.repository }}'
|
||||
|
||||
- name: 'Set up Node.js ${{ matrix.node-version }}'
|
||||
uses: 'actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020' # ratchet:actions-node@v4
|
||||
with:
|
||||
node-version: '${{ matrix.node-version }}'
|
||||
|
||||
- name: 'Install dependencies'
|
||||
run: 'npm ci'
|
||||
|
||||
- name: 'Build project'
|
||||
run: 'npm run build'
|
||||
|
||||
- name: 'Set up Docker'
|
||||
if: "matrix.sandbox == 'sandbox:docker'"
|
||||
uses: 'docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435' # ratchet:docker/setup-buildx-action@v3
|
||||
|
||||
- name: 'Run E2E tests'
|
||||
env:
|
||||
GEMINI_API_KEY: '${{ secrets.GEMINI_API_KEY }}'
|
||||
IS_DOCKER: "${{ matrix.sandbox == 'sandbox:docker' }}"
|
||||
KEEP_OUTPUT: 'true'
|
||||
RUNS: '${{ github.event.inputs.runs }}'
|
||||
TEST_NAME_PATTERN: '${{ github.event.inputs.test_name_pattern }}'
|
||||
VERBOSE: 'true'
|
||||
shell: 'bash'
|
||||
run: |
|
||||
ECHO 'DEFLAKE WORKFLOW'
|
||||
if [[ "${{ env.IS_DOCKER }}" == "true" ]]; then
|
||||
npm run deflake:test:integration:sandbox:docker -- --runs="${{ env.RUNS }}" -- --testNamePattern "'${{ env.TEST_NAME_PATTERN }}'"
|
||||
else
|
||||
npm run deflake:test:integration:sandbox:none -- --runs="${{ env.RUNS }}" -- --testNamePattern "'${{ env.TEST_NAME_PATTERN }}'"
|
||||
fi
|
||||
|
||||
deflake_e2e_mac:
|
||||
name: 'E2E Test (macOS)'
|
||||
runs-on: 'macos-latest'
|
||||
steps:
|
||||
- name: 'Checkout'
|
||||
uses: 'actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955' # ratchet:actions/checkout@v5
|
||||
with:
|
||||
ref: '${{ github.event.pull_request.head.sha }}'
|
||||
repository: '${{ github.repository }}'
|
||||
|
||||
- name: 'Set up Node.js 20.x'
|
||||
uses: 'actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020' # ratchet:actions-node@v4
|
||||
with:
|
||||
node-version: '20.x'
|
||||
|
||||
- name: 'Install dependencies'
|
||||
run: 'npm ci'
|
||||
|
||||
- name: 'Build project'
|
||||
run: 'npm run build'
|
||||
|
||||
- name: 'Fix rollup optional dependencies on macOS'
|
||||
if: "runner.os == 'macOS'"
|
||||
run: |
|
||||
npm cache clean --force
|
||||
- name: 'Run E2E tests (non-Windows)'
|
||||
if: "runner.os != 'Windows'"
|
||||
env:
|
||||
GEMINI_API_KEY: '${{ secrets.GEMINI_API_KEY }}'
|
||||
KEEP_OUTPUT: 'true'
|
||||
RUNS: '${{ github.event.inputs.runs }}'
|
||||
SANDBOX: 'sandbox:none'
|
||||
TEST_NAME_PATTERN: '${{ github.event.inputs.test_name_pattern }}'
|
||||
VERBOSE: 'true'
|
||||
run: |
|
||||
npm run deflake:test:integration:sandbox:none -- --runs="${{ env.RUNS }}" -- --testNamePattern "'${{ env.TEST_NAME_PATTERN }}'"
|
||||
|
||||
deflake_e2e_windows:
|
||||
name: 'Slow E2E - Win'
|
||||
runs-on: 'gemini-cli-windows-16-core'
|
||||
|
||||
steps:
|
||||
- name: 'Checkout'
|
||||
uses: 'actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955' # ratchet:actions/checkout@v5
|
||||
with:
|
||||
ref: '${{ github.event.pull_request.head.sha }}'
|
||||
repository: '${{ github.repository }}'
|
||||
|
||||
- name: 'Set up Node.js 20.x'
|
||||
uses: 'actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020' # ratchet:actions-node@v4
|
||||
with:
|
||||
node-version: '20.x'
|
||||
cache: 'npm'
|
||||
|
||||
- name: 'Configure Windows Defender exclusions'
|
||||
run: |
|
||||
Add-MpPreference -ExclusionPath $env:GITHUB_WORKSPACE -Force
|
||||
Add-MpPreference -ExclusionPath "$env:GITHUB_WORKSPACE\node_modules" -Force
|
||||
Add-MpPreference -ExclusionPath "$env:GITHUB_WORKSPACE\packages" -Force
|
||||
Add-MpPreference -ExclusionPath "$env:TEMP" -Force
|
||||
shell: 'pwsh'
|
||||
|
||||
- name: 'Configure npm for Windows performance'
|
||||
run: |
|
||||
npm config set progress false
|
||||
npm config set audit false
|
||||
npm config set fund false
|
||||
npm config set loglevel error
|
||||
npm config set maxsockets 32
|
||||
npm config set registry https://registry.npmjs.org/
|
||||
shell: 'pwsh'
|
||||
|
||||
- name: 'Install dependencies'
|
||||
run: 'npm ci'
|
||||
shell: 'pwsh'
|
||||
|
||||
- name: 'Build project'
|
||||
run: 'npm run build'
|
||||
shell: 'pwsh'
|
||||
|
||||
- name: 'Run E2E tests'
|
||||
env:
|
||||
GEMINI_API_KEY: '${{ secrets.GEMINI_API_KEY }}'
|
||||
KEEP_OUTPUT: 'true'
|
||||
SANDBOX: 'sandbox:none'
|
||||
VERBOSE: 'true'
|
||||
NODE_OPTIONS: '--max-old-space-size=32768 --max-semi-space-size=256'
|
||||
UV_THREADPOOL_SIZE: '32'
|
||||
NODE_ENV: 'test'
|
||||
RUNS: '${{ github.event.inputs.runs }}'
|
||||
TEST_NAME_PATTERN: '${{ github.event.inputs.test_name_pattern }}'
|
||||
shell: 'pwsh'
|
||||
run: |
|
||||
npm run deflake:test:integration:sandbox:none -- --runs="${{ env.RUNS }}" -- --testNamePattern "'${{ env.TEST_NAME_PATTERN }}'"
|
||||
|
||||
@@ -275,7 +275,7 @@ contain other project-specific files related to Gemini CLI's operation, such as:
|
||||
|
||||
- **`telemetry`** (object)
|
||||
- **Description:** Configures logging and metrics collection for Gemini CLI.
|
||||
For more information, see [Telemetry](../telemetry.md).
|
||||
For more information, see [Telemetry](./telemetry.md).
|
||||
- **Default:**
|
||||
`{"enabled": false, "target": "local", "otlpEndpoint": "http://localhost:4317", "logPrompts": true}`
|
||||
- **Properties:**
|
||||
|
||||
@@ -202,6 +202,26 @@ allowlisting with `coreTools`, as it relies on blocking known-bad commands, and
|
||||
clever users may find ways to bypass simple string-based blocks. **Allowlisting
|
||||
is the recommended approach.**
|
||||
|
||||
### Disabling YOLO Mode
|
||||
|
||||
To ensure that users cannot bypass the confirmation prompt for tool execution,
|
||||
you can disable YOLO mode at the policy level. This adds a critical layer of
|
||||
safety, as it prevents the model from executing tools without explicit user
|
||||
approval.
|
||||
|
||||
**Example:** Force all tool executions to require user confirmation.
|
||||
|
||||
```json
|
||||
{
|
||||
"security": {
|
||||
"disableYoloMode": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
This setting is highly recommended in an enterprise environment to prevent
|
||||
unintended tool execution.
|
||||
|
||||
## Managing Custom Tools (MCP Servers)
|
||||
|
||||
If your organization uses custom tools via
|
||||
|
||||
@@ -59,12 +59,20 @@ npm run test:e2e -- --test-name-pattern "reads a file"
|
||||
### Deflaking a test
|
||||
|
||||
Before adding a **new** integration test, you should test it at least 5 times
|
||||
with the deflake script to make sure that it is not flaky.
|
||||
with the deflake script or workflow to make sure that it is not flaky.
|
||||
|
||||
### Deflake script
|
||||
|
||||
```bash
|
||||
npm run deflake -- --runs=5 --command="npm run test:e2e -- -- --test-name-pattern '<your-new-test-name>'"
|
||||
```
|
||||
|
||||
#### Deflake Workflow
|
||||
|
||||
```bash
|
||||
gh workflow run deflake.yml --ref <your-branch> -f test_name_pattern="<your-test-name-pattern>"
|
||||
```
|
||||
|
||||
### Running all tests
|
||||
|
||||
To run the entire suite of integration tests, use the following command:
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
prompt = """
|
||||
Please summarize the findings for the pattern `{{args}}`.
|
||||
|
||||
Search Results:
|
||||
!{grep -r {{args}} .}
|
||||
"""
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"name": "custom-commands",
|
||||
"version": "1.0.0"
|
||||
}
|
||||
@@ -20,7 +20,16 @@ describe('Interactive file system', () => {
|
||||
|
||||
it('should perform a read-then-write sequence', async () => {
|
||||
const fileName = 'version.txt';
|
||||
await rig.setup('interactive-read-then-write');
|
||||
await rig.setup('interactive-read-then-write', {
|
||||
settings: {
|
||||
security: {
|
||||
auth: {
|
||||
selectedType: 'gemini-api-key',
|
||||
},
|
||||
disableYoloMode: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
rig.createFile(fileName, '1.0.0');
|
||||
|
||||
const run = await rig.runInteractive();
|
||||
|
||||
Generated
+7
-7
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@google/gemini-cli",
|
||||
"version": "0.11.0-nightly.20251021.e72c00cf",
|
||||
"version": "0.12.0-nightly.20251022.0542de95",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@google/gemini-cli",
|
||||
"version": "0.11.0-nightly.20251021.e72c00cf",
|
||||
"version": "0.12.0-nightly.20251022.0542de95",
|
||||
"workspaces": [
|
||||
"packages/*"
|
||||
],
|
||||
@@ -17694,7 +17694,7 @@
|
||||
},
|
||||
"packages/a2a-server": {
|
||||
"name": "@google/gemini-cli-a2a-server",
|
||||
"version": "0.11.0-nightly.20251021.e72c00cf",
|
||||
"version": "0.12.0-nightly.20251022.0542de95",
|
||||
"dependencies": {
|
||||
"@a2a-js/sdk": "^0.3.2",
|
||||
"@google-cloud/storage": "^7.16.0",
|
||||
@@ -17968,7 +17968,7 @@
|
||||
},
|
||||
"packages/cli": {
|
||||
"name": "@google/gemini-cli",
|
||||
"version": "0.11.0-nightly.20251021.e72c00cf",
|
||||
"version": "0.12.0-nightly.20251022.0542de95",
|
||||
"dependencies": {
|
||||
"@google/gemini-cli-core": "file:../core",
|
||||
"@google/genai": "1.16.0",
|
||||
@@ -18081,7 +18081,7 @@
|
||||
},
|
||||
"packages/core": {
|
||||
"name": "@google/gemini-cli-core",
|
||||
"version": "0.11.0-nightly.20251021.e72c00cf",
|
||||
"version": "0.12.0-nightly.20251022.0542de95",
|
||||
"dependencies": {
|
||||
"@google-cloud/logging": "^11.2.1",
|
||||
"@google-cloud/opentelemetry-cloud-monitoring-exporter": "^0.21.0",
|
||||
@@ -18222,7 +18222,7 @@
|
||||
},
|
||||
"packages/test-utils": {
|
||||
"name": "@google/gemini-cli-test-utils",
|
||||
"version": "0.11.0-nightly.20251021.e72c00cf",
|
||||
"version": "0.12.0-nightly.20251022.0542de95",
|
||||
"license": "Apache-2.0",
|
||||
"devDependencies": {
|
||||
"typescript": "^5.3.3"
|
||||
@@ -18233,7 +18233,7 @@
|
||||
},
|
||||
"packages/vscode-ide-companion": {
|
||||
"name": "gemini-cli-vscode-ide-companion",
|
||||
"version": "0.11.0-nightly.20251021.e72c00cf",
|
||||
"version": "0.12.0-nightly.20251022.0542de95",
|
||||
"license": "LICENSE",
|
||||
"dependencies": {
|
||||
"@modelcontextprotocol/sdk": "^1.15.1",
|
||||
|
||||
+7
-7
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@google/gemini-cli",
|
||||
"version": "0.11.0-nightly.20251021.e72c00cf",
|
||||
"version": "0.12.0-nightly.20251022.0542de95",
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
},
|
||||
@@ -13,15 +13,15 @@
|
||||
"url": "git+https://github.com/google-gemini/gemini-cli.git"
|
||||
},
|
||||
"config": {
|
||||
"sandboxImageUri": "us-docker.pkg.dev/gemini-code-dev/gemini-cli/sandbox:0.11.0-nightly.20251021.e72c00cf"
|
||||
"sandboxImageUri": "us-docker.pkg.dev/gemini-code-dev/gemini-cli/sandbox:0.12.0-nightly.20251022.0542de95"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "cross-env NODE_ENV=development node scripts/start.js",
|
||||
"start:a2a-server": "CODER_AGENT_PORT=41242 npm run start --workspace @google/gemini-cli-a2a-server",
|
||||
"debug": "cross-env DEBUG=1 node --inspect-brk scripts/start.js",
|
||||
"deflake": "node scripts/deflake.js",
|
||||
"deflake:test:integration:sandbox:none": "npm run deflake -- --command=\"npm run test:integration:sandbox:none -- --retry=0",
|
||||
"deflake:test:integration:sandbox:docker": "npm run deflake -- --command=\"npm run test:integration:sandbox:docker -- --retry=0",
|
||||
"deflake:test:integration:sandbox:none": "npm run deflake -- --command=\"npm run test:integration:sandbox:none -- --retry=0\"",
|
||||
"deflake:test:integration:sandbox:docker": "npm run deflake -- --command=\"npm run test:integration:sandbox:docker -- --retry=0\"",
|
||||
"auth:npm": "npx google-artifactregistry-auth",
|
||||
"auth:docker": "gcloud auth configure-docker us-west1-docker.pkg.dev",
|
||||
"auth": "npm run auth:npm && npm run auth:docker",
|
||||
@@ -41,9 +41,9 @@
|
||||
"test:integration:sandbox:none": "cross-env GEMINI_SANDBOX=false vitest run --root ./integration-tests",
|
||||
"test:integration:sandbox:docker": "cross-env GEMINI_SANDBOX=docker npm run build:sandbox && cross-env GEMINI_SANDBOX=docker vitest run --root ./integration-tests",
|
||||
"test:integration:sandbox:podman": "cross-env GEMINI_SANDBOX=podman vitest run --root ./integration-tests",
|
||||
"lint": "eslint . --ext .ts,.tsx && eslint integration-tests",
|
||||
"lint:fix": "eslint . --fix && eslint integration-tests --fix",
|
||||
"lint:ci": "eslint . --ext .ts,.tsx --max-warnings 0 && eslint integration-tests --max-warnings 0",
|
||||
"lint": "eslint . --ext .ts,.tsx && eslint integration-tests && eslint scripts",
|
||||
"lint:fix": "eslint . --fix --ext .ts,.tsx && eslint integration-tests --fix && eslint scripts --fix && npm run format",
|
||||
"lint:ci": "npm run lint:all",
|
||||
"lint:all": "node scripts/lint.js",
|
||||
"format": "prettier --experimental-cli --write .",
|
||||
"typecheck": "npm run typecheck --workspaces --if-present",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@google/gemini-cli-a2a-server",
|
||||
"version": "0.11.0-nightly.20251021.e72c00cf",
|
||||
"version": "0.12.0-nightly.20251022.0542de95",
|
||||
"description": "Gemini CLI A2A Server",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@google/gemini-cli",
|
||||
"version": "0.11.0-nightly.20251021.e72c00cf",
|
||||
"version": "0.12.0-nightly.20251022.0542de95",
|
||||
"description": "Gemini CLI",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -25,7 +25,7 @@
|
||||
"dist"
|
||||
],
|
||||
"config": {
|
||||
"sandboxImageUri": "us-docker.pkg.dev/gemini-code-dev/gemini-cli/sandbox:0.11.0-nightly.20251021.e72c00cf"
|
||||
"sandboxImageUri": "us-docker.pkg.dev/gemini-code-dev/gemini-cli/sandbox:0.12.0-nightly.20251022.0542de95"
|
||||
},
|
||||
"dependencies": {
|
||||
"@google/gemini-cli-core": "file:../core",
|
||||
|
||||
@@ -16,6 +16,7 @@ import { newCommand } from './extensions/new.js';
|
||||
|
||||
export const extensionsCommand: CommandModule = {
|
||||
command: 'extensions <command>',
|
||||
aliases: ['extension'],
|
||||
describe: 'Manage Gemini CLI extensions.',
|
||||
builder: (yargs) =>
|
||||
yargs
|
||||
|
||||
@@ -1112,6 +1112,23 @@ describe('Approval mode tool exclusion logic', () => {
|
||||
expect(excludedTools).not.toContain(WRITE_FILE_TOOL_NAME); // Should be allowed in auto_edit
|
||||
});
|
||||
|
||||
it('should throw an error if YOLO mode is attempted when disableYoloMode is true', async () => {
|
||||
process.argv = ['node', 'script.js', '--yolo'];
|
||||
const argv = await parseArguments({} as Settings);
|
||||
const settings: Settings = {
|
||||
security: {
|
||||
disableYoloMode: true,
|
||||
},
|
||||
};
|
||||
const extensions: GeminiCLIExtension[] = [];
|
||||
|
||||
await expect(
|
||||
loadCliConfig(settings, extensions, 'test-session', argv),
|
||||
).rejects.toThrow(
|
||||
'Cannot start in YOLO mode when it is disabled by settings',
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw an error for invalid approval mode values in loadCliConfig', async () => {
|
||||
// Create a mock argv with an invalid approval mode that bypasses argument parsing validation
|
||||
const invalidArgv: Partial<CliArgs> & { approvalMode: string } = {
|
||||
|
||||
@@ -442,6 +442,21 @@ export async function loadCliConfig(
|
||||
argv.yolo || false ? ApprovalMode.YOLO : ApprovalMode.DEFAULT;
|
||||
}
|
||||
|
||||
// Override approval mode if disableYoloMode is set.
|
||||
if (settings.security?.disableYoloMode) {
|
||||
if (approvalMode === ApprovalMode.YOLO) {
|
||||
debugLogger.error('YOLO mode is disabled by the "disableYolo" setting.');
|
||||
throw new FatalConfigError(
|
||||
'Cannot start in YOLO mode when it is disabled by settings',
|
||||
);
|
||||
}
|
||||
approvalMode = ApprovalMode.DEFAULT;
|
||||
} else if (approvalMode === ApprovalMode.YOLO) {
|
||||
debugLogger.warn(
|
||||
'YOLO mode is enabled. All tool calls will be automatically approved.',
|
||||
);
|
||||
}
|
||||
|
||||
// Force approval mode to default if the folder is not trusted.
|
||||
if (!trustedFolder && approvalMode !== ApprovalMode.DEFAULT) {
|
||||
debugLogger.warn(
|
||||
@@ -583,6 +598,7 @@ export async function loadCliConfig(
|
||||
geminiMdFileCount: fileCount,
|
||||
geminiMdFilePaths: filePaths,
|
||||
approvalMode,
|
||||
disableYoloMode: settings.security?.disableYoloMode,
|
||||
showMemoryUsage: settings.ui?.showMemoryUsage || false,
|
||||
accessibility: {
|
||||
...settings.ui?.accessibility,
|
||||
|
||||
@@ -609,6 +609,40 @@ describe('Settings Loading and Merging', () => {
|
||||
expect(settings.merged.security?.folderTrust?.enabled).toBe(true); // System setting should be used
|
||||
});
|
||||
|
||||
it('should not allow user or workspace to override system disableYoloMode', () => {
|
||||
(mockFsExistsSync as Mock).mockReturnValue(true);
|
||||
const userSettingsContent = {
|
||||
security: {
|
||||
disableYoloMode: false,
|
||||
},
|
||||
};
|
||||
const workspaceSettingsContent = {
|
||||
security: {
|
||||
disableYoloMode: false, // This should be ignored
|
||||
},
|
||||
};
|
||||
const systemSettingsContent = {
|
||||
security: {
|
||||
disableYoloMode: true,
|
||||
},
|
||||
};
|
||||
|
||||
(fs.readFileSync as Mock).mockImplementation(
|
||||
(p: fs.PathOrFileDescriptor) => {
|
||||
if (p === getSystemSettingsPath())
|
||||
return JSON.stringify(systemSettingsContent);
|
||||
if (p === USER_SETTINGS_PATH)
|
||||
return JSON.stringify(userSettingsContent);
|
||||
if (p === MOCK_WORKSPACE_SETTINGS_PATH)
|
||||
return JSON.stringify(workspaceSettingsContent);
|
||||
return '{}';
|
||||
},
|
||||
);
|
||||
|
||||
const settings = loadSettings(MOCK_WORKSPACE_DIR);
|
||||
expect(settings.merged.security?.disableYoloMode).toBe(true); // System setting should be used
|
||||
});
|
||||
|
||||
it('should handle contextFileName correctly when only in user settings', () => {
|
||||
(mockFsExistsSync as Mock).mockImplementation(
|
||||
(p: fs.PathLike) => p === USER_SETTINGS_PATH,
|
||||
|
||||
@@ -937,6 +937,15 @@ const SETTINGS_SCHEMA = {
|
||||
description: 'Security-related settings.',
|
||||
showInDialog: false,
|
||||
properties: {
|
||||
disableYoloMode: {
|
||||
type: 'boolean',
|
||||
label: 'Disable YOLO Mode',
|
||||
category: 'Security',
|
||||
requiresRestart: true,
|
||||
default: false,
|
||||
description: 'Disable YOLO mode, even if enabled by a flag.',
|
||||
showInDialog: true,
|
||||
},
|
||||
folderTrust: {
|
||||
type: 'object',
|
||||
label: 'Folder Trust',
|
||||
|
||||
@@ -254,7 +254,7 @@ describe('BaseSelectionList', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe.skip('Scrolling and Pagination (maxItemsToShow)', () => {
|
||||
describe('Scrolling and Pagination (maxItemsToShow)', () => {
|
||||
const longList = Array.from({ length: 10 }, (_, i) => ({
|
||||
value: `Item ${i + 1}`,
|
||||
label: `Item ${i + 1}`,
|
||||
@@ -323,11 +323,13 @@ describe('BaseSelectionList', () => {
|
||||
// New visible window should be Items 2, 3, 4 (scroll offset 1).
|
||||
await updateActiveIndex(3);
|
||||
|
||||
const output = lastFrame();
|
||||
expect(output).not.toContain('Item 1');
|
||||
expect(output).toContain('Item 2');
|
||||
expect(output).toContain('Item 4');
|
||||
expect(output).not.toContain('Item 5');
|
||||
await waitFor(() => {
|
||||
const output = lastFrame();
|
||||
expect(output).not.toContain('Item 1');
|
||||
expect(output).toContain('Item 2');
|
||||
expect(output).toContain('Item 4');
|
||||
expect(output).not.toContain('Item 5');
|
||||
});
|
||||
});
|
||||
|
||||
it('should scroll up when activeIndex moves before the visible window', async () => {
|
||||
@@ -335,19 +337,23 @@ describe('BaseSelectionList', () => {
|
||||
|
||||
await updateActiveIndex(4);
|
||||
|
||||
let output = lastFrame();
|
||||
expect(output).toContain('Item 3'); // Should see items 3, 4, 5
|
||||
expect(output).toContain('Item 5');
|
||||
expect(output).not.toContain('Item 2');
|
||||
await waitFor(() => {
|
||||
const output = lastFrame();
|
||||
expect(output).toContain('Item 3'); // Should see items 3, 4, 5
|
||||
expect(output).toContain('Item 5');
|
||||
expect(output).not.toContain('Item 2');
|
||||
});
|
||||
|
||||
// Now test scrolling up: move to index 1 (Item 2)
|
||||
// This should trigger scroll up to show items 2, 3, 4
|
||||
await updateActiveIndex(1);
|
||||
|
||||
output = lastFrame();
|
||||
expect(output).toContain('Item 2');
|
||||
expect(output).toContain('Item 4');
|
||||
expect(output).not.toContain('Item 5'); // Item 5 should no longer be visible
|
||||
await waitFor(() => {
|
||||
const output = lastFrame();
|
||||
expect(output).toContain('Item 2');
|
||||
expect(output).toContain('Item 4');
|
||||
expect(output).not.toContain('Item 5'); // Item 5 should no longer be visible
|
||||
});
|
||||
});
|
||||
|
||||
it('should pin the scroll offset to the end if selection starts near the end', async () => {
|
||||
@@ -375,16 +381,19 @@ describe('BaseSelectionList', () => {
|
||||
expect(lastFrame()).toContain('Item 1');
|
||||
|
||||
await updateActiveIndex(3); // Should trigger scroll
|
||||
let output = lastFrame();
|
||||
expect(output).toContain('Item 2');
|
||||
expect(output).toContain('Item 4');
|
||||
expect(output).not.toContain('Item 1');
|
||||
|
||||
await waitFor(() => {
|
||||
const output = lastFrame();
|
||||
expect(output).toContain('Item 2');
|
||||
expect(output).toContain('Item 4');
|
||||
expect(output).not.toContain('Item 1');
|
||||
});
|
||||
await updateActiveIndex(5); // Scroll further
|
||||
output = lastFrame();
|
||||
expect(output).toContain('Item 4');
|
||||
expect(output).toContain('Item 6');
|
||||
expect(output).not.toContain('Item 3');
|
||||
await waitFor(() => {
|
||||
const output = lastFrame();
|
||||
expect(output).toContain('Item 4');
|
||||
expect(output).toContain('Item 6');
|
||||
expect(output).not.toContain('Item 3');
|
||||
});
|
||||
});
|
||||
|
||||
it('should correctly identify the selected item within the visible window', () => {
|
||||
@@ -412,17 +421,16 @@ describe('BaseSelectionList', () => {
|
||||
expect.objectContaining({ value: 'Item 6' }),
|
||||
expect.objectContaining({ isSelected: true }),
|
||||
);
|
||||
});
|
||||
|
||||
// Item 4 (index 3) should not be selected
|
||||
expect(mockRenderItem).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ value: 'Item 4' }),
|
||||
expect.objectContaining({ isSelected: false }),
|
||||
);
|
||||
// Item 4 (index 3) should not be selected
|
||||
expect(mockRenderItem).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ value: 'Item 4' }),
|
||||
expect.objectContaining({ isSelected: false }),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle maxItemsToShow larger than the list length', () => {
|
||||
// Test edge case where maxItemsToShow exceeds available items
|
||||
const { lastFrame } = renderComponent(
|
||||
{ items: longList, maxItemsToShow: 15 },
|
||||
0,
|
||||
|
||||
@@ -953,6 +953,40 @@ describe('useTextBuffer', () => {
|
||||
expect(getBufferState(result).lines).toEqual(['', '']);
|
||||
});
|
||||
|
||||
it('should do nothing for a tab key press', () => {
|
||||
const { result } = renderHook(() =>
|
||||
useTextBuffer({ viewport, isValidPath: () => false }),
|
||||
);
|
||||
act(() =>
|
||||
result.current.handleInput({
|
||||
name: 'tab',
|
||||
ctrl: false,
|
||||
meta: false,
|
||||
shift: false,
|
||||
paste: false,
|
||||
sequence: '\t',
|
||||
}),
|
||||
);
|
||||
expect(getBufferState(result).text).toBe('');
|
||||
});
|
||||
|
||||
it('should do nothing for a shift tab key press', () => {
|
||||
const { result } = renderHook(() =>
|
||||
useTextBuffer({ viewport, isValidPath: () => false }),
|
||||
);
|
||||
act(() =>
|
||||
result.current.handleInput({
|
||||
name: 'tab',
|
||||
ctrl: false,
|
||||
meta: false,
|
||||
shift: true,
|
||||
paste: false,
|
||||
sequence: '\u001b[9;2u',
|
||||
}),
|
||||
);
|
||||
expect(getBufferState(result).text).toBe('');
|
||||
});
|
||||
|
||||
it('should handle "Backspace" key', () => {
|
||||
const { result } = renderHook(() =>
|
||||
useTextBuffer({
|
||||
|
||||
@@ -1933,7 +1933,7 @@ export function useTextBuffer({
|
||||
else if (key.name === 'delete' || (key.ctrl && key.name === 'd')) del();
|
||||
else if (key.ctrl && !key.shift && key.name === 'z') undo();
|
||||
else if (key.ctrl && key.shift && key.name === 'z') redo();
|
||||
else if (input && !key.ctrl && !key.meta) {
|
||||
else if (input && !key.ctrl && !key.meta && key.name !== 'tab') {
|
||||
insert(input, { paste: key.paste });
|
||||
}
|
||||
},
|
||||
|
||||
@@ -37,6 +37,7 @@ vi.mock('@google/gemini-cli-core', async () => {
|
||||
interface MockConfigInstanceShape {
|
||||
getApprovalMode: Mock<() => ApprovalMode>;
|
||||
setApprovalMode: Mock<(value: ApprovalMode) => void>;
|
||||
isYoloModeDisabled: Mock<() => boolean>;
|
||||
isTrustedFolder: Mock<() => boolean>;
|
||||
getCoreTools: Mock<() => string[]>;
|
||||
getToolDiscoveryCommand: Mock<() => string | undefined>;
|
||||
@@ -76,6 +77,7 @@ describe('useAutoAcceptIndicator', () => {
|
||||
setApprovalMode: instanceSetApprovalModeMock as Mock<
|
||||
(value: ApprovalMode) => void
|
||||
>,
|
||||
isYoloModeDisabled: vi.fn().mockReturnValue(false),
|
||||
isTrustedFolder: vi.fn().mockReturnValue(true) as Mock<() => boolean>,
|
||||
getCoreTools: vi.fn().mockReturnValue([]) as Mock<() => string[]>,
|
||||
getToolDiscoveryCommand: vi.fn().mockReturnValue(undefined) as Mock<
|
||||
@@ -471,6 +473,45 @@ describe('useAutoAcceptIndicator', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('when YOLO mode is disabled by settings', () => {
|
||||
beforeEach(() => {
|
||||
// Ensure isYoloModeDisabled returns true for these tests
|
||||
if (mockConfigInstance && mockConfigInstance.isYoloModeDisabled) {
|
||||
mockConfigInstance.isYoloModeDisabled.mockReturnValue(true);
|
||||
}
|
||||
});
|
||||
|
||||
it('should not enable YOLO mode when Ctrl+Y is pressed and add an info message', () => {
|
||||
mockConfigInstance.getApprovalMode.mockReturnValue(ApprovalMode.DEFAULT);
|
||||
const mockAddItem = vi.fn();
|
||||
const { result } = renderHook(() =>
|
||||
useAutoAcceptIndicator({
|
||||
config: mockConfigInstance as unknown as ActualConfigType,
|
||||
addItem: mockAddItem,
|
||||
}),
|
||||
);
|
||||
|
||||
expect(result.current).toBe(ApprovalMode.DEFAULT);
|
||||
|
||||
act(() => {
|
||||
capturedUseKeypressHandler({ name: 'y', ctrl: true } as Key);
|
||||
});
|
||||
|
||||
// setApprovalMode should not be called because the check should return early
|
||||
expect(mockConfigInstance.setApprovalMode).not.toHaveBeenCalled();
|
||||
// An info message should be added
|
||||
expect(mockAddItem).toHaveBeenCalledWith(
|
||||
{
|
||||
type: MessageType.WARNING,
|
||||
text: 'You cannot enter YOLO mode since it is disabled in your settings.',
|
||||
},
|
||||
expect.any(Number),
|
||||
);
|
||||
// The mode should not change
|
||||
expect(result.current).toBe(ApprovalMode.DEFAULT);
|
||||
});
|
||||
});
|
||||
|
||||
it('should call onApprovalModeChange when switching to YOLO mode', () => {
|
||||
mockConfigInstance.getApprovalMode.mockReturnValue(ApprovalMode.DEFAULT);
|
||||
|
||||
|
||||
@@ -34,6 +34,21 @@ export function useAutoAcceptIndicator({
|
||||
let nextApprovalMode: ApprovalMode | undefined;
|
||||
|
||||
if (key.ctrl && key.name === 'y') {
|
||||
if (
|
||||
config.isYoloModeDisabled() &&
|
||||
config.getApprovalMode() !== ApprovalMode.YOLO
|
||||
) {
|
||||
if (addItem) {
|
||||
addItem(
|
||||
{
|
||||
type: MessageType.WARNING,
|
||||
text: 'You cannot enter YOLO mode since it is disabled in your settings.',
|
||||
},
|
||||
Date.now(),
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
nextApprovalMode =
|
||||
config.getApprovalMode() === ApprovalMode.YOLO
|
||||
? ApprovalMode.DEFAULT
|
||||
|
||||
@@ -32,7 +32,6 @@ import {
|
||||
ApprovalMode,
|
||||
MockTool,
|
||||
} from '@google/gemini-cli-core';
|
||||
import type { HistoryItemWithoutId, HistoryItemToolGroup } from '../types.js';
|
||||
import { ToolCallStatus } from '../types.js';
|
||||
|
||||
// Mocks
|
||||
@@ -101,11 +100,9 @@ const mockToolRequiresConfirmation = new MockTool({
|
||||
|
||||
describe('useReactToolScheduler in YOLO Mode', () => {
|
||||
let onComplete: Mock;
|
||||
let setPendingHistoryItem: Mock;
|
||||
|
||||
beforeEach(() => {
|
||||
onComplete = vi.fn();
|
||||
setPendingHistoryItem = vi.fn();
|
||||
mockToolRegistry.getTool.mockClear();
|
||||
(mockToolRequiresConfirmation.execute as Mock).mockClear();
|
||||
(mockToolRequiresConfirmation.shouldConfirmExecute as Mock).mockClear();
|
||||
@@ -128,7 +125,7 @@ describe('useReactToolScheduler in YOLO Mode', () => {
|
||||
useReactToolScheduler(
|
||||
onComplete,
|
||||
mockConfig as unknown as Config,
|
||||
setPendingHistoryItem,
|
||||
() => undefined,
|
||||
() => {},
|
||||
),
|
||||
);
|
||||
@@ -187,26 +184,11 @@ describe('useReactToolScheduler in YOLO Mode', () => {
|
||||
}),
|
||||
}),
|
||||
]);
|
||||
|
||||
// Ensure no confirmation UI was triggered (setPendingHistoryItem should not have been called with confirmation details)
|
||||
const setPendingHistoryItemCalls = setPendingHistoryItem.mock.calls;
|
||||
const confirmationCall = setPendingHistoryItemCalls.find((call) => {
|
||||
const item = typeof call[0] === 'function' ? call[0]({}) : call[0];
|
||||
return item?.tools?.[0]?.confirmationDetails;
|
||||
});
|
||||
expect(confirmationCall).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('useReactToolScheduler', () => {
|
||||
// TODO(ntaylormullen): The following tests are skipped due to difficulties in
|
||||
// reliably testing the asynchronous state updates and interactions with timers.
|
||||
// These tests involve complex sequences of events, including confirmations,
|
||||
// live output updates, and cancellations, which are challenging to assert
|
||||
// correctly with the current testing setup. Further investigation is needed
|
||||
// to find a robust way to test these scenarios.
|
||||
let onComplete: Mock;
|
||||
let setPendingHistoryItem: Mock;
|
||||
let capturedOnConfirmForTest:
|
||||
| ((outcome: ToolConfirmationOutcome) => void | Promise<void>)
|
||||
| undefined;
|
||||
@@ -214,29 +196,6 @@ describe('useReactToolScheduler', () => {
|
||||
beforeEach(() => {
|
||||
onComplete = vi.fn();
|
||||
capturedOnConfirmForTest = undefined;
|
||||
setPendingHistoryItem = vi.fn((updaterOrValue) => {
|
||||
let pendingItem: HistoryItemWithoutId | null = null;
|
||||
if (typeof updaterOrValue === 'function') {
|
||||
// Loosen the type for prevState to allow for more flexible updates in tests
|
||||
const prevState: Partial<HistoryItemToolGroup> = {
|
||||
type: 'tool_group', // Still default to tool_group for most cases
|
||||
tools: [],
|
||||
};
|
||||
|
||||
pendingItem = updaterOrValue(prevState as any); // Allow any for more flexibility
|
||||
} else {
|
||||
pendingItem = updaterOrValue;
|
||||
}
|
||||
// Capture onConfirm if it exists, regardless of the exact type of pendingItem
|
||||
// This is a common pattern in these tests.
|
||||
if (
|
||||
(pendingItem as HistoryItemToolGroup)?.tools?.[0]?.confirmationDetails
|
||||
?.onConfirm
|
||||
) {
|
||||
capturedOnConfirmForTest = (pendingItem as HistoryItemToolGroup)
|
||||
.tools[0].confirmationDetails?.onConfirm;
|
||||
}
|
||||
});
|
||||
|
||||
mockToolRegistry.getTool.mockClear();
|
||||
(mockTool.execute as Mock).mockClear();
|
||||
@@ -273,7 +232,7 @@ describe('useReactToolScheduler', () => {
|
||||
useReactToolScheduler(
|
||||
onComplete,
|
||||
mockConfig as unknown as Config,
|
||||
setPendingHistoryItem,
|
||||
() => undefined,
|
||||
() => {},
|
||||
),
|
||||
);
|
||||
@@ -448,7 +407,7 @@ describe('useReactToolScheduler', () => {
|
||||
expect(result.current[0]).toEqual([]);
|
||||
});
|
||||
|
||||
it.skip('should handle tool requiring confirmation - approved', async () => {
|
||||
it('should handle tool requiring confirmation - approved', async () => {
|
||||
mockToolRegistry.getTool.mockReturnValue(mockToolRequiresConfirmation);
|
||||
const expectedOutput = 'Confirmed output';
|
||||
(mockToolRequiresConfirmation.execute as Mock).mockResolvedValue({
|
||||
@@ -471,7 +430,9 @@ describe('useReactToolScheduler', () => {
|
||||
await vi.runAllTimersAsync();
|
||||
});
|
||||
|
||||
expect(setPendingHistoryItem).toHaveBeenCalled();
|
||||
const waitingCall = result.current[0][0] as any;
|
||||
expect(waitingCall.status).toBe('awaiting_approval');
|
||||
capturedOnConfirmForTest = waitingCall.confirmationDetails?.onConfirm;
|
||||
expect(capturedOnConfirmForTest).toBeDefined();
|
||||
|
||||
await act(async () => {
|
||||
@@ -510,7 +471,7 @@ describe('useReactToolScheduler', () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it.skip('should handle tool requiring confirmation - cancelled by user', async () => {
|
||||
it('should handle tool requiring confirmation - cancelled by user', async () => {
|
||||
mockToolRegistry.getTool.mockReturnValue(mockToolRequiresConfirmation);
|
||||
const { result } = renderScheduler();
|
||||
const schedule = result.current[1];
|
||||
@@ -527,7 +488,9 @@ describe('useReactToolScheduler', () => {
|
||||
await vi.runAllTimersAsync();
|
||||
});
|
||||
|
||||
expect(setPendingHistoryItem).toHaveBeenCalled();
|
||||
const waitingCall = result.current[0][0] as any;
|
||||
expect(waitingCall.status).toBe('awaiting_approval');
|
||||
capturedOnConfirmForTest = waitingCall.confirmationDetails?.onConfirm;
|
||||
expect(capturedOnConfirmForTest).toBeDefined();
|
||||
|
||||
await act(async () => {
|
||||
@@ -552,7 +515,8 @@ describe('useReactToolScheduler', () => {
|
||||
expect.objectContaining({
|
||||
functionResponse: expect.objectContaining({
|
||||
response: expect.objectContaining({
|
||||
error: `User did not allow tool call ${request.name}. Reason: User cancelled.`,
|
||||
error:
|
||||
'[Operation Cancelled] Reason: User did not allow tool call',
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
@@ -562,7 +526,7 @@ describe('useReactToolScheduler', () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it.skip('should handle live output updates', async () => {
|
||||
it('should handle live output updates', async () => {
|
||||
mockToolRegistry.getTool.mockReturnValue(mockToolWithLiveOutput);
|
||||
let liveUpdateFn: ((output: string) => void) | undefined;
|
||||
let resolveExecutePromise: (value: ToolResult) => void;
|
||||
@@ -600,7 +564,7 @@ describe('useReactToolScheduler', () => {
|
||||
});
|
||||
|
||||
expect(liveUpdateFn).toBeDefined();
|
||||
expect(setPendingHistoryItem).toHaveBeenCalled();
|
||||
expect(result.current[0][0].status).toBe('executing');
|
||||
|
||||
await act(async () => {
|
||||
liveUpdateFn?.('Live output 1');
|
||||
@@ -742,7 +706,7 @@ describe('useReactToolScheduler', () => {
|
||||
expect(result.current[0]).toEqual([]);
|
||||
});
|
||||
|
||||
it.skip('should throw error if scheduling while already running', async () => {
|
||||
it('should queue if scheduling while already running', async () => {
|
||||
mockToolRegistry.getTool.mockReturnValue(mockTool);
|
||||
const longExecutePromise = new Promise<ToolResult>((resolve) =>
|
||||
setTimeout(
|
||||
@@ -777,9 +741,7 @@ describe('useReactToolScheduler', () => {
|
||||
await vi.runAllTimersAsync();
|
||||
});
|
||||
|
||||
expect(() => schedule(request2, new AbortController().signal)).toThrow(
|
||||
'Cannot schedule tool calls while other tool calls are running',
|
||||
);
|
||||
schedule(request2, new AbortController().signal);
|
||||
|
||||
await act(async () => {
|
||||
await vi.advanceTimersByTimeAsync(50);
|
||||
@@ -795,6 +757,21 @@ describe('useReactToolScheduler', () => {
|
||||
response: expect.objectContaining({ resultDisplay: 'done display' }),
|
||||
}),
|
||||
]);
|
||||
// Wait for request2 to complete
|
||||
await act(async () => {
|
||||
await vi.advanceTimersByTimeAsync(50);
|
||||
await vi.runAllTimersAsync();
|
||||
await act(async () => {
|
||||
await vi.runAllTimersAsync();
|
||||
});
|
||||
});
|
||||
expect(onComplete).toHaveBeenCalledWith([
|
||||
expect.objectContaining({
|
||||
status: 'success',
|
||||
request: request2,
|
||||
response: expect.objectContaining({ resultDisplay: 'done display' }),
|
||||
}),
|
||||
]);
|
||||
expect(result.current[0]).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@google/gemini-cli-core",
|
||||
"version": "0.11.0-nightly.20251021.e72c00cf",
|
||||
"version": "0.12.0-nightly.20251022.0542de95",
|
||||
"description": "Gemini CLI Core",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
||||
@@ -995,6 +995,40 @@ describe('setApprovalMode with folder trust', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('isYoloModeDisabled', () => {
|
||||
const baseParams: ConfigParameters = {
|
||||
sessionId: 'test',
|
||||
targetDir: '.',
|
||||
debugMode: false,
|
||||
model: 'test-model',
|
||||
cwd: '.',
|
||||
};
|
||||
|
||||
it('should return false when yolo mode is not disabled and folder is trusted', () => {
|
||||
const config = new Config(baseParams);
|
||||
vi.spyOn(config, 'isTrustedFolder').mockReturnValue(true);
|
||||
expect(config.isYoloModeDisabled()).toBe(false);
|
||||
});
|
||||
|
||||
it('should return true when yolo mode is disabled by parameter', () => {
|
||||
const config = new Config({ ...baseParams, disableYoloMode: true });
|
||||
vi.spyOn(config, 'isTrustedFolder').mockReturnValue(true);
|
||||
expect(config.isYoloModeDisabled()).toBe(true);
|
||||
});
|
||||
|
||||
it('should return true when folder is untrusted', () => {
|
||||
const config = new Config(baseParams);
|
||||
vi.spyOn(config, 'isTrustedFolder').mockReturnValue(false);
|
||||
expect(config.isYoloModeDisabled()).toBe(true);
|
||||
});
|
||||
|
||||
it('should return true when yolo is disabled and folder is untrusted', () => {
|
||||
const config = new Config({ ...baseParams, disableYoloMode: true });
|
||||
vi.spyOn(config, 'isTrustedFolder').mockReturnValue(false);
|
||||
expect(config.isYoloModeDisabled()).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('BaseLlmClient Lifecycle', () => {
|
||||
const MODEL = 'gemini-pro';
|
||||
const SANDBOX: SandboxConfig = {
|
||||
|
||||
@@ -284,6 +284,7 @@ export interface ConfigParameters {
|
||||
retryFetchErrors?: boolean;
|
||||
enableShellOutputEfficiency?: boolean;
|
||||
ptyInfo?: string;
|
||||
disableYoloMode?: boolean;
|
||||
}
|
||||
|
||||
export class Config {
|
||||
@@ -380,6 +381,7 @@ export class Config {
|
||||
private readonly continueOnFailedApiCall: boolean;
|
||||
private readonly retryFetchErrors: boolean;
|
||||
private readonly enableShellOutputEfficiency: boolean;
|
||||
private readonly disableYoloMode: boolean;
|
||||
|
||||
constructor(params: ConfigParameters) {
|
||||
this.sessionId = params.sessionId;
|
||||
@@ -496,6 +498,7 @@ export class Config {
|
||||
format: params.output?.format ?? OutputFormat.TEXT,
|
||||
};
|
||||
this.retryFetchErrors = params.retryFetchErrors ?? false;
|
||||
this.disableYoloMode = params.disableYoloMode ?? false;
|
||||
|
||||
if (params.contextFileName) {
|
||||
setGeminiMdFilename(params.contextFileName);
|
||||
@@ -761,6 +764,10 @@ export class Config {
|
||||
this.approvalMode = mode;
|
||||
}
|
||||
|
||||
isYoloModeDisabled(): boolean {
|
||||
return this.disableYoloMode || !this.isTrustedFolder();
|
||||
}
|
||||
|
||||
getShowMemoryUsage(): boolean {
|
||||
return this.showMemoryUsage;
|
||||
}
|
||||
|
||||
@@ -297,14 +297,32 @@ describe('OAuthUtils', () => {
|
||||
const result = OAuthUtils.buildResourceParameter(
|
||||
'https://example.com/oauth/token',
|
||||
);
|
||||
expect(result).toBe('https://example.com');
|
||||
expect(result).toBe('https://example.com/oauth/token');
|
||||
});
|
||||
|
||||
it('should handle URLs with ports', () => {
|
||||
const result = OAuthUtils.buildResourceParameter(
|
||||
'https://example.com:8080/oauth/token',
|
||||
);
|
||||
expect(result).toBe('https://example.com:8080');
|
||||
expect(result).toBe('https://example.com:8080/oauth/token');
|
||||
});
|
||||
|
||||
it('should strip query parameters from the URL', () => {
|
||||
const result = OAuthUtils.buildResourceParameter(
|
||||
'https://example.com/api/v1/data?user=123&scope=read',
|
||||
);
|
||||
expect(result).toBe('https://example.com/api/v1/data');
|
||||
});
|
||||
|
||||
it('should strip URL fragments from the URL', () => {
|
||||
const result = OAuthUtils.buildResourceParameter(
|
||||
'https://example.com/api/v1/data#section-one',
|
||||
);
|
||||
expect(result).toBe('https://example.com/api/v1/data');
|
||||
});
|
||||
|
||||
it('should throw an error for invalid URLs', () => {
|
||||
expect(() => OAuthUtils.buildResourceParameter('not-a-url')).toThrow();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -360,6 +360,6 @@ export class OAuthUtils {
|
||||
*/
|
||||
static buildResourceParameter(endpointUrl: string): string {
|
||||
const url = new URL(endpointUrl);
|
||||
return `${url.protocol}//${url.host}`;
|
||||
return `${url.protocol}//${url.host}${url.pathname}`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -291,6 +291,54 @@ describe('ShellExecutionService', () => {
|
||||
expect(mockHeadlessTerminal.resize).toHaveBeenCalledWith(100, 40);
|
||||
});
|
||||
|
||||
it('should not resize the pty if it is not active', async () => {
|
||||
const isPtyActiveSpy = vi
|
||||
.spyOn(ShellExecutionService, 'isPtyActive')
|
||||
.mockReturnValue(false);
|
||||
|
||||
await simulateExecution('ls -l', (pty) => {
|
||||
ShellExecutionService.resizePty(pty.pid!, 100, 40);
|
||||
pty.onExit.mock.calls[0][0]({ exitCode: 0, signal: null });
|
||||
});
|
||||
|
||||
expect(mockPtyProcess.resize).not.toHaveBeenCalled();
|
||||
expect(mockHeadlessTerminal.resize).not.toHaveBeenCalled();
|
||||
isPtyActiveSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('should ignore errors when resizing an exited pty', async () => {
|
||||
const resizeError = new Error(
|
||||
'Cannot resize a pty that has already exited',
|
||||
);
|
||||
mockPtyProcess.resize.mockImplementation(() => {
|
||||
throw resizeError;
|
||||
});
|
||||
|
||||
// We don't expect this test to throw an error
|
||||
await expect(
|
||||
simulateExecution('ls -l', (pty) => {
|
||||
ShellExecutionService.resizePty(pty.pid!, 100, 40);
|
||||
pty.onExit.mock.calls[0][0]({ exitCode: 0, signal: null });
|
||||
}),
|
||||
).resolves.not.toThrow();
|
||||
|
||||
expect(mockPtyProcess.resize).toHaveBeenCalledWith(100, 40);
|
||||
});
|
||||
|
||||
it('should re-throw other errors during resize', async () => {
|
||||
const otherError = new Error('Some other error');
|
||||
mockPtyProcess.resize.mockImplementation(() => {
|
||||
throw otherError;
|
||||
});
|
||||
|
||||
await expect(
|
||||
simulateExecution('ls -l', (pty) => {
|
||||
ShellExecutionService.resizePty(pty.pid!, 100, 40);
|
||||
pty.onExit.mock.calls[0][0]({ exitCode: 0, signal: null });
|
||||
}),
|
||||
).rejects.toThrow('Some other error');
|
||||
});
|
||||
|
||||
it('should scroll the headless terminal', async () => {
|
||||
await simulateExecution('ls -l', (pty) => {
|
||||
pty.onData.mock.calls[0][0]('file1.txt\n');
|
||||
|
||||
@@ -750,7 +750,11 @@ export class ShellExecutionService {
|
||||
} catch (e) {
|
||||
// Ignore errors if the pty has already exited, which can happen
|
||||
// due to a race condition between the exit event and this call.
|
||||
if (e instanceof Error && 'code' in e && e.code === 'ESRCH') {
|
||||
if (
|
||||
e instanceof Error &&
|
||||
(('code' in e && e.code === 'ESRCH') ||
|
||||
e.message === 'Cannot resize a pty that has already exited')
|
||||
) {
|
||||
// ignore
|
||||
} else {
|
||||
throw e;
|
||||
|
||||
@@ -1269,8 +1269,6 @@ export class ClearcutLogger {
|
||||
}
|
||||
|
||||
getConfigJson() {
|
||||
const configJson = safeJsonStringifyBooleanValuesOnly(this.config);
|
||||
debugLogger.debug(configJson);
|
||||
return safeJsonStringifyBooleanValuesOnly(this.config);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@google/gemini-cli-test-utils",
|
||||
"version": "0.11.0-nightly.20251021.e72c00cf",
|
||||
"version": "0.12.0-nightly.20251022.0542de95",
|
||||
"private": true,
|
||||
"main": "src/index.ts",
|
||||
"license": "Apache-2.0",
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"name": "gemini-cli-vscode-ide-companion",
|
||||
"displayName": "Gemini CLI Companion",
|
||||
"description": "Enable Gemini CLI with direct access to your IDE workspace.",
|
||||
"version": "0.11.0-nightly.20251021.e72c00cf",
|
||||
"version": "0.12.0-nightly.20251022.0542de95",
|
||||
"publisher": "google",
|
||||
"icon": "assets/icon.png",
|
||||
"repository": {
|
||||
|
||||
@@ -424,7 +424,13 @@ export function getVersion(options = {}) {
|
||||
}
|
||||
break;
|
||||
case 'promote-nightly':
|
||||
versionData = promoteNightlyVersion();
|
||||
versionData = promoteNightlyVersion({ args });
|
||||
// A promoted nightly version is still a nightly, so we should check for conflicts.
|
||||
if (doesVersionExist({ args, version: versionData.releaseVersion })) {
|
||||
throw new Error(
|
||||
`Version conflict! Promoted nightly version ${versionData.releaseVersion} already exists.`,
|
||||
);
|
||||
}
|
||||
break;
|
||||
case 'stable':
|
||||
versionData = getStableVersion(args);
|
||||
|
||||
+1
-1
@@ -142,7 +142,7 @@ export function setupLinters() {
|
||||
|
||||
export function runESLint() {
|
||||
console.log('\nRunning ESLint...');
|
||||
if (!runCommand('npm run lint:ci')) {
|
||||
if (!runCommand('npm run lint')) {
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user