# Copyright (c) 2026 Krzysztof Rudnicki. MIT licence. """The Analytics tab's seven routes. The empty-database cases matter as much as the populated ones: this page is most useful exactly when something is wrong, so every panel has to render against a bot that has never run. """ from __future__ import annotations from typing import TYPE_CHECKING from signal_bot.external import Result from signal_bot.manage import routes_analytics from signal_bot.store import Store from tests.analytics_seed import ALA, BASIA, Shape, a_call, backfilled, ran, reconstructed from tests.manage_fixture import FIXTURE_GROUP, client_for, deps_at if TYPE_CHECKING: from pathlib import Path import pytest from signal_bot.manage.deps import ManageDeps DAY = "2026-08-29" NEXT = "2026-08-30" PANELS = ("overview", "time", "health", "demand", "behaviour", "content") def seeded(tmp_path: Path) -> ManageDeps: """A management app pointed at a database with a small history in it.""" deps = deps_at(tmp_path) with Store(str(deps.database_path)) as store: for minute in range(3): ran( store, day=DAY, hour=19, minute=minute, who=ALA, command="pope", shape=Shape( group_id=FIXTURE_GROUP, item_id=f"Q{minute}", handler_ms=300 + minute, send_ms=200, calls=(a_call("commons.example.test", 200, 40),), ), ) ran(store, day=NEXT, hour=9, who=BASIA, command="cat", shape=Shape(group_id=FIXTURE_GROUP)) reconstructed(store, day=DAY, hour=8, command="dog", who=BASIA, group_id=FIXTURE_GROUP) backfilled(store, day=DAY, command="gecko", who=ALA, group_id=FIXTURE_GROUP) store.events.note_person(FIXTURE_GROUP, ALA, "ala", "", DAY) store.suggestions.record_unknown(FIXTURE_GROUP, "!papiez!", "!papiez!", 5) return deps def test_every_panel_renders_against_a_bot_that_never_ran(tmp_path: Path) -> None: """A fresh install must show empty panels, never a stack trace.""" client = client_for(deps_at(tmp_path)) for panel in PANELS: response = client.get(f"/api/analytics/{panel}") assert response.status_code == 200, panel body = response.json() assert body["window"]["events"] == 0 assert client.get("/api/analytics/groups").json() == {"groups": []} def test_every_panel_renders_against_a_file_of_nonsense(tmp_path: Path) -> None: """Half a database is the same answer as none of one.""" deps = deps_at(tmp_path) deps.database_path.write_bytes(b"this is not a database") client = client_for(deps) for panel in PANELS: assert client.get(f"/api/analytics/{panel}").status_code == 200 def test_the_groups_route_lists_what_has_a_history(tmp_path: Path) -> None: client = client_for(seeded(tmp_path)) groups = client.get("/api/analytics/groups").json()["groups"] assert [one["id"] for one in groups] == [FIXTURE_GROUP] assert groups[0]["events"] == 6 def test_the_window_names_all_three_sources(tmp_path: Path) -> None: """The footnote's three numbers, so the page can say what is reconstructed.""" client = client_for(seeded(tmp_path)) window = client.get("/api/analytics/overview").json()["window"] assert window["sources"] == {"live": 4, "journald": 1, "daily_uses": 1} assert window["days"] == 2 assert window["enough_for_a_trend"] is False def test_overview_names_people_rather_than_uuids(tmp_path: Path) -> None: """A uuid never appears on the page without a name beside it.""" client = client_for(seeded(tmp_path)) people = client.get("/api/analytics/overview").json()["people"] named = {one["uuid"]: one["name"] for one in people} assert named[ALA] == "ala" assert named[BASIA] == "" def test_time_returns_dense_buckets(tmp_path: Path) -> None: """Twenty-four hours and seven weekdays, empty ones included.""" body = client_for(seeded(tmp_path)).get("/api/analytics/time").json() assert len(body["hours"]) == 24 assert len(body["weekday_hours"]) == 7 assert body["peak"]["busiest"] == 19 assert body["direction"] == "unknown" def test_health_reports_timings_and_upstream(tmp_path: Path) -> None: body = client_for(seeded(tmp_path)).get("/api/analytics/health").json() assert body["latencies"][0]["command"] == "pope" assert body["upstream"][0]["host"] == "commons.example.test" assert body["upstream"][0]["failures"] == 0 def test_demand_groups_a_typo_by_what_it_nearly_hit(tmp_path: Path) -> None: body = client_for(seeded(tmp_path)).get("/api/analytics/demand").json() assert [one["target"] for one in body["near_misses"]] == ["!papiez"] def test_behaviour_reports_sittings_and_share(tmp_path: Path) -> None: body = client_for(seeded(tmp_path)).get("/api/analytics/behaviour").json() assert body["sessions"]["count"] >= 1 assert "fraction" in body["share"] def test_content_reports_pool_variety(tmp_path: Path) -> None: body = client_for(seeded(tmp_path)).get("/api/analytics/content").json() assert body["pools"][0] == { "command": "pope", "draws": 3, "distinct": 3, "repeat_rate": 0.0, } def test_a_group_filter_narrows_the_answer(tmp_path: Path) -> None: """Absent means the roll-up; present means one group, same SQL either way.""" client = client_for(seeded(tmp_path)) everywhere = client.get("/api/analytics/overview").json() here = client.get("/api/analytics/overview", params={"group": FIXTURE_GROUP}).json() nowhere = client.get("/api/analytics/overview", params={"group": "group.NOTHING"}).json() assert everywhere["window"]["events"] == here["window"]["events"] assert nowhere["window"]["events"] == 0 def test_the_triggers_come_from_the_alias_file() -> None: """A second process with no registry in it still knows every trigger.""" triggers = routes_analytics.triggers() assert "!analityka" in triggers assert "!kot" in triggers def test_release_dates_come_from_the_tags_and_degrade_to_nothing() -> None: """The changelog says what shipped; only the tags say when. Never raises: a checkout with no tags, or a deployment that is not a git tree at all, comes back empty and the timeline draws unannotated. """ for version, day in routes_analytics.releases(): assert version[0].isdigit() assert len(day) == len("2026-08-31") def test_a_database_that_cannot_be_opened_is_the_empty_shape(tmp_path: Path) -> None: """A file that exists and refuses to open is still just "nothing to show". Distinct from a corrupt one: sqlite opens a garbage file happily and fails on the first query, so this covers the other half -- a permissions problem, which raises at connect. """ deps = deps_at(tmp_path) deps.database_path.write_bytes(b"") deps.database_path.chmod(0o000) try: body = client_for(deps).get("/api/analytics/overview").json() finally: deps.database_path.chmod(0o600) assert body["window"]["events"] == 0 def test_release_parsing_ignores_anything_that_is_not_a_version_tag( monkeypatch: pytest.MonkeyPatch, ) -> None: """Tags are not all releases, and a failed shell-out is not a crash.""" monkeypatch.setattr( routes_analytics, "run", lambda *_args, **_kwargs: Result( ok=True, stdout="v0.2.5 2026-08-31\nnot-a-release 2026-08-30\nv0.1.0\n", stderr="" ), ) assert routes_analytics.releases() == (("0.2.5", "2026-08-31"),) monkeypatch.setattr( routes_analytics, "run", lambda *_a, **_k: Result(ok=False, stdout="", stderr="no git") ) assert routes_analytics.releases() == ()