mirror of
https://gitee.com/mateos/mateclaw.git
synced 2026-09-16 04:18:17 +08:00
Guard persistent Goal queued input selection
This commit is contained in:
parent
cc3aa3e765
commit
c23fa8fa2e
@ -18,6 +18,7 @@ import vip.mate.goal.model.SegmentOutcome;
|
||||
import vip.mate.workspace.conversation.ConversationService;
|
||||
import vip.mate.workspace.conversation.model.MessageEntity;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.time.LocalDateTime;
|
||||
@ -51,6 +52,8 @@ public class GoalSegmentRunner {
|
||||
private GoalService goals;
|
||||
@org.springframework.beans.factory.annotation.Autowired
|
||||
private GoalRunCoordinator coordinator;
|
||||
@org.springframework.beans.factory.annotation.Autowired
|
||||
private GoalApprovalRunService approvalRuns;
|
||||
|
||||
public GoalSegmentRunner(AgentService agents, ConversationService conversations,
|
||||
ApprovalWorkflowService approvals, ChatStreamTracker streams, ObjectMapper mapper,
|
||||
@ -131,7 +134,7 @@ public class GoalSegmentRunner {
|
||||
.withExecutionAttribution(new ExecutionAttribution(goal.getId(),
|
||||
claimedRun == null ? null : claimedRun.attempt().id(), null, null,
|
||||
claimedRun == null ? null : claimedRun.attempt().leaseToken()));
|
||||
SegmentResult result;
|
||||
SegmentResult result=null;
|
||||
ConversationInputQueueStore.QueuedInput queued=claimNextInput(convId,claimedRun);
|
||||
do {
|
||||
String input=guidance+prompt;
|
||||
@ -142,15 +145,33 @@ public class GoalSegmentRunner {
|
||||
claimedInput.set(null);
|
||||
throw new IllegalStateException("Queued input targets a different agent; user review required");
|
||||
}
|
||||
Long originMessageId=queued.persistedMessageId();
|
||||
if (originMessageId==null) {
|
||||
var saved=conversations.saveMessage(convId,"user",queued.message(),queued.contentParts(),"queued");
|
||||
originMessageId=saved==null ? null : saved.getId();
|
||||
if (originMessageId==null || !inputQueue.bindMessage(queued.id(),queued.claimedByAttemptId(),
|
||||
originMessageId,LocalDateTime.now())) {
|
||||
throw new IllegalStateException("Queued input could not be bound to its persisted message");
|
||||
GoalEntity currentGoal=goals==null ? goal : goals.getById(goal.getId());
|
||||
if (currentGoal!=null && currentGoal.getStatus()==vip.mate.goal.model.GoalStatus.PAUSED)
|
||||
return new SegmentOutcome.Cancelled("paused");
|
||||
boolean required=currentGoal!=null && currentGoal.isJsonAcceptanceRequired();
|
||||
boolean selected=queued.selectedGoalId()!=null && queued.selectedGoalId()>0;
|
||||
if (required || selected) {
|
||||
var queuedOrigin=ChatOrigin.web(convId,queued.createdBy(),goal.getWorkspaceId(),
|
||||
null,null,queued.requesterUserId()).withAgent(goal.getAgentId())
|
||||
.withSelectedGoalId(queued.selectedGoalId());
|
||||
if (!required || currentGoal.getStatus()!=vip.mate.goal.model.GoalStatus.ACTIVE
|
||||
|| !Objects.equals(queued.selectedGoalId(),goal.getId())
|
||||
|| approvalRuns==null || !approvalRuns.queuedSelectionStillCurrent(queuedOrigin)) {
|
||||
persistQueuedInput(convId,queued);
|
||||
if (!inputQueue.consume(queued.id(),queued.claimedByAttemptId(),LocalDateTime.now()))
|
||||
throw new IllegalStateException("Rejected queued input claim was lost");
|
||||
claimedInput.set(null);
|
||||
conversations.saveMessage(convId,"assistant",
|
||||
"Queued input was not run because its selected Goal or account is no longer current. Review and resend it.",
|
||||
List.of(),"completed");
|
||||
streams.broadcastObject(convId,"warning",Map.of("message",
|
||||
"Queued input selected a different or unavailable Goal; text was saved for review."));
|
||||
queued=claimNextInput(convId,claimedRun);
|
||||
if (queued==null) break;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
Long originMessageId=persistQueuedInput(convId,queued);
|
||||
if (!inputQueue.consume(queued.id(),queued.claimedByAttemptId(),LocalDateTime.now())) {
|
||||
throw new IllegalStateException("Queued input claim was lost before execution");
|
||||
}
|
||||
@ -167,6 +188,7 @@ public class GoalSegmentRunner {
|
||||
if ("stopped".equals(result.finishReason())) return new SegmentOutcome.Cancelled("stopped");
|
||||
queued=claimNextInput(convId,claimedRun);
|
||||
} while (queued!=null);
|
||||
if(result==null) return new SegmentOutcome.Continue("queued_input_rejected");
|
||||
if(result.evaluationUnavailable()) return new SegmentOutcome.Retry("evaluation","evaluation_unavailable");
|
||||
if("error_fallback".equals(result.finishReason())) {
|
||||
return new SegmentOutcome.Blocked("graph","graph_error_requires_review");
|
||||
@ -288,6 +310,17 @@ public class GoalSegmentRunner {
|
||||
return inputQueue.claimNext(conversationId,claimant,LocalDateTime.now()).orElse(null);
|
||||
}
|
||||
|
||||
private Long persistQueuedInput(String conversationId,ConversationInputQueueStore.QueuedInput queued) {
|
||||
if (queued.persistedMessageId()!=null) return queued.persistedMessageId();
|
||||
var saved=conversations.saveMessage(conversationId,"user",queued.message(),queued.contentParts(),"queued");
|
||||
Long messageId=saved==null ? null : saved.getId();
|
||||
if (messageId==null || !inputQueue.bindMessage(queued.id(),queued.claimedByAttemptId(),
|
||||
messageId,LocalDateTime.now())) {
|
||||
throw new IllegalStateException("Queued input could not be bound to its persisted message");
|
||||
}
|
||||
return messageId;
|
||||
}
|
||||
|
||||
private String queuedPrompt(ConversationInputQueueStore.QueuedInput queued) {
|
||||
if (queued.contentParts()==null || queued.contentParts().isEmpty()) return queued.message();
|
||||
var message=new vip.mate.workspace.conversation.model.MessageEntity();
|
||||
|
||||
@ -18,7 +18,7 @@ Prefix: `/api/v1/goals/{goalId}/json-acceptance`. An enabled account with conver
|
||||
|
||||
Publication requires an active or paused goal and a slot referenced by a current requirement. Content must be a strict JSON object: duplicate keys, trailing documents, nesting beyond 32 levels and UTF-8 content over 1 MiB are rejected. Each goal can retain at most 32 versions; the limit rejects new publication instead of overwriting history. Each version expires after 24 hours. Republishing identical bytes still creates a new version. Reload after a generation conflict rather than automatically overwriting another publication. Retries can reuse a suitable current version and refresh its check binding; reaching the quota still permits checking and completing with that version. If the version is expired or its content must change and all 32 versions are used, further publication remains unavailable.
|
||||
|
||||
Managed bodies live independently in the database. Ordinary workspace files, cache paths and hashes in text are not substitutes. No publication API edits historical bodies; bodies and pointers commit together. SHA-256 identifies content and supports integrity checks; it does not isolate an attacker with database credentials or host privileges. The database and service host are trusted foundations of this limited protocol. The JSON service contract has been exercised on H2, MySQL 8.0.46 and PostgreSQL 16.14. MySQL and PostgreSQL each passed 72 JSON protocol cases with V201 on cycle062 and 15 opt-in HTTP approval/authentication cases on cycle065. The MySQL run isolated an existing V192 migration failure using a test-only migration copy; PostgreSQL used the original Kingbase migration tree while skipping an unrelated bundled-skill import failure. These are protocol tests, not confirmation that an unmodified full installation succeeds. The proprietary Kingbase engine has not been tested.
|
||||
Managed bodies live independently in the database. Ordinary workspace files, cache paths and hashes in text are not substitutes. No publication API edits historical bodies; bodies and pointers commit together. SHA-256 identifies content and supports integrity checks; it does not isolate an attacker with database credentials or host privileges. The database and service host are trusted foundations of this limited protocol. The JSON service contract has been exercised on H2, MySQL 8.0.46 and PostgreSQL 16.14. MySQL and PostgreSQL each passed 72 JSON protocol cases with V201 on cycle062 and 17 opt-in HTTP approval/authentication and scheduled-queue cases on cycle066. The MySQL run isolated an existing V192 migration failure using a test-only migration copy; PostgreSQL used the original Kingbase migration tree while skipping an unrelated bundled-skill import failure. These are protocol tests, not confirmation that an unmodified full installation succeeds. The proprietary Kingbase engine has not been tested.
|
||||
|
||||
## Agent publication
|
||||
|
||||
@ -59,7 +59,7 @@ Built-in shell/code execution is not OS-isolated from the service host. Selectin
|
||||
|
||||
Recovery attempts receive guidance to inspect existing evidence before repeating work. If the first recovered segment is deferred before execution, its recovery context is retained for the next claim. Ordinary continuation after an executed segment does not become a new recovery.
|
||||
|
||||
From V199, queued Web input stores the authenticated account ID at enqueue time, and ordinary Web replay carries the conversation workspace. V201 also stores the selected managed Goal ID at enqueue time. Before a selected queued turn starts, the server rechecks its account and Goal. If either is no longer current, it saves the user text and asks for a fresh request without starting the agent. An old queue row without a selection snapshot is handled the same way when its conversation has managed Goal history. The chat interface clears that queued item and prompts the user to resend; later queued items continue. Managed operations still recheck the account, ownership and current requirements. Legacy queue items do not gain an asserted identity from a username; users must resend an authenticated request for managed JSON operations. Persistent Goal workers retain their existing attempt-owner validation when consuming input; this does not introduce an account path without a lease check.
|
||||
From V199, queued Web input stores the authenticated account ID at enqueue time, and ordinary Web replay carries the conversation workspace. V201 also stores the selected managed Goal ID at enqueue time. Before a selected queued turn starts, the server rechecks its account and Goal. If either is no longer current, it saves the user text and asks for a fresh request without starting the agent. An old queue row without a selection snapshot is handled the same way when its conversation has managed Goal history. The chat interface clears that queued item and prompts the user to resend; later queued items continue. A persistent Goal worker also checks the queued selection and original account before running it. It saves a mismatched or revoked input as conversation text with a durable assistant notice, then continues to later queued items. A paused Goal retains its claimed input for processing after resume. Managed operations still recheck the account, ownership and current requirements. Legacy queue items do not gain an asserted identity from a username; users must resend an authenticated request for managed JSON operations. Persistent Goal workers also retain their attempt-owner validation; queue validation does not replace the lease check.
|
||||
|
||||
Approval replay restores the persisted runtime identity; approval does not renew an expired attempt lease or override account revocation. Legacy snapshots without an authenticated account ID cannot gain managed JSON access from a display username alone.
|
||||
|
||||
|
||||
@ -18,7 +18,7 @@
|
||||
|
||||
仅当前要求引用的槽可发布,Goal 必须 active 或 paused。正文必须是严格 JSON 对象,拒绝重复键、尾随文档、超过 32 层的嵌套及超过 1 MiB 的 UTF-8 内容。每个 Goal 最多保存 32 个版本;达到配额拒绝继续发布,不覆盖旧版本。每版有效期 24 小时,重复发布同样正文也产生新版本。客户端遇到 generation 冲突应重新读取,不自动覆盖他人发布。重试可以复用适用的当前版本并更新检查绑定;达到配额后仍可检查当前版本并在合格时完成。如果版本已过期或正文必须修改且32个版本均已使用,则不能继续发布。
|
||||
|
||||
这些版本独立存储在数据库,不能用普通工作区文件、缓存路径或文字中的 hash 替代。发布接口不支持更新历史正文;所有版本与槽指针同事务保存。SHA-256 用于标识及完整性核对,不能隔离拥有数据库凭据或宿主权限的攻击者;数据库和服务宿主是此有限协议的可信基础。MySQL与PostgreSQL在带V201的cycle062源码上各通过72项JSON协议案例,在cycle065各通过15项显式启用的HTTP审批/认证案例。MySQL 使用仅修正既有 V192 失败的临时迁移副本;PostgreSQL 使用原始 Kingbase 迁移树,跳过无关的内置技能导入失败。这是协议验证,不能代表未修改的完整安装成功;尚未实测 Kingbase 专有引擎。
|
||||
这些版本独立存储在数据库,不能用普通工作区文件、缓存路径或文字中的 hash 替代。发布接口不支持更新历史正文;所有版本与槽指针同事务保存。SHA-256 用于标识及完整性核对,不能隔离拥有数据库凭据或宿主权限的攻击者;数据库和服务宿主是此有限协议的可信基础。MySQL与PostgreSQL在带V201的cycle062源码上各通过72项JSON协议案例,在cycle066各通过17项显式启用的HTTP审批/认证及后台队列案例。MySQL 使用仅修正既有 V192 失败的临时迁移副本;PostgreSQL 使用原始 Kingbase 迁移树,跳过无关的内置技能导入失败。这是协议验证,不能代表未修改的完整安装成功;尚未实测 Kingbase 专有引擎。
|
||||
|
||||
## 代理发布
|
||||
|
||||
@ -57,7 +57,7 @@ V198 同样以绝对时间保存调度租约截止。升级时旧租约失效,
|
||||
|
||||
内置 shell/code 执行没有与服务宿主做操作系统隔离。选择 JSON 验收不会把这些工具变成沙箱;此协议不能抵抗能访问数据库凭据或文件的宿主代码,环境变量名称过滤和工作区路径检查也不能替代隔离。租约截止从绝对时刻计算,覆盖夏令时回拨;调度显示字段仍使用本地时间戳。
|
||||
|
||||
Web排队消息从V199起保存入队时已认证账户的内部ID,普通Web续跑同时携带当前会话工作区;V201还在入队时保存选定的受管Goal ID。选定Goal的排队消息在开始执行前复查账户与Goal;若已失效,只保存用户文字并提示重新发送,不启动Agent。升级前没有选择快照的队列行,若会话有受管Goal历史,也按此方式处理。聊天页面会移除这条排队状态并提示重新发送,后续排队消息继续处理。受管工具执行时仍重新校验账户、归属和当前要求。旧队列项不按用户名补造身份,不能用于受管JSON操作;需要用户重新发送已认证请求。持久Goal工作器消费输入时继续使用原有attempt owner校验,没有转换成免租约的账户路径。
|
||||
Web排队消息从V199起保存入队时已认证账户的内部ID,普通Web续跑同时携带当前会话工作区;V201还在入队时保存选定的受管Goal ID。选定Goal的排队消息在开始执行前复查账户与Goal;若已失效,只保存用户文字并提示重新发送,不启动Agent。升级前没有选择快照的队列行,若会话有受管Goal历史,也按此方式处理。聊天页面会移除这条排队状态并提示重新发送,后续排队消息继续处理。持久Goal工作器也在执行排队消息前校验原选定Goal与账户;不匹配或已撤权时,保存用户正文和持久的助手告知,再继续后续队列。Goal暂停时保留已领取的队列行,恢复后再处理。受管工具执行时仍重新校验账户、归属和当前要求。旧队列项不按用户名补造身份,不能用于受管JSON操作;需要用户重新发送已认证请求。持久Goal工作器仍校验原有attempt owner及租约,队列校验不能代替它。
|
||||
|
||||
恢复执行会收到先核实已有证据、不要重放未知副作用的提示。首次恢复执行若在实际运行前延期,下一次领取仍保留恢复关联;已经执行过后的普通续跑不会因此变成新恢复。
|
||||
|
||||
|
||||
@ -15,6 +15,7 @@ class GoalJsonExternalApprovalIntegrationTest extends GoalJsonHttpRuntimeIntegra
|
||||
@Override
|
||||
@ParameterizedTest
|
||||
@CsvSource({"false,approval,true", "true,approval,true",
|
||||
"false,scheduled-queued,true", "true,scheduled-queued,true",
|
||||
"false,terminal-approval,true", "true,terminal-approval,true",
|
||||
"false,legacy-terminal-approval,true", "true,legacy-terminal-approval,true",
|
||||
"false,originless-terminal-approval,true", "true,originless-terminal-approval,true",
|
||||
|
||||
@ -77,7 +77,7 @@ class GoalJsonHttpRuntimeIntegrationTest {
|
||||
|
||||
@org.junit.jupiter.params.ParameterizedTest
|
||||
@org.junit.jupiter.params.provider.CsvSource({"false,sync,true", "true,sync,true", "false,stream,true", "true,stream,true",
|
||||
"false,scheduled,true", "true,scheduled,true", "false,recovered,true", "true,recovered,true",
|
||||
"false,scheduled,true", "true,scheduled,true", "false,scheduled-queued,true", "true,scheduled-queued,true", "false,recovered,true", "true,recovered,true",
|
||||
"false,scheduled,false", "true,scheduled,false", "false,recovered,false", "true,recovered,false",
|
||||
"false,queued,true", "false,reuse,true", "true,reuse,true", "false,recheck,true", "true,recheck,true",
|
||||
"false,supervised,true", "true,supervised,true", "false,supervised-recovered,true", "true,supervised-recovered,true",
|
||||
@ -591,7 +591,17 @@ class GoalJsonHttpRuntimeIntegrationTest {
|
||||
runner.cancel(goal.getId());
|
||||
}
|
||||
} else if (scheduled) {
|
||||
Long queuedInputId = null;
|
||||
if (entry.equals("scheduled-queued")) {
|
||||
var queuedInput = new vip.mate.channel.web.ConversationInputQueueStore(jdbc, json).enqueue(
|
||||
conversation, agentId, username, message, List.of(), userId, goal.getId(),
|
||||
java.time.LocalDateTime.now());
|
||||
queuedInputId = queuedInput.id();
|
||||
assertEquals(goal.getId(), queuedInput.selectedGoalId());
|
||||
}
|
||||
SegmentOutcome outcome = runner.run(run, message, recovered);
|
||||
if (queuedInputId != null) assertEquals("consumed", jdbc.queryForObject(
|
||||
"SELECT state FROM mate_conversation_input_queue WHERE id=?", String.class, queuedInputId));
|
||||
assertEquals(accepted ? GoalStatus.COMPLETED : GoalStatus.ACTIVE, goals.getById(goal.getId()).getStatus(), outcome.toString());
|
||||
if (!accepted) assertInstanceOf(SegmentOutcome.Retry.class, outcome, "Runner must consume the actual rejected-completion event");
|
||||
var savedAttempt = attempts.get(run.attempt().id());
|
||||
|
||||
@ -61,7 +61,8 @@ class GoalSegmentRunnerTest {
|
||||
if(input==null) return java.util.Optional.empty();
|
||||
return java.util.Optional.of(new ConversationInputQueueStore.QueuedInput(input.id(),input.conversationId(),
|
||||
input.agentId(),input.createdBy(),input.message(),input.contentParts(),"claimed",
|
||||
inv.getArgument(1),input.persistedMessageId(),null,input.createdAt(),LocalDateTime.now()));
|
||||
inv.getArgument(1),input.persistedMessageId(),null,input.createdAt(),LocalDateTime.now(),
|
||||
input.requesterUserId(),input.selectedGoalId()));
|
||||
});
|
||||
when(inputQueue.bindMessage(anyLong(),anyString(),anyLong(),any())).thenReturn(true);
|
||||
when(inputQueue.consume(anyLong(),anyString(),any())).thenReturn(true);
|
||||
@ -119,6 +120,110 @@ class GoalSegmentRunnerTest {
|
||||
verify(conversations).saveMessage("conv","user","new user instruction",null,"queued");
|
||||
}
|
||||
|
||||
@Test void managedGoalDoesNotExecuteQueuedInputSelectedForAnotherGoal() {
|
||||
goal.setJsonAcceptanceRequired(true);
|
||||
goal.setStatus(vip.mate.goal.model.GoalStatus.ACTIVE);
|
||||
var now = LocalDateTime.now();
|
||||
durableInputs.add(new ConversationInputQueueStore.QueuedInput(99L, "conv", 2L, "alice",
|
||||
"instruction for another Goal", List.of(), "queued", null, null, null,
|
||||
now, now, 42L, 999L));
|
||||
when(agents.chatStructuredStream(eq(2L), anyString(), eq("conv"), eq("alice"), isNull(), any()))
|
||||
.thenReturn(Flux.just(new AgentService.StreamDelta("incorrect execution", null)));
|
||||
|
||||
runner.run(goal, "continue", false);
|
||||
|
||||
verify(agents, never()).chatStructuredStream(any(), any(), any(), any(), any(), any());
|
||||
verify(conversations).saveMessage("conv", "user", "instruction for another Goal", List.of(), "queued");
|
||||
verify(conversations).saveMessage(eq("conv"),eq("assistant"),contains("was not run"),
|
||||
eq(List.of()),eq("completed"));
|
||||
verify(inputQueue).consume(eq(99L), anyString(), any());
|
||||
}
|
||||
|
||||
@Test void managedGoalExecutesCurrentSelectedQueuedInput() {
|
||||
goal.setJsonAcceptanceRequired(true);
|
||||
goal.setStatus(vip.mate.goal.model.GoalStatus.ACTIVE);
|
||||
var approvalRuns=mock(GoalApprovalRunService.class);
|
||||
org.springframework.test.util.ReflectionTestUtils.setField(runner,"approvalRuns",approvalRuns);
|
||||
when(approvalRuns.queuedSelectionStillCurrent(any())).thenReturn(true);
|
||||
var now=LocalDateTime.now();
|
||||
durableInputs.add(new ConversationInputQueueStore.QueuedInput(100L,"conv",2L,"alice",
|
||||
"current Goal instruction",List.of(),"queued",null,null,null,
|
||||
now,now,42L,1L));
|
||||
when(agents.chatStructuredStream(eq(2L),anyString(),eq("conv"),eq("alice"),isNull(),any()))
|
||||
.thenReturn(Flux.just(new AgentService.StreamDelta("output",null),
|
||||
AgentService.StreamDelta.event("finish_reason",Map.of("reason","normal"))));
|
||||
|
||||
runner.run(goal,"continue",false);
|
||||
|
||||
verify(agents).chatStructuredStream(eq(2L),eq("current Goal instruction"),eq("conv"),
|
||||
eq("alice"),isNull(),any());
|
||||
verify(inputQueue).consume(eq(100L),anyString(),any());
|
||||
verify(approvalRuns).queuedSelectionStillCurrent(argThat(origin ->
|
||||
origin.selectedGoalId().equals(1L) && origin.requesterUserId().equals(42L)));
|
||||
}
|
||||
|
||||
@Test void pausedManagedGoalPreservesSelectedQueuedInputForResume() {
|
||||
goal.setJsonAcceptanceRequired(true);
|
||||
goal.setStatus(vip.mate.goal.model.GoalStatus.PAUSED);
|
||||
var now=LocalDateTime.now();
|
||||
durableInputs.add(new ConversationInputQueueStore.QueuedInput(101L,"conv",2L,"alice",
|
||||
"paused Goal instruction",List.of(),"queued",null,null,null,
|
||||
now,now,42L,1L));
|
||||
|
||||
var outcome=runner.run(goal,"continue",false);
|
||||
|
||||
assertInstanceOf(SegmentOutcome.Cancelled.class,outcome);
|
||||
verify(agents,never()).chatStructuredStream(any(),any(),any(),any(),any(),any());
|
||||
verify(conversations,never()).saveMessage("conv","user","paused Goal instruction",List.of(),"queued");
|
||||
verify(inputQueue,never()).consume(eq(101L),anyString(),any());
|
||||
verify(inputQueue).release(eq(101L),anyString(),any());
|
||||
}
|
||||
|
||||
@Test void rejectedQueuedInputDoesNotBlockFollowingCurrentSelectedInput() {
|
||||
goal.setJsonAcceptanceRequired(true);
|
||||
goal.setStatus(vip.mate.goal.model.GoalStatus.ACTIVE);
|
||||
var approvalRuns=mock(GoalApprovalRunService.class);
|
||||
org.springframework.test.util.ReflectionTestUtils.setField(runner,"approvalRuns",approvalRuns);
|
||||
when(approvalRuns.queuedSelectionStillCurrent(any())).thenReturn(true);
|
||||
var now=LocalDateTime.now();
|
||||
durableInputs.add(new ConversationInputQueueStore.QueuedInput(102L,"conv",2L,"alice",
|
||||
"other Goal instruction",List.of(),"queued",null,null,null,
|
||||
now,now,42L,999L));
|
||||
durableInputs.add(new ConversationInputQueueStore.QueuedInput(103L,"conv",2L,"alice",
|
||||
"current Goal instruction",List.of(),"queued",null,null,null,
|
||||
now,now,42L,1L));
|
||||
when(agents.chatStructuredStream(eq(2L),anyString(),eq("conv"),eq("alice"),isNull(),any()))
|
||||
.thenReturn(Flux.just(new AgentService.StreamDelta("output",null),
|
||||
AgentService.StreamDelta.event("finish_reason",Map.of("reason","normal"))));
|
||||
|
||||
runner.run(goal,"continue",false);
|
||||
|
||||
verify(agents,times(1)).chatStructuredStream(eq(2L),eq("current Goal instruction"),
|
||||
eq("conv"),eq("alice"),isNull(),any());
|
||||
verify(inputQueue).consume(eq(102L),anyString(),any());
|
||||
verify(inputQueue).consume(eq(103L),anyString(),any());
|
||||
}
|
||||
|
||||
@Test void managedGoalDoesNotRunQueueWhenOriginalAccountIsRevoked() {
|
||||
goal.setJsonAcceptanceRequired(true);
|
||||
goal.setStatus(vip.mate.goal.model.GoalStatus.ACTIVE);
|
||||
var approvalRuns=mock(GoalApprovalRunService.class);
|
||||
org.springframework.test.util.ReflectionTestUtils.setField(runner,"approvalRuns",approvalRuns);
|
||||
when(approvalRuns.queuedSelectionStillCurrent(any())).thenReturn(false);
|
||||
var now=LocalDateTime.now();
|
||||
durableInputs.add(new ConversationInputQueueStore.QueuedInput(104L,"conv",2L,"alice",
|
||||
"revoked account instruction",List.of(),"queued",null,null,null,
|
||||
now,now,42L,1L));
|
||||
|
||||
runner.run(goal,"continue",false);
|
||||
|
||||
verify(agents,never()).chatStructuredStream(any(),any(),any(),any(),any(),any());
|
||||
verify(conversations).saveMessage("conv","user","revoked account instruction",List.of(),"queued");
|
||||
verify(conversations).saveMessage(eq("conv"),eq("assistant"),contains("was not run"),
|
||||
eq(List.of()),eq("completed"));
|
||||
verify(inputQueue).consume(eq(104L),anyString(),any());
|
||||
}
|
||||
|
||||
@Test void workerCancellationPersistsPartialEvidenceAndReleasesAdmission() throws Exception {
|
||||
var subscribed=new java.util.concurrent.CountDownLatch(1);
|
||||
var toolCancelled=new java.util.concurrent.atomic.AtomicBoolean();
|
||||
|
||||
Loading…
Reference in New Issue
Block a user