mirror of
https://gitee.com/mateos/mateclaw.git
synced 2026-09-13 19:23:42 +08:00
135 lines
5.6 KiB
Java
135 lines
5.6 KiB
Java
package vip.mate.agent;
|
||
|
||
import com.fasterxml.jackson.databind.JsonNode;
|
||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||
import org.junit.jupiter.api.BeforeEach;
|
||
import org.junit.jupiter.api.DisplayName;
|
||
import org.junit.jupiter.api.Test;
|
||
import org.springframework.ai.chat.model.ToolContext;
|
||
import org.springframework.beans.factory.annotation.Autowired;
|
||
import org.springframework.boot.test.context.SpringBootTest;
|
||
import org.springframework.test.context.TestPropertySource;
|
||
import vip.mate.MateClawApplication;
|
||
import vip.mate.agent.binding.service.AgentBindingService;
|
||
import vip.mate.agent.context.ChatOrigin;
|
||
import vip.mate.agent.model.AgentEntity;
|
||
|
||
import java.util.Set;
|
||
import java.util.concurrent.atomic.AtomicLong;
|
||
|
||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||
|
||
/**
|
||
* Exercises the agent authoring tool against a real Spring context so the
|
||
* create-then-bind sequence runs through {@link AgentService} and
|
||
* {@link AgentBindingService} exactly as it would at chat time. Builtin
|
||
* skills are seeded on startup, so skill-name resolution hits real rows.
|
||
*/
|
||
@SpringBootTest(
|
||
classes = MateClawApplication.class,
|
||
webEnvironment = SpringBootTest.WebEnvironment.NONE
|
||
)
|
||
@TestPropertySource(properties = {
|
||
"spring.datasource.url=jdbc:h2:mem:agent_authoring_test_${random.uuid};MODE=MySQL;DATABASE_TO_LOWER=TRUE;CASE_INSENSITIVE_IDENTIFIERS=TRUE;DB_CLOSE_DELAY=-1",
|
||
"spring.ai.dashscope.api-key=test-key",
|
||
"spring.main.web-application-type=none"
|
||
})
|
||
class AgentAuthoringToolTest {
|
||
|
||
private static final AtomicLong WS_SEQ = new AtomicLong(70_000L);
|
||
|
||
@Autowired
|
||
private AgentAuthoringTool tool;
|
||
@Autowired
|
||
private AgentService agentService;
|
||
@Autowired
|
||
private AgentBindingService bindingService;
|
||
|
||
private final ObjectMapper mapper = new ObjectMapper();
|
||
|
||
private long workspaceId;
|
||
|
||
@BeforeEach
|
||
void setUp() {
|
||
workspaceId = WS_SEQ.getAndIncrement();
|
||
}
|
||
|
||
private ToolContext ctxFor(long ws) {
|
||
return ChatOrigin.web("conv-" + ws, "123", ws, null).toToolContext();
|
||
}
|
||
|
||
@Test
|
||
@DisplayName("create_employee 在 ChatOrigin 的 workspace 内创建 Agent,无能力名时继承全局默认")
|
||
void createsGeneralistInOriginWorkspace() throws Exception {
|
||
String json = tool.create_employee(
|
||
"generalist-helper", "general assistant", "You help with anything.",
|
||
null, null, null, null, ctxFor(workspaceId));
|
||
|
||
JsonNode node = mapper.readTree(json);
|
||
long agentId = Long.parseLong(node.get("agentId").asText());
|
||
|
||
AgentEntity created = agentService.getAgent(agentId);
|
||
assertEquals("generalist-helper", created.getName());
|
||
assertEquals(workspaceId, created.getWorkspaceId());
|
||
assertEquals("react", created.getAgentType());
|
||
// Creator attribution parsed from the numeric requesterId.
|
||
assertEquals(123L, created.getCreatorUserId());
|
||
// No bindings declared → inherits global defaults (null sentinel).
|
||
assertNull(bindingService.getBoundSkillIds(agentId));
|
||
assertNull(bindingService.getBoundToolNames(agentId));
|
||
}
|
||
|
||
@Test
|
||
@DisplayName("create_employee 绑定指定的内置技能(按名解析为 id)")
|
||
void bindsRequestedBuiltinSkill() throws Exception {
|
||
String json = tool.create_employee(
|
||
"planner-employee", "planning specialist", "You break goals into plans.",
|
||
"react", null, "[\"make_plan\"]", null, ctxFor(workspaceId));
|
||
|
||
JsonNode node = mapper.readTree(json);
|
||
long agentId = Long.parseLong(node.get("agentId").asText());
|
||
|
||
Set<Long> boundSkills = bindingService.getBoundSkillIds(agentId);
|
||
assertNotNull(boundSkills, "declaring a skill must create a binding set");
|
||
assertEquals(1, boundSkills.size());
|
||
// The summary echoes the bound skill name.
|
||
assertTrue(node.get("skillsBound").toString().contains("make_plan"));
|
||
}
|
||
|
||
@Test
|
||
@DisplayName("create_employee 缺少 workspace 上下文时拒绝执行")
|
||
void rejectsWithoutWorkspace() {
|
||
String result = tool.create_employee(
|
||
"no-ws", "x", "y", null, null, null, null, ChatOrigin.EMPTY.toToolContext());
|
||
assertTrue(result.startsWith("[error]"));
|
||
}
|
||
|
||
@Test
|
||
@DisplayName("create_employee 重名返回友好错误而非抛出")
|
||
void duplicateNameReturnsFriendlyError() {
|
||
ToolContext ctx = ctxFor(workspaceId);
|
||
tool.create_employee("dup-employee", "first", "p", null, null, null, null, ctx);
|
||
String second = tool.create_employee("dup-employee", "second", "p", null, null, null, null, ctx);
|
||
assertTrue(second.startsWith("[error]"), "duplicate name should surface as a friendly error");
|
||
}
|
||
|
||
@Test
|
||
@DisplayName("list_capability_catalog 返回技能与工具清单")
|
||
void catalogReturnsSkillsAndTools() throws Exception {
|
||
String json = tool.list_capability_catalog(ctxFor(workspaceId));
|
||
JsonNode node = mapper.readTree(json);
|
||
assertTrue(node.has("skills"));
|
||
assertTrue(node.has("tools"));
|
||
assertTrue(node.get("skills").isArray());
|
||
// make_plan is a seeded builtin skill, so the catalog must surface it.
|
||
boolean hasMakePlan = false;
|
||
for (JsonNode s : node.get("skills")) {
|
||
if ("make_plan".equals(s.path("name").asText())) { hasMakePlan = true; break; }
|
||
}
|
||
assertTrue(hasMakePlan, "seeded builtin skill make_plan should appear in the catalog");
|
||
}
|
||
}
|