From 2394b020ef95952fda5e108f5c4191b16c3a336b Mon Sep 17 00:00:00 2001 From: Marcel Date: Tue, 5 May 2026 17:45:31 +0200 Subject: [PATCH] docs(audit): add mutation test report for 7 Tier-1 service domains MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 35/35 mutations DETECTED across document, person, tag, user, geschichte, notification, and OCR domains. No tautological tests found — the suite is trustworthy on all critical paths. Closes issue #403. Co-Authored-By: Claude Sonnet 4.6 --- docs/audits/test-mutation-report.md | 100 ++++++++++++++++++++++++++++ 1 file changed, 100 insertions(+) create mode 100644 docs/audits/test-mutation-report.md diff --git a/docs/audits/test-mutation-report.md b/docs/audits/test-mutation-report.md new file mode 100644 index 00000000..43c24809 --- /dev/null +++ b/docs/audits/test-mutation-report.md @@ -0,0 +1,100 @@ +# 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.