fix: 完善工具审批调用绑定
- 以 toolCallId、工具名称和入参绑定一次性执行授权 - 支持批次审批、重复调用去重及拒绝过期处理 - 补充多工具审批与授权消费回归测试
This commit is contained in:
@@ -56,6 +56,7 @@ import java.util.Set;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.function.BooleanSupplier;
|
||||
|
||||
/**
|
||||
@@ -843,6 +844,9 @@ public class AgentScopeStatefulRuntimeTest {
|
||||
Assert.assertFalse(((List<?>) suspended.getPayload().get("pendingApprovals")).isEmpty());
|
||||
Assert.assertFalse(events.stream().anyMatch(event -> event.getEventType() == AgentRuntimeEventType.COMPLETED));
|
||||
Assert.assertFalse(events.stream().anyMatch(event -> event.getEventType() == AgentRuntimeEventType.TOOL_RESULT));
|
||||
Assert.assertEquals(1, events.stream()
|
||||
.filter(event -> event.getEventType() == AgentRuntimeEventType.SUSPENDED)
|
||||
.count());
|
||||
}
|
||||
|
||||
@Test(expected = AgentRuntimeException.class)
|
||||
@@ -927,6 +931,267 @@ public class AgentScopeStatefulRuntimeTest {
|
||||
Assert.assertTrue(sessionStore.exists("session-1"));
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证同一轮推理包含多个审批工具时,全部批准前不会执行任何工具。
|
||||
*/
|
||||
@Test
|
||||
public void shouldWaitForAllToolApprovalsBeforeExecutingBatch() {
|
||||
AgentInitRequest request = initRequest();
|
||||
AgentToolSpec searchSpec = new AgentToolSpec();
|
||||
searchSpec.setName("search");
|
||||
searchSpec.setDescription("search");
|
||||
searchSpec.setApprovalRequired(true);
|
||||
AgentToolSpec auditSpec = new AgentToolSpec();
|
||||
auditSpec.setName("audit");
|
||||
auditSpec.setDescription("audit");
|
||||
auditSpec.setApprovalRequired(true);
|
||||
request.getAgentDefinition().setToolSpecs(List.of(searchSpec, auditSpec));
|
||||
AtomicInteger invocationCount = new AtomicInteger();
|
||||
request.setToolInvokers(Map.of(
|
||||
"search", (arguments, context) -> {
|
||||
invocationCount.incrementAndGet();
|
||||
return AgentToolResult.success("search result");
|
||||
},
|
||||
"audit", (arguments, context) -> {
|
||||
invocationCount.incrementAndGet();
|
||||
return AgentToolResult.success("audit result");
|
||||
}));
|
||||
AgentScopeReActRuntime runtime = runtimeWithModel(List.of(
|
||||
ChatResponse.builder()
|
||||
.id("tool-call-message")
|
||||
.content(List.of(
|
||||
ToolUseBlock.builder()
|
||||
.id("call-search")
|
||||
.name("search")
|
||||
.input(Map.of("q", "easyflow"))
|
||||
.build(),
|
||||
ToolUseBlock.builder()
|
||||
.id("call-audit")
|
||||
.name("audit")
|
||||
.input(Map.of("scope", "current"))
|
||||
.build()))
|
||||
.finishReason("tool_calls")
|
||||
.build(),
|
||||
ChatResponse.builder()
|
||||
.id("final-message")
|
||||
.content(List.of(TextBlock.builder().text("done").build()))
|
||||
.finishReason("stop")
|
||||
.build()));
|
||||
runtime.init(request);
|
||||
|
||||
List<AgentRuntimeEvent> initialEvents = runtime.stream(
|
||||
AgentMessage.text(AgentMessageRole.USER, "use tools"))
|
||||
.collectList()
|
||||
.block();
|
||||
List<AgentRuntimeEvent> approvals = initialEvents.stream()
|
||||
.filter(event -> event.getEventType() == AgentRuntimeEventType.TOOL_APPROVAL_REQUIRED)
|
||||
.toList();
|
||||
|
||||
Assert.assertEquals(2, approvals.size());
|
||||
Assert.assertEquals(1, initialEvents.stream()
|
||||
.filter(event -> event.getEventType() == AgentRuntimeEventType.SUSPENDED)
|
||||
.count());
|
||||
List<AgentRuntimeEvent> firstResumeEvents = runtime.resume(resumeFromApproval(approvals.get(0), true))
|
||||
.collectList()
|
||||
.block();
|
||||
|
||||
Assert.assertEquals(0, invocationCount.get());
|
||||
AgentRuntimeEvent waitingEvent = firstResumeEvents.stream()
|
||||
.filter(event -> event.getEventType() == AgentRuntimeEventType.SUSPENDED)
|
||||
.findFirst()
|
||||
.orElseThrow();
|
||||
Assert.assertEquals(1, ((List<?>) waitingEvent.getPayload().get("pendingApprovals")).size());
|
||||
Assert.assertFalse(firstResumeEvents.stream()
|
||||
.anyMatch(event -> event.getEventType() == AgentRuntimeEventType.TOOL_CALL));
|
||||
|
||||
List<AgentRuntimeEvent> secondResumeEvents = runtime.resume(resumeFromApproval(approvals.get(1), true))
|
||||
.collectList()
|
||||
.block();
|
||||
|
||||
Assert.assertEquals(2, invocationCount.get());
|
||||
Assert.assertEquals(2, secondResumeEvents.stream()
|
||||
.filter(event -> event.getEventType() == AgentRuntimeEventType.TOOL_RESULT)
|
||||
.count());
|
||||
Assert.assertTrue(secondResumeEvents.stream()
|
||||
.anyMatch(event -> event.getEventType() == AgentRuntimeEventType.COMPLETED));
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证不同 toolCallId 即使名称和入参相同也会分别审批和执行。
|
||||
*/
|
||||
@Test
|
||||
public void shouldPreserveDistinctApprovedToolCallsWithIdenticalInput() {
|
||||
AgentInitRequest request = initRequest();
|
||||
AgentToolSpec toolSpec = new AgentToolSpec();
|
||||
toolSpec.setName("search");
|
||||
toolSpec.setDescription("search");
|
||||
toolSpec.setApprovalRequired(true);
|
||||
request.getAgentDefinition().setToolSpecs(List.of(toolSpec));
|
||||
AtomicInteger invocationCount = new AtomicInteger();
|
||||
request.setToolInvokers(Map.of("search", (arguments, context) -> {
|
||||
invocationCount.incrementAndGet();
|
||||
return AgentToolResult.success("tool result");
|
||||
}));
|
||||
Map<String, Object> input = Map.of("q", "easyflow");
|
||||
AgentScopeReActRuntime runtime = runtimeWithModel(List.of(
|
||||
ChatResponse.builder()
|
||||
.id("tool-call-message")
|
||||
.content(List.of(
|
||||
ToolUseBlock.builder()
|
||||
.id("call-search")
|
||||
.name("search")
|
||||
.input(input)
|
||||
.build(),
|
||||
ToolUseBlock.builder()
|
||||
.id("call-search-duplicate")
|
||||
.name("search")
|
||||
.input(input)
|
||||
.build()))
|
||||
.finishReason("tool_calls")
|
||||
.build(),
|
||||
ChatResponse.builder()
|
||||
.id("final-message")
|
||||
.content(List.of(TextBlock.builder().text("done").build()))
|
||||
.finishReason("stop")
|
||||
.build()));
|
||||
runtime.init(request);
|
||||
|
||||
List<AgentRuntimeEvent> suspendedEvents = runtime.stream(
|
||||
AgentMessage.text(AgentMessageRole.USER, "use tool"))
|
||||
.collectList()
|
||||
.block();
|
||||
List<AgentRuntimeEvent> approvals = suspendedEvents.stream()
|
||||
.filter(event -> event.getEventType() == AgentRuntimeEventType.TOOL_APPROVAL_REQUIRED)
|
||||
.toList();
|
||||
|
||||
Assert.assertEquals(2, approvals.size());
|
||||
List<AgentRuntimeEvent> firstResumeEvents = runtime.resume(resumeFromApproval(approvals.get(0), true))
|
||||
.collectList()
|
||||
.block();
|
||||
|
||||
Assert.assertEquals(0, invocationCount.get());
|
||||
Assert.assertTrue(firstResumeEvents.stream()
|
||||
.anyMatch(event -> event.getEventType() == AgentRuntimeEventType.SUSPENDED));
|
||||
List<AgentRuntimeEvent> secondResumeEvents = runtime.resume(resumeFromApproval(approvals.get(1), true))
|
||||
.collectList()
|
||||
.block();
|
||||
|
||||
Assert.assertEquals(2, invocationCount.get());
|
||||
Assert.assertEquals(2, secondResumeEvents.stream()
|
||||
.filter(event -> event.getEventType() == AgentRuntimeEventType.TOOL_RESULT)
|
||||
.count());
|
||||
Assert.assertTrue(secondResumeEvents.stream()
|
||||
.anyMatch(event -> event.getEventType() == AgentRuntimeEventType.COMPLETED));
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证相同 toolCallId 的同轮重放只审批并执行一次。
|
||||
*/
|
||||
@Test
|
||||
public void shouldDeduplicateRepeatedToolCallIdWithinReasoning() {
|
||||
AgentInitRequest request = initRequest();
|
||||
AgentToolSpec toolSpec = new AgentToolSpec();
|
||||
toolSpec.setName("search");
|
||||
toolSpec.setDescription("search");
|
||||
toolSpec.setApprovalRequired(true);
|
||||
request.getAgentDefinition().setToolSpecs(List.of(toolSpec));
|
||||
AtomicInteger invocationCount = new AtomicInteger();
|
||||
request.setToolInvokers(Map.of("search", (arguments, context) -> {
|
||||
invocationCount.incrementAndGet();
|
||||
return AgentToolResult.success("tool result");
|
||||
}));
|
||||
Map<String, Object> input = Map.of("q", "easyflow");
|
||||
AgentScopeReActRuntime runtime = runtimeWithModel(List.of(
|
||||
ChatResponse.builder()
|
||||
.id("tool-call-message")
|
||||
.content(List.of(
|
||||
ToolUseBlock.builder()
|
||||
.id("call-search")
|
||||
.name("search")
|
||||
.input(input)
|
||||
.build(),
|
||||
ToolUseBlock.builder()
|
||||
.id("call-search")
|
||||
.name("search")
|
||||
.input(input)
|
||||
.build()))
|
||||
.finishReason("tool_calls")
|
||||
.build(),
|
||||
ChatResponse.builder()
|
||||
.id("final-message")
|
||||
.content(List.of(TextBlock.builder().text("done").build()))
|
||||
.finishReason("stop")
|
||||
.build()));
|
||||
runtime.init(request);
|
||||
|
||||
List<AgentRuntimeEvent> suspendedEvents = runtime.stream(
|
||||
AgentMessage.text(AgentMessageRole.USER, "use tool"))
|
||||
.collectList()
|
||||
.block();
|
||||
List<AgentRuntimeEvent> approvals = suspendedEvents.stream()
|
||||
.filter(event -> event.getEventType() == AgentRuntimeEventType.TOOL_APPROVAL_REQUIRED)
|
||||
.toList();
|
||||
|
||||
Assert.assertEquals(1, approvals.size());
|
||||
List<AgentRuntimeEvent> resumeEvents = runtime.resume(resumeFromApproval(approvals.get(0), true))
|
||||
.collectList()
|
||||
.block();
|
||||
|
||||
Assert.assertEquals(1, invocationCount.get());
|
||||
Assert.assertEquals(1, resumeEvents.stream()
|
||||
.filter(event -> event.getEventType() == AgentRuntimeEventType.TOOL_RESULT)
|
||||
.count());
|
||||
Assert.assertTrue(resumeEvents.stream()
|
||||
.anyMatch(event -> event.getEventType() == AgentRuntimeEventType.COMPLETED));
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证模型未提供 toolCallId 时审批链路仍会获得完整的调用身份。
|
||||
*/
|
||||
@Test
|
||||
public void shouldProvideApprovalIdentityWhenModelOmitsToolCallId() {
|
||||
AgentInitRequest request = initRequest();
|
||||
AgentToolSpec toolSpec = new AgentToolSpec();
|
||||
toolSpec.setName("search");
|
||||
toolSpec.setDescription("search");
|
||||
toolSpec.setApprovalRequired(true);
|
||||
request.getAgentDefinition().setToolSpecs(List.of(toolSpec));
|
||||
request.setToolInvokers(Map.of("search", (arguments, context) ->
|
||||
AgentToolResult.success("tool result")));
|
||||
AgentScopeReActRuntime runtime = runtimeWithModel(List.of(
|
||||
ChatResponse.builder()
|
||||
.id("tool-call-message")
|
||||
.content(List.of(ToolUseBlock.builder()
|
||||
.name("search")
|
||||
.input(Map.of("q", "easyflow"))
|
||||
.build()))
|
||||
.finishReason("tool_calls")
|
||||
.build(),
|
||||
ChatResponse.builder()
|
||||
.id("final-message")
|
||||
.content(List.of(TextBlock.builder().text("done").build()))
|
||||
.finishReason("stop")
|
||||
.build()));
|
||||
runtime.init(request);
|
||||
|
||||
List<AgentRuntimeEvent> events = runtime.stream(
|
||||
AgentMessage.text(AgentMessageRole.USER, "use tool"))
|
||||
.collectList()
|
||||
.block();
|
||||
AgentRuntimeEvent approval = events.stream()
|
||||
.filter(event -> event.getEventType() == AgentRuntimeEventType.TOOL_APPROVAL_REQUIRED)
|
||||
.findFirst()
|
||||
.orElseThrow();
|
||||
|
||||
String toolCallId = String.valueOf(approval.getPayload().get("toolCallId"));
|
||||
Assert.assertFalse(toolCallId.isBlank());
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, Object> metadata =
|
||||
(Map<String, Object>) approval.getPayload().get("approvalMetadata");
|
||||
String approvalBatchId = String.valueOf(metadata.get("approvalBatchId"));
|
||||
Assert.assertFalse(approvalBatchId.isBlank());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldCancelRejectedToolResumeWithoutExecutingTool() {
|
||||
InMemoryAgentSessionStore sessionStore = new InMemoryAgentSessionStore();
|
||||
@@ -1105,6 +1370,22 @@ public class AgentScopeStatefulRuntimeTest {
|
||||
new AgentScopeMessageAdapter());
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据审批事件创建恢复请求。
|
||||
*
|
||||
* @param approvalEvent 工具审批事件
|
||||
* @param approved 是否批准
|
||||
* @return 恢复请求
|
||||
*/
|
||||
private AgentResumeRequest resumeFromApproval(AgentRuntimeEvent approvalEvent, boolean approved) {
|
||||
AgentResumeRequest request = new AgentResumeRequest();
|
||||
AgentResumeToken token = new AgentResumeToken();
|
||||
token.setValue(String.valueOf(approvalEvent.getPayload().get("resumeToken")));
|
||||
request.setResumeToken(token);
|
||||
request.setApproved(approved);
|
||||
return request;
|
||||
}
|
||||
|
||||
private static class ScriptedModel implements Model {
|
||||
|
||||
private final String modelName;
|
||||
|
||||
@@ -0,0 +1,258 @@
|
||||
package com.easyagents.agent.runtime.hitl;
|
||||
|
||||
import com.easyagents.agent.runtime.AgentResumeRequest;
|
||||
import com.easyagents.agent.runtime.AgentRuntimeException;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.Future;
|
||||
|
||||
/**
|
||||
* 测试工具审批协调器。
|
||||
*/
|
||||
public class AgentToolApprovalCoordinatorTest {
|
||||
|
||||
/**
|
||||
* 验证同批次全部调用批准后才签发逐调用执行授权。
|
||||
*/
|
||||
@Test
|
||||
public void shouldAuthorizeBatchOnlyAfterAllCallsApproved() {
|
||||
AgentToolApprovalCoordinator coordinator = AgentToolApprovalCoordinator.enabled();
|
||||
AgentPendingState first = register(coordinator, "call-1", "search", Map.of("q", "first"), "batch-1");
|
||||
AgentPendingState second = register(coordinator, "call-2", "search", Map.of("q", "second"), "batch-1");
|
||||
|
||||
AgentToolApprovalResolution firstResolution = coordinator.resolve(resume(first, true));
|
||||
|
||||
Assert.assertEquals(AgentToolApprovalResolution.Status.WAITING, firstResolution.getStatus());
|
||||
Assert.assertEquals(1, firstResolution.getRemainingStates().size());
|
||||
assertAuthorizationRejected(coordinator, "call-1", "search", Map.of("q", "first"));
|
||||
|
||||
AgentToolApprovalResolution secondResolution = coordinator.resolve(resume(second, true));
|
||||
|
||||
Assert.assertEquals(AgentToolApprovalResolution.Status.READY, secondResolution.getStatus());
|
||||
coordinator.consumeExecutionAuthorization("call-1", "search", Map.of("q", "first"));
|
||||
coordinator.consumeExecutionAuthorization("call-2", "search", Map.of("q", "second"));
|
||||
assertAuthorizationRejected(coordinator, "call-1", "search", Map.of("q", "first"));
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证拒绝一个调用会关闭整个审批批次。
|
||||
*/
|
||||
@Test
|
||||
public void shouldRejectWholeBatchWhenAnyCallRejected() {
|
||||
AgentToolApprovalCoordinator coordinator = AgentToolApprovalCoordinator.enabled();
|
||||
AgentPendingState first = register(coordinator, "call-1", "search", Map.of("q", "first"), "batch-1");
|
||||
AgentPendingState second = register(coordinator, "call-2", "search", Map.of("q", "second"), "batch-1");
|
||||
coordinator.resolve(resume(first, true));
|
||||
|
||||
AgentResumeRequest rejection = resume(second, false);
|
||||
rejection.setRejectReason("not allowed");
|
||||
AgentToolApprovalResolution resolution = coordinator.resolve(rejection);
|
||||
|
||||
Assert.assertEquals(AgentToolApprovalResolution.Status.REJECTED, resolution.getStatus());
|
||||
Assert.assertEquals("not allowed", resolution.getReason());
|
||||
assertAuthorizationRejected(coordinator, "call-1", "search", Map.of("q", "first"));
|
||||
try {
|
||||
coordinator.resolve(resume(first, true));
|
||||
Assert.fail("已消费的审批令牌不能重复使用");
|
||||
} catch (AgentRuntimeException expected) {
|
||||
Assert.assertTrue(expected.getMessage().contains("invalid"));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证过期令牌不能签发工具执行授权。
|
||||
*/
|
||||
@Test
|
||||
public void shouldExpireApprovalBeforeResolution() {
|
||||
AgentToolApprovalCoordinator coordinator = AgentToolApprovalCoordinator.enabled();
|
||||
AgentPendingState expired = coordinator.register(
|
||||
"session-1",
|
||||
"agent-1",
|
||||
"call-expired",
|
||||
"search",
|
||||
"approve",
|
||||
Map.of("q", "expired"),
|
||||
Map.of(),
|
||||
Instant.now().minusSeconds(1),
|
||||
"batch-expired");
|
||||
|
||||
AgentToolApprovalResolution resolution = coordinator.resolve(resume(expired, true));
|
||||
|
||||
Assert.assertEquals(AgentToolApprovalResolution.Status.EXPIRED, resolution.getStatus());
|
||||
assertAuthorizationRejected(coordinator, "call-expired", "search", Map.of("q", "expired"));
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证并发重复点击同一令牌时最多一个请求可以成功消费。
|
||||
*
|
||||
* @throws Exception 并发任务执行失败时抛出
|
||||
*/
|
||||
@Test
|
||||
public void shouldConsumeConcurrentDuplicateApprovalOnlyOnce() throws Exception {
|
||||
AgentToolApprovalCoordinator coordinator = AgentToolApprovalCoordinator.enabled();
|
||||
AgentPendingState pending = register(
|
||||
coordinator, "call-1", "search", Map.of("q", "easyflow"), "batch-1");
|
||||
CountDownLatch start = new CountDownLatch(1);
|
||||
ExecutorService executor = Executors.newFixedThreadPool(2);
|
||||
try {
|
||||
Future<Boolean> first = executor.submit(() -> resolveAfter(start, coordinator, pending));
|
||||
Future<Boolean> second = executor.submit(() -> resolveAfter(start, coordinator, pending));
|
||||
start.countDown();
|
||||
|
||||
int successCount = (first.get() ? 1 : 0) + (second.get() ? 1 : 0);
|
||||
|
||||
Assert.assertEquals(1, successCount);
|
||||
coordinator.consumeExecutionAuthorization(
|
||||
"call-1", "search", Map.of("q", "easyflow"));
|
||||
assertAuthorizationRejected(
|
||||
coordinator, "call-1", "search", Map.of("q", "easyflow"));
|
||||
} finally {
|
||||
executor.shutdownNow();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证工具名称或入参变化时批准凭证立即失效。
|
||||
*/
|
||||
@Test
|
||||
public void shouldRejectExecutionWhenApprovedCallIsModified() {
|
||||
AgentToolApprovalCoordinator coordinator = AgentToolApprovalCoordinator.enabled();
|
||||
AgentPendingState pending = register(
|
||||
coordinator, "call-1", "search", Map.of("q", "easyflow"), "batch-1");
|
||||
coordinator.resolve(resume(pending, true));
|
||||
|
||||
assertAuthorizationRejected(
|
||||
coordinator, "call-1", "search", Map.of("q", "modified"));
|
||||
assertAuthorizationRejected(
|
||||
coordinator, "call-1", "search", Map.of("q", "easyflow"));
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证跨节点受信任恢复仍需绑定明确的工具调用信息。
|
||||
*/
|
||||
@Test
|
||||
public void shouldAuthorizeTrustedExecutionByToolCallIdentity() {
|
||||
AgentToolApprovalCoordinator coordinator = AgentToolApprovalCoordinator.enabled();
|
||||
AgentResumeRequest request = new AgentResumeRequest();
|
||||
AgentResumeToken token = new AgentResumeToken();
|
||||
token.setValue("persisted-token");
|
||||
request.setResumeToken(token);
|
||||
request.setApproved(true);
|
||||
request.setTrusted(true);
|
||||
request.setMetadata(Map.of(
|
||||
"toolCallId", "call-1",
|
||||
"toolName", "search",
|
||||
"toolInput", Map.of("q", "easyflow")));
|
||||
|
||||
coordinator.authorizeTrustedExecution(request);
|
||||
|
||||
coordinator.consumeExecutionAuthorization(
|
||||
"call-1", "search", Map.of("q", "easyflow"));
|
||||
assertAuthorizationRejected(
|
||||
coordinator, "call-1", "search", Map.of("q", "easyflow"));
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证同一 toolCallId 不能被重新绑定到不同工具内容。
|
||||
*/
|
||||
@Test
|
||||
public void shouldRejectDuplicateToolCallIdWithDifferentInput() {
|
||||
AgentToolApprovalCoordinator coordinator = AgentToolApprovalCoordinator.enabled();
|
||||
register(coordinator, "call-1", "search", Map.of("q", "easyflow"), "batch-1");
|
||||
|
||||
try {
|
||||
register(coordinator, "call-1", "search", Map.of("q", "modified"), "batch-2");
|
||||
Assert.fail("重复 toolCallId 不能绑定不同入参");
|
||||
} catch (AgentRuntimeException expected) {
|
||||
Assert.assertTrue(expected.getMessage().contains("Duplicate toolCallId"));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册测试用审批状态。
|
||||
*
|
||||
* @param coordinator 审批协调器
|
||||
* @param toolCallId 工具调用ID
|
||||
* @param toolName 工具名称
|
||||
* @param toolInput 工具入参
|
||||
* @param batchId 审批批次ID
|
||||
* @return 待审批状态
|
||||
*/
|
||||
private AgentPendingState register(AgentToolApprovalCoordinator coordinator,
|
||||
String toolCallId,
|
||||
String toolName,
|
||||
Map<String, Object> toolInput,
|
||||
String batchId) {
|
||||
return coordinator.register(
|
||||
"session-1",
|
||||
"agent-1",
|
||||
toolCallId,
|
||||
toolName,
|
||||
"approve",
|
||||
toolInput,
|
||||
Map.of(),
|
||||
Instant.now().plusSeconds(60),
|
||||
batchId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建测试用恢复请求。
|
||||
*
|
||||
* @param state 待审批状态
|
||||
* @param approved 是否批准
|
||||
* @return 恢复请求
|
||||
*/
|
||||
private AgentResumeRequest resume(AgentPendingState state, boolean approved) {
|
||||
AgentResumeRequest request = new AgentResumeRequest();
|
||||
request.setResumeToken(state.getResumeToken());
|
||||
request.setApproved(approved);
|
||||
return request;
|
||||
}
|
||||
|
||||
/**
|
||||
* 等待并发起跑信号后消费审批令牌。
|
||||
*
|
||||
* @param start 起跑信号
|
||||
* @param coordinator 审批协调器
|
||||
* @param pending 待审批状态
|
||||
* @return 成功消费时为 true
|
||||
* @throws InterruptedException 等待被中断时抛出
|
||||
*/
|
||||
private boolean resolveAfter(CountDownLatch start,
|
||||
AgentToolApprovalCoordinator coordinator,
|
||||
AgentPendingState pending) throws InterruptedException {
|
||||
start.await();
|
||||
try {
|
||||
coordinator.resolve(resume(pending, true));
|
||||
return true;
|
||||
} catch (AgentRuntimeException expected) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 断言工具执行授权不可用。
|
||||
*
|
||||
* @param coordinator 审批协调器
|
||||
* @param toolCallId 工具调用ID
|
||||
* @param toolName 工具名称
|
||||
* @param toolInput 工具入参
|
||||
*/
|
||||
private void assertAuthorizationRejected(AgentToolApprovalCoordinator coordinator,
|
||||
String toolCallId,
|
||||
String toolName,
|
||||
Map<String, Object> toolInput) {
|
||||
try {
|
||||
coordinator.consumeExecutionAuthorization(toolCallId, toolName, toolInput);
|
||||
Assert.fail("未授权或已消费的工具调用必须被拒绝");
|
||||
} catch (AgentToolApprovalRejectedException expected) {
|
||||
Assert.assertNotNull(expected.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user