test(person): address PR #736 review nits
Some checks failed
CI / Unit & Component Tests (pull_request) Successful in 3m21s
CI / OCR Service Tests (pull_request) Successful in 23s
CI / Backend Unit Tests (pull_request) Failing after 3m20s
CI / fail2ban Regex (pull_request) Successful in 49s
CI / Semgrep Security Scan (pull_request) Successful in 23s
CI / Compose Bucket Idempotency (pull_request) Successful in 1m17s

- AC-3 cascade test: assert an innocent bystander's mention row survives the
  delete, proving the cascade is scoped to the deleted person (Nora).
- Fix integration-test comment: receivers is @ManyToMany(LAZY), not an EAGER
  @ElementCollection (Sara).
- ADR-032: note the @ prefix is kept in the degraded path, stripped in live
  mentions (Leonie).
- Add trailing newline to PersonRepository.java (Felix).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Marcel
2026-06-06 12:14:54 +02:00
parent 135082c5c0
commit 4c2d7ba128
4 changed files with 20 additions and 5 deletions

View File

@@ -207,4 +207,4 @@ public interface PersonRepository extends JpaRepository<Person, UUID> {
) )
""", nativeQuery = true) """, nativeQuery = true)
void insertMissingReceiverReference(@Param("source") UUID source, @Param("target") UUID target); void insertMissingReceiverReference(@Param("source") UUID source, @Param("target") UUID target);
} }

View File

@@ -776,6 +776,7 @@ class PersonRepositoryTest {
// AC-3: the @-mention sidecar is a CASCADE soft reference, but the literal "@Name" lives // AC-3: the @-mention sidecar is a CASCADE soft reference, but the literal "@Name" lives
// in transcription_blocks.text and must stay visible as plain text after the person goes. // in transcription_blocks.text and must stay visible as plain text after the person goes.
Person mentioned = personRepository.save(Person.builder().firstName("Auguste").lastName("Raddatz").build()); Person mentioned = personRepository.save(Person.builder().firstName("Auguste").lastName("Raddatz").build());
Person survivor = personRepository.save(Person.builder().firstName("Clara").lastName("Cram").build());
Document doc = documentRepository.save(Document.builder() Document doc = documentRepository.save(Document.builder()
.title("Brief").originalFilename("brief.pdf") .title("Brief").originalFilename("brief.pdf")
.status(DocumentStatus.UPLOADED).build()); .status(DocumentStatus.UPLOADED).build());
@@ -790,12 +791,18 @@ class PersonRepositoryTest {
entityManager.createNativeQuery( entityManager.createNativeQuery(
"INSERT INTO transcription_blocks (id, annotation_id, document_id, text) VALUES (?1, ?2, ?3, ?4)") "INSERT INTO transcription_blocks (id, annotation_id, document_id, text) VALUES (?1, ?2, ?3, ?4)")
.setParameter(1, blockId).setParameter(2, annotationId).setParameter(3, doc.getId()) .setParameter(1, blockId).setParameter(2, annotationId).setParameter(3, doc.getId())
.setParameter(4, "Brief an @Auguste Raddatz").executeUpdate(); .setParameter(4, "Brief an @Auguste Raddatz und @Clara Cram").executeUpdate();
// Two mention rows on the same block: the deleted person and an innocent bystander.
entityManager.createNativeQuery( entityManager.createNativeQuery(
"INSERT INTO transcription_block_mentioned_persons (block_id, person_id, display_name) " "INSERT INTO transcription_block_mentioned_persons (block_id, person_id, display_name) "
+ "VALUES (?1, ?2, ?3)") + "VALUES (?1, ?2, ?3)")
.setParameter(1, blockId).setParameter(2, mentioned.getId()) .setParameter(1, blockId).setParameter(2, mentioned.getId())
.setParameter(3, "Auguste Raddatz").executeUpdate(); .setParameter(3, "Auguste Raddatz").executeUpdate();
entityManager.createNativeQuery(
"INSERT INTO transcription_block_mentioned_persons (block_id, person_id, display_name) "
+ "VALUES (?1, ?2, ?3)")
.setParameter(1, blockId).setParameter(2, survivor.getId())
.setParameter(3, "Clara Cram").executeUpdate();
entityManager.flush(); entityManager.flush();
entityManager.clear(); entityManager.clear();
@@ -808,9 +815,15 @@ class PersonRepositoryTest {
.setParameter(1, mentioned.getId()).getSingleResult(); .setParameter(1, mentioned.getId()).getSingleResult();
assertThat(mentionRows.longValue()).isZero(); assertThat(mentionRows.longValue()).isZero();
// The cascade is scoped to the deleted person — the bystander's mention row is untouched.
Number survivorRows = (Number) entityManager.createNativeQuery(
"SELECT count(*) FROM transcription_block_mentioned_persons WHERE person_id = ?1")
.setParameter(1, survivor.getId()).getSingleResult();
assertThat(survivorRows.longValue()).isEqualTo(1);
String text = (String) entityManager.createNativeQuery( String text = (String) entityManager.createNativeQuery(
"SELECT text FROM transcription_blocks WHERE id = ?1") "SELECT text FROM transcription_blocks WHERE id = ?1")
.setParameter(1, blockId).getSingleResult(); .setParameter(1, blockId).getSingleResult();
assertThat(text).isEqualTo("Brief an @Auguste Raddatz"); assertThat(text).isEqualTo("Brief an @Auguste Raddatz und @Clara Cram");
} }
} }

View File

@@ -205,7 +205,7 @@ class PersonServiceIntegrationTest {
// The ON DELETE cascade fires beneath Hibernate — flush the delete and clear the L1 // The ON DELETE cascade fires beneath Hibernate — flush the delete and clear the L1
// cache so the asserting reads observe the post-delete database state, not stale // cache so the asserting reads observe the post-delete database state, not stale
// managed entities (the EAGER @ElementCollection on receivers makes this load-bearing). // managed entities still holding the dropped sender/receiver associations.
entityManager.flush(); entityManager.flush();
entityManager.clear(); entityManager.clear();

View File

@@ -45,7 +45,9 @@ letters. This is pinned by a non-negotiable document-survival assertion in
stay thin (`deleteById` + the cascade); `reassignSenderToNull` and `deleteReceiverReferences` stay thin (`deleteById` + the cascade); `reassignSenderToNull` and `deleteReceiverReferences`
are deleted. are deleted.
- This *fixes* the pre-existing dead-link-on-deleted-person case — it is not a purely - This *fixes* the pre-existing dead-link-on-deleted-person case — it is not a purely
invisible refactor. invisible refactor. Note the read renderer strips the `@` prefix when it emits a live
mention link, but the degraded (deleted-person) path leaves the literal `@Name` in the
block text as-is — the reader sees `@Auguste Raddatz` as plain text, never a dead link.
- DB cascades run below `AuditService`, so the row-level cleanup is intentionally not - DB cascades run below `AuditService`, so the row-level cleanup is intentionally not
audit-logged; the person-delete action itself is still logged at the service layer. audit-logged; the person-delete action itself is still logged at the service layer.
- The V71 FK validation requires cleaning pre-existing orphan mention rows first; the - The V71 FK validation requires cleaning pre-existing orphan mention rows first; the