# 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.
