feat(alerts): DS alignment + AGENT_LIFECYCLE + single-inbox redesign #146

Merged
hsiegeln merged 49 commits from feat/alerts-ds-alignment into main 2026-04-21 19:53:12 +02:00
10 changed files with 27 additions and 20 deletions
Showing only changes of commit 5b1b3f215a - Show all commits

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",