@@ -245,6 +245,112 @@ describe("isToolAllowedForMode", () => {
245245 expect ( isToolAllowedForMode ( "browser_action" , "architect" , [ ] ) ) . toBe ( true )
246246 expect ( isToolAllowedForMode ( "use_mcp_tool" , "architect" , [ ] ) ) . toBe ( true )
247247 } )
248+
249+ it ( "applies restrictions to all edit tools including search_and_replace and insert_content" , ( ) => {
250+ // Test search_and_replace with matching file
251+ expect (
252+ isToolAllowedForMode ( "search_and_replace" , "architect" , [ ] , undefined , {
253+ path : "test.md" ,
254+ search : "old text" ,
255+ replace : "new text" ,
256+ } ) ,
257+ ) . toBe ( true )
258+
259+ // Test insert_content with matching file
260+ expect (
261+ isToolAllowedForMode ( "insert_content" , "architect" , [ ] , undefined , {
262+ path : "test.md" ,
263+ line : "1" ,
264+ content : "# New content" ,
265+ } ) ,
266+ ) . toBe ( true )
267+
268+ // Test search_and_replace with non-matching file - should throw error
269+ expect ( ( ) =>
270+ isToolAllowedForMode ( "search_and_replace" , "architect" , [ ] , undefined , {
271+ path : "test.py" ,
272+ search : "old text" ,
273+ replace : "new text" ,
274+ } ) ,
275+ ) . toThrow ( FileRestrictionError )
276+ expect ( ( ) =>
277+ isToolAllowedForMode ( "search_and_replace" , "architect" , [ ] , undefined , {
278+ path : "test.py" ,
279+ search : "old text" ,
280+ replace : "new text" ,
281+ } ) ,
282+ ) . toThrow ( / M a r k d o w n f i l e s o n l y / )
283+
284+ // Test insert_content with non-matching file - should throw error
285+ expect ( ( ) =>
286+ isToolAllowedForMode ( "insert_content" , "architect" , [ ] , undefined , {
287+ path : "test.py" ,
288+ line : "1" ,
289+ content : "print('hello')" ,
290+ } ) ,
291+ ) . toThrow ( FileRestrictionError )
292+ expect ( ( ) =>
293+ isToolAllowedForMode ( "insert_content" , "architect" , [ ] , undefined , {
294+ path : "test.py" ,
295+ line : "1" ,
296+ content : "print('hello')" ,
297+ } ) ,
298+ ) . toThrow ( / M a r k d o w n f i l e s o n l y / )
299+ } )
300+
301+ it ( "applies restrictions to apply_diff with concurrent file edits (MULTI_FILE_APPLY_DIFF experiment)" , ( ) => {
302+ // Test apply_diff with args parameter (used when MULTI_FILE_APPLY_DIFF experiment is enabled)
303+ // This simulates concurrent/batch file editing
304+ const xmlArgs =
305+ "<args><file><path>test.md</path><diff><content>- old content\\n+ new content</content></diff></file></args>"
306+
307+ // Should allow markdown files in architect mode
308+ expect (
309+ isToolAllowedForMode ( "apply_diff" , "architect" , [ ] , undefined , {
310+ args : xmlArgs ,
311+ } ) ,
312+ ) . toBe ( true )
313+
314+ // Test with non-markdown file - should throw error
315+ const xmlArgsNonMd =
316+ "<args><file><path>test.py</path><diff><content>- old content\\n+ new content</content></diff></file></args>"
317+
318+ expect ( ( ) =>
319+ isToolAllowedForMode ( "apply_diff" , "architect" , [ ] , undefined , {
320+ args : xmlArgsNonMd ,
321+ } ) ,
322+ ) . toThrow ( FileRestrictionError )
323+ expect ( ( ) =>
324+ isToolAllowedForMode ( "apply_diff" , "architect" , [ ] , undefined , {
325+ args : xmlArgsNonMd ,
326+ } ) ,
327+ ) . toThrow ( / M a r k d o w n f i l e s o n l y / )
328+
329+ // Test with multiple files - should allow only markdown files
330+ const xmlArgsMultiple =
331+ "<args><file><path>readme.md</path><diff><content>- old content\\n+ new content</content></diff></file><file><path>docs.md</path><diff><content>- old content\\n+ new content</content></diff></file></args>"
332+
333+ expect (
334+ isToolAllowedForMode ( "apply_diff" , "architect" , [ ] , undefined , {
335+ args : xmlArgsMultiple ,
336+ } ) ,
337+ ) . toBe ( true )
338+
339+ // Test with mixed file types - should throw error for non-markdown
340+ const xmlArgsMixed =
341+ "<args><file><path>readme.md</path><diff><content>- old content\\n+ new content</content></diff></file><file><path>script.py</path><diff><content>- old content\\n+ new content</content></diff></file></args>"
342+
343+ expect ( ( ) =>
344+ isToolAllowedForMode ( "apply_diff" , "architect" , [ ] , undefined , {
345+ args : xmlArgsMixed ,
346+ } ) ,
347+ ) . toThrow ( FileRestrictionError )
348+ expect ( ( ) =>
349+ isToolAllowedForMode ( "apply_diff" , "architect" , [ ] , undefined , {
350+ args : xmlArgsMixed ,
351+ } ) ,
352+ ) . toThrow ( / M a r k d o w n f i l e s o n l y / )
353+ } )
248354 } )
249355
250356 it ( "handles non-existent modes" , ( ) => {
@@ -269,6 +375,14 @@ describe("FileRestrictionError", () => {
269375 expect ( error . name ) . toBe ( "FileRestrictionError" )
270376 } )
271377
378+ it ( "formats error message with tool name when provided" , ( ) => {
379+ const error = new FileRestrictionError ( "Markdown Editor" , "\\.md$" , undefined , "test.js" , "write_to_file" )
380+ expect ( error . message ) . toBe (
381+ "Tool 'write_to_file' in mode 'Markdown Editor' can only edit files matching pattern: \\.md$. Got: test.js" ,
382+ )
383+ expect ( error . name ) . toBe ( "FileRestrictionError" )
384+ } )
385+
272386 describe ( "debug mode" , ( ) => {
273387 it ( "is configured correctly" , ( ) => {
274388 const debugMode = modes . find ( ( mode ) => mode . slug === "debug" )
@@ -368,6 +482,20 @@ describe("FileRestrictionError", () => {
368482 )
369483 expect ( error . name ) . toBe ( "FileRestrictionError" )
370484 } )
485+
486+ it ( "formats error message with both tool name and description when provided" , ( ) => {
487+ const error = new FileRestrictionError (
488+ "Markdown Editor" ,
489+ "\\.md$" ,
490+ "Markdown files only" ,
491+ "test.js" ,
492+ "apply_diff" ,
493+ )
494+ expect ( error . message ) . toBe (
495+ "Tool 'apply_diff' in mode 'Markdown Editor' can only edit files matching pattern: \\.md$ (Markdown files only). Got: test.js" ,
496+ )
497+ expect ( error . name ) . toBe ( "FileRestrictionError" )
498+ } )
371499} )
372500
373501describe ( "getModeSelection" , ( ) => {
0 commit comments