@google/gemini-cli 0.10.0-preview.3 → 0.11.0-nightly.20251021.e72c00cf

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 (472) hide show
  1. package/README.md +9 -0
  2. package/dist/google-gemini-cli-0.11.0-nightly.20251020.a96f0659.tgz +0 -0
  3. package/dist/index.js +5 -5
  4. package/dist/index.js.map +1 -1
  5. package/dist/package.json +2 -2
  6. package/dist/src/commands/extensions/disable.js +8 -5
  7. package/dist/src/commands/extensions/disable.js.map +1 -1
  8. package/dist/src/commands/extensions/enable.js +5 -3
  9. package/dist/src/commands/extensions/enable.js.map +1 -1
  10. package/dist/src/commands/extensions/examples/mcp-server/example.d.ts +6 -0
  11. package/dist/src/commands/extensions/examples/mcp-server/example.js +46 -0
  12. package/dist/src/commands/extensions/examples/mcp-server/example.js.map +1 -0
  13. package/dist/src/commands/extensions/install.d.ts +1 -0
  14. package/dist/src/commands/extensions/install.js +18 -4
  15. package/dist/src/commands/extensions/install.js.map +1 -1
  16. package/dist/src/commands/extensions/link.js +3 -2
  17. package/dist/src/commands/extensions/link.js.map +1 -1
  18. package/dist/src/commands/extensions/list.js +7 -5
  19. package/dist/src/commands/extensions/list.js.map +1 -1
  20. package/dist/src/commands/extensions/new.js +14 -20
  21. package/dist/src/commands/extensions/new.js.map +1 -1
  22. package/dist/src/commands/extensions/uninstall.js +3 -2
  23. package/dist/src/commands/extensions/uninstall.js.map +1 -1
  24. package/dist/src/commands/extensions/update.js +17 -17
  25. package/dist/src/commands/extensions/update.js.map +1 -1
  26. package/dist/src/commands/mcp/add.js +7 -4
  27. package/dist/src/commands/mcp/add.js.map +1 -1
  28. package/dist/src/commands/mcp/add.test.d.ts +6 -0
  29. package/dist/src/commands/mcp/add.test.js +244 -0
  30. package/dist/src/commands/mcp/add.test.js.map +1 -0
  31. package/dist/src/commands/mcp/list.js +6 -4
  32. package/dist/src/commands/mcp/list.js.map +1 -1
  33. package/dist/src/commands/mcp/list.test.d.ts +6 -0
  34. package/dist/src/commands/mcp/list.test.js +117 -0
  35. package/dist/src/commands/mcp/list.test.js.map +1 -0
  36. package/dist/src/commands/mcp/remove.test.d.ts +6 -0
  37. package/dist/src/commands/mcp/remove.test.js +175 -0
  38. package/dist/src/commands/mcp/remove.test.js.map +1 -0
  39. package/dist/src/commands/mcp.test.d.ts +6 -0
  40. package/dist/src/commands/mcp.test.js +62 -0
  41. package/dist/src/commands/mcp.test.js.map +1 -0
  42. package/dist/src/config/auth.js +3 -1
  43. package/dist/src/config/auth.js.map +1 -1
  44. package/dist/src/config/auth.test.js +3 -1
  45. package/dist/src/config/auth.test.js.map +1 -1
  46. package/dist/src/config/config.d.ts +2 -14
  47. package/dist/src/config/config.integration.test.d.ts +6 -0
  48. package/dist/src/config/config.integration.test.js +321 -0
  49. package/dist/src/config/config.integration.test.js.map +1 -0
  50. package/dist/src/config/config.js +38 -102
  51. package/dist/src/config/config.js.map +1 -1
  52. package/dist/src/config/config.test.d.ts +6 -0
  53. package/dist/src/config/config.test.js +1961 -0
  54. package/dist/src/config/config.test.js.map +1 -0
  55. package/dist/src/config/extension.d.ts +4 -15
  56. package/dist/src/config/extension.js +84 -97
  57. package/dist/src/config/extension.js.map +1 -1
  58. package/dist/src/config/extension.test.d.ts +6 -0
  59. package/dist/src/config/extension.test.js +1076 -0
  60. package/dist/src/config/extension.test.js.map +1 -0
  61. package/dist/src/config/extensions/extensionEnablement.d.ts +1 -1
  62. package/dist/src/config/extensions/extensionEnablement.js +4 -3
  63. package/dist/src/config/extensions/extensionEnablement.js.map +1 -1
  64. package/dist/src/config/extensions/extensionEnablement.test.js +21 -18
  65. package/dist/src/config/extensions/extensionEnablement.test.js.map +1 -1
  66. package/dist/src/config/extensions/github.d.ts +18 -9
  67. package/dist/src/config/extensions/github.js +106 -29
  68. package/dist/src/config/extensions/github.js.map +1 -1
  69. package/dist/src/config/extensions/github.test.js +20 -17
  70. package/dist/src/config/extensions/github.test.js.map +1 -1
  71. package/dist/src/config/extensions/update.d.ts +5 -4
  72. package/dist/src/config/extensions/update.js +10 -6
  73. package/dist/src/config/extensions/update.js.map +1 -1
  74. package/dist/src/config/extensions/update.test.js +57 -57
  75. package/dist/src/config/extensions/update.test.js.map +1 -1
  76. package/dist/src/config/extensions/variableSchema.d.ts +2 -0
  77. package/dist/src/config/extensions/variableSchema.js.map +1 -1
  78. package/dist/src/config/keyBindings.d.ts +2 -1
  79. package/dist/src/config/keyBindings.js +4 -2
  80. package/dist/src/config/keyBindings.js.map +1 -1
  81. package/dist/src/config/policy.d.ts +3 -2
  82. package/dist/src/config/policy.js +32 -23
  83. package/dist/src/config/policy.js.map +1 -1
  84. package/dist/src/config/policy.test.js +60 -36
  85. package/dist/src/config/policy.test.js.map +1 -1
  86. package/dist/src/config/sandboxConfig.d.ts +0 -1
  87. package/dist/src/config/sandboxConfig.js +1 -3
  88. package/dist/src/config/sandboxConfig.js.map +1 -1
  89. package/dist/src/config/settings.js +3 -1
  90. package/dist/src/config/settings.js.map +1 -1
  91. package/dist/src/config/settings.test.d.ts +6 -0
  92. package/dist/src/config/settings.test.js +1937 -0
  93. package/dist/src/config/settings.test.js.map +1 -0
  94. package/dist/src/config/settingsSchema.d.ts +1 -1
  95. package/dist/src/config/settingsSchema.js +1 -1
  96. package/dist/src/config/settingsSchema.js.map +1 -1
  97. package/dist/src/config/settingsSchema.test.js +1 -1
  98. package/dist/src/config/settingsSchema.test.js.map +1 -1
  99. package/dist/src/gemini.js +30 -24
  100. package/dist/src/gemini.js.map +1 -1
  101. package/dist/src/gemini.test.js +42 -11
  102. package/dist/src/gemini.test.js.map +1 -1
  103. package/dist/src/generated/git-commit.d.ts +2 -2
  104. package/dist/src/generated/git-commit.js +2 -2
  105. package/dist/src/generated/git-commit.js.map +1 -1
  106. package/dist/src/nonInteractiveCli.js +92 -4
  107. package/dist/src/nonInteractiveCli.js.map +1 -1
  108. package/dist/src/nonInteractiveCli.test.d.ts +6 -0
  109. package/dist/src/nonInteractiveCli.test.js +711 -0
  110. package/dist/src/nonInteractiveCli.test.js.map +1 -0
  111. package/dist/src/services/FileCommandLoader.test.d.ts +6 -0
  112. package/dist/src/services/FileCommandLoader.test.js +971 -0
  113. package/dist/src/services/FileCommandLoader.test.js.map +1 -0
  114. package/dist/src/services/prompt-processors/argumentProcessor.test.d.ts +6 -0
  115. package/dist/src/services/prompt-processors/argumentProcessor.test.js +40 -0
  116. package/dist/src/services/prompt-processors/argumentProcessor.test.js.map +1 -0
  117. package/dist/src/services/prompt-processors/atFileProcessor.js +3 -2
  118. package/dist/src/services/prompt-processors/atFileProcessor.js.map +1 -1
  119. package/dist/src/services/prompt-processors/shellProcessor.test.d.ts +6 -0
  120. package/dist/src/services/prompt-processors/shellProcessor.test.js +482 -0
  121. package/dist/src/services/prompt-processors/shellProcessor.test.js.map +1 -0
  122. package/dist/src/test-utils/render.d.ts +2 -1
  123. package/dist/src/test-utils/render.js +5 -2
  124. package/dist/src/test-utils/render.js.map +1 -1
  125. package/dist/src/ui/App.test.d.ts +6 -0
  126. package/dist/src/ui/App.test.js +110 -0
  127. package/dist/src/ui/App.test.js.map +1 -0
  128. package/dist/src/ui/AppContainer.js +43 -28
  129. package/dist/src/ui/AppContainer.js.map +1 -1
  130. package/dist/src/ui/AppContainer.test.js +35 -9
  131. package/dist/src/ui/AppContainer.test.js.map +1 -1
  132. package/dist/src/ui/auth/AuthDialog.d.ts +1 -1
  133. package/dist/src/ui/auth/AuthDialog.js +3 -1
  134. package/dist/src/ui/auth/AuthDialog.js.map +1 -1
  135. package/dist/src/ui/auth/useAuth.d.ts +1 -1
  136. package/dist/src/ui/auth/useAuth.js +3 -1
  137. package/dist/src/ui/auth/useAuth.js.map +1 -1
  138. package/dist/src/ui/commands/aboutCommand.js +1 -1
  139. package/dist/src/ui/commands/aboutCommand.test.d.ts +6 -0
  140. package/dist/src/ui/commands/aboutCommand.test.js +130 -0
  141. package/dist/src/ui/commands/aboutCommand.test.js.map +1 -0
  142. package/dist/src/ui/commands/authCommand.js +1 -1
  143. package/dist/src/ui/commands/authCommand.test.d.ts +6 -0
  144. package/dist/src/ui/commands/authCommand.test.js +30 -0
  145. package/dist/src/ui/commands/authCommand.test.js.map +1 -0
  146. package/dist/src/ui/commands/bugCommand.js +1 -1
  147. package/dist/src/ui/commands/bugCommand.test.d.ts +6 -0
  148. package/dist/src/ui/commands/bugCommand.test.js +105 -0
  149. package/dist/src/ui/commands/bugCommand.test.js.map +1 -0
  150. package/dist/src/ui/commands/chatCommand.js +1 -1
  151. package/dist/src/ui/commands/chatCommand.js.map +1 -1
  152. package/dist/src/ui/commands/chatCommand.test.d.ts +6 -0
  153. package/dist/src/ui/commands/chatCommand.test.js +555 -0
  154. package/dist/src/ui/commands/chatCommand.test.js.map +1 -0
  155. package/dist/src/ui/commands/clearCommand.js +1 -1
  156. package/dist/src/ui/commands/clearCommand.test.d.ts +6 -0
  157. package/dist/src/ui/commands/clearCommand.test.js +76 -0
  158. package/dist/src/ui/commands/clearCommand.test.js.map +1 -0
  159. package/dist/src/ui/commands/compressCommand.js +1 -1
  160. package/dist/src/ui/commands/compressCommand.js.map +1 -1
  161. package/dist/src/ui/commands/compressCommand.test.d.ts +6 -0
  162. package/dist/src/ui/commands/compressCommand.test.js +98 -0
  163. package/dist/src/ui/commands/compressCommand.test.js.map +1 -0
  164. package/dist/src/ui/commands/copyCommand.test.d.ts +6 -0
  165. package/dist/src/ui/commands/copyCommand.test.js +242 -0
  166. package/dist/src/ui/commands/copyCommand.test.js.map +1 -0
  167. package/dist/src/ui/commands/corgiCommand.js +1 -1
  168. package/dist/src/ui/commands/corgiCommand.js.map +1 -1
  169. package/dist/src/ui/commands/corgiCommand.test.d.ts +6 -0
  170. package/dist/src/ui/commands/corgiCommand.test.js +28 -0
  171. package/dist/src/ui/commands/corgiCommand.test.js.map +1 -0
  172. package/dist/src/ui/commands/directoryCommand.js +1 -1
  173. package/dist/src/ui/commands/directoryCommand.js.map +1 -1
  174. package/dist/src/ui/commands/directoryCommand.test.d.ts +6 -0
  175. package/dist/src/ui/commands/directoryCommand.test.js +144 -0
  176. package/dist/src/ui/commands/directoryCommand.test.js.map +1 -0
  177. package/dist/src/ui/commands/docsCommand.js +1 -1
  178. package/dist/src/ui/commands/docsCommand.test.d.ts +6 -0
  179. package/dist/src/ui/commands/docsCommand.test.js +72 -0
  180. package/dist/src/ui/commands/docsCommand.test.js.map +1 -0
  181. package/dist/src/ui/commands/editorCommand.js +1 -1
  182. package/dist/src/ui/commands/editorCommand.test.d.ts +6 -0
  183. package/dist/src/ui/commands/editorCommand.test.js +27 -0
  184. package/dist/src/ui/commands/editorCommand.test.js.map +1 -0
  185. package/dist/src/ui/commands/extensionsCommand.test.d.ts +6 -0
  186. package/dist/src/ui/commands/extensionsCommand.test.js +241 -0
  187. package/dist/src/ui/commands/extensionsCommand.test.js.map +1 -0
  188. package/dist/src/ui/commands/helpCommand.js +1 -1
  189. package/dist/src/ui/commands/helpCommand.test.d.ts +6 -0
  190. package/dist/src/ui/commands/helpCommand.test.js +42 -0
  191. package/dist/src/ui/commands/helpCommand.test.js.map +1 -0
  192. package/dist/src/ui/commands/ideCommand.js +6 -6
  193. package/dist/src/ui/commands/ideCommand.test.d.ts +6 -0
  194. package/dist/src/ui/commands/ideCommand.test.js +203 -0
  195. package/dist/src/ui/commands/ideCommand.test.js.map +1 -0
  196. package/dist/src/ui/commands/initCommand.js +1 -1
  197. package/dist/src/ui/commands/initCommand.js.map +1 -1
  198. package/dist/src/ui/commands/initCommand.test.d.ts +6 -0
  199. package/dist/src/ui/commands/initCommand.test.js +80 -0
  200. package/dist/src/ui/commands/initCommand.test.js.map +1 -0
  201. package/dist/src/ui/commands/mcpCommand.js +98 -88
  202. package/dist/src/ui/commands/mcpCommand.js.map +1 -1
  203. package/dist/src/ui/commands/mcpCommand.test.d.ts +6 -0
  204. package/dist/src/ui/commands/mcpCommand.test.js +148 -0
  205. package/dist/src/ui/commands/mcpCommand.test.js.map +1 -0
  206. package/dist/src/ui/commands/memoryCommand.js +6 -6
  207. package/dist/src/ui/commands/memoryCommand.js.map +1 -1
  208. package/dist/src/ui/commands/memoryCommand.test.d.ts +6 -0
  209. package/dist/src/ui/commands/memoryCommand.test.js +266 -0
  210. package/dist/src/ui/commands/memoryCommand.test.js.map +1 -0
  211. package/dist/src/ui/commands/privacyCommand.js +1 -1
  212. package/dist/src/ui/commands/privacyCommand.test.d.ts +6 -0
  213. package/dist/src/ui/commands/privacyCommand.test.js +32 -0
  214. package/dist/src/ui/commands/privacyCommand.test.js.map +1 -0
  215. package/dist/src/ui/commands/quitCommand.js +1 -1
  216. package/dist/src/ui/commands/quitCommand.test.d.ts +6 -0
  217. package/dist/src/ui/commands/quitCommand.test.js +50 -0
  218. package/dist/src/ui/commands/quitCommand.test.js.map +1 -0
  219. package/dist/src/ui/commands/restoreCommand.test.d.ts +6 -0
  220. package/dist/src/ui/commands/restoreCommand.test.js +190 -0
  221. package/dist/src/ui/commands/restoreCommand.test.js.map +1 -0
  222. package/dist/src/ui/commands/settingsCommand.test.d.ts +6 -0
  223. package/dist/src/ui/commands/settingsCommand.test.js +30 -0
  224. package/dist/src/ui/commands/settingsCommand.test.js.map +1 -0
  225. package/dist/src/ui/commands/setupGithubCommand.test.js +1 -2
  226. package/dist/src/ui/commands/setupGithubCommand.test.js.map +1 -1
  227. package/dist/src/ui/commands/statsCommand.js +3 -3
  228. package/dist/src/ui/commands/statsCommand.js.map +1 -1
  229. package/dist/src/ui/commands/statsCommand.test.d.ts +6 -0
  230. package/dist/src/ui/commands/statsCommand.test.js +53 -0
  231. package/dist/src/ui/commands/statsCommand.test.js.map +1 -0
  232. package/dist/src/ui/commands/terminalSetupCommand.test.d.ts +6 -0
  233. package/dist/src/ui/commands/terminalSetupCommand.test.js +66 -0
  234. package/dist/src/ui/commands/terminalSetupCommand.test.js.map +1 -0
  235. package/dist/src/ui/commands/themeCommand.js +1 -1
  236. package/dist/src/ui/commands/themeCommand.test.d.ts +6 -0
  237. package/dist/src/ui/commands/themeCommand.test.js +32 -0
  238. package/dist/src/ui/commands/themeCommand.test.js.map +1 -0
  239. package/dist/src/ui/commands/toolsCommand.js +1 -1
  240. package/dist/src/ui/commands/toolsCommand.test.d.ts +6 -0
  241. package/dist/src/ui/commands/toolsCommand.test.js +100 -0
  242. package/dist/src/ui/commands/toolsCommand.test.js.map +1 -0
  243. package/dist/src/ui/commands/vimCommand.js +1 -1
  244. package/dist/src/ui/components/Composer.js +5 -3
  245. package/dist/src/ui/components/Composer.js.map +1 -1
  246. package/dist/src/ui/components/Composer.test.js +16 -1
  247. package/dist/src/ui/components/Composer.test.js.map +1 -1
  248. package/dist/src/ui/components/ContextSummaryDisplay.d.ts +0 -1
  249. package/dist/src/ui/components/ContextSummaryDisplay.js +2 -12
  250. package/dist/src/ui/components/ContextSummaryDisplay.js.map +1 -1
  251. package/dist/src/ui/components/ContextSummaryDisplay.test.d.ts +6 -0
  252. package/dist/src/ui/components/ContextSummaryDisplay.test.js +66 -0
  253. package/dist/src/ui/components/ContextSummaryDisplay.test.js.map +1 -0
  254. package/dist/src/ui/components/DialogManager.js +1 -5
  255. package/dist/src/ui/components/DialogManager.js.map +1 -1
  256. package/dist/src/ui/components/EditorSettingsDialog.js +1 -1
  257. package/dist/src/ui/components/EditorSettingsDialog.js.map +1 -1
  258. package/dist/src/ui/components/FolderTrustDialog.test.js +7 -3
  259. package/dist/src/ui/components/FolderTrustDialog.test.js.map +1 -1
  260. package/dist/src/ui/components/Footer.js +1 -1
  261. package/dist/src/ui/components/Footer.js.map +1 -1
  262. package/dist/src/ui/components/Footer.test.d.ts +6 -0
  263. package/dist/src/ui/components/Footer.test.js +231 -0
  264. package/dist/src/ui/components/Footer.test.js.map +1 -0
  265. package/dist/src/ui/components/InputPrompt.d.ts +4 -0
  266. package/dist/src/ui/components/InputPrompt.js +53 -4
  267. package/dist/src/ui/components/InputPrompt.js.map +1 -1
  268. package/dist/src/ui/components/InputPrompt.test.d.ts +6 -0
  269. package/dist/src/ui/components/InputPrompt.test.js +1716 -0
  270. package/dist/src/ui/components/InputPrompt.test.js.map +1 -0
  271. package/dist/src/ui/components/ModelStatsDisplay.test.d.ts +6 -0
  272. package/dist/src/ui/components/ModelStatsDisplay.test.js +285 -0
  273. package/dist/src/ui/components/ModelStatsDisplay.test.js.map +1 -0
  274. package/dist/src/ui/components/PermissionsModifyTrustDialog.js +22 -18
  275. package/dist/src/ui/components/PermissionsModifyTrustDialog.js.map +1 -1
  276. package/dist/src/ui/components/PermissionsModifyTrustDialog.test.js +10 -2
  277. package/dist/src/ui/components/PermissionsModifyTrustDialog.test.js.map +1 -1
  278. package/dist/src/ui/components/QueuedMessageDisplay.js +3 -3
  279. package/dist/src/ui/components/QueuedMessageDisplay.js.map +1 -1
  280. package/dist/src/ui/components/QueuedMessageDisplay.test.js +4 -0
  281. package/dist/src/ui/components/QueuedMessageDisplay.test.js.map +1 -1
  282. package/dist/src/ui/components/RawMarkdownIndicator.d.ts +7 -0
  283. package/dist/src/ui/components/RawMarkdownIndicator.js +8 -0
  284. package/dist/src/ui/components/RawMarkdownIndicator.js.map +1 -0
  285. package/dist/src/ui/components/SessionSummaryDisplay.test.d.ts +6 -0
  286. package/dist/src/ui/components/SessionSummaryDisplay.test.js +74 -0
  287. package/dist/src/ui/components/SessionSummaryDisplay.test.js.map +1 -0
  288. package/dist/src/ui/components/SettingsDialog.js +8 -8
  289. package/dist/src/ui/components/SettingsDialog.js.map +1 -1
  290. package/dist/src/ui/components/SettingsDialog.test.js +189 -76
  291. package/dist/src/ui/components/SettingsDialog.test.js.map +1 -1
  292. package/dist/src/ui/components/StatsDisplay.test.d.ts +6 -0
  293. package/dist/src/ui/components/StatsDisplay.test.js +351 -0
  294. package/dist/src/ui/components/StatsDisplay.test.js.map +1 -0
  295. package/dist/src/ui/components/ThemeDialog.d.ts +4 -2
  296. package/dist/src/ui/components/ThemeDialog.js +3 -3
  297. package/dist/src/ui/components/ThemeDialog.js.map +1 -1
  298. package/dist/src/ui/components/ThemeDialog.test.js +13 -0
  299. package/dist/src/ui/components/ThemeDialog.test.js.map +1 -1
  300. package/dist/src/ui/components/ToolStatsDisplay.test.d.ts +6 -0
  301. package/dist/src/ui/components/ToolStatsDisplay.test.js +227 -0
  302. package/dist/src/ui/components/ToolStatsDisplay.test.js.map +1 -0
  303. package/dist/src/ui/components/messages/GeminiMessage.js +3 -1
  304. package/dist/src/ui/components/messages/GeminiMessage.js.map +1 -1
  305. package/dist/src/ui/components/messages/GeminiMessage.test.d.ts +6 -0
  306. package/dist/src/ui/components/messages/GeminiMessage.test.js +35 -0
  307. package/dist/src/ui/components/messages/GeminiMessage.test.js.map +1 -0
  308. package/dist/src/ui/components/messages/GeminiMessageContent.js +3 -1
  309. package/dist/src/ui/components/messages/GeminiMessageContent.js.map +1 -1
  310. package/dist/src/ui/components/messages/Todo.d.ts +7 -0
  311. package/dist/src/ui/components/messages/Todo.js +59 -0
  312. package/dist/src/ui/components/messages/Todo.js.map +1 -0
  313. package/dist/src/ui/components/messages/Todo.test.d.ts +6 -0
  314. package/dist/src/ui/components/messages/Todo.test.js +113 -0
  315. package/dist/src/ui/components/messages/Todo.test.js.map +1 -0
  316. package/dist/src/ui/components/messages/ToolGroupMessage.js +1 -1
  317. package/dist/src/ui/components/messages/ToolGroupMessage.js.map +1 -1
  318. package/dist/src/ui/components/messages/ToolMessage.js +8 -3
  319. package/dist/src/ui/components/messages/ToolMessage.js.map +1 -1
  320. package/dist/src/ui/components/messages/ToolMessage.test.js +2 -2
  321. package/dist/src/ui/components/messages/ToolMessage.test.js.map +1 -1
  322. package/dist/src/ui/components/messages/ToolMessageRawMarkdown.test.d.ts +6 -0
  323. package/dist/src/ui/components/messages/ToolMessageRawMarkdown.test.js +30 -0
  324. package/dist/src/ui/components/messages/ToolMessageRawMarkdown.test.js.map +1 -0
  325. package/dist/src/ui/components/messages/UserShellMessage.js +1 -1
  326. package/dist/src/ui/components/messages/UserShellMessage.js.map +1 -1
  327. package/dist/src/ui/components/shared/BaseSelectionList.test.js +1 -1
  328. package/dist/src/ui/components/shared/BaseSelectionList.test.js.map +1 -1
  329. package/dist/src/ui/components/shared/text-buffer.test.d.ts +6 -0
  330. package/dist/src/ui/components/shared/text-buffer.test.js +1554 -0
  331. package/dist/src/ui/components/shared/text-buffer.test.js.map +1 -0
  332. package/dist/src/ui/components/shared/vim-buffer-actions.test.d.ts +6 -0
  333. package/dist/src/ui/components/shared/vim-buffer-actions.test.js +951 -0
  334. package/dist/src/ui/components/shared/vim-buffer-actions.test.js.map +1 -0
  335. package/dist/src/ui/components/views/ExtensionsList.js +3 -4
  336. package/dist/src/ui/components/views/ExtensionsList.js.map +1 -1
  337. package/dist/src/ui/components/views/ExtensionsList.test.js +2 -9
  338. package/dist/src/ui/components/views/ExtensionsList.test.js.map +1 -1
  339. package/dist/src/ui/components/views/McpStatus.d.ts +0 -1
  340. package/dist/src/ui/components/views/McpStatus.js +4 -4
  341. package/dist/src/ui/components/views/McpStatus.js.map +1 -1
  342. package/dist/src/ui/components/views/McpStatus.test.js +0 -5
  343. package/dist/src/ui/components/views/McpStatus.test.js.map +1 -1
  344. package/dist/src/ui/components/views/ToolsList.test.js +4 -4
  345. package/dist/src/ui/components/views/ToolsList.test.js.map +1 -1
  346. package/dist/src/ui/contexts/KeypressContext.d.ts +1 -0
  347. package/dist/src/ui/contexts/KeypressContext.js +176 -50
  348. package/dist/src/ui/contexts/KeypressContext.js.map +1 -1
  349. package/dist/src/ui/contexts/KeypressContext.test.js +413 -14
  350. package/dist/src/ui/contexts/KeypressContext.test.js.map +1 -1
  351. package/dist/src/ui/contexts/SessionContext.test.d.ts +6 -0
  352. package/dist/src/ui/contexts/SessionContext.test.js +177 -0
  353. package/dist/src/ui/contexts/SessionContext.test.js.map +1 -0
  354. package/dist/src/ui/contexts/UIActionsContext.d.ts +5 -4
  355. package/dist/src/ui/contexts/UIActionsContext.js.map +1 -1
  356. package/dist/src/ui/contexts/UIStateContext.d.ts +3 -3
  357. package/dist/src/ui/contexts/UIStateContext.js.map +1 -1
  358. package/dist/src/ui/hooks/slashCommandProcessor.test.d.ts +6 -0
  359. package/dist/src/ui/hooks/slashCommandProcessor.test.js +779 -0
  360. package/dist/src/ui/hooks/slashCommandProcessor.test.js.map +1 -0
  361. package/dist/src/ui/hooks/useAtCompletion.js +2 -2
  362. package/dist/src/ui/hooks/useAtCompletion.js.map +1 -1
  363. package/dist/src/ui/hooks/useAtCompletion.test.d.ts +6 -0
  364. package/dist/src/ui/hooks/useAtCompletion.test.js +385 -0
  365. package/dist/src/ui/hooks/useAtCompletion.test.js.map +1 -0
  366. package/dist/src/ui/hooks/useAutoAcceptIndicator.test.js +0 -1
  367. package/dist/src/ui/hooks/useAutoAcceptIndicator.test.js.map +1 -1
  368. package/dist/src/ui/hooks/useCommandCompletion.d.ts +1 -1
  369. package/dist/src/ui/hooks/useCommandCompletion.js +5 -3
  370. package/dist/src/ui/hooks/useCommandCompletion.js.map +1 -1
  371. package/dist/src/ui/hooks/useCommandCompletion.test.d.ts +6 -0
  372. package/dist/src/ui/hooks/useCommandCompletion.test.js +375 -0
  373. package/dist/src/ui/hooks/useCommandCompletion.test.js.map +1 -0
  374. package/dist/src/ui/hooks/useConsoleMessages.test.d.ts +6 -0
  375. package/dist/src/ui/hooks/useConsoleMessages.test.js +110 -0
  376. package/dist/src/ui/hooks/useConsoleMessages.test.js.map +1 -0
  377. package/dist/src/ui/hooks/useExtensionUpdates.d.ts +2 -1
  378. package/dist/src/ui/hooks/useExtensionUpdates.js +5 -3
  379. package/dist/src/ui/hooks/useExtensionUpdates.js.map +1 -1
  380. package/dist/src/ui/hooks/useExtensionUpdates.test.js +22 -13
  381. package/dist/src/ui/hooks/useExtensionUpdates.test.js.map +1 -1
  382. package/dist/src/ui/hooks/useFocus.test.d.ts +6 -0
  383. package/dist/src/ui/hooks/useFocus.test.js +115 -0
  384. package/dist/src/ui/hooks/useFocus.test.js.map +1 -0
  385. package/dist/src/ui/hooks/useFolderTrust.test.d.ts +6 -0
  386. package/dist/src/ui/hooks/useFolderTrust.test.js +164 -0
  387. package/dist/src/ui/hooks/useFolderTrust.test.js.map +1 -0
  388. package/dist/src/ui/hooks/useGeminiStream.js +33 -31
  389. package/dist/src/ui/hooks/useGeminiStream.js.map +1 -1
  390. package/dist/src/ui/hooks/useGeminiStream.test.d.ts +6 -0
  391. package/dist/src/ui/hooks/useGeminiStream.test.js +1936 -0
  392. package/dist/src/ui/hooks/useGeminiStream.test.js.map +1 -0
  393. package/dist/src/ui/hooks/useKeypress.test.d.ts +6 -0
  394. package/dist/src/ui/hooks/useKeypress.test.js +234 -0
  395. package/dist/src/ui/hooks/useKeypress.test.js.map +1 -0
  396. package/dist/src/ui/hooks/useLoadingIndicator.test.js +5 -0
  397. package/dist/src/ui/hooks/useLoadingIndicator.test.js.map +1 -1
  398. package/dist/src/ui/hooks/useMessageQueue.d.ts +1 -0
  399. package/dist/src/ui/hooks/useMessageQueue.js +14 -0
  400. package/dist/src/ui/hooks/useMessageQueue.js.map +1 -1
  401. package/dist/src/ui/hooks/useMessageQueue.test.js +121 -0
  402. package/dist/src/ui/hooks/useMessageQueue.test.js.map +1 -1
  403. package/dist/src/ui/hooks/usePhraseCycler.d.ts +1 -0
  404. package/dist/src/ui/hooks/usePhraseCycler.js +156 -5
  405. package/dist/src/ui/hooks/usePhraseCycler.js.map +1 -1
  406. package/dist/src/ui/hooks/usePhraseCycler.test.d.ts +6 -0
  407. package/dist/src/ui/hooks/usePhraseCycler.test.js +155 -0
  408. package/dist/src/ui/hooks/usePhraseCycler.test.js.map +1 -0
  409. package/dist/src/ui/hooks/useThemeCommand.d.ts +2 -1
  410. package/dist/src/ui/hooks/useThemeCommand.js +6 -0
  411. package/dist/src/ui/hooks/useThemeCommand.js.map +1 -1
  412. package/dist/src/ui/hooks/useToolScheduler.test.js +3 -0
  413. package/dist/src/ui/hooks/useToolScheduler.test.js.map +1 -1
  414. package/dist/src/ui/hooks/vim.test.d.ts +6 -0
  415. package/dist/src/ui/hooks/vim.test.js +1389 -0
  416. package/dist/src/ui/hooks/vim.test.js.map +1 -0
  417. package/dist/src/ui/keyMatchers.test.js +9 -3
  418. package/dist/src/ui/keyMatchers.test.js.map +1 -1
  419. package/dist/src/ui/themes/theme.test.d.ts +6 -0
  420. package/dist/src/ui/themes/theme.test.js +85 -0
  421. package/dist/src/ui/themes/theme.test.js.map +1 -0
  422. package/dist/src/ui/types.d.ts +0 -1
  423. package/dist/src/ui/types.js.map +1 -1
  424. package/dist/src/ui/utils/CodeColorizer.d.ts +1 -1
  425. package/dist/src/ui/utils/CodeColorizer.js +4 -2
  426. package/dist/src/ui/utils/CodeColorizer.js.map +1 -1
  427. package/dist/src/ui/utils/MarkdownDisplay.d.ts +1 -0
  428. package/dist/src/ui/utils/MarkdownDisplay.js +8 -1
  429. package/dist/src/ui/utils/MarkdownDisplay.js.map +1 -1
  430. package/dist/src/ui/utils/commandUtils.js +18 -2
  431. package/dist/src/ui/utils/commandUtils.js.map +1 -1
  432. package/dist/src/ui/utils/commandUtils.test.js +61 -6
  433. package/dist/src/ui/utils/commandUtils.test.js.map +1 -1
  434. package/dist/src/ui/utils/computeStats.js +5 -2
  435. package/dist/src/ui/utils/computeStats.js.map +1 -1
  436. package/dist/src/ui/utils/computeStats.test.d.ts +6 -0
  437. package/dist/src/ui/utils/computeStats.test.js +262 -0
  438. package/dist/src/ui/utils/computeStats.test.js.map +1 -0
  439. package/dist/src/ui/utils/updateCheck.d.ts +2 -1
  440. package/dist/src/ui/utils/updateCheck.js +4 -1
  441. package/dist/src/ui/utils/updateCheck.js.map +1 -1
  442. package/dist/src/ui/utils/updateCheck.test.js +25 -10
  443. package/dist/src/ui/utils/updateCheck.test.js.map +1 -1
  444. package/dist/src/utils/cleanup.test.d.ts +6 -0
  445. package/dist/src/utils/cleanup.test.js +49 -0
  446. package/dist/src/utils/cleanup.test.js.map +1 -0
  447. package/dist/src/utils/errors.d.ts +1 -0
  448. package/dist/src/utils/errors.js +66 -5
  449. package/dist/src/utils/errors.js.map +1 -1
  450. package/dist/src/utils/handleAutoUpdate.test.d.ts +6 -0
  451. package/dist/src/utils/handleAutoUpdate.test.js +225 -0
  452. package/dist/src/utils/handleAutoUpdate.test.js.map +1 -0
  453. package/dist/src/utils/sandbox-macos-permissive-open.sb +3 -1
  454. package/dist/src/utils/startupWarnings.test.d.ts +6 -0
  455. package/dist/src/utils/startupWarnings.test.js +61 -0
  456. package/dist/src/utils/startupWarnings.test.js.map +1 -0
  457. package/dist/src/validateNonInterActiveAuth.js +2 -2
  458. package/dist/src/validateNonInterActiveAuth.js.map +1 -1
  459. package/dist/src/validateNonInterActiveAuth.test.d.ts +6 -0
  460. package/dist/src/validateNonInterActiveAuth.test.js +336 -0
  461. package/dist/src/validateNonInterActiveAuth.test.js.map +1 -0
  462. package/dist/src/zed-integration/zedIntegration.js +1 -3
  463. package/dist/src/zed-integration/zedIntegration.js.map +1 -1
  464. package/dist/tsconfig.tsbuildinfo +1 -1
  465. package/package.json +3 -3
  466. package/dist/google-gemini-cli-0.10.0-preview.2.tgz +0 -0
  467. package/dist/src/ui/components/WorkspaceMigrationDialog.d.ts +0 -11
  468. package/dist/src/ui/components/WorkspaceMigrationDialog.js +0 -44
  469. package/dist/src/ui/components/WorkspaceMigrationDialog.js.map +0 -1
  470. package/dist/src/ui/hooks/useWorkspaceMigration.d.ts +0 -13
  471. package/dist/src/ui/hooks/useWorkspaceMigration.js +0 -59
  472. package/dist/src/ui/hooks/useWorkspaceMigration.js.map +0 -1
@@ -0,0 +1,1937 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2025 Google LLC
4
+ * SPDX-License-Identifier: Apache-2.0
5
+ */
6
+ /// <reference types="vitest/globals" />
7
+ // Mock 'os' first.
8
+ import * as osActual from 'node:os'; // Import for type info for the mock factory
9
+ vi.mock('os', async (importOriginal) => {
10
+ const actualOs = await importOriginal();
11
+ return {
12
+ ...actualOs,
13
+ homedir: vi.fn(() => '/mock/home/user'),
14
+ platform: vi.fn(() => 'linux'),
15
+ };
16
+ });
17
+ // Mock './settings.js' to ensure it uses the mocked 'os.homedir()' for its internal constants.
18
+ vi.mock('./settings.js', async (importActual) => {
19
+ const originalModule = await importActual();
20
+ return {
21
+ __esModule: true, // Ensure correct module shape
22
+ ...originalModule, // Re-export all original members
23
+ // We are relying on originalModule's USER_SETTINGS_PATH being constructed with mocked os.homedir()
24
+ };
25
+ });
26
+ // Mock trustedFolders
27
+ vi.mock('./trustedFolders.js', () => ({
28
+ isWorkspaceTrusted: vi
29
+ .fn()
30
+ .mockReturnValue({ isTrusted: true, source: 'file' }),
31
+ }));
32
+ // NOW import everything else, including the (now effectively re-exported) settings.js
33
+ import path, * as pathActual from 'node:path'; // Restored for MOCK_WORKSPACE_SETTINGS_PATH
34
+ import { describe, it, expect, vi, beforeEach, afterEach, } from 'vitest';
35
+ import * as fs from 'node:fs'; // fs will be mocked separately
36
+ import stripJsonComments from 'strip-json-comments'; // Will be mocked separately
37
+ import { isWorkspaceTrusted } from './trustedFolders.js';
38
+ import { disableExtension, ExtensionStorage } from './extension.js';
39
+ // These imports will get the versions from the vi.mock('./settings.js', ...) factory.
40
+ import { loadSettings, USER_SETTINGS_PATH, // This IS the mocked path.
41
+ getSystemSettingsPath, getSystemDefaultsPath, migrateSettingsToV1, needsMigration, loadEnvironment, migrateDeprecatedSettings, SettingScope, } from './settings.js';
42
+ import { FatalConfigError, GEMINI_DIR, Storage } from '@google/gemini-cli-core';
43
+ import { ExtensionEnablementManager } from './extensions/extensionEnablement.js';
44
+ const MOCK_WORKSPACE_DIR = '/mock/workspace';
45
+ // Use the (mocked) GEMINI_DIR for consistency
46
+ const MOCK_WORKSPACE_SETTINGS_PATH = pathActual.join(MOCK_WORKSPACE_DIR, GEMINI_DIR, 'settings.json');
47
+ vi.mock('fs', async (importOriginal) => {
48
+ // Get all the functions from the real 'fs' module
49
+ const actualFs = await importOriginal();
50
+ return {
51
+ ...actualFs, // Keep all the real functions
52
+ // Now, just override the ones we need for the test
53
+ existsSync: vi.fn(),
54
+ readFileSync: vi.fn(),
55
+ writeFileSync: vi.fn(),
56
+ mkdirSync: vi.fn(),
57
+ realpathSync: (p) => p,
58
+ };
59
+ });
60
+ vi.mock('./extension.js');
61
+ vi.mock('strip-json-comments', () => ({
62
+ default: vi.fn((content) => content),
63
+ }));
64
+ describe('Settings Loading and Merging', () => {
65
+ let mockFsExistsSync;
66
+ let mockStripJsonComments;
67
+ let mockFsMkdirSync;
68
+ beforeEach(() => {
69
+ vi.resetAllMocks();
70
+ mockFsExistsSync = vi.mocked(fs.existsSync);
71
+ mockFsMkdirSync = vi.mocked(fs.mkdirSync);
72
+ mockStripJsonComments = vi.mocked(stripJsonComments);
73
+ vi.mocked(osActual.homedir).mockReturnValue('/mock/home/user');
74
+ mockStripJsonComments.mockImplementation((jsonString) => jsonString);
75
+ mockFsExistsSync.mockReturnValue(false);
76
+ fs.readFileSync.mockReturnValue('{}'); // Return valid empty JSON
77
+ mockFsMkdirSync.mockImplementation(() => undefined);
78
+ vi.mocked(isWorkspaceTrusted).mockReturnValue({
79
+ isTrusted: true,
80
+ source: 'file',
81
+ });
82
+ });
83
+ afterEach(() => {
84
+ vi.restoreAllMocks();
85
+ });
86
+ describe('loadSettings', () => {
87
+ it('should load empty settings if no files exist', () => {
88
+ const settings = loadSettings(MOCK_WORKSPACE_DIR);
89
+ expect(settings.system.settings).toEqual({});
90
+ expect(settings.user.settings).toEqual({});
91
+ expect(settings.workspace.settings).toEqual({});
92
+ expect(settings.merged).toEqual({});
93
+ });
94
+ it('should load system settings if only system file exists', () => {
95
+ mockFsExistsSync.mockImplementation((p) => p === getSystemSettingsPath());
96
+ const systemSettingsContent = {
97
+ ui: {
98
+ theme: 'system-default',
99
+ },
100
+ tools: {
101
+ sandbox: false,
102
+ },
103
+ };
104
+ fs.readFileSync.mockImplementation((p) => {
105
+ if (p === getSystemSettingsPath())
106
+ return JSON.stringify(systemSettingsContent);
107
+ return '{}';
108
+ });
109
+ const settings = loadSettings(MOCK_WORKSPACE_DIR);
110
+ expect(fs.readFileSync).toHaveBeenCalledWith(getSystemSettingsPath(), 'utf-8');
111
+ expect(settings.system.settings).toEqual(systemSettingsContent);
112
+ expect(settings.user.settings).toEqual({});
113
+ expect(settings.workspace.settings).toEqual({});
114
+ expect(settings.merged).toEqual({
115
+ ...systemSettingsContent,
116
+ });
117
+ });
118
+ it('should load user settings if only user file exists', () => {
119
+ const expectedUserSettingsPath = USER_SETTINGS_PATH; // Use the path actually resolved by the (mocked) module
120
+ mockFsExistsSync.mockImplementation((p) => p === expectedUserSettingsPath);
121
+ const userSettingsContent = {
122
+ ui: {
123
+ theme: 'dark',
124
+ },
125
+ context: {
126
+ fileName: 'USER_CONTEXT.md',
127
+ },
128
+ };
129
+ fs.readFileSync.mockImplementation((p) => {
130
+ if (p === expectedUserSettingsPath)
131
+ return JSON.stringify(userSettingsContent);
132
+ return '{}';
133
+ });
134
+ const settings = loadSettings(MOCK_WORKSPACE_DIR);
135
+ expect(fs.readFileSync).toHaveBeenCalledWith(expectedUserSettingsPath, 'utf-8');
136
+ expect(settings.user.settings).toEqual(userSettingsContent);
137
+ expect(settings.workspace.settings).toEqual({});
138
+ expect(settings.merged).toEqual({
139
+ ...userSettingsContent,
140
+ });
141
+ });
142
+ it('should load workspace settings if only workspace file exists', () => {
143
+ mockFsExistsSync.mockImplementation((p) => p === MOCK_WORKSPACE_SETTINGS_PATH);
144
+ const workspaceSettingsContent = {
145
+ tools: {
146
+ sandbox: true,
147
+ },
148
+ context: {
149
+ fileName: 'WORKSPACE_CONTEXT.md',
150
+ },
151
+ };
152
+ fs.readFileSync.mockImplementation((p) => {
153
+ if (p === MOCK_WORKSPACE_SETTINGS_PATH)
154
+ return JSON.stringify(workspaceSettingsContent);
155
+ return '';
156
+ });
157
+ const settings = loadSettings(MOCK_WORKSPACE_DIR);
158
+ expect(fs.readFileSync).toHaveBeenCalledWith(MOCK_WORKSPACE_SETTINGS_PATH, 'utf-8');
159
+ expect(settings.user.settings).toEqual({});
160
+ expect(settings.workspace.settings).toEqual(workspaceSettingsContent);
161
+ expect(settings.merged).toEqual({
162
+ ...workspaceSettingsContent,
163
+ });
164
+ });
165
+ it('should merge system, user and workspace settings, with system taking precedence over workspace, and workspace over user', () => {
166
+ mockFsExistsSync.mockImplementation((p) => p === getSystemSettingsPath() ||
167
+ p === USER_SETTINGS_PATH ||
168
+ p === MOCK_WORKSPACE_SETTINGS_PATH);
169
+ const systemSettingsContent = {
170
+ ui: {
171
+ theme: 'system-theme',
172
+ },
173
+ tools: {
174
+ sandbox: false,
175
+ },
176
+ mcp: {
177
+ allowed: ['server1', 'server2'],
178
+ },
179
+ telemetry: { enabled: false },
180
+ };
181
+ const userSettingsContent = {
182
+ ui: {
183
+ theme: 'dark',
184
+ },
185
+ tools: {
186
+ sandbox: true,
187
+ },
188
+ context: {
189
+ fileName: 'USER_CONTEXT.md',
190
+ },
191
+ };
192
+ const workspaceSettingsContent = {
193
+ tools: {
194
+ sandbox: false,
195
+ core: ['tool1'],
196
+ },
197
+ context: {
198
+ fileName: 'WORKSPACE_CONTEXT.md',
199
+ },
200
+ mcp: {
201
+ allowed: ['server1', 'server2', 'server3'],
202
+ },
203
+ };
204
+ fs.readFileSync.mockImplementation((p) => {
205
+ if (p === getSystemSettingsPath())
206
+ return JSON.stringify(systemSettingsContent);
207
+ if (p === USER_SETTINGS_PATH)
208
+ return JSON.stringify(userSettingsContent);
209
+ if (p === MOCK_WORKSPACE_SETTINGS_PATH)
210
+ return JSON.stringify(workspaceSettingsContent);
211
+ return '';
212
+ });
213
+ const settings = loadSettings(MOCK_WORKSPACE_DIR);
214
+ expect(settings.system.settings).toEqual(systemSettingsContent);
215
+ expect(settings.user.settings).toEqual(userSettingsContent);
216
+ expect(settings.workspace.settings).toEqual(workspaceSettingsContent);
217
+ expect(settings.merged).toEqual({
218
+ ui: {
219
+ theme: 'system-theme',
220
+ },
221
+ tools: {
222
+ sandbox: false,
223
+ core: ['tool1'],
224
+ },
225
+ telemetry: { enabled: false },
226
+ context: {
227
+ fileName: 'WORKSPACE_CONTEXT.md',
228
+ },
229
+ mcp: {
230
+ allowed: ['server1', 'server2'],
231
+ },
232
+ });
233
+ });
234
+ it('should correctly migrate a complex legacy (v1) settings file', () => {
235
+ mockFsExistsSync.mockImplementation((p) => p === USER_SETTINGS_PATH);
236
+ const legacySettingsContent = {
237
+ theme: 'legacy-dark',
238
+ vimMode: true,
239
+ contextFileName: 'LEGACY_CONTEXT.md',
240
+ model: 'gemini-pro',
241
+ mcpServers: {
242
+ 'legacy-server-1': {
243
+ command: 'npm',
244
+ args: ['run', 'start:server1'],
245
+ description: 'Legacy Server 1',
246
+ },
247
+ 'legacy-server-2': {
248
+ command: 'node',
249
+ args: ['server2.js'],
250
+ description: 'Legacy Server 2',
251
+ },
252
+ },
253
+ allowMCPServers: ['legacy-server-1'],
254
+ someUnrecognizedSetting: 'should-be-preserved',
255
+ };
256
+ fs.readFileSync.mockImplementation((p) => {
257
+ if (p === USER_SETTINGS_PATH)
258
+ return JSON.stringify(legacySettingsContent);
259
+ return '{}';
260
+ });
261
+ const settings = loadSettings(MOCK_WORKSPACE_DIR);
262
+ expect(settings.merged).toEqual({
263
+ ui: {
264
+ theme: 'legacy-dark',
265
+ },
266
+ general: {
267
+ vimMode: true,
268
+ },
269
+ context: {
270
+ fileName: 'LEGACY_CONTEXT.md',
271
+ },
272
+ model: {
273
+ name: 'gemini-pro',
274
+ },
275
+ mcpServers: {
276
+ 'legacy-server-1': {
277
+ command: 'npm',
278
+ args: ['run', 'start:server1'],
279
+ description: 'Legacy Server 1',
280
+ },
281
+ 'legacy-server-2': {
282
+ command: 'node',
283
+ args: ['server2.js'],
284
+ description: 'Legacy Server 2',
285
+ },
286
+ },
287
+ mcp: {
288
+ allowed: ['legacy-server-1'],
289
+ },
290
+ someUnrecognizedSetting: 'should-be-preserved',
291
+ });
292
+ });
293
+ it('should rewrite allowedTools to tools.allowed during migration', () => {
294
+ mockFsExistsSync.mockImplementation((p) => p === USER_SETTINGS_PATH);
295
+ const legacySettingsContent = {
296
+ allowedTools: ['fs', 'shell'],
297
+ };
298
+ fs.readFileSync.mockImplementation((p) => {
299
+ if (p === USER_SETTINGS_PATH)
300
+ return JSON.stringify(legacySettingsContent);
301
+ return '{}';
302
+ });
303
+ const settings = loadSettings(MOCK_WORKSPACE_DIR);
304
+ expect(settings.merged.tools?.allowed).toEqual(['fs', 'shell']);
305
+ expect(settings.merged['allowedTools']).toBeUndefined();
306
+ });
307
+ it('should correctly merge and migrate legacy array properties from multiple scopes', () => {
308
+ mockFsExistsSync.mockReturnValue(true);
309
+ const legacyUserSettings = {
310
+ includeDirectories: ['/user/dir'],
311
+ excludeTools: ['user-tool'],
312
+ excludedProjectEnvVars: ['USER_VAR'],
313
+ };
314
+ const legacyWorkspaceSettings = {
315
+ includeDirectories: ['/workspace/dir'],
316
+ excludeTools: ['workspace-tool'],
317
+ excludedProjectEnvVars: ['WORKSPACE_VAR', 'USER_VAR'],
318
+ };
319
+ fs.readFileSync.mockImplementation((p) => {
320
+ if (p === USER_SETTINGS_PATH)
321
+ return JSON.stringify(legacyUserSettings);
322
+ if (p === MOCK_WORKSPACE_SETTINGS_PATH)
323
+ return JSON.stringify(legacyWorkspaceSettings);
324
+ return '{}';
325
+ });
326
+ const settings = loadSettings(MOCK_WORKSPACE_DIR);
327
+ // Verify includeDirectories are concatenated
328
+ expect(settings.merged.context?.includeDirectories).toEqual([
329
+ '/user/dir',
330
+ '/workspace/dir',
331
+ ]);
332
+ // Verify excludeTools are concatenated and de-duped
333
+ expect(settings.merged.tools?.exclude).toEqual([
334
+ 'user-tool',
335
+ 'workspace-tool',
336
+ ]);
337
+ // Verify excludedProjectEnvVars are concatenated and de-duped
338
+ expect(settings.merged.advanced?.excludedEnvVars).toEqual(expect.arrayContaining(['USER_VAR', 'WORKSPACE_VAR']));
339
+ expect(settings.merged.advanced?.excludedEnvVars).toHaveLength(2);
340
+ });
341
+ it('should merge all settings files with the correct precedence', () => {
342
+ mockFsExistsSync.mockReturnValue(true);
343
+ const systemDefaultsContent = {
344
+ ui: {
345
+ theme: 'default-theme',
346
+ },
347
+ tools: {
348
+ sandbox: true,
349
+ },
350
+ telemetry: true,
351
+ context: {
352
+ includeDirectories: ['/system/defaults/dir'],
353
+ },
354
+ };
355
+ const userSettingsContent = {
356
+ ui: {
357
+ theme: 'user-theme',
358
+ },
359
+ context: {
360
+ fileName: 'USER_CONTEXT.md',
361
+ includeDirectories: ['/user/dir1', '/user/dir2'],
362
+ },
363
+ };
364
+ const workspaceSettingsContent = {
365
+ tools: {
366
+ sandbox: false,
367
+ },
368
+ context: {
369
+ fileName: 'WORKSPACE_CONTEXT.md',
370
+ includeDirectories: ['/workspace/dir'],
371
+ },
372
+ };
373
+ const systemSettingsContent = {
374
+ ui: {
375
+ theme: 'system-theme',
376
+ },
377
+ telemetry: false,
378
+ context: {
379
+ includeDirectories: ['/system/dir'],
380
+ },
381
+ };
382
+ fs.readFileSync.mockImplementation((p) => {
383
+ if (p === getSystemDefaultsPath())
384
+ return JSON.stringify(systemDefaultsContent);
385
+ if (p === getSystemSettingsPath())
386
+ return JSON.stringify(systemSettingsContent);
387
+ if (p === USER_SETTINGS_PATH)
388
+ return JSON.stringify(userSettingsContent);
389
+ if (p === MOCK_WORKSPACE_SETTINGS_PATH)
390
+ return JSON.stringify(workspaceSettingsContent);
391
+ return '';
392
+ });
393
+ const settings = loadSettings(MOCK_WORKSPACE_DIR);
394
+ expect(settings.systemDefaults.settings).toEqual(systemDefaultsContent);
395
+ expect(settings.system.settings).toEqual(systemSettingsContent);
396
+ expect(settings.user.settings).toEqual(userSettingsContent);
397
+ expect(settings.workspace.settings).toEqual(workspaceSettingsContent);
398
+ expect(settings.merged).toEqual({
399
+ context: {
400
+ fileName: 'WORKSPACE_CONTEXT.md',
401
+ includeDirectories: [
402
+ '/system/defaults/dir',
403
+ '/user/dir1',
404
+ '/user/dir2',
405
+ '/workspace/dir',
406
+ '/system/dir',
407
+ ],
408
+ },
409
+ telemetry: false,
410
+ tools: {
411
+ sandbox: false,
412
+ },
413
+ ui: {
414
+ theme: 'system-theme',
415
+ },
416
+ });
417
+ });
418
+ it('should use folderTrust from workspace settings when trusted', () => {
419
+ mockFsExistsSync.mockReturnValue(true);
420
+ const userSettingsContent = {
421
+ security: {
422
+ folderTrust: {
423
+ enabled: true,
424
+ },
425
+ },
426
+ };
427
+ const workspaceSettingsContent = {
428
+ security: {
429
+ folderTrust: {
430
+ enabled: false, // This should be used
431
+ },
432
+ },
433
+ };
434
+ const systemSettingsContent = {
435
+ // No folderTrust here
436
+ };
437
+ fs.readFileSync.mockImplementation((p) => {
438
+ if (p === getSystemSettingsPath())
439
+ return JSON.stringify(systemSettingsContent);
440
+ if (p === USER_SETTINGS_PATH)
441
+ return JSON.stringify(userSettingsContent);
442
+ if (p === MOCK_WORKSPACE_SETTINGS_PATH)
443
+ return JSON.stringify(workspaceSettingsContent);
444
+ return '{}';
445
+ });
446
+ const settings = loadSettings(MOCK_WORKSPACE_DIR);
447
+ expect(settings.merged.security?.folderTrust?.enabled).toBe(false); // Workspace setting should be used
448
+ });
449
+ it('should use system folderTrust over user setting', () => {
450
+ mockFsExistsSync.mockReturnValue(true);
451
+ const userSettingsContent = {
452
+ security: {
453
+ folderTrust: {
454
+ enabled: false,
455
+ },
456
+ },
457
+ };
458
+ const workspaceSettingsContent = {
459
+ security: {
460
+ folderTrust: {
461
+ enabled: true, // This should be ignored
462
+ },
463
+ },
464
+ };
465
+ const systemSettingsContent = {
466
+ security: {
467
+ folderTrust: {
468
+ enabled: true,
469
+ },
470
+ },
471
+ };
472
+ fs.readFileSync.mockImplementation((p) => {
473
+ if (p === getSystemSettingsPath())
474
+ return JSON.stringify(systemSettingsContent);
475
+ if (p === USER_SETTINGS_PATH)
476
+ return JSON.stringify(userSettingsContent);
477
+ if (p === MOCK_WORKSPACE_SETTINGS_PATH)
478
+ return JSON.stringify(workspaceSettingsContent);
479
+ return '{}';
480
+ });
481
+ const settings = loadSettings(MOCK_WORKSPACE_DIR);
482
+ expect(settings.merged.security?.folderTrust?.enabled).toBe(true); // System setting should be used
483
+ });
484
+ it('should handle contextFileName correctly when only in user settings', () => {
485
+ mockFsExistsSync.mockImplementation((p) => p === USER_SETTINGS_PATH);
486
+ const userSettingsContent = { context: { fileName: 'CUSTOM.md' } };
487
+ fs.readFileSync.mockImplementation((p) => {
488
+ if (p === USER_SETTINGS_PATH)
489
+ return JSON.stringify(userSettingsContent);
490
+ return '';
491
+ });
492
+ const settings = loadSettings(MOCK_WORKSPACE_DIR);
493
+ expect(settings.merged.context?.fileName).toBe('CUSTOM.md');
494
+ });
495
+ it('should handle contextFileName correctly when only in workspace settings', () => {
496
+ mockFsExistsSync.mockImplementation((p) => p === MOCK_WORKSPACE_SETTINGS_PATH);
497
+ const workspaceSettingsContent = {
498
+ context: { fileName: 'PROJECT_SPECIFIC.md' },
499
+ };
500
+ fs.readFileSync.mockImplementation((p) => {
501
+ if (p === MOCK_WORKSPACE_SETTINGS_PATH)
502
+ return JSON.stringify(workspaceSettingsContent);
503
+ return '';
504
+ });
505
+ const settings = loadSettings(MOCK_WORKSPACE_DIR);
506
+ expect(settings.merged.context?.fileName).toBe('PROJECT_SPECIFIC.md');
507
+ });
508
+ it('should handle excludedProjectEnvVars correctly when only in user settings', () => {
509
+ mockFsExistsSync.mockImplementation((p) => p === USER_SETTINGS_PATH);
510
+ const userSettingsContent = {
511
+ general: {},
512
+ advanced: { excludedEnvVars: ['DEBUG', 'NODE_ENV', 'CUSTOM_VAR'] },
513
+ };
514
+ fs.readFileSync.mockImplementation((p) => {
515
+ if (p === USER_SETTINGS_PATH)
516
+ return JSON.stringify(userSettingsContent);
517
+ return '';
518
+ });
519
+ const settings = loadSettings(MOCK_WORKSPACE_DIR);
520
+ expect(settings.merged.advanced?.excludedEnvVars).toEqual([
521
+ 'DEBUG',
522
+ 'NODE_ENV',
523
+ 'CUSTOM_VAR',
524
+ ]);
525
+ });
526
+ it('should handle excludedProjectEnvVars correctly when only in workspace settings', () => {
527
+ mockFsExistsSync.mockImplementation((p) => p === MOCK_WORKSPACE_SETTINGS_PATH);
528
+ const workspaceSettingsContent = {
529
+ general: {},
530
+ advanced: { excludedEnvVars: ['WORKSPACE_DEBUG', 'WORKSPACE_VAR'] },
531
+ };
532
+ fs.readFileSync.mockImplementation((p) => {
533
+ if (p === MOCK_WORKSPACE_SETTINGS_PATH)
534
+ return JSON.stringify(workspaceSettingsContent);
535
+ return '';
536
+ });
537
+ const settings = loadSettings(MOCK_WORKSPACE_DIR);
538
+ expect(settings.merged.advanced?.excludedEnvVars).toEqual([
539
+ 'WORKSPACE_DEBUG',
540
+ 'WORKSPACE_VAR',
541
+ ]);
542
+ });
543
+ it('should merge excludedProjectEnvVars with workspace taking precedence over user', () => {
544
+ mockFsExistsSync.mockImplementation((p) => p === USER_SETTINGS_PATH || p === MOCK_WORKSPACE_SETTINGS_PATH);
545
+ const userSettingsContent = {
546
+ general: {},
547
+ advanced: { excludedEnvVars: ['DEBUG', 'NODE_ENV', 'USER_VAR'] },
548
+ };
549
+ const workspaceSettingsContent = {
550
+ general: {},
551
+ advanced: { excludedEnvVars: ['WORKSPACE_DEBUG', 'WORKSPACE_VAR'] },
552
+ };
553
+ fs.readFileSync.mockImplementation((p) => {
554
+ if (p === USER_SETTINGS_PATH)
555
+ return JSON.stringify(userSettingsContent);
556
+ if (p === MOCK_WORKSPACE_SETTINGS_PATH)
557
+ return JSON.stringify(workspaceSettingsContent);
558
+ return '';
559
+ });
560
+ const settings = loadSettings(MOCK_WORKSPACE_DIR);
561
+ expect(settings.user.settings.advanced?.excludedEnvVars).toEqual([
562
+ 'DEBUG',
563
+ 'NODE_ENV',
564
+ 'USER_VAR',
565
+ ]);
566
+ expect(settings.workspace.settings.advanced?.excludedEnvVars).toEqual([
567
+ 'WORKSPACE_DEBUG',
568
+ 'WORKSPACE_VAR',
569
+ ]);
570
+ expect(settings.merged.advanced?.excludedEnvVars).toEqual([
571
+ 'DEBUG',
572
+ 'NODE_ENV',
573
+ 'USER_VAR',
574
+ 'WORKSPACE_DEBUG',
575
+ 'WORKSPACE_VAR',
576
+ ]);
577
+ });
578
+ it('should default contextFileName to undefined if not in any settings file', () => {
579
+ mockFsExistsSync.mockImplementation((p) => p === USER_SETTINGS_PATH || p === MOCK_WORKSPACE_SETTINGS_PATH);
580
+ const userSettingsContent = { ui: { theme: 'dark' } };
581
+ const workspaceSettingsContent = { tools: { sandbox: true } };
582
+ fs.readFileSync.mockImplementation((p) => {
583
+ if (p === USER_SETTINGS_PATH)
584
+ return JSON.stringify(userSettingsContent);
585
+ if (p === MOCK_WORKSPACE_SETTINGS_PATH)
586
+ return JSON.stringify(workspaceSettingsContent);
587
+ return '';
588
+ });
589
+ const settings = loadSettings(MOCK_WORKSPACE_DIR);
590
+ expect(settings.merged.context?.fileName).toBeUndefined();
591
+ });
592
+ it('should load telemetry setting from user settings', () => {
593
+ mockFsExistsSync.mockImplementation((p) => p === USER_SETTINGS_PATH);
594
+ const userSettingsContent = { telemetry: { enabled: true } };
595
+ fs.readFileSync.mockImplementation((p) => {
596
+ if (p === USER_SETTINGS_PATH)
597
+ return JSON.stringify(userSettingsContent);
598
+ return '{}';
599
+ });
600
+ const settings = loadSettings(MOCK_WORKSPACE_DIR);
601
+ expect(settings.merged.telemetry?.enabled).toBe(true);
602
+ });
603
+ it('should load telemetry setting from workspace settings', () => {
604
+ mockFsExistsSync.mockImplementation((p) => p === MOCK_WORKSPACE_SETTINGS_PATH);
605
+ const workspaceSettingsContent = { telemetry: { enabled: false } };
606
+ fs.readFileSync.mockImplementation((p) => {
607
+ if (p === MOCK_WORKSPACE_SETTINGS_PATH)
608
+ return JSON.stringify(workspaceSettingsContent);
609
+ return '{}';
610
+ });
611
+ const settings = loadSettings(MOCK_WORKSPACE_DIR);
612
+ expect(settings.merged.telemetry?.enabled).toBe(false);
613
+ });
614
+ it('should prioritize workspace telemetry setting over user setting', () => {
615
+ mockFsExistsSync.mockReturnValue(true);
616
+ const userSettingsContent = { telemetry: { enabled: true } };
617
+ const workspaceSettingsContent = { telemetry: { enabled: false } };
618
+ fs.readFileSync.mockImplementation((p) => {
619
+ if (p === USER_SETTINGS_PATH)
620
+ return JSON.stringify(userSettingsContent);
621
+ if (p === MOCK_WORKSPACE_SETTINGS_PATH)
622
+ return JSON.stringify(workspaceSettingsContent);
623
+ return '{}';
624
+ });
625
+ const settings = loadSettings(MOCK_WORKSPACE_DIR);
626
+ expect(settings.merged.telemetry?.enabled).toBe(false);
627
+ });
628
+ it('should have telemetry as undefined if not in any settings file', () => {
629
+ mockFsExistsSync.mockReturnValue(false); // No settings files exist
630
+ fs.readFileSync.mockReturnValue('{}');
631
+ const settings = loadSettings(MOCK_WORKSPACE_DIR);
632
+ expect(settings.merged.telemetry).toBeUndefined();
633
+ expect(settings.merged.ui).toBeUndefined();
634
+ expect(settings.merged.mcpServers).toBeUndefined();
635
+ });
636
+ it('should merge MCP servers correctly, with workspace taking precedence', () => {
637
+ mockFsExistsSync.mockImplementation((p) => p === USER_SETTINGS_PATH || p === MOCK_WORKSPACE_SETTINGS_PATH);
638
+ const userSettingsContent = {
639
+ mcpServers: {
640
+ 'user-server': {
641
+ command: 'user-command',
642
+ args: ['--user-arg'],
643
+ description: 'User MCP server',
644
+ },
645
+ 'shared-server': {
646
+ command: 'user-shared-command',
647
+ description: 'User shared server config',
648
+ },
649
+ },
650
+ };
651
+ const workspaceSettingsContent = {
652
+ mcpServers: {
653
+ 'workspace-server': {
654
+ command: 'workspace-command',
655
+ args: ['--workspace-arg'],
656
+ description: 'Workspace MCP server',
657
+ },
658
+ 'shared-server': {
659
+ command: 'workspace-shared-command',
660
+ description: 'Workspace shared server config',
661
+ },
662
+ },
663
+ };
664
+ fs.readFileSync.mockImplementation((p) => {
665
+ if (p === USER_SETTINGS_PATH)
666
+ return JSON.stringify(userSettingsContent);
667
+ if (p === MOCK_WORKSPACE_SETTINGS_PATH)
668
+ return JSON.stringify(workspaceSettingsContent);
669
+ return '';
670
+ });
671
+ const settings = loadSettings(MOCK_WORKSPACE_DIR);
672
+ expect(settings.user.settings).toEqual(userSettingsContent);
673
+ expect(settings.workspace.settings).toEqual(workspaceSettingsContent);
674
+ expect(settings.merged.mcpServers).toEqual({
675
+ 'user-server': {
676
+ command: 'user-command',
677
+ args: ['--user-arg'],
678
+ description: 'User MCP server',
679
+ },
680
+ 'workspace-server': {
681
+ command: 'workspace-command',
682
+ args: ['--workspace-arg'],
683
+ description: 'Workspace MCP server',
684
+ },
685
+ 'shared-server': {
686
+ command: 'workspace-shared-command',
687
+ description: 'Workspace shared server config',
688
+ },
689
+ });
690
+ });
691
+ it('should handle MCP servers when only in user settings', () => {
692
+ mockFsExistsSync.mockImplementation((p) => p === USER_SETTINGS_PATH);
693
+ const userSettingsContent = {
694
+ mcpServers: {
695
+ 'user-only-server': {
696
+ command: 'user-only-command',
697
+ description: 'User only server',
698
+ },
699
+ },
700
+ };
701
+ fs.readFileSync.mockImplementation((p) => {
702
+ if (p === USER_SETTINGS_PATH)
703
+ return JSON.stringify(userSettingsContent);
704
+ return '';
705
+ });
706
+ const settings = loadSettings(MOCK_WORKSPACE_DIR);
707
+ expect(settings.merged.mcpServers).toEqual({
708
+ 'user-only-server': {
709
+ command: 'user-only-command',
710
+ description: 'User only server',
711
+ },
712
+ });
713
+ });
714
+ it('should handle MCP servers when only in workspace settings', () => {
715
+ mockFsExistsSync.mockImplementation((p) => p === MOCK_WORKSPACE_SETTINGS_PATH);
716
+ const workspaceSettingsContent = {
717
+ mcpServers: {
718
+ 'workspace-only-server': {
719
+ command: 'workspace-only-command',
720
+ description: 'Workspace only server',
721
+ },
722
+ },
723
+ };
724
+ fs.readFileSync.mockImplementation((p) => {
725
+ if (p === MOCK_WORKSPACE_SETTINGS_PATH)
726
+ return JSON.stringify(workspaceSettingsContent);
727
+ return '';
728
+ });
729
+ const settings = loadSettings(MOCK_WORKSPACE_DIR);
730
+ expect(settings.merged.mcpServers).toEqual({
731
+ 'workspace-only-server': {
732
+ command: 'workspace-only-command',
733
+ description: 'Workspace only server',
734
+ },
735
+ });
736
+ });
737
+ it('should have mcpServers as undefined if not in any settings file', () => {
738
+ mockFsExistsSync.mockReturnValue(false); // No settings files exist
739
+ fs.readFileSync.mockReturnValue('{}');
740
+ const settings = loadSettings(MOCK_WORKSPACE_DIR);
741
+ expect(settings.merged.mcpServers).toBeUndefined();
742
+ });
743
+ it('should merge MCP servers from system, user, and workspace with system taking precedence', () => {
744
+ mockFsExistsSync.mockReturnValue(true);
745
+ const systemSettingsContent = {
746
+ mcpServers: {
747
+ 'shared-server': {
748
+ command: 'system-command',
749
+ args: ['--system-arg'],
750
+ },
751
+ 'system-only-server': {
752
+ command: 'system-only-command',
753
+ },
754
+ },
755
+ };
756
+ const userSettingsContent = {
757
+ mcpServers: {
758
+ 'user-server': {
759
+ command: 'user-command',
760
+ },
761
+ 'shared-server': {
762
+ command: 'user-command',
763
+ description: 'from user',
764
+ },
765
+ },
766
+ };
767
+ const workspaceSettingsContent = {
768
+ mcpServers: {
769
+ 'workspace-server': {
770
+ command: 'workspace-command',
771
+ },
772
+ 'shared-server': {
773
+ command: 'workspace-command',
774
+ args: ['--workspace-arg'],
775
+ },
776
+ },
777
+ };
778
+ fs.readFileSync.mockImplementation((p) => {
779
+ if (p === getSystemSettingsPath())
780
+ return JSON.stringify(systemSettingsContent);
781
+ if (p === USER_SETTINGS_PATH)
782
+ return JSON.stringify(userSettingsContent);
783
+ if (p === MOCK_WORKSPACE_SETTINGS_PATH)
784
+ return JSON.stringify(workspaceSettingsContent);
785
+ return '{}';
786
+ });
787
+ const settings = loadSettings(MOCK_WORKSPACE_DIR);
788
+ expect(settings.merged.mcpServers).toEqual({
789
+ 'user-server': {
790
+ command: 'user-command',
791
+ },
792
+ 'workspace-server': {
793
+ command: 'workspace-command',
794
+ },
795
+ 'system-only-server': {
796
+ command: 'system-only-command',
797
+ },
798
+ 'shared-server': {
799
+ command: 'system-command',
800
+ args: ['--system-arg'],
801
+ },
802
+ });
803
+ });
804
+ it('should merge mcp allowed/excluded lists with system taking precedence over workspace', () => {
805
+ mockFsExistsSync.mockReturnValue(true);
806
+ const systemSettingsContent = {
807
+ mcp: {
808
+ allowed: ['system-allowed'],
809
+ },
810
+ };
811
+ const userSettingsContent = {
812
+ mcp: {
813
+ allowed: ['user-allowed'],
814
+ excluded: ['user-excluded'],
815
+ },
816
+ };
817
+ const workspaceSettingsContent = {
818
+ mcp: {
819
+ allowed: ['workspace-allowed'],
820
+ excluded: ['workspace-excluded'],
821
+ },
822
+ };
823
+ fs.readFileSync.mockImplementation((p) => {
824
+ if (p === getSystemSettingsPath())
825
+ return JSON.stringify(systemSettingsContent);
826
+ if (p === USER_SETTINGS_PATH)
827
+ return JSON.stringify(userSettingsContent);
828
+ if (p === MOCK_WORKSPACE_SETTINGS_PATH)
829
+ return JSON.stringify(workspaceSettingsContent);
830
+ return '{}';
831
+ });
832
+ const settings = loadSettings(MOCK_WORKSPACE_DIR);
833
+ expect(settings.merged.mcp).toEqual({
834
+ allowed: ['system-allowed'],
835
+ excluded: ['workspace-excluded'],
836
+ });
837
+ });
838
+ it('should merge chatCompression settings, with workspace taking precedence', () => {
839
+ mockFsExistsSync.mockReturnValue(true);
840
+ const userSettingsContent = {
841
+ general: {},
842
+ model: { chatCompression: { contextPercentageThreshold: 0.5 } },
843
+ };
844
+ const workspaceSettingsContent = {
845
+ general: {},
846
+ model: { chatCompression: { contextPercentageThreshold: 0.8 } },
847
+ };
848
+ fs.readFileSync.mockImplementation((p) => {
849
+ if (p === USER_SETTINGS_PATH)
850
+ return JSON.stringify(userSettingsContent);
851
+ if (p === MOCK_WORKSPACE_SETTINGS_PATH)
852
+ return JSON.stringify(workspaceSettingsContent);
853
+ return '{}';
854
+ });
855
+ const settings = loadSettings(MOCK_WORKSPACE_DIR);
856
+ const e = settings.user.settings.model?.chatCompression;
857
+ console.log(e);
858
+ expect(settings.user.settings.model?.chatCompression).toEqual({
859
+ contextPercentageThreshold: 0.5,
860
+ });
861
+ expect(settings.workspace.settings.model?.chatCompression).toEqual({
862
+ contextPercentageThreshold: 0.8,
863
+ });
864
+ expect(settings.merged.model?.chatCompression).toEqual({
865
+ contextPercentageThreshold: 0.8,
866
+ });
867
+ });
868
+ it('should merge output format settings, with workspace taking precedence', () => {
869
+ mockFsExistsSync.mockReturnValue(true);
870
+ const userSettingsContent = {
871
+ output: { format: 'text' },
872
+ };
873
+ const workspaceSettingsContent = {
874
+ output: { format: 'json' },
875
+ };
876
+ fs.readFileSync.mockImplementation((p) => {
877
+ if (p === USER_SETTINGS_PATH)
878
+ return JSON.stringify(userSettingsContent);
879
+ if (p === MOCK_WORKSPACE_SETTINGS_PATH)
880
+ return JSON.stringify(workspaceSettingsContent);
881
+ return '{}';
882
+ });
883
+ const settings = loadSettings(MOCK_WORKSPACE_DIR);
884
+ expect(settings.merged.output?.format).toBe('json');
885
+ });
886
+ it('should handle chatCompression when only in user settings', () => {
887
+ mockFsExistsSync.mockImplementation((p) => p === USER_SETTINGS_PATH);
888
+ const userSettingsContent = {
889
+ general: {},
890
+ model: { chatCompression: { contextPercentageThreshold: 0.5 } },
891
+ };
892
+ fs.readFileSync.mockImplementation((p) => {
893
+ if (p === USER_SETTINGS_PATH)
894
+ return JSON.stringify(userSettingsContent);
895
+ return '{}';
896
+ });
897
+ const settings = loadSettings(MOCK_WORKSPACE_DIR);
898
+ expect(settings.merged.model?.chatCompression).toEqual({
899
+ contextPercentageThreshold: 0.5,
900
+ });
901
+ });
902
+ it('should have model as undefined if not in any settings file', () => {
903
+ mockFsExistsSync.mockReturnValue(false); // No settings files exist
904
+ fs.readFileSync.mockReturnValue('{}');
905
+ const settings = loadSettings(MOCK_WORKSPACE_DIR);
906
+ expect(settings.merged.model).toBeUndefined();
907
+ });
908
+ it('should ignore chatCompression if contextPercentageThreshold is invalid', () => {
909
+ const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => { });
910
+ mockFsExistsSync.mockImplementation((p) => p === USER_SETTINGS_PATH);
911
+ const userSettingsContent = {
912
+ general: {},
913
+ model: { chatCompression: { contextPercentageThreshold: 1.5 } },
914
+ };
915
+ fs.readFileSync.mockImplementation((p) => {
916
+ if (p === USER_SETTINGS_PATH)
917
+ return JSON.stringify(userSettingsContent);
918
+ return '{}';
919
+ });
920
+ const settings = loadSettings(MOCK_WORKSPACE_DIR);
921
+ expect(settings.merged.model?.chatCompression).toEqual({
922
+ contextPercentageThreshold: 1.5,
923
+ });
924
+ warnSpy.mockRestore();
925
+ });
926
+ it('should deep merge chatCompression settings', () => {
927
+ mockFsExistsSync.mockReturnValue(true);
928
+ const userSettingsContent = {
929
+ general: {},
930
+ model: { chatCompression: { contextPercentageThreshold: 0.5 } },
931
+ };
932
+ const workspaceSettingsContent = {
933
+ general: {},
934
+ model: { chatCompression: {} },
935
+ };
936
+ fs.readFileSync.mockImplementation((p) => {
937
+ if (p === USER_SETTINGS_PATH)
938
+ return JSON.stringify(userSettingsContent);
939
+ if (p === MOCK_WORKSPACE_SETTINGS_PATH)
940
+ return JSON.stringify(workspaceSettingsContent);
941
+ return '{}';
942
+ });
943
+ const settings = loadSettings(MOCK_WORKSPACE_DIR);
944
+ expect(settings.merged.model?.chatCompression).toEqual({
945
+ contextPercentageThreshold: 0.5,
946
+ });
947
+ });
948
+ it('should merge includeDirectories from all scopes', () => {
949
+ mockFsExistsSync.mockReturnValue(true);
950
+ const systemSettingsContent = {
951
+ context: { includeDirectories: ['/system/dir'] },
952
+ };
953
+ const systemDefaultsContent = {
954
+ context: { includeDirectories: ['/system/defaults/dir'] },
955
+ };
956
+ const userSettingsContent = {
957
+ context: { includeDirectories: ['/user/dir1', '/user/dir2'] },
958
+ };
959
+ const workspaceSettingsContent = {
960
+ context: { includeDirectories: ['/workspace/dir'] },
961
+ };
962
+ fs.readFileSync.mockImplementation((p) => {
963
+ if (p === getSystemSettingsPath())
964
+ return JSON.stringify(systemSettingsContent);
965
+ if (p === getSystemDefaultsPath())
966
+ return JSON.stringify(systemDefaultsContent);
967
+ if (p === USER_SETTINGS_PATH)
968
+ return JSON.stringify(userSettingsContent);
969
+ if (p === MOCK_WORKSPACE_SETTINGS_PATH)
970
+ return JSON.stringify(workspaceSettingsContent);
971
+ return '{}';
972
+ });
973
+ const settings = loadSettings(MOCK_WORKSPACE_DIR);
974
+ expect(settings.merged.context?.includeDirectories).toEqual([
975
+ '/system/defaults/dir',
976
+ '/user/dir1',
977
+ '/user/dir2',
978
+ '/workspace/dir',
979
+ '/system/dir',
980
+ ]);
981
+ });
982
+ it('should handle JSON parsing errors gracefully', () => {
983
+ mockFsExistsSync.mockReturnValue(true); // Both files "exist"
984
+ const invalidJsonContent = 'invalid json';
985
+ const userReadError = new SyntaxError("Expected ',' or '}' after property value in JSON at position 10");
986
+ const workspaceReadError = new SyntaxError('Unexpected token i in JSON at position 0');
987
+ fs.readFileSync.mockImplementation((p) => {
988
+ if (p === USER_SETTINGS_PATH) {
989
+ // Simulate JSON.parse throwing for user settings
990
+ vi.spyOn(JSON, 'parse').mockImplementationOnce(() => {
991
+ throw userReadError;
992
+ });
993
+ return invalidJsonContent; // Content that would cause JSON.parse to throw
994
+ }
995
+ if (p === MOCK_WORKSPACE_SETTINGS_PATH) {
996
+ // Simulate JSON.parse throwing for workspace settings
997
+ vi.spyOn(JSON, 'parse').mockImplementationOnce(() => {
998
+ throw workspaceReadError;
999
+ });
1000
+ return invalidJsonContent;
1001
+ }
1002
+ return '{}'; // Default for other reads
1003
+ });
1004
+ try {
1005
+ loadSettings(MOCK_WORKSPACE_DIR);
1006
+ throw new Error('loadSettings should have thrown a FatalConfigError');
1007
+ }
1008
+ catch (e) {
1009
+ expect(e).toBeInstanceOf(FatalConfigError);
1010
+ const error = e;
1011
+ expect(error.message).toContain(`Error in ${USER_SETTINGS_PATH}: ${userReadError.message}`);
1012
+ expect(error.message).toContain(`Error in ${MOCK_WORKSPACE_SETTINGS_PATH}: ${workspaceReadError.message}`);
1013
+ expect(error.message).toContain('Please fix the configuration file(s) and try again.');
1014
+ }
1015
+ // Restore JSON.parse mock if it was spied on specifically for this test
1016
+ vi.restoreAllMocks(); // Or more targeted restore if needed
1017
+ });
1018
+ it('should resolve environment variables in user settings', () => {
1019
+ process.env['TEST_API_KEY'] = 'user_api_key_from_env';
1020
+ const userSettingsContent = {
1021
+ apiKey: '$TEST_API_KEY',
1022
+ someUrl: 'https://test.com/${TEST_API_KEY}',
1023
+ };
1024
+ mockFsExistsSync.mockImplementation((p) => p === USER_SETTINGS_PATH);
1025
+ fs.readFileSync.mockImplementation((p) => {
1026
+ if (p === USER_SETTINGS_PATH)
1027
+ return JSON.stringify(userSettingsContent);
1028
+ return '{}';
1029
+ });
1030
+ const settings = loadSettings(MOCK_WORKSPACE_DIR);
1031
+ expect(settings.user.settings['apiKey']).toBe('user_api_key_from_env');
1032
+ expect(settings.user.settings['someUrl']).toBe('https://test.com/user_api_key_from_env');
1033
+ expect(settings.merged['apiKey']).toBe('user_api_key_from_env');
1034
+ delete process.env['TEST_API_KEY'];
1035
+ });
1036
+ it('should resolve environment variables in workspace settings', () => {
1037
+ process.env['WORKSPACE_ENDPOINT'] = 'workspace_endpoint_from_env';
1038
+ const workspaceSettingsContent = {
1039
+ endpoint: '${WORKSPACE_ENDPOINT}/api',
1040
+ nested: { value: '$WORKSPACE_ENDPOINT' },
1041
+ };
1042
+ mockFsExistsSync.mockImplementation((p) => p === MOCK_WORKSPACE_SETTINGS_PATH);
1043
+ fs.readFileSync.mockImplementation((p) => {
1044
+ if (p === MOCK_WORKSPACE_SETTINGS_PATH)
1045
+ return JSON.stringify(workspaceSettingsContent);
1046
+ return '{}';
1047
+ });
1048
+ const settings = loadSettings(MOCK_WORKSPACE_DIR);
1049
+ expect(settings.workspace.settings['endpoint']).toBe('workspace_endpoint_from_env/api');
1050
+ const nested = settings.workspace.settings['nested'];
1051
+ expect(nested['value']).toBe('workspace_endpoint_from_env');
1052
+ expect(settings.merged['endpoint']).toBe('workspace_endpoint_from_env/api');
1053
+ delete process.env['WORKSPACE_ENDPOINT'];
1054
+ });
1055
+ it('should correctly resolve and merge env variables from different scopes', () => {
1056
+ process.env['SYSTEM_VAR'] = 'system_value';
1057
+ process.env['USER_VAR'] = 'user_value';
1058
+ process.env['WORKSPACE_VAR'] = 'workspace_value';
1059
+ process.env['SHARED_VAR'] = 'final_value';
1060
+ const systemSettingsContent = {
1061
+ configValue: '$SHARED_VAR',
1062
+ systemOnly: '$SYSTEM_VAR',
1063
+ };
1064
+ const userSettingsContent = {
1065
+ configValue: '$SHARED_VAR',
1066
+ userOnly: '$USER_VAR',
1067
+ ui: {
1068
+ theme: 'dark',
1069
+ },
1070
+ };
1071
+ const workspaceSettingsContent = {
1072
+ configValue: '$SHARED_VAR',
1073
+ workspaceOnly: '$WORKSPACE_VAR',
1074
+ ui: {
1075
+ theme: 'light',
1076
+ },
1077
+ };
1078
+ mockFsExistsSync.mockReturnValue(true);
1079
+ fs.readFileSync.mockImplementation((p) => {
1080
+ if (p === getSystemSettingsPath()) {
1081
+ return JSON.stringify(systemSettingsContent);
1082
+ }
1083
+ if (p === USER_SETTINGS_PATH) {
1084
+ return JSON.stringify(userSettingsContent);
1085
+ }
1086
+ if (p === MOCK_WORKSPACE_SETTINGS_PATH) {
1087
+ return JSON.stringify(workspaceSettingsContent);
1088
+ }
1089
+ return '{}';
1090
+ });
1091
+ const settings = loadSettings(MOCK_WORKSPACE_DIR);
1092
+ // Check resolved values in individual scopes
1093
+ expect(settings.system.settings['configValue']).toBe('final_value');
1094
+ expect(settings.system.settings['systemOnly']).toBe('system_value');
1095
+ expect(settings.user.settings['configValue']).toBe('final_value');
1096
+ expect(settings.user.settings['userOnly']).toBe('user_value');
1097
+ expect(settings.workspace.settings['configValue']).toBe('final_value');
1098
+ expect(settings.workspace.settings['workspaceOnly']).toBe('workspace_value');
1099
+ // Check merged values (system > workspace > user)
1100
+ expect(settings.merged['configValue']).toBe('final_value');
1101
+ expect(settings.merged['systemOnly']).toBe('system_value');
1102
+ expect(settings.merged['userOnly']).toBe('user_value');
1103
+ expect(settings.merged['workspaceOnly']).toBe('workspace_value');
1104
+ expect(settings.merged.ui?.theme).toBe('light'); // workspace overrides user
1105
+ delete process.env['SYSTEM_VAR'];
1106
+ delete process.env['USER_VAR'];
1107
+ delete process.env['WORKSPACE_VAR'];
1108
+ delete process.env['SHARED_VAR'];
1109
+ });
1110
+ it('should correctly merge dnsResolutionOrder with workspace taking precedence', () => {
1111
+ mockFsExistsSync.mockReturnValue(true);
1112
+ const userSettingsContent = {
1113
+ advanced: { dnsResolutionOrder: 'ipv4first' },
1114
+ };
1115
+ const workspaceSettingsContent = {
1116
+ advanced: { dnsResolutionOrder: 'verbatim' },
1117
+ };
1118
+ fs.readFileSync.mockImplementation((p) => {
1119
+ if (p === USER_SETTINGS_PATH)
1120
+ return JSON.stringify(userSettingsContent);
1121
+ if (p === MOCK_WORKSPACE_SETTINGS_PATH)
1122
+ return JSON.stringify(workspaceSettingsContent);
1123
+ return '{}';
1124
+ });
1125
+ const settings = loadSettings(MOCK_WORKSPACE_DIR);
1126
+ expect(settings.merged.advanced?.dnsResolutionOrder).toBe('verbatim');
1127
+ });
1128
+ it('should use user dnsResolutionOrder if workspace is not defined', () => {
1129
+ mockFsExistsSync.mockImplementation((p) => p === USER_SETTINGS_PATH);
1130
+ const userSettingsContent = {
1131
+ advanced: { dnsResolutionOrder: 'verbatim' },
1132
+ };
1133
+ fs.readFileSync.mockImplementation((p) => {
1134
+ if (p === USER_SETTINGS_PATH)
1135
+ return JSON.stringify(userSettingsContent);
1136
+ return '{}';
1137
+ });
1138
+ const settings = loadSettings(MOCK_WORKSPACE_DIR);
1139
+ expect(settings.merged.advanced?.dnsResolutionOrder).toBe('verbatim');
1140
+ });
1141
+ it('should leave unresolved environment variables as is', () => {
1142
+ const userSettingsContent = { apiKey: '$UNDEFINED_VAR' };
1143
+ mockFsExistsSync.mockImplementation((p) => p === USER_SETTINGS_PATH);
1144
+ fs.readFileSync.mockImplementation((p) => {
1145
+ if (p === USER_SETTINGS_PATH)
1146
+ return JSON.stringify(userSettingsContent);
1147
+ return '{}';
1148
+ });
1149
+ const settings = loadSettings(MOCK_WORKSPACE_DIR);
1150
+ expect(settings.user.settings['apiKey']).toBe('$UNDEFINED_VAR');
1151
+ expect(settings.merged['apiKey']).toBe('$UNDEFINED_VAR');
1152
+ });
1153
+ it('should resolve multiple environment variables in a single string', () => {
1154
+ process.env['VAR_A'] = 'valueA';
1155
+ process.env['VAR_B'] = 'valueB';
1156
+ const userSettingsContent = {
1157
+ path: '/path/$VAR_A/${VAR_B}/end',
1158
+ };
1159
+ mockFsExistsSync.mockImplementation((p) => p === USER_SETTINGS_PATH);
1160
+ fs.readFileSync.mockImplementation((p) => {
1161
+ if (p === USER_SETTINGS_PATH)
1162
+ return JSON.stringify(userSettingsContent);
1163
+ return '{}';
1164
+ });
1165
+ const settings = loadSettings(MOCK_WORKSPACE_DIR);
1166
+ expect(settings.user.settings['path']).toBe('/path/valueA/valueB/end');
1167
+ delete process.env['VAR_A'];
1168
+ delete process.env['VAR_B'];
1169
+ });
1170
+ it('should resolve environment variables in arrays', () => {
1171
+ process.env['ITEM_1'] = 'item1_env';
1172
+ process.env['ITEM_2'] = 'item2_env';
1173
+ const userSettingsContent = {
1174
+ list: ['$ITEM_1', '${ITEM_2}', 'literal'],
1175
+ };
1176
+ mockFsExistsSync.mockImplementation((p) => p === USER_SETTINGS_PATH);
1177
+ fs.readFileSync.mockImplementation((p) => {
1178
+ if (p === USER_SETTINGS_PATH)
1179
+ return JSON.stringify(userSettingsContent);
1180
+ return '{}';
1181
+ });
1182
+ const settings = loadSettings(MOCK_WORKSPACE_DIR);
1183
+ expect(settings.user.settings['list']).toEqual([
1184
+ 'item1_env',
1185
+ 'item2_env',
1186
+ 'literal',
1187
+ ]);
1188
+ delete process.env['ITEM_1'];
1189
+ delete process.env['ITEM_2'];
1190
+ });
1191
+ it('should correctly pass through null, boolean, and number types, and handle undefined properties', () => {
1192
+ process.env['MY_ENV_STRING'] = 'env_string_value';
1193
+ process.env['MY_ENV_STRING_NESTED'] = 'env_string_nested_value';
1194
+ const userSettingsContent = {
1195
+ nullVal: null,
1196
+ trueVal: true,
1197
+ falseVal: false,
1198
+ numberVal: 123.45,
1199
+ stringVal: '$MY_ENV_STRING',
1200
+ nestedObj: {
1201
+ nestedNull: null,
1202
+ nestedBool: true,
1203
+ nestedNum: 0,
1204
+ nestedString: 'literal',
1205
+ anotherEnv: '${MY_ENV_STRING_NESTED}',
1206
+ },
1207
+ };
1208
+ mockFsExistsSync.mockImplementation((p) => p === USER_SETTINGS_PATH);
1209
+ fs.readFileSync.mockImplementation((p) => {
1210
+ if (p === USER_SETTINGS_PATH)
1211
+ return JSON.stringify(userSettingsContent);
1212
+ return '{}';
1213
+ });
1214
+ const settings = loadSettings(MOCK_WORKSPACE_DIR);
1215
+ expect(settings.user.settings['nullVal']).toBeNull();
1216
+ expect(settings.user.settings['trueVal']).toBe(true);
1217
+ expect(settings.user.settings['falseVal']).toBe(false);
1218
+ expect(settings.user.settings['numberVal']).toBe(123.45);
1219
+ expect(settings.user.settings['stringVal']).toBe('env_string_value');
1220
+ expect(settings.user.settings['undefinedVal']).toBeUndefined();
1221
+ const nestedObj = settings.user.settings['nestedObj'];
1222
+ expect(nestedObj['nestedNull']).toBeNull();
1223
+ expect(nestedObj['nestedBool']).toBe(true);
1224
+ expect(nestedObj['nestedNum']).toBe(0);
1225
+ expect(nestedObj['nestedString']).toBe('literal');
1226
+ expect(nestedObj['anotherEnv']).toBe('env_string_nested_value');
1227
+ delete process.env['MY_ENV_STRING'];
1228
+ delete process.env['MY_ENV_STRING_NESTED'];
1229
+ });
1230
+ it('should resolve multiple concatenated environment variables in a single string value', () => {
1231
+ process.env['TEST_HOST'] = 'myhost';
1232
+ process.env['TEST_PORT'] = '9090';
1233
+ const userSettingsContent = {
1234
+ serverAddress: '${TEST_HOST}:${TEST_PORT}/api',
1235
+ };
1236
+ mockFsExistsSync.mockImplementation((p) => p === USER_SETTINGS_PATH);
1237
+ fs.readFileSync.mockImplementation((p) => {
1238
+ if (p === USER_SETTINGS_PATH)
1239
+ return JSON.stringify(userSettingsContent);
1240
+ return '{}';
1241
+ });
1242
+ const settings = loadSettings(MOCK_WORKSPACE_DIR);
1243
+ expect(settings.user.settings['serverAddress']).toBe('myhost:9090/api');
1244
+ delete process.env['TEST_HOST'];
1245
+ delete process.env['TEST_PORT'];
1246
+ });
1247
+ describe('when GEMINI_CLI_SYSTEM_SETTINGS_PATH is set', () => {
1248
+ const MOCK_ENV_SYSTEM_SETTINGS_PATH = '/mock/env/system/settings.json';
1249
+ beforeEach(() => {
1250
+ process.env['GEMINI_CLI_SYSTEM_SETTINGS_PATH'] =
1251
+ MOCK_ENV_SYSTEM_SETTINGS_PATH;
1252
+ });
1253
+ afterEach(() => {
1254
+ delete process.env['GEMINI_CLI_SYSTEM_SETTINGS_PATH'];
1255
+ });
1256
+ it('should load system settings from the path specified in the environment variable', () => {
1257
+ mockFsExistsSync.mockImplementation((p) => p === MOCK_ENV_SYSTEM_SETTINGS_PATH);
1258
+ const systemSettingsContent = {
1259
+ ui: { theme: 'env-var-theme' },
1260
+ tools: { sandbox: true },
1261
+ };
1262
+ fs.readFileSync.mockImplementation((p) => {
1263
+ if (p === MOCK_ENV_SYSTEM_SETTINGS_PATH)
1264
+ return JSON.stringify(systemSettingsContent);
1265
+ return '{}';
1266
+ });
1267
+ const settings = loadSettings(MOCK_WORKSPACE_DIR);
1268
+ expect(fs.readFileSync).toHaveBeenCalledWith(MOCK_ENV_SYSTEM_SETTINGS_PATH, 'utf-8');
1269
+ expect(settings.system.path).toBe(MOCK_ENV_SYSTEM_SETTINGS_PATH);
1270
+ expect(settings.system.settings).toEqual(systemSettingsContent);
1271
+ expect(settings.merged).toEqual({
1272
+ ...systemSettingsContent,
1273
+ });
1274
+ });
1275
+ });
1276
+ });
1277
+ describe('excludedProjectEnvVars integration', () => {
1278
+ const originalEnv = { ...process.env };
1279
+ beforeEach(() => {
1280
+ process.env = { ...originalEnv };
1281
+ });
1282
+ afterEach(() => {
1283
+ process.env = originalEnv;
1284
+ });
1285
+ it('should exclude DEBUG and DEBUG_MODE from project .env files by default', () => {
1286
+ // Create a workspace settings file with excludedProjectEnvVars
1287
+ const workspaceSettingsContent = {
1288
+ general: {},
1289
+ advanced: { excludedEnvVars: ['DEBUG', 'DEBUG_MODE'] },
1290
+ };
1291
+ mockFsExistsSync.mockImplementation((p) => p === MOCK_WORKSPACE_SETTINGS_PATH);
1292
+ fs.readFileSync.mockImplementation((p) => {
1293
+ if (p === MOCK_WORKSPACE_SETTINGS_PATH)
1294
+ return JSON.stringify(workspaceSettingsContent);
1295
+ return '{}';
1296
+ });
1297
+ // Mock findEnvFile to return a project .env file
1298
+ const originalFindEnvFile = loadSettings.findEnvFile;
1299
+ loadSettings.findEnvFile =
1300
+ () => '/mock/project/.env';
1301
+ // Mock fs.readFileSync for .env file content
1302
+ const originalReadFileSync = fs.readFileSync;
1303
+ fs.readFileSync.mockImplementation((p) => {
1304
+ if (p === '/mock/project/.env') {
1305
+ return 'DEBUG=true\nDEBUG_MODE=1\nGEMINI_API_KEY=test-key';
1306
+ }
1307
+ if (p === MOCK_WORKSPACE_SETTINGS_PATH) {
1308
+ return JSON.stringify(workspaceSettingsContent);
1309
+ }
1310
+ return '{}';
1311
+ });
1312
+ try {
1313
+ // This will call loadEnvironment internally with the merged settings
1314
+ const settings = loadSettings(MOCK_WORKSPACE_DIR);
1315
+ // Verify the settings were loaded correctly
1316
+ expect(settings.merged.advanced?.excludedEnvVars).toEqual([
1317
+ 'DEBUG',
1318
+ 'DEBUG_MODE',
1319
+ ]);
1320
+ // Note: We can't directly test process.env changes here because the mocking
1321
+ // prevents the actual file system operations, but we can verify the settings
1322
+ // are correctly merged and passed to loadEnvironment
1323
+ }
1324
+ finally {
1325
+ loadSettings.findEnvFile =
1326
+ originalFindEnvFile;
1327
+ fs.readFileSync.mockImplementation(originalReadFileSync);
1328
+ }
1329
+ });
1330
+ it('should respect custom excludedProjectEnvVars from user settings', () => {
1331
+ const userSettingsContent = {
1332
+ general: {},
1333
+ advanced: { excludedEnvVars: ['NODE_ENV', 'DEBUG'] },
1334
+ };
1335
+ mockFsExistsSync.mockImplementation((p) => p === USER_SETTINGS_PATH);
1336
+ fs.readFileSync.mockImplementation((p) => {
1337
+ if (p === USER_SETTINGS_PATH)
1338
+ return JSON.stringify(userSettingsContent);
1339
+ return '{}';
1340
+ });
1341
+ const settings = loadSettings(MOCK_WORKSPACE_DIR);
1342
+ expect(settings.user.settings.advanced?.excludedEnvVars).toEqual([
1343
+ 'NODE_ENV',
1344
+ 'DEBUG',
1345
+ ]);
1346
+ expect(settings.merged.advanced?.excludedEnvVars).toEqual([
1347
+ 'NODE_ENV',
1348
+ 'DEBUG',
1349
+ ]);
1350
+ });
1351
+ it('should merge excludedProjectEnvVars with workspace taking precedence', () => {
1352
+ const userSettingsContent = {
1353
+ general: {},
1354
+ advanced: { excludedEnvVars: ['DEBUG', 'NODE_ENV', 'USER_VAR'] },
1355
+ };
1356
+ const workspaceSettingsContent = {
1357
+ general: {},
1358
+ advanced: { excludedEnvVars: ['WORKSPACE_DEBUG', 'WORKSPACE_VAR'] },
1359
+ };
1360
+ mockFsExistsSync.mockReturnValue(true);
1361
+ fs.readFileSync.mockImplementation((p) => {
1362
+ if (p === USER_SETTINGS_PATH)
1363
+ return JSON.stringify(userSettingsContent);
1364
+ if (p === MOCK_WORKSPACE_SETTINGS_PATH)
1365
+ return JSON.stringify(workspaceSettingsContent);
1366
+ return '{}';
1367
+ });
1368
+ const settings = loadSettings(MOCK_WORKSPACE_DIR);
1369
+ expect(settings.user.settings.advanced?.excludedEnvVars).toEqual([
1370
+ 'DEBUG',
1371
+ 'NODE_ENV',
1372
+ 'USER_VAR',
1373
+ ]);
1374
+ expect(settings.workspace.settings.advanced?.excludedEnvVars).toEqual([
1375
+ 'WORKSPACE_DEBUG',
1376
+ 'WORKSPACE_VAR',
1377
+ ]);
1378
+ expect(settings.merged.advanced?.excludedEnvVars).toEqual([
1379
+ 'DEBUG',
1380
+ 'NODE_ENV',
1381
+ 'USER_VAR',
1382
+ 'WORKSPACE_DEBUG',
1383
+ 'WORKSPACE_VAR',
1384
+ ]);
1385
+ });
1386
+ });
1387
+ describe('with workspace trust', () => {
1388
+ it('should merge workspace settings when workspace is trusted', () => {
1389
+ mockFsExistsSync.mockReturnValue(true);
1390
+ const userSettingsContent = {
1391
+ ui: { theme: 'dark' },
1392
+ tools: { sandbox: false },
1393
+ };
1394
+ const workspaceSettingsContent = {
1395
+ tools: { sandbox: true },
1396
+ context: { fileName: 'WORKSPACE.md' },
1397
+ };
1398
+ fs.readFileSync.mockImplementation((p) => {
1399
+ if (p === USER_SETTINGS_PATH)
1400
+ return JSON.stringify(userSettingsContent);
1401
+ if (p === MOCK_WORKSPACE_SETTINGS_PATH)
1402
+ return JSON.stringify(workspaceSettingsContent);
1403
+ return '{}';
1404
+ });
1405
+ const settings = loadSettings(MOCK_WORKSPACE_DIR);
1406
+ expect(settings.merged.tools?.sandbox).toBe(true);
1407
+ expect(settings.merged.context?.fileName).toBe('WORKSPACE.md');
1408
+ expect(settings.merged.ui?.theme).toBe('dark');
1409
+ });
1410
+ it('should NOT merge workspace settings when workspace is not trusted', () => {
1411
+ vi.mocked(isWorkspaceTrusted).mockReturnValue({
1412
+ isTrusted: false,
1413
+ source: 'file',
1414
+ });
1415
+ mockFsExistsSync.mockReturnValue(true);
1416
+ const userSettingsContent = {
1417
+ ui: { theme: 'dark' },
1418
+ tools: { sandbox: false },
1419
+ context: { fileName: 'USER.md' },
1420
+ };
1421
+ const workspaceSettingsContent = {
1422
+ tools: { sandbox: true },
1423
+ context: { fileName: 'WORKSPACE.md' },
1424
+ };
1425
+ fs.readFileSync.mockImplementation((p) => {
1426
+ if (p === USER_SETTINGS_PATH)
1427
+ return JSON.stringify(userSettingsContent);
1428
+ if (p === MOCK_WORKSPACE_SETTINGS_PATH)
1429
+ return JSON.stringify(workspaceSettingsContent);
1430
+ return '{}';
1431
+ });
1432
+ const settings = loadSettings(MOCK_WORKSPACE_DIR);
1433
+ expect(settings.merged.tools?.sandbox).toBe(false); // User setting
1434
+ expect(settings.merged.context?.fileName).toBe('USER.md'); // User setting
1435
+ expect(settings.merged.ui?.theme).toBe('dark'); // User setting
1436
+ });
1437
+ });
1438
+ describe('migrateSettingsToV1', () => {
1439
+ it('should handle an empty object', () => {
1440
+ const v2Settings = {};
1441
+ const v1Settings = migrateSettingsToV1(v2Settings);
1442
+ expect(v1Settings).toEqual({});
1443
+ });
1444
+ it('should migrate a simple v2 settings object to v1', () => {
1445
+ const v2Settings = {
1446
+ general: {
1447
+ preferredEditor: 'vscode',
1448
+ vimMode: true,
1449
+ },
1450
+ ui: {
1451
+ theme: 'dark',
1452
+ },
1453
+ };
1454
+ const v1Settings = migrateSettingsToV1(v2Settings);
1455
+ expect(v1Settings).toEqual({
1456
+ preferredEditor: 'vscode',
1457
+ vimMode: true,
1458
+ theme: 'dark',
1459
+ });
1460
+ });
1461
+ it('should handle nested properties correctly', () => {
1462
+ const v2Settings = {
1463
+ security: {
1464
+ folderTrust: {
1465
+ enabled: true,
1466
+ },
1467
+ auth: {
1468
+ selectedType: 'oauth',
1469
+ },
1470
+ },
1471
+ advanced: {
1472
+ autoConfigureMemory: true,
1473
+ },
1474
+ };
1475
+ const v1Settings = migrateSettingsToV1(v2Settings);
1476
+ expect(v1Settings).toEqual({
1477
+ folderTrust: true,
1478
+ selectedAuthType: 'oauth',
1479
+ autoConfigureMaxOldSpaceSize: true,
1480
+ });
1481
+ });
1482
+ it('should preserve mcpServers at the top level', () => {
1483
+ const v2Settings = {
1484
+ general: {
1485
+ preferredEditor: 'vscode',
1486
+ },
1487
+ mcpServers: {
1488
+ 'my-server': {
1489
+ command: 'npm start',
1490
+ },
1491
+ },
1492
+ };
1493
+ const v1Settings = migrateSettingsToV1(v2Settings);
1494
+ expect(v1Settings).toEqual({
1495
+ preferredEditor: 'vscode',
1496
+ mcpServers: {
1497
+ 'my-server': {
1498
+ command: 'npm start',
1499
+ },
1500
+ },
1501
+ });
1502
+ });
1503
+ it('should carry over unrecognized top-level properties', () => {
1504
+ const v2Settings = {
1505
+ general: {
1506
+ vimMode: false,
1507
+ },
1508
+ unrecognized: 'value',
1509
+ another: {
1510
+ nested: true,
1511
+ },
1512
+ };
1513
+ const v1Settings = migrateSettingsToV1(v2Settings);
1514
+ expect(v1Settings).toEqual({
1515
+ vimMode: false,
1516
+ unrecognized: 'value',
1517
+ another: {
1518
+ nested: true,
1519
+ },
1520
+ });
1521
+ });
1522
+ it('should handle a complex object with mixed properties', () => {
1523
+ const v2Settings = {
1524
+ general: {
1525
+ disableAutoUpdate: true,
1526
+ },
1527
+ ui: {
1528
+ hideBanner: true,
1529
+ customThemes: {
1530
+ myTheme: {},
1531
+ },
1532
+ },
1533
+ model: {
1534
+ name: 'gemini-pro',
1535
+ chatCompression: {
1536
+ contextPercentageThreshold: 0.5,
1537
+ },
1538
+ },
1539
+ mcpServers: {
1540
+ 'server-1': {
1541
+ command: 'node server.js',
1542
+ },
1543
+ },
1544
+ unrecognized: {
1545
+ should: 'be-preserved',
1546
+ },
1547
+ };
1548
+ const v1Settings = migrateSettingsToV1(v2Settings);
1549
+ expect(v1Settings).toEqual({
1550
+ disableAutoUpdate: true,
1551
+ hideBanner: true,
1552
+ customThemes: {
1553
+ myTheme: {},
1554
+ },
1555
+ model: 'gemini-pro',
1556
+ chatCompression: {
1557
+ contextPercentageThreshold: 0.5,
1558
+ },
1559
+ mcpServers: {
1560
+ 'server-1': {
1561
+ command: 'node server.js',
1562
+ },
1563
+ },
1564
+ unrecognized: {
1565
+ should: 'be-preserved',
1566
+ },
1567
+ });
1568
+ });
1569
+ it('should not migrate a v1 settings object', () => {
1570
+ const v1Settings = {
1571
+ preferredEditor: 'vscode',
1572
+ vimMode: true,
1573
+ theme: 'dark',
1574
+ };
1575
+ const migratedSettings = migrateSettingsToV1(v1Settings);
1576
+ expect(migratedSettings).toEqual({
1577
+ preferredEditor: 'vscode',
1578
+ vimMode: true,
1579
+ theme: 'dark',
1580
+ });
1581
+ });
1582
+ it('should migrate a full v2 settings object to v1', () => {
1583
+ const v2Settings = {
1584
+ general: {
1585
+ preferredEditor: 'code',
1586
+ vimMode: true,
1587
+ },
1588
+ ui: {
1589
+ theme: 'dark',
1590
+ },
1591
+ privacy: {
1592
+ usageStatisticsEnabled: false,
1593
+ },
1594
+ model: {
1595
+ name: 'gemini-pro',
1596
+ chatCompression: {
1597
+ contextPercentageThreshold: 0.8,
1598
+ },
1599
+ },
1600
+ context: {
1601
+ fileName: 'CONTEXT.md',
1602
+ includeDirectories: ['/src'],
1603
+ },
1604
+ tools: {
1605
+ sandbox: true,
1606
+ exclude: ['toolA'],
1607
+ },
1608
+ mcp: {
1609
+ allowed: ['server1'],
1610
+ },
1611
+ security: {
1612
+ folderTrust: {
1613
+ enabled: true,
1614
+ },
1615
+ },
1616
+ advanced: {
1617
+ dnsResolutionOrder: 'ipv4first',
1618
+ excludedEnvVars: ['SECRET'],
1619
+ },
1620
+ mcpServers: {
1621
+ 'my-server': {
1622
+ command: 'npm start',
1623
+ },
1624
+ },
1625
+ unrecognizedTopLevel: {
1626
+ value: 'should be preserved',
1627
+ },
1628
+ };
1629
+ const v1Settings = migrateSettingsToV1(v2Settings);
1630
+ expect(v1Settings).toEqual({
1631
+ preferredEditor: 'code',
1632
+ vimMode: true,
1633
+ theme: 'dark',
1634
+ usageStatisticsEnabled: false,
1635
+ model: 'gemini-pro',
1636
+ chatCompression: {
1637
+ contextPercentageThreshold: 0.8,
1638
+ },
1639
+ contextFileName: 'CONTEXT.md',
1640
+ includeDirectories: ['/src'],
1641
+ sandbox: true,
1642
+ excludeTools: ['toolA'],
1643
+ allowMCPServers: ['server1'],
1644
+ folderTrust: true,
1645
+ dnsResolutionOrder: 'ipv4first',
1646
+ excludedProjectEnvVars: ['SECRET'],
1647
+ mcpServers: {
1648
+ 'my-server': {
1649
+ command: 'npm start',
1650
+ },
1651
+ },
1652
+ unrecognizedTopLevel: {
1653
+ value: 'should be preserved',
1654
+ },
1655
+ });
1656
+ });
1657
+ it('should handle partial v2 settings', () => {
1658
+ const v2Settings = {
1659
+ general: {
1660
+ vimMode: false,
1661
+ },
1662
+ ui: {},
1663
+ model: {
1664
+ name: 'gemini-1.5-pro',
1665
+ },
1666
+ unrecognized: 'value',
1667
+ };
1668
+ const v1Settings = migrateSettingsToV1(v2Settings);
1669
+ expect(v1Settings).toEqual({
1670
+ vimMode: false,
1671
+ model: 'gemini-1.5-pro',
1672
+ unrecognized: 'value',
1673
+ });
1674
+ });
1675
+ it('should handle settings with different data types', () => {
1676
+ const v2Settings = {
1677
+ general: {
1678
+ vimMode: false,
1679
+ },
1680
+ model: {
1681
+ maxSessionTurns: -1,
1682
+ },
1683
+ context: {
1684
+ includeDirectories: [],
1685
+ },
1686
+ security: {
1687
+ folderTrust: {
1688
+ enabled: undefined,
1689
+ },
1690
+ },
1691
+ };
1692
+ const v1Settings = migrateSettingsToV1(v2Settings);
1693
+ expect(v1Settings).toEqual({
1694
+ vimMode: false,
1695
+ maxSessionTurns: -1,
1696
+ includeDirectories: [],
1697
+ security: {
1698
+ folderTrust: {
1699
+ enabled: undefined,
1700
+ },
1701
+ },
1702
+ });
1703
+ });
1704
+ it('should preserve unrecognized top-level keys', () => {
1705
+ const v2Settings = {
1706
+ general: {
1707
+ vimMode: true,
1708
+ },
1709
+ customTopLevel: {
1710
+ a: 1,
1711
+ b: [2],
1712
+ },
1713
+ anotherOne: 'hello',
1714
+ };
1715
+ const v1Settings = migrateSettingsToV1(v2Settings);
1716
+ expect(v1Settings).toEqual({
1717
+ vimMode: true,
1718
+ customTopLevel: {
1719
+ a: 1,
1720
+ b: [2],
1721
+ },
1722
+ anotherOne: 'hello',
1723
+ });
1724
+ });
1725
+ it('should handle an empty v2 settings object', () => {
1726
+ const v2Settings = {};
1727
+ const v1Settings = migrateSettingsToV1(v2Settings);
1728
+ expect(v1Settings).toEqual({});
1729
+ });
1730
+ it('should correctly handle mcpServers at the top level', () => {
1731
+ const v2Settings = {
1732
+ mcpServers: {
1733
+ serverA: { command: 'a' },
1734
+ },
1735
+ mcp: {
1736
+ allowed: ['serverA'],
1737
+ },
1738
+ };
1739
+ const v1Settings = migrateSettingsToV1(v2Settings);
1740
+ expect(v1Settings).toEqual({
1741
+ mcpServers: {
1742
+ serverA: { command: 'a' },
1743
+ },
1744
+ allowMCPServers: ['serverA'],
1745
+ });
1746
+ });
1747
+ it('should correctly migrate customWittyPhrases', () => {
1748
+ const v2Settings = {
1749
+ ui: {
1750
+ customWittyPhrases: ['test phrase'],
1751
+ },
1752
+ };
1753
+ const v1Settings = migrateSettingsToV1(v2Settings);
1754
+ expect(v1Settings).toEqual({
1755
+ customWittyPhrases: ['test phrase'],
1756
+ });
1757
+ });
1758
+ });
1759
+ describe('loadEnvironment', () => {
1760
+ function setup({ isFolderTrustEnabled = true, isWorkspaceTrustedValue = true, }) {
1761
+ delete process.env['TESTTEST']; // reset
1762
+ const geminiEnvPath = path.resolve(path.join(GEMINI_DIR, '.env'));
1763
+ vi.mocked(isWorkspaceTrusted).mockReturnValue({
1764
+ isTrusted: isWorkspaceTrustedValue,
1765
+ source: 'file',
1766
+ });
1767
+ mockFsExistsSync.mockImplementation((p) => [USER_SETTINGS_PATH, geminiEnvPath].includes(p.toString()));
1768
+ const userSettingsContent = {
1769
+ ui: {
1770
+ theme: 'dark',
1771
+ },
1772
+ security: {
1773
+ folderTrust: {
1774
+ enabled: isFolderTrustEnabled,
1775
+ },
1776
+ },
1777
+ context: {
1778
+ fileName: 'USER_CONTEXT.md',
1779
+ },
1780
+ };
1781
+ fs.readFileSync.mockImplementation((p) => {
1782
+ if (p === USER_SETTINGS_PATH)
1783
+ return JSON.stringify(userSettingsContent);
1784
+ if (p === geminiEnvPath)
1785
+ return 'TESTTEST=1234';
1786
+ return '{}';
1787
+ });
1788
+ }
1789
+ it('sets environment variables from .env files', () => {
1790
+ setup({ isFolderTrustEnabled: false, isWorkspaceTrustedValue: true });
1791
+ loadEnvironment(loadSettings(MOCK_WORKSPACE_DIR).merged);
1792
+ expect(process.env['TESTTEST']).toEqual('1234');
1793
+ });
1794
+ it('does not load env files from untrusted spaces', () => {
1795
+ setup({ isFolderTrustEnabled: true, isWorkspaceTrustedValue: false });
1796
+ loadEnvironment(loadSettings(MOCK_WORKSPACE_DIR).merged);
1797
+ expect(process.env['TESTTEST']).not.toEqual('1234');
1798
+ });
1799
+ });
1800
+ describe('needsMigration', () => {
1801
+ it('should return false for an empty object', () => {
1802
+ expect(needsMigration({})).toBe(false);
1803
+ });
1804
+ it('should return false for settings that are already in V2 format', () => {
1805
+ const v2Settings = {
1806
+ ui: {
1807
+ theme: 'dark',
1808
+ },
1809
+ tools: {
1810
+ sandbox: true,
1811
+ },
1812
+ };
1813
+ expect(needsMigration(v2Settings)).toBe(false);
1814
+ });
1815
+ it('should return true for settings with a V1 key that needs to be moved', () => {
1816
+ const v1Settings = {
1817
+ theme: 'dark', // v1 key
1818
+ };
1819
+ expect(needsMigration(v1Settings)).toBe(true);
1820
+ });
1821
+ it('should return true for settings with a mix of V1 and V2 keys', () => {
1822
+ const mixedSettings = {
1823
+ theme: 'dark', // v1 key
1824
+ tools: {
1825
+ sandbox: true, // v2 key
1826
+ },
1827
+ };
1828
+ expect(needsMigration(mixedSettings)).toBe(true);
1829
+ });
1830
+ it('should return false for settings with only V1 keys that are the same in V2', () => {
1831
+ const v1Settings = {
1832
+ mcpServers: {},
1833
+ telemetry: {},
1834
+ extensions: [],
1835
+ };
1836
+ expect(needsMigration(v1Settings)).toBe(false);
1837
+ });
1838
+ it('should return true for settings with a mix of V1 keys that are the same in V2 and V1 keys that need moving', () => {
1839
+ const v1Settings = {
1840
+ mcpServers: {}, // same in v2
1841
+ theme: 'dark', // needs moving
1842
+ };
1843
+ expect(needsMigration(v1Settings)).toBe(true);
1844
+ });
1845
+ it('should return false for settings with unrecognized keys', () => {
1846
+ const settings = {
1847
+ someUnrecognizedKey: 'value',
1848
+ };
1849
+ expect(needsMigration(settings)).toBe(false);
1850
+ });
1851
+ it('should return false for settings with v2 keys and unrecognized keys', () => {
1852
+ const settings = {
1853
+ ui: { theme: 'dark' },
1854
+ someUnrecognizedKey: 'value',
1855
+ };
1856
+ expect(needsMigration(settings)).toBe(false);
1857
+ });
1858
+ });
1859
+ describe('migrateDeprecatedSettings', () => {
1860
+ let mockFsExistsSync;
1861
+ let mockFsReadFileSync;
1862
+ let mockDisableExtension;
1863
+ beforeEach(() => {
1864
+ vi.resetAllMocks();
1865
+ mockFsExistsSync = vi.mocked(fs.existsSync);
1866
+ mockFsReadFileSync = vi.mocked(fs.readFileSync);
1867
+ mockDisableExtension = vi.mocked(disableExtension);
1868
+ vi.mocked(ExtensionStorage.getUserExtensionsDir).mockReturnValue(new Storage(osActual.homedir()).getExtensionsDir());
1869
+ mockFsExistsSync.mockReturnValue(true);
1870
+ vi.mocked(isWorkspaceTrusted).mockReturnValue({
1871
+ isTrusted: true,
1872
+ source: undefined,
1873
+ });
1874
+ });
1875
+ afterEach(() => {
1876
+ vi.restoreAllMocks();
1877
+ });
1878
+ it('should migrate disabled extensions from user and workspace settings', () => {
1879
+ const userSettingsContent = {
1880
+ extensions: {
1881
+ disabled: ['user-ext-1', 'shared-ext'],
1882
+ },
1883
+ };
1884
+ const workspaceSettingsContent = {
1885
+ extensions: {
1886
+ disabled: ['workspace-ext-1', 'shared-ext'],
1887
+ },
1888
+ };
1889
+ mockFsReadFileSync.mockImplementation((p) => {
1890
+ if (p === USER_SETTINGS_PATH)
1891
+ return JSON.stringify(userSettingsContent);
1892
+ if (p === MOCK_WORKSPACE_SETTINGS_PATH)
1893
+ return JSON.stringify(workspaceSettingsContent);
1894
+ return '{}';
1895
+ });
1896
+ const loadedSettings = loadSettings(MOCK_WORKSPACE_DIR);
1897
+ const setValueSpy = vi.spyOn(loadedSettings, 'setValue');
1898
+ migrateDeprecatedSettings(loadedSettings, MOCK_WORKSPACE_DIR);
1899
+ // Check user settings migration
1900
+ expect(mockDisableExtension).toHaveBeenCalledWith('user-ext-1', SettingScope.User, expect.any(ExtensionEnablementManager), MOCK_WORKSPACE_DIR);
1901
+ expect(mockDisableExtension).toHaveBeenCalledWith('shared-ext', SettingScope.User, expect.any(ExtensionEnablementManager), MOCK_WORKSPACE_DIR);
1902
+ // Check workspace settings migration
1903
+ expect(mockDisableExtension).toHaveBeenCalledWith('workspace-ext-1', SettingScope.Workspace, expect.any(ExtensionEnablementManager), MOCK_WORKSPACE_DIR);
1904
+ expect(mockDisableExtension).toHaveBeenCalledWith('shared-ext', SettingScope.Workspace, expect.any(ExtensionEnablementManager), MOCK_WORKSPACE_DIR);
1905
+ // Check that setValue was called to remove the deprecated setting
1906
+ expect(setValueSpy).toHaveBeenCalledWith(SettingScope.User, 'extensions', {
1907
+ disabled: undefined,
1908
+ });
1909
+ expect(setValueSpy).toHaveBeenCalledWith(SettingScope.Workspace, 'extensions', {
1910
+ disabled: undefined,
1911
+ });
1912
+ });
1913
+ it('should not do anything if there are no deprecated settings', () => {
1914
+ const userSettingsContent = {
1915
+ extensions: {
1916
+ enabled: ['user-ext-1'],
1917
+ },
1918
+ };
1919
+ const workspaceSettingsContent = {
1920
+ someOtherSetting: 'value',
1921
+ };
1922
+ mockFsReadFileSync.mockImplementation((p) => {
1923
+ if (p === USER_SETTINGS_PATH)
1924
+ return JSON.stringify(userSettingsContent);
1925
+ if (p === MOCK_WORKSPACE_SETTINGS_PATH)
1926
+ return JSON.stringify(workspaceSettingsContent);
1927
+ return '{}';
1928
+ });
1929
+ const loadedSettings = loadSettings(MOCK_WORKSPACE_DIR);
1930
+ const setValueSpy = vi.spyOn(loadedSettings, 'setValue');
1931
+ migrateDeprecatedSettings(loadedSettings, MOCK_WORKSPACE_DIR);
1932
+ expect(mockDisableExtension).not.toHaveBeenCalled();
1933
+ expect(setValueSpy).not.toHaveBeenCalled();
1934
+ });
1935
+ });
1936
+ });
1937
+ //# sourceMappingURL=settings.test.js.map