mirror of
https://gitee.com/mateos/mateclaw.git
synced 2026-09-15 03:55:09 +08:00
fix: fence runtime goal completion with current authenticated owner
This commit is contained in:
parent
4a7f0e211e
commit
8cca7159fc
@ -202,7 +202,7 @@ public class GoalEvaluationNode implements NodeAction {
|
|||||||
// Completion is the deterministic "all criteria passed" signal the
|
// Completion is the deterministic "all criteria passed" signal the
|
||||||
// evaluator already folded into result.completed() — no score gate.
|
// evaluator already folded into result.completed() — no score gate.
|
||||||
if (result.completed()) {
|
if (result.completed()) {
|
||||||
GoalEntity completed = goalService.markEvaluatedCompleted(refreshed.getId(), result);
|
GoalEntity completed = goalService.markRuntimeEvaluatedCompleted(refreshed.getId(), result, accessor.chatOrigin());
|
||||||
return MateClawStateAccessor.output()
|
return MateClawStateAccessor.output()
|
||||||
.goalEvaluationResult(result.toMap())
|
.goalEvaluationResult(result.toMap())
|
||||||
.goalEvaluatedThisRun(true)
|
.goalEvaluatedThisRun(true)
|
||||||
|
|||||||
@ -56,12 +56,16 @@ public interface GoalService {
|
|||||||
GoalEntity resume(Long id, String username);
|
GoalEntity resume(Long id, String username);
|
||||||
GoalEntity abandon(Long id, String username);
|
GoalEntity abandon(Long id, String username);
|
||||||
|
|
||||||
/** Flip active->completed. Writes a 'completed' event. */
|
/** Trusted platform completion. Runtime callers must use markRuntimeCompleted to carry their identity. */
|
||||||
GoalEntity markCompleted(Long id, GoalEvaluationResult result);
|
GoalEntity markCompleted(Long id, GoalEvaluationResult result);
|
||||||
|
|
||||||
/** Complete an evaluator result only if the current active checklist still passes with evidence. */
|
/** Trusted platform evaluation completion. Runtime callers must use markRuntimeEvaluatedCompleted. */
|
||||||
GoalEntity markEvaluatedCompleted(Long id, GoalEvaluationResult result);
|
GoalEntity markEvaluatedCompleted(Long id, GoalEvaluationResult result);
|
||||||
|
|
||||||
|
/** Runtime entry points carry server-issued identity; selected JSON goals also fence the completing owner. */
|
||||||
|
GoalEntity markRuntimeCompleted(Long id, GoalEvaluationResult result, vip.mate.agent.context.ChatOrigin origin);
|
||||||
|
GoalEntity markRuntimeEvaluatedCompleted(Long id, GoalEvaluationResult result, vip.mate.agent.context.ChatOrigin origin);
|
||||||
|
|
||||||
/** Flip active->exhausted with the reason that triggered it. */
|
/** Flip active->exhausted with the reason that triggered it. */
|
||||||
GoalEntity markExhausted(Long id, String reason);
|
GoalEntity markExhausted(Long id, String reason);
|
||||||
|
|
||||||
|
|||||||
@ -73,6 +73,10 @@ public class GoalServiceImpl implements GoalService {
|
|||||||
*/
|
*/
|
||||||
private vip.mate.memory.spi.MemoryManager memoryManager;
|
private vip.mate.memory.spi.MemoryManager memoryManager;
|
||||||
private GoalJsonBindingService jsonBindings;
|
private GoalJsonBindingService jsonBindings;
|
||||||
|
private ManagedGoalJsonService managedArtifacts;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
public void setManagedArtifacts(ManagedGoalJsonService managedArtifacts) { this.managedArtifacts = managedArtifacts; }
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
public void setJsonBindings(GoalJsonBindingService jsonBindings) {
|
public void setJsonBindings(GoalJsonBindingService jsonBindings) {
|
||||||
@ -380,7 +384,7 @@ public class GoalServiceImpl implements GoalService {
|
|||||||
@Override
|
@Override
|
||||||
@Transactional
|
@Transactional
|
||||||
public GoalEntity markCompleted(Long id, GoalEvaluationResult result) {
|
public GoalEntity markCompleted(Long id, GoalEvaluationResult result) {
|
||||||
return completeGoal(id, result, false);
|
return completeGoal(id, result, false, null, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -391,10 +395,28 @@ public class GoalServiceImpl implements GoalService {
|
|||||||
throw new MateClawException("err.goal.completion_not_verified", 409,
|
throw new MateClawException("err.goal.completion_not_verified", 409,
|
||||||
"Automatic completion requires a completed evaluation");
|
"Automatic completion requires a completed evaluation");
|
||||||
}
|
}
|
||||||
return completeGoal(id, result, true);
|
return completeGoal(id, result, true, null, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
private GoalEntity completeGoal(Long id, GoalEvaluationResult result, boolean evaluated) {
|
@Override
|
||||||
|
@Transactional
|
||||||
|
public GoalEntity markRuntimeCompleted(Long id, GoalEvaluationResult result, vip.mate.agent.context.ChatOrigin origin) {
|
||||||
|
return completeGoal(id, result, false, origin, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional
|
||||||
|
public GoalEntity markRuntimeEvaluatedCompleted(Long id, GoalEvaluationResult result, vip.mate.agent.context.ChatOrigin origin) {
|
||||||
|
if (result == null || !result.completed()
|
||||||
|
|| !GoalEvaluationResult.DECISION_COMPLETED.equals(result.decision())) {
|
||||||
|
throw new MateClawException("err.goal.completion_not_verified", 409,
|
||||||
|
"Automatic completion requires a completed evaluation");
|
||||||
|
}
|
||||||
|
return completeGoal(id, result, true, origin, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private GoalEntity completeGoal(Long id, GoalEvaluationResult result, boolean evaluated,
|
||||||
|
vip.mate.agent.context.ChatOrigin origin, boolean runtimeCaller) {
|
||||||
boolean[] transitioned = {false};
|
boolean[] transitioned = {false};
|
||||||
var jsonProof = new java.util.concurrent.atomic.AtomicReference<List<GoalJsonBindingService.State>>(List.of());
|
var jsonProof = new java.util.concurrent.atomic.AtomicReference<List<GoalJsonBindingService.State>>(List.of());
|
||||||
GoalEntity g = retryOptimistic(id, "markCompleted", fresh -> {
|
GoalEntity g = retryOptimistic(id, "markCompleted", fresh -> {
|
||||||
@ -408,6 +430,12 @@ public class GoalServiceImpl implements GoalService {
|
|||||||
}
|
}
|
||||||
return null; // idempotent
|
return null; // idempotent
|
||||||
}
|
}
|
||||||
|
ManagedGoalJsonService.RuntimeScope runtime = null;
|
||||||
|
if (runtimeCaller && fresh.isJsonAcceptanceRequired()) {
|
||||||
|
if (managedArtifacts == null) throw new MateClawException(409, "Managed JSON runtime verification is unavailable");
|
||||||
|
runtime = managedArtifacts.runtimeGoal(origin);
|
||||||
|
if (runtime.goal().id() != fresh.getId()) throw new MateClawException(403, "Completion runtime goal mismatch");
|
||||||
|
}
|
||||||
if (evaluated && result.evaluationRevision() != fresh.getEvaluationRevision()) {
|
if (evaluated && result.evaluationRevision() != fresh.getEvaluationRevision()) {
|
||||||
throw new MateClawException("err.goal.completion_not_verified", 409,
|
throw new MateClawException("err.goal.completion_not_verified", 409,
|
||||||
"Automatic completion requires the current evaluation definition revision");
|
"Automatic completion requires the current evaluation definition revision");
|
||||||
@ -441,6 +469,7 @@ public class GoalServiceImpl implements GoalService {
|
|||||||
if (jsonBindings == null) throw new MateClawException("err.goal.json_acceptance_required", 409,
|
if (jsonBindings == null) throw new MateClawException("err.goal.json_acceptance_required", 409,
|
||||||
"Managed JSON verification service is unavailable");
|
"Managed JSON verification service is unavailable");
|
||||||
jsonProof.set(jsonBindings.requireForCompletion(fresh));
|
jsonProof.set(jsonBindings.requireForCompletion(fresh));
|
||||||
|
if (runtime != null) ManagedGoalJsonService.verifyLease(runtime);
|
||||||
}
|
}
|
||||||
bumpVersionAndTime(w);
|
bumpVersionAndTime(w);
|
||||||
transitioned[0] = true;
|
transitioned[0] = true;
|
||||||
|
|||||||
@ -166,7 +166,7 @@ public class GoalManagementTool {
|
|||||||
true, "manual", 0, 0L,
|
true, "manual", 0, 0L,
|
||||||
java.util.List.of(), null);
|
java.util.List.of(), null);
|
||||||
try {
|
try {
|
||||||
GoalEntity completed = goalService.markCompleted(goal.getId(), synthetic);
|
GoalEntity completed = goalService.markRuntimeCompleted(goal.getId(), synthetic, ChatOrigin.from(ctx));
|
||||||
// Broadcast a goal_completed event with the same shape as the
|
// Broadcast a goal_completed event with the same shape as the
|
||||||
// GoalEvaluationNode auto-completed path, so the frontend
|
// GoalEvaluationNode auto-completed path, so the frontend
|
||||||
// handler doesn't need to branch on which path completed it.
|
// handler doesn't need to branch on which path completed it.
|
||||||
|
|||||||
@ -34,3 +34,5 @@ After publication, call `POST /checks/{criterionKey}` with `expectedRequirementR
|
|||||||
This is an explicit per-goal managed JSON protocol with a limited scope. The broad execution-evidence ledger retains its existing prerequisites for global ENFORCE. Ordinary tool-success text and diagnostic MATCH results never become bindings automatically. Backend services cover success, invalidation, races and rollback; full browser/service flows, restart and external database validation are still in progress.
|
This is an explicit per-goal managed JSON protocol with a limited scope. The broad execution-evidence ledger retains its existing prerequisites for global ENFORCE. Ordinary tool-success text and diagnostic MATCH results never become bindings automatically. Backend services cover success, invalidation, races and rollback; full browser/service flows, restart and external database validation are still in progress.
|
||||||
|
|
||||||
ReAct, Plan and persistent-goal continuations receive managed JSON instructions. Business-skill tool allowlists retain the three goal-level read, publish and check tools, while service identity checks and child-agent restrictions still apply. For selected goals, follow-up and scheduling projections cannot end on a model completion claim or segment Complete alone: the Goal must already have committed completed status. Rejected automatic completion produces a continue result with recheck guidance.
|
ReAct, Plan and persistent-goal continuations receive managed JSON instructions. Business-skill tool allowlists retain the three goal-level read, publish and check tools, while service identity checks and child-agent restrictions still apply. For selected goals, follow-up and scheduling projections cannot end on a model completion claim or segment Complete alone: the Goal must already have committed completed status. Rejected automatic completion produces a continue result with recheck guidance.
|
||||||
|
|
||||||
|
Runtime completion must also carry server-issued identity. The completeGoal tool and automatic evaluation node use runtime completion entry points that recheck the enabled account, current goal/conversation/workspace/agent and scheduled-owner leases in the same transaction as all bindings. Valid bindings do not authorize an expired owner, a different goal or a revoked identity to complete. Internal platform completion APIs retain the binding gate; they are not identity-free model or HTTP entry points.
|
||||||
|
|||||||
@ -34,3 +34,5 @@
|
|||||||
这是逐 Goal 显式选择的有限受管 JSON 协议;宽泛执行证据账本的全局 ENFORCE 配置仍遵循原有准入限制。此协议不把任何普通工具成功文本或诊断 MATCH 自动升级为绑定。后端服务已覆盖成功、失效、竞争和回滚;完整浏览器服务闭环、重启和外部数据库验证仍在推进。
|
这是逐 Goal 显式选择的有限受管 JSON 协议;宽泛执行证据账本的全局 ENFORCE 配置仍遵循原有准入限制。此协议不把任何普通工具成功文本或诊断 MATCH 自动升级为绑定。后端服务已覆盖成功、失效、竞争和回滚;完整浏览器服务闭环、重启和外部数据库验证仍在推进。
|
||||||
|
|
||||||
代理在 ReAct、Plan 和持久 Goal 续跑入口都会收到受管 JSON 操作指引。业务技能的工具白名单保留读取、发布和检查这三个 Goal 通用工具,仍执行服务端身份校验与子代理禁用。选中模式下,follow-up 和调度投影不能凭模型的“已完成”或 segment Complete 声明结束;必须先有已提交的 Goal completed 状态。自动完成被拒绝时,向运行时返回 continue 和重检指引,不暴露已接受完成的信号。
|
代理在 ReAct、Plan 和持久 Goal 续跑入口都会收到受管 JSON 操作指引。业务技能的工具白名单保留读取、发布和检查这三个 Goal 通用工具,仍执行服务端身份校验与子代理禁用。选中模式下,follow-up 和调度投影不能凭模型的“已完成”或 segment Complete 声明结束;必须先有已提交的 Goal completed 状态。自动完成被拒绝时,向运行时返回 continue 和重检指引,不暴露已接受完成的信号。
|
||||||
|
|
||||||
|
运行时完成也必须携带服务端身份:completeGoal 工具与自动评估节点使用专门的 runtime 完成入口,在同一事务中复查启用账户、当前 Goal/对话/工作区/Agent 和调度 owner 租约,再检查所有绑定。即使绑定仍有效,旧租约、跨 Goal 或已撤销的身份也不能发起完成。平台内部完成 API 仍执行绑定门;它不是向模型或 HTTP 暴露的免身份入口。
|
||||||
|
|||||||
@ -220,10 +220,10 @@ class GoalEvaluationNodeContinuationTest {
|
|||||||
Fixture f = new Fixture();
|
Fixture f = new Fixture();
|
||||||
var completed = new GoalEvaluationResult(1.0, "", "completed", true, "fixture", 1, 0, List.of(), null);
|
var completed = new GoalEvaluationResult(1.0, "", "completed", true, "fixture", 1, 0, List.of(), null);
|
||||||
when(f.evaluationService.evaluate(any(), anyList(), anyString())).thenReturn(completed);
|
when(f.evaluationService.evaluate(any(), anyList(), anyString())).thenReturn(completed);
|
||||||
when(f.goalService.markEvaluatedCompleted(eq(1L), eq(completed)))
|
when(f.goalService.markRuntimeEvaluatedCompleted(eq(1L), eq(completed), any()))
|
||||||
.thenThrow(new vip.mate.exception.MateClawException(409, "current criteria changed"));
|
.thenThrow(new vip.mate.exception.MateClawException(409, "current criteria changed"));
|
||||||
var out = f.node().apply(f.state(FinishReason.NORMAL.getValue(), 0, 0));
|
var out = f.node().apply(f.state(FinishReason.NORMAL.getValue(), 0, 0));
|
||||||
verify(f.goalService).markEvaluatedCompleted(1L, completed);
|
verify(f.goalService).markRuntimeEvaluatedCompleted(eq(1L), eq(completed), any());
|
||||||
verify(f.goalService, never()).markCompleted(any(), any());
|
verify(f.goalService, never()).markCompleted(any(), any());
|
||||||
@SuppressWarnings("unchecked")
|
@SuppressWarnings("unchecked")
|
||||||
var events = (List<GraphEventPublisher.GraphEvent>) out.get(MateClawStateKeys.PENDING_EVENTS);
|
var events = (List<GraphEventPublisher.GraphEvent>) out.get(MateClawStateKeys.PENDING_EVENTS);
|
||||||
@ -237,7 +237,7 @@ class GoalEvaluationNodeContinuationTest {
|
|||||||
when(f.goalService.getById(1L)).thenReturn(goal);
|
when(f.goalService.getById(1L)).thenReturn(goal);
|
||||||
var claim = new GoalEvaluationResult(1, "done", "completed", true, "fixture", 1, 0, List.of(), null);
|
var claim = new GoalEvaluationResult(1, "done", "completed", true, "fixture", 1, 0, List.of(), null);
|
||||||
when(f.evaluationService.evaluate(any(), anyList(), anyString())).thenReturn(claim);
|
when(f.evaluationService.evaluate(any(), anyList(), anyString())).thenReturn(claim);
|
||||||
when(f.goalService.markEvaluatedCompleted(1L, claim)).thenThrow(new vip.mate.exception.MateClawException(409, "binding missing"));
|
when(f.goalService.markRuntimeEvaluatedCompleted(eq(1L), eq(claim), any())).thenThrow(new vip.mate.exception.MateClawException(409, "binding missing"));
|
||||||
var out = f.node().apply(f.state(FinishReason.NORMAL.getValue(), 0, 0));
|
var out = f.node().apply(f.state(FinishReason.NORMAL.getValue(), 0, 0));
|
||||||
var result = (Map<?, ?>) out.get(MateClawStateKeys.GOAL_EVALUATION_RESULT);
|
var result = (Map<?, ?>) out.get(MateClawStateKeys.GOAL_EVALUATION_RESULT);
|
||||||
assertEquals(false, result.get("completed"));
|
assertEquals(false, result.get("completed"));
|
||||||
|
|||||||
@ -554,4 +554,51 @@ class GoalJsonAcceptanceIntegrationTest {
|
|||||||
assertEquals(GoalStatus.COMPLETED, goals.getById(goal.getId()).getStatus());
|
assertEquals(GoalStatus.COMPLETED, goals.getById(goal.getId()).getStatus());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test void expiredRuntimeCannotCompleteEvenWithCurrentPassingBindings() {
|
||||||
|
GoalEntity goal = goal(true);
|
||||||
|
goals.appendCriterion(goal.getId(), "report", alice);
|
||||||
|
var evaluation = new GoalEvaluationResult(1, "checked", "completed", true, "fixture", 1, 0,
|
||||||
|
List.of(new GoalChecklistVerdict.CriterionVerdict("C1", true, "semantic fixture")), null);
|
||||||
|
goals.recordEvaluation(goal.getId(), evaluation, 1, 1);
|
||||||
|
acceptance.configure(goal.getId(), "r", request(0, "summary"), alice);
|
||||||
|
var run = claimed(goal);
|
||||||
|
var origin = attemptOrigin(goal, run);
|
||||||
|
var version = artifacts.publishForRuntime(origin, "report", publication(0, "{\"summary\":true}"));
|
||||||
|
bindings.checkForRuntime(origin, "r", checkRequest(1, version));
|
||||||
|
var properties = new vip.mate.goal.config.GoalProperties(); properties.setEnabled(true);
|
||||||
|
var tool = new vip.mate.tool.builtin.GoalManagementTool(goals, properties, new com.fasterxml.jackson.databind.ObjectMapper(), null);
|
||||||
|
jdbc.update("UPDATE mate_goal_attempt SET lease_until=? WHERE attempt_id=?", java.time.LocalDateTime.now().minusSeconds(1), run.attempt().id());
|
||||||
|
assertTrue(tool.completeGoal(origin.toToolContext()).contains("error"));
|
||||||
|
assertEquals(GoalStatus.ACTIVE, goals.getById(goal.getId()).getStatus());
|
||||||
|
jdbc.update("UPDATE mate_goal_attempt SET lease_until=? WHERE attempt_id=?", java.time.LocalDateTime.now().plusSeconds(60), run.attempt().id());
|
||||||
|
assertTrue(tool.completeGoal(origin.toToolContext()).contains("\"status\":\"completed\""));
|
||||||
|
}
|
||||||
|
|
||||||
|
@ParameterizedTest @ValueSource(booleans = {false, true})
|
||||||
|
void runtimeCompletionRejectsMissingForeignAndRevokedIdentity(boolean automatic) {
|
||||||
|
GoalEntity goal = goal(false);
|
||||||
|
goals.appendCriterion(goal.getId(), "report", alice);
|
||||||
|
var evaluation = new GoalEvaluationResult(1, "checked", "completed", true, "fixture", 1, 0,
|
||||||
|
List.of(new GoalChecklistVerdict.CriterionVerdict("C1", true, "semantic fixture")), null);
|
||||||
|
goals.recordEvaluation(goal.getId(), evaluation, 1, 1);
|
||||||
|
acceptance.configure(goal.getId(), "r", request(0, "summary"), alice);
|
||||||
|
var version = artifacts.publish(goal.getId(), "report", publication(0, "{\"summary\":true}"), alice);
|
||||||
|
bindings.check(goal.getId(), "r", checkRequest(1, version), alice);
|
||||||
|
var owner = accountOrigin(goal, alice);
|
||||||
|
for (var origin : List.of(vip.mate.agent.context.ChatOrigin.EMPTY, accountOrigin(goal, bob),
|
||||||
|
owner.withAgent(999L), owner.withWorkspace(999L, null), accountOrigin(goal(false), alice))) {
|
||||||
|
assertThrows(MateClawException.class, () -> runtimeComplete(goal, evaluation, origin, automatic));
|
||||||
|
}
|
||||||
|
jdbc.update("UPDATE mate_user SET enabled=FALSE WHERE username=?", alice);
|
||||||
|
assertThrows(MateClawException.class, () -> runtimeComplete(goal, evaluation, owner, automatic));
|
||||||
|
assertEquals(GoalStatus.ACTIVE, goals.getById(goal.getId()).getStatus());
|
||||||
|
jdbc.update("UPDATE mate_user SET enabled=TRUE WHERE username=?", alice);
|
||||||
|
assertEquals(GoalStatus.COMPLETED, runtimeComplete(goal, evaluation, owner, automatic).getStatus());
|
||||||
|
}
|
||||||
|
|
||||||
|
private GoalEntity runtimeComplete(GoalEntity goal, GoalEvaluationResult evaluation, vip.mate.agent.context.ChatOrigin origin, boolean automatic) {
|
||||||
|
return automatic ? goals.markRuntimeEvaluatedCompleted(goal.getId(), evaluation, origin)
|
||||||
|
: goals.markRuntimeCompleted(goal.getId(), evaluation, origin);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -142,14 +142,14 @@ class GoalManagementToolTest {
|
|||||||
when(goalService.findActiveByConversation("conv-1")).thenReturn(null);
|
when(goalService.findActiveByConversation("conv-1")).thenReturn(null);
|
||||||
String result = tool.completeGoal(ctxWith("conv-1", 10L, "alice"));
|
String result = tool.completeGoal(ctxWith("conv-1", 10L, "alice"));
|
||||||
assertTrue(result.contains("No active goal"));
|
assertTrue(result.contains("No active goal"));
|
||||||
verify(goalService, never()).markCompleted(any(), any(GoalEvaluationResult.class));
|
verify(goalService, never()).markRuntimeCompleted(any(), any(GoalEvaluationResult.class), any());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void completeGoal_happyPath_callsMarkCompleted() {
|
void completeGoal_happyPath_callsMarkCompleted() {
|
||||||
when(goalService.findActiveByConversation("conv-1")).thenReturn(goal(GoalStatus.ACTIVE));
|
when(goalService.findActiveByConversation("conv-1")).thenReturn(goal(GoalStatus.ACTIVE));
|
||||||
GoalEntity completed = goal(GoalStatus.COMPLETED);
|
GoalEntity completed = goal(GoalStatus.COMPLETED);
|
||||||
when(goalService.markCompleted(eq(123L), any(GoalEvaluationResult.class)))
|
when(goalService.markRuntimeCompleted(eq(123L), any(GoalEvaluationResult.class), any()))
|
||||||
.thenReturn(completed);
|
.thenReturn(completed);
|
||||||
when(goalService.toResponse(any())).thenReturn(new vip.mate.goal.model.GoalResponse());
|
when(goalService.toResponse(any())).thenReturn(new vip.mate.goal.model.GoalResponse());
|
||||||
String result = tool.completeGoal(ctxWith("conv-1", 10L, "alice"));
|
String result = tool.completeGoal(ctxWith("conv-1", 10L, "alice"));
|
||||||
@ -246,7 +246,7 @@ class GoalManagementToolTest {
|
|||||||
assertTrue(result.contains("\"status\":\"paused\""));
|
assertTrue(result.contains("\"status\":\"paused\""));
|
||||||
assertTrue(result.contains("Need the deployment hostname"));
|
assertTrue(result.contains("Need the deployment hostname"));
|
||||||
verify(streamTracker).broadcastObject(eq("conv-1"), eq("goal_updated"), any());
|
verify(streamTracker).broadcastObject(eq("conv-1"), eq("goal_updated"), any());
|
||||||
verify(goalService, never()).markCompleted(any(), any());
|
verify(goalService, never()).markRuntimeCompleted(any(), any(), any());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user