feat(harness): freeze aci tool contracts
This commit is contained in:
@@ -0,0 +1,73 @@
|
||||
package com.superbiz.agent.harness.tool.contract;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.superbiz.agent.harness.contract.EvidenceStatus;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
|
||||
class MysqlToolContractTest {
|
||||
|
||||
private final ObjectMapper objectMapper = new ObjectMapper();
|
||||
|
||||
@Test
|
||||
void freezesLogicalParameterizedRequestWithoutConnectionData() {
|
||||
List<Object> params = new ArrayList<>(List.of("order-123"));
|
||||
MysqlToolRequest request = new MysqlToolRequest(
|
||||
"order_readonly",
|
||||
"SELECT order_id, payment_status FROM biz_order WHERE order_id = ?",
|
||||
params);
|
||||
params.clear();
|
||||
|
||||
JsonNode json = objectMapper.valueToTree(request);
|
||||
assertEquals(Set.of("data_source", "sql", "params"), ToolContractAssertions.fieldNames(json));
|
||||
assertEquals(1, json.path("params").size());
|
||||
assertFalse(json.has("tool_call_id"));
|
||||
assertFalse(json.has("jdbc_url"));
|
||||
assertFalse(json.has("username"));
|
||||
assertFalse(json.has("password"));
|
||||
assertFalse(json.has("timeout"));
|
||||
assertThrows(UnsupportedOperationException.class, () -> request.params().add("other"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void freezesBoundedStructuredRowsWithDeepImmutability() {
|
||||
Map<String, Object> row = new LinkedHashMap<>();
|
||||
row.put("order_id", "order-123");
|
||||
row.put("payment_status", "FAILED");
|
||||
List<Map<String, Object>> rows = new ArrayList<>(List.of(row));
|
||||
List<String> columns = new ArrayList<>(List.of("order_id", "payment_status"));
|
||||
MysqlToolResult result = new MysqlToolResult(
|
||||
EvidenceStatus.EVIDENCE_FOUND,
|
||||
"framework-call-mysql-1",
|
||||
columns,
|
||||
rows,
|
||||
1,
|
||||
false);
|
||||
row.put("secret", "must-not-appear");
|
||||
rows.clear();
|
||||
columns.clear();
|
||||
|
||||
JsonNode json = objectMapper.valueToTree(result);
|
||||
assertEquals(Set.of("evidence_status", "tool_call_id", "columns", "rows",
|
||||
"returned_count", "truncated"), ToolContractAssertions.fieldNames(json));
|
||||
assertEquals("framework-call-mysql-1", json.path("tool_call_id").asText());
|
||||
assertEquals(List.of("order_id", "payment_status"), result.columns());
|
||||
assertEquals(1, json.path("rows").size());
|
||||
assertFalse(json.path("rows").get(0).has("secret"));
|
||||
assertThrows(UnsupportedOperationException.class,
|
||||
() -> result.rows().get(0).put("other", "value"));
|
||||
|
||||
assertEquals("query_mysql", AgentToolContracts.QUERY_MYSQL);
|
||||
ToolContractAssertions.assertImplementationNeutral(AgentToolContracts.QUERY_MYSQL_DESCRIPTION);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
package com.superbiz.agent.harness.tool.contract;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.superbiz.agent.harness.contract.EvidenceStatus;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
|
||||
class QueryLogsToolContractTest {
|
||||
|
||||
private final ObjectMapper objectMapper = new ObjectMapper();
|
||||
|
||||
@Test
|
||||
void freezesLogicalRequestWithoutInfrastructureControls() {
|
||||
JsonNode json = objectMapper.valueToTree(
|
||||
new QueryLogsRequest(LogTopic.APPLICATION, "order-service HikariCP timeout", 30));
|
||||
|
||||
assertEquals(Set.of("topic", "query", "lookback_minutes"), ToolContractAssertions.fieldNames(json));
|
||||
assertEquals("APPLICATION", json.path("topic").asText());
|
||||
assertFalse(json.has("tool_call_id"));
|
||||
assertFalse(json.has("region"));
|
||||
assertFalse(json.has("log_topic"));
|
||||
assertFalse(json.has("limit"));
|
||||
assertEquals(Set.of("APPLICATION", "DATABASE_SLOW_QUERY", "SYSTEM_EVENTS"),
|
||||
java.util.Arrays.stream(LogTopic.values()).map(Enum::name)
|
||||
.collect(java.util.stream.Collectors.toSet()));
|
||||
}
|
||||
|
||||
@Test
|
||||
void freezesMockProvenanceScopeAndBoundedTimeline() {
|
||||
List<LogPattern> patterns = new ArrayList<>(List.of(new LogPattern(
|
||||
84, "2026-07-21T10:00:00Z", "2026-07-21T10:29:00Z",
|
||||
"ERROR", "order-service", "HikariPool connection is not available")));
|
||||
List<LogEvent> events = new ArrayList<>(List.of(new LogEvent(
|
||||
"2026-07-21T10:29:00Z", "ERROR", "order-service",
|
||||
"HikariPool connection is not available")));
|
||||
QueryLogsToolResult result = new QueryLogsToolResult(
|
||||
EvidenceStatus.EVIDENCE_FOUND,
|
||||
"framework-call-log-1",
|
||||
LogSourceKind.MOCK,
|
||||
new LogQueryScope(LogTopic.APPLICATION, "order-service HikariCP timeout",
|
||||
"2026-07-21T10:00:00Z", "2026-07-21T10:30:00Z"),
|
||||
126,
|
||||
1,
|
||||
patterns,
|
||||
events,
|
||||
true);
|
||||
patterns.clear();
|
||||
events.clear();
|
||||
|
||||
JsonNode json = objectMapper.valueToTree(result);
|
||||
assertEquals(Set.of("evidence_status", "tool_call_id", "source_kind", "scope",
|
||||
"match_count", "returned_count", "patterns", "events", "truncated"),
|
||||
ToolContractAssertions.fieldNames(json));
|
||||
assertEquals("framework-call-log-1", json.path("tool_call_id").asText());
|
||||
assertEquals("MOCK", json.path("source_kind").asText());
|
||||
assertEquals(126, json.path("match_count").asLong());
|
||||
assertEquals(1, json.path("returned_count").asInt());
|
||||
assertEquals(1, json.path("patterns").size());
|
||||
assertEquals(1, json.path("events").size());
|
||||
assertEquals(Set.of("topic", "query", "start_time", "end_time"),
|
||||
ToolContractAssertions.fieldNames(json.path("scope")));
|
||||
assertThrows(UnsupportedOperationException.class,
|
||||
() -> result.events().add(new LogEvent(null, null, null, null)));
|
||||
|
||||
assertEquals("query_logs", AgentToolContracts.QUERY_LOGS);
|
||||
ToolContractAssertions.assertImplementationNeutral(AgentToolContracts.QUERY_LOGS_DESCRIPTION);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
package com.superbiz.agent.harness.tool.contract;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.superbiz.agent.harness.contract.EvidenceStatus;
|
||||
import com.superbiz.agent.harness.contract.InvocationStatus;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
|
||||
class RagToolContractTest {
|
||||
|
||||
private final ObjectMapper objectMapper = new ObjectMapper();
|
||||
|
||||
@Test
|
||||
void freezesMinimalRequestAndBoundedEvidenceResult() {
|
||||
RagToolRequest request = new RagToolRequest("支付超时处理方式");
|
||||
JsonNode requestJson = objectMapper.valueToTree(request);
|
||||
|
||||
assertEquals(Set.of("query"), ToolContractAssertions.fieldNames(requestJson));
|
||||
assertFalse(requestJson.has("tool_call_id"));
|
||||
|
||||
List<RagEvidence> source = new ArrayList<>();
|
||||
source.add(new RagEvidence("payment-timeout-guide", "payment-timeout.md",
|
||||
"支付超时排查", "支付系统 > 故障排查", "先检查 ERR_TIMEOUT 发生时间。"));
|
||||
RagToolResult result = new RagToolResult(EvidenceStatus.EVIDENCE_FOUND, "framework-call-rag-1",
|
||||
request.query(), source, 1, false);
|
||||
source.clear();
|
||||
|
||||
JsonNode json = objectMapper.valueToTree(result);
|
||||
assertEquals(Set.of("evidence_status", "tool_call_id", "query", "evidence",
|
||||
"returned_count", "truncated"), ToolContractAssertions.fieldNames(json));
|
||||
assertEquals("EVIDENCE_FOUND", json.path("evidence_status").asText());
|
||||
assertEquals("framework-call-rag-1", json.path("tool_call_id").asText());
|
||||
assertEquals(1, json.path("evidence").size());
|
||||
assertEquals(Set.of("document_id", "source", "title", "breadcrumb", "excerpt"),
|
||||
ToolContractAssertions.fieldNames(json.path("evidence").get(0)));
|
||||
assertFalse(json.has("context_pack"));
|
||||
assertFalse(json.has("retrieval_trace"));
|
||||
assertThrows(UnsupportedOperationException.class,
|
||||
() -> result.evidence().add(new RagEvidence("other", "other.md", null, null, "other")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void keepsLifecycleAndEvidenceStatusNamespacesIndependent() {
|
||||
assertEquals(Set.of("PROJECTING", "READY", "ERROR"), enumNames(InvocationStatus.values()));
|
||||
assertEquals(Set.of("EVIDENCE_FOUND", "NO_EVIDENCE", "ERROR"), enumNames(EvidenceStatus.values()));
|
||||
assertEquals("lookup_knowledge", AgentToolContracts.LOOKUP_KNOWLEDGE);
|
||||
ToolContractAssertions.assertImplementationNeutral(AgentToolContracts.LOOKUP_KNOWLEDGE_DESCRIPTION);
|
||||
}
|
||||
|
||||
private Set<String> enumNames(Enum<?>[] values) {
|
||||
return java.util.Arrays.stream(values).map(Enum::name).collect(java.util.stream.Collectors.toSet());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package com.superbiz.agent.harness.tool.contract;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.Locale;
|
||||
import java.util.Set;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
final class ToolContractAssertions {
|
||||
|
||||
private static final Set<String> INTERNAL_TERMS = Set.of(
|
||||
"milvus", "l0", "l1", "rerank", "region", "topicid", "redis",
|
||||
"jdbc", "credential", "password", "topk", "limit", "trace");
|
||||
|
||||
private ToolContractAssertions() {
|
||||
}
|
||||
|
||||
static Set<String> fieldNames(JsonNode node) {
|
||||
Set<String> names = new HashSet<>();
|
||||
node.fieldNames().forEachRemaining(names::add);
|
||||
return names;
|
||||
}
|
||||
|
||||
static void assertImplementationNeutral(String description) {
|
||||
assertTrue(description.length() < 180, "Tool description must remain concise");
|
||||
String normalized = description.toLowerCase(Locale.ROOT);
|
||||
INTERNAL_TERMS.forEach(term -> assertFalse(normalized.contains(term),
|
||||
() -> "Tool description leaks internal term: " + term));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user