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> summaries = service.buildVerifierTraceSummary("session-1", "application-logs point to timeout"); assertEquals(2, summaries.size()); Map 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 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> summaries = service.buildVerifierTraceSummary("session-2", "连接池耗尽 HighCPUUsage"); Map 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 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> summaries = service.buildVerifierTraceSummary("session-3", "payment-service timeout"); Map 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")); } }