@@ -11,12 +11,99 @@ import {
1111 getPermissionScopesRequiringAsk ,
1212 hasUserPermissionReplies ,
1313 isPathInAnyDirectory ,
14+ normalizeAskPermissions ,
1415 parseBashSideEffects ,
1516} from "../common/permissions" ;
1617import type { PermissionScope , PermissionSettings } from "../settings" ;
18+ import { buildFileDiff } from "../common/file-utils" ;
1719
1820const tempDirs : string [ ] = [ ] ;
1921
22+ test ( "permission previews cover write and edit without mutating files" , ( ) => {
23+ const projectRoot = createTempDir ( "deepcode-permission-diff-" ) ;
24+ const existing = path . join ( projectRoot , "existing.txt" ) ;
25+ const created = path . join ( projectRoot , "new" , "file.txt" ) ;
26+ fs . writeFileSync ( existing , "\ufeffbefore\r\n" , "utf16le" ) ;
27+ const inputs = [
28+ {
29+ name : "write" ,
30+ args : { file_path : existing , content : "after\r\n" } ,
31+ old : "\ufeffbefore\n" ,
32+ next : "after\n" ,
33+ filePath : existing ,
34+ } ,
35+ { name : "Write" , args : { file_path : created , content : "new\n" } , old : null , next : "new\n" , filePath : created } ,
36+ {
37+ name : "write" ,
38+ args : { file_path : path . join ( projectRoot , "data.json" ) , content : { a : 1 } } ,
39+ old : null ,
40+ next : '{\n "a": 1\n}' ,
41+ filePath : path . join ( projectRoot , "data.json" ) ,
42+ } ,
43+ ...[
44+ [ "old" , "new" ] ,
45+ [ "" , "inserted" ] ,
46+ [ "deleted" , "" ] ,
47+ ] . map ( ( [ old , next ] ) => ( {
48+ name : "edit" ,
49+ args : { snippet_id : "snippet" , old_string : old , new_string : next , replace_all : true } ,
50+ old,
51+ next,
52+ filePath : existing ,
53+ } ) ) ,
54+ ] ;
55+ const snapshot = fs . readFileSync ( existing ) ;
56+ const plan = computeToolCallPermissions ( {
57+ sessionId : "diff-session" ,
58+ projectRoot,
59+ settings : { allow : [ ] , deny : [ ] , ask : [ ] , defaultMode : "askAll" } ,
60+ resolveSnippetPath : ( ) => existing ,
61+ toolCalls : inputs . map ( ( { name, args } , index ) => ( {
62+ id : String ( index ) ,
63+ type : "function" ,
64+ function : { name, arguments : JSON . stringify ( args ) } ,
65+ } ) ) ,
66+ } ) ;
67+ assert . equal ( plan . askPermissions . length , inputs . length ) ;
68+ inputs . forEach ( ( { filePath, old, next } , index ) => {
69+ assert . deepEqual ( plan . askPermissions [ index ] . diff , {
70+ filePath,
71+ details : buildFileDiff ( filePath , old , next ) . details ,
72+ } ) ;
73+ } ) ;
74+ assert . deepEqual ( fs . readFileSync ( existing ) , snapshot ) ;
75+ assert . equal ( fs . existsSync ( path . dirname ( created ) ) , false ) ;
76+ assert . equal ( fs . existsSync ( path . join ( projectRoot , "data.json" ) ) , false ) ;
77+ assert . deepEqual ( normalizeAskPermissions ( JSON . parse ( JSON . stringify ( plan . askPermissions ) ) ) , plan . askPermissions ) ;
78+ } ) ;
79+
80+ test ( "missing permission previews do not affect approval decisions" , ( ) => {
81+ const projectRoot = createTempDir ( "deepcode-permission-diff-fallback-" ) ;
82+ const toolCalls = [
83+ { name : "write" , args : { file_path : projectRoot , content : "directory cannot be read" } } ,
84+ { name : "write" , args : { file_path : path . join ( projectRoot , "file" ) , content : 123 } } ,
85+ { name : "edit" , args : { file_path : path . join ( projectRoot , "file" ) , old_string : "same" , new_string : "same" } } ,
86+ { name : "edit" , args : { file_path : path . join ( projectRoot , "file" ) , new_string : "missing old_string" } } ,
87+ { name : "bash" , args : { command : "echo hello" , sideEffects : [ "network" ] } } ,
88+ ] . map ( ( { name, args } , index ) => ( {
89+ id : String ( index ) ,
90+ type : "function" ,
91+ function : { name, arguments : JSON . stringify ( args ) } ,
92+ } ) ) ;
93+ const options = { sessionId : "diff-session" , projectRoot, toolCalls } ;
94+ const plan = computeToolCallPermissions ( {
95+ ...options ,
96+ settings : { allow : [ ] , deny : [ ] , ask : [ ] , defaultMode : "askAll" } ,
97+ } ) ;
98+ assert . equal ( plan . askPermissions . length , toolCalls . length ) ;
99+ assert . ok ( plan . askPermissions . every ( ( request ) => request . diff === undefined ) ) ;
100+ assert . equal ( computeToolCallPermissions ( options ) . askPermissions . length , 0 ) ;
101+ const legacy = { toolCallId : "old" , name : "write" , command : "write file" , scopes : [ ] } ;
102+ for ( const diff of [ undefined , null , { filePath : "file" , details : { lines : [ ] } } ] ) {
103+ assert . equal ( normalizeAskPermissions ( [ { ...legacy , diff } ] ) ?. [ 0 ] . diff , undefined ) ;
104+ }
105+ } ) ;
106+
20107afterEach ( ( ) => {
21108 while ( tempDirs . length > 0 ) {
22109 const dir = tempDirs . pop ( ) ;
0 commit comments