From 7adfa643f9edef0c48b98fa54dcd030e492517f6 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 11 Aug 2026 15:19:45 +0900 Subject: [PATCH 1/4] test(etl): prove durable job failure metadata invariant --- .../com/xtrmetl/etl/job/EtlJobModelTest.java | 52 +++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/etl-service/src/test/java/com/xtrmetl/etl/job/EtlJobModelTest.java b/etl-service/src/test/java/com/xtrmetl/etl/job/EtlJobModelTest.java index cd7556ae..c661c0d2 100644 --- a/etl-service/src/test/java/com/xtrmetl/etl/job/EtlJobModelTest.java +++ b/etl-service/src/test/java/com/xtrmetl/etl/job/EtlJobModelTest.java @@ -144,4 +144,56 @@ void rejectsNegativeAttemptCounts() { ) ); } + + @Test + void rejectsFailureCodeOutsideFailedState() { + assertThrows( + IllegalArgumentException.class, + () -> new EtlJobSnapshot( + JOB_RECORD_ID, + EtlJobStatus.SUCCEEDED, + 1, + "target_write_failed", + CREATED_AT, + UPDATED_AT + ) + ); + assertThrows( + IllegalArgumentException.class, + () -> new EtlJobStatusResponse( + JOB_RECORD_ID, + EtlJobStatus.RUNNING, + 1, + "target_write_failed", + CREATED_AT, + UPDATED_AT + ) + ); + } + + @Test + void requiresNonBlankFailureCodeForFailedState() { + assertThrows( + IllegalArgumentException.class, + () -> new EtlJobSnapshot( + JOB_RECORD_ID, + EtlJobStatus.FAILED, + 1, + null, + CREATED_AT, + UPDATED_AT + ) + ); + assertThrows( + IllegalArgumentException.class, + () -> new EtlJobStatusResponse( + JOB_RECORD_ID, + EtlJobStatus.FAILED, + 1, + " ", + CREATED_AT, + UPDATED_AT + ) + ); + } } From 816e30bdaf42bde2347d94eabe818d4375243591 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 11 Aug 2026 15:24:07 +0900 Subject: [PATCH 2/4] test(etl): preserve valid failed job representation --- .../com/xtrmetl/etl/job/EtlJobModelTest.java | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/etl-service/src/test/java/com/xtrmetl/etl/job/EtlJobModelTest.java b/etl-service/src/test/java/com/xtrmetl/etl/job/EtlJobModelTest.java index c661c0d2..79a6b361 100644 --- a/etl-service/src/test/java/com/xtrmetl/etl/job/EtlJobModelTest.java +++ b/etl-service/src/test/java/com/xtrmetl/etl/job/EtlJobModelTest.java @@ -62,6 +62,22 @@ void retainsValidSnapshotsAndMapsThemToStatusResponses() { assertEquals(UPDATED_AT, response.updatedAt()); } + @Test + void retainsValidFailedSnapshotsAndStatusResponses() { + EtlJobSnapshot snapshot = new EtlJobSnapshot( + JOB_RECORD_ID, + EtlJobStatus.FAILED, + 1, + "target_write_failed", + CREATED_AT, + UPDATED_AT + ); + EtlJobStatusResponse response = EtlJobStatusResponse.from(snapshot); + + assertEquals(EtlJobStatus.FAILED, response.jobStatus()); + assertEquals("target_write_failed", response.failureCode()); + } + @Test void rejectsNullRequiredModelValues() { assertThrows( From 2026960805d27eb945b859013023e01b16ea8d0e Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 11 Aug 2026 15:26:06 +0900 Subject: [PATCH 3/4] fix(etl): validate snapshot failure metadata lifecycle --- .../com/xtrmetl/etl/job/EtlJobSnapshot.java | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/etl-service/src/main/java/com/xtrmetl/etl/job/EtlJobSnapshot.java b/etl-service/src/main/java/com/xtrmetl/etl/job/EtlJobSnapshot.java index e8b20996..1ded14ca 100644 --- a/etl-service/src/main/java/com/xtrmetl/etl/job/EtlJobSnapshot.java +++ b/etl-service/src/main/java/com/xtrmetl/etl/job/EtlJobSnapshot.java @@ -16,7 +16,8 @@ * @param jobRecordId opaque durable job identifier * @param jobStatus current stable lifecycle state * @param attemptCount number of worker claims recorded for this job - * @param failureCode stable terminal failure code, or {@code null} before failure + * @param failureCode non-blank stable terminal failure code when {@code jobStatus} is + * {@link EtlJobStatus#FAILED}; otherwise {@code null} * @param createdAt creation timestamp * @param updatedAt most recent state-change timestamp */ @@ -30,12 +31,12 @@ public record EtlJobSnapshot( ) { /** - * Validates the immutable status representation. + * Validates the immutable status representation and its lifecycle-dependent failure metadata. * * @param jobRecordId opaque durable job identifier * @param jobStatus current stable lifecycle state * @param attemptCount non-negative number of worker claims - * @param failureCode stable terminal failure code, or {@code null} + * @param failureCode non-blank stable failure code exactly when the job has failed * @param createdAt creation timestamp * @param updatedAt most recent state-change timestamp */ @@ -47,5 +48,16 @@ public record EtlJobSnapshot( if (attemptCount < 0) { throw new IllegalArgumentException("attemptCount must not be negative"); } + if (jobStatus == EtlJobStatus.FAILED) { + if (failureCode == null || failureCode.isBlank()) { + throw new IllegalArgumentException( + "failureCode must be non-blank when jobStatus is FAILED" + ); + } + } else if (failureCode != null) { + throw new IllegalArgumentException( + "failureCode must be null unless jobStatus is FAILED" + ); + } } } From c051e5205614a5e1558c2dd36f6ffb639add1bc4 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 11 Aug 2026 15:26:46 +0900 Subject: [PATCH 4/4] fix(etl): validate response failure metadata lifecycle --- .../xtrmetl/etl/job/EtlJobStatusResponse.java | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/etl-service/src/main/java/com/xtrmetl/etl/job/EtlJobStatusResponse.java b/etl-service/src/main/java/com/xtrmetl/etl/job/EtlJobStatusResponse.java index abf23aa0..9975b49f 100644 --- a/etl-service/src/main/java/com/xtrmetl/etl/job/EtlJobStatusResponse.java +++ b/etl-service/src/main/java/com/xtrmetl/etl/job/EtlJobStatusResponse.java @@ -18,7 +18,8 @@ * @param jobRecordId opaque durable job identifier * @param jobStatus current stable lifecycle state * @param attemptCount number of worker claims recorded for this job - * @param failureCode stable terminal failure code, omitted before failure + * @param failureCode non-blank stable terminal failure code when {@code jobStatus} is + * {@link EtlJobStatus#FAILED}; otherwise omitted as {@code null} * @param createdAt creation timestamp serialized as an ISO-8601 string * @param updatedAt most recent state-change timestamp serialized as an ISO-8601 string */ @@ -32,12 +33,12 @@ public record EtlJobStatusResponse( ) { /** - * Validates the immutable status response. + * Validates the immutable status response and its lifecycle-dependent failure metadata. * * @param jobRecordId opaque durable job identifier * @param jobStatus current stable lifecycle state * @param attemptCount non-negative worker claim count - * @param failureCode stable terminal failure code, or {@code null} + * @param failureCode non-blank stable failure code exactly when the job has failed * @param createdAt creation timestamp * @param updatedAt most recent state-change timestamp */ @@ -49,6 +50,17 @@ public record EtlJobStatusResponse( if (attemptCount < 0) { throw new IllegalArgumentException("attemptCount must not be negative"); } + if (jobStatus == EtlJobStatus.FAILED) { + if (failureCode == null || failureCode.isBlank()) { + throw new IllegalArgumentException( + "failureCode must be non-blank when jobStatus is FAILED" + ); + } + } else if (failureCode != null) { + throw new IllegalArgumentException( + "failureCode must be null unless jobStatus is FAILED" + ); + } } /**