diff --git a/src/main/java/org/egothor/methodatlas/MethodAtlasApp.java b/src/main/java/org/egothor/methodatlas/MethodAtlasApp.java
index 0fe858f..cdc402b 100644
--- a/src/main/java/org/egothor/methodatlas/MethodAtlasApp.java
+++ b/src/main/java/org/egothor/methodatlas/MethodAtlasApp.java
@@ -20,19 +20,20 @@ import org.egothor.methodatlas.ai.AiProvider;
import org.egothor.methodatlas.ai.AiSuggestionEngine;
import org.egothor.methodatlas.ai.AiSuggestionEngineImpl;
import org.egothor.methodatlas.ai.AiSuggestionException;
+import org.egothor.methodatlas.ai.PromptBuilder;
import org.egothor.methodatlas.ai.SuggestionLookup;
import com.github.javaparser.ParserConfiguration;
import com.github.javaparser.ParserConfiguration.LanguageLevel;
import com.github.javaparser.StaticJavaParser;
import com.github.javaparser.ast.CompilationUnit;
-import com.github.javaparser.ast.PackageDeclaration;
import com.github.javaparser.ast.body.ClassOrInterfaceDeclaration;
import com.github.javaparser.ast.body.MethodDeclaration;
import com.github.javaparser.ast.expr.AnnotationExpr;
import com.github.javaparser.ast.expr.ArrayInitializerExpr;
import com.github.javaparser.ast.expr.Expression;
import com.github.javaparser.ast.expr.MemberValuePair;
+import com.github.javaparser.ast.nodeTypes.NodeWithName;
/**
* Command-line application for scanning Java test sources, extracting JUnit
@@ -298,33 +299,63 @@ public class MethodAtlasApp { // NOPMD
private static void processFile(Path path, OutputMode mode, AiOptions aiOptions, AiSuggestionEngine aiEngine) {
try {
CompilationUnit compilationUnit = StaticJavaParser.parse(path);
- String packageName = compilationUnit.getPackageDeclaration().map(PackageDeclaration::getNameAsString)
- .orElse("");
+ String packageName = compilationUnit.getPackageDeclaration().map(NodeWithName::getNameAsString).orElse("");
compilationUnit.findAll(ClassOrInterfaceDeclaration.class).forEach(clazz -> {
String className = clazz.getNameAsString();
String fqcn = packageName.isEmpty() ? className : packageName + "." + className;
- SuggestionLookup suggestionLookup = resolveSuggestionLookup(clazz, fqcn, aiOptions, aiEngine);
- clazz.findAll(MethodDeclaration.class).forEach(method -> {
- if (!isJUnitTest(method)) {
- return;
- }
+ List testMethods = findJUnitTestMethods(clazz);
+ SuggestionLookup suggestionLookup = resolveSuggestionLookup(clazz, fqcn, testMethods, aiOptions,
+ aiEngine);
+ for (MethodDeclaration method : testMethods) {
int loc = countLOC(method);
List tags = getTagValues(method);
AiMethodSuggestion suggestion = suggestionLookup.find(method.getNameAsString()).orElse(null);
emit(mode, aiOptions.enabled(), fqcn, method.getNameAsString(), loc, tags, suggestion);
- });
+ }
});
} catch (Exception e) {
if (LOG.isLoggable(Level.WARNING)) {
- LOG.log(Level.WARNING, "Failed to parse: {0} due to {1}", new Object[] { path, e.getMessage() });
+ LOG.log(Level.WARNING, "Failed to parse: " + path, e);
}
}
}
+ /**
+ * Returns all JUnit test methods declared within the specified class.
+ *
+ *
+ * The method traverses the supplied {@link ClassOrInterfaceDeclaration} and
+ * collects all {@link MethodDeclaration} instances that satisfy the
+ * {@link #isJUnitTest(MethodDeclaration)} predicate.
+ *
+ *
+ *
+ * The detection logic currently recognizes methods annotated with supported
+ * JUnit Jupiter test annotations such as {@code @Test},
+ * {@code @ParameterizedTest}, and {@code @RepeatedTest}. Only methods matching
+ * these criteria are included in the returned list.
+ *
+ *
+ *
+ * The returned list preserves the discovery order produced by
+ * {@link com.github.javaparser.ast.Node#findAll(Class)}, which corresponds to
+ * the order of method declarations in the source file.
+ *
+ *
+ * @param clazz parsed class declaration whose methods should be inspected
+ * @return list of JUnit test method declarations contained in the class;
+ * possibly empty but never {@code null}
+ *
+ * @see #isJUnitTest(MethodDeclaration)
+ */
+ private static List findJUnitTestMethods(ClassOrInterfaceDeclaration clazz) {
+ return clazz.findAll(MethodDeclaration.class).stream().filter(MethodAtlasApp::isJUnitTest).toList();
+ }
+
/**
* Resolves method-level AI suggestions for a parsed class.
*
@@ -342,8 +373,8 @@ public class MethodAtlasApp { // NOPMD
* @return lookup of AI suggestions keyed by method name; never {@code null}
*/
private static SuggestionLookup resolveSuggestionLookup(ClassOrInterfaceDeclaration clazz, String fqcn,
- AiOptions aiOptions, AiSuggestionEngine aiEngine) {
- if (!aiOptions.enabled() || aiEngine == null) {
+ List testMethods, AiOptions aiOptions, AiSuggestionEngine aiEngine) {
+ if (!aiOptions.enabled() || aiEngine == null || testMethods.isEmpty()) {
return SuggestionLookup.from(null);
}
@@ -356,18 +387,55 @@ public class MethodAtlasApp { // NOPMD
return SuggestionLookup.from(null);
}
+ List targetMethods = toTargetMethods(testMethods);
+
try {
- AiClassSuggestion aiClassSuggestion = aiEngine.suggestForClass(fqcn, classSource);
+ AiClassSuggestion aiClassSuggestion = aiEngine.suggestForClass(fqcn, classSource, targetMethods);
return SuggestionLookup.from(aiClassSuggestion);
} catch (AiSuggestionException e) {
if (LOG.isLoggable(Level.WARNING)) {
- LOG.log(Level.WARNING, "AI suggestion failed for class {0}: {1}",
- new Object[] { fqcn, e.getMessage() });
+ LOG.log(Level.WARNING, "AI suggestion failed for class " + fqcn, e);
}
return SuggestionLookup.from(null);
}
}
+ /**
+ * Converts parsed JUnit test method declarations into prompt target
+ * descriptors.
+ *
+ *
+ * The returned {@link PromptBuilder.TargetMethod} objects provide a compact
+ * representation of the methods that should be analyzed by the AI
+ * classification prompt. Each descriptor contains the method name together with
+ * the optional begin and end line numbers derived from the parser source range.
+ *
+ *
+ *
+ * Line numbers are obtained from {@link MethodDeclaration#getRange()} when
+ * source position information is available. If the parser did not retain range
+ * metadata for a method, the corresponding line value is set to {@code null}.
+ *
+ *
+ *
+ * The resulting list preserves the order of the supplied method declarations.
+ *
+ *
+ * @param testMethods list of parsed JUnit test method declarations
+ * @return list of prompt target descriptors representing the supplied methods;
+ * possibly empty but never {@code null}
+ *
+ * @see PromptBuilder.TargetMethod
+ * @see MethodDeclaration#getRange()
+ */
+ private static List toTargetMethods(List testMethods) {
+ return testMethods.stream()
+ .map(method -> new PromptBuilder.TargetMethod(method.getNameAsString(),
+ method.getRange().map(range -> range.begin.line).orElse(null),
+ method.getRange().map(range -> range.end.line).orElse(null)))
+ .toList();
+ }
+
/**
* Creates the AI suggestion engine for the current run.
*
diff --git a/src/main/java/org/egothor/methodatlas/ai/AiProviderClient.java b/src/main/java/org/egothor/methodatlas/ai/AiProviderClient.java
index 81a8e27..e824440 100644
--- a/src/main/java/org/egothor/methodatlas/ai/AiProviderClient.java
+++ b/src/main/java/org/egothor/methodatlas/ai/AiProviderClient.java
@@ -1,5 +1,7 @@
package org.egothor.methodatlas.ai;
+import java.util.List;
+
/**
* Provider-specific client abstraction used to communicate with external AI
* inference services.
@@ -76,10 +78,12 @@ public interface AiProviderClient {
* objects describing individual test methods.
*
*
- * @param fqcn fully qualified name of the analyzed class
- * @param classSource complete source code of the class being analyzed
- * @param taxonomyText security taxonomy definition guiding the AI
- * classification
+ * @param fqcn fully qualified name of the analyzed class
+ * @param classSource complete source code of the class being analyzed
+ * @param taxonomyText security taxonomy definition guiding the AI
+ * classification
+ * @param targetMethods deterministically extracted JUnit test methods that must
+ * be classified
* @return normalized AI classification result
*
* @throws AiSuggestionException if the request fails due to provider errors,
@@ -88,6 +92,6 @@ public interface AiProviderClient {
* @see AiClassSuggestion
* @see AiMethodSuggestion
*/
- AiClassSuggestion suggestForClass(String fqcn, String classSource, String taxonomyText)
- throws AiSuggestionException;
+ AiClassSuggestion suggestForClass(String fqcn, String classSource, String taxonomyText,
+ List targetMethods) throws AiSuggestionException;
}
\ No newline at end of file
diff --git a/src/main/java/org/egothor/methodatlas/ai/AiSuggestionEngine.java b/src/main/java/org/egothor/methodatlas/ai/AiSuggestionEngine.java
index 5001167..c3600c1 100644
--- a/src/main/java/org/egothor/methodatlas/ai/AiSuggestionEngine.java
+++ b/src/main/java/org/egothor/methodatlas/ai/AiSuggestionEngine.java
@@ -1,5 +1,7 @@
package org.egothor.methodatlas.ai;
+import java.util.List;
+
/**
* High-level AI orchestration contract for security classification of parsed
* test classes.
@@ -52,8 +54,10 @@ public interface AiSuggestionEngine {
* using full class context.
*
*
- * @param fqcn fully qualified class name of the parsed test class
- * @param classSource complete source code of the class to analyze
+ * @param fqcn fully qualified class name of the parsed test class
+ * @param classSource complete source code of the class to analyze
+ * @param targetMethods deterministically extracted JUnit test methods that must
+ * be classified
* @return normalized AI classification result for the class and its methods
*
* @throws AiSuggestionException if analysis fails due to provider communication
@@ -63,5 +67,6 @@ public interface AiSuggestionEngine {
* @see AiClassSuggestion
* @see AiMethodSuggestion
*/
- AiClassSuggestion suggestForClass(String fqcn, String classSource) throws AiSuggestionException;
+ AiClassSuggestion suggestForClass(String fqcn, String classSource, List targetMethods)
+ throws AiSuggestionException;
}
\ No newline at end of file
diff --git a/src/main/java/org/egothor/methodatlas/ai/AiSuggestionEngineImpl.java b/src/main/java/org/egothor/methodatlas/ai/AiSuggestionEngineImpl.java
index 8da4da9..c966b8e 100644
--- a/src/main/java/org/egothor/methodatlas/ai/AiSuggestionEngineImpl.java
+++ b/src/main/java/org/egothor/methodatlas/ai/AiSuggestionEngineImpl.java
@@ -2,6 +2,7 @@ package org.egothor.methodatlas.ai;
import java.io.IOException;
import java.nio.file.Files;
+import java.util.List;
/**
* Default implementation of {@link AiSuggestionEngine} that coordinates
@@ -73,8 +74,10 @@ public final class AiSuggestionEngineImpl implements AiSuggestionEngine {
* taxonomy text loaded at engine initialization time.
*
*
- * @param fqcn fully qualified class name of the analyzed test class
- * @param classSource complete source code of the class to analyze
+ * @param fqcn fully qualified class name of the analyzed test class
+ * @param classSource complete source code of the class to analyze
+ * @param targetMethods deterministically extracted JUnit test methods that must
+ * be classified
* @return normalized AI classification result for the class and its methods
*
* @throws AiSuggestionException if the provider fails to analyze the class or
@@ -84,8 +87,9 @@ public final class AiSuggestionEngineImpl implements AiSuggestionEngine {
* @see AiProviderClient#suggestForClass(String, String, String)
*/
@Override
- public AiClassSuggestion suggestForClass(String fqcn, String classSource) throws AiSuggestionException {
- return client.suggestForClass(fqcn, classSource, taxonomyText);
+ public AiClassSuggestion suggestForClass(String fqcn, String classSource,
+ List targetMethods) throws AiSuggestionException {
+ return client.suggestForClass(fqcn, classSource, taxonomyText, targetMethods);
}
/**
diff --git a/src/main/java/org/egothor/methodatlas/ai/AnthropicClient.java b/src/main/java/org/egothor/methodatlas/ai/AnthropicClient.java
index 1c96371..dc3ab63 100644
--- a/src/main/java/org/egothor/methodatlas/ai/AnthropicClient.java
+++ b/src/main/java/org/egothor/methodatlas/ai/AnthropicClient.java
@@ -118,9 +118,11 @@ public final class AnthropicClient implements AiProviderClient {
* model, which is then deserialized into an {@link AiClassSuggestion}.
*
*
- * @param fqcn fully qualified class name being analyzed
- * @param classSource complete source code of the class
- * @param taxonomyText taxonomy definition guiding classification
+ * @param fqcn fully qualified class name being analyzed
+ * @param classSource complete source code of the class
+ * @param taxonomyText taxonomy definition guiding classification
+ * @param targetMethods deterministically extracted JUnit test methods that must
+ * be classified
*
* @return normalized AI classification result
*
@@ -129,10 +131,10 @@ public final class AnthropicClient implements AiProviderClient {
* invalid content
*/
@Override
- public AiClassSuggestion suggestForClass(String fqcn, String classSource, String taxonomyText)
- throws AiSuggestionException {
+ public AiClassSuggestion suggestForClass(String fqcn, String classSource, String taxonomyText,
+ List targetMethods) throws AiSuggestionException {
try {
- String prompt = PromptBuilder.build(fqcn, classSource, taxonomyText);
+ String prompt = PromptBuilder.build(fqcn, classSource, taxonomyText, targetMethods);
MessageRequest payload = new MessageRequest(options.modelName(), SYSTEM_PROMPT,
List.of(new ContentMessage("user", List.of(new ContentBlock("text", prompt)))), 0.0, 2_000);
diff --git a/src/main/java/org/egothor/methodatlas/ai/OllamaClient.java b/src/main/java/org/egothor/methodatlas/ai/OllamaClient.java
index 0421d65..c92f9f0 100644
--- a/src/main/java/org/egothor/methodatlas/ai/OllamaClient.java
+++ b/src/main/java/org/egothor/methodatlas/ai/OllamaClient.java
@@ -128,9 +128,11 @@ public final class OllamaClient implements AiProviderClient {
* {@link AiClassSuggestion}, and then normalized before being returned.
*
*
- * @param fqcn fully qualified class name being analyzed
- * @param classSource complete source code of the class being analyzed
- * @param taxonomyText taxonomy definition guiding classification
+ * @param fqcn fully qualified class name being analyzed
+ * @param classSource complete source code of the class being analyzed
+ * @param taxonomyText taxonomy definition guiding classification
+ * @param targetMethods deterministically extracted JUnit test methods that must
+ * be classified
* @return normalized AI classification result
*
* @throws AiSuggestionException if the request fails, if the provider returns
@@ -138,10 +140,10 @@ public final class OllamaClient implements AiProviderClient {
* fails
*/
@Override
- public AiClassSuggestion suggestForClass(String fqcn, String classSource, String taxonomyText)
- throws AiSuggestionException {
+ public AiClassSuggestion suggestForClass(String fqcn, String classSource, String taxonomyText,
+ List targetMethods) throws AiSuggestionException {
try {
- String prompt = PromptBuilder.build(fqcn, classSource, taxonomyText);
+ String prompt = PromptBuilder.build(fqcn, classSource, taxonomyText, targetMethods);
ChatRequest payload = new ChatRequest(options.modelName(),
List.of(new Message("system", SYSTEM_PROMPT), new Message("user", prompt)), false,
diff --git a/src/main/java/org/egothor/methodatlas/ai/OpenAiCompatibleClient.java b/src/main/java/org/egothor/methodatlas/ai/OpenAiCompatibleClient.java
index 629bc7f..f77f2c9 100644
--- a/src/main/java/org/egothor/methodatlas/ai/OpenAiCompatibleClient.java
+++ b/src/main/java/org/egothor/methodatlas/ai/OpenAiCompatibleClient.java
@@ -128,10 +128,11 @@ public final class OpenAiCompatibleClient implements AiProviderClient {
* {@link AiClassSuggestion}.
*
*
- * @param fqcn fully qualified class name being analyzed
- * @param classSource complete source code of the class
- * @param taxonomyText taxonomy definition guiding classification
- *
+ * @param fqcn fully qualified class name being analyzed
+ * @param classSource complete source code of the class
+ * @param taxonomyText taxonomy definition guiding classification
+ * @param targetMethods deterministically extracted JUnit test methods that must
+ * be classified
* @return normalized classification result
*
* @throws AiSuggestionException if the provider request fails, the model
@@ -139,10 +140,10 @@ public final class OpenAiCompatibleClient implements AiProviderClient {
* fails
*/
@Override
- public AiClassSuggestion suggestForClass(String fqcn, String classSource, String taxonomyText)
- throws AiSuggestionException {
+ public AiClassSuggestion suggestForClass(String fqcn, String classSource, String taxonomyText,
+ List targetMethods) throws AiSuggestionException {
try {
- String prompt = PromptBuilder.build(fqcn, classSource, taxonomyText);
+ String prompt = PromptBuilder.build(fqcn, classSource, taxonomyText, targetMethods);
ChatRequest payload = new ChatRequest(options.modelName(),
List.of(new Message("system", SYSTEM_PROMPT), new Message("user", prompt)), 0.0);
diff --git a/src/main/java/org/egothor/methodatlas/ai/PromptBuilder.java b/src/main/java/org/egothor/methodatlas/ai/PromptBuilder.java
index 15e62f9..876b48e 100644
--- a/src/main/java/org/egothor/methodatlas/ai/PromptBuilder.java
+++ b/src/main/java/org/egothor/methodatlas/ai/PromptBuilder.java
@@ -1,5 +1,9 @@
package org.egothor.methodatlas.ai;
+import java.util.List;
+import java.util.Objects;
+import java.util.stream.Collectors;
+
/**
* Utility responsible for constructing the prompt supplied to AI providers for
* security classification of JUnit test classes.
@@ -18,6 +22,13 @@ package org.egothor.methodatlas.ai;
*
*
*
+ * This revision keeps the full class source as semantic context but removes
+ * method discovery from the AI model. The caller supplies the exact list of
+ * JUnit test methods that must be classified, optionally with source line
+ * anchors.
+ *
+ *
+ *
* The resulting prompt is passed to the configured AI provider and instructs
* the model to produce a deterministic JSON classification result describing
* security relevance and taxonomy tags for individual test methods.
@@ -38,6 +49,23 @@ package org.egothor.methodatlas.ai;
* @see OptimizedSecurityTaxonomy
*/
public final class PromptBuilder {
+
+ /**
+ * Deterministically extracted test method descriptor supplied to the prompt.
+ *
+ * @param methodName name of the JUnit test method
+ * @param beginLine first source line of the method, or {@code null} if unknown
+ * @param endLine last source line of the method, or {@code null} if unknown
+ */
+ public record TargetMethod(String methodName, Integer beginLine, Integer endLine) {
+ public TargetMethod {
+ Objects.requireNonNull(methodName, "methodName");
+ if (methodName.isBlank()) {
+ throw new IllegalArgumentException("methodName must not be blank");
+ }
+ }
+ }
+
/**
* Prevents instantiation of this utility class.
*/
@@ -55,6 +83,7 @@ public final class PromptBuilder {
*
* - task instructions describing the classification objective
* - the security taxonomy definition controlling allowed tags
+ * - the exact list of target test methods to classify
* - strict output rules enforcing JSON-only responses
* - a formal JSON schema describing the expected result structure
* - the fully qualified class name of the analyzed test class
@@ -73,22 +102,40 @@ public final class PromptBuilder {
* in chat-based inference APIs.
*
*
- * @param fqcn fully qualified class name of the test class being
- * analyzed
- * @param classSource complete source code of the test class
- * @param taxonomyText taxonomy definition guiding classification
+ * @param fqcn fully qualified class name of the test class being
+ * analyzed
+ * @param classSource complete source code of the test class
+ * @param taxonomyText taxonomy definition guiding classification
+ * @param targetMethods exact list of deterministically discovered JUnit test
+ * methods to classify
* @return formatted prompt supplied to the AI provider
*
* @see AiSuggestionEngine#suggestForClass(String, String)
*/
- public static String build(String fqcn, String classSource, String taxonomyText) {
+ public static String build(String fqcn, String classSource, String taxonomyText, List targetMethods) {
+ Objects.requireNonNull(fqcn, "fqcn");
+ Objects.requireNonNull(classSource, "classSource");
+ Objects.requireNonNull(taxonomyText, "taxonomyText");
+ Objects.requireNonNull(targetMethods, "targetMethods");
+
+ if (targetMethods.isEmpty()) {
+ throw new IllegalArgumentException("targetMethods must not be empty");
+ }
+
+ String targetMethodBlock = targetMethods.stream().map(PromptBuilder::formatTargetMethod)
+ .collect(Collectors.joining("\n"));
+
+ String expectedMethodNames = targetMethods.stream().map(TargetMethod::methodName)
+ .map(name -> "\"" + name + "\"").collect(Collectors.joining(", "));
+
return """
You are analyzing a single JUnit 5 test class and suggesting security tags.
TASK
- Analyze the WHOLE class for context.
- - Return per-method suggestions for JUnit test methods only.
+ - Classify ONLY the methods explicitly listed in TARGET TEST METHODS.
- Do not invent methods that do not exist.
+ - Do not classify helper methods, lifecycle methods, nested classes, or any method not listed.
- Be conservative.
- If uncertain, classify the method as securityRelevant=false.
- Ignore pure functional / performance / UX tests unless they explicitly validate a security property.
@@ -96,17 +143,30 @@ public final class PromptBuilder {
CONTROLLED TAXONOMY
%s
+ TARGET TEST METHODS
+ The following methods were extracted deterministically by the parser and are the ONLY methods
+ you are allowed to classify. Use the full class source only as context for understanding them.
+
+ %s
+
OUTPUT RULES
- Return JSON only.
- No markdown.
- No prose outside JSON.
+ - Return exactly one result for each target method.
+ - methodName values in the output must exactly match one of:
+ [%s]
+ - Do not omit any listed method.
+ - Do not include any additional methods.
- Tags must come only from this closed set:
security, auth, access-control, crypto, input-validation, injection, data-protection, logging, error-handling, owasp
- If securityRelevant=true, tags MUST include "security".
- Add 1-3 tags total per method.
- - displayName must be null when securityRelevant=false.
+ - If securityRelevant=false, displayName must be null.
+ - If securityRelevant=false, tags must be [].
- If securityRelevant=true, displayName must match:
SECURITY: -
+ - reason should be short and specific.
JSON SHAPE
{
@@ -131,6 +191,17 @@ public final class PromptBuilder {
SOURCE
%s
"""
- .formatted(taxonomyText, fqcn, classSource);
+ .formatted(taxonomyText, targetMethodBlock, expectedMethodNames, fqcn, classSource);
+ }
+
+ private static String formatTargetMethod(TargetMethod targetMethod) {
+ StringBuilder builder = new StringBuilder("- ").append(targetMethod.methodName());
+
+ if (targetMethod.beginLine() != null || targetMethod.endLine() != null) {
+ builder.append(" [lines ").append(targetMethod.beginLine() == null ? "?" : targetMethod.beginLine())
+ .append('-').append(targetMethod.endLine() == null ? "?" : targetMethod.endLine()).append(']');
+ }
+
+ return builder.toString();
}
}
\ No newline at end of file
diff --git a/src/test/java/org/egothor/methodatlas/MethodAtlasAppAiTest.java b/src/test/java/org/egothor/methodatlas/MethodAtlasAppAiTest.java
index 7e6a067..76a75c3 100644
--- a/src/test/java/org/egothor/methodatlas/MethodAtlasAppAiTest.java
+++ b/src/test/java/org/egothor/methodatlas/MethodAtlasAppAiTest.java
@@ -4,6 +4,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mockConstruction;
@@ -39,15 +40,15 @@ class MethodAtlasAppAiTest {
try (MockedConstruction mocked = mockConstruction(AiSuggestionEngineImpl.class,
(mock, context) -> {
- when(mock.suggestForClass(eq("com.acme.tests.SampleOneTest"), anyString()))
+ when(mock.suggestForClass(eq("com.acme.tests.SampleOneTest"), anyString(), any()))
.thenReturn(sampleOneSuggestion());
- when(mock.suggestForClass(eq("com.acme.other.AnotherTest"), anyString()))
+ when(mock.suggestForClass(eq("com.acme.other.AnotherTest"), anyString(), any()))
.thenReturn(anotherSuggestion());
- when(mock.suggestForClass(eq("com.acme.security.AccessControlServiceTest"), anyString()))
+ when(mock.suggestForClass(eq("com.acme.security.AccessControlServiceTest"), anyString(), any()))
.thenReturn(accessControlSuggestion());
- when(mock.suggestForClass(eq("com.acme.storage.PathTraversalValidationTest"), anyString()))
+ when(mock.suggestForClass(eq("com.acme.storage.PathTraversalValidationTest"), anyString(), any()))
.thenReturn(pathTraversalSuggestion());
- when(mock.suggestForClass(eq("com.acme.audit.AuditLoggingTest"), anyString()))
+ when(mock.suggestForClass(eq("com.acme.audit.AuditLoggingTest"), anyString(), any()))
.thenReturn(auditLoggingSuggestion());
})) {
@@ -99,15 +100,15 @@ class MethodAtlasAppAiTest {
try (MockedConstruction mocked = mockConstruction(AiSuggestionEngineImpl.class,
(mock, context) -> {
- when(mock.suggestForClass(eq("com.acme.tests.SampleOneTest"), anyString()))
+ when(mock.suggestForClass(eq("com.acme.tests.SampleOneTest"), anyString(), any()))
.thenReturn(sampleOneSuggestion());
- when(mock.suggestForClass(eq("com.acme.other.AnotherTest"), anyString()))
+ when(mock.suggestForClass(eq("com.acme.other.AnotherTest"), anyString(), any()))
.thenReturn(anotherSuggestion());
- when(mock.suggestForClass(eq("com.acme.security.AccessControlServiceTest"), anyString()))
+ when(mock.suggestForClass(eq("com.acme.security.AccessControlServiceTest"), anyString(), any()))
.thenThrow(new AiSuggestionException("Simulated provider failure"));
- when(mock.suggestForClass(eq("com.acme.storage.PathTraversalValidationTest"), anyString()))
+ when(mock.suggestForClass(eq("com.acme.storage.PathTraversalValidationTest"), anyString(), any()))
.thenReturn(pathTraversalSuggestion());
- when(mock.suggestForClass(eq("com.acme.audit.AuditLoggingTest"), anyString()))
+ when(mock.suggestForClass(eq("com.acme.audit.AuditLoggingTest"), anyString(), any()))
.thenReturn(auditLoggingSuggestion());
})) {
@@ -167,7 +168,7 @@ class MethodAtlasAppAiTest {
assertEquals("", row.get(7));
assertEquals(1, mocked.constructed().size(), "Expected one AI engine instance");
- verify(mocked.constructed().get(0), never()).suggestForClass(anyString(), anyString());
+ verify(mocked.constructed().get(0), never()).suggestForClass(anyString(), anyString(), any());
}
}
diff --git a/src/test/java/org/egothor/methodatlas/ai/AiSuggestionEngineImplTest.java b/src/test/java/org/egothor/methodatlas/ai/AiSuggestionEngineImplTest.java
index e4d4cdd..ea3f9e0 100644
--- a/src/test/java/org/egothor/methodatlas/ai/AiSuggestionEngineImplTest.java
+++ b/src/test/java/org/egothor/methodatlas/ai/AiSuggestionEngineImplTest.java
@@ -34,22 +34,31 @@ class AiSuggestionEngineImplTest {
"SECURITY: authentication - reject unauthenticated request", List.of("security", "auth"),
"The test verifies that anonymous access is rejected.")));
+ List targetMethods = List
+ .of(new PromptBuilder.TargetMethod("shouldAllowOwnerToReadOwnStatement", null, null),
+ new PromptBuilder.TargetMethod("shouldAllowAdministratorToReadAnyStatement", null, null),
+ new PromptBuilder.TargetMethod("shouldDenyForeignUserFromReadingAnotherUsersStatement", null,
+ null),
+ new PromptBuilder.TargetMethod("shouldRejectUnauthenticatedRequest", null, null),
+ new PromptBuilder.TargetMethod("shouldRenderFriendlyAccountLabel", null, null));
+
AiOptions options = AiOptions.builder().enabled(true).provider(AiProvider.OPENAI).build();
try (MockedStatic factory = mockStatic(AiProviderFactory.class)) {
factory.when(() -> AiProviderFactory.create(options)).thenReturn(client);
when(client.suggestForClass(eq("com.acme.security.AccessControlServiceTest"),
- eq("class AccessControlServiceTest {}"), eq(DefaultSecurityTaxonomy.text()))).thenReturn(expected);
+ eq("class AccessControlServiceTest {}"), eq(DefaultSecurityTaxonomy.text()), eq(targetMethods)))
+ .thenReturn(expected);
AiSuggestionEngineImpl engine = new AiSuggestionEngineImpl(options);
AiClassSuggestion actual = engine.suggestForClass("com.acme.security.AccessControlServiceTest",
- "class AccessControlServiceTest {}");
+ "class AccessControlServiceTest {}", targetMethods);
assertSame(expected, actual);
factory.verify(() -> AiProviderFactory.create(options));
verify(client).suggestForClass("com.acme.security.AccessControlServiceTest",
- "class AccessControlServiceTest {}", DefaultSecurityTaxonomy.text());
+ "class AccessControlServiceTest {}", DefaultSecurityTaxonomy.text(), targetMethods);
verifyNoMoreInteractions(client);
}
}
@@ -64,24 +73,30 @@ class AiSuggestionEngineImplTest {
List.of("security", "input-validation", "owasp"),
"The test rejects a classic path traversal payload.")));
+ List targetMethods = List.of(
+ new PromptBuilder.TargetMethod("shouldRejectRelativePathTraversalSequence", null, null),
+ new PromptBuilder.TargetMethod("shouldRejectNestedTraversalAfterNormalization", null, null),
+ new PromptBuilder.TargetMethod("shouldAllowSafePathInsideUploadRoot", null, null),
+ new PromptBuilder.TargetMethod("shouldBuildDownloadFileName", null, null));
+
AiOptions options = AiOptions.builder().enabled(true).provider(AiProvider.OLLAMA)
.taxonomyMode(AiOptions.TaxonomyMode.OPTIMIZED).build();
try (MockedStatic factory = mockStatic(AiProviderFactory.class)) {
factory.when(() -> AiProviderFactory.create(options)).thenReturn(client);
when(client.suggestForClass(eq("com.acme.storage.PathTraversalValidationTest"),
- eq("class PathTraversalValidationTest {}"), eq(OptimizedSecurityTaxonomy.text())))
- .thenReturn(expected);
+ eq("class PathTraversalValidationTest {}"), eq(OptimizedSecurityTaxonomy.text()),
+ eq(targetMethods))).thenReturn(expected);
AiSuggestionEngineImpl engine = new AiSuggestionEngineImpl(options);
AiClassSuggestion actual = engine.suggestForClass("com.acme.storage.PathTraversalValidationTest",
- "class PathTraversalValidationTest {}");
+ "class PathTraversalValidationTest {}", targetMethods);
assertSame(expected, actual);
factory.verify(() -> AiProviderFactory.create(options));
verify(client).suggestForClass("com.acme.storage.PathTraversalValidationTest",
- "class PathTraversalValidationTest {}", OptimizedSecurityTaxonomy.text());
+ "class PathTraversalValidationTest {}", OptimizedSecurityTaxonomy.text(), targetMethods);
verifyNoMoreInteractions(client);
}
}
@@ -104,23 +119,29 @@ class AiSuggestionEngineImplTest {
"SECURITY: logging - redact bearer token", List.of("security", "logging"),
"The test ensures credentials are not written to logs.")));
+ List targetMethods = List.of(
+ new PromptBuilder.TargetMethod("shouldWriteAuditEventForPrivilegeChange", null, null),
+ new PromptBuilder.TargetMethod("shouldNotLogRawBearerToken", null, null),
+ new PromptBuilder.TargetMethod("shouldNotLogPlaintextPasswordOnAuthenticationFailure", null, null),
+ new PromptBuilder.TargetMethod("shouldFormatHumanReadableSupportMessage", null, null));
+
AiOptions options = AiOptions.builder().enabled(true).provider(AiProvider.OPENROUTER).taxonomyFile(taxonomyFile)
.build();
try (MockedStatic factory = mockStatic(AiProviderFactory.class)) {
factory.when(() -> AiProviderFactory.create(options)).thenReturn(client);
when(client.suggestForClass(eq("com.acme.audit.AuditLoggingTest"), eq("class AuditLoggingTest {}"),
- eq(taxonomyText))).thenReturn(expected);
+ eq(taxonomyText), eq(targetMethods))).thenReturn(expected);
AiSuggestionEngineImpl engine = new AiSuggestionEngineImpl(options);
AiClassSuggestion actual = engine.suggestForClass("com.acme.audit.AuditLoggingTest",
- "class AuditLoggingTest {}");
+ "class AuditLoggingTest {}", targetMethods);
assertSame(expected, actual);
factory.verify(() -> AiProviderFactory.create(options));
- verify(client).suggestForClass("com.acme.audit.AuditLoggingTest", "class AuditLoggingTest {}",
- taxonomyText);
+ verify(client).suggestForClass("com.acme.audit.AuditLoggingTest", "class AuditLoggingTest {}", taxonomyText,
+ targetMethods);
verifyNoMoreInteractions(client);
}
}
diff --git a/src/test/java/org/egothor/methodatlas/ai/OllamaClientTest.java b/src/test/java/org/egothor/methodatlas/ai/OllamaClientTest.java
index 892e669..1bb78f8 100644
--- a/src/test/java/org/egothor/methodatlas/ai/OllamaClientTest.java
+++ b/src/test/java/org/egothor/methodatlas/ai/OllamaClientTest.java
@@ -82,6 +82,11 @@ class OllamaClientTest {
}
""";
String taxonomyText = "security, input-validation, owasp";
+ List targetMethods = List.of(
+ new PromptBuilder.TargetMethod("shouldRejectRelativePathTraversalSequence", null, null),
+ new PromptBuilder.TargetMethod("shouldRejectNestedTraversalAfterNormalization", null, null),
+ new PromptBuilder.TargetMethod("shouldAllowSafePathInsideUploadRoot", null, null),
+ new PromptBuilder.TargetMethod("shouldBuildDownloadFileName", null, null));
String responseBody = """
{
@@ -111,7 +116,7 @@ class OllamaClientTest {
.modelName("qwen2.5-coder:7b").baseUrl("http://localhost:11434").build();
OllamaClient client = new OllamaClient(options);
- AiClassSuggestion suggestion = client.suggestForClass(fqcn, classSource, taxonomyText);
+ AiClassSuggestion suggestion = client.suggestForClass(fqcn, classSource, taxonomyText, targetMethods);
assertEquals(fqcn, suggestion.className());
assertEquals(Boolean.TRUE, suggestion.classSecurityRelevant());
@@ -137,12 +142,12 @@ class OllamaClientTest {
assertNotNull(requestBody);
assertTrue(requestBody.contains("\"model\":\"qwen2.5-coder:7b\""));
assertTrue(requestBody.contains("\"stream\":false"));
- assertTrue(requestBody.contains("You are a precise software security classification engine."));
- assertTrue(requestBody.contains("You classify JUnit 5 tests and return strict JSON only."));
- assertTrue(requestBody.contains("\"temperature\":0.0"));
assertTrue(requestBody.contains("FQCN: " + fqcn));
assertTrue(requestBody.contains("PathTraversalValidationTest"));
assertTrue(requestBody.contains("shouldRejectRelativePathTraversalSequence"));
+ assertTrue(requestBody.contains("shouldRejectNestedTraversalAfterNormalization"));
+ assertTrue(requestBody.contains("shouldAllowSafePathInsideUploadRoot"));
+ assertTrue(requestBody.contains("shouldBuildDownloadFileName"));
assertTrue(requestBody.contains(taxonomyText));
}
}
@@ -152,6 +157,12 @@ class OllamaClientTest {
ObjectMapper mapper = new ObjectMapper().configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
String fqcn = "com.acme.audit.AuditLoggingTest";
+ List targetMethods = List.of(
+ new PromptBuilder.TargetMethod("shouldWriteAuditEventForPrivilegeChange", null, null),
+ new PromptBuilder.TargetMethod("shouldNotLogRawBearerToken", null, null),
+ new PromptBuilder.TargetMethod("shouldNotLogPlaintextPasswordOnAuthenticationFailure", null, null),
+ new PromptBuilder.TargetMethod("shouldFormatHumanReadableSupportMessage", null, null));
+
String responseBody = """
{
"message": {
@@ -176,7 +187,8 @@ class OllamaClientTest {
OllamaClient client = new OllamaClient(options);
AiSuggestionException ex = org.junit.jupiter.api.Assertions.assertThrows(AiSuggestionException.class,
- () -> client.suggestForClass(fqcn, "class AuditLoggingTest {}", "security, logging"));
+ () -> client.suggestForClass(fqcn, "class AuditLoggingTest {}", "security, logging",
+ targetMethods));
assertEquals("Ollama suggestion failed for " + fqcn, ex.getMessage());
assertInstanceOf(AiSuggestionException.class, ex.getCause());
@@ -188,4 +200,4 @@ class OllamaClientTest {
private static HttpResponse.BodyHandler anyVoidBodyHandler() {
return any(HttpResponse.BodyHandler.class);
}
-}
+}
\ No newline at end of file
diff --git a/src/test/java/org/egothor/methodatlas/ai/OpenAiCompatibleClientTest.java b/src/test/java/org/egothor/methodatlas/ai/OpenAiCompatibleClientTest.java
index 303894b..ebefe72 100644
--- a/src/test/java/org/egothor/methodatlas/ai/OpenAiCompatibleClientTest.java
+++ b/src/test/java/org/egothor/methodatlas/ai/OpenAiCompatibleClientTest.java
@@ -54,6 +54,13 @@ class OpenAiCompatibleClientTest {
}
""";
String taxonomyText = "security, auth, access-control";
+ List targetMethods = List
+ .of(new PromptBuilder.TargetMethod("shouldAllowOwnerToReadOwnStatement", null, null),
+ new PromptBuilder.TargetMethod("shouldAllowAdministratorToReadAnyStatement", null, null),
+ new PromptBuilder.TargetMethod("shouldDenyForeignUserFromReadingAnotherUsersStatement", null,
+ null),
+ new PromptBuilder.TargetMethod("shouldRejectUnauthenticatedRequest", null, null),
+ new PromptBuilder.TargetMethod("shouldRenderFriendlyAccountLabel", null, null));
String responseBody = """
{
@@ -74,7 +81,7 @@ class OpenAiCompatibleClientTest {
.baseUrl("https://api.openai.com").apiKey("sk-test-value").build();
OpenAiCompatibleClient client = new OpenAiCompatibleClient(options);
- AiClassSuggestion suggestion = client.suggestForClass(fqcn, classSource, taxonomyText);
+ AiClassSuggestion suggestion = client.suggestForClass(fqcn, classSource, taxonomyText, targetMethods);
assertEquals(fqcn, suggestion.className());
assertEquals(Boolean.TRUE, suggestion.classSecurityRelevant());
@@ -100,11 +107,13 @@ class OpenAiCompatibleClientTest {
String requestBody = capturedBody.get();
assertNotNull(requestBody);
assertTrue(requestBody.contains("\"model\":\"gpt-4o-mini\""));
- assertTrue(requestBody.contains("You are a precise software security classification engine."));
- assertTrue(requestBody.contains("You classify JUnit 5 tests and return strict JSON only."));
assertTrue(requestBody.contains("FQCN: " + fqcn));
assertTrue(requestBody.contains("AccessControlServiceTest"));
+ assertTrue(requestBody.contains("shouldAllowOwnerToReadOwnStatement"));
+ assertTrue(requestBody.contains("shouldAllowAdministratorToReadAnyStatement"));
+ assertTrue(requestBody.contains("shouldDenyForeignUserFromReadingAnotherUsersStatement"));
assertTrue(requestBody.contains("shouldRejectUnauthenticatedRequest"));
+ assertTrue(requestBody.contains("shouldRenderFriendlyAccountLabel"));
assertTrue(requestBody.contains(taxonomyText));
assertTrue(requestBody.contains("\"temperature\":0.0"));
}
@@ -126,13 +135,19 @@ class OpenAiCompatibleClientTest {
}
""";
+ List targetMethods = List.of(
+ new PromptBuilder.TargetMethod("shouldWriteAuditEventForPrivilegeChange", null, null),
+ new PromptBuilder.TargetMethod("shouldNotLogRawBearerToken", null, null),
+ new PromptBuilder.TargetMethod("shouldNotLogPlaintextPasswordOnAuthenticationFailure", null, null),
+ new PromptBuilder.TargetMethod("shouldFormatHumanReadableSupportMessage", null, null));
+
try (MockedConstruction mocked = mockHttpSupport(mapper, responseBody, null)) {
AiOptions options = AiOptions.builder().enabled(true).provider(AiProvider.OPENROUTER)
.modelName("openai/gpt-4o-mini").baseUrl("https://openrouter.ai/api").apiKey("or-test-key").build();
OpenAiCompatibleClient client = new OpenAiCompatibleClient(options);
AiClassSuggestion suggestion = client.suggestForClass("com.acme.audit.AuditLoggingTest",
- "class AuditLoggingTest {}", "security, logging");
+ "class AuditLoggingTest {}", "security, logging", targetMethods);
assertEquals("com.acme.audit.AuditLoggingTest", suggestion.className());
@@ -157,6 +172,12 @@ class OpenAiCompatibleClientTest {
}
""";
+ List targetMethods = List.of(
+ new PromptBuilder.TargetMethod("shouldWriteAuditEventForPrivilegeChange", null, null),
+ new PromptBuilder.TargetMethod("shouldNotLogRawBearerToken", null, null),
+ new PromptBuilder.TargetMethod("shouldNotLogPlaintextPasswordOnAuthenticationFailure", null, null),
+ new PromptBuilder.TargetMethod("shouldFormatHumanReadableSupportMessage", null, null));
+
try (MockedConstruction mocked = mockHttpSupport(mapper, responseBody, null)) {
AiOptions options = AiOptions.builder().enabled(true).provider(AiProvider.OPENAI).apiKey("sk-test-value")
.build();
@@ -164,7 +185,8 @@ class OpenAiCompatibleClientTest {
OpenAiCompatibleClient client = new OpenAiCompatibleClient(options);
AiSuggestionException ex = org.junit.jupiter.api.Assertions.assertThrows(AiSuggestionException.class,
- () -> client.suggestForClass(fqcn, "class AuditLoggingTest {}", "security, logging"));
+ () -> client.suggestForClass(fqcn, "class AuditLoggingTest {}", "security, logging",
+ targetMethods));
assertEquals("OpenAI-compatible suggestion failed for " + fqcn, ex.getMessage());
assertInstanceOf(AiSuggestionException.class, ex.getCause());
@@ -189,6 +211,12 @@ class OpenAiCompatibleClientTest {
}
""";
+ List targetMethods = List.of(
+ new PromptBuilder.TargetMethod("shouldWriteAuditEventForPrivilegeChange", null, null),
+ new PromptBuilder.TargetMethod("shouldNotLogRawBearerToken", null, null),
+ new PromptBuilder.TargetMethod("shouldNotLogPlaintextPasswordOnAuthenticationFailure", null, null),
+ new PromptBuilder.TargetMethod("shouldFormatHumanReadableSupportMessage", null, null));
+
try (MockedConstruction mocked = mockHttpSupport(mapper, responseBody, null)) {
AiOptions options = AiOptions.builder().enabled(true).provider(AiProvider.OPENAI).apiKey("sk-test-value")
.build();
@@ -196,7 +224,8 @@ class OpenAiCompatibleClientTest {
OpenAiCompatibleClient client = new OpenAiCompatibleClient(options);
AiSuggestionException ex = org.junit.jupiter.api.Assertions.assertThrows(AiSuggestionException.class,
- () -> client.suggestForClass(fqcn, "class AuditLoggingTest {}", "security, logging"));
+ () -> client.suggestForClass(fqcn, "class AuditLoggingTest {}", "security, logging",
+ targetMethods));
assertEquals("OpenAI-compatible suggestion failed for " + fqcn, ex.getMessage());
assertInstanceOf(AiSuggestionException.class, ex.getCause());
diff --git a/src/test/java/org/egothor/methodatlas/ai/PromptBuilderTest.java b/src/test/java/org/egothor/methodatlas/ai/PromptBuilderTest.java
index 404db43..629a61f 100644
--- a/src/test/java/org/egothor/methodatlas/ai/PromptBuilderTest.java
+++ b/src/test/java/org/egothor/methodatlas/ai/PromptBuilderTest.java
@@ -1,8 +1,11 @@
package org.egothor.methodatlas.ai;
import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
+import java.util.List;
+
import org.junit.jupiter.api.Test;
class PromptBuilderTest {
@@ -33,22 +36,31 @@ class PromptBuilderTest {
- logging
""";
- String prompt = PromptBuilder.build(fqcn, classSource, taxonomyText);
+ List targetMethods = List.of(
+ new PromptBuilder.TargetMethod("shouldRejectUnauthenticatedRequest", 8, 8),
+ new PromptBuilder.TargetMethod("shouldAllowOwnerToReadOwnStatement", 11, 11));
+
+ String prompt = PromptBuilder.build(fqcn, classSource, taxonomyText, targetMethods);
assertTrue(prompt.contains("FQCN: " + fqcn));
assertTrue(prompt.contains(classSource));
assertTrue(prompt.contains(taxonomyText));
+ assertTrue(prompt.contains("- shouldRejectUnauthenticatedRequest [lines 8-8]"));
+ assertTrue(prompt.contains("- shouldAllowOwnerToReadOwnStatement [lines 11-11]"));
}
@Test
void build_containsExpectedTaskInstructions() {
String prompt = PromptBuilder.build("com.acme.audit.AuditLoggingTest", "class AuditLoggingTest {}",
- "security, logging");
+ "security, logging",
+ List.of(new PromptBuilder.TargetMethod("shouldWriteAuditEventForPrivilegeChange", null, null)));
assertTrue(prompt.contains("You are analyzing a single JUnit 5 test class and suggesting security tags."));
assertTrue(prompt.contains("- Analyze the WHOLE class for context."));
- assertTrue(prompt.contains("- Return per-method suggestions for JUnit test methods only."));
+ assertTrue(prompt.contains("- Classify ONLY the methods explicitly listed in TARGET TEST METHODS."));
assertTrue(prompt.contains("- Do not invent methods that do not exist."));
+ assertTrue(prompt.contains(
+ "- Do not classify helper methods, lifecycle methods, nested classes, or any method not listed."));
assertTrue(prompt.contains("- Be conservative."));
assertTrue(prompt.contains("- If uncertain, classify the method as securityRelevant=false."));
}
@@ -56,7 +68,8 @@ class PromptBuilderTest {
@Test
void build_containsClosedTaxonomyRules() {
String prompt = PromptBuilder.build("com.acme.storage.PathTraversalValidationTest",
- "class PathTraversalValidationTest {}", "security, input-validation, injection");
+ "class PathTraversalValidationTest {}", "security, input-validation, injection",
+ List.of(new PromptBuilder.TargetMethod("shouldRejectRelativePathTraversalSequence", null, null)));
assertTrue(prompt.contains("Tags must come only from this closed set:"));
assertTrue(prompt.contains(
@@ -68,9 +81,10 @@ class PromptBuilderTest {
@Test
void build_containsDisplayNameRules() {
String prompt = PromptBuilder.build("com.acme.security.AccessControlServiceTest",
- "class AccessControlServiceTest {}", "security, auth, access-control");
+ "class AccessControlServiceTest {}", "security, auth, access-control",
+ List.of(new PromptBuilder.TargetMethod("shouldRejectUnauthenticatedRequest", null, null)));
- assertTrue(prompt.contains("displayName must be null when securityRelevant=false."));
+ assertTrue(prompt.contains("If securityRelevant=false, displayName must be null."));
assertTrue(prompt.contains("If securityRelevant=true, displayName must match:"));
assertTrue(prompt.contains("SECURITY: - "));
}
@@ -78,7 +92,8 @@ class PromptBuilderTest {
@Test
void build_containsJsonShapeContract() {
String prompt = PromptBuilder.build("com.acme.audit.AuditLoggingTest", "class AuditLoggingTest {}",
- "security, logging");
+ "security, logging",
+ List.of(new PromptBuilder.TargetMethod("shouldWriteAuditEventForPrivilegeChange", null, null)));
assertTrue(prompt.contains("JSON SHAPE"));
assertTrue(prompt.contains("\"className\": \"string\""));
@@ -105,10 +120,24 @@ class PromptBuilderTest {
""";
String prompt = PromptBuilder.build("com.acme.storage.PathTraversalValidationTest", classSource,
- "security, input-validation, injection");
+ "security, input-validation, injection",
+ List.of(new PromptBuilder.TargetMethod("shouldRejectRelativePathTraversalSequence", 3, 5)));
assertTrue(prompt.contains("String userInput = \"../etc/passwd\";"));
assertTrue(prompt.contains("void shouldRejectRelativePathTraversalSequence()"));
+ assertTrue(prompt.contains("- shouldRejectRelativePathTraversalSequence [lines 3-5]"));
+ }
+
+ @Test
+ void build_includesExpectedMethodNamesConstraint() {
+ String prompt = PromptBuilder.build("com.acme.tests.SampleOneTest", "class SampleOneTest {}",
+ "security, crypto", List.of(new PromptBuilder.TargetMethod("alpha", 1, 1),
+ new PromptBuilder.TargetMethod("beta", 2, 2), new PromptBuilder.TargetMethod("gamma", 3, 3)));
+
+ assertTrue(prompt.contains("- methodName values in the output must exactly match one of:"));
+ assertTrue(prompt.contains("[\"alpha\", \"beta\", \"gamma\"]"));
+ assertTrue(prompt.contains("- Do not omit any listed method."));
+ assertTrue(prompt.contains("- Do not include any additional methods."));
}
@Test
@@ -116,10 +145,19 @@ class PromptBuilderTest {
String fqcn = "com.example.X";
String source = "class X {}";
String taxonomy = "security, logging";
+ List targetMethods = List.of(new PromptBuilder.TargetMethod("alpha", null, null));
- String prompt1 = PromptBuilder.build(fqcn, source, taxonomy);
- String prompt2 = PromptBuilder.build(fqcn, source, taxonomy);
+ String prompt1 = PromptBuilder.build(fqcn, source, taxonomy, targetMethods);
+ String prompt2 = PromptBuilder.build(fqcn, source, taxonomy, targetMethods);
assertEquals(prompt1, prompt2);
}
+
+ @Test
+ void build_rejectsEmptyTargetMethods() {
+ IllegalArgumentException ex = assertThrows(IllegalArgumentException.class,
+ () -> PromptBuilder.build("com.example.X", "class X {}", "security", List.of()));
+
+ assertEquals("targetMethods must not be empty", ex.getMessage());
+ }
}
\ No newline at end of file