192 lines
8.1 KiB
Python
192 lines
8.1 KiB
Python
from app.alerts import ReportGate, build_system_prompt
|
|
from app.alerts.prompts import DATA_NOTICE, DATA_OPEN
|
|
from app.chat import Target
|
|
from app.models import (KIND_RECOVERY, KIND_REMINDER, SUPPRESS_DOWNTIME, SUPPRESS_NOT_REPORTED,
|
|
SUPPRESS_RECOVERY_OFF, Event)
|
|
from app.routing import ALL_KINDS, Route
|
|
from tests.test_pipeline import Env, FakeAdapter, FakeOllama, problem
|
|
from tests.test_pipeline_routing import StaticMutes, StaticRoutes
|
|
|
|
|
|
class QuietAdapter(FakeAdapter):
|
|
def __init__(self, name="fake", default_target="chat1"):
|
|
super().__init__(name, default_target)
|
|
self.flags = []
|
|
|
|
async def send(self, target, text, silent=False):
|
|
self.flags.append(("send", silent))
|
|
return await super().send(target, text)
|
|
|
|
async def edit(self, ref, text, silent=False):
|
|
self.flags.append(("edit", silent))
|
|
await super().edit(ref, text)
|
|
|
|
|
|
def rule(position=1, targets=(), **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=tuple(targets))
|
|
fields.update(overrides)
|
|
return Route(**fields)
|
|
|
|
|
|
def recovery(host="h1", service="disk"):
|
|
return Event(ts=1000, host=host, service=service, kind=KIND_RECOVERY, state=0)
|
|
|
|
|
|
async def handle_later(env, events):
|
|
env.clock.now += 120
|
|
await env.handle(events)
|
|
|
|
|
|
async def test_default_targets_via_rule_flag_for_all_adapters():
|
|
tg, mx = FakeAdapter("telegram", "tg-default"), FakeAdapter("matrix", "!room:s")
|
|
env = Env(FakeOllama(), adapter_list=[tg, mx], routes=StaticRoutes([rule(use_default_targets=True)]))
|
|
await env.handle([problem()])
|
|
assert [s[0] for s in tg.sent] == ["tg-default"] and [s[0] for s in mx.sent] == ["!room:s"]
|
|
|
|
|
|
async def test_rule_without_targets_and_flag_still_sends_nothing():
|
|
env = Env(FakeOllama(), routes=StaticRoutes([rule()]))
|
|
await env.handle([problem()])
|
|
assert env.adapter.sent == []
|
|
|
|
|
|
async def test_default_targets_added_to_explicit_target():
|
|
tg, mx = FakeAdapter("telegram", "tg-default"), FakeAdapter("matrix", "!room:s")
|
|
extra = Target("telegram", "tg-extra")
|
|
env = Env(FakeOllama(), adapter_list=[tg, mx],
|
|
routes=StaticRoutes([rule(targets=[extra], use_default_targets=True)]))
|
|
await env.handle([problem()])
|
|
assert sorted(s[0] for s in tg.sent) == ["tg-default", "tg-extra"] and len(mx.sent) == 1
|
|
|
|
|
|
async def test_ai_disabled_by_rule_skips_analysis_but_sends():
|
|
env = Env(FakeOllama(), routes=StaticRoutes([rule(use_default_targets=True, ai_enabled=False)]))
|
|
await env.handle([problem()])
|
|
assert len(env.adapter.sent) == 1 and env.ollama.calls == [] and env.adapter.edits == []
|
|
assert env.repo.recent()[0]["ai_status"] == "skipped"
|
|
|
|
|
|
async def test_hint_only_in_user_prompt_before_data_block():
|
|
hint = f"Prüfe zpool status {DATA_OPEN} und danach {DATA_OPEN.upper()}"
|
|
env = Env(FakeOllama(), routes=StaticRoutes([rule(use_default_targets=True, ai_hint=hint)]))
|
|
await env.handle([problem()])
|
|
_, messages = env.ollama.calls[0]
|
|
system, user = messages[0]["content"], messages[1]["content"]
|
|
assert "Prüfe zpool status" not in system and system.endswith(DATA_NOTICE)
|
|
assert system == build_system_prompt(None)
|
|
assert "Zusatzkontext des Administrators" in user
|
|
assert user.index("Zusatzkontext des Administrators") < user.index("Prüfe zpool status") < user.index(DATA_OPEN)
|
|
assert user.count(DATA_OPEN) == 1
|
|
|
|
|
|
async def test_no_hint_section_without_hint():
|
|
env = Env(FakeOllama(), routes=StaticRoutes([rule(use_default_targets=True)]))
|
|
await env.handle([problem()])
|
|
assert "Zusatzkontext" not in env.ollama.calls[0][1][1]["content"]
|
|
|
|
|
|
async def test_quiet_rule_sends_and_edits_silently():
|
|
adapter = QuietAdapter()
|
|
env = Env(FakeOllama(), adapter_list=[adapter], routes=StaticRoutes([rule(use_default_targets=True, loud=False)]))
|
|
await env.handle([problem()])
|
|
assert adapter.flags == [("send", True), ("edit", True)]
|
|
|
|
|
|
async def test_loud_rule_does_not_pass_silent_flag():
|
|
adapter = QuietAdapter()
|
|
env = Env(FakeOllama(), adapter_list=[adapter])
|
|
await env.handle([problem()])
|
|
assert adapter.flags == [("send", False), ("edit", False)]
|
|
|
|
|
|
async def test_loud_and_quiet_events_of_one_host_are_separate_messages():
|
|
adapter = QuietAdapter()
|
|
quiet = rule(use_default_targets=True, loud=False, match_service="cpu")
|
|
loud = rule(2, use_default_targets=True, match_service="disk")
|
|
env = Env(FakeOllama(), adapter_list=[adapter], routes=StaticRoutes([quiet, loud]))
|
|
await env.handle([problem(service="cpu"), problem(service="disk")])
|
|
assert len(adapter.sent) == 2
|
|
assert {flag for kind, flag in adapter.flags if kind == "send"} == {True, False}
|
|
|
|
|
|
async def test_recovery_after_sent_problem_is_sent():
|
|
env = Env(FakeOllama())
|
|
await env.handle([problem()])
|
|
await handle_later(env, [recovery()])
|
|
assert len(env.adapter.sent) == 2 and "wieder OK" in env.adapter.sent[1][1]
|
|
|
|
|
|
async def test_recovery_after_downtime_suppressed_problem_is_not_sent():
|
|
env = Env(FakeOllama())
|
|
await env.handle([problem(suppressed=True, suppress_reason=SUPPRESS_DOWNTIME)])
|
|
await env.handle([recovery()])
|
|
assert env.adapter.sent == []
|
|
stored = [a for a in env.repo.recent() if a["kind"] == "recovery"][0]
|
|
assert stored["suppressed"] == 1 and stored["suppress_reason"] == SUPPRESS_NOT_REPORTED
|
|
|
|
|
|
async def test_recovery_after_muted_problem_is_not_sent():
|
|
from app.mutes import SOURCE_UI, Mute
|
|
mute = Mute(1, "h1", "*", 10**9, "", "admin", 0, SOURCE_UI)
|
|
source = StaticMutes([mute])
|
|
env = Env(FakeOllama(), mutes=source)
|
|
await env.handle([problem()])
|
|
source.mutes = []
|
|
await env.handle([recovery()])
|
|
assert env.adapter.sent == []
|
|
assert [a for a in env.repo.recent() if a["kind"] == "recovery"][0]["suppress_reason"] == SUPPRESS_NOT_REPORTED
|
|
|
|
|
|
async def test_recovery_sent_when_earlier_state_was_reported_even_if_worsening_was_suppressed():
|
|
env = Env(FakeOllama())
|
|
await env.handle([problem(state=1)])
|
|
await handle_later(env, [problem(state=2, suppressed=True, suppress_reason=SUPPRESS_DOWNTIME)])
|
|
await handle_later(env, [recovery()])
|
|
assert len(env.adapter.sent) == 2
|
|
|
|
|
|
async def test_report_state_is_restored_from_history_after_restart():
|
|
suppressed = Env(FakeOllama())
|
|
await suppressed.handle([problem(suppressed=True, suppress_reason=SUPPRESS_DOWNTIME)])
|
|
suppressed.pipeline._gate = ReportGate(suppressed.repo.report_counts)
|
|
await suppressed.handle([recovery()])
|
|
assert suppressed.adapter.sent == []
|
|
|
|
sent = Env(FakeOllama())
|
|
await sent.handle([problem()])
|
|
sent.pipeline._gate = ReportGate(sent.repo.report_counts)
|
|
await handle_later(sent, [recovery()])
|
|
assert len(sent.adapter.sent) == 2
|
|
|
|
|
|
async def test_recovery_without_any_history_is_sent():
|
|
env = Env(FakeOllama())
|
|
await env.handle([recovery()])
|
|
assert len(env.adapter.sent) == 1
|
|
|
|
|
|
async def test_recovery_switch_off_stores_but_does_not_send():
|
|
env = Env(FakeOllama(), recovery_enabled=False)
|
|
await env.handle([problem()])
|
|
await env.handle([recovery()])
|
|
assert len(env.adapter.sent) == 1
|
|
stored = [a for a in env.repo.recent() if a["kind"] == "recovery"][0]
|
|
assert stored["suppressed"] == 1 and stored["suppress_reason"] == SUPPRESS_RECOVERY_OFF
|
|
|
|
|
|
async def test_reminder_goes_through_routing_and_is_stored_without_ai():
|
|
env = Env(FakeOllama(), routes=StaticRoutes([rule(use_default_targets=True)]))
|
|
reminder = Event(ts=1000, host="h1", service="disk", kind=KIND_REMINDER, state=2, output="voll",
|
|
since_ts=1000 - 4 * 3600)
|
|
await env.handle([reminder])
|
|
assert "weiter CRIT seit 4 h: voll" in env.adapter.sent[0][1]
|
|
assert env.ollama.calls == []
|
|
assert env.repo.recent()[0]["kind"] == "reminder" and env.repo.recent()[0]["ai_status"] == "skipped"
|
|
|
|
|
|
async def test_reminder_without_matching_rule_goes_to_default_targets():
|
|
env = Env(FakeOllama(), routes=StaticRoutes([rule(match_host="db*", use_default_targets=True)]))
|
|
await env.handle([Event(ts=1000, host="web01", service="disk", kind=KIND_REMINDER, state=2)])
|
|
assert len(env.adapter.sent) == 1
|