@google/gemini-cli-core 0.34.0-preview.2 → 0.35.0-nightly.20260313.bb060d7a9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/docs/changelogs/index.md +19 -0
- package/dist/docs/changelogs/latest.md +214 -191
- package/dist/docs/changelogs/preview.md +447 -184
- package/dist/docs/cli/model-routing.md +18 -1
- package/dist/docs/cli/model-steering.md +79 -0
- package/dist/docs/cli/plan-mode.md +99 -14
- package/dist/docs/cli/settings.md +14 -12
- package/dist/docs/cli/telemetry.md +45 -0
- package/dist/docs/cli/tutorials/plan-mode-steering.md +89 -0
- package/dist/docs/core/index.md +2 -0
- package/dist/docs/core/local-model-routing.md +193 -0
- package/dist/docs/reference/configuration.md +27 -3
- package/dist/docs/reference/keyboard-shortcuts.md +168 -90
- package/dist/docs/reference/policy-engine.md +30 -6
- package/dist/docs/resources/troubleshooting.md +15 -0
- package/dist/docs/sidebar.json +10 -0
- package/dist/docs/tools/shell.md +8 -0
- package/dist/google-gemini-cli-core-0.35.0-nightly.20260311.657f19c1f.tgz +0 -0
- package/dist/src/agents/a2a-client-manager.d.ts +11 -4
- package/dist/src/agents/a2a-client-manager.js +61 -34
- package/dist/src/agents/a2a-client-manager.js.map +1 -1
- package/dist/src/agents/a2a-client-manager.test.js +178 -115
- package/dist/src/agents/a2a-client-manager.test.js.map +1 -1
- package/dist/src/agents/a2aUtils.d.ts +4 -28
- package/dist/src/agents/a2aUtils.js +20 -181
- package/dist/src/agents/a2aUtils.js.map +1 -1
- package/dist/src/agents/a2aUtils.test.js +19 -106
- package/dist/src/agents/a2aUtils.test.js.map +1 -1
- package/dist/src/agents/agent-scheduler.d.ts +2 -0
- package/dist/src/agents/agent-scheduler.js +3 -2
- package/dist/src/agents/agent-scheduler.js.map +1 -1
- package/dist/src/agents/agent-scheduler.test.js +12 -3
- package/dist/src/agents/agent-scheduler.test.js.map +1 -1
- package/dist/src/agents/agentLoader.d.ts +2 -2
- package/dist/src/agents/agentLoader.js +15 -0
- package/dist/src/agents/agentLoader.js.map +1 -1
- package/dist/src/agents/auth-provider/factory.d.ts +2 -0
- package/dist/src/agents/auth-provider/factory.js +6 -3
- package/dist/src/agents/auth-provider/factory.js.map +1 -1
- package/dist/src/agents/auth-provider/google-credentials-provider.d.ts +26 -0
- package/dist/src/agents/auth-provider/google-credentials-provider.js +117 -0
- package/dist/src/agents/auth-provider/google-credentials-provider.js.map +1 -0
- package/dist/src/agents/auth-provider/google-credentials-provider.test.d.ts +6 -0
- package/dist/src/agents/auth-provider/google-credentials-provider.test.js +126 -0
- package/dist/src/agents/auth-provider/google-credentials-provider.test.js.map +1 -0
- package/dist/src/agents/browser/browserAgentFactory.js +11 -2
- package/dist/src/agents/browser/browserAgentFactory.js.map +1 -1
- package/dist/src/agents/browser/browserAgentInvocation.d.ts +4 -3
- package/dist/src/agents/browser/browserAgentInvocation.js +10 -4
- package/dist/src/agents/browser/browserAgentInvocation.js.map +1 -1
- package/dist/src/agents/browser/browserAgentInvocation.test.js +1 -0
- package/dist/src/agents/browser/browserAgentInvocation.test.js.map +1 -1
- package/dist/src/agents/browser/browserManager.d.ts +10 -0
- package/dist/src/agents/browser/browserManager.js +53 -8
- package/dist/src/agents/browser/browserManager.js.map +1 -1
- package/dist/src/agents/browser/inputBlocker.d.ts +51 -0
- package/dist/src/agents/browser/inputBlocker.js +234 -0
- package/dist/src/agents/browser/inputBlocker.js.map +1 -0
- package/dist/src/agents/browser/inputBlocker.test.d.ts +6 -0
- package/dist/src/agents/browser/inputBlocker.test.js +82 -0
- package/dist/src/agents/browser/inputBlocker.test.js.map +1 -0
- package/dist/src/agents/browser/mcpToolWrapper.d.ts +3 -2
- package/dist/src/agents/browser/mcpToolWrapper.js +49 -8
- package/dist/src/agents/browser/mcpToolWrapper.js.map +1 -1
- package/dist/src/agents/browser/mcpToolWrapper.test.js +51 -0
- package/dist/src/agents/browser/mcpToolWrapper.test.js.map +1 -1
- package/dist/src/agents/local-executor.d.ts +5 -4
- package/dist/src/agents/local-executor.js +30 -25
- package/dist/src/agents/local-executor.js.map +1 -1
- package/dist/src/agents/local-executor.test.js +269 -3
- package/dist/src/agents/local-executor.test.js.map +1 -1
- package/dist/src/agents/local-invocation.d.ts +4 -4
- package/dist/src/agents/local-invocation.js +12 -9
- package/dist/src/agents/local-invocation.js.map +1 -1
- package/dist/src/agents/local-invocation.test.js +19 -3
- package/dist/src/agents/local-invocation.test.js.map +1 -1
- package/dist/src/agents/registry.js +3 -2
- package/dist/src/agents/registry.js.map +1 -1
- package/dist/src/agents/registry.test.js +1 -0
- package/dist/src/agents/registry.test.js.map +1 -1
- package/dist/src/agents/remote-invocation.d.ts +0 -9
- package/dist/src/agents/remote-invocation.js +1 -28
- package/dist/src/agents/remote-invocation.js.map +1 -1
- package/dist/src/agents/remote-invocation.test.js +1 -0
- package/dist/src/agents/remote-invocation.test.js.map +1 -1
- package/dist/src/agents/subagent-tool-wrapper.d.ts +5 -4
- package/dist/src/agents/subagent-tool-wrapper.js +9 -5
- package/dist/src/agents/subagent-tool-wrapper.js.map +1 -1
- package/dist/src/agents/subagent-tool-wrapper.test.js +15 -1
- package/dist/src/agents/subagent-tool-wrapper.test.js.map +1 -1
- package/dist/src/agents/subagent-tool.d.ts +3 -3
- package/dist/src/agents/subagent-tool.js +16 -12
- package/dist/src/agents/subagent-tool.js.map +1 -1
- package/dist/src/agents/subagent-tool.test.js +13 -3
- package/dist/src/agents/subagent-tool.test.js.map +1 -1
- package/dist/src/code_assist/oauth2.js +0 -1
- package/dist/src/code_assist/oauth2.js.map +1 -1
- package/dist/src/code_assist/server.js +2 -2
- package/dist/src/code_assist/server.js.map +1 -1
- package/dist/src/code_assist/server.test.js +5 -1
- package/dist/src/code_assist/server.test.js.map +1 -1
- package/dist/src/code_assist/telemetry.d.ts +2 -2
- package/dist/src/code_assist/telemetry.js +5 -3
- package/dist/src/code_assist/telemetry.js.map +1 -1
- package/dist/src/code_assist/telemetry.test.js +14 -12
- package/dist/src/code_assist/telemetry.test.js.map +1 -1
- package/dist/src/code_assist/types.d.ts +14 -12
- package/dist/src/code_assist/types.js.map +1 -1
- package/dist/src/config/agent-loop-context.d.ts +3 -0
- package/dist/src/config/config.d.ts +75 -2
- package/dist/src/config/config.js +78 -11
- package/dist/src/config/config.js.map +1 -1
- package/dist/src/config/config.test.js +170 -10
- package/dist/src/config/config.test.js.map +1 -1
- package/dist/src/config/models.d.ts +14 -0
- package/dist/src/config/models.js +37 -0
- package/dist/src/config/models.js.map +1 -1
- package/dist/src/config/models.test.js +51 -1
- package/dist/src/config/models.test.js.map +1 -1
- package/dist/src/config/storage.d.ts +1 -0
- package/dist/src/config/storage.js +3 -0
- package/dist/src/config/storage.js.map +1 -1
- package/dist/src/core/baseLlmClient.js +19 -3
- package/dist/src/core/baseLlmClient.js.map +1 -1
- package/dist/src/core/baseLlmClient.test.js +2 -0
- package/dist/src/core/baseLlmClient.test.js.map +1 -1
- package/dist/src/core/client.d.ts +4 -3
- package/dist/src/core/client.js +42 -17
- package/dist/src/core/client.js.map +1 -1
- package/dist/src/core/client.test.js +8 -0
- package/dist/src/core/client.test.js.map +1 -1
- package/dist/src/core/contentGenerator.d.ts +1 -0
- package/dist/src/core/contentGenerator.js +35 -3
- package/dist/src/core/contentGenerator.js.map +1 -1
- package/dist/src/core/contentGenerator.test.js +137 -2
- package/dist/src/core/contentGenerator.test.js.map +1 -1
- package/dist/src/core/coreToolHookTriggers.d.ts +2 -3
- package/dist/src/core/coreToolHookTriggers.js +5 -11
- package/dist/src/core/coreToolHookTriggers.js.map +1 -1
- package/dist/src/core/coreToolHookTriggers.test.js +24 -0
- package/dist/src/core/coreToolHookTriggers.test.js.map +1 -1
- package/dist/src/core/coreToolScheduler.js +1 -1
- package/dist/src/core/coreToolScheduler.js.map +1 -1
- package/dist/src/core/geminiChat.js +31 -21
- package/dist/src/core/geminiChat.js.map +1 -1
- package/dist/src/core/geminiChat.test.js +6 -4
- package/dist/src/core/geminiChat.test.js.map +1 -1
- package/dist/src/core/geminiChat_network_retry.test.js +21 -11
- package/dist/src/core/geminiChat_network_retry.test.js.map +1 -1
- package/dist/src/fallback/handler.js +0 -4
- package/dist/src/fallback/handler.js.map +1 -1
- package/dist/src/fallback/handler.test.js +0 -6
- package/dist/src/fallback/handler.test.js.map +1 -1
- package/dist/src/generated/git-commit.d.ts +2 -2
- package/dist/src/generated/git-commit.js +2 -2
- package/dist/src/generated/git-commit.js.map +1 -1
- package/dist/src/ide/detect-ide.d.ts +8 -0
- package/dist/src/ide/detect-ide.js +12 -1
- package/dist/src/ide/detect-ide.js.map +1 -1
- package/dist/src/ide/detect-ide.test.js +12 -0
- package/dist/src/ide/detect-ide.test.js.map +1 -1
- package/dist/src/ide/ide-installer.js +20 -8
- package/dist/src/ide/ide-installer.js.map +1 -1
- package/dist/src/ide/ide-installer.test.js +60 -4
- package/dist/src/ide/ide-installer.test.js.map +1 -1
- package/dist/src/mcp/oauth-provider.js +0 -2
- package/dist/src/mcp/oauth-provider.js.map +1 -1
- package/dist/src/mcp/oauth-utils.js +0 -2
- package/dist/src/mcp/oauth-utils.js.map +1 -1
- package/dist/src/policy/config.d.ts +14 -2
- package/dist/src/policy/config.js +94 -49
- package/dist/src/policy/config.js.map +1 -1
- package/dist/src/policy/config.test.js +208 -488
- package/dist/src/policy/config.test.js.map +1 -1
- package/dist/src/policy/policies/plan.toml +1 -1
- package/dist/src/policy/stable-stringify.js +15 -5
- package/dist/src/policy/stable-stringify.js.map +1 -1
- package/dist/src/policy/types.d.ts +1 -0
- package/dist/src/policy/types.js.map +1 -1
- package/dist/src/policy/utils.d.ts +17 -0
- package/dist/src/policy/utils.js +27 -7
- package/dist/src/policy/utils.js.map +1 -1
- package/dist/src/prompts/snippets.js +2 -1
- package/dist/src/prompts/snippets.js.map +1 -1
- package/dist/src/prompts/utils.test.d.ts +6 -0
- package/dist/src/prompts/utils.test.js +225 -0
- package/dist/src/prompts/utils.test.js.map +1 -0
- package/dist/src/routing/modelRouterService.js +2 -2
- package/dist/src/routing/modelRouterService.js.map +1 -1
- package/dist/src/routing/modelRouterService.test.js +4 -3
- package/dist/src/routing/modelRouterService.test.js.map +1 -1
- package/dist/src/routing/strategies/numericalClassifierStrategy.js +6 -40
- package/dist/src/routing/strategies/numericalClassifierStrategy.js.map +1 -1
- package/dist/src/routing/strategies/numericalClassifierStrategy.test.js +29 -64
- package/dist/src/routing/strategies/numericalClassifierStrategy.test.js.map +1 -1
- package/dist/src/scheduler/policy.d.ts +3 -7
- package/dist/src/scheduler/policy.js +4 -3
- package/dist/src/scheduler/policy.js.map +1 -1
- package/dist/src/scheduler/policy.test.js +51 -31
- package/dist/src/scheduler/policy.test.js.map +1 -1
- package/dist/src/scheduler/scheduler.d.ts +4 -2
- package/dist/src/scheduler/scheduler.js +19 -16
- package/dist/src/scheduler/scheduler.js.map +1 -1
- package/dist/src/scheduler/scheduler.test.js +33 -10
- package/dist/src/scheduler/scheduler.test.js.map +1 -1
- package/dist/src/scheduler/scheduler_parallel.test.js +40 -2
- package/dist/src/scheduler/scheduler_parallel.test.js.map +1 -1
- package/dist/src/scheduler/scheduler_waiting_callback.test.js +1 -1
- package/dist/src/scheduler/scheduler_waiting_callback.test.js.map +1 -1
- package/dist/src/scheduler/tool-executor.d.ts +3 -3
- package/dist/src/scheduler/tool-executor.js +15 -21
- package/dist/src/scheduler/tool-executor.js.map +1 -1
- package/dist/src/scheduler/tool-executor.test.js +43 -8
- package/dist/src/scheduler/tool-executor.test.js.map +1 -1
- package/dist/src/services/chatRecordingService.js +2 -1
- package/dist/src/services/chatRecordingService.js.map +1 -1
- package/dist/src/services/chatRecordingService.test.js +21 -0
- package/dist/src/services/chatRecordingService.test.js.map +1 -1
- package/dist/src/services/executionLifecycleService.d.ts +83 -0
- package/dist/src/services/executionLifecycleService.js +271 -0
- package/dist/src/services/executionLifecycleService.js.map +1 -0
- package/dist/src/services/executionLifecycleService.test.d.ts +6 -0
- package/dist/src/services/executionLifecycleService.test.js +227 -0
- package/dist/src/services/executionLifecycleService.test.js.map +1 -0
- package/dist/src/services/sandboxManager.d.ts +54 -0
- package/dist/src/services/sandboxManager.js +30 -0
- package/dist/src/services/sandboxManager.js.map +1 -0
- package/dist/src/services/sandboxManager.test.d.ts +6 -0
- package/dist/src/services/sandboxManager.test.js +95 -0
- package/dist/src/services/sandboxManager.test.js.map +1 -0
- package/dist/src/services/shellExecutionService.d.ts +4 -52
- package/dist/src/services/shellExecutionService.js +437 -498
- package/dist/src/services/shellExecutionService.js.map +1 -1
- package/dist/src/services/shellExecutionService.test.js +10 -21
- package/dist/src/services/shellExecutionService.test.js.map +1 -1
- package/dist/src/telemetry/clearcut-logger/clearcut-logger.d.ts +13 -2
- package/dist/src/telemetry/clearcut-logger/clearcut-logger.js +91 -1
- package/dist/src/telemetry/clearcut-logger/clearcut-logger.js.map +1 -1
- package/dist/src/telemetry/clearcut-logger/clearcut-logger.test.js +77 -0
- package/dist/src/telemetry/clearcut-logger/clearcut-logger.test.js.map +1 -1
- package/dist/src/telemetry/clearcut-logger/event-metadata-key.d.ts +10 -1
- package/dist/src/telemetry/clearcut-logger/event-metadata-key.js +25 -1
- package/dist/src/telemetry/clearcut-logger/event-metadata-key.js.map +1 -1
- package/dist/src/telemetry/index.d.ts +3 -3
- package/dist/src/telemetry/index.js +3 -3
- package/dist/src/telemetry/index.js.map +1 -1
- package/dist/src/telemetry/loggers.d.ts +2 -1
- package/dist/src/telemetry/loggers.js +32 -1
- package/dist/src/telemetry/loggers.js.map +1 -1
- package/dist/src/telemetry/loggers.test.js +37 -2
- package/dist/src/telemetry/loggers.test.js.map +1 -1
- package/dist/src/telemetry/metrics.d.ts +13 -0
- package/dist/src/telemetry/metrics.js +17 -0
- package/dist/src/telemetry/metrics.js.map +1 -1
- package/dist/src/telemetry/startupProfiler.js +4 -4
- package/dist/src/telemetry/startupProfiler.js.map +1 -1
- package/dist/src/telemetry/startupProfiler.test.js +19 -0
- package/dist/src/telemetry/startupProfiler.test.js.map +1 -1
- package/dist/src/telemetry/telemetry-utils.test.js +3 -3
- package/dist/src/telemetry/types.d.ts +16 -2
- package/dist/src/telemetry/types.js +34 -0
- package/dist/src/telemetry/types.js.map +1 -1
- package/dist/src/telemetry/uiTelemetry.d.ts +7 -0
- package/dist/src/telemetry/uiTelemetry.js +78 -0
- package/dist/src/telemetry/uiTelemetry.js.map +1 -1
- package/dist/src/telemetry/uiTelemetry.test.js +101 -0
- package/dist/src/telemetry/uiTelemetry.test.js.map +1 -1
- package/dist/src/tools/exit-plan-mode.js +10 -2
- package/dist/src/tools/exit-plan-mode.js.map +1 -1
- package/dist/src/tools/exit-plan-mode.test.js +15 -0
- package/dist/src/tools/exit-plan-mode.test.js.map +1 -1
- package/dist/src/tools/ls.js +2 -2
- package/dist/src/tools/ls.js.map +1 -1
- package/dist/src/tools/mcp-client.js +0 -1
- package/dist/src/tools/mcp-client.js.map +1 -1
- package/dist/src/tools/mcp-client.test.js +4 -0
- package/dist/src/tools/mcp-client.test.js.map +1 -1
- package/dist/src/tools/mcp-tool.test.js +10 -1
- package/dist/src/tools/mcp-tool.test.js.map +1 -1
- package/dist/src/tools/read-many-files.js +2 -4
- package/dist/src/tools/read-many-files.js.map +1 -1
- package/dist/src/tools/shell.d.ts +1 -1
- package/dist/src/tools/shell.js +3 -3
- package/dist/src/tools/shell.js.map +1 -1
- package/dist/src/tools/tool-registry.test.js +4 -0
- package/dist/src/tools/tool-registry.test.js.map +1 -1
- package/dist/src/tools/tools.d.ts +34 -1
- package/dist/src/tools/tools.js +47 -1
- package/dist/src/tools/tools.js.map +1 -1
- package/dist/src/tools/web-fetch.d.ts +5 -0
- package/dist/src/tools/web-fetch.js +177 -100
- package/dist/src/tools/web-fetch.js.map +1 -1
- package/dist/src/tools/web-fetch.test.js +130 -26
- package/dist/src/tools/web-fetch.test.js.map +1 -1
- package/dist/src/tools/web-search.test.js +2 -0
- package/dist/src/tools/web-search.test.js.map +1 -1
- package/dist/src/utils/fetch.d.ts +0 -29
- package/dist/src/utils/fetch.js +10 -123
- package/dist/src/utils/fetch.js.map +1 -1
- package/dist/src/utils/fetch.test.js +1 -103
- package/dist/src/utils/fetch.test.js.map +1 -1
- package/dist/src/utils/language-detection.js +96 -89
- package/dist/src/utils/language-detection.js.map +1 -1
- package/dist/src/utils/language-detection.test.d.ts +6 -0
- package/dist/src/utils/language-detection.test.js +39 -0
- package/dist/src/utils/language-detection.test.js.map +1 -0
- package/dist/src/utils/oauth-flow.js +0 -2
- package/dist/src/utils/oauth-flow.js.map +1 -1
- package/dist/src/utils/paths.js +5 -2
- package/dist/src/utils/paths.js.map +1 -1
- package/dist/src/utils/paths.test.js +11 -0
- package/dist/src/utils/paths.test.js.map +1 -1
- package/dist/src/utils/process-utils.js +18 -4
- package/dist/src/utils/process-utils.js.map +1 -1
- package/dist/src/utils/process-utils.test.js +32 -10
- package/dist/src/utils/process-utils.test.js.map +1 -1
- package/dist/src/utils/retry.d.ts +7 -0
- package/dist/src/utils/retry.js +40 -3
- package/dist/src/utils/retry.js.map +1 -1
- package/dist/src/utils/retry.test.js +13 -0
- package/dist/src/utils/retry.test.js.map +1 -1
- package/dist/src/utils/summarizer.test.js +5 -0
- package/dist/src/utils/summarizer.test.js.map +1 -1
- package/dist/src/utils/surface.d.ts +18 -0
- package/dist/src/utils/surface.js +46 -0
- package/dist/src/utils/surface.js.map +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/dist/google-gemini-cli-core-0.34.0-preview.1.tgz +0 -0
|
@@ -5,29 +5,85 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import { describe, it, expect, vi, afterEach, beforeEach } from 'vitest';
|
|
7
7
|
import nodePath from 'node:path';
|
|
8
|
+
import * as fs from 'node:fs/promises';
|
|
9
|
+
import {} from 'node:fs';
|
|
8
10
|
import { ApprovalMode, PolicyDecision, InProcessCheckerType, } from './types.js';
|
|
9
11
|
import { isDirectorySecure } from '../utils/security.js';
|
|
12
|
+
import { createPolicyEngineConfig, clearEmittedPolicyWarnings, getPolicyDirectories, } from './config.js';
|
|
13
|
+
import { Storage } from '../config/storage.js';
|
|
14
|
+
import * as tomlLoader from './toml-loader.js';
|
|
15
|
+
import { coreEvents } from '../utils/events.js';
|
|
10
16
|
vi.unmock('../config/storage.js');
|
|
11
17
|
vi.mock('../utils/security.js', () => ({
|
|
12
18
|
isDirectorySecure: vi.fn().mockResolvedValue({ secure: true }),
|
|
13
19
|
}));
|
|
20
|
+
vi.mock('node:fs/promises', async (importOriginal) => {
|
|
21
|
+
const actual = await importOriginal();
|
|
22
|
+
const mockFs = {
|
|
23
|
+
...actual,
|
|
24
|
+
readdir: vi.fn(actual.readdir),
|
|
25
|
+
readFile: vi.fn(actual.readFile),
|
|
26
|
+
stat: vi.fn(actual.stat),
|
|
27
|
+
mkdir: vi.fn(actual.mkdir),
|
|
28
|
+
open: vi.fn(actual.open),
|
|
29
|
+
rename: vi.fn(actual.rename),
|
|
30
|
+
};
|
|
31
|
+
return {
|
|
32
|
+
...mockFs,
|
|
33
|
+
default: mockFs,
|
|
34
|
+
};
|
|
35
|
+
});
|
|
14
36
|
afterEach(() => {
|
|
15
|
-
vi.
|
|
16
|
-
vi.restoreAllMocks();
|
|
17
|
-
vi.doUnmock('node:fs/promises');
|
|
37
|
+
vi.resetAllMocks();
|
|
18
38
|
});
|
|
19
39
|
describe('createPolicyEngineConfig', () => {
|
|
40
|
+
const MOCK_DEFAULT_DIR = '/tmp/mock/default/policies';
|
|
20
41
|
beforeEach(async () => {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
// Mock Storage to avoid picking up real user/system policies from the host environment
|
|
42
|
+
clearEmittedPolicyWarnings();
|
|
43
|
+
// Mock Storage to avoid host environment contamination
|
|
24
44
|
vi.spyOn(Storage, 'getUserPoliciesDir').mockReturnValue('/non/existent/user/policies');
|
|
25
45
|
vi.spyOn(Storage, 'getSystemPoliciesDir').mockReturnValue('/non/existent/system/policies');
|
|
26
|
-
// Reset security check to default secure
|
|
27
46
|
vi.mocked(isDirectorySecure).mockResolvedValue({ secure: true });
|
|
28
47
|
});
|
|
48
|
+
/**
|
|
49
|
+
* Helper to mock a policy file in the filesystem.
|
|
50
|
+
*/
|
|
51
|
+
function mockPolicyFile(path, content) {
|
|
52
|
+
vi.mocked(fs.readdir).mockImplementation(async (p) => {
|
|
53
|
+
if (nodePath.resolve(p.toString()) === nodePath.dirname(path)) {
|
|
54
|
+
return [
|
|
55
|
+
{
|
|
56
|
+
name: nodePath.basename(path),
|
|
57
|
+
isFile: () => true,
|
|
58
|
+
isDirectory: () => false,
|
|
59
|
+
},
|
|
60
|
+
];
|
|
61
|
+
}
|
|
62
|
+
return (await vi.importActual('node:fs/promises')).readdir(p);
|
|
63
|
+
});
|
|
64
|
+
vi.mocked(fs.stat).mockImplementation(async (p) => {
|
|
65
|
+
if (nodePath.resolve(p.toString()) === nodePath.dirname(path)) {
|
|
66
|
+
return {
|
|
67
|
+
isDirectory: () => true,
|
|
68
|
+
isFile: () => false,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
if (nodePath.resolve(p.toString()) === path) {
|
|
72
|
+
return {
|
|
73
|
+
isDirectory: () => false,
|
|
74
|
+
isFile: () => true,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
return (await vi.importActual('node:fs/promises')).stat(p);
|
|
78
|
+
});
|
|
79
|
+
vi.mocked(fs.readFile).mockImplementation(async (p) => {
|
|
80
|
+
if (nodePath.resolve(p.toString()) === path) {
|
|
81
|
+
return content;
|
|
82
|
+
}
|
|
83
|
+
return (await vi.importActual('node:fs/promises')).readFile(p);
|
|
84
|
+
});
|
|
85
|
+
}
|
|
29
86
|
it('should filter out insecure system policy directories', async () => {
|
|
30
|
-
const { Storage } = await import('../config/storage.js');
|
|
31
87
|
const systemPolicyDir = '/insecure/system/policies';
|
|
32
88
|
vi.spyOn(Storage, 'getSystemPoliciesDir').mockReturnValue(systemPolicyDir);
|
|
33
89
|
vi.mocked(isDirectorySecure).mockImplementation(async (path) => {
|
|
@@ -36,125 +92,74 @@ describe('createPolicyEngineConfig', () => {
|
|
|
36
92
|
}
|
|
37
93
|
return { secure: true };
|
|
38
94
|
});
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
const loadPoliciesSpy = vi.spyOn(tomlLoader, 'loadPoliciesFromToml');
|
|
44
|
-
loadPoliciesSpy.mockResolvedValue({
|
|
45
|
-
rules: [],
|
|
46
|
-
checkers: [],
|
|
47
|
-
errors: [],
|
|
48
|
-
});
|
|
49
|
-
const { createPolicyEngineConfig } = await import('./config.js');
|
|
50
|
-
const settings = {};
|
|
51
|
-
await createPolicyEngineConfig(settings, ApprovalMode.DEFAULT, '/tmp/mock/default/policies');
|
|
52
|
-
// Verify loadPoliciesFromToml was called
|
|
95
|
+
const loadPoliciesSpy = vi
|
|
96
|
+
.spyOn(tomlLoader, 'loadPoliciesFromToml')
|
|
97
|
+
.mockResolvedValue({ rules: [], checkers: [], errors: [] });
|
|
98
|
+
await createPolicyEngineConfig({}, ApprovalMode.DEFAULT, '/tmp/mock/default/policies');
|
|
53
99
|
expect(loadPoliciesSpy).toHaveBeenCalled();
|
|
54
100
|
const calledDirs = loadPoliciesSpy.mock.calls[0][0];
|
|
55
|
-
// The system directory should NOT be in the list
|
|
56
101
|
expect(calledDirs).not.toContain(systemPolicyDir);
|
|
57
|
-
// But other directories (user, default) should be there
|
|
58
102
|
expect(calledDirs).toContain('/non/existent/user/policies');
|
|
59
103
|
expect(calledDirs).toContain('/tmp/mock/default/policies');
|
|
60
104
|
});
|
|
61
|
-
it('should
|
|
62
|
-
const
|
|
63
|
-
|
|
64
|
-
if (
|
|
65
|
-
|
|
66
|
-
.normalize(path)
|
|
67
|
-
.includes(nodePath.normalize('.gemini/policies'))) {
|
|
68
|
-
// Return empty array for user policies
|
|
69
|
-
return [];
|
|
105
|
+
it('should NOT filter out insecure supplemental admin policy directories', async () => {
|
|
106
|
+
const adminPolicyDir = '/insecure/admin/policies';
|
|
107
|
+
vi.mocked(isDirectorySecure).mockImplementation(async (path) => {
|
|
108
|
+
if (nodePath.resolve(path) === nodePath.resolve(adminPolicyDir)) {
|
|
109
|
+
return { secure: false, reason: 'Insecure directory' };
|
|
70
110
|
}
|
|
71
|
-
return
|
|
111
|
+
return { secure: true };
|
|
72
112
|
});
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
const config = await createPolicyEngineConfig(
|
|
113
|
+
const loadPoliciesSpy = vi
|
|
114
|
+
.spyOn(tomlLoader, 'loadPoliciesFromToml')
|
|
115
|
+
.mockResolvedValue({ rules: [], checkers: [], errors: [] });
|
|
116
|
+
await createPolicyEngineConfig({ adminPolicyPaths: [adminPolicyDir] }, ApprovalMode.DEFAULT, '/tmp/mock/default/policies');
|
|
117
|
+
const calledDirs = loadPoliciesSpy.mock.calls[0][0];
|
|
118
|
+
expect(calledDirs).toContain(adminPolicyDir);
|
|
119
|
+
expect(calledDirs).toContain('/non/existent/system/policies');
|
|
120
|
+
expect(calledDirs).toContain('/non/existent/user/policies');
|
|
121
|
+
expect(calledDirs).toContain('/tmp/mock/default/policies');
|
|
122
|
+
});
|
|
123
|
+
it('should return ASK_USER for write tools and ALLOW for read-only tools by default', async () => {
|
|
124
|
+
vi.mocked(fs.readdir).mockResolvedValue([]);
|
|
125
|
+
const config = await createPolicyEngineConfig({}, ApprovalMode.DEFAULT, MOCK_DEFAULT_DIR);
|
|
86
126
|
expect(config.defaultDecision).toBe(PolicyDecision.ASK_USER);
|
|
87
|
-
// The order of the rules is not guaranteed, so we sort them by tool name.
|
|
88
|
-
config.rules?.sort((a, b) => (a.toolName ?? '').localeCompare(b.toolName ?? ''));
|
|
89
|
-
// Since we are mocking an empty policy directory, we expect NO rules from TOML.
|
|
90
|
-
// Wait, the CLI test expected a bunch of default rules. Those must have come from
|
|
91
|
-
// the actual default policies directory in the CLI package.
|
|
92
|
-
// In the core package, we don't necessarily have those default policy files yet
|
|
93
|
-
// or we need to point to them.
|
|
94
|
-
// For this unit test, if we mock the default dir as empty, we should get NO rules
|
|
95
|
-
// if no settings are provided.
|
|
96
|
-
// Actually, let's look at how CLI test gets them. It uses `__dirname` in `policy.ts`.
|
|
97
|
-
// If we want to test default rules, we need to provide them.
|
|
98
|
-
// For now, let's assert it's empty if we provide no TOML files, to ensure the *mechanism* works.
|
|
99
|
-
// Or better, mock one default rule to ensure it's loaded.
|
|
100
127
|
expect(config.rules).toEqual([]);
|
|
101
|
-
|
|
102
|
-
}, 30000);
|
|
128
|
+
});
|
|
103
129
|
it('should allow tools in tools.allowed', async () => {
|
|
104
|
-
|
|
105
|
-
const
|
|
106
|
-
tools: { allowed: ['run_shell_command'] },
|
|
107
|
-
};
|
|
108
|
-
const config = await createPolicyEngineConfig(settings, ApprovalMode.DEFAULT, '/tmp/mock/default/policies');
|
|
130
|
+
vi.mocked(fs.readdir).mockResolvedValue([]);
|
|
131
|
+
const config = await createPolicyEngineConfig({ tools: { allowed: ['run_shell_command'] } }, ApprovalMode.DEFAULT, MOCK_DEFAULT_DIR);
|
|
109
132
|
const rule = config.rules?.find((r) => r.toolName === 'run_shell_command' &&
|
|
110
133
|
r.decision === PolicyDecision.ALLOW);
|
|
111
134
|
expect(rule).toBeDefined();
|
|
112
135
|
expect(rule?.priority).toBeCloseTo(4.3, 5); // Command line allow
|
|
113
136
|
});
|
|
114
137
|
it('should deny tools in tools.exclude', async () => {
|
|
115
|
-
const
|
|
116
|
-
const settings = {
|
|
117
|
-
tools: { exclude: ['run_shell_command'] },
|
|
118
|
-
};
|
|
119
|
-
const config = await createPolicyEngineConfig(settings, ApprovalMode.DEFAULT, '/tmp/mock/default/policies');
|
|
138
|
+
const config = await createPolicyEngineConfig({ tools: { exclude: ['run_shell_command'] } }, ApprovalMode.DEFAULT, MOCK_DEFAULT_DIR);
|
|
120
139
|
const rule = config.rules?.find((r) => r.toolName === 'run_shell_command' &&
|
|
121
140
|
r.decision === PolicyDecision.DENY);
|
|
122
141
|
expect(rule).toBeDefined();
|
|
123
142
|
expect(rule?.priority).toBeCloseTo(4.4, 5); // Command line exclude
|
|
124
143
|
});
|
|
125
144
|
it('should allow tools from allowed MCP servers', async () => {
|
|
126
|
-
const
|
|
127
|
-
const settings = {
|
|
128
|
-
mcp: { allowed: ['my-server'] },
|
|
129
|
-
};
|
|
130
|
-
const config = await createPolicyEngineConfig(settings, ApprovalMode.DEFAULT, '/tmp/mock/default/policies');
|
|
145
|
+
const config = await createPolicyEngineConfig({ mcp: { allowed: ['my-server'] } }, ApprovalMode.DEFAULT, MOCK_DEFAULT_DIR);
|
|
131
146
|
const rule = config.rules?.find((r) => r.mcpName === 'my-server' && r.decision === PolicyDecision.ALLOW);
|
|
132
147
|
expect(rule).toBeDefined();
|
|
133
148
|
expect(rule?.priority).toBe(4.1); // MCP allowed server
|
|
134
149
|
});
|
|
135
150
|
it('should deny tools from excluded MCP servers', async () => {
|
|
136
|
-
const
|
|
137
|
-
const settings = {
|
|
138
|
-
mcp: { excluded: ['my-server'] },
|
|
139
|
-
};
|
|
140
|
-
const config = await createPolicyEngineConfig(settings, ApprovalMode.DEFAULT, '/tmp/mock/default/policies');
|
|
151
|
+
const config = await createPolicyEngineConfig({ mcp: { excluded: ['my-server'] } }, ApprovalMode.DEFAULT, MOCK_DEFAULT_DIR);
|
|
141
152
|
const rule = config.rules?.find((r) => r.mcpName === 'my-server' && r.decision === PolicyDecision.DENY);
|
|
142
153
|
expect(rule).toBeDefined();
|
|
143
154
|
expect(rule?.priority).toBe(4.9); // MCP excluded server
|
|
144
155
|
});
|
|
145
156
|
it('should allow tools from trusted MCP servers', async () => {
|
|
146
|
-
const
|
|
147
|
-
const settings = {
|
|
157
|
+
const config = await createPolicyEngineConfig({
|
|
148
158
|
mcpServers: {
|
|
149
|
-
'trusted-server': {
|
|
150
|
-
|
|
151
|
-
},
|
|
152
|
-
'untrusted-server': {
|
|
153
|
-
trust: false,
|
|
154
|
-
},
|
|
159
|
+
'trusted-server': { trust: true },
|
|
160
|
+
'untrusted-server': { trust: false },
|
|
155
161
|
},
|
|
156
|
-
};
|
|
157
|
-
const config = await createPolicyEngineConfig(settings, ApprovalMode.DEFAULT, '/tmp/mock/default/policies');
|
|
162
|
+
}, ApprovalMode.DEFAULT, MOCK_DEFAULT_DIR);
|
|
158
163
|
const trustedRule = config.rules?.find((r) => r.mcpName === 'trusted-server' && r.decision === PolicyDecision.ALLOW);
|
|
159
164
|
expect(trustedRule).toBeDefined();
|
|
160
165
|
expect(trustedRule?.priority).toBe(4.2); // MCP trusted server
|
|
@@ -163,19 +168,10 @@ describe('createPolicyEngineConfig', () => {
|
|
|
163
168
|
expect(untrustedRule).toBeUndefined();
|
|
164
169
|
});
|
|
165
170
|
it('should handle multiple MCP server configurations together', async () => {
|
|
166
|
-
const
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
excluded: ['excluded-server'],
|
|
171
|
-
},
|
|
172
|
-
mcpServers: {
|
|
173
|
-
'trusted-server': {
|
|
174
|
-
trust: true,
|
|
175
|
-
},
|
|
176
|
-
},
|
|
177
|
-
};
|
|
178
|
-
const config = await createPolicyEngineConfig(settings, ApprovalMode.DEFAULT, '/tmp/mock/default/policies');
|
|
171
|
+
const config = await createPolicyEngineConfig({
|
|
172
|
+
mcp: { allowed: ['allowed-server'], excluded: ['excluded-server'] },
|
|
173
|
+
mcpServers: { 'trusted-server': { trust: true } },
|
|
174
|
+
}, ApprovalMode.DEFAULT, MOCK_DEFAULT_DIR);
|
|
179
175
|
// Check allowed server
|
|
180
176
|
const allowedRule = config.rules?.find((r) => r.mcpName === 'allowed-server' && r.decision === PolicyDecision.ALLOW);
|
|
181
177
|
expect(allowedRule).toBeDefined();
|
|
@@ -190,46 +186,38 @@ describe('createPolicyEngineConfig', () => {
|
|
|
190
186
|
expect(excludedRule?.priority).toBe(4.9); // MCP excluded server
|
|
191
187
|
});
|
|
192
188
|
it('should allow all tools in YOLO mode', async () => {
|
|
193
|
-
const
|
|
194
|
-
const settings = {};
|
|
195
|
-
const config = await createPolicyEngineConfig(settings, ApprovalMode.YOLO);
|
|
189
|
+
const config = await createPolicyEngineConfig({}, ApprovalMode.YOLO);
|
|
196
190
|
const rule = config.rules?.find((r) => r.decision === PolicyDecision.ALLOW && !r.toolName);
|
|
197
191
|
expect(rule).toBeDefined();
|
|
198
|
-
// Priority 998 in default tier → 1.998 (999 reserved for ask_user exception)
|
|
199
192
|
expect(rule?.priority).toBeCloseTo(1.998, 5);
|
|
200
193
|
});
|
|
201
194
|
it('should allow edit tool in AUTO_EDIT mode', async () => {
|
|
202
|
-
const
|
|
203
|
-
const settings = {};
|
|
204
|
-
const config = await createPolicyEngineConfig(settings, ApprovalMode.AUTO_EDIT);
|
|
195
|
+
const config = await createPolicyEngineConfig({}, ApprovalMode.AUTO_EDIT);
|
|
205
196
|
const rule = config.rules?.find((r) => r.toolName === 'replace' &&
|
|
206
197
|
r.decision === PolicyDecision.ALLOW &&
|
|
207
198
|
r.modes?.includes(ApprovalMode.AUTO_EDIT));
|
|
208
199
|
expect(rule).toBeDefined();
|
|
209
|
-
// Priority 15 in default tier → 1.015
|
|
210
200
|
expect(rule?.priority).toBeCloseTo(1.015, 5);
|
|
211
201
|
});
|
|
212
202
|
it('should prioritize exclude over allow', async () => {
|
|
213
|
-
const
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
203
|
+
const config = await createPolicyEngineConfig({
|
|
204
|
+
tools: {
|
|
205
|
+
allowed: ['run_shell_command'],
|
|
206
|
+
exclude: ['run_shell_command'],
|
|
207
|
+
},
|
|
208
|
+
}, ApprovalMode.DEFAULT, MOCK_DEFAULT_DIR);
|
|
218
209
|
const denyRule = config.rules?.find((r) => r.toolName === 'run_shell_command' &&
|
|
219
210
|
r.decision === PolicyDecision.DENY);
|
|
220
211
|
const allowRule = config.rules?.find((r) => r.toolName === 'run_shell_command' &&
|
|
221
212
|
r.decision === PolicyDecision.ALLOW);
|
|
222
|
-
expect(denyRule).toBeDefined();
|
|
223
|
-
expect(allowRule).toBeDefined();
|
|
224
213
|
expect(denyRule.priority).toBeGreaterThan(allowRule.priority);
|
|
225
214
|
});
|
|
226
215
|
it('should prioritize specific tool allows over MCP server excludes', async () => {
|
|
227
|
-
const { createPolicyEngineConfig } = await import('./config.js');
|
|
228
216
|
const settings = {
|
|
229
217
|
mcp: { excluded: ['my-server'] },
|
|
230
218
|
tools: { allowed: ['mcp_my-server_specific-tool'] },
|
|
231
219
|
};
|
|
232
|
-
const config = await createPolicyEngineConfig(settings, ApprovalMode.DEFAULT,
|
|
220
|
+
const config = await createPolicyEngineConfig(settings, ApprovalMode.DEFAULT, MOCK_DEFAULT_DIR);
|
|
233
221
|
const serverDenyRule = config.rules?.find((r) => r.mcpName === 'my-server' && r.decision === PolicyDecision.DENY);
|
|
234
222
|
const toolAllowRule = config.rules?.find((r) => r.toolName === 'mcp_my-server_specific-tool' &&
|
|
235
223
|
r.decision === PolicyDecision.ALLOW);
|
|
@@ -262,6 +250,7 @@ describe('createPolicyEngineConfig', () => {
|
|
|
262
250
|
expect(toolDenyRule.priority).toBeGreaterThan(serverAllowRule.priority);
|
|
263
251
|
});
|
|
264
252
|
it('should handle complex priority scenarios correctly', async () => {
|
|
253
|
+
mockPolicyFile(nodePath.join(MOCK_DEFAULT_DIR, 'default.toml'), '[[rule]]\ntoolName = "glob"\ndecision = "allow"\npriority = 50\n');
|
|
265
254
|
const settings = {
|
|
266
255
|
tools: {
|
|
267
256
|
allowed: ['mcp_trusted-server_tool1', 'other-tool'], // Priority 4.3
|
|
@@ -277,57 +266,7 @@ describe('createPolicyEngineConfig', () => {
|
|
|
277
266
|
},
|
|
278
267
|
},
|
|
279
268
|
};
|
|
280
|
-
|
|
281
|
-
const actualFs = await vi.importActual('node:fs/promises');
|
|
282
|
-
const mockReaddir = vi.fn(async (p, _o) => {
|
|
283
|
-
if (typeof p === 'string' && p.includes('/tmp/mock/default/policies')) {
|
|
284
|
-
return [
|
|
285
|
-
{
|
|
286
|
-
name: 'default.toml',
|
|
287
|
-
isFile: () => true,
|
|
288
|
-
isDirectory: () => false,
|
|
289
|
-
},
|
|
290
|
-
];
|
|
291
|
-
}
|
|
292
|
-
return [];
|
|
293
|
-
});
|
|
294
|
-
const mockStat = vi.fn(async (p) => {
|
|
295
|
-
if (typeof p === 'string' && p.includes('/tmp/mock/default/policies')) {
|
|
296
|
-
return {
|
|
297
|
-
isDirectory: () => true,
|
|
298
|
-
isFile: () => false,
|
|
299
|
-
};
|
|
300
|
-
}
|
|
301
|
-
if (typeof p === 'string' && p.includes('default.toml')) {
|
|
302
|
-
return {
|
|
303
|
-
isDirectory: () => false,
|
|
304
|
-
isFile: () => true,
|
|
305
|
-
};
|
|
306
|
-
}
|
|
307
|
-
return actualFs.stat(p);
|
|
308
|
-
});
|
|
309
|
-
const mockReadFile = vi.fn(async (p, _o) => {
|
|
310
|
-
if (typeof p === 'string' && p.includes('default.toml')) {
|
|
311
|
-
return '[[rule]]\ntoolName = "glob"\ndecision = "allow"\npriority = 50\n';
|
|
312
|
-
}
|
|
313
|
-
return '';
|
|
314
|
-
});
|
|
315
|
-
vi.doMock('node:fs/promises', () => ({
|
|
316
|
-
...actualFs,
|
|
317
|
-
default: {
|
|
318
|
-
...actualFs,
|
|
319
|
-
readdir: mockReaddir,
|
|
320
|
-
readFile: mockReadFile,
|
|
321
|
-
stat: mockStat,
|
|
322
|
-
},
|
|
323
|
-
readdir: mockReaddir,
|
|
324
|
-
readFile: mockReadFile,
|
|
325
|
-
stat: mockStat,
|
|
326
|
-
}));
|
|
327
|
-
vi.resetModules();
|
|
328
|
-
const { createPolicyEngineConfig: createConfig } = await import('./config.js');
|
|
329
|
-
const config = await createConfig(settings, ApprovalMode.DEFAULT, '/tmp/mock/default/policies');
|
|
330
|
-
// Verify glob is denied even though default would allow it
|
|
269
|
+
const config = await createPolicyEngineConfig(settings, ApprovalMode.DEFAULT, MOCK_DEFAULT_DIR);
|
|
331
270
|
const globDenyRule = config.rules?.find((r) => r.toolName === 'glob' && r.decision === PolicyDecision.DENY);
|
|
332
271
|
const globAllowRule = config.rules?.find((r) => r.toolName === 'glob' && r.decision === PolicyDecision.ALLOW);
|
|
333
272
|
expect(globDenyRule).toBeDefined();
|
|
@@ -348,21 +287,14 @@ describe('createPolicyEngineConfig', () => {
|
|
|
348
287
|
const highestPriorityExcludes = priorities?.filter((p) => Math.abs(p.priority - 4.4) < 0.01 ||
|
|
349
288
|
Math.abs(p.priority - 4.9) < 0.01);
|
|
350
289
|
expect(highestPriorityExcludes?.every((p) => p.decision === PolicyDecision.DENY)).toBe(true);
|
|
351
|
-
vi.doUnmock('node:fs/promises');
|
|
352
290
|
});
|
|
353
291
|
it('should handle MCP servers with undefined trust property', async () => {
|
|
354
|
-
const
|
|
355
|
-
const settings = {
|
|
292
|
+
const config = await createPolicyEngineConfig({
|
|
356
293
|
mcpServers: {
|
|
357
|
-
'no-trust-property': {
|
|
358
|
-
|
|
359
|
-
},
|
|
360
|
-
'explicit-false': {
|
|
361
|
-
trust: false,
|
|
362
|
-
},
|
|
294
|
+
'no-trust-property': {},
|
|
295
|
+
'explicit-false': { trust: false },
|
|
363
296
|
},
|
|
364
|
-
};
|
|
365
|
-
const config = await createPolicyEngineConfig(settings, ApprovalMode.DEFAULT, '/tmp/mock/default/policies');
|
|
297
|
+
}, ApprovalMode.DEFAULT, MOCK_DEFAULT_DIR);
|
|
366
298
|
// Neither server should have an allow rule
|
|
367
299
|
const noTrustRule = config.rules?.find((r) => r.mcpName === 'no-trust-property' &&
|
|
368
300
|
r.decision === PolicyDecision.ALLOW);
|
|
@@ -371,29 +303,11 @@ describe('createPolicyEngineConfig', () => {
|
|
|
371
303
|
expect(explicitFalseRule).toBeUndefined();
|
|
372
304
|
});
|
|
373
305
|
it('should have YOLO allow-all rule beat write tool rules in YOLO mode', async () => {
|
|
374
|
-
|
|
375
|
-
vi.doUnmock('node:fs/promises');
|
|
376
|
-
const { createPolicyEngineConfig: createConfig } = await import('./config.js');
|
|
377
|
-
// Re-mock Storage after resetModules because it was reloaded
|
|
378
|
-
const { Storage: FreshStorage } = await import('../config/storage.js');
|
|
379
|
-
vi.spyOn(FreshStorage, 'getUserPoliciesDir').mockReturnValue('/non/existent/user/policies');
|
|
380
|
-
vi.spyOn(FreshStorage, 'getSystemPoliciesDir').mockReturnValue('/non/existent/system/policies');
|
|
381
|
-
const settings = {
|
|
382
|
-
tools: { exclude: ['dangerous-tool'] },
|
|
383
|
-
};
|
|
384
|
-
// Use default policy dir (no third arg) to load real yolo.toml and write.toml
|
|
385
|
-
const config = await createConfig(settings, ApprovalMode.YOLO);
|
|
386
|
-
// Should have the wildcard allow rule
|
|
306
|
+
const config = await createPolicyEngineConfig({ tools: { exclude: ['dangerous-tool'] } }, ApprovalMode.YOLO);
|
|
387
307
|
const wildcardRule = config.rules?.find((r) => !r.toolName && r.decision === PolicyDecision.ALLOW);
|
|
388
|
-
expect(wildcardRule).toBeDefined();
|
|
389
|
-
// Priority 998 in default tier → 1.998 (999 reserved for ask_user exception)
|
|
390
|
-
expect(wildcardRule?.priority).toBeCloseTo(1.998, 5);
|
|
391
|
-
// Write tool ASK_USER rules are present (from write.toml)
|
|
392
308
|
const writeToolRules = config.rules?.filter((r) => ['run_shell_command'].includes(r.toolName || '') &&
|
|
393
309
|
r.decision === PolicyDecision.ASK_USER);
|
|
394
|
-
expect(
|
|
395
|
-
expect(writeToolRules?.length).toBeGreaterThan(0);
|
|
396
|
-
// But YOLO allow-all rule has higher priority than all write tool rules
|
|
310
|
+
expect(wildcardRule).toBeDefined();
|
|
397
311
|
writeToolRules?.forEach((writeRule) => {
|
|
398
312
|
expect(wildcardRule.priority).toBeGreaterThan(writeRule.priority);
|
|
399
313
|
});
|
|
@@ -403,106 +317,25 @@ describe('createPolicyEngineConfig', () => {
|
|
|
403
317
|
expect(excludeRule?.priority).toBeCloseTo(4.4, 5); // Command line exclude
|
|
404
318
|
});
|
|
405
319
|
it('should support argsPattern in policy rules', async () => {
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
name: 'write.toml',
|
|
415
|
-
isFile: () => true,
|
|
416
|
-
isDirectory: () => false,
|
|
417
|
-
},
|
|
418
|
-
];
|
|
419
|
-
}
|
|
420
|
-
return actualFs.readdir(path, options);
|
|
421
|
-
});
|
|
422
|
-
const mockReadFile = vi.fn(async (path, options) => {
|
|
423
|
-
if (typeof path === 'string' &&
|
|
424
|
-
nodePath
|
|
425
|
-
.normalize(path)
|
|
426
|
-
.includes(nodePath.normalize('.gemini/policies/write.toml'))) {
|
|
427
|
-
return `
|
|
428
|
-
[[rule]]
|
|
429
|
-
toolName = "run_shell_command"
|
|
430
|
-
argsPattern = "\\"command\\":\\"git (status|diff|log)\\""
|
|
431
|
-
decision = "allow"
|
|
432
|
-
priority = 150
|
|
433
|
-
`;
|
|
434
|
-
}
|
|
435
|
-
return actualFs.readFile(path, options);
|
|
436
|
-
});
|
|
437
|
-
const mockStat = vi.fn(async (path, options) => {
|
|
438
|
-
if (typeof path === 'string' &&
|
|
439
|
-
nodePath
|
|
440
|
-
.normalize(path)
|
|
441
|
-
.includes(nodePath.normalize('.gemini/policies'))) {
|
|
442
|
-
return {
|
|
443
|
-
isDirectory: () => true,
|
|
444
|
-
isFile: () => false,
|
|
445
|
-
};
|
|
446
|
-
}
|
|
447
|
-
return actualFs.stat(path, options);
|
|
448
|
-
});
|
|
449
|
-
vi.doMock('node:fs/promises', () => ({
|
|
450
|
-
...actualFs,
|
|
451
|
-
default: {
|
|
452
|
-
...actualFs,
|
|
453
|
-
readFile: mockReadFile,
|
|
454
|
-
readdir: mockReaddir,
|
|
455
|
-
stat: mockStat,
|
|
456
|
-
},
|
|
457
|
-
readFile: mockReadFile,
|
|
458
|
-
readdir: mockReaddir,
|
|
459
|
-
stat: mockStat,
|
|
460
|
-
}));
|
|
461
|
-
vi.resetModules();
|
|
462
|
-
const { createPolicyEngineConfig } = await import('./config.js');
|
|
463
|
-
const settings = {};
|
|
464
|
-
const config = await createPolicyEngineConfig(settings, ApprovalMode.DEFAULT, '/tmp/mock/default/policies');
|
|
320
|
+
mockPolicyFile(nodePath.join(MOCK_DEFAULT_DIR, 'write.toml'), `
|
|
321
|
+
[[rule]]
|
|
322
|
+
toolName = "run_shell_command"
|
|
323
|
+
argsPattern = "\\"command\\":\\"git (status|diff|log)\\""
|
|
324
|
+
decision = "allow"
|
|
325
|
+
priority = 150
|
|
326
|
+
`);
|
|
327
|
+
const config = await createPolicyEngineConfig({}, ApprovalMode.DEFAULT, MOCK_DEFAULT_DIR);
|
|
465
328
|
const rule = config.rules?.find((r) => r.toolName === 'run_shell_command' &&
|
|
466
329
|
r.decision === PolicyDecision.ALLOW);
|
|
467
330
|
expect(rule).toBeDefined();
|
|
468
|
-
// Priority 150 in
|
|
469
|
-
expect(rule?.priority).toBeCloseTo(
|
|
331
|
+
// Priority 150 in default tier → 1.150
|
|
332
|
+
expect(rule?.priority).toBeCloseTo(1.15, 5);
|
|
470
333
|
expect(rule?.argsPattern).toBeInstanceOf(RegExp);
|
|
471
334
|
expect(rule?.argsPattern?.test('{"command":"git status"}')).toBe(true);
|
|
472
|
-
expect(rule?.argsPattern?.test('{"command":"git diff"}')).toBe(true);
|
|
473
|
-
expect(rule?.argsPattern?.test('{"command":"git log"}')).toBe(true);
|
|
474
335
|
expect(rule?.argsPattern?.test('{"command":"git commit"}')).toBe(false);
|
|
475
|
-
expect(rule?.argsPattern?.test('{"command":"git push"}')).toBe(false);
|
|
476
|
-
vi.doUnmock('node:fs/promises');
|
|
477
336
|
});
|
|
478
337
|
it('should load safety_checker configuration from TOML', async () => {
|
|
479
|
-
|
|
480
|
-
const mockReaddir = vi.fn(async (path, options) => {
|
|
481
|
-
if (typeof path === 'string' &&
|
|
482
|
-
nodePath
|
|
483
|
-
.normalize(path)
|
|
484
|
-
.includes(nodePath.normalize('.gemini/policies'))) {
|
|
485
|
-
return [
|
|
486
|
-
{
|
|
487
|
-
name: 'safety.toml',
|
|
488
|
-
isFile: () => true,
|
|
489
|
-
isDirectory: () => false,
|
|
490
|
-
},
|
|
491
|
-
];
|
|
492
|
-
}
|
|
493
|
-
return actualFs.readdir(path, options);
|
|
494
|
-
});
|
|
495
|
-
const mockReadFile = vi.fn(async (path, options) => {
|
|
496
|
-
if (typeof path === 'string' &&
|
|
497
|
-
nodePath
|
|
498
|
-
.normalize(path)
|
|
499
|
-
.includes(nodePath.normalize('.gemini/policies/safety.toml'))) {
|
|
500
|
-
return `
|
|
501
|
-
[[rule]]
|
|
502
|
-
toolName = "write_file"
|
|
503
|
-
decision = "allow"
|
|
504
|
-
priority = 10
|
|
505
|
-
|
|
338
|
+
mockPolicyFile(nodePath.join(MOCK_DEFAULT_DIR, 'safety.toml'), `
|
|
506
339
|
[[rule]]
|
|
507
340
|
toolName = "write_file"
|
|
508
341
|
decision = "allow"
|
|
@@ -515,71 +348,14 @@ priority = 10
|
|
|
515
348
|
type = "in-process"
|
|
516
349
|
name = "allowed-path"
|
|
517
350
|
required_context = ["environment"]
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
return actualFs.readFile(path, options);
|
|
522
|
-
});
|
|
523
|
-
const mockStat = vi.fn(async (path, options) => {
|
|
524
|
-
if (typeof path === 'string' &&
|
|
525
|
-
nodePath
|
|
526
|
-
.normalize(path)
|
|
527
|
-
.includes(nodePath.normalize('.gemini/policies'))) {
|
|
528
|
-
return {
|
|
529
|
-
isDirectory: () => true,
|
|
530
|
-
isFile: () => false,
|
|
531
|
-
};
|
|
532
|
-
}
|
|
533
|
-
return actualFs.stat(path, options);
|
|
534
|
-
});
|
|
535
|
-
vi.doMock('node:fs/promises', () => ({
|
|
536
|
-
...actualFs,
|
|
537
|
-
default: {
|
|
538
|
-
...actualFs,
|
|
539
|
-
readFile: mockReadFile,
|
|
540
|
-
readdir: mockReaddir,
|
|
541
|
-
stat: mockStat,
|
|
542
|
-
},
|
|
543
|
-
readFile: mockReadFile,
|
|
544
|
-
readdir: mockReaddir,
|
|
545
|
-
stat: mockStat,
|
|
546
|
-
}));
|
|
547
|
-
vi.resetModules();
|
|
548
|
-
const { createPolicyEngineConfig } = await import('./config.js');
|
|
549
|
-
const settings = {};
|
|
550
|
-
const config = await createPolicyEngineConfig(settings, ApprovalMode.DEFAULT, '/tmp/mock/default/policies');
|
|
551
|
-
const rule = config.rules?.find((r) => r.toolName === 'write_file' && r.decision === PolicyDecision.ALLOW);
|
|
552
|
-
expect(rule).toBeDefined();
|
|
351
|
+
`);
|
|
352
|
+
const config = await createPolicyEngineConfig({}, ApprovalMode.DEFAULT, MOCK_DEFAULT_DIR);
|
|
353
|
+
expect(config.rules?.some((r) => r.toolName === 'write_file' && r.decision === PolicyDecision.ALLOW)).toBe(true);
|
|
553
354
|
const checker = config.checkers?.find((c) => c.toolName === 'write_file' && c.checker.type === 'in-process');
|
|
554
|
-
expect(checker).toBeDefined();
|
|
555
|
-
expect(checker?.checker.type).toBe('in-process');
|
|
556
355
|
expect(checker?.checker.name).toBe(InProcessCheckerType.ALLOWED_PATH);
|
|
557
|
-
expect(checker?.checker.required_context).toEqual(['environment']);
|
|
558
|
-
vi.doUnmock('node:fs/promises');
|
|
559
356
|
});
|
|
560
357
|
it('should reject invalid in-process checker names', async () => {
|
|
561
|
-
|
|
562
|
-
const mockReaddir = vi.fn(async (path, options) => {
|
|
563
|
-
if (typeof path === 'string' &&
|
|
564
|
-
nodePath
|
|
565
|
-
.normalize(path)
|
|
566
|
-
.includes(nodePath.normalize('.gemini/policies'))) {
|
|
567
|
-
return [
|
|
568
|
-
{
|
|
569
|
-
name: 'invalid_safety.toml',
|
|
570
|
-
isFile: () => true,
|
|
571
|
-
isDirectory: () => false,
|
|
572
|
-
},
|
|
573
|
-
];
|
|
574
|
-
}
|
|
575
|
-
return actualFs.readdir(path, options);
|
|
576
|
-
});
|
|
577
|
-
const mockReadFile = vi.fn(async (path, options) => {
|
|
578
|
-
if (typeof path === 'string' &&
|
|
579
|
-
nodePath
|
|
580
|
-
.normalize(path)
|
|
581
|
-
.includes(nodePath.normalize('.gemini/policies/invalid_safety.toml'))) {
|
|
582
|
-
return `
|
|
358
|
+
mockPolicyFile(nodePath.join(MOCK_DEFAULT_DIR, 'invalid_safety.toml'), `
|
|
583
359
|
[[rule]]
|
|
584
360
|
toolName = "write_file"
|
|
585
361
|
decision = "allow"
|
|
@@ -591,75 +367,20 @@ priority = 10
|
|
|
591
367
|
[safety_checker.checker]
|
|
592
368
|
type = "in-process"
|
|
593
369
|
name = "invalid-name"
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
});
|
|
598
|
-
const mockStat = vi.fn(async (path, options) => {
|
|
599
|
-
if (typeof path === 'string' &&
|
|
600
|
-
nodePath
|
|
601
|
-
.normalize(path)
|
|
602
|
-
.includes(nodePath.normalize('.gemini/policies'))) {
|
|
603
|
-
return {
|
|
604
|
-
isDirectory: () => true,
|
|
605
|
-
isFile: () => false,
|
|
606
|
-
};
|
|
607
|
-
}
|
|
608
|
-
return actualFs.stat(path, options);
|
|
609
|
-
});
|
|
610
|
-
vi.doMock('node:fs/promises', () => ({
|
|
611
|
-
...actualFs,
|
|
612
|
-
default: {
|
|
613
|
-
...actualFs,
|
|
614
|
-
readFile: mockReadFile,
|
|
615
|
-
readdir: mockReaddir,
|
|
616
|
-
stat: mockStat,
|
|
617
|
-
},
|
|
618
|
-
readFile: mockReadFile,
|
|
619
|
-
readdir: mockReaddir,
|
|
620
|
-
stat: mockStat,
|
|
621
|
-
}));
|
|
622
|
-
vi.resetModules();
|
|
623
|
-
const { createPolicyEngineConfig } = await import('./config.js');
|
|
624
|
-
const settings = {};
|
|
625
|
-
const config = await createPolicyEngineConfig(settings, ApprovalMode.DEFAULT, '/tmp/mock/default/policies');
|
|
626
|
-
// The rule should be rejected because 'invalid-name' is not in the enum
|
|
627
|
-
const rule = config.rules?.find((r) => r.toolName === 'write_file');
|
|
628
|
-
expect(rule).toBeUndefined();
|
|
629
|
-
vi.doUnmock('node:fs/promises');
|
|
370
|
+
`);
|
|
371
|
+
const config = await createPolicyEngineConfig({}, ApprovalMode.DEFAULT, MOCK_DEFAULT_DIR);
|
|
372
|
+
expect(config.rules?.find((r) => r.toolName === 'write_file')).toBeUndefined();
|
|
630
373
|
});
|
|
631
374
|
it('should have default ASK_USER rule for discovered tools', async () => {
|
|
632
|
-
|
|
633
|
-
vi.doUnmock('node:fs/promises');
|
|
634
|
-
const { createPolicyEngineConfig: createConfig } = await import('./config.js');
|
|
635
|
-
// Re-mock Storage after resetModules because it was reloaded
|
|
636
|
-
const { Storage: FreshStorage } = await import('../config/storage.js');
|
|
637
|
-
vi.spyOn(FreshStorage, 'getUserPoliciesDir').mockReturnValue('/non/existent/user/policies');
|
|
638
|
-
vi.spyOn(FreshStorage, 'getSystemPoliciesDir').mockReturnValue('/non/existent/system/policies');
|
|
639
|
-
const settings = {};
|
|
640
|
-
// Use default policy dir to load real discovered.toml
|
|
641
|
-
const config = await createConfig(settings, ApprovalMode.DEFAULT);
|
|
375
|
+
const config = await createPolicyEngineConfig({}, ApprovalMode.DEFAULT);
|
|
642
376
|
const discoveredRule = config.rules?.find((r) => r.toolName === 'discovered_tool_*' &&
|
|
643
377
|
r.decision === PolicyDecision.ASK_USER);
|
|
644
378
|
expect(discoveredRule).toBeDefined();
|
|
645
|
-
// Priority 10 in default tier → 1.010
|
|
646
379
|
expect(discoveredRule?.priority).toBeCloseTo(1.01, 5);
|
|
647
380
|
});
|
|
648
381
|
it('should normalize legacy "ShellTool" alias to "run_shell_command"', async () => {
|
|
649
|
-
vi.
|
|
650
|
-
|
|
651
|
-
const actualFs = await vi.importActual('node:fs/promises');
|
|
652
|
-
const mockReaddir = vi.fn(async () => []);
|
|
653
|
-
vi.doMock('node:fs/promises', () => ({
|
|
654
|
-
...actualFs,
|
|
655
|
-
default: { ...actualFs, readdir: mockReaddir },
|
|
656
|
-
readdir: mockReaddir,
|
|
657
|
-
}));
|
|
658
|
-
const { createPolicyEngineConfig } = await import('./config.js');
|
|
659
|
-
const settings = {
|
|
660
|
-
tools: { allowed: ['ShellTool'] },
|
|
661
|
-
};
|
|
662
|
-
const config = await createPolicyEngineConfig(settings, ApprovalMode.DEFAULT, '/tmp/mock/default/policies');
|
|
382
|
+
vi.mocked(fs.readdir).mockResolvedValue([]);
|
|
383
|
+
const config = await createPolicyEngineConfig({ tools: { allowed: ['ShellTool'] } }, ApprovalMode.DEFAULT, MOCK_DEFAULT_DIR);
|
|
663
384
|
const rule = config.rules?.find((r) => r.toolName === 'run_shell_command' &&
|
|
664
385
|
r.decision === PolicyDecision.ALLOW);
|
|
665
386
|
expect(rule).toBeDefined();
|
|
@@ -667,34 +388,9 @@ name = "invalid-name"
|
|
|
667
388
|
vi.doUnmock('node:fs/promises');
|
|
668
389
|
});
|
|
669
390
|
it('should allow overriding Plan Mode deny with user policy', async () => {
|
|
670
|
-
const
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
if (normalizedPath.includes('gemini-cli-test/user/policies')) {
|
|
674
|
-
return [
|
|
675
|
-
{
|
|
676
|
-
name: 'user-plan.toml',
|
|
677
|
-
isFile: () => true,
|
|
678
|
-
isDirectory: () => false,
|
|
679
|
-
},
|
|
680
|
-
];
|
|
681
|
-
}
|
|
682
|
-
return actualFs.readdir(path, options);
|
|
683
|
-
});
|
|
684
|
-
const mockStat = vi.fn(async (path, options) => {
|
|
685
|
-
const normalizedPath = nodePath.normalize(path.toString());
|
|
686
|
-
if (normalizedPath.includes('gemini-cli-test/user/policies')) {
|
|
687
|
-
return {
|
|
688
|
-
isDirectory: () => true,
|
|
689
|
-
isFile: () => false,
|
|
690
|
-
};
|
|
691
|
-
}
|
|
692
|
-
return actualFs.stat(path, options);
|
|
693
|
-
});
|
|
694
|
-
const mockReadFile = vi.fn(async (path, options) => {
|
|
695
|
-
const normalizedPath = nodePath.normalize(path.toString());
|
|
696
|
-
if (normalizedPath.includes('user-plan.toml')) {
|
|
697
|
-
return `
|
|
391
|
+
const userPolicyDir = '/tmp/gemini-cli-test/user/policies';
|
|
392
|
+
vi.spyOn(Storage, 'getUserPoliciesDir').mockReturnValue(userPolicyDir);
|
|
393
|
+
mockPolicyFile(nodePath.join(userPolicyDir, 'user-plan.toml'), `
|
|
698
394
|
[[rule]]
|
|
699
395
|
toolName = "run_shell_command"
|
|
700
396
|
commandPrefix = ["git status", "git diff"]
|
|
@@ -707,53 +403,77 @@ toolName = "codebase_investigator"
|
|
|
707
403
|
decision = "allow"
|
|
708
404
|
priority = 100
|
|
709
405
|
modes = ["plan"]
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
return actualFs.readFile(path, options);
|
|
713
|
-
});
|
|
714
|
-
vi.doMock('node:fs/promises', () => ({
|
|
715
|
-
...actualFs,
|
|
716
|
-
default: {
|
|
717
|
-
...actualFs,
|
|
718
|
-
readFile: mockReadFile,
|
|
719
|
-
readdir: mockReaddir,
|
|
720
|
-
stat: mockStat,
|
|
721
|
-
},
|
|
722
|
-
readFile: mockReadFile,
|
|
723
|
-
readdir: mockReaddir,
|
|
724
|
-
stat: mockStat,
|
|
725
|
-
}));
|
|
726
|
-
vi.resetModules();
|
|
727
|
-
// Robustly mock Storage using doMock to ensure it persists through imports in config.js
|
|
728
|
-
vi.doMock('../config/storage.js', async () => {
|
|
729
|
-
const actual = await vi.importActual('../config/storage.js');
|
|
730
|
-
class MockStorage extends actual.Storage {
|
|
731
|
-
static getUserPoliciesDir() {
|
|
732
|
-
return '/tmp/gemini-cli-test/user/policies';
|
|
733
|
-
}
|
|
734
|
-
static getSystemPoliciesDir() {
|
|
735
|
-
return '/tmp/gemini-cli-test/system/policies';
|
|
736
|
-
}
|
|
737
|
-
}
|
|
738
|
-
return { ...actual, Storage: MockStorage };
|
|
739
|
-
});
|
|
740
|
-
const { createPolicyEngineConfig } = await import('./config.js');
|
|
741
|
-
const settings = {};
|
|
742
|
-
const config = await createPolicyEngineConfig(settings, ApprovalMode.PLAN, nodePath.join(__dirname, 'policies'));
|
|
406
|
+
`);
|
|
407
|
+
const config = await createPolicyEngineConfig({}, ApprovalMode.PLAN, nodePath.join(__dirname, 'policies'));
|
|
743
408
|
const shellRules = config.rules?.filter((r) => r.toolName === 'run_shell_command' &&
|
|
744
|
-
r.decision === PolicyDecision.ALLOW &&
|
|
745
|
-
r.modes?.includes(ApprovalMode.PLAN) &&
|
|
746
|
-
r.argsPattern);
|
|
747
|
-
expect(shellRules).toHaveLength(2);
|
|
748
|
-
expect(shellRules?.some((r) => r.argsPattern?.test('{"command":"git status"}'))).toBe(true);
|
|
749
|
-
expect(shellRules?.some((r) => r.argsPattern?.test('{"command":"git diff"}'))).toBe(true);
|
|
750
|
-
expect(shellRules?.every((r) => !r.argsPattern?.test('{"command":"git commit"}'))).toBe(true);
|
|
751
|
-
const subagentRule = config.rules?.find((r) => r.toolName === 'codebase_investigator' &&
|
|
752
409
|
r.decision === PolicyDecision.ALLOW &&
|
|
753
410
|
r.modes?.includes(ApprovalMode.PLAN));
|
|
411
|
+
expect(shellRules?.length).toBeGreaterThan(0);
|
|
412
|
+
shellRules?.forEach((r) => expect(r.priority).toBeCloseTo(4.1, 5));
|
|
413
|
+
const subagentRule = config.rules?.find((r) => r.toolName === 'codebase_investigator' &&
|
|
414
|
+
r.decision === PolicyDecision.ALLOW);
|
|
754
415
|
expect(subagentRule).toBeDefined();
|
|
755
416
|
expect(subagentRule?.priority).toBeCloseTo(4.1, 5);
|
|
756
|
-
|
|
417
|
+
});
|
|
418
|
+
it('should deduplicate security warnings when called multiple times', async () => {
|
|
419
|
+
const systemPoliciesDir = '/tmp/gemini-cli-test/system/policies';
|
|
420
|
+
vi.spyOn(Storage, 'getSystemPoliciesDir').mockReturnValue(systemPoliciesDir);
|
|
421
|
+
vi.mocked(fs.readdir).mockImplementation(async (path) => {
|
|
422
|
+
if (nodePath.resolve(path.toString()) === systemPoliciesDir) {
|
|
423
|
+
return ['policy.toml'];
|
|
424
|
+
}
|
|
425
|
+
return [];
|
|
426
|
+
});
|
|
427
|
+
const feedbackSpy = vi
|
|
428
|
+
.spyOn(coreEvents, 'emitFeedback')
|
|
429
|
+
.mockImplementation(() => { });
|
|
430
|
+
// First call
|
|
431
|
+
await createPolicyEngineConfig({ adminPolicyPaths: ['/tmp/other/admin/policies'] }, ApprovalMode.DEFAULT);
|
|
432
|
+
expect(feedbackSpy).toHaveBeenCalledWith('warning', expect.stringContaining('Ignoring --admin-policy'));
|
|
433
|
+
const count = feedbackSpy.mock.calls.length;
|
|
434
|
+
// Second call
|
|
435
|
+
await createPolicyEngineConfig({ adminPolicyPaths: ['/tmp/other/admin/policies'] }, ApprovalMode.DEFAULT);
|
|
436
|
+
expect(feedbackSpy.mock.calls.length).toBe(count);
|
|
437
|
+
feedbackSpy.mockRestore();
|
|
438
|
+
});
|
|
439
|
+
});
|
|
440
|
+
describe('getPolicyDirectories', () => {
|
|
441
|
+
const USER_POLICIES_DIR = '/mock/user/policies';
|
|
442
|
+
const SYSTEM_POLICIES_DIR = '/mock/system/policies';
|
|
443
|
+
beforeEach(() => {
|
|
444
|
+
vi.spyOn(Storage, 'getUserPoliciesDir').mockReturnValue(USER_POLICIES_DIR);
|
|
445
|
+
vi.spyOn(Storage, 'getSystemPoliciesDir').mockReturnValue(SYSTEM_POLICIES_DIR);
|
|
446
|
+
});
|
|
447
|
+
it('should include default user policies directory when policyPaths is undefined', () => {
|
|
448
|
+
const dirs = getPolicyDirectories();
|
|
449
|
+
expect(dirs).toContain(USER_POLICIES_DIR);
|
|
450
|
+
});
|
|
451
|
+
it('should include default user policies directory when policyPaths is an empty array', () => {
|
|
452
|
+
// This is the specific case that regressed
|
|
453
|
+
const dirs = getPolicyDirectories(undefined, []);
|
|
454
|
+
expect(dirs).toContain(USER_POLICIES_DIR);
|
|
455
|
+
});
|
|
456
|
+
it('should replace default user policies directory when policyPaths has entries', () => {
|
|
457
|
+
const customPath = '/custom/policies';
|
|
458
|
+
const dirs = getPolicyDirectories(undefined, [customPath]);
|
|
459
|
+
expect(dirs).toContain(customPath);
|
|
460
|
+
expect(dirs).not.toContain(USER_POLICIES_DIR);
|
|
461
|
+
});
|
|
462
|
+
it('should include all tiers in correct order', () => {
|
|
463
|
+
const defaultDir = '/default/policies';
|
|
464
|
+
const workspaceDir = '/workspace/policies';
|
|
465
|
+
const adminPath = '/admin/extra/policies';
|
|
466
|
+
const userPath = '/user/custom/policies';
|
|
467
|
+
const dirs = getPolicyDirectories(defaultDir, [userPath], workspaceDir, [
|
|
468
|
+
adminPath,
|
|
469
|
+
]);
|
|
470
|
+
// Order should be Admin -> User -> Workspace -> Default
|
|
471
|
+
// getPolicyDirectories returns them in that order (which is then reversed by the loader)
|
|
472
|
+
expect(dirs[0]).toBe(SYSTEM_POLICIES_DIR);
|
|
473
|
+
expect(dirs[1]).toBe(adminPath);
|
|
474
|
+
expect(dirs[2]).toBe(userPath);
|
|
475
|
+
expect(dirs[3]).toBe(workspaceDir);
|
|
476
|
+
expect(dirs[4]).toBe(defaultDir);
|
|
757
477
|
});
|
|
758
478
|
});
|
|
759
479
|
//# sourceMappingURL=config.test.js.map
|