feat: restrict AI tagging to parser-discovered test methods

This commit is contained in:
2026-03-10 20:42:26 +01:00
parent 32ddfa988b
commit a592ce1330
13 changed files with 357 additions and 99 deletions

View File

@@ -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<AiSuggestionEngineImpl> 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<AiSuggestionEngineImpl> 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());
}
}

View File

@@ -34,22 +34,31 @@ class AiSuggestionEngineImplTest {
"SECURITY: authentication - reject unauthenticated request", List.of("security", "auth"),
"The test verifies that anonymous access is rejected.")));
List<PromptBuilder.TargetMethod> 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<AiProviderFactory> 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<PromptBuilder.TargetMethod> 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<AiProviderFactory> 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<PromptBuilder.TargetMethod> 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<AiProviderFactory> 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);
}
}

View File

@@ -82,6 +82,11 @@ class OllamaClientTest {
}
""";
String taxonomyText = "security, input-validation, owasp";
List<PromptBuilder.TargetMethod> 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<PromptBuilder.TargetMethod> 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<Void> anyVoidBodyHandler() {
return any(HttpResponse.BodyHandler.class);
}
}
}

View File

@@ -54,6 +54,13 @@ class OpenAiCompatibleClientTest {
}
""";
String taxonomyText = "security, auth, access-control";
List<PromptBuilder.TargetMethod> 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<PromptBuilder.TargetMethod> 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<HttpSupport> 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<PromptBuilder.TargetMethod> 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<HttpSupport> 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<PromptBuilder.TargetMethod> 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<HttpSupport> 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());

View File

@@ -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<PromptBuilder.TargetMethod> 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: <control/property> - <scenario>"));
}
@@ -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<PromptBuilder.TargetMethod> 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());
}
}