FILE / ScuroNeko/Laniakea

bot_test.go

Исходный файл и его история в репозитории.
FILE 66eb72cb3c5bad74c99bfedec5e1b1e4c327cf92
Files
Laniakea/bot_test.go
T
ScuroNeko 66eb72cb3c Finalize rc.13 framework contracts
Formalize update routing, config freeze, and error visibility
Add regression coverage and sync backlog records
2026-03-30 01:01:13 +03:00

442 lines
13 KiB
Go

package laniakea
import (
"context"
"errors"
"path/filepath"
"reflect"
"testing"
"time"
"git.scuroneko.dev/scuroneko/laniakea/tgapi"
"git.scuroneko.dev/scuroneko/slog"
)
func TestGetUpdateTypesReturnsCopy(t *testing.T) {
bot := &Bot[NoDB]{updateTypes: []tgapi.UpdateType{tgapi.UpdateTypeMessage}}
got := bot.GetUpdateTypes()
got[0] = tgapi.UpdateTypeCallbackQuery
if want := []tgapi.UpdateType{tgapi.UpdateTypeMessage}; !reflect.DeepEqual(bot.updateTypes, want) {
t.Fatalf("GetUpdateTypes exposed internal slice: got %v want %v", bot.updateTypes, want)
}
}
func TestAddPluginsSnapshotsConfiguration(t *testing.T) {
bot := &Bot[NoDB]{logger: slog.CreateLogger()}
plugin := NewPlugin[NoDB]("demo")
cmd := plugin.NewCommand(func(ctx *MsgContext, db NoDB) error { return nil }, "start")
plugin.AddMiddleware(NewMiddleware("base", func(ctx *MsgContext, db NoDB) bool { return true }))
bot.AddPlugins(plugin)
cmd.SetDescription("mutated after registration")
plugin.NewCommand(func(ctx *MsgContext, db NoDB) error { return nil }, "late")
plugin.AddMiddleware(NewMiddleware("late", func(ctx *MsgContext, db NoDB) bool { return true }))
registered := bot.plugins[0]
if _, exists := registered.commands["late"]; exists {
t.Fatal("late command leaked into registered plugin snapshot")
}
if registered.commands["start"].description != "" {
t.Fatalf("registered command description unexpectedly mutated: %q", registered.commands["start"].description)
}
if len(registered.middlewares) != 1 {
t.Fatalf("registered middlewares unexpectedly mutated: got %d want 1", len(registered.middlewares))
}
}
func TestBotPayloadTypeConfiguration(t *testing.T) {
bot := &Bot[NoDB]{payloadType: BotPayloadBase64}
if got := bot.GetPayloadType(); got != BotPayloadBase64 {
t.Fatalf("unexpected initial payload type: %q", got)
}
bot.SetPayloadType(BotPayloadJson)
if got := bot.GetPayloadType(); got != BotPayloadJson {
t.Fatalf("unexpected updated payload type: %q", got)
}
bot.SetStrictPayloadType(true)
if !bot.strictPayloadType {
t.Fatal("expected strict payload type to be enabled")
}
}
func TestAddPluginsSkipsNilPlugin(t *testing.T) {
bot := &Bot[NoDB]{logger: slog.CreateLogger()}
plugin := NewPlugin[NoDB]("demo")
bot.AddPlugins(nil, plugin)
if len(bot.plugins) != 1 {
t.Fatalf("expected exactly one registered plugin, got %d", len(bot.plugins))
}
if bot.plugins[0].name != "demo" {
t.Fatalf("unexpected plugin name: %q", bot.plugins[0].name)
}
}
func TestInitLoggersFallsBackToStdoutLoggerOnFileError(t *testing.T) {
bot := &Bot[NoDB]{}
bot.initLoggers(&BotOpts{
Debug: true,
WriteToFile: true,
UseRequestLogger: true,
LoggerBasePath: filepath.Join(t.TempDir(), "missing", "nested"),
})
if bot.logger == nil {
t.Fatal("expected main logger fallback")
}
if bot.RequestLogger == nil {
t.Fatal("expected request logger fallback")
}
if err := bot.RequestLogger.Close(); err != nil {
t.Fatalf("failed to close request logger: %v", err)
}
if err := bot.logger.Close(); err != nil {
t.Fatalf("failed to close main logger: %v", err)
}
}
func TestNextPollRetryDelay(t *testing.T) {
tests := []struct {
name string
prev time.Duration
want time.Duration
}{
{name: "initial", prev: 0, want: time.Second},
{name: "double", prev: 2 * time.Second, want: 4 * time.Second},
{name: "cap", prev: 20 * time.Second, want: 30 * time.Second},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := nextPollRetryDelay(tt.prev); got != tt.want {
t.Fatalf("nextPollRetryDelay(%s) = %s, want %s", tt.prev, got, tt.want)
}
})
}
}
func TestAddDatabaseLoggerWriterSkipsWhenDBContextIsUnset(t *testing.T) {
bot := &Bot[NoDB]{logger: slog.CreateLogger()}
called := false
bot.AddDatabaseLoggerWriter(func(db NoDB) slog.LoggerWriter {
called = true
return nil
})
if called {
t.Fatal("expected database logger writer to be skipped when db context is unset")
}
}
func TestAddDatabaseLoggerWriterSkipsWhenDBContextIsNil(t *testing.T) {
type testDB struct{}
bot := &Bot[*testDB]{logger: slog.CreateLogger()}
var db *testDB
bot.DatabaseContext(db)
called := false
bot.AddDatabaseLoggerWriter(func(db *testDB) slog.LoggerWriter {
called = true
return nil
})
if called {
t.Fatal("expected database logger writer to be skipped when db context is nil")
}
}
func TestShouldWarnOnValueDBContext(t *testing.T) {
type testDB struct{}
type dbIface interface{ Ping() error }
tests := []struct {
name string
got bool
want bool
}{
{name: "NoDB", got: shouldWarnOnValueDBContext[NoDB](), want: false},
{name: "pointer", got: shouldWarnOnValueDBContext[*testDB](), want: false},
{name: "interface", got: shouldWarnOnValueDBContext[dbIface](), want: false},
{name: "map", got: shouldWarnOnValueDBContext[map[string]int](), want: false},
{name: "struct", got: shouldWarnOnValueDBContext[testDB](), want: true},
{name: "int", got: shouldWarnOnValueDBContext[int](), want: true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if tt.got != tt.want {
t.Fatalf("shouldWarnOnValueDBContext = %v, want %v", tt.got, tt.want)
}
})
}
}
func TestDatabaseContextMarksValueWarningOnce(t *testing.T) {
type testDB struct{}
bot := &Bot[testDB]{logger: slog.CreateLogger()}
bot.DatabaseContext(testDB{})
if !bot.warnedValueDB {
t.Fatal("expected value-typed database context to mark warning state")
}
ptrBot := &Bot[*testDB]{logger: slog.CreateLogger()}
ptrBot.DatabaseContext(&testDB{})
if ptrBot.warnedValueDB {
t.Fatal("did not expect pointer-typed database context to mark warning state")
}
}
func TestRunWithContextRejectsSecondRun(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
cancel()
bot := &Bot[NoDB]{
logger: slog.CreateLogger(),
prefixes: []string{"/"},
plugins: []Plugin[NoDB]{{name: "demo"}},
updateQueue: make(chan *tgapi.Update, 1),
maxWorkers: 1,
}
if err := bot.RunWithContext(ctx); err != nil {
t.Fatalf("first RunWithContext returned error: %v", err)
}
if err := bot.RunWithContext(ctx); !errors.Is(err, ErrBotAlreadyRun) {
t.Fatalf("expected ErrBotAlreadyRun on second run, got %v", err)
}
}
func TestBotConfigurationFreezesAfterRunStarts(t *testing.T) {
type testDB struct{ Name string }
makeBot := func() *Bot[*testDB] {
return &Bot[*testDB]{
logger: slog.CreateLogger(),
prefixes: []string{"/"},
updateTypes: []tgapi.UpdateType{tgapi.UpdateTypeMessage},
payloadType: BotPayloadBase64,
strictPayloadType: false,
errorTemplate: "%s",
l10n: &L10n{},
draftProvider: &DraftProvider{},
sessionStore: NewMemorySessionStore(),
sceneScopePriority: []SceneScope{SceneScopeUserChat, SceneScopeChat, SceneScopeUser},
}
}
tests := []struct {
name string
check func(t *testing.T, bot *Bot[*testDB])
}{
{
name: "DatabaseContext",
check: func(t *testing.T, bot *Bot[*testDB]) {
original := &testDB{Name: "before"}
bot.DatabaseContext(original)
if err := bot.beginRun(); err != nil {
t.Fatalf("beginRun returned error: %v", err)
}
t.Cleanup(bot.finishRun)
later := &testDB{Name: "after"}
bot.DatabaseContext(later)
if bot.dbContext != original {
t.Fatal("DatabaseContext mutated after configuration freeze")
}
},
},
{
name: "UpdateTypes",
check: func(t *testing.T, bot *Bot[*testDB]) {
original := append([]tgapi.UpdateType(nil), bot.updateTypes...)
if err := bot.beginRun(); err != nil {
t.Fatalf("beginRun returned error: %v", err)
}
t.Cleanup(bot.finishRun)
bot.UpdateTypes(tgapi.UpdateTypePoll)
if !reflect.DeepEqual(bot.updateTypes, original) {
t.Fatalf("UpdateTypes mutated after configuration freeze: got %v want %v", bot.updateTypes, original)
}
},
},
{
name: "AddUpdateType",
check: func(t *testing.T, bot *Bot[*testDB]) {
original := append([]tgapi.UpdateType(nil), bot.updateTypes...)
if err := bot.beginRun(); err != nil {
t.Fatalf("beginRun returned error: %v", err)
}
t.Cleanup(bot.finishRun)
bot.AddUpdateType(tgapi.UpdateTypePoll)
if !reflect.DeepEqual(bot.updateTypes, original) {
t.Fatalf("AddUpdateType mutated after configuration freeze: got %v want %v", bot.updateTypes, original)
}
},
},
{
name: "SetPayloadType",
check: func(t *testing.T, bot *Bot[*testDB]) {
if err := bot.beginRun(); err != nil {
t.Fatalf("beginRun returned error: %v", err)
}
t.Cleanup(bot.finishRun)
bot.SetPayloadType(BotPayloadJson)
if bot.payloadType != BotPayloadBase64 {
t.Fatalf("payloadType mutated after configuration freeze: got %q want %q", bot.payloadType, BotPayloadBase64)
}
},
},
{
name: "SetStrictPayloadType",
check: func(t *testing.T, bot *Bot[*testDB]) {
if err := bot.beginRun(); err != nil {
t.Fatalf("beginRun returned error: %v", err)
}
t.Cleanup(bot.finishRun)
bot.SetStrictPayloadType(true)
if bot.strictPayloadType {
t.Fatal("strictPayloadType mutated after configuration freeze")
}
},
},
{
name: "AddPrefixes",
check: func(t *testing.T, bot *Bot[*testDB]) {
original := append([]string(nil), bot.prefixes...)
if err := bot.beginRun(); err != nil {
t.Fatalf("beginRun returned error: %v", err)
}
t.Cleanup(bot.finishRun)
bot.AddPrefixes("!")
if !reflect.DeepEqual(bot.prefixes, original) {
t.Fatalf("prefixes mutated after configuration freeze: got %v want %v", bot.prefixes, original)
}
},
},
{
name: "ErrorTemplate",
check: func(t *testing.T, bot *Bot[*testDB]) {
if err := bot.beginRun(); err != nil {
t.Fatalf("beginRun returned error: %v", err)
}
t.Cleanup(bot.finishRun)
bot.ErrorTemplate("changed")
if bot.errorTemplate != "%s" {
t.Fatalf("errorTemplate mutated after configuration freeze: got %q want %q", bot.errorTemplate, "%s")
}
},
},
{
name: "SetDraftProvider",
check: func(t *testing.T, bot *Bot[*testDB]) {
original := bot.draftProvider
if err := bot.beginRun(); err != nil {
t.Fatalf("beginRun returned error: %v", err)
}
t.Cleanup(bot.finishRun)
bot.SetDraftProvider(&DraftProvider{})
if bot.draftProvider != original {
t.Fatal("draftProvider mutated after configuration freeze")
}
},
},
{
name: "SetSessionStore",
check: func(t *testing.T, bot *Bot[*testDB]) {
original := bot.sessionStore
if err := bot.beginRun(); err != nil {
t.Fatalf("beginRun returned error: %v", err)
}
t.Cleanup(bot.finishRun)
bot.SetSessionStore(NewMemorySessionStore())
if bot.sessionStore != original {
t.Fatal("sessionStore mutated after configuration freeze")
}
},
},
{
name: "SetSceneScopePriority",
check: func(t *testing.T, bot *Bot[*testDB]) {
original := append([]SceneScope(nil), bot.sceneScopePriority...)
if err := bot.beginRun(); err != nil {
t.Fatalf("beginRun returned error: %v", err)
}
t.Cleanup(bot.finishRun)
bot.SetSceneScopePriority([]SceneScope{SceneScopeUser})
if !reflect.DeepEqual(bot.sceneScopePriority, original) {
t.Fatalf("sceneScopePriority mutated after configuration freeze: got %v want %v", bot.sceneScopePriority, original)
}
},
},
{
name: "AddL10n",
check: func(t *testing.T, bot *Bot[*testDB]) {
original := bot.l10n
if err := bot.beginRun(); err != nil {
t.Fatalf("beginRun returned error: %v", err)
}
t.Cleanup(bot.finishRun)
bot.AddL10n(&L10n{})
if bot.l10n != original {
t.Fatal("l10n mutated after configuration freeze")
}
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
tt.check(t, makeBot())
})
}
}
func TestAddPluginsAndRuntimeRegistrationsNoOpAfterRunStarts(t *testing.T) {
bot := &Bot[NoDB]{
logger: slog.CreateLogger(),
prefixes: []string{"/"},
middlewares: []Middleware[NoDB]{NewMiddleware("base", func(ctx *MsgContext, db NoDB) bool { return true })},
runners: []Runner[NoDB]{NewRunner("base", func(bot *Bot[NoDB]) error { return nil })},
}
plugin := NewPlugin[NoDB]("late")
if err := bot.beginRun(); err != nil {
t.Fatalf("beginRun returned error: %v", err)
}
defer bot.finishRun()
bot.AddPlugins(plugin)
bot.AddMiddleware(NewMiddleware("late", func(ctx *MsgContext, db NoDB) bool { return true }))
bot.AddRunner(NewRunner("late", func(bot *Bot[NoDB]) error { return nil }))
if len(bot.plugins) != 0 {
t.Fatalf("expected AddPlugins to be ignored after configuration freeze, got %d plugins", len(bot.plugins))
}
if len(bot.middlewares) != 1 {
t.Fatalf("expected AddMiddleware to be ignored after configuration freeze, got %d middlewares", len(bot.middlewares))
}
if len(bot.runners) != 1 {
t.Fatalf("expected AddRunner to be ignored after configuration freeze, got %d runners", len(bot.runners))
}
}