fix(alerting): populate AlertInstance.rule_snapshot so history survives rule delete
- Add withRuleSnapshot(Map) wither to AlertInstance (same pattern as other withers) - Call snapshotRule(rule) + withRuleSnapshot in both applyResult (single-firing) and applyBatchFiring paths so every persisted instance carries a non-empty JSONB snapshot - Strip null values from the Jackson-serialized map before wrapping in the immutable snapshot so Map.copyOf in the compact ctor does not throw NPE on nullable rule fields - Add ruleSnapshotIsPersistedOnInstanceCreation IT: asserts name/severity/conditionKind appear in the rule_snapshot column after a tick fires an instance - Add historySurvivesRuleDelete IT: fires an instance, deletes the rule, asserts rule_id IS NULL and rule_snapshot still contains the rule name (spec §5 guarantee) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -196,4 +196,53 @@ class AlertEvaluatorJobIT extends AbstractPostgresIT {
|
||||
jdbcTemplate.update("DELETE FROM alert_instances WHERE rule_id = ?", ruleId2);
|
||||
jdbcTemplate.update("DELETE FROM alert_rules WHERE id = ?", ruleId2);
|
||||
}
|
||||
|
||||
@Test
|
||||
void ruleSnapshotIsPersistedOnInstanceCreation() {
|
||||
// Dead agent → FIRING instance created
|
||||
when(agentRegistryService.findAll())
|
||||
.thenReturn(List.of(deadAgent(Instant.now().minusSeconds(120))));
|
||||
|
||||
job.tick();
|
||||
|
||||
// Read rule_snapshot directly from the DB — must contain name, severity, conditionKind
|
||||
String snapshot = jdbcTemplate.queryForObject(
|
||||
"SELECT rule_snapshot::text FROM alert_instances WHERE rule_id = ?",
|
||||
String.class, ruleId);
|
||||
|
||||
assertThat(snapshot).isNotNull();
|
||||
assertThat(snapshot).contains("\"name\": \"dead-agent-rule\"");
|
||||
assertThat(snapshot).contains("\"severity\": \"WARNING\"");
|
||||
assertThat(snapshot).contains("\"conditionKind\": \"AGENT_STATE\"");
|
||||
}
|
||||
|
||||
@Test
|
||||
void historySurvivesRuleDelete() {
|
||||
// Seed: dead agent → FIRING instance created
|
||||
when(agentRegistryService.findAll())
|
||||
.thenReturn(List.of(deadAgent(Instant.now().minusSeconds(120))));
|
||||
job.tick();
|
||||
|
||||
// Verify instance exists with a populated snapshot
|
||||
String snapshotBefore = jdbcTemplate.queryForObject(
|
||||
"SELECT rule_snapshot::text FROM alert_instances WHERE rule_id = ?",
|
||||
String.class, ruleId);
|
||||
assertThat(snapshotBefore).contains("\"name\": \"dead-agent-rule\"");
|
||||
|
||||
// Delete the rule — ON DELETE SET NULL clears rule_id on the instance
|
||||
ruleRepo.delete(ruleId);
|
||||
|
||||
// rule_id must be NULL on the instance row
|
||||
Long nullRuleIdCount = jdbcTemplate.queryForObject(
|
||||
"SELECT count(*) FROM alert_instances WHERE rule_id IS NULL AND rule_snapshot::text LIKE '%dead-agent-rule%'",
|
||||
Long.class);
|
||||
assertThat(nullRuleIdCount).isEqualTo(1L);
|
||||
|
||||
// snapshot still contains the rule name — history survives deletion
|
||||
String snapshotAfter = jdbcTemplate.queryForObject(
|
||||
"SELECT rule_snapshot::text FROM alert_instances WHERE rule_id IS NULL AND rule_snapshot::text LIKE '%dead-agent-rule%'",
|
||||
String.class);
|
||||
assertThat(snapshotAfter).contains("\"name\": \"dead-agent-rule\"");
|
||||
assertThat(snapshotAfter).contains("\"severity\": \"WARNING\"");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user