Files
checkbot/tests/test_rule_decisions.py
patrickandClaude Sonnet 5.5 347f55cf7c
test / pytest (3.11) (push) Canceled after 0s
test / pytest (3.13) (push) Canceled after 0s
feat: Phase 6c baseline, ack, host alarms, outage notice, send limit, privacy mode, UI link
Includes atomic migrations, per-rule reminder semantics, first-run helper script
and the note on the undecided UI chat idea.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-06 03:06:23 +01:00

136 lines
5.8 KiB
Python

from app.chat import Target
from app.models import KIND_FLAPPING, KIND_PROBLEM, KIND_RECOVERY, KIND_REMINDER, Event
from app.routing import (ALL_KINDS, Route, ai_allowed, ai_hints, delay_provider, resolve_reminder_hours,
resolve_delay, resolve_loud, resolve_targets, route_matches)
TG = Target("telegram", "1")
MX = Target("matrix", "!r:s")
DEFAULTS = [TG, MX]
def rule(position=1, **overrides):
fields = dict(id=position, name=f"r{position}", position=position, enabled=True, match_host="",
match_service="", min_state=1, kinds=ALL_KINDS, stop_on_match=False, targets=())
fields.update(overrides)
return Route(**fields)
def event(kind=KIND_PROBLEM, state=2, output="boom", host="web01", service="disk"):
return Event(ts=1, host=host, service=service, kind=kind, state=state, output=output)
def test_output_contains_is_case_insensitive_substring():
r = rule(output_contains="DEGRADED")
assert route_matches(r, event(output="pool is degraded now"))
assert not route_matches(r, event(output="pool online"))
def test_output_contains_is_not_a_regex():
r = rule(output_contains="a.c")
assert route_matches(r, event(output="xa.cx"))
assert not route_matches(r, event(output="abc"))
def test_empty_output_filter_matches_everything():
assert route_matches(rule(), event(output=""))
def test_reminder_follows_problem_rules():
only_problem = rule(kinds=(KIND_PROBLEM,), min_state=2)
assert route_matches(only_problem, event(kind=KIND_REMINDER, state=2))
assert not route_matches(only_problem, event(kind=KIND_REMINDER, state=1))
assert not route_matches(rule(kinds=(KIND_RECOVERY,)), event(kind=KIND_REMINDER))
def test_kinds_and_min_state_still_apply():
r = rule(min_state=2, kinds=(KIND_PROBLEM, KIND_FLAPPING))
assert not route_matches(r, event(state=1))
assert not route_matches(r, event(kind=KIND_RECOVERY, state=0))
assert route_matches(r, event(kind=KIND_FLAPPING, state=1))
def test_delay_without_match_uses_default():
assert resolve_delay([], "h", "s", 2, "", 15) == 15
assert resolve_delay([rule(match_host="db*", delay_minutes=1)], "web01", "s", 2, "", 15) == 15
def test_delay_rule_without_value_uses_default():
assert resolve_delay([rule()], "h", "s", 2, "", 15) == 15
def test_delay_smallest_wins_and_zero_overrides_default():
rules = [rule(1, delay_minutes=10), rule(2, delay_minutes=0), rule(3, delay_minutes=5)]
assert resolve_delay(rules, "h", "s", 2, "", 15) == 0
assert resolve_delay([rule(1, delay_minutes=10), rule(2, delay_minutes=5)], "h", "s", 2, "", 15) == 5
def test_delay_respects_service_glob_state_and_output():
r = rule(match_service="*zpool*", min_state=1, output_contains="error", delay_minutes=0)
assert resolve_delay([r], "h", "ZPool status", 1, "Some Error", 15) == 0
assert resolve_delay([r], "h", "cpu", 1, "Some Error", 15) == 15
assert resolve_delay([r], "h", "zpool", 1, "fine", 15) == 15
assert resolve_delay([rule(min_state=2, delay_minutes=0)], "h", "s", 1, "", 15) == 15
def test_delay_ignores_disabled_rules_and_honours_stop():
assert resolve_delay([rule(enabled=False, delay_minutes=0)], "h", "s", 2, "", 15) == 15
rules = [rule(1, delay_minutes=7, stop_on_match=True), rule(2, delay_minutes=0)]
assert resolve_delay(rules, "h", "s", 2, "", 15) == 7
def test_delay_provider_returns_seconds():
provider = delay_provider([rule(match_service="zpool*", delay_minutes=0)], 15)
assert provider("h", "zpool status", 1, "") == 0
assert provider("h", "cpu", 1, "") == 900
def test_ai_allowed_off_as_soon_as_one_matching_rule_disables():
assert ai_allowed([], event())
assert ai_allowed([rule(1), rule(2, match_host="db*", ai_enabled=False)], event())
assert not ai_allowed([rule(1), rule(2, ai_enabled=False)], event())
def test_resolve_loud():
assert resolve_loud([], event())
assert not resolve_loud([rule(loud=False)], event())
assert resolve_loud([rule(1, loud=False), rule(2, loud=True)], event())
assert resolve_loud([rule(1, loud=False, match_host="db*")], event())
def test_ai_hints_in_rule_order_and_only_matching():
rules = [rule(2, ai_hint="zweiter"), rule(1, ai_hint="erster"), rule(3, ai_hint="x", match_host="db*"),
rule(4, ai_hint=" ")]
assert ai_hints(rules, event()) == "erster\nzweiter"
assert ai_hints([], event()) == ""
def test_ai_hints_clipped_to_1200_characters():
rules = [rule(1, ai_hint="a" * 800), rule(2, ai_hint="b" * 800)]
text = ai_hints(rules, event())
assert len(text) == 1200 and text.startswith("a" * 800) and text.endswith("b" * 399)
def test_reminder_hours_null_uses_default_zero_turns_off_positive_takes_smallest():
assert resolve_reminder_hours([], event(), 3) == 3
assert resolve_reminder_hours([rule(1, reminder_hours=None)], event(), 3) == 3
assert resolve_reminder_hours([rule(1, reminder_hours=0)], event(), 3) == 0
assert resolve_reminder_hours([rule(1, reminder_hours=8), rule(2, reminder_hours=4), rule(3)], event(), 3) == 4
def test_reminder_hours_explicit_zero_beats_other_matching_rules():
rules = [rule(1, reminder_hours=0), rule(2, reminder_hours=4), rule(3, reminder_hours=None)]
assert resolve_reminder_hours(rules, event(), 6) == 0
def test_reminder_hours_ignore_non_matching_rules():
rules = [rule(1, reminder_hours=0, match_host="db*"), rule(2, reminder_hours=5)]
assert resolve_reminder_hours(rules, event(), 6) == 5
def test_use_default_targets_adds_defaults_to_explicit_targets():
other = Target("telegram", "9")
assert resolve_targets([rule(use_default_targets=True)], event(), DEFAULTS) == DEFAULTS
assert resolve_targets([rule(targets=(other,), use_default_targets=True)], event(), DEFAULTS) == [other, TG, MX]
assert resolve_targets([rule(targets=(other,))], event(), DEFAULTS) == [other]
assert resolve_targets([rule()], event(), DEFAULTS) == []