fix: 同步工具审批过期状态
- 以条件更新避免过期任务覆盖并发人工审批 - 将过期结果同步到本地或远程运行节点 - 补充跨节点命令与过期任务回归测试
This commit is contained in:
@@ -87,6 +87,32 @@ public class AgentRuntimeCommandConsumerTest {
|
||||
Assert.assertNull(resultRegistry.lastFailureCommandId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证审批过期命令走专用运行时入口,不重复消费持久化 pending。
|
||||
*
|
||||
* @throws Exception 消息序列化异常
|
||||
*/
|
||||
@Test
|
||||
public void consumerShouldHandleExpireCommand() throws Exception {
|
||||
AgentRuntimeProperties properties = new AgentRuntimeProperties();
|
||||
properties.setInstanceId("node-a");
|
||||
MQProperties mqProperties = new MQProperties();
|
||||
RecordingAgentRunService service = new RecordingAgentRunService();
|
||||
RecordingCommandResultRegistry resultRegistry = new RecordingCommandResultRegistry();
|
||||
AgentRuntimeCommandConsumer consumer =
|
||||
new AgentRuntimeCommandConsumer(new ObjectMapper(), properties, mqProperties, service, resultRegistry);
|
||||
AgentRuntimeCommandMessage command = command("cmd-expire", "node-a");
|
||||
command.setAction(AgentRuntimeCommandAction.EXPIRE);
|
||||
command.setReason("expired");
|
||||
|
||||
consumer.handle(List.of(message(command)));
|
||||
|
||||
Assert.assertEquals(1, service.expireCount);
|
||||
Assert.assertEquals("request-cmd-expire", service.lastRequestId);
|
||||
Assert.assertEquals("expired", service.lastReason);
|
||||
Assert.assertEquals("cmd-expire", resultRegistry.lastSuccessCommandId);
|
||||
}
|
||||
|
||||
private AgentRuntimeCommandMessage command(String commandId, String targetNodeId) {
|
||||
AgentRuntimeCommandMessage command = new AgentRuntimeCommandMessage();
|
||||
command.setCommandId(commandId);
|
||||
@@ -109,13 +135,22 @@ public class AgentRuntimeCommandConsumerTest {
|
||||
private static final class RecordingAgentRunService extends AgentRunService {
|
||||
|
||||
private int approveCount;
|
||||
private int expireCount;
|
||||
private String lastRequestId;
|
||||
private String lastReason;
|
||||
|
||||
@Override
|
||||
public void approveRuntimeLocal(String requestId, String resumeToken, BigInteger operatorId, String userId) {
|
||||
approveCount++;
|
||||
lastRequestId = requestId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void expireApprovalLocal(String requestId, String resumeToken, String reason) {
|
||||
expireCount++;
|
||||
lastRequestId = requestId;
|
||||
lastReason = reason;
|
||||
}
|
||||
}
|
||||
|
||||
private static class RecordingCommandResultRegistry extends AgentRuntimeCommandResultRegistry {
|
||||
|
||||
@@ -685,6 +685,31 @@ public class AgentRunServiceDraftAndHitlTest {
|
||||
Assert.assertEquals("request-remote-approve", commandProducer.lastRequestId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证审批过期时会向远程 owner 投递专用命令。
|
||||
*
|
||||
* @throws Exception 运行态处理失败时抛出
|
||||
*/
|
||||
@Test
|
||||
public void expireApprovalShouldDispatchRemoteWhenOwnerIsRemoteNode() throws Exception {
|
||||
AgentRunService service = new AgentRunService();
|
||||
RecordingRouteRegistry routeRegistry = new RecordingRouteRegistry("node-b");
|
||||
routeRegistry.ownerNode = "node-a";
|
||||
routeRegistry.ownerBootId = "boot-a";
|
||||
routeRegistry.currentOwnerBootId = "boot-a";
|
||||
routeRegistry.nodeAlive = true;
|
||||
RecordingCommandProducer commandProducer = new RecordingCommandProducer();
|
||||
setField(service, "agentRunRegistry", new AgentRunRegistry());
|
||||
setField(service, "agentRuntimeRouteRegistry", routeRegistry);
|
||||
setField(service, "agentRuntimeCommandProducer", commandProducer);
|
||||
|
||||
service.expireApproval("request-remote-expire", "token-remote-expire");
|
||||
|
||||
Assert.assertEquals(1, commandProducer.expireCount);
|
||||
Assert.assertEquals("node-a", commandProducer.lastTargetNodeId);
|
||||
Assert.assertEquals("request-remote-expire", commandProducer.lastRequestId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证 owner 缺失时明确失败。
|
||||
*
|
||||
@@ -1073,6 +1098,7 @@ public class AgentRunServiceDraftAndHitlTest {
|
||||
private static class RecordingCommandProducer extends AgentRuntimeCommandProducer {
|
||||
|
||||
private int approveCount;
|
||||
private int expireCount;
|
||||
private String lastTargetNodeId;
|
||||
private String lastRequestId;
|
||||
|
||||
@@ -1086,6 +1112,16 @@ public class AgentRunServiceDraftAndHitlTest {
|
||||
lastTargetNodeId = targetNodeId;
|
||||
lastRequestId = requestId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sendExpire(String targetNodeId,
|
||||
String requestId,
|
||||
String resumeToken,
|
||||
String reason) {
|
||||
expireCount++;
|
||||
lastTargetNodeId = targetNodeId;
|
||||
lastRequestId = requestId;
|
||||
}
|
||||
}
|
||||
|
||||
private static class RecordingAgentRuntimeFactory implements AgentRuntimeFactory {
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
package tech.easyflow.agent.runtime.hitl;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.mockito.Mockito;
|
||||
import tech.easyflow.agent.entity.AgentHitlPending;
|
||||
import tech.easyflow.agent.runtime.AgentRunService;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* {@link AgentHitlPendingExpirationTask} 回归测试。
|
||||
*/
|
||||
public class AgentHitlPendingExpirationTaskTest {
|
||||
|
||||
/**
|
||||
* 验证数据库审批过期后通知所有运行节点,单条通知失败不阻断同批其他记录。
|
||||
*/
|
||||
@Test
|
||||
public void expirePendingShouldNotifyEveryRuntime() {
|
||||
AgentHitlPendingService pendingService = Mockito.mock(AgentHitlPendingService.class);
|
||||
AgentRunService runService = Mockito.mock(AgentRunService.class);
|
||||
AgentHitlPending first = pending("request-1", "token-1");
|
||||
AgentHitlPending second = pending("request-2", "token-2");
|
||||
Mockito.when(pendingService.expirePending(100)).thenReturn(List.of(first, second));
|
||||
Mockito.doThrow(new RuntimeException("owner unavailable"))
|
||||
.when(runService).expireApproval("request-1", "token-1");
|
||||
AgentHitlPendingExpirationTask task =
|
||||
new AgentHitlPendingExpirationTask(pendingService, runService);
|
||||
|
||||
task.expirePending();
|
||||
|
||||
Mockito.verify(runService).expireApproval("request-1", "token-1");
|
||||
Mockito.verify(runService).expireApproval("request-2", "token-2");
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建已过期审批记录。
|
||||
*
|
||||
* @param requestId 请求 ID
|
||||
* @param resumeToken 恢复令牌
|
||||
* @return 审批记录
|
||||
*/
|
||||
private AgentHitlPending pending(String requestId, String resumeToken) {
|
||||
AgentHitlPending pending = new AgentHitlPending();
|
||||
pending.setRequestId(requestId);
|
||||
pending.setResumeToken(resumeToken);
|
||||
pending.setStatus(AgentHitlPendingStatus.EXPIRED.name());
|
||||
return pending;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
package tech.easyflow.agent.runtime.hitl;
|
||||
|
||||
import com.mybatisflex.core.query.QueryWrapper;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
import org.mockito.Mockito;
|
||||
import tech.easyflow.agent.config.AgentRuntimeProperties;
|
||||
import tech.easyflow.agent.entity.AgentHitlPending;
|
||||
import tech.easyflow.agent.mapper.AgentHitlPendingMapper;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* {@link AgentHitlPendingServiceImpl} 回归测试。
|
||||
*/
|
||||
public class AgentHitlPendingServiceImplTest {
|
||||
|
||||
/**
|
||||
* 验证过期扫描只返回成功从 PENDING 原子更新为 EXPIRED 的记录。
|
||||
*/
|
||||
@Test
|
||||
public void expirePendingShouldIgnoreConcurrentlyConsumedRecord() {
|
||||
AgentHitlPendingMapper mapper = Mockito.mock(AgentHitlPendingMapper.class);
|
||||
AgentHitlPending first = pending(BigInteger.ONE, "request-1", "token-1");
|
||||
AgentHitlPending concurrentlyApproved = pending(BigInteger.TWO, "request-2", "token-2");
|
||||
Mockito.when(mapper.selectListByQuery(Mockito.any(QueryWrapper.class)))
|
||||
.thenReturn(List.of(first, concurrentlyApproved));
|
||||
Mockito.when(mapper.updateByQuery(
|
||||
Mockito.any(AgentHitlPending.class), Mockito.any(QueryWrapper.class)))
|
||||
.thenReturn(1, 0);
|
||||
AgentHitlPendingServiceImpl service =
|
||||
new AgentHitlPendingServiceImpl(mapper, new AgentRuntimeProperties());
|
||||
|
||||
List<AgentHitlPending> expired = service.expirePending(100);
|
||||
|
||||
Assert.assertEquals(1, expired.size());
|
||||
Assert.assertSame(first, expired.get(0));
|
||||
Assert.assertEquals(AgentHitlPendingStatus.EXPIRED.name(), first.getStatus());
|
||||
Assert.assertEquals(AgentHitlPendingStatus.PENDING.name(), concurrentlyApproved.getStatus());
|
||||
Mockito.verify(mapper, Mockito.times(2)).updateByQuery(
|
||||
Mockito.any(AgentHitlPending.class), Mockito.any(QueryWrapper.class));
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建待过期审批记录。
|
||||
*
|
||||
* @param id 记录 ID
|
||||
* @param requestId 请求 ID
|
||||
* @param resumeToken 恢复令牌
|
||||
* @return 审批记录
|
||||
*/
|
||||
private AgentHitlPending pending(BigInteger id, String requestId, String resumeToken) {
|
||||
AgentHitlPending pending = new AgentHitlPending();
|
||||
pending.setId(id);
|
||||
pending.setRequestId(requestId);
|
||||
pending.setResumeToken(resumeToken);
|
||||
pending.setStatus(AgentHitlPendingStatus.PENDING.name());
|
||||
pending.setIsDeleted(0);
|
||||
return pending;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user