FILE / ScuroNeko/Laniakea
scene_test.go
Исходный файл и его история в репозитории.
Instrument command, payload, update, scene, runner, polling, and policy flows Add observer regression coverage and mark observability backlog item done
633 lines
19 KiB
Go
633 lines
19 KiB
Go
package laniakea
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"testing"
|
|
|
|
"git.scuroneko.dev/scuroneko/laniakea/tgapi"
|
|
"git.scuroneko.dev/scuroneko/slog"
|
|
)
|
|
|
|
type failingSessionStore struct {
|
|
getErr error
|
|
setErr error
|
|
deleteErr error
|
|
}
|
|
|
|
func (s failingSessionStore) Get(key string) (SceneSession, error) {
|
|
return SceneSession{}, s.getErr
|
|
}
|
|
|
|
func (s failingSessionStore) Set(key string, session SceneSession) error {
|
|
return s.setErr
|
|
}
|
|
|
|
func (s failingSessionStore) Delete(key string) error {
|
|
return s.deleteErr
|
|
}
|
|
|
|
func TestPluginAddSceneRegistersScene(t *testing.T) {
|
|
plugin := NewPlugin[NoData]("wizard")
|
|
scene := NewScene[NoData]("signup")
|
|
|
|
plugin.AddScene(scene)
|
|
|
|
if got, ok := plugin.scenes["signup"]; !ok || got != scene {
|
|
t.Fatalf("scene was not registered in plugin: ok=%v got=%p want=%p", ok, got, scene)
|
|
}
|
|
if scene.PluginName != "wizard" {
|
|
t.Fatalf("unexpected plugin name on scene: got %q want %q", scene.PluginName, "wizard")
|
|
}
|
|
}
|
|
|
|
func TestBotAddPluginsPreservesScenesAndHandlesThem(t *testing.T) {
|
|
called := false
|
|
|
|
plugin := NewPlugin[NoData]("wizard")
|
|
plugin.NewScene("signup").
|
|
SetEntry("start").
|
|
OnStep("start", func(ctx *SceneContext, db NoData) (SceneResult, error) {
|
|
called = true
|
|
if ctx.Text != "hello there" {
|
|
t.Fatalf("unexpected scene text: got %q want %q", ctx.Text, "hello there")
|
|
}
|
|
return ctx.Exit(), nil
|
|
})
|
|
|
|
bot := &Bot[NoData]{
|
|
logger: slog.CreateLogger(),
|
|
prefixes: []string{"/"},
|
|
sessionStore: NewMemorySessionStore(),
|
|
sceneScopePriority: []SceneScope{SceneScopeUserChat, SceneScopeChat, SceneScopeUser},
|
|
}
|
|
bot.AddPlugins(plugin)
|
|
|
|
sceneMeta, ok := bot.findScene("signup")
|
|
if !ok {
|
|
t.Fatal("expected scene metadata to be available after plugin registration")
|
|
}
|
|
if sceneMeta.Entry != "start" {
|
|
t.Fatalf("unexpected scene entry: got %q want %q", sceneMeta.Entry, "start")
|
|
}
|
|
|
|
enterCtx := &MsgContext{
|
|
Msg: &tgapi.Message{Chat: &tgapi.Chat{ID: 100, Type: tgapi.ChatTypePrivate}},
|
|
FromID: 42,
|
|
sceneRuntime: bot,
|
|
}
|
|
if err := enterCtx.EnterScene("signup"); err != nil {
|
|
t.Fatalf("EnterScene returned error: %v", err)
|
|
}
|
|
|
|
bot.handle(context.Background(), &tgapi.Update{
|
|
UpdateID: 1,
|
|
Type: tgapi.UpdateTypeMessage,
|
|
Message: &tgapi.Message{
|
|
MessageID: 7,
|
|
Text: "hello there",
|
|
Chat: &tgapi.Chat{ID: 100, Type: tgapi.ChatTypePrivate},
|
|
From: &tgapi.User{ID: 42},
|
|
},
|
|
})
|
|
|
|
if !called {
|
|
t.Fatal("expected scene step handler to be called")
|
|
}
|
|
|
|
lookupCtx := &MsgContext{
|
|
Msg: &tgapi.Message{Chat: &tgapi.Chat{ID: 100, Type: tgapi.ChatTypePrivate}},
|
|
FromID: 42,
|
|
}
|
|
if _, session, err := bot.findSceneSession(lookupCtx); err == nil && session.Scene != "" {
|
|
t.Fatalf("expected scene session to be removed after exit, got %#v", session)
|
|
}
|
|
}
|
|
|
|
func TestBuildSceneKeyRejectsMissingContextFields(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
scope SceneScope
|
|
ctx *MsgContext
|
|
}{
|
|
{
|
|
name: "nil context",
|
|
scope: SceneScopeUserChat,
|
|
ctx: nil,
|
|
},
|
|
{
|
|
name: "missing message for chat scope",
|
|
scope: SceneScopeChat,
|
|
ctx: &MsgContext{},
|
|
},
|
|
{
|
|
name: "missing from id for user scope",
|
|
scope: SceneScopeUser,
|
|
ctx: &MsgContext{},
|
|
},
|
|
{
|
|
name: "missing from id for user chat scope",
|
|
scope: SceneScopeUserChat,
|
|
ctx: &MsgContext{
|
|
Msg: &tgapi.Message{Chat: &tgapi.Chat{ID: 100, Type: tgapi.ChatTypePrivate}},
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
if key, ok := buildSceneKey(tt.scope, tt.ctx); ok || key != "" {
|
|
t.Fatalf("expected invalid scene key, got key=%q ok=%v", key, ok)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestEnterSceneRejectsMissingEntryConfiguration(t *testing.T) {
|
|
t.Run("empty entry", func(t *testing.T) {
|
|
plugin := NewPlugin[NoData]("wizard")
|
|
plugin.NewScene("signup")
|
|
|
|
bot := &Bot[NoData]{
|
|
logger: slog.CreateLogger(),
|
|
sessionStore: NewMemorySessionStore(),
|
|
sceneScopePriority: []SceneScope{SceneScopeUserChat, SceneScopeChat, SceneScopeUser},
|
|
}
|
|
bot.AddPlugins(plugin)
|
|
|
|
ctx := &MsgContext{
|
|
Msg: &tgapi.Message{Chat: &tgapi.Chat{ID: 100, Type: tgapi.ChatTypePrivate}},
|
|
FromID: 42,
|
|
sceneRuntime: bot,
|
|
}
|
|
|
|
err := ctx.EnterScene("signup")
|
|
if !errors.Is(err, ErrSceneEntryNotSet) {
|
|
t.Fatalf("expected ErrSceneEntryNotSet, got %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("missing entry step", func(t *testing.T) {
|
|
plugin := NewPlugin[NoData]("wizard")
|
|
plugin.NewScene("signup").SetEntry("start")
|
|
|
|
bot := &Bot[NoData]{
|
|
logger: slog.CreateLogger(),
|
|
sessionStore: NewMemorySessionStore(),
|
|
sceneScopePriority: []SceneScope{SceneScopeUserChat, SceneScopeChat, SceneScopeUser},
|
|
}
|
|
bot.AddPlugins(plugin)
|
|
|
|
ctx := &MsgContext{
|
|
Msg: &tgapi.Message{Chat: &tgapi.Chat{ID: 100, Type: tgapi.ChatTypePrivate}},
|
|
FromID: 42,
|
|
sceneRuntime: bot,
|
|
}
|
|
|
|
err := ctx.EnterScene("signup")
|
|
if !errors.Is(err, ErrSceneStepNotFound) {
|
|
t.Fatalf("expected ErrSceneStepNotFound, got %v", err)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestSceneContextMethodsRequireRuntime(t *testing.T) {
|
|
ctx := &MsgContext{}
|
|
|
|
if err := ctx.EnterScene("signup"); !errors.Is(err, ErrSceneRuntimeNil) {
|
|
t.Fatalf("expected ErrSceneRuntimeNil from EnterScene, got %v", err)
|
|
}
|
|
if err := ctx.EnterSceneStep("signup", "start"); !errors.Is(err, ErrSceneRuntimeNil) {
|
|
t.Fatalf("expected ErrSceneRuntimeNil from EnterSceneStep, got %v", err)
|
|
}
|
|
if err := ctx.ExitScene(); !errors.Is(err, ErrSceneRuntimeNil) {
|
|
t.Fatalf("expected ErrSceneRuntimeNil from ExitScene, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestSceneCommandHandlerRunsBeforeStep(t *testing.T) {
|
|
sceneCommandCalled := false
|
|
stepCalled := false
|
|
|
|
plugin := NewPlugin[NoData]("wizard")
|
|
plugin.NewScene("signup").
|
|
SetEntry("start").
|
|
OnStep("start", func(ctx *SceneContext, db NoData) (SceneResult, error) {
|
|
stepCalled = true
|
|
return ctx.Stay(), nil
|
|
}).
|
|
OnCommand("cancel", func(ctx *SceneContext, db NoData) (SceneResult, error) {
|
|
sceneCommandCalled = true
|
|
if ctx.Prefix != "/" {
|
|
t.Fatalf("unexpected prefix: got %q want /", ctx.Prefix)
|
|
}
|
|
if ctx.Text != "right now" {
|
|
t.Fatalf("unexpected scene command text: got %q want %q", ctx.Text, "right now")
|
|
}
|
|
if len(ctx.Args) != 2 || ctx.Args[0] != "right" || ctx.Args[1] != "now" {
|
|
t.Fatalf("unexpected scene command args: %#v", ctx.Args)
|
|
}
|
|
return ctx.Exit(), nil
|
|
})
|
|
|
|
bot := &Bot[NoData]{
|
|
logger: slog.CreateLogger(),
|
|
prefixes: []string{"/"},
|
|
sessionStore: NewMemorySessionStore(),
|
|
sceneScopePriority: []SceneScope{SceneScopeUserChat, SceneScopeChat, SceneScopeUser},
|
|
}
|
|
bot.AddPlugins(plugin)
|
|
|
|
enterCtx := &MsgContext{
|
|
Msg: &tgapi.Message{Chat: &tgapi.Chat{ID: 100, Type: tgapi.ChatTypePrivate}},
|
|
FromID: 42,
|
|
sceneRuntime: bot,
|
|
}
|
|
if err := enterCtx.EnterScene("signup"); err != nil {
|
|
t.Fatalf("EnterScene returned error: %v", err)
|
|
}
|
|
|
|
bot.handle(context.Background(), &tgapi.Update{
|
|
UpdateID: 2,
|
|
Type: tgapi.UpdateTypeMessage,
|
|
Message: &tgapi.Message{
|
|
MessageID: 8,
|
|
Text: "/cancel right now",
|
|
Chat: &tgapi.Chat{ID: 100, Type: tgapi.ChatTypePrivate},
|
|
From: &tgapi.User{ID: 42},
|
|
},
|
|
})
|
|
|
|
if !sceneCommandCalled {
|
|
t.Fatal("expected scene command handler to be called")
|
|
}
|
|
if stepCalled {
|
|
t.Fatal("expected scene command to short-circuit the scene step")
|
|
}
|
|
}
|
|
|
|
func TestSceneCommandObserverEmitsLifecycleEvents(t *testing.T) {
|
|
observer := &recordingObserver{}
|
|
plugin := NewPlugin[NoData]("wizard")
|
|
plugin.NewScene("signup").
|
|
SetEntry("start").
|
|
OnStep("start", func(ctx *SceneContext, db NoData) (SceneResult, error) {
|
|
return ctx.Stay(), nil
|
|
}).
|
|
OnCommand("cancel", func(ctx *SceneContext, db NoData) (SceneResult, error) {
|
|
return ctx.Exit(), nil
|
|
})
|
|
|
|
bot := &Bot[NoData]{
|
|
logger: slog.CreateLogger(),
|
|
prefixes: []string{"/"},
|
|
sessionStore: NewMemorySessionStore(),
|
|
sceneScopePriority: []SceneScope{SceneScopeUserChat, SceneScopeChat, SceneScopeUser},
|
|
observer: observer,
|
|
}
|
|
bot.AddPlugins(plugin)
|
|
|
|
enterCtx := &MsgContext{
|
|
Msg: &tgapi.Message{Chat: &tgapi.Chat{ID: 100, Type: tgapi.ChatTypePrivate}},
|
|
FromID: 42,
|
|
sceneRuntime: bot,
|
|
}
|
|
if err := enterCtx.EnterScene("signup"); err != nil {
|
|
t.Fatalf("EnterScene returned error: %v", err)
|
|
}
|
|
|
|
bot.handle(context.Background(), &tgapi.Update{
|
|
UpdateID: 22,
|
|
Type: tgapi.UpdateTypeMessage,
|
|
Message: &tgapi.Message{
|
|
MessageID: 9,
|
|
Text: "/cancel",
|
|
Chat: &tgapi.Chat{ID: 100, Type: tgapi.ChatTypePrivate},
|
|
From: &tgapi.User{ID: 42},
|
|
},
|
|
})
|
|
|
|
if len(observer.started) != 1 {
|
|
t.Fatalf("expected one scene started event, got %d", len(observer.started))
|
|
}
|
|
if got := observer.started[0]; got.HandlerKind != HandlerSceneCommandKind || got.HandlerName != "cancel" || got.Plugin != "wizard" {
|
|
t.Fatalf("unexpected scene started event: %#v", got)
|
|
}
|
|
if len(observer.finished) != 1 {
|
|
t.Fatalf("expected one scene finished event, got %d", len(observer.finished))
|
|
}
|
|
if got := observer.finished[0]; got.HandlerKind != HandlerSceneCommandKind || got.HandlerName != "cancel" || got.Plugin != "wizard" || got.Err != nil {
|
|
t.Fatalf("unexpected scene finished event: %#v", got)
|
|
}
|
|
}
|
|
|
|
func TestSceneStepObserverEmitsLifecycleEvents(t *testing.T) {
|
|
observer := &recordingObserver{}
|
|
plugin := NewPlugin[NoData]("wizard")
|
|
plugin.NewScene("signup").
|
|
SetEntry("start").
|
|
OnStep("start", func(ctx *SceneContext, db NoData) (SceneResult, error) {
|
|
return ctx.Stay(), nil
|
|
})
|
|
|
|
bot := &Bot[NoData]{
|
|
logger: slog.CreateLogger(),
|
|
prefixes: []string{"/"},
|
|
sessionStore: NewMemorySessionStore(),
|
|
sceneScopePriority: []SceneScope{SceneScopeUserChat, SceneScopeChat, SceneScopeUser},
|
|
observer: observer,
|
|
}
|
|
bot.AddPlugins(plugin)
|
|
|
|
enterCtx := &MsgContext{
|
|
Msg: &tgapi.Message{Chat: &tgapi.Chat{ID: 100, Type: tgapi.ChatTypePrivate}},
|
|
FromID: 42,
|
|
sceneRuntime: bot,
|
|
}
|
|
if err := enterCtx.EnterScene("signup"); err != nil {
|
|
t.Fatalf("EnterScene returned error: %v", err)
|
|
}
|
|
|
|
bot.handle(context.Background(), &tgapi.Update{
|
|
UpdateID: 23,
|
|
Type: tgapi.UpdateTypeMessage,
|
|
Message: &tgapi.Message{
|
|
MessageID: 10,
|
|
Text: "hello there",
|
|
Chat: &tgapi.Chat{ID: 100, Type: tgapi.ChatTypePrivate},
|
|
From: &tgapi.User{ID: 42},
|
|
},
|
|
})
|
|
|
|
if len(observer.started) != 1 {
|
|
t.Fatalf("expected one scene started event, got %d", len(observer.started))
|
|
}
|
|
if got := observer.started[0]; got.HandlerKind != HandlerSceneStepKind || got.HandlerName != "start" || got.Plugin != "wizard" {
|
|
t.Fatalf("unexpected scene step started event: %#v", got)
|
|
}
|
|
if len(observer.finished) != 1 {
|
|
t.Fatalf("expected one scene finished event, got %d", len(observer.finished))
|
|
}
|
|
if got := observer.finished[0]; got.HandlerKind != HandlerSceneStepKind || got.HandlerName != "start" || got.Plugin != "wizard" || got.Err != nil {
|
|
t.Fatalf("unexpected scene step finished event: %#v", got)
|
|
}
|
|
}
|
|
|
|
func TestSceneMessageObserverEmitsLifecycleEvents(t *testing.T) {
|
|
observer := &recordingObserver{}
|
|
plugin := NewPlugin[NoData]("wizard")
|
|
scene := plugin.NewScene("signup").
|
|
SetEntry("start").
|
|
OnStep("start", func(ctx *SceneContext, db NoData) (SceneResult, error) {
|
|
return ctx.Stay(), nil
|
|
}).
|
|
OnMessage(func(ctx *SceneContext, db NoData) (SceneResult, error) {
|
|
return ctx.Exit(), nil
|
|
})
|
|
|
|
bot := &Bot[NoData]{
|
|
logger: slog.CreateLogger(),
|
|
prefixes: []string{"/"},
|
|
sessionStore: NewMemorySessionStore(),
|
|
sceneScopePriority: []SceneScope{SceneScopeUserChat, SceneScopeChat, SceneScopeUser},
|
|
observer: observer,
|
|
}
|
|
bot.AddPlugins(plugin)
|
|
|
|
key, ok := buildSceneKey(SceneScopeUserChat, &MsgContext{
|
|
Msg: &tgapi.Message{Chat: &tgapi.Chat{ID: 100, Type: tgapi.ChatTypePrivate}},
|
|
FromID: 42,
|
|
})
|
|
if !ok {
|
|
t.Fatal("expected scene key to be built")
|
|
}
|
|
if err := bot.sessionStore.Set(key, SceneSession{Scene: scene.Name}); err != nil {
|
|
t.Fatalf("failed to seed scene session: %v", err)
|
|
}
|
|
|
|
bot.handle(context.Background(), &tgapi.Update{
|
|
UpdateID: 24,
|
|
Type: tgapi.UpdateTypeMessage,
|
|
Message: &tgapi.Message{
|
|
MessageID: 11,
|
|
Text: "hello there",
|
|
Chat: &tgapi.Chat{ID: 100, Type: tgapi.ChatTypePrivate},
|
|
From: &tgapi.User{ID: 42},
|
|
},
|
|
})
|
|
|
|
if len(observer.started) != 1 {
|
|
t.Fatalf("expected one scene started event, got %d", len(observer.started))
|
|
}
|
|
if got := observer.started[0]; got.HandlerKind != HandlerSceneMessageKind || got.HandlerName != "message_fallback" || got.Plugin != "wizard" {
|
|
t.Fatalf("unexpected scene message started event: %#v", got)
|
|
}
|
|
if len(observer.finished) != 1 {
|
|
t.Fatalf("expected one scene finished event, got %d", len(observer.finished))
|
|
}
|
|
if got := observer.finished[0]; got.HandlerKind != HandlerSceneMessageKind || got.HandlerName != "message_fallback" || got.Plugin != "wizard" || got.Err != nil {
|
|
t.Fatalf("unexpected scene message finished event: %#v", got)
|
|
}
|
|
}
|
|
|
|
func TestScenePassDoesNotPersistSessionData(t *testing.T) {
|
|
commandCalled := false
|
|
|
|
plugin := NewPlugin[NoData]("wizard")
|
|
plugin.NewCommand(func(ctx *MsgContext, db NoData) error {
|
|
commandCalled = true
|
|
return nil
|
|
}, "ping")
|
|
plugin.NewScene("signup").
|
|
SetEntry("start").
|
|
OnStep("start", func(ctx *SceneContext, db NoData) (SceneResult, error) {
|
|
if err := ctx.SaveData(struct {
|
|
Value string `json:"value"`
|
|
}{Value: "changed"}); err != nil {
|
|
t.Fatalf("SaveData returned error: %v", err)
|
|
}
|
|
return ctx.Pass(), nil
|
|
})
|
|
|
|
bot := &Bot[NoData]{
|
|
logger: slog.CreateLogger(),
|
|
prefixes: []string{"/"},
|
|
sessionStore: NewMemorySessionStore(),
|
|
sceneScopePriority: []SceneScope{SceneScopeUserChat, SceneScopeChat, SceneScopeUser},
|
|
}
|
|
bot.AddPlugins(plugin)
|
|
|
|
enterCtx := &MsgContext{
|
|
Msg: &tgapi.Message{Chat: &tgapi.Chat{ID: 100, Type: tgapi.ChatTypePrivate}},
|
|
FromID: 42,
|
|
sceneRuntime: bot,
|
|
}
|
|
if err := enterCtx.EnterScene("signup"); err != nil {
|
|
t.Fatalf("EnterScene returned error: %v", err)
|
|
}
|
|
|
|
key, ok := buildSceneKey(SceneScopeUserChat, &MsgContext{
|
|
Msg: &tgapi.Message{Chat: &tgapi.Chat{ID: 100, Type: tgapi.ChatTypePrivate}},
|
|
FromID: 42,
|
|
})
|
|
if !ok {
|
|
t.Fatal("expected scene key to be built")
|
|
}
|
|
|
|
before, err := bot.sessionStore.Get(key)
|
|
if err != nil {
|
|
t.Fatalf("Get before handle returned error: %v", err)
|
|
}
|
|
if before.HasData() {
|
|
t.Fatalf("expected empty session data before handle, got %#v", before)
|
|
}
|
|
|
|
bot.handle(context.Background(), &tgapi.Update{
|
|
UpdateID: 3,
|
|
Type: tgapi.UpdateTypeMessage,
|
|
Message: &tgapi.Message{
|
|
MessageID: 9,
|
|
Text: "/ping",
|
|
Chat: &tgapi.Chat{ID: 100, Type: tgapi.ChatTypePrivate},
|
|
From: &tgapi.User{ID: 42},
|
|
},
|
|
})
|
|
|
|
if !commandCalled {
|
|
t.Fatal("expected normal command routing to continue after SceneActionPass")
|
|
}
|
|
|
|
after, err := bot.sessionStore.Get(key)
|
|
if err != nil {
|
|
t.Fatalf("Get after handle returned error: %v", err)
|
|
}
|
|
if after.Scene != "signup" || after.Step != "start" {
|
|
t.Fatalf("unexpected session after pass: %#v", after)
|
|
}
|
|
if after.HasData() {
|
|
t.Fatalf("expected SceneActionPass to leave session data unchanged, got %#v", after)
|
|
}
|
|
}
|
|
|
|
func TestSceneMessageFallbackRunsWhenNoCommandOrStepMatch(t *testing.T) {
|
|
fallbackCalled := false
|
|
|
|
plugin := NewPlugin[NoData]("wizard")
|
|
plugin.NewScene("signup").
|
|
SetEntry("start").
|
|
OnStep("start", func(ctx *SceneContext, db NoData) (SceneResult, error) {
|
|
return ctx.Stay(), nil
|
|
}).
|
|
OnMessage(func(ctx *SceneContext, db NoData) (SceneResult, error) {
|
|
fallbackCalled = true
|
|
if ctx.Text != "hello fallback" {
|
|
t.Fatalf("unexpected fallback text: got %q want %q", ctx.Text, "hello fallback")
|
|
}
|
|
return ctx.Exit(), nil
|
|
})
|
|
|
|
bot := &Bot[NoData]{
|
|
logger: slog.CreateLogger(),
|
|
prefixes: []string{"/"},
|
|
sessionStore: NewMemorySessionStore(),
|
|
sceneScopePriority: []SceneScope{SceneScopeUserChat, SceneScopeChat, SceneScopeUser},
|
|
}
|
|
bot.AddPlugins(plugin)
|
|
|
|
enterCtx := &MsgContext{
|
|
Msg: &tgapi.Message{Chat: &tgapi.Chat{ID: 100, Type: tgapi.ChatTypePrivate}},
|
|
FromID: 42,
|
|
sceneRuntime: bot,
|
|
}
|
|
if err := enterCtx.EnterScene("signup"); err != nil {
|
|
t.Fatalf("EnterScene returned error: %v", err)
|
|
}
|
|
|
|
key, ok := buildSceneKey(SceneScopeUserChat, &MsgContext{
|
|
Msg: &tgapi.Message{Chat: &tgapi.Chat{ID: 100, Type: tgapi.ChatTypePrivate}},
|
|
FromID: 42,
|
|
})
|
|
if !ok {
|
|
t.Fatal("expected scene key to be built")
|
|
}
|
|
if err := bot.sessionStore.Set(key, SceneSession{Scene: "signup", Step: "unknown"}); err != nil {
|
|
t.Fatalf("Set returned error: %v", err)
|
|
}
|
|
|
|
bot.handle(context.Background(), &tgapi.Update{
|
|
UpdateID: 4,
|
|
Type: tgapi.UpdateTypeMessage,
|
|
Message: &tgapi.Message{
|
|
MessageID: 10,
|
|
Text: "hello fallback",
|
|
Chat: &tgapi.Chat{ID: 100, Type: tgapi.ChatTypePrivate},
|
|
From: &tgapi.User{ID: 42},
|
|
},
|
|
})
|
|
|
|
if !fallbackCalled {
|
|
t.Fatal("expected scene fallback handler to be called")
|
|
}
|
|
}
|
|
|
|
func TestFindSceneSessionSupportsUserScopeWithoutMessage(t *testing.T) {
|
|
bot := &Bot[NoData]{
|
|
logger: slog.CreateLogger(),
|
|
sessionStore: NewMemorySessionStore(),
|
|
sceneScopePriority: []SceneScope{SceneScopeUser, SceneScopeChat, SceneScopeUserChat},
|
|
}
|
|
|
|
if err := bot.sessionStore.Set("user_id:42", SceneSession{Scene: "signup", Step: "start"}); err != nil {
|
|
t.Fatalf("Set returned error: %v", err)
|
|
}
|
|
|
|
key, session, err := bot.findSceneSession(&MsgContext{FromID: 42})
|
|
if err != nil {
|
|
t.Fatalf("findSceneSession returned error: %v", err)
|
|
}
|
|
if key != "user_id:42" {
|
|
t.Fatalf("unexpected session key: got %q want %q", key, "user_id:42")
|
|
}
|
|
if session.Scene != "signup" || session.Step != "start" {
|
|
t.Fatalf("unexpected session: %#v", session)
|
|
}
|
|
}
|
|
|
|
func TestSceneStoreErrorsPropagate(t *testing.T) {
|
|
getErr := errors.New("get failed")
|
|
setErr := errors.New("set failed")
|
|
|
|
t.Run("find scene session get error", func(t *testing.T) {
|
|
bot := &Bot[NoData]{
|
|
logger: slog.CreateLogger(),
|
|
sessionStore: failingSessionStore{getErr: getErr},
|
|
sceneScopePriority: []SceneScope{SceneScopeUser},
|
|
}
|
|
|
|
_, _, err := bot.findSceneSession(&MsgContext{FromID: 42})
|
|
if !errors.Is(err, getErr) {
|
|
t.Fatalf("expected getErr, got %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("apply scene result set error", func(t *testing.T) {
|
|
scene := NewScene[NoData]("signup").OnStep("start", func(ctx *SceneContext, db NoData) (SceneResult, error) {
|
|
return ctx.Stay(), nil
|
|
})
|
|
bot := &Bot[NoData]{
|
|
logger: slog.CreateLogger(),
|
|
sessionStore: failingSessionStore{setErr: setErr},
|
|
sceneScopePriority: []SceneScope{SceneScopeUserChat, SceneScopeChat, SceneScopeUser},
|
|
}
|
|
|
|
_, err := bot.applySceneResult(scene, &SceneContext{
|
|
MsgContext: &MsgContext{},
|
|
sess: SceneSession{Scene: "signup", Step: "start"},
|
|
key: "user_id:42:chat_id:100",
|
|
}, SceneResult{Action: SceneActionStay})
|
|
if !errors.Is(err, setErr) {
|
|
t.Fatalf("expected setErr, got %v", err)
|
|
}
|
|
})
|
|
}
|