Compare commits

..

4 Commits

Author SHA1 Message Date
Marcel
13e0801b30 refactor(transcription): extract rewriteBlockText from propagation loop
Some checks failed
CI / Unit & Component Tests (push) Failing after 4m2s
CI / OCR Service Tests (push) Successful in 47s
CI / Backend Unit Tests (push) Failing after 3m16s
CI / Unit & Component Tests (pull_request) Failing after 3m16s
CI / OCR Service Tests (pull_request) Successful in 40s
CI / Backend Unit Tests (pull_request) Failing after 3m6s
Extracts the Pattern+Matcher+replaceAll block into a private helper so the
loop body reads as three lines: rewrite text, update sidecar entries, nothing
else. Moves the boundary-condition rationale comment to the helper.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-28 23:04:26 +02:00
Marcel
4c3aa159c5 test(transcription): add updateBlock 400 test for null personId in mention
createBlock has both validation guards (displayName length + personId null).
updateBlock had only the displayName test. Add the symmetric null-personId case
so a future @Valid drop from updateBlock's @RequestBody would be caught.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-28 23:03:00 +02:00
Marcel
eb51155b4e test(transcription): rename latency floor test to reflect 5s assertion
Method said inUnderTwoSeconds; assertion checks isLessThan(5000L) with message
"5s". Three sources of truth, three different values. Rename aligns method name
with the assertion that was intentionally raised from 2s to 5s in a prior commit.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-28 23:02:00 +02:00
Marcel
43f474fc5b refactor(repository): remove dead findByMentionedPersons_PersonId derived query
The listener exclusively calls findByPersonIdWithMentionsFetched (JOIN FETCH).
Zero callers exist in production or test code. Leaving it is a maintenance
trap: a future caller would silently trigger N+1 loads on the lazy collection.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-28 23:00:56 +02:00
4 changed files with 26 additions and 11 deletions

View File

@@ -29,8 +29,6 @@ public interface TranscriptionBlockRepository extends JpaRepository<Transcriptio
Optional<TranscriptionBlock> findByAnnotationId(UUID annotationId);
List<TranscriptionBlock> findByMentionedPersons_PersonId(UUID personId);
@Query("""
SELECT DISTINCT b FROM TranscriptionBlock b
JOIN FETCH b.mentionedPersons

View File

@@ -45,19 +45,12 @@ public class PersonMentionPropagationListener {
String oldNeedle = "@" + event.oldDisplayName();
String newNeedle = "@" + event.newDisplayName();
// Match @OldName only at a token boundary: not followed by a letter/digit/hyphen
// (catches @Hans-Peter when renaming Hans) AND not followed by " <Uppercase>"
// (catches @Hans Müller when renaming the single-name @Hans). False negatives —
// e.g. "@Hans Bekam" where Bekam is sentence-initial — are accepted as the
// conservative trade-off; the alternative (corruption) is irrecoverable.
Pattern boundary = Pattern.compile(
Pattern.quote(oldNeedle) + "(?![\\p{L}0-9\\-]| (?=\\p{Lu}))");
String replacement = Matcher.quoteReplacement(newNeedle);
for (TranscriptionBlock block : blocks) {
if (block.getText() != null) {
block.setText(boundary.matcher(block.getText()).replaceAll(replacement));
}
rewriteBlockText(block, boundary, replacement);
for (PersonMention mention : block.getMentionedPersons()) {
if (mention.getPersonId().equals(event.personId())) {
mention.setDisplayName(event.newDisplayName());
@@ -70,4 +63,15 @@ public class PersonMentionPropagationListener {
log.info("Propagated rename {} → {} across {} block(s) for person {}",
event.oldDisplayName(), event.newDisplayName(), blocks.size(), event.personId());
}
// Match @OldName only at a token boundary: not followed by a letter/digit/hyphen
// (catches @Hans-Peter when renaming Hans) AND not followed by " <Uppercase>"
// (catches @Hans Müller when renaming the single-name @Hans). False negatives —
// e.g. "@Hans Bekam" where Bekam is sentence-initial — are accepted as the
// conservative trade-off; the alternative (corruption) is irrecoverable.
private void rewriteBlockText(TranscriptionBlock block, Pattern boundary, String replacement) {
if (block.getText() != null) {
block.setText(boundary.matcher(block.getText()).replaceAll(replacement));
}
}
}

View File

@@ -266,6 +266,19 @@ class TranscriptionBlockControllerTest {
.andExpect(jsonPath("$.code").value("VALIDATION_ERROR"));
}
@Test
@WithMockUser(authorities = "WRITE_ALL")
void updateBlock_returns400_whenMentionedPersonPersonIdIsNull() throws Exception {
when(userService.findByEmail(any())).thenReturn(mockUser());
String body = "{\"text\":\"x\",\"mentionedPersons\":[{\"personId\":null,\"displayName\":\"Auguste Raddatz\"}]}";
mockMvc.perform(put(URL_BLOCK)
.contentType(MediaType.APPLICATION_JSON)
.content(body))
.andExpect(status().isBadRequest())
.andExpect(jsonPath("$.code").value("VALIDATION_ERROR"));
}
@Test
@WithMockUser(authorities = "WRITE_ALL")
void updateBlock_returns404_whenBlockDoesNotExist() throws Exception {

View File

@@ -165,7 +165,7 @@ class PersonMentionPropagationListenerTest {
}
@Test
void propagatesAcross200Blocks_inUnderTwoSeconds_latencyFloor() {
void propagatesAcross200Blocks_inUnderFiveSeconds_latencyFloor() {
UUID personId = savedPersonId("Auguste", "Raddatz");
List<UUID> blockIds = new ArrayList<>();
for (int i = 0; i < 200; i++) {