Adds !papież / !papiez / !pope: a random pope as a photo, captioned with his Latin name, reign and pl.wikipedia link. popes.json already carried a Commons portrait filename for all 267 and nothing had ever consumed it. The Latin name comes from the Latin column of Poczet papieży, not from the la.wikipedia sitelink. The sitelink resolved for all 267 and was still wrong: la.wikipedia numbers several popes differently, so #154 came out as "Stefan IX — Stephanus X" in a single caption, which reads as an off-by-one. The table's own column says "Stephanus Nonus" and cannot disagree with the name beside it. LATIN_OVERRIDES ships empty; it is a tripwire, not a backlog. Portraits are cached on disk under CacheDirectory=signal-bot, named from the ordinal so no upstream string ever reaches a path, and bounded at 267 entries so nothing needs evicting. Verified that CacheDirectory wins against ProtectHome=read-only before relying on it. Also adds POST /_selftest and scripts/selftest.sh. Signal never delivers an account its own sends, so no command has ever been exercised end-to-end without asking a human to type it. This injects a real frame into the live dispatcher; it is loopback-only and refuses any group not in SELFTEST_GROUPS, which is empty by default. Verified against the real endpoints, not just the fake: 267/267 portraits resolve on Commons, regeneration is byte-identical on every previously shipped field, and !papież was driven 7 times through the live dispatcher in testbot, returning distinct popes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015d48t4qsQLBJnDwDeo2Nom
1.8 KiB
Testing a command without a second Signal account
Signal never delivers an account its own sends. The bot cannot type !kot at
itself, so until now every command was verified one of two weak ways: by asking
a human to type it, or by rebuilding the object graph in a scratch script. The
first proves nothing about the path between the websocket and the handler; the
second is not the wiring that runs in production.
POST /_selftest injects a real envelope frame into the live dispatcher.
Everything downstream of the socket read is the code that actually serves the
group: envelope parsing, the scoping gate, the tally, trigger resolution, the
cooldown decorator and the real REST transport. Only the socket read is skipped.
Using it
Name the groups it may post into -- in .env, because the service reads it:
SELFTEST_GROUPS=testbot,catbot,finaltestbot
Then:
scripts/selftest.sh testbot '!papież' 3
SELFTEST_INTERVAL (default 11s) spaces the calls past the per-user cooldown;
the self-test is always the same author, so a tighter loop just exercises the
cooldown instead of the command.
What it will not do
It posts for real, into a real group. Two things keep that from becoming a way to make the bot say anything anywhere:
- The endpoint lives on the same loopback bind as
/healthz, never a public one. SELFTEST_GROUPSis empty by default, and any group not named in it is refused with 403. So a fresh checkout cannot post into the production group even by accident.
Why the author matters
The reply quotes whoever asked, and quote_author has to be somebody Signal can
resolve -- a made-up uuid comes back as The user ... is not registered and the
whole send fails. resolve_group therefore attributes the message to a real
member of the target group who is not the bot.