184 lines
8.7 KiB
Java
184 lines
8.7 KiB
Java
package com.superbiz.agent.service;
|
|
|
|
import com.superbiz.agent.domain.entity.ToolInvocation;
|
|
import com.superbiz.agent.repository.ToolInvocationRepository;
|
|
import org.junit.jupiter.api.Test;
|
|
|
|
import java.util.List;
|
|
import java.util.Map;
|
|
|
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
|
import static org.junit.jupiter.api.Assertions.assertFalse;
|
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
|
import static org.mockito.Mockito.mock;
|
|
import static org.mockito.Mockito.when;
|
|
|
|
class ToolTraceSummaryServiceTest {
|
|
|
|
@Test
|
|
void buildVerifierTraceSummaryTreatsNoEvidenceAsGapWithoutLosingSuccessfulEvidence() {
|
|
ToolInvocationRepository repository = mock(ToolInvocationRepository.class);
|
|
when(repository.findBySessionIdOrderByIdAsc("session-1")).thenReturn(List.of(
|
|
ToolInvocation.builder()
|
|
.id(1L)
|
|
.sessionId("session-1")
|
|
.toolName("query_logs")
|
|
.inputParams("{\"query\":\"timeout\"}")
|
|
.outputPreview("payment timeout stack trace")
|
|
.retrievalDetails("{\"retrieved_domains\":[\"application-logs\"],\"evidence_status\":\"supported\"}")
|
|
.success(true)
|
|
.build(),
|
|
ToolInvocation.builder()
|
|
.id(2L)
|
|
.sessionId("session-1")
|
|
.toolName("query_logs")
|
|
.inputParams("{\"query\":\"timeout\"}")
|
|
.outputPreview("{\"success\":false,\"message\":\"未找到匹配的日志\"}")
|
|
.retrievalDetails("{\"retrieved_domains\":[\"application-logs\"],\"evidence_status\":\"no_evidence\"}")
|
|
.success(true)
|
|
.build(),
|
|
ToolInvocation.builder()
|
|
.id(3L)
|
|
.sessionId("session-1")
|
|
.toolName("query_metrics")
|
|
.inputParams("{\"query\":\"active_prometheus_alerts\"}")
|
|
.errorMessage("prometheus timeout")
|
|
.retrievalDetails("{\"retrieved_domains\":[\"prometheus_alerts\"],\"evidence_status\":\"failed\"}")
|
|
.success(false)
|
|
.build()
|
|
));
|
|
|
|
ToolTraceSummaryService service = new ToolTraceSummaryService(repository);
|
|
|
|
List<Map<String, Object>> summaries = service.buildVerifierTraceSummary("session-1", "application-logs point to timeout");
|
|
|
|
assertEquals(2, summaries.size());
|
|
|
|
Map<String, Object> logsSummary = summaries.stream()
|
|
.filter(item -> "query_logs".equals(item.get("tool_name")))
|
|
.findFirst()
|
|
.orElseThrow();
|
|
assertEquals(Boolean.TRUE, logsSummary.get("success"));
|
|
assertEquals("direct", logsSummary.get("evidence_level"));
|
|
assertEquals(2, logsSummary.get("invocation_count"));
|
|
assertEquals(1, logsSummary.get("no_hit_invocation_count"));
|
|
assertTrue(String.valueOf(logsSummary.get("output_summary")).contains("payment timeout stack trace"));
|
|
|
|
Map<String, Object> metricsSummary = summaries.stream()
|
|
.filter(item -> "query_metrics".equals(item.get("tool_name")))
|
|
.findFirst()
|
|
.orElseThrow();
|
|
assertEquals(Boolean.FALSE, metricsSummary.get("success"));
|
|
assertEquals("none", metricsSummary.get("evidence_level"));
|
|
assertEquals(1, metricsSummary.get("failed_invocation_count"));
|
|
assertTrue(String.valueOf(metricsSummary.get("output_summary")).contains("call failed"));
|
|
}
|
|
|
|
@Test
|
|
void buildVerifierTraceSummaryPreservesConcreteFactsFromTruncatedLogAndMetricRows() {
|
|
ToolInvocationRepository repository = mock(ToolInvocationRepository.class);
|
|
String logPreview = """
|
|
{
|
|
"success" : true,
|
|
"logs" : [ {
|
|
"timestamp" : "2026-07-06 22:15:45",
|
|
"level" : "ERROR",
|
|
"service" : "order-service",
|
|
"message" : "数据库连接池耗尽: Cannot acquire connection from pool, active: 50/50, waiting: 23, timeout: 30000ms"
|
|
} ]
|
|
}
|
|
""";
|
|
String metricPreview = """
|
|
{
|
|
"success" : true,
|
|
"alerts" : [ {
|
|
"alert_name" : "HighCPUUsage",
|
|
"service" : "payment-service",
|
|
"description" : "服务 payment-service 的 CPU 使用率持续超过 80%,当前值为 92%。"
|
|
} ]
|
|
}
|
|
""";
|
|
when(repository.findBySessionIdOrderByIdAsc("session-2")).thenReturn(List.of(
|
|
ToolInvocation.builder()
|
|
.id(10L)
|
|
.sessionId("session-2")
|
|
.toolName("query_logs")
|
|
.inputParams("{\"query\":\"pool\"}")
|
|
.outputPreview(logPreview)
|
|
.retrievalDetails("{\"retrieved_domains\":[\"application-logs\"],\"evidence_status\":\"supported\"}")
|
|
.isTruncated(true)
|
|
.success(true)
|
|
.build(),
|
|
ToolInvocation.builder()
|
|
.id(11L)
|
|
.sessionId("session-2")
|
|
.toolName("query_metrics")
|
|
.inputParams("{\"query\":\"active_prometheus_alerts\"}")
|
|
.outputPreview(metricPreview)
|
|
.retrievalDetails("{\"retrieved_domains\":[\"prometheus_alerts\"],\"evidence_status\":\"supported\"}")
|
|
.isTruncated(true)
|
|
.success(true)
|
|
.build()
|
|
));
|
|
|
|
ToolTraceSummaryService service = new ToolTraceSummaryService(repository);
|
|
|
|
List<Map<String, Object>> summaries = service.buildVerifierTraceSummary("session-2", "连接池耗尽 HighCPUUsage");
|
|
|
|
Map<String, Object> logsSummary = summaries.stream()
|
|
.filter(item -> "query_logs".equals(item.get("tool_name")))
|
|
.findFirst()
|
|
.orElseThrow();
|
|
assertTrue(String.valueOf(logsSummary.get("output_summary")).contains("连接池耗尽"));
|
|
assertTrue(String.valueOf(logsSummary.get("output_summary")).contains("active: 50/50"));
|
|
assertEquals(List.of(10L), logsSummary.get("source_invocation_ids"));
|
|
|
|
Map<String, Object> metricsSummary = summaries.stream()
|
|
.filter(item -> "query_metrics".equals(item.get("tool_name")))
|
|
.findFirst()
|
|
.orElseThrow();
|
|
assertTrue(String.valueOf(metricsSummary.get("output_summary")).contains("HighCPUUsage"));
|
|
assertTrue(String.valueOf(metricsSummary.get("output_summary")).contains("payment-service"));
|
|
}
|
|
|
|
@Test
|
|
void buildVerifierTraceSummaryDoesNotTreatGenericMockLogsAsDirectEvidence() {
|
|
ToolInvocationRepository repository = mock(ToolInvocationRepository.class);
|
|
String genericLogPreview = """
|
|
{
|
|
"success" : true,
|
|
"logs" : [ {
|
|
"timestamp" : "2026-07-06 23:44:41",
|
|
"level" : "ERROR",
|
|
"service" : "generic-service",
|
|
"message" : "日志消息 #0, 查询条件: service:payment-service"
|
|
} ]
|
|
}
|
|
""";
|
|
when(repository.findBySessionIdOrderByIdAsc("session-3")).thenReturn(List.of(
|
|
ToolInvocation.builder()
|
|
.id(20L)
|
|
.sessionId("session-3")
|
|
.toolName("query_logs")
|
|
.inputParams("{\"query\":\"service:payment-service\"}")
|
|
.outputPreview(genericLogPreview)
|
|
.retrievalDetails("{\"retrieved_domains\":[\"system-metrics\"],\"evidence_status\":\"supported\"}")
|
|
.success(true)
|
|
.build()
|
|
));
|
|
|
|
ToolTraceSummaryService service = new ToolTraceSummaryService(repository);
|
|
|
|
List<Map<String, Object>> summaries = service.buildVerifierTraceSummary("session-3", "payment-service timeout");
|
|
|
|
Map<String, Object> logsSummary = summaries.stream()
|
|
.filter(item -> "query_logs".equals(item.get("tool_name")))
|
|
.findFirst()
|
|
.orElseThrow();
|
|
assertEquals(Boolean.FALSE, logsSummary.get("success"));
|
|
assertEquals("none", logsSummary.get("evidence_level"));
|
|
assertEquals(1, logsSummary.get("no_hit_invocation_count"));
|
|
assertTrue(String.valueOf(logsSummary.get("output_summary")).contains("日志消息 #0"));
|
|
}
|
|
}
|