@@ -118,6 +118,202 @@ void testBuilderChaining() {
118118 }
119119 }
120120
121+ @ Nested
122+ @ DisplayName ("Copy Builder pattern" )
123+ class CopyBuilderTests {
124+
125+ @ Test
126+ @ DisplayName ("Should copy all fields from source" )
127+ void testCopyAllFields () {
128+ ToolUseBlock toolUseBlock =
129+ ToolUseBlock .builder ()
130+ .id ("test-id" )
131+ .name ("test_tool" )
132+ .input (Map .of ("key" , "value" ))
133+ .build ();
134+
135+ Agent mockAgent = mock (Agent .class );
136+ when (mockAgent .getName ()).thenReturn ("TestAgent" );
137+
138+ ToolExecutionContext context =
139+ ToolExecutionContext .builder ().register ("testContext" ).build ();
140+
141+ List <ToolResultBlock > emittedChunks = new ArrayList <>();
142+ ToolEmitter emitter = emittedChunks ::add ;
143+
144+ Map <String , Object > input = new HashMap <>();
145+ input .put ("param1" , "value1" );
146+
147+ ToolCallParam original =
148+ ToolCallParam .builder ()
149+ .toolUseBlock (toolUseBlock )
150+ .input (input )
151+ .agent (mockAgent )
152+ .context (context )
153+ .emitter (emitter )
154+ .build ();
155+
156+ // Create copy
157+ ToolCallParam copy = ToolCallParam .builder (original ).build ();
158+
159+ assertNotNull (copy );
160+ assertEquals (original .getToolUseBlock (), copy .getToolUseBlock ());
161+ assertEquals (original .getInput (), copy .getInput ());
162+ assertEquals (original .getAgent (), copy .getAgent ());
163+ assertEquals (original .getContext (), copy .getContext ());
164+ assertSame (original .getEmitter (), copy .getEmitter ());
165+ }
166+
167+ @ Test
168+ @ DisplayName ("Should allow modifying copied fields" )
169+ void testModifyCopiedFields () {
170+ ToolUseBlock toolUseBlock =
171+ ToolUseBlock .builder ()
172+ .id ("test-id" )
173+ .name ("test_tool" )
174+ .input (Map .of ("key" , "value" ))
175+ .build ();
176+
177+ Map <String , Object > input = new HashMap <>();
178+ input .put ("param1" , "value1" );
179+
180+ ToolCallParam original =
181+ ToolCallParam .builder ().toolUseBlock (toolUseBlock ).input (input ).build ();
182+
183+ // Create copy with modified input
184+ Map <String , Object > newInput = new HashMap <>();
185+ newInput .put ("param2" , "value2" );
186+ ToolCallParam modified = ToolCallParam .builder (original ).input (newInput ).build ();
187+
188+ // Original should be unchanged
189+ assertEquals ("value1" , original .getInput ().get ("param1" ));
190+ assertNull (original .getInput ().get ("param2" ));
191+
192+ // Modified should have new value
193+ assertEquals ("value2" , modified .getInput ().get ("param2" ));
194+ assertNull (modified .getInput ().get ("param1" ));
195+ }
196+
197+ @ Test
198+ @ DisplayName ("Should shallow copy input map (entries independent, nested values shared)" )
199+ @ SuppressWarnings ("unchecked" )
200+ void testShallowCopyInputMap () {
201+ ToolUseBlock toolUseBlock =
202+ ToolUseBlock .builder ().id ("id" ).name ("tool" ).input (Map .of ()).build ();
203+
204+ // Use a nested mutable value to verify shallow copy semantics
205+ List <String > nestedList = new ArrayList <>(List .of ("a" , "b" ));
206+ Map <String , Object > input = new HashMap <>();
207+ input .put ("list" , nestedList );
208+
209+ ToolCallParam original =
210+ ToolCallParam .builder ().toolUseBlock (toolUseBlock ).input (input ).build ();
211+
212+ // Create copy (no input override)
213+ ToolCallParam copy = ToolCallParam .builder (original ).build ();
214+
215+ // Input maps should be equal
216+ assertEquals (original .getInput (), copy .getInput ());
217+
218+ // Nested mutable value should be the SAME reference (shallow copy)
219+ assertSame (
220+ original .getInput ().get ("list" ),
221+ copy .getInput ().get ("list" ),
222+ "Nested values should be shared (shallow copy)" );
223+
224+ // But top-level entries should be independent:
225+ // adding a new top-level key to copy's input should NOT affect original
226+ // (because ToolCallParam constructor does new HashMap<>(builder.input))
227+ Map <String , Object > modifiedInput = new HashMap <>(copy .getInput ());
228+ modifiedInput .put ("newKey" , "newValue" );
229+ ToolCallParam modified = ToolCallParam .builder (original ).input (modifiedInput ).build ();
230+
231+ assertNull (
232+ original .getInput ().get ("newKey" ),
233+ "Original should not have the new key added to the modified copy" );
234+ assertEquals ("newValue" , modified .getInput ().get ("newKey" ));
235+ }
236+
237+ @ Test
238+ @ DisplayName ("Should copy with null fields" )
239+ void testCopyWithNullFields () {
240+ ToolUseBlock toolUseBlock =
241+ ToolUseBlock .builder ().id ("id" ).name ("tool" ).input (Map .of ()).build ();
242+
243+ ToolCallParam original = ToolCallParam .builder ().toolUseBlock (toolUseBlock ).build ();
244+
245+ // Create copy
246+ ToolCallParam copy = ToolCallParam .builder (original ).build ();
247+
248+ assertNotNull (copy );
249+ assertEquals (original .getToolUseBlock (), copy .getToolUseBlock ());
250+ assertTrue (copy .getInput ().isEmpty ());
251+ assertNull (copy .getAgent ());
252+ assertNull (copy .getContext ());
253+ }
254+
255+ @ Test
256+ @ DisplayName ("Should throw NPE when source is null" )
257+ void testCopyWithNullSource () {
258+ org .junit .jupiter .api .Assertions .assertThrows (
259+ NullPointerException .class , () -> ToolCallParam .builder ((ToolCallParam ) null ));
260+ }
261+
262+ @ Test
263+ @ DisplayName ("Should preserve immutable fields as references" )
264+ void testPreserveImmutableFields () {
265+ ToolUseBlock toolUseBlock =
266+ ToolUseBlock .builder ()
267+ .id ("test-id" )
268+ .name ("test_tool" )
269+ .input (Map .of ("key" , "value" ))
270+ .build ();
271+
272+ Agent mockAgent = mock (Agent .class );
273+ when (mockAgent .getName ()).thenReturn ("TestAgent" );
274+
275+ ToolExecutionContext context =
276+ ToolExecutionContext .builder ().register ("testContext" ).build ();
277+
278+ ToolCallParam original =
279+ ToolCallParam .builder ()
280+ .toolUseBlock (toolUseBlock )
281+ .agent (mockAgent )
282+ .context (context )
283+ .build ();
284+
285+ ToolCallParam copy = ToolCallParam .builder (original ).build ();
286+
287+ // Immutable fields should be the same references
288+ assertSame (original .getToolUseBlock (), copy .getToolUseBlock ());
289+ assertSame (original .getAgent (), copy .getAgent ());
290+ assertSame (original .getContext (), copy .getContext ());
291+ }
292+
293+ @ Test
294+ @ DisplayName ("Should allow replacing immutable fields in copy" )
295+ void testReplaceImmutableFieldsInCopy () {
296+ ToolUseBlock originalToolUseBlock =
297+ ToolUseBlock .builder ().id ("id1" ).name ("tool1" ).input (Map .of ()).build ();
298+ ToolUseBlock newToolUseBlock =
299+ ToolUseBlock .builder ().id ("id2" ).name ("tool2" ).input (Map .of ()).build ();
300+
301+ ToolCallParam original =
302+ ToolCallParam .builder ().toolUseBlock (originalToolUseBlock ).build ();
303+
304+ ToolCallParam modified =
305+ ToolCallParam .builder (original ).toolUseBlock (newToolUseBlock ).build ();
306+
307+ // Original should be unchanged
308+ assertEquals ("id1" , original .getToolUseBlock ().getId ());
309+ assertEquals ("tool1" , original .getToolUseBlock ().getName ());
310+
311+ // Modified should have new values
312+ assertEquals ("id2" , modified .getToolUseBlock ().getId ());
313+ assertEquals ("tool2" , modified .getToolUseBlock ().getName ());
314+ }
315+ }
316+
121317 @ Nested
122318 @ DisplayName ("Emitter functionality" )
123319 class EmitterTests {
0 commit comments