mirror of
https://gitee.com/mateos/mateclaw.git
synced 2026-09-13 03:13:41 +08:00
fix: enforce team deliverable completion gates
This commit is contained in:
parent
cc0661c07c
commit
fb7cb6c04a
@ -1,6 +1,7 @@
|
||||
package vip.mate.team.service;
|
||||
|
||||
import cn.hutool.core.util.IdUtil;
|
||||
import cn.hutool.json.JSONUtil;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.scheduling.annotation.Scheduled;
|
||||
@ -247,6 +248,49 @@ public class TeamDispatchService {
|
||||
return;
|
||||
}
|
||||
if (TeamTaskStatus.IN_PROGRESS.equals(current.getStatus())) {
|
||||
String invalidReason = invalidResultReason(current, reply);
|
||||
if (invalidReason != null) {
|
||||
int attempts = current.getDispatchCount() == null ? 0 : current.getDispatchCount();
|
||||
if (attempts < TeamTaskService.MAX_DISPATCHES
|
||||
&& taskService.requeueUnusableResult(task.getId(), invalidReason)) {
|
||||
log.warn("Team task #{} produced an unusable result on attempt {}/{}; requeued: {}",
|
||||
task.getTaskNumber(), attempts, TeamTaskService.MAX_DISPATCHES,
|
||||
invalidReason);
|
||||
broadcast(task, "team_task_retrying", Map.of("reason", invalidReason));
|
||||
return;
|
||||
}
|
||||
boolean failed = taskService.failTask(task.getId(), invalidReason);
|
||||
TeamTaskEntity failedTask = taskService.getTask(task.getId());
|
||||
if (failed) {
|
||||
broadcast(task, "team_task_failed", Map.of("reason", invalidReason));
|
||||
announceService.announceTaskSettled(failedTask);
|
||||
}
|
||||
return;
|
||||
}
|
||||
String terminalCheckpoint = taskService.checkpointTerminalTag(current);
|
||||
if (terminalCheckpoint != null) {
|
||||
String terminalEvidence = "[checkpoint:" + terminalCheckpoint + "] acknowledged";
|
||||
boolean terminalAlreadyAcknowledged = taskService.listComments(current.getId()).stream()
|
||||
.anyMatch(comment -> terminalEvidence.equals(comment.getContent()));
|
||||
if (terminalAlreadyAcknowledged) {
|
||||
List<Long> released = taskService.completeTask(task.getId(), null,
|
||||
truncate(reply, MAX_RESULT_CHARS));
|
||||
TeamTaskEntity completed = taskService.getTask(task.getId());
|
||||
log.info("Team task #{} completed after deferred {} acknowledgement "
|
||||
+ "({} dependents released)",
|
||||
task.getTaskNumber(), terminalCheckpoint, released.size());
|
||||
broadcast(task, "team_task_completed", Map.of("status", TeamTaskStatus.COMPLETED));
|
||||
announceService.announceTaskSettled(completed);
|
||||
return;
|
||||
}
|
||||
int percent = current.getProgressPercent() == null
|
||||
? 1 : Math.max(1, current.getProgressPercent());
|
||||
taskService.updateProgress(task.getId(), null, percent,
|
||||
"waiting for " + terminalCheckpoint + " checkpoint");
|
||||
log.info("Team task #{} parked as long-running checkpoint tracker until {}",
|
||||
task.getTaskNumber(), terminalCheckpoint);
|
||||
return;
|
||||
}
|
||||
List<Long> released = taskService.completeTask(task.getId(), null,
|
||||
truncate(reply == null || reply.isBlank() ? "(no output)" : reply,
|
||||
MAX_RESULT_CHARS));
|
||||
@ -271,6 +315,30 @@ public class TeamDispatchService {
|
||||
announceService.announceTaskSettled(current);
|
||||
}
|
||||
|
||||
private String invalidResultReason(TeamTaskEntity task, String reply) {
|
||||
if (reply == null || reply.isBlank()) {
|
||||
return "member produced no result";
|
||||
}
|
||||
String normalized = reply.strip().toLowerCase();
|
||||
if (normalized.contains("failed to generate a response, please retry")
|
||||
|| normalized.equals("(no output)")) {
|
||||
return "member response generation failed";
|
||||
}
|
||||
if (requiresDeliverable(task) && taskService.listDeliverables(task).isEmpty()) {
|
||||
return "required deliverable was not attached";
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private boolean requiresDeliverable(TeamTaskEntity task) {
|
||||
try {
|
||||
return task.getMetadata() != null
|
||||
&& JSONUtil.parseObj(task.getMetadata()).getBool("deliverableRequired", false);
|
||||
} catch (Exception ignored) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/** Per-prerequisite and whole-section caps keeping the envelope bounded. */
|
||||
static final int MAX_PREREQ_RESULT_CHARS = 1500;
|
||||
static final int MAX_PREREQ_SECTION_CHARS = 6000;
|
||||
|
||||
@ -55,6 +55,9 @@ public class TeamPlanBridge {
|
||||
/** Task subject cap; the full step text rides in the description. */
|
||||
static final int SUBJECT_MAX_CHARS = 120;
|
||||
private static final Pattern CHECKPOINT_TAG = Pattern.compile("(?i)(?:^|\\b)(R\\d{3})(?:\\b|/)");
|
||||
private static final Pattern DELIVERABLE_REQUEST = Pattern.compile(
|
||||
"(?i)(交付物|生成.{0,8}(文件|文档)|文档成稿|报告成稿|"
|
||||
+ "docx|xlsx|pptx|pdf|deliverable|document|spreadsheet|presentation)");
|
||||
|
||||
private final TeamService teamService;
|
||||
private final TeamTaskService taskService;
|
||||
@ -197,6 +200,7 @@ public class TeamPlanBridge {
|
||||
.metadata(new JSONObject()
|
||||
.set("planId", String.valueOf(planId))
|
||||
.set("stepIndex", i)
|
||||
.set("deliverableRequired", DELIVERABLE_REQUEST.matcher(step).find())
|
||||
.toString())
|
||||
.build());
|
||||
created.add(task);
|
||||
@ -258,6 +262,7 @@ public class TeamPlanBridge {
|
||||
List<TeamTaskEntity> tasks = taskService.listTasksByRun(latest.get().getId());
|
||||
if (!tasks.isEmpty()) {
|
||||
recordCheckpointEvidence(latest.get().getTeamId(), tasks, checkpointTag);
|
||||
tasks = taskService.listTasksByRun(latest.get().getId());
|
||||
return new InFlight(buildCheckpointText(tasks, checkpointTag));
|
||||
}
|
||||
}
|
||||
@ -283,6 +288,7 @@ public class TeamPlanBridge {
|
||||
.toList();
|
||||
if (checkpointTag != null) {
|
||||
recordCheckpointEvidence(teamOpt.get().getId(), tasks, checkpointTag);
|
||||
tasks = taskService.listTasksByPlan(teamOpt.get().getId(), plan.getId());
|
||||
return new InFlight(buildCheckpointText(tasks, checkpointTag));
|
||||
}
|
||||
if (!allTerminal) {
|
||||
@ -408,23 +414,45 @@ public class TeamPlanBridge {
|
||||
if ("R100".equalsIgnoreCase(checkpointTag)) {
|
||||
compact.append("(已完成第100轮检查点)");
|
||||
}
|
||||
compact.append("|证据 [checkpoint:").append(checkpointTag).append("] acknowledged");
|
||||
return compact.toString();
|
||||
}
|
||||
|
||||
private void recordCheckpointEvidence(Long teamId, List<TeamTaskEntity> tasks,
|
||||
String checkpointTag) {
|
||||
TeamTaskEntity tracker = taskService.findCheckpointTracker(teamId).orElseGet(() -> tasks.stream()
|
||||
.filter(this::isCheckpointTracker)
|
||||
TeamTaskEntity tracker = tasks.stream()
|
||||
.filter(task -> taskService.checkpointTerminalTag(task) != null)
|
||||
.findFirst()
|
||||
.orElse(tasks.get(tasks.size() - 1)));
|
||||
.orElseGet(() -> tasks.stream()
|
||||
.filter(this::isCheckpointTracker)
|
||||
.findFirst()
|
||||
.orElseGet(() -> taskService.findCheckpointTracker(teamId)
|
||||
.orElse(tasks.get(tasks.size() - 1))));
|
||||
String content = "[checkpoint:" + checkpointTag + "] acknowledged";
|
||||
taskService.addCommentOnce(tracker.getId(), TeamTaskService.AUTHOR_SYSTEM,
|
||||
"team-plan-bridge", TeamTaskService.COMMENT_NOTE, content);
|
||||
String terminalTag = taskService.checkpointTerminalTag(tracker);
|
||||
if (terminalTag != null && TeamTaskStatus.IN_PROGRESS.equals(tracker.getStatus())) {
|
||||
int current = Integer.parseInt(checkpointTag.substring(1));
|
||||
int terminal = Integer.parseInt(terminalTag.substring(1));
|
||||
int percent = terminal <= 0 ? 1
|
||||
: Math.min(99, Math.max(1, current * 100 / terminal));
|
||||
if (checkpointTag.equalsIgnoreCase(terminalTag)) {
|
||||
taskService.completeTask(tracker.getId(), null,
|
||||
"Checkpoint tracking completed at " + checkpointTag);
|
||||
eventPublisher.publishEvent(new TeamTasksDelegatedEvent(teamId));
|
||||
} else {
|
||||
taskService.updateProgress(tracker.getId(), null, percent,
|
||||
checkpointTag + "/" + terminalTag + " acknowledged");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isCheckpointTracker(TeamTaskEntity task) {
|
||||
String text = (task.getSubject() == null ? "" : task.getSubject()) + " "
|
||||
+ (task.getDescription() == null ? "" : task.getDescription());
|
||||
if (taskService.checkpointTerminalTag(task) != null) {
|
||||
return true;
|
||||
}
|
||||
String text = task.getSubject() == null ? "" : task.getSubject();
|
||||
String lower = text.toLowerCase();
|
||||
return text.contains("检查点") || text.contains("共享跟踪")
|
||||
|| lower.contains("checkpoint") || lower.contains("r001-r100");
|
||||
|
||||
@ -29,6 +29,8 @@ import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* Shared task board service. All status transitions are guarded conditional
|
||||
@ -43,6 +45,9 @@ import java.util.Set;
|
||||
@RequiredArgsConstructor
|
||||
public class TeamTaskService {
|
||||
|
||||
private static final Pattern CHECKPOINT_RANGE = Pattern.compile(
|
||||
"(?i)R(\\d{3})\\s*[-–—]\\s*R(\\d{3})");
|
||||
|
||||
/** Execution lease length; renewed by the runner while the member works. */
|
||||
static final int LOCK_MINUTES = 60;
|
||||
|
||||
@ -177,6 +182,7 @@ public class TeamTaskService {
|
||||
.eq(TeamTaskEntity::getStatus, TeamTaskStatus.PENDING)
|
||||
.set(TeamTaskEntity::getStatus, TeamTaskStatus.IN_PROGRESS)
|
||||
.set(TeamTaskEntity::getOwnerAgentId, agentId)
|
||||
.set(TeamTaskEntity::getReason, null)
|
||||
.set(TeamTaskEntity::getLockExpiresAt, newLease())) == 1;
|
||||
if (assigned) {
|
||||
projectTask(taskId);
|
||||
@ -325,6 +331,28 @@ public class TeamTaskService {
|
||||
return retried;
|
||||
}
|
||||
|
||||
/**
|
||||
* Requeue an automatically dispatched task whose member result is unusable.
|
||||
* Unlike a manual retry this deliberately preserves {@code dispatchCount},
|
||||
* so the existing dispatch circuit breaker remains the hard upper bound.
|
||||
*/
|
||||
public boolean requeueUnusableResult(Long taskId, String reason) {
|
||||
TeamTaskEntity task = taskMapper.selectById(taskId);
|
||||
boolean requeued = taskMapper.update(null, Wrappers.<TeamTaskEntity>lambdaUpdate()
|
||||
.eq(TeamTaskEntity::getId, taskId)
|
||||
.eq(TeamTaskEntity::getStatus, TeamTaskStatus.IN_PROGRESS)
|
||||
.set(TeamTaskEntity::getStatus, TeamTaskStatus.PENDING)
|
||||
.set(TeamTaskEntity::getOwnerAgentId, null)
|
||||
.set(TeamTaskEntity::getLockExpiresAt, null)
|
||||
.set(TeamTaskEntity::getReason, reason)) == 1;
|
||||
if (requeued) {
|
||||
recordEvent(task == null ? null : task.getTeamId(), taskId,
|
||||
TeamTaskEventEntity.RETRIED, AUTHOR_SYSTEM, null, reason);
|
||||
projectTask(taskId);
|
||||
}
|
||||
return requeued;
|
||||
}
|
||||
|
||||
// ==================== progress / comments ====================
|
||||
|
||||
/** Update progress and renew the execution lease in one shot. */
|
||||
@ -667,14 +695,33 @@ public class TeamTaskService {
|
||||
.eq(TeamTaskEntity::getTeamId, teamId)
|
||||
.and(candidate -> candidate
|
||||
.like(TeamTaskEntity::getSubject, "共享跟踪")
|
||||
.or().like(TeamTaskEntity::getDescription, "R001-R100")
|
||||
.or().like(TeamTaskEntity::getSubject, "checkpoint")
|
||||
.or().like(TeamTaskEntity::getDescription, "checkpoint"))
|
||||
.or().like(TeamTaskEntity::getSubject, "检查点")
|
||||
.or().like(TeamTaskEntity::getSubject, "checkpoint"))
|
||||
.orderByDesc(TeamTaskEntity::getPriority)
|
||||
.orderByDesc(TeamTaskEntity::getCreateTime)
|
||||
.last("LIMIT 1")));
|
||||
}
|
||||
|
||||
/** Terminal checkpoint tag declared by a long-running tracker, e.g. R300. */
|
||||
public String checkpointTerminalTag(TeamTaskEntity task) {
|
||||
if (task == null) {
|
||||
return null;
|
||||
}
|
||||
String description = task.getDescription() == null ? "" : task.getDescription();
|
||||
int contextStart = description.indexOf("[Plan context]");
|
||||
if (contextStart >= 0) {
|
||||
description = description.substring(0, contextStart);
|
||||
}
|
||||
String text = (task.getSubject() == null ? "" : task.getSubject()) + " " + description;
|
||||
String lower = text.toLowerCase();
|
||||
if (!text.contains("共享跟踪") && !text.contains("检查点")
|
||||
&& !lower.contains("checkpoint")) {
|
||||
return null;
|
||||
}
|
||||
Matcher matcher = CHECKPOINT_RANGE.matcher(text);
|
||||
return matcher.find() ? "R" + matcher.group(2) : null;
|
||||
}
|
||||
|
||||
public List<TeamTaskEntity> listTasks(Long teamId, List<String> statuses) {
|
||||
return listTasks(teamId, statuses, null, null);
|
||||
}
|
||||
|
||||
@ -7,6 +7,7 @@ import vip.mate.agent.AgentService;
|
||||
import vip.mate.channel.web.ChatStreamTracker;
|
||||
import vip.mate.team.model.AgentTeamEntity;
|
||||
import vip.mate.team.model.TeamTaskEntity;
|
||||
import vip.mate.team.model.TeamTaskCommentEntity;
|
||||
import vip.mate.team.model.TeamTaskStatus;
|
||||
import vip.mate.workspace.conversation.ConversationService;
|
||||
|
||||
@ -153,6 +154,100 @@ class TeamDispatchServiceTest {
|
||||
verify(announceService).announceTaskSettled(done);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("a fallback final answer is requeued instead of being reported as completed")
|
||||
void settleFallbackRequeues() {
|
||||
TeamTaskEntity running = task(1L, MEMBER_A);
|
||||
running.setStatus(TeamTaskStatus.IN_PROGRESS);
|
||||
running.setDispatchCount(1);
|
||||
when(taskService.getTask(1L)).thenReturn(running);
|
||||
when(taskService.requeueUnusableResult(1L, "member response generation failed"))
|
||||
.thenReturn(true);
|
||||
|
||||
service.settleOutcome(running,
|
||||
"I inspected the task. Failed to generate a response, please retry.");
|
||||
|
||||
verify(taskService).requeueUnusableResult(1L, "member response generation failed");
|
||||
verify(taskService, never()).completeTask(any(), any(), anyString());
|
||||
verify(announceService, never()).announceTaskSettled(any());
|
||||
verify(eventChannel).publishTaskEvent(any(), eq("team_task_retrying"), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("an unusable third result fails instead of bypassing the circuit breaker")
|
||||
void settleFallbackFailsAfterDispatchBudget() {
|
||||
TeamTaskEntity running = task(1L, MEMBER_A);
|
||||
running.setStatus(TeamTaskStatus.IN_PROGRESS);
|
||||
running.setDispatchCount(TeamTaskService.MAX_DISPATCHES);
|
||||
TeamTaskEntity failed = task(1L, MEMBER_A);
|
||||
failed.setStatus(TeamTaskStatus.FAILED);
|
||||
failed.setReason("member response generation failed");
|
||||
when(taskService.getTask(1L)).thenReturn(running, failed);
|
||||
when(taskService.failTask(1L, "member response generation failed")).thenReturn(true);
|
||||
|
||||
service.settleOutcome(running, "Failed to generate a response, please retry.");
|
||||
|
||||
verify(taskService, never()).requeueUnusableResult(any(), anyString());
|
||||
verify(taskService).failTask(1L, "member response generation failed");
|
||||
verify(eventChannel).publishTaskEvent(any(), eq("team_task_failed"), any());
|
||||
verify(announceService).announceTaskSettled(failed);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("a declared deliverable task without an attachment is requeued")
|
||||
void settleMissingDeliverableRequeues() {
|
||||
TeamTaskEntity running = task(1L, MEMBER_A);
|
||||
running.setStatus(TeamTaskStatus.IN_PROGRESS);
|
||||
running.setDispatchCount(1);
|
||||
running.setMetadata("{\"deliverableRequired\":true}");
|
||||
when(taskService.getTask(1L)).thenReturn(running);
|
||||
when(taskService.listDeliverables(running)).thenReturn(List.of());
|
||||
when(taskService.requeueUnusableResult(1L, "required deliverable was not attached"))
|
||||
.thenReturn(true);
|
||||
|
||||
service.settleOutcome(running, "handbook completed");
|
||||
|
||||
verify(taskService).requeueUnusableResult(1L, "required deliverable was not attached");
|
||||
verify(taskService, never()).completeTask(any(), any(), anyString());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("a long-running checkpoint tracker stays active until its terminal round")
|
||||
void settleParksCheckpointTracker() {
|
||||
TeamTaskEntity running = task(1L, MEMBER_A);
|
||||
running.setStatus(TeamTaskStatus.IN_PROGRESS);
|
||||
running.setProgressPercent(1);
|
||||
when(taskService.getTask(1L)).thenReturn(running);
|
||||
when(taskService.checkpointTerminalTag(running)).thenReturn("R300");
|
||||
|
||||
service.settleOutcome(running, "R001 tracker initialized");
|
||||
|
||||
verify(taskService).updateProgress(1L, null, 1,
|
||||
"waiting for R300 checkpoint");
|
||||
verify(taskService, never()).completeTask(any(), any(), anyString());
|
||||
verify(announceService, never()).announceTaskSettled(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("a tracker initialized after its terminal checkpoint completes immediately")
|
||||
void settleCompletesTrackerWhenTerminalEvidenceAlreadyExists() {
|
||||
TeamTaskEntity running = task(1L, MEMBER_A);
|
||||
running.setStatus(TeamTaskStatus.IN_PROGRESS);
|
||||
TeamTaskEntity completed = task(1L, MEMBER_A);
|
||||
completed.setStatus(TeamTaskStatus.COMPLETED);
|
||||
TeamTaskCommentEntity evidence = new TeamTaskCommentEntity();
|
||||
evidence.setContent("[checkpoint:R300] acknowledged");
|
||||
when(taskService.getTask(1L)).thenReturn(running, completed);
|
||||
when(taskService.checkpointTerminalTag(running)).thenReturn("R300");
|
||||
when(taskService.listComments(1L)).thenReturn(List.of(evidence));
|
||||
when(taskService.completeTask(1L, null, "tracker initialized")).thenReturn(List.of());
|
||||
|
||||
service.settleOutcome(running, "tracker initialized");
|
||||
|
||||
verify(taskService).completeTask(1L, null, "tracker initialized");
|
||||
verify(announceService).announceTaskSettled(completed);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("a task the member already failed via blocker is not completed on top")
|
||||
void settleRespectsMemberFailure() {
|
||||
|
||||
@ -300,7 +300,8 @@ class TeamPlanBridgeTest {
|
||||
bridge.checkParkedPlan(CONV,
|
||||
"R100/100 最终检查点:仅用一行回复,并确认已连续完成100轮"));
|
||||
|
||||
assertEquals("R100|执行中 1/2|#47 in_progress 80%(已完成第100轮检查点)",
|
||||
assertEquals("R100|执行中 1/2|#47 in_progress 80%(已完成第100轮检查点)"
|
||||
+ "|证据 [checkpoint:R100] acknowledged",
|
||||
state.progressText());
|
||||
assertFalse(state.progressText().contains("\n"));
|
||||
verify(taskService).addCommentOnce(102L, TeamTaskService.AUTHOR_SYSTEM,
|
||||
@ -330,10 +331,36 @@ class TeamPlanBridgeTest {
|
||||
TeamPlanBridge.InFlight state = assertInstanceOf(TeamPlanBridge.InFlight.class,
|
||||
bridge.checkParkedPlan(CONV, "R047/100 checkpoint"));
|
||||
|
||||
assertEquals("R047|已完成 2/2|#47 completed 100%", state.progressText());
|
||||
verify(taskService).addCommentOnce(103L, TeamTaskService.AUTHOR_SYSTEM,
|
||||
assertEquals("R047|已完成 2/2|#47 completed 100%"
|
||||
+ "|证据 [checkpoint:R047] acknowledged", state.progressText());
|
||||
verify(taskService).addCommentOnce(102L, TeamTaskService.AUTHOR_SYSTEM,
|
||||
"team-plan-bridge", TeamTaskService.COMMENT_NOTE,
|
||||
"[checkpoint:R047] acknowledged");
|
||||
verify(taskService, never()).addCommentOnce(eq(103L), anyString(), anyString(),
|
||||
anyString(), anyString());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("the terminal checkpoint completes the current run tracker and releases dispatch")
|
||||
void terminalCheckpointCompletesCurrentTracker() {
|
||||
parkedPlan();
|
||||
TeamTaskEntity tracker = task(102L, 59, 0, TeamTaskStatus.IN_PROGRESS);
|
||||
tracker.setSubject("R001-R300 唯一共享跟踪条目");
|
||||
when(taskService.listTasksByPlan(TEAM_ID, PLAN_ID)).thenReturn(List.of(tracker));
|
||||
when(taskService.checkpointTerminalTag(tracker)).thenReturn("R300");
|
||||
when(taskService.completeTask(102L, null,
|
||||
"Checkpoint tracking completed at R300")).thenReturn(List.of());
|
||||
|
||||
TeamPlanBridge.InFlight state = assertInstanceOf(TeamPlanBridge.InFlight.class,
|
||||
bridge.checkParkedPlan(CONV, "最终检查点 R300"));
|
||||
|
||||
assertTrue(state.progressText().contains("[checkpoint:R300] acknowledged"));
|
||||
verify(taskService).addCommentOnce(102L, TeamTaskService.AUTHOR_SYSTEM,
|
||||
"team-plan-bridge", TeamTaskService.COMMENT_NOTE,
|
||||
"[checkpoint:R300] acknowledged");
|
||||
verify(taskService).completeTask(102L, null,
|
||||
"Checkpoint tracking completed at R300");
|
||||
verify(eventPublisher).publishEvent(new TeamTasksDelegatedEvent(TEAM_ID));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@ -600,4 +600,18 @@ class TeamTaskServiceTest {
|
||||
query.getValue().getSqlSegment();
|
||||
assertTrue(query.getValue().getParamNameValuePairs().containsValue(RUN_ID));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("checkpoint range detection ignores the injected overall plan context")
|
||||
void checkpointTerminalTagUsesOnlyLocalTaskText() {
|
||||
TeamTaskEntity tracker = task(1L, TeamTaskStatus.IN_PROGRESS);
|
||||
tracker.setSubject("R001-R300 唯一共享跟踪条目");
|
||||
tracker.setDescription("每轮登记证据,R300 前保持进行中\n\n[Plan context]\nOverall request");
|
||||
assertEquals("R300", service.checkpointTerminalTag(tracker));
|
||||
|
||||
TeamTaskEntity ordinary = task(2L, TeamTaskStatus.IN_PROGRESS);
|
||||
ordinary.setSubject("设计稳定性指标");
|
||||
ordinary.setDescription("产出指标清单\n\n[Plan context]\nOverall request: R001-R300 共享跟踪");
|
||||
assertNull(service.checkpointTerminalTag(ordinary));
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user