mirror of
https://gitee.com/mateos/mateclaw.git
synced 2026-09-13 03:13:41 +08:00
fix(binding): drop @TableLogic on agent-tool/skill to fix rebind error
This commit is contained in:
parent
cf8a6b9fcb
commit
5aed6f176d
@ -17,6 +17,5 @@ public class AgentSkillBinding {
|
||||
private LocalDateTime createTime;
|
||||
@TableField(fill = FieldFill.INSERT_UPDATE)
|
||||
private LocalDateTime updateTime;
|
||||
@TableLogic
|
||||
private Integer deleted;
|
||||
}
|
||||
|
||||
@ -16,6 +16,5 @@ public class AgentToolBinding {
|
||||
private LocalDateTime createTime;
|
||||
@TableField(fill = FieldFill.INSERT_UPDATE)
|
||||
private LocalDateTime updateTime;
|
||||
@TableLogic
|
||||
private Integer deleted;
|
||||
}
|
||||
|
||||
@ -78,11 +78,6 @@ mybatis-plus:
|
||||
configuration:
|
||||
map-underscore-to-camel-case: true
|
||||
log-impl: org.apache.ibatis.logging.slf4j.Slf4jImpl
|
||||
global-config:
|
||||
db-config:
|
||||
logic-delete-field: deleted
|
||||
logic-delete-value: 1
|
||||
logic-not-delete-value: 0
|
||||
|
||||
# SpringDoc OpenAPI
|
||||
springdoc:
|
||||
|
||||
@ -0,0 +1,10 @@
|
||||
-- V17: Purge soft-deleted rows from binding tables.
|
||||
-- mate_agent_tool / mate_agent_skill previously had @TableLogic on their `deleted`
|
||||
-- column, but the unique indexes uk_agent_tool(agent_id, tool_name) /
|
||||
-- uk_agent_skill(agent_id, skill_id) did not include `deleted`. Any rebind after
|
||||
-- an unbind therefore hit a duplicate-key error because the soft-deleted row
|
||||
-- still occupied the unique slot. Soft-delete has been removed for these tables
|
||||
-- (see AgentToolBinding / AgentSkillBinding) — clear residual deleted=1 rows so
|
||||
-- pre-existing deployments can rebind the affected pairs.
|
||||
DELETE FROM mate_agent_tool WHERE deleted = 1;
|
||||
DELETE FROM mate_agent_skill WHERE deleted = 1;
|
||||
@ -0,0 +1,10 @@
|
||||
-- V17: Purge soft-deleted rows from binding tables.
|
||||
-- mate_agent_tool / mate_agent_skill previously had @TableLogic on their `deleted`
|
||||
-- column, but the unique indexes uk_agent_tool(agent_id, tool_name) /
|
||||
-- uk_agent_skill(agent_id, skill_id) did not include `deleted`. Any rebind after
|
||||
-- an unbind therefore hit a duplicate-key error because the soft-deleted row
|
||||
-- still occupied the unique slot. Soft-delete has been removed for these tables
|
||||
-- (see AgentToolBinding / AgentSkillBinding) — clear residual deleted=1 rows so
|
||||
-- pre-existing deployments can rebind the affected pairs.
|
||||
DELETE FROM mate_agent_tool WHERE deleted = 1;
|
||||
DELETE FROM mate_agent_skill WHERE deleted = 1;
|
||||
@ -0,0 +1,150 @@
|
||||
package vip.mate.agent.binding;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.dao.DuplicateKeyException;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.test.context.TestPropertySource;
|
||||
import vip.mate.MateClawApplication;
|
||||
import vip.mate.agent.binding.model.AgentToolBinding;
|
||||
import vip.mate.agent.binding.service.AgentBindingService;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
/**
|
||||
* 覆盖 issue #8 的重绑定 bug 回归:在去掉 @TableLogic 之前,
|
||||
* bind → unbind → rebind 会因为 uk_agent_tool / uk_agent_skill 唯一索引
|
||||
* 与软删除并存而抛 DuplicateKeyException。本测试断言修复后各条路径都成功,
|
||||
* 同时断言合法的唯一约束仍被保留(不能让修 bug 顺带破坏唯一性)。
|
||||
*/
|
||||
@SpringBootTest(
|
||||
classes = MateClawApplication.class,
|
||||
webEnvironment = SpringBootTest.WebEnvironment.NONE
|
||||
)
|
||||
@TestPropertySource(properties = {
|
||||
"spring.datasource.url=jdbc:h2:mem:binding_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 AgentBindingServiceTest {
|
||||
|
||||
private static final AtomicLong AGENT_ID_SEQ = new AtomicLong(9_000_000L);
|
||||
|
||||
@Autowired
|
||||
private AgentBindingService bindingService;
|
||||
|
||||
@Autowired
|
||||
private JdbcTemplate jdbcTemplate;
|
||||
|
||||
private long agentId;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
// 每个用例用独立 agent_id,互不干扰
|
||||
agentId = AGENT_ID_SEQ.getAndIncrement();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("bindTool → unbindTool → bindTool 同一 (agent, tool) 不抛异常")
|
||||
void rebindToolAfterUnbind() {
|
||||
bindingService.bindTool(agentId, "echo");
|
||||
bindingService.unbindTool(agentId, "echo");
|
||||
assertDoesNotThrow(() -> bindingService.bindTool(agentId, "echo"));
|
||||
|
||||
Set<String> names = bindingService.getBoundToolNames(agentId);
|
||||
assertNotNull(names);
|
||||
assertTrue(names.contains("echo"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("bindSkill → unbindSkill → bindSkill 同一 (agent, skill) 不抛异常")
|
||||
void rebindSkillAfterUnbind() {
|
||||
long skillId = 7_777_001L;
|
||||
bindingService.bindSkill(agentId, skillId);
|
||||
bindingService.unbindSkill(agentId, skillId);
|
||||
assertDoesNotThrow(() -> bindingService.bindSkill(agentId, skillId));
|
||||
|
||||
Set<Long> ids = bindingService.getBoundSkillIds(agentId);
|
||||
assertNotNull(ids);
|
||||
assertTrue(ids.contains(skillId));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("setToolBindings 连续调用两次相同列表不抛异常,状态收敛")
|
||||
void setToolBindingsIsIdempotent() {
|
||||
List<String> desired = List.of("tool_a", "tool_b");
|
||||
bindingService.setToolBindings(agentId, desired);
|
||||
assertDoesNotThrow(() -> bindingService.setToolBindings(agentId, desired));
|
||||
|
||||
Set<String> names = bindingService.getBoundToolNames(agentId);
|
||||
assertNotNull(names);
|
||||
assertEquals(2, names.size());
|
||||
assertTrue(names.containsAll(desired));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("setSkillBindings 连续调用两次相同列表不抛异常,状态收敛")
|
||||
void setSkillBindingsIsIdempotent() {
|
||||
List<Long> desired = List.of(7_777_101L, 7_777_102L);
|
||||
bindingService.setSkillBindings(agentId, desired);
|
||||
assertDoesNotThrow(() -> bindingService.setSkillBindings(agentId, desired));
|
||||
|
||||
Set<Long> ids = bindingService.getBoundSkillIds(agentId);
|
||||
assertNotNull(ids);
|
||||
assertEquals(2, ids.size());
|
||||
assertTrue(ids.containsAll(desired));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("唯一性回归:同一 (agent, tool) 直接 INSERT 第二行仍被唯一索引拦截")
|
||||
void uniqueIndexStillEnforcedForTool() {
|
||||
bindingService.bindTool(agentId, "unique_probe");
|
||||
|
||||
// 绕过 service,直接 INSERT 第二行,断言 DB 层唯一约束仍生效
|
||||
assertThrows(DuplicateKeyException.class, () ->
|
||||
jdbcTemplate.update(
|
||||
"INSERT INTO mate_agent_tool " +
|
||||
"(id, agent_id, tool_name, enabled, create_time, update_time, deleted) " +
|
||||
"VALUES (?, ?, ?, TRUE, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, 0)",
|
||||
System.nanoTime(), agentId, "unique_probe"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("唯一性回归:同一 (agent, skill) 直接 INSERT 第二行仍被唯一索引拦截")
|
||||
void uniqueIndexStillEnforcedForSkill() {
|
||||
long skillId = 7_777_201L;
|
||||
bindingService.bindSkill(agentId, skillId);
|
||||
|
||||
assertThrows(DuplicateKeyException.class, () ->
|
||||
jdbcTemplate.update(
|
||||
"INSERT INTO mate_agent_skill " +
|
||||
"(id, agent_id, skill_id, enabled, create_time, update_time, deleted) " +
|
||||
"VALUES (?, ?, ?, TRUE, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, 0)",
|
||||
System.nanoTime(), agentId, skillId
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("unbindTool 后 DB 里真的没行(物理 delete,不是软删留 deleted=1)")
|
||||
void unbindPhysicallyRemovesRow() {
|
||||
bindingService.bindTool(agentId, "physical_check");
|
||||
bindingService.unbindTool(agentId, "physical_check");
|
||||
|
||||
Integer count = jdbcTemplate.queryForObject(
|
||||
"SELECT COUNT(*) FROM mate_agent_tool WHERE agent_id = ? AND tool_name = ?",
|
||||
Integer.class, agentId, "physical_check"
|
||||
);
|
||||
assertNotNull(count);
|
||||
assertEquals(0, count, "unbind 应该物理删除,而不是软删(软删会留 deleted=1 行,占用唯一索引槽位导致 rebind 失败)");
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user