tests for the inbound telegram poller (V-637)

The cases that matter: the turn runs with the chat as its dialogue id, the
reply carries the gesture, a turn nothing persisted carries no buttons, a
stranger gets no answer at all, the first tap writes nothing, and a write
that failed says so on the button instead of going quiet.
This commit is contained in:
2026-08-06 20:53:48 +04:00
parent 38be702188
commit 0a5211b038
@@ -0,0 +1,201 @@
package telegramsink
import (
"context"
"encoding/json"
"errors"
"strings"
"testing"
)
// The turn he types in the chat is the turn the web would run, and the reply
// carries the one gesture beside it.
func TestIntakeRunsTheTurnAndOffersTheCorrection(t *testing.T) {
b := newFakeBot(t)
rec := &recorder{reply: "поняла", traceID: 91}
p := newTestPoller(t, b, rec)
p.handle(context.Background(), msg(ownerChat, " поужинал "))
if got := rec.took(); len(got) != 1 || got[0] != "поужинал" {
t.Fatalf("turns %q, want the trimmed utterance once", got)
}
// The dialogue is keyed per chat, so a clarify asked here is not answered on
// the web.
if rec.conversation != "telegram:"+ownerChat {
t.Errorf("conversation %q does not name the chat", rec.conversation)
}
sends := b.called("sendMessage")
if len(sends) != 1 {
t.Fatalf("%d sends, want 1", len(sends))
}
if sends[0].body["text"] != "поняла" {
t.Errorf("sent %v, want the reply", sends[0].body["text"])
}
if sends[0].body["protect_content"] != true {
t.Error("his own data went out forwardable")
}
kb, _ := json.Marshal(sends[0].body["reply_markup"])
if !strings.Contains(string(kb), "w:91") {
t.Errorf("keyboard %s does not point at the turn's trace", kb)
}
}
// A turn nothing persisted has no row to correct, and a button that would name
// one reports a failure he cannot act on.
func TestIntakeSkipsTheGestureWithNoTrace(t *testing.T) {
b := newFakeBot(t)
p := newTestPoller(t, b, &recorder{reply: "поняла", traceID: 0})
p.handle(context.Background(), msg(ownerChat, "привет"))
sends := b.called("sendMessage")
if len(sends) != 1 {
t.Fatalf("%d sends, want 1", len(sends))
}
if _, ok := sends[0].body["reply_markup"]; ok {
t.Error("offered a correction on a turn with no trace")
}
}
// One chat, and a stranger is not answered at all: a reply confirms the bot
// exists and whose it is.
func TestIntakeIgnoresAnyOtherChat(t *testing.T) {
b := newFakeBot(t)
rec := &recorder{reply: "поняла", traceID: 5}
p := newTestPoller(t, b, rec)
p.handle(context.Background(), msg("9999", "включи свет"))
p.handle(context.Background(), update{UpdateID: 8, CallbackQuery: &callbackQuery{
ID: "cb", Data: "t:5:note", Message: message{Chat: chat{ID: json.Number("9999")}},
}})
if got := rec.took(); len(got) != 0 {
t.Errorf("ran %q for a chat that is not the owner's", got)
}
if len(rec.corrections) != 0 {
t.Errorf("wrote %v from a chat that is not the owner's", rec.corrections)
}
if len(b.calls) != 0 {
t.Errorf("answered a stranger: %v", b.calls)
}
}
// Tapping "не то" opens the seven and writes nothing yet. The target is worth
// much more than the negative, so it must not be spent before he can name it.
func TestIntakeFirstTapOnlyOpensTheTargets(t *testing.T) {
b := newFakeBot(t)
rec := &recorder{}
p := newTestPoller(t, b, rec)
p.handle(context.Background(), update{UpdateID: 9, CallbackQuery: &callbackQuery{
ID: "cb", Data: "w:77", Message: message{MessageID: 11, Chat: chat{ID: json.Number(ownerChat)}},
}})
if len(rec.corrections) != 0 {
t.Fatalf("wrote %v before he named a target", rec.corrections)
}
if len(b.called("answerCallbackQuery")) != 1 {
t.Error("left the clock spinning on the button")
}
edits := b.called("editMessageReplyMarkup")
if len(edits) != 1 {
t.Fatalf("%d edits, want the target row", len(edits))
}
kb, _ := json.Marshal(edits[0].body["reply_markup"])
for _, want := range CorrectionTargets {
if !strings.Contains(string(kb), `"`+want+`"`) {
t.Errorf("target row %s is missing %s", kb, want)
}
}
// And the way out, because he opened the row without knowing he had to name
// anything.
if !strings.Contains(string(kb), `"t:77:"`) {
t.Errorf("target row %s prices out the untargeted negative", kb)
}
}
func TestIntakeWritesTheCorrection(t *testing.T) {
for _, tc := range []struct {
name, data, want string
}{
{"with a target", "t:77:note", "note"},
{"untargeted", "t:77:", ""},
} {
t.Run(tc.name, func(t *testing.T) {
b := newFakeBot(t)
rec := &recorder{}
p := newTestPoller(t, b, rec)
p.handle(context.Background(), update{UpdateID: 9, CallbackQuery: &callbackQuery{
ID: "cb", Data: tc.data, Message: message{MessageID: 11, Chat: chat{ID: json.Number(ownerChat)}},
}})
if len(rec.corrections) != 1 || rec.corrections[0] != (correction{77, tc.want}) {
t.Fatalf("corrections %v, want trace 77 → %q", rec.corrections, tc.want)
}
// The buttons come off once the gesture is given.
edits := b.called("editMessageReplyMarkup")
if len(edits) != 1 {
t.Fatalf("%d edits, want the keyboard cleared", len(edits))
}
kb, _ := json.Marshal(edits[0].body["reply_markup"])
if strings.Contains(string(kb), "t:77") {
t.Errorf("keyboard %s still invites a second correction", kb)
}
})
}
}
// A write that failed says so on the button. Silence would read as recorded.
func TestIntakeSaysWhenTheLabelDidNotLand(t *testing.T) {
b := newFakeBot(t)
rec := &recorder{correctErr: errors.New("no such routing trace")}
p := newTestPoller(t, b, rec)
p.handle(context.Background(), update{UpdateID: 9, CallbackQuery: &callbackQuery{
ID: "cb", Data: "t:77:fact", Message: message{MessageID: 11, Chat: chat{ID: json.Number(ownerChat)}},
}})
answers := b.called("answerCallbackQuery")
if len(answers) != 1 || answers[0].body["text"] == "" {
t.Fatalf("answers %v, want a toast saying it did not land", answers)
}
if len(b.called("editMessageReplyMarkup")) != 0 {
t.Error("cleared the buttons after a failed write, so he cannot try again")
}
}
// A question asked while the daemon was down has been answered by him or has
// stopped mattering, and a reminder set from it would land at the wrong time.
func TestIntakeDiscardsTheBacklog(t *testing.T) {
b := newFakeBot(t, []update{msg(ownerChat, "напомни в 7 позвонить маме")})
rec := &recorder{reply: "поняла", traceID: 3}
p := newTestPoller(t, b, rec)
p.discardBacklog(context.Background())
if got := rec.took(); len(got) != 0 {
t.Errorf("answered %q from the overnight queue", got)
}
// And the offset moved past it, so the next poll does not see it again.
if p.offset != 8 {
t.Errorf("offset %d, want the skipped update acknowledged", p.offset)
}
}
// The poller does not start without somewhere to send the turn.
func TestNewPollerNeedsATurn(t *testing.T) {
sink, err := New(Config{BotToken: "t", ChatID: ownerChat})
if err != nil {
t.Fatal(err)
}
if _, err := NewPoller(sink, nil, nil); err == nil {
t.Error("built a poller that reads the chat and answers nothing")
}
if _, err := NewPoller(nil, func(context.Context, string, string) (string, int64, error) {
return "", 0, nil
}, nil); err == nil {
t.Error("built a poller with no sink to answer through")
}
}