# Test Mutation Report **Date:** 2026-05-05 **Branch:** `worktree-test-issue-402-legibility-preflight` **Method:** Manual targeted mutation (Approach A from issue #403) **Scope:** 7 Tier-1 backend service domains × 5 mutations each = 35 total For each mutation: the service method was broken, the paired test was run in isolation, and the result recorded. All mutations were reverted before proceeding to the next. **Summary: 35/35 DETECTED (100%)** --- ## Document Domain | ID | Test | Mutation | Result | |----|------|----------|--------| | D1 | `deleteDocument_deletesById_whenExists` | Removed `documentRepository.deleteById(id)` call | **DETECTED** | | D2 | `deleteDocument_throwsNotFound_whenMissing` | Removed `existsById` guard — always proceed to delete | **DETECTED** | | D3 | `deleteTagCascading_removesTagFromAllDocumentsAndDeletesTag` | Removed `tagService.delete(tagId)` at end of cascade | **DETECTED** | | D4 | `updateDocument_setsArchiveBoxAndFolder` | Removed `doc.setArchiveBox(dto.getArchiveBox())` | **DETECTED** | | D5 | `createDocument_setsFileHashFromUpload_whenFileProvided` | Removed `doc.setFileHash(upload.fileHash())` | **DETECTED** | --- ## Person Domain | ID | Test | Mutation | Result | |----|------|----------|--------| | P1 | `createPerson_savesTrimmedAlias_whenAliasIsNonBlank` | Removed `.trim()` — stored raw whitespace-padded alias | **DETECTED** | | P2 | `mergePersons_reassignsDocumentsAndDeletesSource` | Removed `personRepository.reassignSender(sourceId, targetId)` | **DETECTED** | | P3 | `findOrCreateByAlias_createsMaidenNameAlias_whenGebPresent` | Changed `MAIDEN_NAME` → `BIRTH` alias type | **DETECTED** | | P4 | `addAlias_savesWithAutoIncrementedSortOrder` | Removed `+1` from `findMaxSortOrder(personId) + 1` | **DETECTED** | | P5 | `removeAlias_throwsForbidden_whenAliasDoesNotBelongToPerson` | Removed ownership check — allowed cross-person alias deletion | **DETECTED** | --- ## Tag Domain | ID | Test | Mutation | Result | |----|------|----------|--------| | T1 | `update_savesNewName` | Removed `tag.setName(dto.name())` | **DETECTED** | | T2 | `mergeTags_reassignsDocumentsReparentsChildrenAndDeletesSource` | Removed `tagRepository.reparentChildren(sourceId, targetId)` | **DETECTED** | | T3 | `deleteWithDescendants_deletesSubtreeDocTagsAndAllTags` | Removed `tagRepository.deleteDocumentTagsByTagIds(ids)` | **DETECTED** | | T4 | `update_throwsCycleDetected_whenTagIsAncestorOfProposedParent` | Flipped `ancestors.contains(tagId)` to `!ancestors.contains(tagId)` | **DETECTED** | | T5 | `update_savesColor` | Removed `tag.setColor(dto.color())` | **DETECTED** | --- ## User Domain | ID | Test | Mutation | Result | |----|------|----------|--------| | U1 | `changePassword_updatesHash_whenCurrentPasswordCorrect` | Removed `passwordEncoder.encode()` — stored raw new password | **DETECTED** | | U2 | `adminUpdateUser_updatesGroups_whenGroupIdsProvided` | Removed `user.setGroups(after)` | **DETECTED** | | U3 | `updateProfile_allowsSameEmailForSameUser` | Removed ID equality check — threw conflict even for own email | **DETECTED** | | U4 | `adminUpdateUser_setsPassword_whenNewPasswordProvided` | Removed `passwordEncoder.encode()` in admin password update | **DETECTED** | | U5 | `updateProfile_setsContactToNull_whenContactIsBlank` | Removed blank→null normalization and trim — stored raw contact | **DETECTED** | --- ## Geschichte Domain | ID | Test | Mutation | Result | |----|------|----------|--------| | G1 | `getById_throws_NOT_FOUND_for_draft_when_user_lacks_BLOG_WRITE` | Removed draft visibility check — exposed drafts to all users | **DETECTED** | | G2 | `create_sanitizes_body_HTML_dropping_disallowed_tags` | Removed HTML sanitization — returned raw body string | **DETECTED** | | G3 | `update_sets_publishedAt_when_status_transitions_to_PUBLISHED` | Removed `g.setPublishedAt(LocalDateTime.now())` on PUBLISH | **DETECTED** | | G4 | `update_clears_publishedAt_when_status_transitions_back_to_DRAFT` | Removed `g.setPublishedAt(null)` on DRAFT transition | **DETECTED** | | G5 | `delete_throws_NOT_FOUND_when_unknown` | Removed `existsById` guard — silently deleted non-existent IDs | **DETECTED** | --- ## Notification Domain | ID | Test | Mutation | Result | |----|------|----------|--------| | N1 | `notifyReply_createsNotificationForThreadParticipants` | Changed `NotificationType.REPLY` → `MENTION` in `notifyReply` | **DETECTED** | | N2 | `markRead_throwsForbidden_whenNotificationBelongsToDifferentUser` | Removed ownership check in `markRead` | **DETECTED** | | N3 | `markRead_marksNotificationAsRead_whenRecipientMatches` | Removed `notification.setRead(true)` | **DETECTED** | | N4 | `notifyReply_sendsEmailOnlyToUsersWithReplyNotificationsEnabled` | Removed `isNotifyOnReply()` guard — sent email unconditionally | **DETECTED** | | N5 | `notifyMentions_createsNotificationPerMentionedUser` | Changed `NotificationType.MENTION` → `REPLY` in `notifyMentions` | **DETECTED** | --- ## OCR Domain | ID | Test | Mutation | Result | |----|------|----------|--------| | O1 | `getJob_throwsNotFound_whenJobDoesNotExist` | Changed error code `OCR_JOB_NOT_FOUND` → `INTERNAL_ERROR` | **DETECTED** | | O2 | `startOcr_throwsBadRequest_whenDocumentIsPlaceholder` | Removed `PLACEHOLDER` status guard | **DETECTED** | | O3 | `startOcr_throwsServiceUnavailable_whenOcrServiceIsDown` | Removed `ocrHealthClient.isHealthy()` check | **DETECTED** | | O4 | `startOcr_createsJobAndDispatchesAsync` | Removed `ocrAsyncRunner.runSingleDocument(...)` call | **DETECTED** | | O5 | `startOcr_updatesScriptType_whenProvided` | Removed `documentService.updateScriptType(documentId, scriptTypeOverride)` | **DETECTED** | --- ## Verdict **All 35 mutations were DETECTED.** No tautological tests found. TEST-2 (rewrite phase) has no work to do — the suite is already trustworthy on these critical paths.