@google/gemini-cli-core 0.13.0-preview.2 → 0.14.0-preview.0

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.
Files changed (134) hide show
  1. package/dist/src/agents/executor.d.ts +3 -0
  2. package/dist/src/agents/executor.js +21 -0
  3. package/dist/src/agents/executor.js.map +1 -1
  4. package/dist/src/agents/executor.test.js +177 -2
  5. package/dist/src/agents/executor.test.js.map +1 -1
  6. package/dist/src/config/config.d.ts +4 -0
  7. package/dist/src/config/config.js +21 -5
  8. package/dist/src/config/config.js.map +1 -1
  9. package/dist/src/config/config.test.js +88 -0
  10. package/dist/src/config/config.test.js.map +1 -1
  11. package/dist/src/config/defaultModelConfigs.d.ts +7 -0
  12. package/dist/src/config/defaultModelConfigs.js +127 -0
  13. package/dist/src/config/defaultModelConfigs.js.map +1 -0
  14. package/dist/src/confirmation-bus/message-bus.js +1 -1
  15. package/dist/src/confirmation-bus/message-bus.js.map +1 -1
  16. package/dist/src/confirmation-bus/types.d.ts +1 -0
  17. package/dist/src/core/prompts.js +0 -2
  18. package/dist/src/core/prompts.js.map +1 -1
  19. package/dist/src/fallback/handler.js +1 -3
  20. package/dist/src/fallback/handler.js.map +1 -1
  21. package/dist/src/fallback/handler.test.js +4 -12
  22. package/dist/src/fallback/handler.test.js.map +1 -1
  23. package/dist/src/fallback/types.d.ts +1 -1
  24. package/dist/src/generated/git-commit.d.ts +2 -2
  25. package/dist/src/generated/git-commit.js +2 -2
  26. package/dist/src/index.d.ts +1 -0
  27. package/dist/src/index.js +1 -0
  28. package/dist/src/index.js.map +1 -1
  29. package/dist/src/policy/config.test.js +17 -0
  30. package/dist/src/policy/config.test.js.map +1 -1
  31. package/dist/src/policy/policies/discovered.toml +8 -0
  32. package/dist/src/policy/policy-engine.d.ts +1 -1
  33. package/dist/src/policy/policy-engine.js +11 -3
  34. package/dist/src/policy/policy-engine.js.map +1 -1
  35. package/dist/src/policy/policy-engine.test.js +98 -50
  36. package/dist/src/policy/policy-engine.test.js.map +1 -1
  37. package/dist/src/policy/toml-loader.test.js +220 -336
  38. package/dist/src/policy/toml-loader.test.js.map +1 -1
  39. package/dist/src/services/modelConfig.golden.test.d.ts +6 -0
  40. package/dist/src/services/modelConfig.golden.test.js +42 -0
  41. package/dist/src/services/modelConfig.golden.test.js.map +1 -0
  42. package/dist/src/services/modelConfig.integration.test.d.ts +6 -0
  43. package/dist/src/services/modelConfig.integration.test.js +213 -0
  44. package/dist/src/services/modelConfig.integration.test.js.map +1 -0
  45. package/dist/src/services/modelConfigService.d.ts +46 -0
  46. package/dist/src/services/modelConfigService.js +146 -0
  47. package/dist/src/services/modelConfigService.js.map +1 -0
  48. package/dist/src/services/modelConfigService.test.d.ts +6 -0
  49. package/dist/src/services/modelConfigService.test.js +509 -0
  50. package/dist/src/services/modelConfigService.test.js.map +1 -0
  51. package/dist/src/services/test-data/resolved-aliases.golden.json +123 -0
  52. package/dist/src/tools/base-tool-invocation.test.d.ts +6 -0
  53. package/dist/src/tools/base-tool-invocation.test.js +85 -0
  54. package/dist/src/tools/base-tool-invocation.test.js.map +1 -0
  55. package/dist/src/tools/edit.d.ts +1 -1
  56. package/dist/src/tools/edit.js +31 -33
  57. package/dist/src/tools/edit.js.map +1 -1
  58. package/dist/src/tools/edit.test.js +30 -20
  59. package/dist/src/tools/edit.test.js.map +1 -1
  60. package/dist/src/tools/glob.d.ts +1 -1
  61. package/dist/src/tools/glob.js +7 -7
  62. package/dist/src/tools/glob.js.map +1 -1
  63. package/dist/src/tools/glob.test.js +20 -17
  64. package/dist/src/tools/glob.test.js.map +1 -1
  65. package/dist/src/tools/grep.d.ts +1 -1
  66. package/dist/src/tools/grep.js +9 -9
  67. package/dist/src/tools/grep.js.map +1 -1
  68. package/dist/src/tools/grep.test.js +15 -12
  69. package/dist/src/tools/grep.test.js.map +1 -1
  70. package/dist/src/tools/ls.d.ts +1 -1
  71. package/dist/src/tools/ls.js +14 -15
  72. package/dist/src/tools/ls.js.map +1 -1
  73. package/dist/src/tools/ls.test.js +32 -33
  74. package/dist/src/tools/ls.test.js.map +1 -1
  75. package/dist/src/tools/mcp-client.js +2 -0
  76. package/dist/src/tools/mcp-client.js.map +1 -1
  77. package/dist/src/tools/mcp-client.test.js +5 -0
  78. package/dist/src/tools/mcp-client.test.js.map +1 -1
  79. package/dist/src/tools/mcp-tool.js +1 -1
  80. package/dist/src/tools/mcp-tool.js.map +1 -1
  81. package/dist/src/tools/read-file.d.ts +2 -2
  82. package/dist/src/tools/read-file.js +20 -24
  83. package/dist/src/tools/read-file.js.map +1 -1
  84. package/dist/src/tools/read-file.test.js +62 -51
  85. package/dist/src/tools/read-file.test.js.map +1 -1
  86. package/dist/src/tools/read-many-files.d.ts +2 -9
  87. package/dist/src/tools/read-many-files.js +10 -21
  88. package/dist/src/tools/read-many-files.js.map +1 -1
  89. package/dist/src/tools/read-many-files.test.js +35 -35
  90. package/dist/src/tools/read-many-files.test.js.map +1 -1
  91. package/dist/src/tools/ripGrep.d.ts +1 -1
  92. package/dist/src/tools/ripGrep.js +8 -8
  93. package/dist/src/tools/ripGrep.js.map +1 -1
  94. package/dist/src/tools/ripGrep.test.js +23 -14
  95. package/dist/src/tools/ripGrep.test.js.map +1 -1
  96. package/dist/src/tools/shell.d.ts +1 -1
  97. package/dist/src/tools/shell.js +16 -14
  98. package/dist/src/tools/shell.js.map +1 -1
  99. package/dist/src/tools/shell.test.js +64 -34
  100. package/dist/src/tools/shell.test.js.map +1 -1
  101. package/dist/src/tools/smart-edit.d.ts +1 -1
  102. package/dist/src/tools/smart-edit.js +8 -11
  103. package/dist/src/tools/smart-edit.js.map +1 -1
  104. package/dist/src/tools/tool-registry.d.ts +12 -2
  105. package/dist/src/tools/tool-registry.js +49 -14
  106. package/dist/src/tools/tool-registry.js.map +1 -1
  107. package/dist/src/tools/tool-registry.test.js +70 -4
  108. package/dist/src/tools/tool-registry.test.js.map +1 -1
  109. package/dist/src/tools/tools.d.ts +2 -1
  110. package/dist/src/tools/tools.js +4 -1
  111. package/dist/src/tools/tools.js.map +1 -1
  112. package/dist/src/tools/write-file.js +31 -31
  113. package/dist/src/tools/write-file.js.map +1 -1
  114. package/dist/src/tools/write-file.test.js +23 -5
  115. package/dist/src/tools/write-file.test.js.map +1 -1
  116. package/dist/src/utils/extensionLoader.js +7 -3
  117. package/dist/src/utils/extensionLoader.js.map +1 -1
  118. package/dist/src/utils/fileUtils.test.js +75 -60
  119. package/dist/src/utils/fileUtils.test.js.map +1 -1
  120. package/dist/src/utils/pathReader.js +4 -4
  121. package/dist/src/utils/pathReader.js.map +1 -1
  122. package/dist/src/utils/pathReader.test.js +44 -1
  123. package/dist/src/utils/pathReader.test.js.map +1 -1
  124. package/dist/src/utils/paths.d.ts +1 -1
  125. package/dist/src/utils/paths.js +5 -3
  126. package/dist/src/utils/paths.js.map +1 -1
  127. package/dist/src/utils/workspaceContext.d.ts +4 -3
  128. package/dist/src/utils/workspaceContext.js +10 -11
  129. package/dist/src/utils/workspaceContext.js.map +1 -1
  130. package/dist/src/utils/workspaceContext.test.js +1 -1
  131. package/dist/src/utils/workspaceContext.test.js.map +1 -1
  132. package/dist/tsconfig.tsbuildinfo +1 -1
  133. package/package.json +1 -1
  134. package/dist/google-gemini-cli-core-0.13.0-preview.1.tgz +0 -0
@@ -6,6 +6,37 @@
6
6
  import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
7
7
  import { ApprovalMode, PolicyDecision } from './types.js';
8
8
  import nodePath from 'node:path';
9
+ async function runLoadPoliciesFromToml(tomlContent, fileName = 'test.toml') {
10
+ const actualFs = await vi.importActual('node:fs/promises');
11
+ const mockReaddir = vi.fn(async (path, _options) => {
12
+ if (nodePath.normalize(path) === nodePath.normalize('/policies')) {
13
+ return [
14
+ {
15
+ name: fileName,
16
+ isFile: () => true,
17
+ isDirectory: () => false,
18
+ },
19
+ ];
20
+ }
21
+ return [];
22
+ });
23
+ const mockReadFile = vi.fn(async (path) => {
24
+ if (nodePath.normalize(path) ===
25
+ nodePath.normalize(nodePath.join('/policies', fileName))) {
26
+ return tomlContent;
27
+ }
28
+ throw new Error('File not found');
29
+ });
30
+ vi.doMock('node:fs/promises', () => ({
31
+ ...actualFs,
32
+ default: { ...actualFs, readFile: mockReadFile, readdir: mockReaddir },
33
+ readFile: mockReadFile,
34
+ readdir: mockReaddir,
35
+ }));
36
+ const { loadPoliciesFromToml: load } = await import('./toml-loader.js');
37
+ const getPolicyTier = (_dir) => 1;
38
+ return load(ApprovalMode.DEFAULT, ['/policies'], getPolicyTier);
39
+ }
9
40
  describe('policy-toml-loader', () => {
10
41
  beforeEach(() => {
11
42
  vi.resetModules();
@@ -16,40 +47,12 @@ describe('policy-toml-loader', () => {
16
47
  });
17
48
  describe('loadPoliciesFromToml', () => {
18
49
  it('should load and parse a simple policy file', async () => {
19
- const actualFs = await vi.importActual('node:fs/promises');
20
- const mockReaddir = vi.fn(async (path, _options) => {
21
- if (nodePath.normalize(path) === nodePath.normalize('/policies')) {
22
- return [
23
- {
24
- name: 'test.toml',
25
- isFile: () => true,
26
- isDirectory: () => false,
27
- },
28
- ];
29
- }
30
- return [];
31
- });
32
- const mockReadFile = vi.fn(async (path) => {
33
- if (nodePath.normalize(path) ===
34
- nodePath.normalize(nodePath.join('/policies', 'test.toml'))) {
35
- return `
50
+ const result = await runLoadPoliciesFromToml(`
36
51
  [[rule]]
37
52
  toolName = "glob"
38
53
  decision = "allow"
39
54
  priority = 100
40
- `;
41
- }
42
- throw new Error('File not found');
43
- });
44
- vi.doMock('node:fs/promises', () => ({
45
- ...actualFs,
46
- default: { ...actualFs, readFile: mockReadFile, readdir: mockReaddir },
47
- readFile: mockReadFile,
48
- readdir: mockReaddir,
49
- }));
50
- const { loadPoliciesFromToml: load } = await import('./toml-loader.js');
51
- const getPolicyTier = (_dir) => 1;
52
- const result = await load(ApprovalMode.DEFAULT, ['/policies'], getPolicyTier);
55
+ `);
53
56
  expect(result.rules).toHaveLength(1);
54
57
  expect(result.rules[0]).toEqual({
55
58
  toolName: 'glob',
@@ -59,41 +62,13 @@ priority = 100
59
62
  expect(result.errors).toHaveLength(0);
60
63
  });
61
64
  it('should expand commandPrefix array to multiple rules', async () => {
62
- const actualFs = await vi.importActual('node:fs/promises');
63
- const mockReaddir = vi.fn(async (path, _options) => {
64
- if (nodePath.normalize(path) === nodePath.normalize('/policies')) {
65
- return [
66
- {
67
- name: 'shell.toml',
68
- isFile: () => true,
69
- isDirectory: () => false,
70
- },
71
- ];
72
- }
73
- return [];
74
- });
75
- const mockReadFile = vi.fn(async (path) => {
76
- if (nodePath.normalize(path) ===
77
- nodePath.normalize(nodePath.join('/policies', 'shell.toml'))) {
78
- return `
65
+ const result = await runLoadPoliciesFromToml(`
79
66
  [[rule]]
80
67
  toolName = "run_shell_command"
81
68
  commandPrefix = ["git status", "git log"]
82
69
  decision = "allow"
83
70
  priority = 100
84
- `;
85
- }
86
- throw new Error('File not found');
87
- });
88
- vi.doMock('node:fs/promises', () => ({
89
- ...actualFs,
90
- default: { ...actualFs, readFile: mockReadFile, readdir: mockReaddir },
91
- readFile: mockReadFile,
92
- readdir: mockReaddir,
93
- }));
94
- const { loadPoliciesFromToml: load } = await import('./toml-loader.js');
95
- const getPolicyTier = (_dir) => 2;
96
- const result = await load(ApprovalMode.DEFAULT, ['/policies'], getPolicyTier);
71
+ `);
97
72
  expect(result.rules).toHaveLength(2);
98
73
  expect(result.rules[0].toolName).toBe('run_shell_command');
99
74
  expect(result.rules[1].toolName).toBe('run_shell_command');
@@ -102,41 +77,13 @@ priority = 100
102
77
  expect(result.errors).toHaveLength(0);
103
78
  });
104
79
  it('should transform commandRegex to argsPattern', async () => {
105
- const actualFs = await vi.importActual('node:fs/promises');
106
- const mockReaddir = vi.fn(async (path, _options) => {
107
- if (nodePath.normalize(path) === nodePath.normalize('/policies')) {
108
- return [
109
- {
110
- name: 'shell.toml',
111
- isFile: () => true,
112
- isDirectory: () => false,
113
- },
114
- ];
115
- }
116
- return [];
117
- });
118
- const mockReadFile = vi.fn(async (path) => {
119
- if (nodePath.normalize(path) ===
120
- nodePath.normalize(nodePath.join('/policies', 'shell.toml'))) {
121
- return `
80
+ const result = await runLoadPoliciesFromToml(`
122
81
  [[rule]]
123
82
  toolName = "run_shell_command"
124
83
  commandRegex = "git (status|log).*"
125
84
  decision = "allow"
126
85
  priority = 100
127
- `;
128
- }
129
- throw new Error('File not found');
130
- });
131
- vi.doMock('node:fs/promises', () => ({
132
- ...actualFs,
133
- default: { ...actualFs, readFile: mockReadFile, readdir: mockReaddir },
134
- readFile: mockReadFile,
135
- readdir: mockReaddir,
136
- }));
137
- const { loadPoliciesFromToml: load } = await import('./toml-loader.js');
138
- const getPolicyTier = (_dir) => 2;
139
- const result = await load(ApprovalMode.DEFAULT, ['/policies'], getPolicyTier);
86
+ `);
140
87
  expect(result.rules).toHaveLength(1);
141
88
  expect(result.rules[0].argsPattern?.test('{"command":"git status"}')).toBe(true);
142
89
  expect(result.rules[0].argsPattern?.test('{"command":"git log --all"}')).toBe(true);
@@ -144,40 +91,12 @@ priority = 100
144
91
  expect(result.errors).toHaveLength(0);
145
92
  });
146
93
  it('should expand toolName array', async () => {
147
- const actualFs = await vi.importActual('node:fs/promises');
148
- const mockReaddir = vi.fn(async (path, _options) => {
149
- if (nodePath.normalize(path) === nodePath.normalize('/policies')) {
150
- return [
151
- {
152
- name: 'tools.toml',
153
- isFile: () => true,
154
- isDirectory: () => false,
155
- },
156
- ];
157
- }
158
- return [];
159
- });
160
- const mockReadFile = vi.fn(async (path) => {
161
- if (nodePath.normalize(path) ===
162
- nodePath.normalize(nodePath.join('/policies', 'tools.toml'))) {
163
- return `
94
+ const result = await runLoadPoliciesFromToml(`
164
95
  [[rule]]
165
96
  toolName = ["glob", "grep", "read"]
166
97
  decision = "allow"
167
98
  priority = 100
168
- `;
169
- }
170
- throw new Error('File not found');
171
- });
172
- vi.doMock('node:fs/promises', () => ({
173
- ...actualFs,
174
- default: { ...actualFs, readFile: mockReadFile, readdir: mockReaddir },
175
- readFile: mockReadFile,
176
- readdir: mockReaddir,
177
- }));
178
- const { loadPoliciesFromToml: load } = await import('./toml-loader.js');
179
- const getPolicyTier = (_dir) => 1;
180
- const result = await load(ApprovalMode.DEFAULT, ['/policies'], getPolicyTier);
99
+ `);
181
100
  expect(result.rules).toHaveLength(3);
182
101
  expect(result.rules.map((r) => r.toolName)).toEqual([
183
102
  'glob',
@@ -187,64 +106,20 @@ priority = 100
187
106
  expect(result.errors).toHaveLength(0);
188
107
  });
189
108
  it('should transform mcpName to composite toolName', async () => {
190
- const actualFs = await vi.importActual('node:fs/promises');
191
- const mockReaddir = vi.fn(async (path, _options) => {
192
- if (nodePath.normalize(path) === nodePath.normalize('/policies')) {
193
- return [
194
- {
195
- name: 'mcp.toml',
196
- isFile: () => true,
197
- isDirectory: () => false,
198
- },
199
- ];
200
- }
201
- return [];
202
- });
203
- const mockReadFile = vi.fn(async (path) => {
204
- if (nodePath.normalize(path) ===
205
- nodePath.normalize(nodePath.join('/policies', 'mcp.toml'))) {
206
- return `
109
+ const result = await runLoadPoliciesFromToml(`
207
110
  [[rule]]
208
111
  mcpName = "google-workspace"
209
112
  toolName = ["calendar.list", "calendar.get"]
210
113
  decision = "allow"
211
114
  priority = 100
212
- `;
213
- }
214
- throw new Error('File not found');
215
- });
216
- vi.doMock('node:fs/promises', () => ({
217
- ...actualFs,
218
- default: { ...actualFs, readFile: mockReadFile, readdir: mockReaddir },
219
- readFile: mockReadFile,
220
- readdir: mockReaddir,
221
- }));
222
- const { loadPoliciesFromToml: load } = await import('./toml-loader.js');
223
- const getPolicyTier = (_dir) => 2;
224
- const result = await load(ApprovalMode.DEFAULT, ['/policies'], getPolicyTier);
115
+ `);
225
116
  expect(result.rules).toHaveLength(2);
226
117
  expect(result.rules[0].toolName).toBe('google-workspace__calendar.list');
227
118
  expect(result.rules[1].toolName).toBe('google-workspace__calendar.get');
228
119
  expect(result.errors).toHaveLength(0);
229
120
  });
230
121
  it('should filter rules by mode', async () => {
231
- const actualFs = await vi.importActual('node:fs/promises');
232
- const mockReaddir = vi.fn(async (path, _options) => {
233
- if (nodePath.normalize(path) === nodePath.normalize('/policies')) {
234
- return [
235
- {
236
- name: 'modes.toml',
237
- isFile: () => true,
238
- isDirectory: () => false,
239
- },
240
- ];
241
- }
242
- return [];
243
- });
244
- const mockReadFile = vi.fn(async (path) => {
245
- if (nodePath.normalize(path) ===
246
- nodePath.normalize(nodePath.join('/policies', 'modes.toml'))) {
247
- return `
122
+ const result = await runLoadPoliciesFromToml(`
248
123
  [[rule]]
249
124
  toolName = "glob"
250
125
  decision = "allow"
@@ -256,189 +131,97 @@ toolName = "grep"
256
131
  decision = "allow"
257
132
  priority = 100
258
133
  modes = ["yolo"]
259
- `;
260
- }
261
- throw new Error('File not found');
262
- });
263
- vi.doMock('node:fs/promises', () => ({
264
- ...actualFs,
265
- default: { ...actualFs, readFile: mockReadFile, readdir: mockReaddir },
266
- readFile: mockReadFile,
267
- readdir: mockReaddir,
268
- }));
269
- const { loadPoliciesFromToml: load } = await import('./toml-loader.js');
270
- const getPolicyTier = (_dir) => 1;
271
- const result = await load(ApprovalMode.DEFAULT, ['/policies'], getPolicyTier);
134
+ `);
272
135
  // Only the first rule should be included (modes includes "default")
273
136
  expect(result.rules).toHaveLength(1);
274
137
  expect(result.rules[0].toolName).toBe('glob');
275
138
  expect(result.errors).toHaveLength(0);
276
139
  });
277
140
  it('should handle TOML parse errors', async () => {
278
- const actualFs = await vi.importActual('node:fs/promises');
279
- const mockReaddir = vi.fn(async (path, _options) => {
280
- if (nodePath.normalize(path) === nodePath.normalize('/policies')) {
281
- return [
282
- {
283
- name: 'invalid.toml',
284
- isFile: () => true,
285
- isDirectory: () => false,
286
- },
287
- ];
288
- }
289
- return [];
290
- });
291
- const mockReadFile = vi.fn(async (path) => {
292
- if (nodePath.normalize(path) ===
293
- nodePath.normalize(nodePath.join('/policies', 'invalid.toml'))) {
294
- return `
141
+ const result = await runLoadPoliciesFromToml(`
295
142
  [[rule]
296
143
  toolName = "glob"
297
144
  decision = "allow"
298
145
  priority = 100
299
- `;
300
- }
301
- throw new Error('File not found');
302
- });
303
- vi.doMock('node:fs/promises', () => ({
304
- ...actualFs,
305
- default: { ...actualFs, readFile: mockReadFile, readdir: mockReaddir },
306
- readFile: mockReadFile,
307
- readdir: mockReaddir,
308
- }));
309
- const { loadPoliciesFromToml: load } = await import('./toml-loader.js');
310
- const getPolicyTier = (_dir) => 1;
311
- const result = await load(ApprovalMode.DEFAULT, ['/policies'], getPolicyTier);
146
+ `);
312
147
  expect(result.rules).toHaveLength(0);
313
148
  expect(result.errors).toHaveLength(1);
314
149
  expect(result.errors[0].errorType).toBe('toml_parse');
315
- expect(result.errors[0].fileName).toBe('invalid.toml');
150
+ expect(result.errors[0].fileName).toBe('test.toml');
316
151
  });
317
152
  it('should handle schema validation errors', async () => {
318
- const actualFs = await vi.importActual('node:fs/promises');
319
- const mockReaddir = vi.fn(async (path, _options) => {
320
- if (nodePath.normalize(path) === nodePath.normalize('/policies')) {
321
- return [
322
- {
323
- name: 'invalid.toml',
324
- isFile: () => true,
325
- isDirectory: () => false,
326
- },
327
- ];
328
- }
329
- return [];
330
- });
331
- const mockReadFile = vi.fn(async (path) => {
332
- if (nodePath.normalize(path) ===
333
- nodePath.normalize(nodePath.join('/policies', 'invalid.toml'))) {
334
- return `
153
+ const result = await runLoadPoliciesFromToml(`
335
154
  [[rule]]
336
155
  toolName = "glob"
337
156
  priority = 100
338
- `;
339
- }
340
- throw new Error('File not found');
341
- });
342
- vi.doMock('node:fs/promises', () => ({
343
- ...actualFs,
344
- default: { ...actualFs, readFile: mockReadFile, readdir: mockReaddir },
345
- readFile: mockReadFile,
346
- readdir: mockReaddir,
347
- }));
348
- const { loadPoliciesFromToml: load } = await import('./toml-loader.js');
349
- const getPolicyTier = (_dir) => 1;
350
- const result = await load(ApprovalMode.DEFAULT, ['/policies'], getPolicyTier);
157
+ `);
351
158
  expect(result.rules).toHaveLength(0);
352
159
  expect(result.errors).toHaveLength(1);
353
160
  expect(result.errors[0].errorType).toBe('schema_validation');
354
161
  expect(result.errors[0].details).toContain('decision');
355
162
  });
356
163
  it('should reject commandPrefix without run_shell_command', async () => {
357
- const actualFs = await vi.importActual('node:fs/promises');
358
- const mockReaddir = vi.fn(async (path, _options) => {
359
- if (nodePath.normalize(path) === nodePath.normalize('/policies')) {
360
- return [
361
- {
362
- name: 'invalid.toml',
363
- isFile: () => true,
364
- isDirectory: () => false,
365
- },
366
- ];
367
- }
368
- return [];
369
- });
370
- const mockReadFile = vi.fn(async (path) => {
371
- if (nodePath.normalize(path) ===
372
- nodePath.normalize(nodePath.join('/policies', 'invalid.toml'))) {
373
- return `
164
+ const result = await runLoadPoliciesFromToml(`
374
165
  [[rule]]
375
166
  toolName = "glob"
376
167
  commandPrefix = "git status"
377
168
  decision = "allow"
378
169
  priority = 100
379
- `;
380
- }
381
- throw new Error('File not found');
382
- });
383
- vi.doMock('node:fs/promises', () => ({
384
- ...actualFs,
385
- default: { ...actualFs, readFile: mockReadFile, readdir: mockReaddir },
386
- readFile: mockReadFile,
387
- readdir: mockReaddir,
388
- }));
389
- const { loadPoliciesFromToml: load } = await import('./toml-loader.js');
390
- const getPolicyTier = (_dir) => 1;
391
- const result = await load(ApprovalMode.DEFAULT, ['/policies'], getPolicyTier);
170
+ `);
392
171
  expect(result.errors).toHaveLength(1);
393
172
  expect(result.errors[0].errorType).toBe('rule_validation');
394
173
  expect(result.errors[0].details).toContain('run_shell_command');
395
174
  });
396
175
  it('should reject commandPrefix + argsPattern combination', async () => {
397
- const actualFs = await vi.importActual('node:fs/promises');
398
- const mockReaddir = vi.fn(async (path, _options) => {
399
- if (nodePath.normalize(path) === nodePath.normalize('/policies')) {
400
- return [
401
- {
402
- name: 'invalid.toml',
403
- isFile: () => true,
404
- isDirectory: () => false,
405
- },
406
- ];
407
- }
408
- return [];
409
- });
410
- const mockReadFile = vi.fn(async (path) => {
411
- if (nodePath.normalize(path) ===
412
- nodePath.normalize(nodePath.join('/policies', 'invalid.toml'))) {
413
- return `
176
+ const result = await runLoadPoliciesFromToml(`
414
177
  [[rule]]
415
178
  toolName = "run_shell_command"
416
179
  commandPrefix = "git status"
417
180
  argsPattern = "test"
418
181
  decision = "allow"
419
182
  priority = 100
420
- `;
421
- }
422
- throw new Error('File not found');
423
- });
424
- vi.doMock('node:fs/promises', () => ({
425
- ...actualFs,
426
- default: { ...actualFs, readFile: mockReadFile, readdir: mockReaddir },
427
- readFile: mockReadFile,
428
- readdir: mockReaddir,
429
- }));
430
- const { loadPoliciesFromToml: load } = await import('./toml-loader.js');
431
- const getPolicyTier = (_dir) => 1;
432
- const result = await load(ApprovalMode.DEFAULT, ['/policies'], getPolicyTier);
183
+ `);
433
184
  expect(result.errors).toHaveLength(1);
434
185
  expect(result.errors[0].errorType).toBe('rule_validation');
435
186
  expect(result.errors[0].details).toContain('mutually exclusive');
436
187
  });
437
188
  it('should handle invalid regex patterns', async () => {
189
+ const result = await runLoadPoliciesFromToml(`
190
+ [[rule]]
191
+ toolName = "run_shell_command"
192
+ commandRegex = "git (status|branch"
193
+ decision = "allow"
194
+ priority = 100
195
+ `);
196
+ expect(result.rules).toHaveLength(0);
197
+ expect(result.errors).toHaveLength(1);
198
+ expect(result.errors[0].errorType).toBe('regex_compilation');
199
+ expect(result.errors[0].details).toContain('git (status|branch');
200
+ });
201
+ it('should escape regex special characters in commandPrefix', async () => {
202
+ const result = await runLoadPoliciesFromToml(`
203
+ [[rule]]
204
+ toolName = "run_shell_command"
205
+ commandPrefix = "git log *.txt"
206
+ decision = "allow"
207
+ priority = 100
208
+ `);
209
+ expect(result.rules).toHaveLength(1);
210
+ // The regex should have escaped the * and .
211
+ expect(result.rules[0].argsPattern?.test('{"command":"git log file.txt"}')).toBe(false);
212
+ expect(result.rules[0].argsPattern?.test('{"command":"git log *.txt"}')).toBe(true);
213
+ expect(result.errors).toHaveLength(0);
214
+ });
215
+ it('should handle a mix of valid and invalid policy files', async () => {
438
216
  const actualFs = await vi.importActual('node:fs/promises');
439
217
  const mockReaddir = vi.fn(async (path, _options) => {
440
218
  if (nodePath.normalize(path) === nodePath.normalize('/policies')) {
441
219
  return [
220
+ {
221
+ name: 'valid.toml',
222
+ isFile: () => true,
223
+ isDirectory: () => false,
224
+ },
442
225
  {
443
226
  name: 'invalid.toml',
444
227
  isFile: () => true,
@@ -450,13 +233,21 @@ priority = 100
450
233
  });
451
234
  const mockReadFile = vi.fn(async (path) => {
452
235
  if (nodePath.normalize(path) ===
453
- nodePath.normalize(nodePath.join('/policies', 'invalid.toml'))) {
236
+ nodePath.normalize(nodePath.join('/policies', 'valid.toml'))) {
454
237
  return `
455
238
  [[rule]]
456
- toolName = "run_shell_command"
457
- commandRegex = "git (status|branch"
239
+ toolName = "glob"
458
240
  decision = "allow"
459
241
  priority = 100
242
+ `;
243
+ }
244
+ if (nodePath.normalize(path) ===
245
+ nodePath.normalize(nodePath.join('/policies', 'invalid.toml'))) {
246
+ return `
247
+ [[rule]]
248
+ toolName = "grep"
249
+ decision = "allow"
250
+ priority = -1
460
251
  `;
461
252
  }
462
253
  throw new Error('File not found');
@@ -470,52 +261,145 @@ priority = 100
470
261
  const { loadPoliciesFromToml: load } = await import('./toml-loader.js');
471
262
  const getPolicyTier = (_dir) => 1;
472
263
  const result = await load(ApprovalMode.DEFAULT, ['/policies'], getPolicyTier);
473
- expect(result.rules).toHaveLength(0);
264
+ expect(result.rules).toHaveLength(1);
265
+ expect(result.rules[0].toolName).toBe('glob');
474
266
  expect(result.errors).toHaveLength(1);
475
- expect(result.errors[0].errorType).toBe('regex_compilation');
476
- expect(result.errors[0].details).toContain('git (status|branch');
267
+ expect(result.errors[0].fileName).toBe('invalid.toml');
268
+ expect(result.errors[0].errorType).toBe('schema_validation');
477
269
  });
478
- it('should escape regex special characters in commandPrefix', async () => {
479
- const actualFs = await vi.importActual('node:fs/promises');
480
- const mockReaddir = vi.fn(async (path, _options) => {
481
- if (nodePath.normalize(path) === nodePath.normalize('/policies')) {
482
- return [
483
- {
484
- name: 'shell.toml',
485
- isFile: () => true,
486
- isDirectory: () => false,
487
- },
488
- ];
489
- }
490
- return [];
491
- });
492
- const mockReadFile = vi.fn(async (path) => {
493
- if (nodePath.normalize(path) ===
494
- nodePath.normalize(nodePath.join('/policies', 'shell.toml'))) {
495
- return `
270
+ });
271
+ describe('Negative Tests', () => {
272
+ it('should return a schema_validation error if priority is missing', async () => {
273
+ const result = await runLoadPoliciesFromToml(`
274
+ [[rule]]
275
+ toolName = "test"
276
+ decision = "allow"
277
+ `);
278
+ expect(result.errors).toHaveLength(1);
279
+ const error = result.errors[0];
280
+ expect(error.errorType).toBe('schema_validation');
281
+ expect(error.details).toContain('priority');
282
+ });
283
+ it('should return a schema_validation error if priority is a float', async () => {
284
+ const result = await runLoadPoliciesFromToml(`
285
+ [[rule]]
286
+ toolName = "test"
287
+ decision = "allow"
288
+ priority = 1.5
289
+ `);
290
+ expect(result.errors).toHaveLength(1);
291
+ const error = result.errors[0];
292
+ expect(error.errorType).toBe('schema_validation');
293
+ expect(error.details).toContain('priority');
294
+ expect(error.details).toContain('integer');
295
+ });
296
+ it('should return a schema_validation error if priority is negative', async () => {
297
+ const result = await runLoadPoliciesFromToml(`
298
+ [[rule]]
299
+ toolName = "test"
300
+ decision = "allow"
301
+ priority = -1
302
+ `);
303
+ expect(result.errors).toHaveLength(1);
304
+ const error = result.errors[0];
305
+ expect(error.errorType).toBe('schema_validation');
306
+ expect(error.details).toContain('priority');
307
+ expect(error.details).toContain('>= 0');
308
+ });
309
+ it('should return a schema_validation error if priority is >= 1000', async () => {
310
+ const result = await runLoadPoliciesFromToml(`
311
+ [[rule]]
312
+ toolName = "test"
313
+ decision = "allow"
314
+ priority = 1000
315
+ `);
316
+ expect(result.errors).toHaveLength(1);
317
+ const error = result.errors[0];
318
+ expect(error.errorType).toBe('schema_validation');
319
+ expect(error.details).toContain('priority');
320
+ expect(error.details).toContain('<= 999');
321
+ });
322
+ it('should return a schema_validation error if decision is invalid', async () => {
323
+ const result = await runLoadPoliciesFromToml(`
324
+ [[rule]]
325
+ toolName = "test"
326
+ decision = "maybe"
327
+ priority = 100
328
+ `);
329
+ expect(result.errors).toHaveLength(1);
330
+ const error = result.errors[0];
331
+ expect(error.errorType).toBe('schema_validation');
332
+ expect(error.details).toContain('decision');
333
+ });
334
+ it('should return a schema_validation error if toolName is not a string or array', async () => {
335
+ const result = await runLoadPoliciesFromToml(`
336
+ [[rule]]
337
+ toolName = 123
338
+ decision = "allow"
339
+ priority = 100
340
+ `);
341
+ expect(result.errors).toHaveLength(1);
342
+ const error = result.errors[0];
343
+ expect(error.errorType).toBe('schema_validation');
344
+ expect(error.details).toContain('toolName');
345
+ });
346
+ it('should return a rule_validation error if commandRegex is used with wrong toolName', async () => {
347
+ const result = await runLoadPoliciesFromToml(`
348
+ [[rule]]
349
+ toolName = "not_shell"
350
+ commandRegex = ".*"
351
+ decision = "allow"
352
+ priority = 100
353
+ `);
354
+ expect(result.errors).toHaveLength(1);
355
+ const error = result.errors[0];
356
+ expect(error.errorType).toBe('rule_validation');
357
+ expect(error.details).toContain('run_shell_command');
358
+ });
359
+ it('should return a rule_validation error if commandPrefix and commandRegex are combined', async () => {
360
+ const result = await runLoadPoliciesFromToml(`
496
361
  [[rule]]
497
362
  toolName = "run_shell_command"
498
- commandPrefix = "git log *.txt"
363
+ commandPrefix = "git"
364
+ commandRegex = ".*"
499
365
  decision = "allow"
500
366
  priority = 100
501
- `;
502
- }
503
- throw new Error('File not found');
367
+ `);
368
+ expect(result.errors).toHaveLength(1);
369
+ const error = result.errors[0];
370
+ expect(error.errorType).toBe('rule_validation');
371
+ expect(error.details).toContain('mutually exclusive');
372
+ });
373
+ it('should return a regex_compilation error for invalid argsPattern', async () => {
374
+ const result = await runLoadPoliciesFromToml(`
375
+ [[rule]]
376
+ toolName = "test"
377
+ argsPattern = "([a-z)"
378
+ decision = "allow"
379
+ priority = 100
380
+ `);
381
+ expect(result.errors).toHaveLength(1);
382
+ const error = result.errors[0];
383
+ expect(error.errorType).toBe('regex_compilation');
384
+ expect(error.message).toBe('Invalid regex pattern');
385
+ });
386
+ it('should return a file_read error if readdir fails', async () => {
387
+ const actualFs = await vi.importActual('node:fs/promises');
388
+ const mockReaddir = vi.fn(async () => {
389
+ throw new Error('Permission denied');
504
390
  });
505
391
  vi.doMock('node:fs/promises', () => ({
506
392
  ...actualFs,
507
- default: { ...actualFs, readFile: mockReadFile, readdir: mockReaddir },
508
- readFile: mockReadFile,
393
+ default: { ...actualFs, readdir: mockReaddir },
509
394
  readdir: mockReaddir,
510
395
  }));
511
396
  const { loadPoliciesFromToml: load } = await import('./toml-loader.js');
512
397
  const getPolicyTier = (_dir) => 1;
513
398
  const result = await load(ApprovalMode.DEFAULT, ['/policies'], getPolicyTier);
514
- expect(result.rules).toHaveLength(1);
515
- // The regex should have escaped the * and .
516
- expect(result.rules[0].argsPattern?.test('{"command":"git log file.txt"}')).toBe(false);
517
- expect(result.rules[0].argsPattern?.test('{"command":"git log *.txt"}')).toBe(true);
518
- expect(result.errors).toHaveLength(0);
399
+ expect(result.errors).toHaveLength(1);
400
+ const error = result.errors[0];
401
+ expect(error.errorType).toBe('file_read');
402
+ expect(error.message).toContain('Failed to read policy directory');
519
403
  });
520
404
  });
521
405
  });