@@ -26,6 +26,11 @@ describe("before_tool_call hook context payload", () => {
2626 content : [ { type : "text" , text : "check this call" } ] ,
2727 timestamp : Date . now ( ) ,
2828 } ,
29+ {
30+ role : "user" ,
31+ content : [ { type : "text" , text : "followup" } ] ,
32+ timestamp : Date . now ( ) ,
33+ } ,
2934 ] ;
3035 const tools = [
3136 {
@@ -64,4 +69,79 @@ describe("before_tool_call hook context payload", () => {
6469 } ) ,
6570 ) ;
6671 } ) ;
72+
73+ it ( "clones and freezes params/messages/tools snapshots before dispatch" , async ( ) => {
74+ const runBeforeToolCall = vi . fn ( ) . mockResolvedValue ( undefined ) ;
75+ mockGetGlobalHookRunner . mockReturnValue ( {
76+ hasHooks : vi . fn ( ( hookName : string ) => hookName === "before_tool_call" ) ,
77+ runBeforeToolCall,
78+ // oxlint-disable-next-line typescript/no-explicit-any
79+ } as any ) ;
80+
81+ const params = { path : "/tmp/file" } ;
82+ const messages : AgentMessage [ ] = [
83+ {
84+ role : "user" ,
85+ content : [ { type : "text" , text : "check this call" } ] ,
86+ timestamp : Date . now ( ) ,
87+ } ,
88+ {
89+ role : "user" ,
90+ content : [ { type : "text" , text : "followup" } ] ,
91+ timestamp : Date . now ( ) ,
92+ } ,
93+ ] ;
94+ const tools = [
95+ {
96+ name : "read" ,
97+ description : "Read a file" ,
98+ parameters : { type : "object" , properties : { path : { type : "string" } } } ,
99+ } ,
100+ ] ;
101+
102+ await runBeforeToolCallHook ( {
103+ toolName : "read" ,
104+ params,
105+ ctx : {
106+ getMessages : ( ) => messages ,
107+ getTools : ( ) => tools ,
108+ } ,
109+ } ) ;
110+
111+ const [ event ] = runBeforeToolCall . mock . calls [ 0 ] as [
112+ {
113+ params : Record < string , unknown > ;
114+ messages ?: AgentMessage [ ] ;
115+ tools ?: Array < { parameters ?: { properties ?: { path ?: { type ?: string } } } } > ;
116+ } ,
117+ ] ;
118+
119+ expect ( event . params ) . not . toBe ( params ) ;
120+ expect ( event . messages ) . not . toBe ( messages ) ;
121+ expect ( event . tools ) . not . toBe ( tools ) ;
122+ expect ( Object . isFrozen ( event ) ) . toBe ( true ) ;
123+ expect ( Object . isFrozen ( event . params ) ) . toBe ( true ) ;
124+ expect ( Object . isFrozen ( event . messages ?? [ ] ) ) . toBe ( true ) ;
125+ expect ( Object . isFrozen ( event . messages ?. [ 0 ] ?? { } ) ) . toBe ( true ) ;
126+ expect ( Object . isFrozen ( event . tools ?? [ ] ) ) . toBe ( true ) ;
127+ expect ( Object . isFrozen ( event . tools ?. [ 0 ] ?? { } ) ) . toBe ( true ) ;
128+
129+ expect ( ( ) => {
130+ event . params . path = "/tmp/hacked" ;
131+ } ) . toThrow ( TypeError ) ;
132+ expect ( ( ) => {
133+ if ( event . messages ) {
134+ event . messages [ 0 ] = event . messages [ 1 ] ;
135+ }
136+ } ) . toThrow ( TypeError ) ;
137+ expect ( ( ) => {
138+ if ( event . tools ?. [ 0 ] ?. parameters ?. properties ?. path ) {
139+ event . tools [ 0 ] . parameters . properties . path . type = "number" ;
140+ }
141+ } ) . toThrow ( TypeError ) ;
142+
143+ expect ( params . path ) . toBe ( "/tmp/file" ) ;
144+ expect ( messages [ 0 ] ?. role ) . toBe ( "user" ) ;
145+ expect ( tools [ 0 ] ?. parameters ?. properties ?. path ?. type ) . toBe ( "string" ) ;
146+ } ) ;
67147} ) ;
0 commit comments