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 ExecutorGatekeeperServiceTest { @Test void ruleCatalogLoadsDefaultMetadata() { GatekeeperRuleCatalog catalog = GatekeeperRuleCatalog.loadDefault(new com.fasterxml.jackson.databind.ObjectMapper()); assertEquals("gatekeeper-rules-v1", catalog.version()); assertFalse(catalog.auditRules().isEmpty()); assertTrue(catalog.auditRules().stream() .anyMatch(rule -> "evidence.raw_path".equals(rule.get("id")))); assertEquals(0.5, catalog.doubleParameter("evidence.excerpt_mismatch", "min_token_overlap", 0.0), 0.001); } @Test void validatePassesForExecutorEvidenceV2WithMatchingInvocation() { 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%, duration=25m") )); ExecutorGatekeeperService service = new ExecutorGatekeeperService(repository); Map 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")); assertRuleAudit(result); assertTrue(((List) result.get("failed_rules")).isEmpty()); } @Test void validatePassesForNoEvidenceReference() { ToolInvocationRepository repository = mock(ToolInvocationRepository.class); when(repository.findBySessionIdOrderByIdAsc("session-1")).thenReturn(List.of( invocation(101L, "query_logs", "$.no_evidence", "query_logs returned no evidence; evidence_status=no_evidence; query=inventory-service HikariCP; topic=application-logs; total=0; message=未找到匹配的日志") )); ExecutorGatekeeperService service = new ExecutorGatekeeperService(repository); Map result = service.validate("session-1", validOutput(101L, "query_logs", "$.no_evidence", "query_logs returned no evidence; query=inventory-service HikariCP; total=0; evidence_status=no_evidence"), Map.of("status", "valid")); assertEquals("pass", result.get("status")); assertEquals("none", result.get("severity")); assertRuleAudit(result); assertTrue(((List) result.get("failed_rules")).isEmpty()); } @Test void validateBackfillsNoEvidenceInvocationByRawPathWhenToolHasMultipleCalls() { ToolInvocationRepository repository = mock(ToolInvocationRepository.class); when(repository.findBySessionIdOrderByIdAsc("session-1")).thenReturn(List.of( invocation(101L, "query_logs", "$.no_evidence", "query_logs returned no evidence; evidence_status=no_evidence; query=inventory-service HikariCP; total=0; message=未找到匹配的日志"), invocation(102L, "query_logs", "$.logs[0]", "order-service HikariCP active=50/50 waiting=32") )); ExecutorGatekeeperService service = new ExecutorGatekeeperService(repository); Map result = service.validate("session-1", validOutput(null, "query_logs", "$.no_evidence", "query_logs returned no evidence; query=inventory-service HikariCP; total=0; evidence_status=no_evidence", "negative_observation"), Map.of("status", "valid")); assertEquals("pass", result.get("status")); assertEquals("none", result.get("severity")); assertTrue(((List) result.get("failed_rules")).isEmpty()); assertEquals("evidence.invocation_auto_backfill_by_raw_path", ((Map) ((List) result.get("warnings")).get(0)).get("rule")); } @Test void validateBackfillsNoEvidenceInvocationByExcerptWhenRawPathIsRepeated() { ToolInvocationRepository repository = mock(ToolInvocationRepository.class); when(repository.findBySessionIdOrderByIdAsc("session-1")).thenReturn(List.of( invocation(101L, "query_logs", "$.no_evidence", "query_logs returned no evidence; evidence_status=no_evidence; query=service:inventory-service AND HikariCP; total=0; message=未找到匹配的日志"), invocation(102L, "query_logs", "$.no_evidence", "query_logs returned no evidence; evidence_status=no_evidence; query=inventory-service; total=0; message=未找到匹配的日志") )); ExecutorGatekeeperService service = new ExecutorGatekeeperService(repository); Map result = service.validate("session-1", validOutput(null, "query_logs", "$.no_evidence", "query_logs returned no evidence; query=service:inventory-service AND HikariCP; total=0; evidence_status=no_evidence", "negative_observation"), Map.of("status", "valid")); assertEquals("pass", result.get("status")); assertEquals("none", result.get("severity")); assertEquals(101L, ((Map) ((List) result.get("checked_bindings")).get(0)).get("source_invocation_id")); } @Test void validateRejectsNoEvidenceReferenceWhenExcerptClaimsPositiveEvidence() { ToolInvocationRepository repository = mock(ToolInvocationRepository.class); when(repository.findBySessionIdOrderByIdAsc("session-1")).thenReturn(List.of( invocation(101L, "query_logs", "$.no_evidence", "query_logs returned no evidence; evidence_status=no_evidence; query=inventory-service HikariCP; total=0; message=未找到匹配的日志") )); ExecutorGatekeeperService service = new ExecutorGatekeeperService(repository); Map result = service.validate("session-1", validOutput(101L, "query_logs", "$.no_evidence", "HikariCP active=50/50 waiting=32"), Map.of("status", "valid")); assertEquals("fail", result.get("status")); assertEquals("reject", result.get("severity")); assertRuleAudit(result); assertTrue(((List) result.get("failed_rules")).contains("evidence.excerpt_mismatch")); } @Test void validateRejectsPositiveBindingOnNegativeObservation() { ToolInvocationRepository repository = mock(ToolInvocationRepository.class); when(repository.findBySessionIdOrderByIdAsc("session-1")).thenReturn(List.of( invocation(101L, "query_logs", "$.logs[0]", "order-service HikariCP active=50/50 waiting=32") )); ExecutorGatekeeperService service = new ExecutorGatekeeperService(repository); Map result = service.validate("session-1", validOutput(101L, "query_logs", "$.logs[0]", "order-service HikariCP active=50/50 waiting=32", "negative_observation"), 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 validateFailsWhenRemovedFieldsArePresent() { ToolInvocationRepository repository = mock(ToolInvocationRepository.class); when(repository.findBySessionIdOrderByIdAsc("session-1")).thenReturn(List.of()); ExecutorGatekeeperService service = new ExecutorGatekeeperService(repository); Map output = validOutput(101L, "query_metrics", "$.alerts[0]", "cpu=92"); output.put("user_facing_answer", "旧版最终答案"); Map 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")); } @Test void validateFailsForFabricatedInvocationId() { 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 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")); assertRuleAudit(result); assertTrue(((List) result.get("failed_rules")).contains("evidence.invocation_ref")); } @Test void failResultIncludesRuleAuditMetadata() { ToolInvocationRepository repository = mock(ToolInvocationRepository.class); ExecutorGatekeeperService service = new ExecutorGatekeeperService(repository); Map result = service.fail("gatekeeper.internal_error", "gatekeeper", "boom"); assertEquals("fail", result.get("status")); assertEquals("reject", result.get("severity")); assertRuleAudit(result); } @Test void validateFailsForToolNameMismatch() { ToolInvocationRepository repository = mock(ToolInvocationRepository.class); when(repository.findBySessionIdOrderByIdAsc("session-1")).thenReturn(List.of( invocation(101L, "query_logs", "$.logs[0]", "cpu=92") )); ExecutorGatekeeperService service = new ExecutorGatekeeperService(repository); Map 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")); } @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 output = validOutput(101L, "query_metrics", null, "cpu=92"); Map 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 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 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 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 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 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 validOutput(Long invocationId, String toolName, String rawPath, String excerpt) { return validOutput(invocationId, toolName, rawPath, excerpt, "symptom"); } private Map validOutput(Long invocationId, String toolName, String rawPath, String excerpt, String claimType) { Map binding = new java.util.LinkedHashMap<>(); binding.put("source_type", "tool_trace"); binding.put("source_id", "trace-1"); binding.put("tool_name", toolName); if (invocationId != null) { 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( "claim_id", "claim-1", "claim_type", claimType, "claim_text", "连接池 active 达到上限", "support_level", "direct", "evidence_bindings", List.of(binding) )), "hypotheses", List.of(), "recommended_actions", List.of(), "missing_info", List.of() )); } private void assertRuleAudit(Map result) { assertEquals("gatekeeper-rules-v1", result.get("rule_set_version")); assertTrue(result.get("rules") instanceof List); List rules = (List) result.get("rules"); assertFalse(rules.isEmpty()); assertTrue(rules.stream().anyMatch(rule -> rule instanceof Map map && "evidence.raw_path".equals(map.get("id")))); } }