test(alerts): state machine — ack is orthogonal, does not transition FIRING

- AlertStateTransitionsTest: add null,null for readAt/deletedAt in openInstance helper;
  replace firingWhenAcknowledgedIsNoOp with firing_with_ack_stays_firing_on_next_firing_tick;
  convert ackedInstanceClearsToResolved to use FIRING+withAck; update section comment.
- PostgresAlertInstanceRepository: stub null,null for readAt/deletedAt in rowMapper
  to unblock compilation (Task 4 will read the actual DB columns).
- All other alerting test files: add null,null for readAt/deletedAt to AlertInstance
  ctor calls so the test source tree compiles; stub ACKNOWLEDGED JSON/state assertions
  with FIRING + TODO Task 4 comments.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
hsiegeln
2026-04-21 17:28:31 +02:00
parent 82e82350f9
commit 5b1b3f215a
10 changed files with 27 additions and 20 deletions

View File

@@ -235,6 +235,8 @@ public class PostgresAlertInstanceRepository implements AlertInstanceRepository
rs.getString("acked_by"),
resolvedAt == null ? null : resolvedAt.toInstant(),
lastNotifiedAt == null ? null : lastNotifiedAt.toInstant(),
null, // readAt — TODO Task 4: read from DB column
null, // deletedAt — TODO Task 4: read from DB column
rs.getBoolean("silenced"),
currentValue,
threshold,

View File

@@ -243,11 +243,11 @@ class AlertingFullLifecycleIT extends AbstractPostgresIT {
assertThat(resp.getStatusCode()).isEqualTo(HttpStatus.OK);
JsonNode body = objectMapper.readTree(resp.getBody());
assertThat(body.path("state").asText()).isEqualTo("ACKNOWLEDGED");
assertThat(body.path("state").asText()).isEqualTo("FIRING"); // TODO Task 4: ack no longer changes state
// DB state
AlertInstance updated = instanceRepo.findById(instanceId).orElseThrow();
assertThat(updated.state()).isEqualTo(AlertState.ACKNOWLEDGED);
assertThat(updated.state()).isEqualTo(AlertState.FIRING); // TODO Task 4: ack is orthogonal to state
}
@Test

View File

@@ -138,7 +138,7 @@ class AlertControllerIT extends AbstractPostgresIT {
assertThat(ack.getStatusCode()).isEqualTo(HttpStatus.OK);
JsonNode body = objectMapper.readTree(ack.getBody());
assertThat(body.path("state").asText()).isEqualTo("ACKNOWLEDGED");
assertThat(body.path("state").asText()).isEqualTo("FIRING"); // TODO Task 4: ack is orthogonal to state
}
@Test
@@ -192,7 +192,7 @@ class AlertControllerIT extends AbstractPostgresIT {
AlertInstance instance = new AlertInstance(
UUID.randomUUID(), null, null, envId,
AlertState.FIRING, AlertSeverity.WARNING,
Instant.now(), null, null, null, null, false,
Instant.now(), null, null, null, null, null, null, false,
42.0, 1000.0, null, "Test alert", "Something happened",
List.of("test-operator"), List.of(), List.of());
return instanceRepo.save(instance);

View File

@@ -175,7 +175,7 @@ class AlertNotificationControllerIT extends AbstractPostgresIT {
AlertInstance instance = new AlertInstance(
UUID.randomUUID(), null, null, envId,
AlertState.FIRING, AlertSeverity.WARNING,
Instant.now(), null, null, null, null, false,
Instant.now(), null, null, null, null, null, null, false,
42.0, 1000.0, null, "Test alert", "Something happened",
List.of(), List.of(), List.of("OPERATOR"));
return instanceRepo.save(instance);

View File

@@ -34,7 +34,7 @@ class AlertStateTransitionsTest {
return new AlertInstance(
UUID.randomUUID(), UUID.randomUUID(), Map.of(), UUID.randomUUID(),
state, AlertSeverity.WARNING,
firedAt, null, ackedBy, null, null, false,
firedAt, null, ackedBy, null, null, null, null, false,
1.0, null, Map.of(), "title", "msg",
List.of(), List.of(), List.of());
}
@@ -71,7 +71,8 @@ class AlertStateTransitionsTest {
@Test
void ackedInstanceClearsToResolved() {
var acked = openInstance(AlertState.ACKNOWLEDGED, NOW.minusSeconds(30), "alice");
var acked = openInstance(AlertState.FIRING, NOW.minusSeconds(30), null)
.withAck("alice", Instant.parse("2026-04-19T11:55:00Z"));
var next = AlertStateTransitions.apply(acked, EvalResult.Clear.INSTANCE, ruleWith(0), NOW);
assertThat(next).hasValueSatisfying(i -> {
assertThat(i.state()).isEqualTo(AlertState.RESOLVED);
@@ -131,7 +132,7 @@ class AlertStateTransitionsTest {
}
// -------------------------------------------------------------------------
// Firing branch — already open FIRING / ACKNOWLEDGED
// Firing branch — already open FIRING (with or without ack)
// -------------------------------------------------------------------------
@Test
@@ -142,9 +143,13 @@ class AlertStateTransitionsTest {
}
@Test
void firingWhenAcknowledgedIsNoOp() {
var acked = openInstance(AlertState.ACKNOWLEDGED, NOW.minusSeconds(30), "alice");
var next = AlertStateTransitions.apply(acked, FIRING_RESULT, ruleWith(0), NOW);
void firing_with_ack_stays_firing_on_next_firing_tick() {
// Pre-redesign this was the "ACKNOWLEDGED stays ACK" case. Post-redesign,
// ack is orthogonal; an acked FIRING row stays FIRING and no update is needed.
var current = openInstance(AlertState.FIRING, NOW.minusSeconds(30), null)
.withAck("alice", Instant.parse("2026-04-21T10:00:00Z"));
var next = AlertStateTransitions.apply(
current, new EvalResult.Firing(1.0, null, Map.of()), ruleWith(0), NOW);
assertThat(next).isEmpty();
}

View File

@@ -75,7 +75,7 @@ class NotificationContextBuilderTest {
INST_ID, RULE_ID, Map.of(), ENV_ID,
AlertState.FIRING, AlertSeverity.CRITICAL,
Instant.parse("2026-04-19T10:00:00Z"),
null, null, null, null,
null, null, null, null, null, null,
false, 0.95, 0.1,
ctx, "Alert fired", "Some message",
List.of(), List.of(), List.of()

View File

@@ -89,7 +89,7 @@ class NotificationDispatchJobIT extends AbstractPostgresIT {
instanceRepo.save(new AlertInstance(
instanceId, ruleId, Map.of(), envId,
AlertState.FIRING, AlertSeverity.WARNING,
Instant.now(), null, null, null, null, false,
Instant.now(), null, null, null, null, null, null, false,
null, null, Map.of(), "title", "msg",
List.of(), List.of(), List.of()));

View File

@@ -30,7 +30,7 @@ class SilenceMatcherServiceTest {
return new AlertInstance(
INST_ID, RULE_ID, Map.of(), ENV_ID,
AlertState.FIRING, AlertSeverity.WARNING,
Instant.now(), null, null, null, null,
Instant.now(), null, null, null, null, null, null,
false, 1.5, 1.0,
Map.of(), "title", "msg",
List.of(), List.of(), List.of()
@@ -85,7 +85,7 @@ class SilenceMatcherServiceTest {
var inst = new AlertInstance(
INST_ID, null, Map.of(), ENV_ID,
AlertState.FIRING, AlertSeverity.WARNING,
Instant.now(), null, null, null, null,
Instant.now(), null, null, null, null, null, null,
false, null, null,
Map.of(), "t", "m",
List.of(), List.of(), List.of()
@@ -99,7 +99,7 @@ class SilenceMatcherServiceTest {
var inst = new AlertInstance(
INST_ID, null, Map.of(), ENV_ID,
AlertState.FIRING, AlertSeverity.WARNING,
Instant.now(), null, null, null, null,
Instant.now(), null, null, null, null, null, null,
false, null, null,
Map.of(), "t", "m",
List.of(), List.of(), List.of()

View File

@@ -188,7 +188,7 @@ class WebhookDispatcherIT {
return new AlertInstance(
UUID.randomUUID(), UUID.randomUUID(), Map.of(),
UUID.randomUUID(), AlertState.FIRING, AlertSeverity.WARNING,
Instant.now(), null, null, null, null, false,
Instant.now(), null, null, null, null, null, null, false,
null, null, Map.of(), "Alert", "Message",
List.of(), List.of(), List.of());
}

View File

@@ -176,7 +176,7 @@ class PostgresAlertInstanceRepositoryIT extends AbstractPostgresIT {
repo.ack(inst.id(), userId, when);
var found = repo.findById(inst.id()).orElseThrow();
assertThat(found.state()).isEqualTo(AlertState.ACKNOWLEDGED);
assertThat(found.state()).isEqualTo(AlertState.FIRING); // TODO Task 4: ack no longer changes state
assertThat(found.ackedBy()).isEqualTo(userId);
assertThat(found.ackedAt()).isNotNull();
}
@@ -325,7 +325,7 @@ class PostgresAlertInstanceRepositoryIT extends AbstractPostgresIT {
return new AlertInstance(
UUID.randomUUID(), ruleId, Map.of(), envId,
AlertState.FIRING, severity,
Instant.now(), null, null, null, null,
Instant.now(), null, null, null, null, null, null,
false, null, null,
Map.of(), "title", "message",
userIds, groupIds, roleNames);
@@ -341,7 +341,7 @@ class PostgresAlertInstanceRepositoryIT extends AbstractPostgresIT {
return new AlertInstance(
UUID.randomUUID(), ruleId, Map.of(), envId,
AlertState.FIRING, AlertSeverity.WARNING,
Instant.now(), null, null, null, null,
Instant.now(), null, null, null, null, null, null,
false, null, null,
Map.of("exchange", Map.of("id", exchangeId)),
"title", "message",