modes.test.ts 9.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348
  1. import { isToolAllowedForMode, FileRestrictionError, ModeConfig } from "../modes"
  2. describe("isToolAllowedForMode", () => {
  3. const customModes: ModeConfig[] = [
  4. {
  5. slug: "markdown-editor",
  6. name: "Markdown Editor",
  7. roleDefinition: "You are a markdown editor",
  8. groups: ["read", ["edit", { fileRegex: "\\.md$" }], "browser"],
  9. },
  10. {
  11. slug: "css-editor",
  12. name: "CSS Editor",
  13. roleDefinition: "You are a CSS editor",
  14. groups: ["read", ["edit", { fileRegex: "\\.css$" }], "browser"],
  15. },
  16. {
  17. slug: "test-exp-mode",
  18. name: "Test Exp Mode",
  19. roleDefinition: "You are an experimental tester",
  20. groups: ["read", "edit", "browser"],
  21. },
  22. ]
  23. it("allows always available tools", () => {
  24. expect(isToolAllowedForMode("ask_followup_question", "markdown-editor", customModes)).toBe(true)
  25. expect(isToolAllowedForMode("attempt_completion", "markdown-editor", customModes)).toBe(true)
  26. })
  27. it("allows unrestricted tools", () => {
  28. expect(isToolAllowedForMode("read_file", "markdown-editor", customModes)).toBe(true)
  29. expect(isToolAllowedForMode("browser_action", "markdown-editor", customModes)).toBe(true)
  30. })
  31. describe("file restrictions", () => {
  32. it("allows editing matching files", () => {
  33. // Test markdown editor mode
  34. const mdResult = isToolAllowedForMode("write_to_file", "markdown-editor", customModes, undefined, {
  35. path: "test.md",
  36. content: "# Test",
  37. })
  38. expect(mdResult).toBe(true)
  39. // Test CSS editor mode
  40. const cssResult = isToolAllowedForMode("write_to_file", "css-editor", customModes, undefined, {
  41. path: "styles.css",
  42. content: ".test { color: red; }",
  43. })
  44. expect(cssResult).toBe(true)
  45. })
  46. it("rejects editing non-matching files", () => {
  47. // Test markdown editor mode with non-markdown file
  48. expect(() =>
  49. isToolAllowedForMode("write_to_file", "markdown-editor", customModes, undefined, {
  50. path: "test.js",
  51. content: "console.log('test')",
  52. }),
  53. ).toThrow(FileRestrictionError)
  54. expect(() =>
  55. isToolAllowedForMode("write_to_file", "markdown-editor", customModes, undefined, {
  56. path: "test.js",
  57. content: "console.log('test')",
  58. }),
  59. ).toThrow(/\\.md\$/)
  60. // Test CSS editor mode with non-CSS file
  61. expect(() =>
  62. isToolAllowedForMode("write_to_file", "css-editor", customModes, undefined, {
  63. path: "test.js",
  64. content: "console.log('test')",
  65. }),
  66. ).toThrow(FileRestrictionError)
  67. expect(() =>
  68. isToolAllowedForMode("write_to_file", "css-editor", customModes, undefined, {
  69. path: "test.js",
  70. content: "console.log('test')",
  71. }),
  72. ).toThrow(/\\.css\$/)
  73. })
  74. it("handles partial streaming cases (path only, no content/diff)", () => {
  75. // Should allow path-only for matching files (no validation yet since content/diff not provided)
  76. expect(
  77. isToolAllowedForMode("write_to_file", "markdown-editor", customModes, undefined, {
  78. path: "test.js",
  79. }),
  80. ).toBe(true)
  81. expect(
  82. isToolAllowedForMode("apply_diff", "markdown-editor", customModes, undefined, {
  83. path: "test.js",
  84. }),
  85. ).toBe(true)
  86. // Should allow path-only for ask mode too
  87. expect(
  88. isToolAllowedForMode("write_to_file", "ask", [], undefined, {
  89. path: "test.js",
  90. }),
  91. ).toBe(true)
  92. })
  93. it("applies restrictions to both write_to_file and apply_diff", () => {
  94. // Test write_to_file
  95. const writeResult = isToolAllowedForMode("write_to_file", "markdown-editor", customModes, undefined, {
  96. path: "test.md",
  97. content: "# Test",
  98. })
  99. expect(writeResult).toBe(true)
  100. // Test apply_diff
  101. const diffResult = isToolAllowedForMode("apply_diff", "markdown-editor", customModes, undefined, {
  102. path: "test.md",
  103. diff: "- old\n+ new",
  104. })
  105. expect(diffResult).toBe(true)
  106. // Test both with non-matching file
  107. expect(() =>
  108. isToolAllowedForMode("write_to_file", "markdown-editor", customModes, undefined, {
  109. path: "test.js",
  110. content: "console.log('test')",
  111. }),
  112. ).toThrow(FileRestrictionError)
  113. expect(() =>
  114. isToolAllowedForMode("apply_diff", "markdown-editor", customModes, undefined, {
  115. path: "test.js",
  116. diff: "- old\n+ new",
  117. }),
  118. ).toThrow(FileRestrictionError)
  119. })
  120. it("uses description in file restriction error for custom modes", () => {
  121. const customModesWithDescription: ModeConfig[] = [
  122. {
  123. slug: "docs-editor",
  124. name: "Documentation Editor",
  125. roleDefinition: "You are a documentation editor",
  126. groups: [
  127. "read",
  128. ["edit", { fileRegex: "\\.(md|txt)$", description: "Documentation files only" }],
  129. "browser",
  130. ],
  131. },
  132. ]
  133. // Test write_to_file with non-matching file
  134. expect(() =>
  135. isToolAllowedForMode("write_to_file", "docs-editor", customModesWithDescription, undefined, {
  136. path: "test.js",
  137. content: "console.log('test')",
  138. }),
  139. ).toThrow(FileRestrictionError)
  140. expect(() =>
  141. isToolAllowedForMode("write_to_file", "docs-editor", customModesWithDescription, undefined, {
  142. path: "test.js",
  143. content: "console.log('test')",
  144. }),
  145. ).toThrow(/Documentation files only/)
  146. // Test apply_diff with non-matching file
  147. expect(() =>
  148. isToolAllowedForMode("apply_diff", "docs-editor", customModesWithDescription, undefined, {
  149. path: "test.js",
  150. diff: "- old\n+ new",
  151. }),
  152. ).toThrow(FileRestrictionError)
  153. expect(() =>
  154. isToolAllowedForMode("apply_diff", "docs-editor", customModesWithDescription, undefined, {
  155. path: "test.js",
  156. diff: "- old\n+ new",
  157. }),
  158. ).toThrow(/Documentation files only/)
  159. // Test that matching files are allowed
  160. expect(
  161. isToolAllowedForMode("write_to_file", "docs-editor", customModesWithDescription, undefined, {
  162. path: "test.md",
  163. content: "# Test",
  164. }),
  165. ).toBe(true)
  166. expect(
  167. isToolAllowedForMode("write_to_file", "docs-editor", customModesWithDescription, undefined, {
  168. path: "test.txt",
  169. content: "Test content",
  170. }),
  171. ).toBe(true)
  172. // Test partial streaming cases
  173. expect(
  174. isToolAllowedForMode("write_to_file", "docs-editor", customModesWithDescription, undefined, {
  175. path: "test.js",
  176. }),
  177. ).toBe(true)
  178. })
  179. it("allows ask mode to edit markdown files only", () => {
  180. // Should allow editing markdown files
  181. expect(
  182. isToolAllowedForMode("write_to_file", "ask", [], undefined, {
  183. path: "test.md",
  184. content: "# Test",
  185. }),
  186. ).toBe(true)
  187. // Should allow applying diffs to markdown files
  188. expect(
  189. isToolAllowedForMode("apply_diff", "ask", [], undefined, {
  190. path: "readme.md",
  191. diff: "- old\n+ new",
  192. }),
  193. ).toBe(true)
  194. // Should reject non-markdown files
  195. expect(() =>
  196. isToolAllowedForMode("write_to_file", "ask", [], undefined, {
  197. path: "test.js",
  198. content: "console.log('test')",
  199. }),
  200. ).toThrow(FileRestrictionError)
  201. expect(() =>
  202. isToolAllowedForMode("write_to_file", "ask", [], undefined, {
  203. path: "test.js",
  204. content: "console.log('test')",
  205. }),
  206. ).toThrow(/Markdown files only/)
  207. // Should maintain read capabilities
  208. expect(isToolAllowedForMode("read_file", "ask", [])).toBe(true)
  209. expect(isToolAllowedForMode("browser_action", "ask", [])).toBe(true)
  210. expect(isToolAllowedForMode("use_mcp_tool", "ask", [])).toBe(true)
  211. })
  212. })
  213. it("handles non-existent modes", () => {
  214. expect(isToolAllowedForMode("write_to_file", "non-existent", customModes)).toBe(false)
  215. })
  216. it("respects tool requirements", () => {
  217. const toolRequirements = {
  218. write_to_file: false,
  219. }
  220. expect(isToolAllowedForMode("write_to_file", "markdown-editor", customModes, toolRequirements)).toBe(false)
  221. })
  222. describe("experimental tools", () => {
  223. it("disables tools when experiment is disabled", () => {
  224. const experiments = {
  225. search_and_replace: false,
  226. insert_code_block: false,
  227. }
  228. expect(
  229. isToolAllowedForMode(
  230. "search_and_replace",
  231. "test-exp-mode",
  232. customModes,
  233. undefined,
  234. undefined,
  235. experiments,
  236. ),
  237. ).toBe(false)
  238. expect(
  239. isToolAllowedForMode(
  240. "insert_code_block",
  241. "test-exp-mode",
  242. customModes,
  243. undefined,
  244. undefined,
  245. experiments,
  246. ),
  247. ).toBe(false)
  248. })
  249. it("allows tools when experiment is enabled", () => {
  250. const experiments = {
  251. search_and_replace: true,
  252. insert_code_block: true,
  253. }
  254. expect(
  255. isToolAllowedForMode(
  256. "search_and_replace",
  257. "test-exp-mode",
  258. customModes,
  259. undefined,
  260. undefined,
  261. experiments,
  262. ),
  263. ).toBe(true)
  264. expect(
  265. isToolAllowedForMode(
  266. "insert_code_block",
  267. "test-exp-mode",
  268. customModes,
  269. undefined,
  270. undefined,
  271. experiments,
  272. ),
  273. ).toBe(true)
  274. })
  275. it("allows non-experimental tools when experiments are disabled", () => {
  276. const experiments = {
  277. search_and_replace: false,
  278. insert_code_block: false,
  279. }
  280. expect(
  281. isToolAllowedForMode("read_file", "markdown-editor", customModes, undefined, undefined, experiments),
  282. ).toBe(true)
  283. expect(
  284. isToolAllowedForMode(
  285. "write_to_file",
  286. "markdown-editor",
  287. customModes,
  288. undefined,
  289. { path: "test.md" },
  290. experiments,
  291. ),
  292. ).toBe(true)
  293. })
  294. })
  295. })
  296. describe("FileRestrictionError", () => {
  297. it("formats error message with pattern when no description provided", () => {
  298. const error = new FileRestrictionError("Markdown Editor", "\\.md$", undefined, "test.js")
  299. expect(error.message).toBe(
  300. "This mode (Markdown Editor) can only edit files matching pattern: \\.md$. Got: test.js",
  301. )
  302. expect(error.name).toBe("FileRestrictionError")
  303. })
  304. it("formats error message with description when provided", () => {
  305. const error = new FileRestrictionError("Markdown Editor", "\\.md$", "Markdown files only", "test.js")
  306. expect(error.message).toBe(
  307. "This mode (Markdown Editor) can only edit files matching pattern: \\.md$ (Markdown files only). Got: test.js",
  308. )
  309. expect(error.name).toBe("FileRestrictionError")
  310. })
  311. })