mirror of
https://gitee.com/mateos/mateclaw.git
synced 2026-09-15 03:55:09 +08:00
test(goal): rewrite evaluator tests for bootstrap/verdict modes
This commit is contained in:
parent
77196acbcd
commit
a756da7817
@ -67,6 +67,14 @@ class GoalEvaluationServiceTest {
|
|||||||
return g;
|
return g;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Goal that already has a 2-item checklist — drives verdict mode. */
|
||||||
|
private GoalEntity goalWithCriteria() {
|
||||||
|
GoalEntity g = goal();
|
||||||
|
g.setCriteria("[{\"id\":\"C1\",\"text\":\"DNS configured\",\"passed\":false,\"evidence\":\"\"},"
|
||||||
|
+ "{\"id\":\"C2\",\"text\":\"TLS enabled\",\"passed\":false,\"evidence\":\"\"}]");
|
||||||
|
return g;
|
||||||
|
}
|
||||||
|
|
||||||
private ModelConfigEntity model(String name) {
|
private ModelConfigEntity model(String name) {
|
||||||
ModelConfigEntity m = new ModelConfigEntity();
|
ModelConfigEntity m = new ModelConfigEntity();
|
||||||
m.setProvider("dashscope");
|
m.setProvider("dashscope");
|
||||||
@ -111,66 +119,70 @@ class GoalEvaluationServiceTest {
|
|||||||
verify(chatModelFactory, never()).buildFor(any(), any());
|
verify(chatModelFactory, never()).buildFor(any(), any());
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==================== Happy paths ====================
|
// ==================== Bootstrap mode (no criteria yet) ====================
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void continueDecision_whenScoreBelowOne() {
|
void bootstrap_createsChecklist_fromDraftJson() {
|
||||||
stubChatResponse("{\"score\": 0.6, \"gap\": \"DNS not configured yet\", \"completed\": false}");
|
stubChatResponse("{\"criteria\":["
|
||||||
|
+ "{\"text\":\"DNS configured\"},"
|
||||||
|
+ "{\"text\":\"TLS enabled\"}]}");
|
||||||
GoalEvaluationResult r = svc.evaluate(goal(),
|
GoalEvaluationResult r = svc.evaluate(goal(),
|
||||||
List.of(new UserMessage("status?")),
|
List.of(new UserMessage("status?")), "working on it");
|
||||||
"DNS configured, still need TLS");
|
|
||||||
assertEquals(GoalEvaluationResult.DECISION_CONTINUE, r.decision());
|
assertEquals(GoalEvaluationResult.DECISION_CONTINUE, r.decision());
|
||||||
assertFalse(r.completed());
|
assertFalse(r.completed(), "bootstrap round never completes");
|
||||||
assertEquals(0.6, r.score(), 1e-9);
|
assertNotNull(r.bootstrapCriteria());
|
||||||
assertEquals("DNS not configured yet", r.gap());
|
assertEquals(2, r.bootstrapCriteria().size());
|
||||||
|
assertEquals("C1", r.bootstrapCriteria().get(0).id());
|
||||||
|
assertFalse(r.bootstrapCriteria().get(0).passed());
|
||||||
assertEquals(1, r.llmCallsConsumed());
|
assertEquals(1, r.llmCallsConsumed());
|
||||||
assertEquals("qwen-turbo", r.evaluatorModel());
|
assertEquals("qwen-turbo", r.evaluatorModel());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void completedDecision_whenJsonSaysCompleted() {
|
void bootstrap_parsesMarkdownFences() {
|
||||||
stubChatResponse("{\"score\": 0.95, \"gap\": \"\", \"completed\": true}");
|
stubChatResponse("```json\n{\"criteria\":[{\"text\":\"only one\"}]}\n```");
|
||||||
GoalEvaluationResult r = svc.evaluate(goal(), List.of(), "all green");
|
GoalEvaluationResult r = svc.evaluate(goal(), List.of(), "x");
|
||||||
|
assertNotNull(r.bootstrapCriteria());
|
||||||
|
assertEquals(1, r.bootstrapCriteria().size());
|
||||||
|
assertEquals("only one", r.bootstrapCriteria().get(0).text());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void bootstrap_emptyDraft_returnsFallback() {
|
||||||
|
stubChatResponse("{\"criteria\":[]}");
|
||||||
|
GoalEvaluationResult r = svc.evaluate(goal(), List.of(), "x");
|
||||||
|
assertEquals(GoalEvaluationResult.DECISION_FALLBACK, r.decision());
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== Verdict mode (criteria exist) ====================
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void verdict_partial_continues() {
|
||||||
|
stubChatResponse("{\"criterionVerdicts\":["
|
||||||
|
+ "{\"id\":\"C1\",\"passed\":true,\"evidence\":\"page returns 200\"}],"
|
||||||
|
+ "\"summary\":\"1 of 2\"}");
|
||||||
|
GoalEvaluationResult r = svc.evaluate(goalWithCriteria(), List.of(), "DNS done");
|
||||||
|
assertEquals(GoalEvaluationResult.DECISION_CONTINUE, r.decision());
|
||||||
|
assertFalse(r.completed());
|
||||||
|
assertEquals(0.5, r.score(), 1e-9); // 1 of 2 merged criteria passed
|
||||||
|
assertEquals(1, r.criterionVerdicts().size());
|
||||||
|
assertEquals(1, r.llmCallsConsumed());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void verdict_allPassed_completes() {
|
||||||
|
stubChatResponse("{\"criterionVerdicts\":["
|
||||||
|
+ "{\"id\":\"C1\",\"passed\":true,\"evidence\":\"200\"},"
|
||||||
|
+ "{\"id\":\"C2\",\"passed\":true,\"evidence\":\"tls ok\"}],"
|
||||||
|
+ "\"summary\":\"done\"}");
|
||||||
|
GoalEvaluationResult r = svc.evaluate(goalWithCriteria(), List.of(), "all green");
|
||||||
assertEquals(GoalEvaluationResult.DECISION_COMPLETED, r.decision());
|
assertEquals(GoalEvaluationResult.DECISION_COMPLETED, r.decision());
|
||||||
assertTrue(r.completed());
|
assertTrue(r.completed());
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void scoreOfOne_implicitlyCompletes_evenWhenJsonSaysFalse() {
|
|
||||||
stubChatResponse("{\"score\": 1.0, \"gap\": \"\", \"completed\": false}");
|
|
||||||
GoalEvaluationResult r = svc.evaluate(goal(), List.of(), "perfect answer");
|
|
||||||
assertTrue(r.completed(), "score=1.0 must imply completed regardless of the bool field");
|
|
||||||
assertEquals(GoalEvaluationResult.DECISION_COMPLETED, r.decision());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void score_clampedTo01_whenModelReturnsOutOfRange() {
|
|
||||||
stubChatResponse("{\"score\": 1.7, \"gap\": \"\", \"completed\": true}");
|
|
||||||
GoalEvaluationResult r = svc.evaluate(goal(), List.of(), "answer");
|
|
||||||
assertEquals(1.0, r.score(), 1e-9);
|
assertEquals(1.0, r.score(), 1e-9);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
|
||||||
void negativeScore_clampedToZero() {
|
|
||||||
stubChatResponse("{\"score\": -0.2, \"gap\": \"x\", \"completed\": false}");
|
|
||||||
GoalEvaluationResult r = svc.evaluate(goal(), List.of(), "answer");
|
|
||||||
assertEquals(0.0, r.score(), 1e-9);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ==================== Parser tolerance ====================
|
// ==================== Parser tolerance ====================
|
||||||
|
|
||||||
@Test
|
|
||||||
void parsesEvenWhenWrappedInMarkdownFences() {
|
|
||||||
// Lenient stub: parser tolerance shouldn't depend on a specific code path.
|
|
||||||
stubChatResponse("```json\n"
|
|
||||||
+ "{\"score\": 0.4, \"gap\": \"still need TLS\", \"completed\": false}\n"
|
|
||||||
+ "```");
|
|
||||||
GoalEvaluationResult r = svc.evaluate(goal(), List.of(), "DNS set up");
|
|
||||||
assertEquals(GoalEvaluationResult.DECISION_CONTINUE, r.decision());
|
|
||||||
assertEquals(0.4, r.score(), 1e-9);
|
|
||||||
assertEquals("still need TLS", r.gap());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void parseFails_whenNoJsonObjectInOutput() {
|
void parseFails_whenNoJsonObjectInOutput() {
|
||||||
stubChatResponse("I think it's about 60% done.");
|
stubChatResponse("I think it's about 60% done.");
|
||||||
@ -179,20 +191,9 @@ class GoalEvaluationServiceTest {
|
|||||||
assertEquals(0, r.llmCallsConsumed());
|
assertEquals(0, r.llmCallsConsumed());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
|
||||||
void parseFails_whenScoreFieldMissing() {
|
|
||||||
stubChatResponse("{\"gap\": \"missing\", \"completed\": false}");
|
|
||||||
GoalEvaluationResult r = svc.evaluate(goal(), List.of(), "x");
|
|
||||||
assertEquals(GoalEvaluationResult.DECISION_FALLBACK, r.decision());
|
|
||||||
assertTrue(r.gap().contains("parse_missing_score"));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void parseFails_whenJsonMalformed() {
|
void parseFails_whenJsonMalformed() {
|
||||||
// Closing brace present but interior is invalid — exercises the
|
stubChatResponse("{\"criteria\": }");
|
||||||
// ObjectMapper.readTree exception path rather than the cheaper
|
|
||||||
// "no object found" pre-check.
|
|
||||||
stubChatResponse("{\"score\": 0.5, \"gap\": }");
|
|
||||||
GoalEvaluationResult r = svc.evaluate(goal(), List.of(), "x");
|
GoalEvaluationResult r = svc.evaluate(goal(), List.of(), "x");
|
||||||
assertEquals(GoalEvaluationResult.DECISION_FALLBACK, r.decision());
|
assertEquals(GoalEvaluationResult.DECISION_FALLBACK, r.decision());
|
||||||
assertTrue(r.gap().contains("parse_failed"));
|
assertTrue(r.gap().contains("parse_failed"));
|
||||||
@ -229,7 +230,7 @@ class GoalEvaluationServiceTest {
|
|||||||
when(chatModelFactory.buildFor(eq(named), any())).thenReturn(chatModel);
|
when(chatModelFactory.buildFor(eq(named), any())).thenReturn(chatModel);
|
||||||
ChatResponse response = new ChatResponse(List.of(
|
ChatResponse response = new ChatResponse(List.of(
|
||||||
new Generation(new AssistantMessage(
|
new Generation(new AssistantMessage(
|
||||||
"{\"score\":0.5,\"gap\":\"\",\"completed\":false}"))));
|
"{\"criteria\":[{\"text\":\"works\"}]}"))));
|
||||||
when(chatModel.call(any(Prompt.class))).thenReturn(response);
|
when(chatModel.call(any(Prompt.class))).thenReturn(response);
|
||||||
// Default lookup is never consulted when an override is configured.
|
// Default lookup is never consulted when an override is configured.
|
||||||
lenient().when(modelConfigService.getDefaultModel()).thenReturn(null);
|
lenient().when(modelConfigService.getDefaultModel()).thenReturn(null);
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user