diff --git a/cameleer-server-app/src/main/java/com/cameleer/server/app/alerting/controller/AlertNotificationController.java b/cameleer-server-app/src/main/java/com/cameleer/server/app/alerting/controller/AlertNotificationController.java index 5cb11d2d..903d0591 100644 --- a/cameleer-server-app/src/main/java/com/cameleer/server/app/alerting/controller/AlertNotificationController.java +++ b/cameleer-server-app/src/main/java/com/cameleer/server/app/alerting/controller/AlertNotificationController.java @@ -69,10 +69,7 @@ public class AlertNotificationController { } // Reset for retry: status -> PENDING, attempts -> 0, next_attempt_at -> now - // We use scheduleRetry to reset attempt timing; then we need to reset attempts count. - // The repository has scheduleRetry which sets next_attempt_at and records last status. - // We use a dedicated pattern: mark as pending by scheduling immediately. - notificationRepo.scheduleRetry(id, Instant.now(), 0, null); + notificationRepo.resetForRetry(id, Instant.now()); return AlertNotificationDto.from(notificationRepo.findById(id) .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND))); diff --git a/cameleer-server-app/src/main/java/com/cameleer/server/app/alerting/storage/PostgresAlertNotificationRepository.java b/cameleer-server-app/src/main/java/com/cameleer/server/app/alerting/storage/PostgresAlertNotificationRepository.java index 88bd5e1a..c05e3e6c 100644 --- a/cameleer-server-app/src/main/java/com/cameleer/server/app/alerting/storage/PostgresAlertNotificationRepository.java +++ b/cameleer-server-app/src/main/java/com/cameleer/server/app/alerting/storage/PostgresAlertNotificationRepository.java @@ -118,6 +118,21 @@ public class PostgresAlertNotificationRepository implements AlertNotificationRep """, Timestamp.from(nextAttemptAt), status, snippet, id); } + @Override + public void resetForRetry(UUID id, Instant nextAttemptAt) { + jdbc.update(""" + UPDATE alert_notifications + SET attempts = 0, + status = 'PENDING'::notification_status_enum, + next_attempt_at = ?, + claimed_by = NULL, + claimed_until = NULL, + last_response_status = NULL, + last_response_snippet = NULL + WHERE id = ? + """, Timestamp.from(nextAttemptAt), id); + } + @Override public void markFailed(UUID id, int status, String snippet) { jdbc.update(""" diff --git a/cameleer-server-app/src/test/java/com/cameleer/server/app/alerting/controller/AlertNotificationControllerIT.java b/cameleer-server-app/src/test/java/com/cameleer/server/app/alerting/controller/AlertNotificationControllerIT.java index 1d19c161..766af9db 100644 --- a/cameleer-server-app/src/test/java/com/cameleer/server/app/alerting/controller/AlertNotificationControllerIT.java +++ b/cameleer-server-app/src/test/java/com/cameleer/server/app/alerting/controller/AlertNotificationControllerIT.java @@ -113,6 +113,35 @@ class AlertNotificationControllerIT extends AbstractPostgresIT { assertThat(resp.getStatusCode()).isEqualTo(HttpStatus.OK); } + @Test + void retryResetsAttemptsToZero() throws Exception { + // Verify Fix I-1: retry endpoint resets attempts to 0, not attempts+1 + AlertInstance instance = seedInstance(); + AlertNotification notification = seedNotification(instance.id()); + + // Mark as failed with attempts at max (simulate exhausted retries) + notificationRepo.markFailed(notification.id(), 500, "server error"); + notificationRepo.markFailed(notification.id(), 500, "server error"); + notificationRepo.markFailed(notification.id(), 500, "server error"); + + // Verify attempts > 0 before retry + AlertNotification before = notificationRepo.findById(notification.id()).orElseThrow(); + assertThat(before.attempts()).isGreaterThan(0); + + // Operator retries + ResponseEntity resp = restTemplate.exchange( + "/api/v1/alerts/notifications/" + notification.id() + "/retry", + HttpMethod.POST, + new HttpEntity<>(securityHelper.authHeaders(operatorJwt)), + String.class); + assertThat(resp.getStatusCode()).isEqualTo(HttpStatus.OK); + + // After retry: attempts must be 0 and status PENDING (not attempts+1) + AlertNotification after = notificationRepo.findById(notification.id()).orElseThrow(); + assertThat(after.attempts()).as("retry must reset attempts to 0").isEqualTo(0); + assertThat(after.status()).isEqualTo(NotificationStatus.PENDING); + } + @Test void viewerCannotRetry() throws Exception { AlertInstance instance = seedInstance(); diff --git a/cameleer-server-core/src/main/java/com/cameleer/server/core/alerting/AlertNotificationRepository.java b/cameleer-server-core/src/main/java/com/cameleer/server/core/alerting/AlertNotificationRepository.java index b49d84f9..58502112 100644 --- a/cameleer-server-core/src/main/java/com/cameleer/server/core/alerting/AlertNotificationRepository.java +++ b/cameleer-server-core/src/main/java/com/cameleer/server/core/alerting/AlertNotificationRepository.java @@ -13,5 +13,7 @@ public interface AlertNotificationRepository { void markDelivered(UUID id, int status, String snippet, Instant when); void scheduleRetry(UUID id, Instant nextAttemptAt, int status, String snippet); void markFailed(UUID id, int status, String snippet); + /** Resets a FAILED notification for operator-triggered retry: attempts → 0, status → PENDING. */ + void resetForRetry(UUID id, Instant nextAttemptAt); void deleteSettledBefore(Instant cutoff); }