mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-05-12 21:03:05 -07:00
feat(cli): hide workspace policy update dialog and auto-accept by default (#20351)
This commit is contained in:
@@ -8,7 +8,11 @@ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|||||||
import * as fs from 'node:fs';
|
import * as fs from 'node:fs';
|
||||||
import * as path from 'node:path';
|
import * as path from 'node:path';
|
||||||
import * as os from 'node:os';
|
import * as os from 'node:os';
|
||||||
import { resolveWorkspacePolicyState } from './policy.js';
|
import {
|
||||||
|
resolveWorkspacePolicyState,
|
||||||
|
autoAcceptWorkspacePolicies,
|
||||||
|
setAutoAcceptWorkspacePolicies,
|
||||||
|
} from './policy.js';
|
||||||
import { writeToStderr } from '@google/gemini-cli-core';
|
import { writeToStderr } from '@google/gemini-cli-core';
|
||||||
|
|
||||||
// Mock debugLogger to avoid noise in test output
|
// Mock debugLogger to avoid noise in test output
|
||||||
@@ -68,24 +72,18 @@ describe('resolveWorkspacePolicyState', () => {
|
|||||||
fs.mkdirSync(policiesDir, { recursive: true });
|
fs.mkdirSync(policiesDir, { recursive: true });
|
||||||
fs.writeFileSync(path.join(policiesDir, 'policy.toml'), 'rules = []');
|
fs.writeFileSync(path.join(policiesDir, 'policy.toml'), 'rules = []');
|
||||||
|
|
||||||
// First call to establish integrity (interactive accept)
|
// First call to establish integrity (interactive auto-accept)
|
||||||
const firstResult = await resolveWorkspacePolicyState({
|
const firstResult = await resolveWorkspacePolicyState({
|
||||||
cwd: workspaceDir,
|
cwd: workspaceDir,
|
||||||
trustedFolder: true,
|
trustedFolder: true,
|
||||||
interactive: true,
|
interactive: true,
|
||||||
});
|
});
|
||||||
expect(firstResult.policyUpdateConfirmationRequest).toBeDefined();
|
expect(firstResult.workspacePoliciesDir).toBe(policiesDir);
|
||||||
|
expect(firstResult.policyUpdateConfirmationRequest).toBeUndefined();
|
||||||
// Establish integrity manually as if accepted
|
expect(writeToStderr).not.toHaveBeenCalled();
|
||||||
const { PolicyIntegrityManager } = await import('@google/gemini-cli-core');
|
|
||||||
const integrityManager = new PolicyIntegrityManager();
|
|
||||||
await integrityManager.acceptIntegrity(
|
|
||||||
'workspace',
|
|
||||||
workspaceDir,
|
|
||||||
firstResult.policyUpdateConfirmationRequest!.newHash,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Second call should match
|
// Second call should match
|
||||||
|
|
||||||
const result = await resolveWorkspacePolicyState({
|
const result = await resolveWorkspacePolicyState({
|
||||||
cwd: workspaceDir,
|
cwd: workspaceDir,
|
||||||
trustedFolder: true,
|
trustedFolder: true,
|
||||||
@@ -107,7 +105,11 @@ describe('resolveWorkspacePolicyState', () => {
|
|||||||
expect(result.policyUpdateConfirmationRequest).toBeUndefined();
|
expect(result.policyUpdateConfirmationRequest).toBeUndefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return confirmation request if changed in interactive mode', async () => {
|
it('should return confirmation request if changed in interactive mode when AUTO_ACCEPT is false', async () => {
|
||||||
|
const originalValue = autoAcceptWorkspacePolicies;
|
||||||
|
setAutoAcceptWorkspacePolicies(false);
|
||||||
|
|
||||||
|
try {
|
||||||
fs.mkdirSync(policiesDir, { recursive: true });
|
fs.mkdirSync(policiesDir, { recursive: true });
|
||||||
fs.writeFileSync(path.join(policiesDir, 'policy.toml'), 'rules = []');
|
fs.writeFileSync(path.join(policiesDir, 'policy.toml'), 'rules = []');
|
||||||
|
|
||||||
@@ -124,9 +126,12 @@ describe('resolveWorkspacePolicyState', () => {
|
|||||||
policyDir: policiesDir,
|
policyDir: policiesDir,
|
||||||
newHash: expect.any(String),
|
newHash: expect.any(String),
|
||||||
});
|
});
|
||||||
|
} finally {
|
||||||
|
setAutoAcceptWorkspacePolicies(originalValue);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should warn and auto-accept if changed in non-interactive mode', async () => {
|
it('should warn and auto-accept if changed in non-interactive mode when AUTO_ACCEPT is true', async () => {
|
||||||
fs.mkdirSync(policiesDir, { recursive: true });
|
fs.mkdirSync(policiesDir, { recursive: true });
|
||||||
fs.writeFileSync(path.join(policiesDir, 'policy.toml'), 'rules = []');
|
fs.writeFileSync(path.join(policiesDir, 'policy.toml'), 'rules = []');
|
||||||
|
|
||||||
@@ -143,6 +148,30 @@ describe('resolveWorkspacePolicyState', () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should warn and auto-accept if changed in non-interactive mode when AUTO_ACCEPT is false', async () => {
|
||||||
|
const originalValue = autoAcceptWorkspacePolicies;
|
||||||
|
setAutoAcceptWorkspacePolicies(false);
|
||||||
|
|
||||||
|
try {
|
||||||
|
fs.mkdirSync(policiesDir, { recursive: true });
|
||||||
|
fs.writeFileSync(path.join(policiesDir, 'policy.toml'), 'rules = []');
|
||||||
|
|
||||||
|
const result = await resolveWorkspacePolicyState({
|
||||||
|
cwd: workspaceDir,
|
||||||
|
trustedFolder: true,
|
||||||
|
interactive: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.workspacePoliciesDir).toBe(policiesDir);
|
||||||
|
expect(result.policyUpdateConfirmationRequest).toBeUndefined();
|
||||||
|
expect(writeToStderr).toHaveBeenCalledWith(
|
||||||
|
expect.stringContaining('Automatically accepting and loading'),
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
setAutoAcceptWorkspacePolicies(originalValue);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
it('should not return workspace policies if cwd is the home directory', async () => {
|
it('should not return workspace policies if cwd is the home directory', async () => {
|
||||||
const policiesDir = path.join(tempDir, '.gemini', 'policies');
|
const policiesDir = path.join(tempDir, '.gemini', 'policies');
|
||||||
fs.mkdirSync(policiesDir, { recursive: true });
|
fs.mkdirSync(policiesDir, { recursive: true });
|
||||||
|
|||||||
@@ -17,9 +17,24 @@ import {
|
|||||||
Storage,
|
Storage,
|
||||||
type PolicyUpdateConfirmationRequest,
|
type PolicyUpdateConfirmationRequest,
|
||||||
writeToStderr,
|
writeToStderr,
|
||||||
|
debugLogger,
|
||||||
} from '@google/gemini-cli-core';
|
} from '@google/gemini-cli-core';
|
||||||
import { type Settings } from './settings.js';
|
import { type Settings } from './settings.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Temporary flag to automatically accept workspace policies to reduce friction.
|
||||||
|
* Exported as 'let' to allow monkey patching in tests via the setter.
|
||||||
|
*/
|
||||||
|
export let autoAcceptWorkspacePolicies = true;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the autoAcceptWorkspacePolicies flag.
|
||||||
|
* Used primarily for testing purposes.
|
||||||
|
*/
|
||||||
|
export function setAutoAcceptWorkspacePolicies(value: boolean) {
|
||||||
|
autoAcceptWorkspacePolicies = value;
|
||||||
|
}
|
||||||
|
|
||||||
export async function createPolicyEngineConfig(
|
export async function createPolicyEngineConfig(
|
||||||
settings: Settings,
|
settings: Settings,
|
||||||
approvalMode: ApprovalMode,
|
approvalMode: ApprovalMode,
|
||||||
@@ -91,8 +106,8 @@ export async function resolveWorkspacePolicyState(options: {
|
|||||||
) {
|
) {
|
||||||
// No workspace policies found
|
// No workspace policies found
|
||||||
workspacePoliciesDir = undefined;
|
workspacePoliciesDir = undefined;
|
||||||
} else if (interactive) {
|
} else if (interactive && !autoAcceptWorkspacePolicies) {
|
||||||
// Policies changed or are new, and we are in interactive mode
|
// Policies changed or are new, and we are in interactive mode and auto-accept is disabled
|
||||||
policyUpdateConfirmationRequest = {
|
policyUpdateConfirmationRequest = {
|
||||||
scope: 'workspace',
|
scope: 'workspace',
|
||||||
identifier: cwd,
|
identifier: cwd,
|
||||||
@@ -100,17 +115,23 @@ export async function resolveWorkspacePolicyState(options: {
|
|||||||
newHash: integrityResult.hash,
|
newHash: integrityResult.hash,
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
// Non-interactive mode: warn and automatically accept/load
|
// Non-interactive mode or auto-accept is enabled: automatically accept/load
|
||||||
await integrityManager.acceptIntegrity(
|
await integrityManager.acceptIntegrity(
|
||||||
'workspace',
|
'workspace',
|
||||||
cwd,
|
cwd,
|
||||||
integrityResult.hash,
|
integrityResult.hash,
|
||||||
);
|
);
|
||||||
workspacePoliciesDir = potentialWorkspacePoliciesDir;
|
workspacePoliciesDir = potentialWorkspacePoliciesDir;
|
||||||
// debugLogger.warn here doesn't show up in the terminal. It is showing up only in debug mode on the debug console
|
|
||||||
|
if (!interactive) {
|
||||||
writeToStderr(
|
writeToStderr(
|
||||||
'WARNING: Workspace policies changed or are new. Automatically accepting and loading them in non-interactive mode.\n',
|
'WARNING: Workspace policies changed or are new. Automatically accepting and loading them.\n',
|
||||||
);
|
);
|
||||||
|
} else {
|
||||||
|
debugLogger.warn(
|
||||||
|
'Workspace policies changed or are new. Automatically accepting and loading them.',
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import { loadCliConfig, type CliArgs } from './config.js';
|
|||||||
import { createTestMergedSettings } from './settings.js';
|
import { createTestMergedSettings } from './settings.js';
|
||||||
import * as ServerConfig from '@google/gemini-cli-core';
|
import * as ServerConfig from '@google/gemini-cli-core';
|
||||||
import { isWorkspaceTrusted } from './trustedFolders.js';
|
import { isWorkspaceTrusted } from './trustedFolders.js';
|
||||||
|
import * as Policy from './policy.js';
|
||||||
|
|
||||||
// Mock dependencies
|
// Mock dependencies
|
||||||
vi.mock('./trustedFolders.js', () => ({
|
vi.mock('./trustedFolders.js', () => ({
|
||||||
@@ -164,7 +165,86 @@ describe('Workspace-Level Policy CLI Integration', () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should set policyUpdateConfirmationRequest if integrity MISMATCH in interactive mode', async () => {
|
it('should automatically accept and load workspacePoliciesDir if integrity MISMATCH in interactive mode when AUTO_ACCEPT is true', async () => {
|
||||||
|
vi.mocked(isWorkspaceTrusted).mockReturnValue({
|
||||||
|
isTrusted: true,
|
||||||
|
source: 'file',
|
||||||
|
});
|
||||||
|
mockCheckIntegrity.mockResolvedValue({
|
||||||
|
status: 'mismatch',
|
||||||
|
hash: 'new-hash',
|
||||||
|
fileCount: 1,
|
||||||
|
});
|
||||||
|
vi.mocked(ServerConfig.isHeadlessMode).mockReturnValue(false); // Interactive
|
||||||
|
|
||||||
|
const settings = createTestMergedSettings();
|
||||||
|
const argv = {
|
||||||
|
query: 'test',
|
||||||
|
promptInteractive: 'test',
|
||||||
|
} as unknown as CliArgs;
|
||||||
|
|
||||||
|
const config = await loadCliConfig(settings, 'test-session', argv, {
|
||||||
|
cwd: MOCK_CWD,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(config.getPolicyUpdateConfirmationRequest()).toBeUndefined();
|
||||||
|
expect(mockAcceptIntegrity).toHaveBeenCalledWith(
|
||||||
|
'workspace',
|
||||||
|
MOCK_CWD,
|
||||||
|
'new-hash',
|
||||||
|
);
|
||||||
|
expect(ServerConfig.createPolicyEngineConfig).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
workspacePoliciesDir: expect.stringContaining(
|
||||||
|
path.join('.gemini', 'policies'),
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
expect.anything(),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should automatically accept and load workspacePoliciesDir if integrity is NEW in interactive mode when AUTO_ACCEPT is true', async () => {
|
||||||
|
vi.mocked(isWorkspaceTrusted).mockReturnValue({
|
||||||
|
isTrusted: true,
|
||||||
|
source: 'file',
|
||||||
|
});
|
||||||
|
mockCheckIntegrity.mockResolvedValue({
|
||||||
|
status: 'new',
|
||||||
|
hash: 'new-hash',
|
||||||
|
fileCount: 5,
|
||||||
|
});
|
||||||
|
vi.mocked(ServerConfig.isHeadlessMode).mockReturnValue(false); // Interactive
|
||||||
|
|
||||||
|
const settings = createTestMergedSettings();
|
||||||
|
const argv = { query: 'test' } as unknown as CliArgs;
|
||||||
|
|
||||||
|
const config = await loadCliConfig(settings, 'test-session', argv, {
|
||||||
|
cwd: MOCK_CWD,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(config.getPolicyUpdateConfirmationRequest()).toBeUndefined();
|
||||||
|
expect(mockAcceptIntegrity).toHaveBeenCalledWith(
|
||||||
|
'workspace',
|
||||||
|
MOCK_CWD,
|
||||||
|
'new-hash',
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(ServerConfig.createPolicyEngineConfig).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
workspacePoliciesDir: expect.stringContaining(
|
||||||
|
path.join('.gemini', 'policies'),
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
expect.anything(),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should set policyUpdateConfirmationRequest if integrity MISMATCH in interactive mode when AUTO_ACCEPT is false', async () => {
|
||||||
|
// Monkey patch autoAcceptWorkspacePolicies using setter
|
||||||
|
const originalValue = Policy.autoAcceptWorkspacePolicies;
|
||||||
|
Policy.setAutoAcceptWorkspacePolicies(false);
|
||||||
|
|
||||||
|
try {
|
||||||
vi.mocked(isWorkspaceTrusted).mockReturnValue({
|
vi.mocked(isWorkspaceTrusted).mockReturnValue({
|
||||||
isTrusted: true,
|
isTrusted: true,
|
||||||
source: 'file',
|
source: 'file',
|
||||||
@@ -192,48 +272,15 @@ describe('Workspace-Level Policy CLI Integration', () => {
|
|||||||
policyDir: expect.stringContaining(path.join('.gemini', 'policies')),
|
policyDir: expect.stringContaining(path.join('.gemini', 'policies')),
|
||||||
newHash: 'new-hash',
|
newHash: 'new-hash',
|
||||||
});
|
});
|
||||||
// In interactive mode without accept flag, it waits for user confirmation (handled by UI),
|
|
||||||
// so it currently DOES NOT pass the directory to createPolicyEngineConfig yet.
|
|
||||||
// The UI will handle the confirmation and reload/update.
|
|
||||||
expect(ServerConfig.createPolicyEngineConfig).toHaveBeenCalledWith(
|
|
||||||
expect.objectContaining({
|
|
||||||
workspacePoliciesDir: undefined,
|
|
||||||
}),
|
|
||||||
expect.anything(),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should set policyUpdateConfirmationRequest if integrity is NEW with files (first time seen) in interactive mode', async () => {
|
|
||||||
vi.mocked(isWorkspaceTrusted).mockReturnValue({
|
|
||||||
isTrusted: true,
|
|
||||||
source: 'file',
|
|
||||||
});
|
|
||||||
mockCheckIntegrity.mockResolvedValue({
|
|
||||||
status: 'new',
|
|
||||||
hash: 'new-hash',
|
|
||||||
fileCount: 5,
|
|
||||||
});
|
|
||||||
vi.mocked(ServerConfig.isHeadlessMode).mockReturnValue(false); // Interactive
|
|
||||||
|
|
||||||
const settings = createTestMergedSettings();
|
|
||||||
const argv = { query: 'test' } as unknown as CliArgs;
|
|
||||||
|
|
||||||
const config = await loadCliConfig(settings, 'test-session', argv, {
|
|
||||||
cwd: MOCK_CWD,
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(config.getPolicyUpdateConfirmationRequest()).toEqual({
|
|
||||||
scope: 'workspace',
|
|
||||||
identifier: MOCK_CWD,
|
|
||||||
policyDir: expect.stringContaining(path.join('.gemini', 'policies')),
|
|
||||||
newHash: 'new-hash',
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(ServerConfig.createPolicyEngineConfig).toHaveBeenCalledWith(
|
expect(ServerConfig.createPolicyEngineConfig).toHaveBeenCalledWith(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
workspacePoliciesDir: undefined,
|
workspacePoliciesDir: undefined,
|
||||||
}),
|
}),
|
||||||
expect.anything(),
|
expect.anything(),
|
||||||
);
|
);
|
||||||
|
} finally {
|
||||||
|
// Restore for other tests
|
||||||
|
Policy.setAutoAcceptWorkspacePolicies(originalValue);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user