test(goal): checklist codec + service coverage; final internal-ref sweep

This commit is contained in:
matevip 2026-06-03 21:19:27 +08:00
parent da332f2fd8
commit 77196acbcd
3 changed files with 178 additions and 1 deletions

View File

@ -42,7 +42,7 @@ public enum GoalStatus {
* the DB stores via {@link EnumValue}. Without this, Jackson defaults to
* {@link #name()} (uppercase) and the frontend's
* {@code status: 'active' | 'paused' | ...} TS literal types reject
* every payload UI bug observed during PR4 manual QA.
* every payload.
*/
@JsonValue
public String getValue() {

View File

@ -0,0 +1,113 @@
package vip.mate.goal.model;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.Test;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
/**
* Pure unit tests for the checklist (de)serialization + merge helpers.
*/
class GoalCriteriaCodecTest {
private final ObjectMapper mapper = new ObjectMapper();
private static GoalCriterion c(String id, String text, boolean passed) {
return new GoalCriterion(id, text, passed, passed ? "ok" : "");
}
// ---------- parse ----------
@Test
void parse_nullOrBlankOrCorrupt_returnsEmptyMutableList() {
assertTrue(GoalCriteriaCodec.parse(null, mapper).isEmpty());
assertTrue(GoalCriteriaCodec.parse("", mapper).isEmpty());
assertTrue(GoalCriteriaCodec.parse(" ", mapper).isEmpty());
assertTrue(GoalCriteriaCodec.parse("{not valid json", mapper).isEmpty());
// mutable: callers append during bootstrap/append paths
GoalCriteriaCodec.parse(null, mapper).add(c("C1", "x", false));
}
@Test
void parse_roundTrip() {
String json = GoalCriteriaCodec.serialize(List.of(c("C1", "tests pass", true)), mapper);
List<GoalCriterion> back = GoalCriteriaCodec.parse(json, mapper);
assertEquals(1, back.size());
assertEquals("C1", back.get(0).id());
assertEquals("tests pass", back.get(0).text());
assertTrue(back.get(0).passed());
}
@Test
void serialize_null_returnsNull() {
assertNull(GoalCriteriaCodec.serialize(null, mapper));
}
// ---------- merge ----------
@Test
void merge_appliesVerdictById_preservesTextAndUntouched() {
List<GoalCriterion> existing = List.of(
c("C1", "first", false),
c("C2", "second", false));
List<GoalChecklistVerdict.CriterionVerdict> delta = List.of(
new GoalChecklistVerdict.CriterionVerdict("C1", true, "did it"));
List<GoalCriterion> merged = GoalCriteriaCodec.merge(existing, delta);
assertEquals(2, merged.size());
assertTrue(merged.get(0).passed());
assertEquals("did it", merged.get(0).evidence());
assertEquals("first", merged.get(0).text()); // text preserved
assertFalse(merged.get(1).passed()); // untouched stays
assertEquals("second", merged.get(1).text());
}
@Test
void merge_unknownVerdictId_isIgnored() {
List<GoalCriterion> existing = List.of(c("C1", "first", false));
List<GoalChecklistVerdict.CriterionVerdict> delta = List.of(
new GoalChecklistVerdict.CriterionVerdict("C9", true, "nope"));
List<GoalCriterion> merged = GoalCriteriaCodec.merge(existing, delta);
assertFalse(merged.get(0).passed());
}
// ---------- allPassed / remaining ----------
@Test
void allPassed_emptyIsFalse() {
assertFalse(GoalCriteriaCodec.allPassed(List.of()));
}
@Test
void allPassed_trueOnlyWhenEveryPassed() {
assertTrue(GoalCriteriaCodec.allPassed(List.of(c("C1", "a", true), c("C2", "b", true))));
assertFalse(GoalCriteriaCodec.allPassed(List.of(c("C1", "a", true), c("C2", "b", false))));
}
@Test
void remaining_returnsOnlyUnpassed() {
List<GoalCriterion> rem = GoalCriteriaCodec.remaining(
List.of(c("C1", "a", true), c("C2", "b", false), c("C3", "c", false)));
assertEquals(2, rem.size());
assertEquals("C2", rem.get(0).id());
assertEquals("C3", rem.get(1).id());
}
// ---------- reindex ----------
@Test
void reindex_assignsSequentialIds() {
List<GoalCriterion> out = GoalCriteriaCodec.reindex(List.of(
c("", "a", false), c("zzz", "b", false), c("C99", "c", false)));
assertEquals("C1", out.get(0).id());
assertEquals("C2", out.get(1).id());
assertEquals("C3", out.get(2).id());
assertEquals("a", out.get(0).text());
}
}

View File

@ -314,6 +314,70 @@ class GoalServiceTest {
verify(goalMapper, never()).selectById(any());
}
// ==================== criteria checklist ====================
@Test
void create_normalizesInitialCriteria_assignsIdsForcesUnpassed() {
when(goalMapper.selectOne(any())).thenReturn(null);
when(goalMapper.insert(any(GoalEntity.class))).thenReturn(1);
GoalCreateRequest r = validReq();
r.setCriteria(java.util.List.of(
new vip.mate.goal.model.GoalCriterion("ignored", "tests pass", true, "x"),
new vip.mate.goal.model.GoalCriterion("", " ", false, ""), // blank dropped
new vip.mate.goal.model.GoalCriterion("", "deployed", false, "")));
ArgumentCaptor<GoalEntity> captor = ArgumentCaptor.forClass(GoalEntity.class);
service.create(r, "alice");
verify(goalMapper).insert(captor.capture());
java.util.List<vip.mate.goal.model.GoalCriterion> parsed =
vip.mate.goal.model.GoalCriteriaCodec.parse(captor.getValue().getCriteria(), new ObjectMapper());
assertEquals(2, parsed.size());
assertEquals("C1", parsed.get(0).id());
assertEquals("tests pass", parsed.get(0).text());
assertFalse(parsed.get(0).passed()); // forced false even though caller said true
assertEquals("C2", parsed.get(1).id());
assertEquals("deployed", parsed.get(1).text());
}
@Test
void create_emptyCriteria_leavesColumnNull_forBootstrap() {
when(goalMapper.selectOne(any())).thenReturn(null);
when(goalMapper.insert(any(GoalEntity.class))).thenReturn(1);
ArgumentCaptor<GoalEntity> captor = ArgumentCaptor.forClass(GoalEntity.class);
service.create(validReq(), "alice");
verify(goalMapper).insert(captor.capture());
assertNull(captor.getValue().getCriteria());
}
@Test
void create_autoFollowup_threeState() {
when(goalMapper.selectOne(any())).thenReturn(null);
when(goalMapper.insert(any(GoalEntity.class))).thenReturn(1);
// null -> config default (true by default)
assertTrue(service.create(validReq(), "alice").getAutoFollowupEnabled());
// explicit false is honored
GoalCreateRequest off = validReq();
off.setAutoFollowupEnabled(false);
assertFalse(service.create(off, "alice").getAutoFollowupEnabled());
}
@Test
void toResponse_parsesCriteriaArray_nullBecomesEmpty() {
GoalEntity g = persisted(1L, GoalStatus.ACTIVE);
g.setCriteria("[{\"id\":\"C1\",\"text\":\"a\",\"passed\":true,\"evidence\":\"ok\"}]");
var resp = service.toResponse(g);
assertEquals(1, resp.getCriteria().size());
assertTrue(resp.getCriteria().get(0).passed());
GoalEntity bare = persisted(2L, GoalStatus.ACTIVE); // criteria == null
assertNotNull(service.toResponse(bare).getCriteria());
assertTrue(service.toResponse(bare).getCriteria().isEmpty());
}
// ==================== optimistic lock retry ====================
@Test