(new): rich message support
Golang lint / lint (pull_request) Successful in 1m20s
Golang lint / lint (push) Successful in 4m8s

(fix): runtime reliability
(tests): regression coverage
(doc): v1.1 release notes
This commit is contained in:
2026-08-12 16:34:44 +03:00
parent 48ddf66540
commit f03a081ed6
83 changed files with 6122 additions and 1925 deletions
+141
View File
@@ -3,7 +3,10 @@ package laniakea
import (
"context"
"errors"
"sync"
"sync/atomic"
"testing"
"time"
"git.scuroneko.dev/scuroneko/laniakea/tgapi"
"git.scuroneko.dev/scuroneko/sneklog/v2"
@@ -41,6 +44,114 @@ func TestPluginAddSceneRegistersScene(t *testing.T) {
}
}
func TestBotAddPluginsSkipsDuplicateSceneNames(t *testing.T) {
first := NewPlugin[NoData]("first")
first.Scene("shared")
second := NewPlugin[NoData]("second")
second.Scene("shared")
bot := &Bot[NoData]{logger: sneklog.NewLogger()}
bot.AddPlugins(first, second)
if _, ok := bot.plugins[0].scenes["shared"]; !ok {
t.Fatal("first registered scene was removed")
}
if _, ok := bot.plugins[1].scenes["shared"]; ok {
t.Fatal("duplicate scene was registered")
}
}
func TestSceneUpdatesForSameSessionAreSerialized(t *testing.T) {
entered := make(chan int, 2)
releaseFirst := make(chan struct{})
var calls atomic.Int64
plugin := NewPlugin[NoData]("wizard")
plugin.Scene("counter").
SetEntry("start").
OnStep("start", func(ctx *SceneContext, db NoData) (SceneResult, error) {
var value int
if err := ctx.BindData(&value); err != nil {
return SceneResult{}, err
}
call := int(calls.Add(1))
entered <- call
if call == 1 {
<-releaseFirst
}
value++
if err := ctx.SaveData(value); err != nil {
return SceneResult{}, err
}
return ctx.Stay(), nil
})
bot := &Bot[NoData]{
logger: sneklog.NewLogger(),
prefixes: []string{"/"},
sessionStore: NewMemorySessionStore(),
sceneScopePriority: []SceneScope{SceneScopeUserChat, SceneScopeChat, SceneScopeUser},
}
bot.AddPlugins(plugin)
key := "user_id:42:chat_id:100"
session := SceneSession{Scene: "counter", Step: "start"}
if err := session.SaveData(0); err != nil {
t.Fatalf("SaveData returned error: %v", err)
}
if err := bot.sessionStore.Set(key, session); err != nil {
t.Fatalf("Set returned error: %v", err)
}
update := func(id int) *tgapi.Update {
return &tgapi.Update{
UpdateID: id,
Type: tgapi.UpdateTypeMessage,
Message: &tgapi.Message{
MessageID: id,
Text: "increment",
Chat: &tgapi.Chat{ID: 100, Type: tgapi.ChatTypePrivate},
From: &tgapi.User{ID: 42},
},
}
}
var wg sync.WaitGroup
wg.Add(2)
go func() {
defer wg.Done()
bot.handle(context.Background(), update(1))
}()
if got := <-entered; got != 1 {
t.Fatalf("first handler call = %d, want 1", got)
}
go func() {
defer wg.Done()
bot.handle(context.Background(), update(2))
}()
select {
case call := <-entered:
t.Fatalf("second handler entered before first completed: call %d", call)
case <-time.After(50 * time.Millisecond):
}
close(releaseFirst)
if got := <-entered; got != 2 {
t.Fatalf("second handler call = %d, want 2", got)
}
wg.Wait()
got, err := bot.sessionStore.Get(key)
if err != nil {
t.Fatalf("Get returned error: %v", err)
}
var value int
if err := got.BindData(&value); err != nil {
t.Fatalf("BindData returned error: %v", err)
}
if value != 2 {
t.Fatalf("session value = %d, want 2", value)
}
}
func TestBotAddPluginsPreservesScenesAndHandlesThem(t *testing.T) {
called := false
@@ -836,6 +947,36 @@ func TestSceneMessageFallbackRunsWhenNoCommandOrStepMatch(t *testing.T) {
}
}
func TestMemorySessionStoreDoesNotAliasSessionData(t *testing.T) {
store := NewMemorySessionStore()
input := []byte("initial")
session := SceneSession{Scene: "signup"}
session.SetData(input)
input[0] = 'X'
if got := string(session.GetData()); got != "initial" {
t.Fatalf("SetData retained caller slice: got %q", got)
}
if err := store.Set("user:1", session); err != nil {
t.Fatalf("Set returned error: %v", err)
}
first, err := store.Get("user:1")
if err != nil {
t.Fatalf("Get returned error: %v", err)
}
data := first.GetData()
data[0] = 'X'
second, err := store.Get("user:1")
if err != nil {
t.Fatalf("second Get returned error: %v", err)
}
if got := string(second.GetData()); got != "initial" {
t.Fatalf("Get exposed stored data for mutation: got %q", got)
}
}
func TestFindSceneSessionSupportsUserScopeWithoutMessage(t *testing.T) {
bot := &Bot[NoData]{
logger: sneklog.NewLogger(),