Skip to content

Commit 7eea5a3

Browse files
Fix HumanInTheMiddleWrapper.getDummyArgsFor passing null for Project constructor arg (#302)
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: jGauravGupta <15934072+jGauravGupta@users.noreply.github.com>
1 parent c445f61 commit 7eea5a3

2 files changed

Lines changed: 107 additions & 3 deletions

File tree

src/main/java/io/github/jeddict/ai/agent/HumanInTheMiddleWrapper.java

Lines changed: 36 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
import net.bytebuddy.matcher.ElementMatchers;
2727

2828
import java.lang.reflect.Constructor;
29+
import java.lang.reflect.Field;
2930
import java.lang.reflect.Method;
3031
import java.util.Arrays;
3132
import java.util.function.Function;
@@ -112,13 +113,13 @@ public <T> T wrap(T originalTool) {
112113
.load(clazz.getClassLoader())
113114
.getLoaded()
114115
.getConstructor(clazz.getDeclaredConstructors()[0].getParameterTypes())
115-
.newInstance(getDummyArgsFor(clazz.getDeclaredConstructors()[0]));
116+
.newInstance(getDummyArgsFor(clazz.getDeclaredConstructors()[0], originalTool));
116117
} catch (Exception e) {
117118
throw new RuntimeException("Failed to wrap tool", e);
118119
}
119120
}
120121

121-
private Object[] getDummyArgsFor(Constructor<?> constructor) {
122+
private Object[] getDummyArgsFor(Constructor<?> constructor, Object originalTool) {
122123
Object[] dummyArgs = new Object[constructor.getParameterCount()];
123124
Class<?>[] parameterTypes = constructor.getParameterTypes();
124125
for (int i = 0; i < parameterTypes.length; i++) {
@@ -128,12 +129,44 @@ private Object[] getDummyArgsFor(Constructor<?> constructor) {
128129
} else if (parameterTypes[i] == String.class) {
129130
dummyArgs[i] = "."; // Satisfy AbstractTool's non-null basedir check
130131
} else {
131-
dummyArgs[i] = null;
132+
// For any other object parameter, try to get the actual value from
133+
// the original tool via reflection so that the proxy subclass
134+
// constructor does not fail on validation (e.g. ProjectTools(Project)
135+
// calls basedirOf(project) which throws when project is null).
136+
dummyArgs[i] = getFieldValueByType(parameterTypes[i], originalTool);
132137
}
133138
}
134139
return dummyArgs;
135140
}
136141

142+
/**
143+
* Searches the class hierarchy of {@code source} for the first field whose
144+
* type is assignable to {@code type} and returns its value. Returns
145+
* {@code null} when no matching field is found or the field is inaccessible.
146+
*
147+
* <p>The check {@code type.isAssignableFrom(field.getType())} answers:
148+
* "can a value of {@code field.getType()} be used where {@code type} is
149+
* expected?" — i.e. the field's declared type is the same as or a subtype
150+
* of the required parameter type.</p>
151+
*/
152+
private Object getFieldValueByType(Class<?> type, Object source) {
153+
Class<?> clazz = source.getClass();
154+
while (clazz != null) {
155+
for (Field field : clazz.getDeclaredFields()) {
156+
if (type.isAssignableFrom(field.getType())) {
157+
try {
158+
field.setAccessible(true);
159+
return field.get(source);
160+
} catch (IllegalAccessException | SecurityException ignored) {
161+
// fall through to the next field / superclass
162+
}
163+
}
164+
}
165+
clazz = clazz.getSuperclass();
166+
}
167+
return null;
168+
}
169+
137170
/**
138171
* The Byte Buddy interceptor that contains the core HITM logic.
139172
*/
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
/**
2+
* Copyright 2025 the original author or authors from the Jeddict project (https://jeddict.github.io/).
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
5+
* use this file except in compliance with the License. You may obtain a copy of
6+
* the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12+
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13+
* License for the specific language governing permissions and limitations under
14+
* the License.
15+
*/
16+
package io.github.jeddict.ai.agent;
17+
18+
import com.github.caciocavallosilano.cacio.ctc.junit.CacioTest;
19+
import dev.langchain4j.agent.tool.ToolExecutionRequest;
20+
import io.github.jeddict.ai.agent.project.MavenProjectTools;
21+
import io.github.jeddict.ai.agent.project.ProjectTools;
22+
import io.github.jeddict.ai.test.TestBase;
23+
import java.util.ArrayList;
24+
import java.util.List;
25+
import java.util.function.Function;
26+
import org.junit.jupiter.api.BeforeEach;
27+
import org.junit.jupiter.api.Test;
28+
29+
import static org.assertj.core.api.BDDAssertions.then;
30+
31+
/**
32+
* Regression tests for {@link HumanInTheMiddleWrapper} wrapping project-aware
33+
* tools whose constructors take {@code Project} (rather than a plain
34+
* {@code String} basedir). These tests require a headless AWT environment
35+
* (hence {@code @CacioTest}).
36+
*/
37+
@CacioTest
38+
public class HumanInTheMiddleWrapperProjectTest extends TestBase {
39+
40+
private List<ToolExecutionRequest> interceptionEvents;
41+
private Function<ToolExecutionRequest, Boolean> interceptor;
42+
43+
@BeforeEach
44+
@Override
45+
public void beforeEach() throws Exception {
46+
super.beforeEach();
47+
interceptionEvents = new ArrayList<>();
48+
interceptor = execution -> {
49+
interceptionEvents.add(execution);
50+
return true;
51+
};
52+
}
53+
54+
@Test
55+
void wrapping_ProjectTools_does_not_throw() throws Exception {
56+
// Regression: wrapping a ProjectTools (whose constructor takes a Project
57+
// and calls basedirOf(project)) must not throw
58+
// "IllegalArgumentException: project cannot be null".
59+
final ProjectTools original = ProjectTools.forProject(project(projectDir));
60+
final ProjectTools wrapped = new HumanInTheMiddleWrapper(interceptor).wrap(original);
61+
then(wrapped).isNotNull();
62+
}
63+
64+
@Test
65+
void wrapping_MavenProjectTools_does_not_throw() throws Exception {
66+
// Same regression check for the MavenProjectTools subclass.
67+
final MavenProjectTools original = new MavenProjectTools(project(projectDir));
68+
final MavenProjectTools wrapped = new HumanInTheMiddleWrapper(interceptor).wrap(original);
69+
then(wrapped).isNotNull();
70+
}
71+
}

0 commit comments

Comments
 (0)