mirror of
https://github.com/kuhyx/signal-bot.git
synced 2026-11-10 20:37:03 +01:00
One row per command attempt, not two aggregates written only on success. command_uses holds last-use only and daily_uses counts successes; both are rate-limiter state and both are written by store.record_use, which the dispatcher calls only when a handler returns cleanly. A gate refusal and a crash returned before it, so a command that failed every time looked exactly like one nobody ran, and nothing knew which command a counted message was. command_events now takes every attempt with its outcome, its duration and the item it served; upstream_calls takes every outbound request made while serving one. The two writes stay separate on purpose -- recording a refusal as a use is what slid the cooldown window forward every time somebody was told to wait -- and test_dispatcher_outcomes asserts last_used does not move. Backfilled from journald joined against message_events, with daily_uses filling the residual so the two sources cannot double-count. Two things were wrong on the first attempt and both were found by measuring rather than by reasoning: the window has to be symmetric, because sent_at_ms comes from the sender's phone and the journal stamp from this host (194 of 299 lines landed *before* their own message, by up to 3.2 s); and matching has to be a global closest-pair assignment, because per-row greedy cannot separate two commands seconds apart. Together those took the dropped rows from 137 to zero, and recovered 418 invocations back to 29 August. Also: !analityka in the group, twelve Polish lines, naming at most three people and only ones whose display name has been seen; an Analytics tab with Nivo charts; and continuous latency measurement, which answers the open question in DOCS-latency.md -- a text send is 235 ms and an image send about 2.2 s, so the upload really is where the time goes. EPHEMERAL_TABLES was doing two unrelated jobs and is now two tuples: what a schema bump rebuilds, and what leaving a group clears. daily_uses left the first (it is a backfill source now) and stayed in the second, and _drop_reshaped took over the repair a bump used to do for it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01T4QTp3h5Q3MvfrwGV8xqPf
92 lines
3.4 KiB
Python
92 lines
3.4 KiB
Python
# Copyright (c) 2026 Krzysztof Rudnicki. MIT licence.
|
|
"""Reading the bot's own journal back, without copying names out of it."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from signal_bot.analytics.journal import parse
|
|
|
|
PREFIX = "2026-08-31T21:28:26+02:00 archlinux run.sh[101862]:"
|
|
STAMP = "2026-08-31 21:28:26,217"
|
|
|
|
|
|
def running(command: str, who: str = "ala") -> str:
|
|
"""One real journal line, in the shape journalctl -o short-iso emits."""
|
|
return f"{PREFIX} {STAMP} INFO signal_bot.dispatcher: running command {command} for {who}"
|
|
|
|
|
|
def test_a_running_line_is_read() -> None:
|
|
(line,) = parse([running("cat")])
|
|
assert (line.command, line.display_name, line.refused) == ("cat", "ala", False)
|
|
|
|
|
|
def test_the_timestamp_comes_from_the_journal_not_the_bot() -> None:
|
|
"""The journal's stamp carries an offset; the bot's asctime does not.
|
|
|
|
A naive stamp read as UTC lands the whole import two hours out, which is
|
|
exactly what the hour columns exist to get right.
|
|
"""
|
|
(line,) = parse([running("cat")])
|
|
# 2026-08-31T21:28:26+02:00 is 19:28:26 UTC.
|
|
assert line.at_ms == 1_788_204_506_000
|
|
|
|
|
|
def test_an_album_command_keeps_its_pseudonym_key() -> None:
|
|
"""`pack:p1`, not `pack`. A \\w+ pattern silently truncates at the colon.
|
|
|
|
That truncation would collapse thirteen different album commands into
|
|
one, which is the difference between "who is asked for most" and a
|
|
single meaningless total.
|
|
"""
|
|
(line,) = parse([running("pack:p1")])
|
|
assert line.command == "pack:p1"
|
|
|
|
|
|
def test_a_cooldown_refusal_is_read_as_one() -> None:
|
|
text = (
|
|
f"{PREFIX} {STAMP} INFO signal_bot.decorators: "
|
|
"cooldown refused cat for ala: told them 8 sekund"
|
|
)
|
|
(line,) = parse([text])
|
|
assert (line.command, line.refused) == ("cat", True)
|
|
|
|
|
|
def test_a_display_name_with_spaces_survives() -> None:
|
|
"""The pattern runs to end of line, because a display name is free text."""
|
|
(line,) = parse([running("cat", "ala z basia")])
|
|
assert line.display_name == "ala z basia"
|
|
|
|
|
|
def test_lines_that_are_not_about_commands_are_skipped() -> None:
|
|
"""Startup banners, tracebacks, envelope shapes, other units."""
|
|
noise = [
|
|
"2026-08-31T21:00:00+02:00 archlinux systemd[1474]: Started Signal group bot.",
|
|
f"{PREFIX} {STAMP} DEBUG signal_bot.dispatcher: envelope shape: {{'a': 'str'}}",
|
|
"not a journal line at all",
|
|
"",
|
|
]
|
|
assert list(parse(noise)) == []
|
|
|
|
|
|
def test_a_line_whose_stamp_is_not_a_timestamp_is_skipped() -> None:
|
|
"""Never guess a time. A wrong hour is worse than a missing row."""
|
|
assert list(parse(["wednesday archlinux run.sh[1]: running command cat for ala"])) == []
|
|
|
|
|
|
def test_a_naive_stamp_is_refused() -> None:
|
|
"""Without an offset there is no way to know which hour this was."""
|
|
text = "2026-08-31T21:28:26 archlinux run.sh[1]: running command cat for ala"
|
|
assert list(parse([text])) == []
|
|
|
|
|
|
def test_a_dst_boundary_is_read_from_the_offset() -> None:
|
|
"""Warsaw is +02:00 in summer and +01:00 in winter, and both occur.
|
|
|
|
Reading the wall clock and assuming one offset puts half the year's
|
|
rows an hour out, in a column whose whole purpose is the hour.
|
|
"""
|
|
summer = "2026-10-25T02:30:00+02:00 archlinux run.sh[1]: running command cat for ala"
|
|
winter = "2026-10-25T02:30:00+01:00 archlinux run.sh[1]: running command cat for ala"
|
|
(early,) = parse([summer])
|
|
(late,) = parse([winter])
|
|
assert late.at_ms - early.at_ms == 3_600_000
|