feat(tag-search): expand children and surface ancestor path in search results

Modifies TagService.search() to enrich name-matches with tree relatives:
root matches expand descendants, child matches prepend ancestors.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Marcel
2026-04-17 11:27:41 +02:00
parent 59b7f7cddf
commit d075bf390a
2 changed files with 93 additions and 1 deletions

View File

@@ -496,4 +496,71 @@ class TagServiceTest {
.extracting(e -> ((DomainException) e).getCode())
.isEqualTo(ErrorCode.TAG_NOT_FOUND);
}
// ─── search ───────────────────────────────────────────────────────────────
@Test
void search_includes_children_when_root_matches() {
UUID rootId = UUID.randomUUID();
UUID childId = UUID.randomUUID();
Tag root = Tag.builder().id(rootId).name("Briefe").build();
Tag child = Tag.builder().id(childId).name("Familienbriefe").parentId(rootId).build();
when(tagRepository.findByNameContainingIgnoreCase("Brief")).thenReturn(List.of(root));
when(tagRepository.findDescendantIds(rootId)).thenReturn(List.of(rootId, childId));
when(tagRepository.findAllById(Set.of(childId))).thenReturn(List.of(child));
List<Tag> result = tagService.search("Brief");
assertThat(result).extracting(Tag::getId).containsExactlyInAnyOrder(rootId, childId);
}
@Test
void search_includes_ancestors_when_child_matches() {
UUID rootId = UUID.randomUUID();
UUID childId = UUID.randomUUID();
Tag root = Tag.builder().id(rootId).name("Briefe").build();
Tag child = Tag.builder().id(childId).name("Hochzeit").parentId(rootId).build();
when(tagRepository.findByNameContainingIgnoreCase("Hochzeit")).thenReturn(List.of(child));
when(tagRepository.findAncestorIds(childId)).thenReturn(List.of(rootId));
when(tagRepository.findAllById(Set.of(rootId))).thenReturn(List.of(root));
List<Tag> result = tagService.search("Hochzeit");
assertThat(result).extracting(Tag::getId).containsExactlyInAnyOrder(rootId, childId);
}
@Test
void search_deduplicates_when_root_also_in_descendant_result() {
UUID rootId = UUID.randomUUID();
UUID childId = UUID.randomUUID();
Tag root = Tag.builder().id(rootId).name("Briefe").build();
Tag child = Tag.builder().id(childId).name("Familienbriefe").parentId(rootId).build();
when(tagRepository.findByNameContainingIgnoreCase("Brief")).thenReturn(List.of(root));
// findDescendantIds includes the seed (rootId) — dedup must remove it from extras
when(tagRepository.findDescendantIds(rootId)).thenReturn(List.of(rootId, childId));
when(tagRepository.findAllById(Set.of(childId))).thenReturn(List.of(child));
List<Tag> result = tagService.search("Brief");
assertThat(result).extracting(Tag::getId).containsExactlyInAnyOrder(rootId, childId);
assertThat(result).hasSize(2);
}
@Test
void search_callsResolveEffectiveColors_onAllResults() {
UUID rootId = UUID.randomUUID();
UUID childId = UUID.randomUUID();
Tag root = Tag.builder().id(rootId).name("Briefe").color("sage").build();
Tag child = Tag.builder().id(childId).name("Familienbriefe").parentId(rootId).build();
when(tagRepository.findByNameContainingIgnoreCase("Brief")).thenReturn(List.of(root));
when(tagRepository.findDescendantIds(rootId)).thenReturn(List.of(rootId, childId));
when(tagRepository.findAllById(Set.of(childId))).thenReturn(List.of(child));
// resolveEffectiveColors will call findAllById for the child's parent color
when(tagRepository.findAllById(Set.of(rootId))).thenReturn(List.of(root));
tagService.search("Brief");
// verify findAllById was called at least twice: once for extras, once inside resolveEffectiveColors
verify(tagRepository, atLeastOnce()).findAllById(any());
}
}