feat(agent): harden verifier evidence references
This commit is contained in:
@@ -18,14 +18,18 @@ class ExecutorGatekeeperServiceTest {
|
||||
void validatePassesForExecutorEvidenceV2WithMatchingInvocation() {
|
||||
ToolInvocationRepository repository = mock(ToolInvocationRepository.class);
|
||||
when(repository.findBySessionIdOrderByIdAsc("session-1")).thenReturn(List.of(
|
||||
ToolInvocation.builder().id(101L).sessionId("session-1").toolName("query_metrics").build()
|
||||
invocation(101L, "query_metrics", "$.alerts[0]",
|
||||
"HighCPUUsage firing, service=payment-service, current=92%, duration=25m")
|
||||
));
|
||||
ExecutorGatekeeperService service = new ExecutorGatekeeperService(repository);
|
||||
|
||||
Map<String, Object> result = service.validate("session-1", validOutput(101L, "query_metrics"),
|
||||
Map<String, Object> result = service.validate("session-1",
|
||||
validOutput(101L, "query_metrics", "$.alerts[0]",
|
||||
"HighCPUUsage firing, service=payment-service, current=92%"),
|
||||
Map.of("status", "valid"));
|
||||
|
||||
assertEquals("pass", result.get("status"));
|
||||
assertEquals("none", result.get("severity"));
|
||||
assertTrue(((List<?>) result.get("failed_rules")).isEmpty());
|
||||
}
|
||||
|
||||
@@ -34,12 +38,13 @@ class ExecutorGatekeeperServiceTest {
|
||||
ToolInvocationRepository repository = mock(ToolInvocationRepository.class);
|
||||
when(repository.findBySessionIdOrderByIdAsc("session-1")).thenReturn(List.of());
|
||||
ExecutorGatekeeperService service = new ExecutorGatekeeperService(repository);
|
||||
Map<String, Object> output = validOutput(101L, "query_metrics");
|
||||
Map<String, Object> output = validOutput(101L, "query_metrics", "$.alerts[0]", "cpu=92");
|
||||
output.put("user_facing_answer", "旧版最终答案");
|
||||
|
||||
Map<String, Object> result = service.validate("session-1", output, Map.of("status", "valid"));
|
||||
|
||||
assertEquals("fail", result.get("status"));
|
||||
assertEquals("reject", result.get("severity"));
|
||||
assertTrue(((List<?>) result.get("failed_rules")).contains("schema.executor_v2"));
|
||||
}
|
||||
|
||||
@@ -47,14 +52,16 @@ class ExecutorGatekeeperServiceTest {
|
||||
void validateFailsForFabricatedInvocationId() {
|
||||
ToolInvocationRepository repository = mock(ToolInvocationRepository.class);
|
||||
when(repository.findBySessionIdOrderByIdAsc("session-1")).thenReturn(List.of(
|
||||
ToolInvocation.builder().id(101L).sessionId("session-1").toolName("query_metrics").build()
|
||||
invocation(101L, "query_metrics", "$.alerts[0]", "cpu=92")
|
||||
));
|
||||
ExecutorGatekeeperService service = new ExecutorGatekeeperService(repository);
|
||||
|
||||
Map<String, Object> result = service.validate("session-1", validOutput(999L, "query_metrics"),
|
||||
Map<String, Object> result = service.validate("session-1",
|
||||
validOutput(999L, "query_metrics", "$.alerts[0]", "cpu=92"),
|
||||
Map.of("status", "valid"));
|
||||
|
||||
assertEquals("fail", result.get("status"));
|
||||
assertEquals("reject", result.get("severity"));
|
||||
assertTrue(((List<?>) result.get("failed_rules")).contains("evidence.invocation_ref"));
|
||||
}
|
||||
|
||||
@@ -62,19 +69,137 @@ class ExecutorGatekeeperServiceTest {
|
||||
void validateFailsForToolNameMismatch() {
|
||||
ToolInvocationRepository repository = mock(ToolInvocationRepository.class);
|
||||
when(repository.findBySessionIdOrderByIdAsc("session-1")).thenReturn(List.of(
|
||||
ToolInvocation.builder().id(101L).sessionId("session-1").toolName("query_logs").build()
|
||||
invocation(101L, "query_logs", "$.logs[0]", "cpu=92")
|
||||
));
|
||||
ExecutorGatekeeperService service = new ExecutorGatekeeperService(repository);
|
||||
|
||||
Map<String, Object> result = service.validate("session-1", validOutput(101L, "query_metrics"),
|
||||
Map<String, Object> result = service.validate("session-1",
|
||||
validOutput(101L, "query_metrics", "$.alerts[0]", "cpu=92"),
|
||||
Map.of("status", "valid"));
|
||||
|
||||
assertEquals("fail", result.get("status"));
|
||||
assertEquals("reject", result.get("severity"));
|
||||
assertTrue(((List<?>) result.get("failed_rules")).contains("evidence.invocation_ref"));
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private Map<String, Object> validOutput(Long invocationId, String toolName) {
|
||||
@Test
|
||||
void validateDowngradesMissingRawPathToLowConfidence() {
|
||||
ToolInvocationRepository repository = mock(ToolInvocationRepository.class);
|
||||
when(repository.findBySessionIdOrderByIdAsc("session-1")).thenReturn(List.of(
|
||||
invocation(101L, "query_metrics", "$.alerts[0]", "cpu=92")
|
||||
));
|
||||
ExecutorGatekeeperService service = new ExecutorGatekeeperService(repository);
|
||||
Map<String, Object> output = validOutput(101L, "query_metrics", null, "cpu=92");
|
||||
|
||||
Map<String, Object> result = service.validate("session-1", output, Map.of("status", "valid"));
|
||||
|
||||
assertEquals("fail", result.get("status"));
|
||||
assertEquals("low_confid", result.get("severity"));
|
||||
assertTrue(((List<?>) result.get("failed_rules")).contains("evidence.raw_path"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void validateRejectsUnknownRawPath() {
|
||||
ToolInvocationRepository repository = mock(ToolInvocationRepository.class);
|
||||
when(repository.findBySessionIdOrderByIdAsc("session-1")).thenReturn(List.of(
|
||||
invocation(101L, "query_metrics", "$.alerts[0]", "cpu=92")
|
||||
));
|
||||
ExecutorGatekeeperService service = new ExecutorGatekeeperService(repository);
|
||||
|
||||
Map<String, Object> result = service.validate("session-1",
|
||||
validOutput(101L, "query_metrics", "$.alerts[99]", "cpu=92"),
|
||||
Map.of("status", "valid"));
|
||||
|
||||
assertEquals("fail", result.get("status"));
|
||||
assertEquals("reject", result.get("severity"));
|
||||
assertTrue(((List<?>) result.get("failed_rules")).contains("evidence.raw_path"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void validateDowngradesOldInvocationWithoutEvidenceRefsToLowConfidence() {
|
||||
ToolInvocationRepository repository = mock(ToolInvocationRepository.class);
|
||||
when(repository.findBySessionIdOrderByIdAsc("session-1")).thenReturn(List.of(
|
||||
ToolInvocation.builder().id(101L).sessionId("session-1").toolName("query_metrics")
|
||||
.retrievalDetails("{\"evidence_status\":\"supported\"}")
|
||||
.build()
|
||||
));
|
||||
ExecutorGatekeeperService service = new ExecutorGatekeeperService(repository);
|
||||
|
||||
Map<String, Object> result = service.validate("session-1",
|
||||
validOutput(101L, "query_metrics", "$.alerts[0]", "cpu=92"),
|
||||
Map.of("status", "valid"));
|
||||
|
||||
assertEquals("fail", result.get("status"));
|
||||
assertEquals("low_confid", result.get("severity"));
|
||||
assertTrue(((List<?>) result.get("failed_rules")).contains("evidence.missing"));
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
void validateRejectsMismatchedExcerpt() {
|
||||
ToolInvocationRepository repository = mock(ToolInvocationRepository.class);
|
||||
when(repository.findBySessionIdOrderByIdAsc("session-1")).thenReturn(List.of(
|
||||
invocation(101L, "query_metrics", "$.alerts[0]", "HighCPUUsage firing service payment-service current 92")
|
||||
));
|
||||
ExecutorGatekeeperService service = new ExecutorGatekeeperService(repository);
|
||||
|
||||
Map<String, Object> result = service.validate("session-1",
|
||||
validOutput(101L, "query_metrics", "$.alerts[0]", "HikariCP active=50/50 waiting=32"),
|
||||
Map.of("status", "valid"));
|
||||
|
||||
assertEquals("fail", result.get("status"));
|
||||
assertEquals("reject", result.get("severity"));
|
||||
assertTrue(((List<?>) result.get("failed_rules")).contains("evidence.excerpt_mismatch"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void validateFailsForRecommendedActionFabricatedInvocationId() {
|
||||
ToolInvocationRepository repository = mock(ToolInvocationRepository.class);
|
||||
when(repository.findBySessionIdOrderByIdAsc("session-1")).thenReturn(List.of(
|
||||
invocation(101L, "query_metrics", "$.alerts[0]", "cpu=92")
|
||||
));
|
||||
ExecutorGatekeeperService service = new ExecutorGatekeeperService(repository);
|
||||
Map<String, Object> output = validOutput(101L, "query_metrics", "$.alerts[0]", "cpu=92");
|
||||
output.put("recommended_actions", List.of(Map.of(
|
||||
"action_text", "restart service",
|
||||
"reason", "alert is firing",
|
||||
"evidence_bindings", List.of(Map.of(
|
||||
"source_type", "tool_trace",
|
||||
"source_id", "trace-1",
|
||||
"tool_name", "query_metrics",
|
||||
"source_invocation_id", 999L,
|
||||
"raw_path", "$.alerts[0]",
|
||||
"evidence_excerpt", "cpu=92"
|
||||
))
|
||||
)));
|
||||
|
||||
Map<String, Object> result = service.validate("session-1", output, Map.of("status", "valid"));
|
||||
|
||||
assertEquals("fail", result.get("status"));
|
||||
assertEquals("reject", result.get("severity"));
|
||||
assertTrue(((List<?>) result.get("failed_rules")).contains("evidence.invocation_ref"));
|
||||
}
|
||||
|
||||
private ToolInvocation invocation(Long id, String toolName, String rawPath, String text) {
|
||||
return ToolInvocation.builder()
|
||||
.id(id)
|
||||
.sessionId("session-1")
|
||||
.toolName(toolName)
|
||||
.retrievalDetails("{\"evidence_refs\":[{\"raw_path\":\"" + rawPath
|
||||
+ "\",\"text\":\"" + text + "\"}]}")
|
||||
.build();
|
||||
}
|
||||
|
||||
private Map<String, Object> validOutput(Long invocationId, String toolName, String rawPath, String excerpt) {
|
||||
Map<String, Object> binding = new java.util.LinkedHashMap<>();
|
||||
binding.put("source_type", "tool_trace");
|
||||
binding.put("source_id", "trace-1");
|
||||
binding.put("tool_name", toolName);
|
||||
binding.put("source_invocation_id", invocationId);
|
||||
if (rawPath != null) {
|
||||
binding.put("raw_path", rawPath);
|
||||
}
|
||||
binding.put("evidence_excerpt", excerpt);
|
||||
return new java.util.LinkedHashMap<>(Map.of(
|
||||
"answer_version", "executor_evidence_v2",
|
||||
"claims", List.of(Map.of(
|
||||
@@ -82,13 +207,7 @@ class ExecutorGatekeeperServiceTest {
|
||||
"claim_type", "symptom",
|
||||
"claim_text", "连接池 active 达到上限",
|
||||
"support_level", "direct",
|
||||
"evidence_bindings", List.of(Map.of(
|
||||
"source_type", "tool_trace",
|
||||
"source_id", "trace-1",
|
||||
"tool_name", toolName,
|
||||
"source_invocation_ids", List.of(invocationId),
|
||||
"evidence_excerpt", "active=50 max=50"
|
||||
))
|
||||
"evidence_bindings", List.of(binding)
|
||||
)),
|
||||
"hypotheses", List.of(),
|
||||
"recommended_actions", List.of(),
|
||||
|
||||
Reference in New Issue
Block a user