mirror of
https://gitee.com/mateos/mateclaw.git
synced 2026-09-14 03:33:43 +08:00
fix(goal): preserve criteria initialized while bootstrap is in flight
This commit is contained in:
parent
bb3d477aa2
commit
b04b0f673b
@ -506,11 +506,17 @@ public class GoalServiceImpl implements GoalService {
|
||||
/**
|
||||
* Compute the next criteria JSON for a record-evaluation write, or
|
||||
* {@code null} when the result carries no checklist change. Bootstrap
|
||||
* results replace the list with the freshly derived draft; verdict
|
||||
* results initialize a still-empty list with the derived draft; verdict
|
||||
* results merge their per-criterion delta into the locked-row list.
|
||||
*/
|
||||
private String nextCriteriaJson(GoalEntity fresh, GoalEvaluationResult result) {
|
||||
if (result.bootstrapCriteria() != null && !result.bootstrapCriteria().isEmpty()) {
|
||||
// A user append or another evaluator may have initialized the
|
||||
// checklist while this model call ran. The fresh canonical list
|
||||
// wins, including after an optimistic-lock retry.
|
||||
if (!GoalCriteriaCodec.parse(fresh.getCriteria(), objectMapper).isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
return GoalCriteriaCodec.serialize(result.bootstrapCriteria(), objectMapper);
|
||||
}
|
||||
if (result.criterionVerdicts() != null && !result.criterionVerdicts().isEmpty()) {
|
||||
|
||||
@ -77,6 +77,24 @@ class GoalPersistenceIntegrationTest {
|
||||
return r;
|
||||
}
|
||||
|
||||
@Test
|
||||
void lateBootstrapCannotReplaceUserCriterionCommittedWhileModelWasRunning() {
|
||||
GoalEntity created = goalService.create(req("bootstrap-append-boundary", "prepare a report"), "alice");
|
||||
// The evaluator started with an empty checklist. A user append commits
|
||||
// before its delayed bootstrap result reaches recordEvaluation.
|
||||
goalService.appendCriterion(created.getId(), "include the user requested appendix", "alice");
|
||||
var delayed = new vip.mate.goal.model.GoalEvaluationResult(0.0, "checklist created", "continue", false,
|
||||
"fixture", 1, 0, java.util.List.of(), java.util.List.of(
|
||||
new vip.mate.goal.model.GoalCriterion("C1", "model draft", false, "")));
|
||||
goalService.recordEvaluation(created.getId(), delayed, 2, 1);
|
||||
GoalEntity saved = goalService.getById(created.getId());
|
||||
var criteria = vip.mate.goal.model.GoalCriteriaCodec.parse(saved.getCriteria(), new com.fasterxml.jackson.databind.ObjectMapper());
|
||||
assertEquals(1, criteria.size());
|
||||
assertEquals("include the user requested appendix", criteria.getFirst().text());
|
||||
assertEquals(1, saved.getEvalLlmCallsUsed());
|
||||
assertEquals(2, saved.getAgentLlmCallsUsed());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("GoalStatus values persist as lowercase literals — load-bearing for uk_agent_goal_active_conv")
|
||||
void status_persistsAsLowercaseString() {
|
||||
|
||||
@ -618,6 +618,53 @@ class GoalServiceTest {
|
||||
verify(goalMapper, never()).selectById(any());
|
||||
}
|
||||
|
||||
private GoalEvaluationResult bootstrapEvaluation() {
|
||||
return new GoalEvaluationResult(0.0, "checklist created", "continue", false, "fixture", 1, 0,
|
||||
java.util.List.of(), java.util.List.of(
|
||||
new vip.mate.goal.model.GoalCriterion("C1", "model draft", false, "")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void bootstrapInitializesStillEmptyChecklist() {
|
||||
GoalEntity empty = persisted(1L, GoalStatus.ACTIVE);
|
||||
when(goalMapper.selectById(1L)).thenReturn(empty);
|
||||
when(goalMapper.update(any(), any(LambdaUpdateWrapper.class))).thenReturn(1);
|
||||
service.recordEvaluation(1L, bootstrapEvaluation(), 2, 1);
|
||||
ArgumentCaptor<LambdaUpdateWrapper> writes = ArgumentCaptor.forClass(LambdaUpdateWrapper.class);
|
||||
verify(goalMapper).update(any(), writes.capture());
|
||||
assertTrue(setsProperty(writes.getValue(), "criteria"));
|
||||
assertTrue(writes.getValue().getParamNameValuePairs().values().stream()
|
||||
.anyMatch(value -> String.valueOf(value).contains("model draft")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void lateBootstrapPreservesChecklistEstablishedByUser() {
|
||||
GoalEntity fresh = persisted(1L, GoalStatus.ACTIVE);
|
||||
fresh.setCriteria("[{\"id\":\"C1\",\"text\":\"user requirement\",\"passed\":false,\"evidence\":\"\"}]");
|
||||
when(goalMapper.selectById(1L)).thenReturn(fresh);
|
||||
when(goalMapper.update(any(), any(LambdaUpdateWrapper.class))).thenReturn(1);
|
||||
service.recordEvaluation(1L, bootstrapEvaluation(), 2, 1);
|
||||
ArgumentCaptor<LambdaUpdateWrapper> writes = ArgumentCaptor.forClass(LambdaUpdateWrapper.class);
|
||||
verify(goalMapper).update(any(), writes.capture());
|
||||
assertFalse(setsProperty(writes.getValue(), "criteria"), "late bootstrap must preserve current user criteria");
|
||||
assertTrue(writes.getValue().getSqlSet().contains("eval_llm_calls_used = eval_llm_calls_used + 1"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void bootstrapRechecksEmptyChecklistAfterCasConflict() {
|
||||
GoalEntity empty = persisted(1L, GoalStatus.ACTIVE);
|
||||
GoalEntity fresh = persisted(1L, GoalStatus.ACTIVE);
|
||||
fresh.setVersion(1);
|
||||
fresh.setCriteria("[{\"id\":\"C1\",\"text\":\"concurrent user requirement\",\"passed\":false,\"evidence\":\"\"}]");
|
||||
when(goalMapper.selectById(1L)).thenReturn(empty, empty, fresh, fresh);
|
||||
when(goalMapper.update(any(), any(LambdaUpdateWrapper.class))).thenReturn(0, 1);
|
||||
service.recordEvaluation(1L, bootstrapEvaluation(), 2, 1);
|
||||
ArgumentCaptor<LambdaUpdateWrapper> writes = ArgumentCaptor.forClass(LambdaUpdateWrapper.class);
|
||||
verify(goalMapper, times(2)).update(any(), writes.capture());
|
||||
assertTrue(setsProperty(writes.getAllValues().get(0), "criteria"));
|
||||
assertFalse(setsProperty(writes.getAllValues().get(1), "criteria"), "retry must not replace newly established criteria");
|
||||
}
|
||||
|
||||
// ==================== criteria checklist ====================
|
||||
|
||||
@Test
|
||||
|
||||
Loading…
Reference in New Issue
Block a user