(fix): stabilize config file versions (tests): cover routing and versions (doc): improve exported godoc
This commit is contained in:
@@ -123,9 +123,12 @@ Prefer the repository’s documented commands. If multiple choices exist, use th
|
||||
|
||||
## Commit message format
|
||||
- When the user asks for a commit message, the agent must produce it in this format:
|
||||
1. a short summary line;
|
||||
2. up to three additional lines with only the most important changes;
|
||||
3. each additional line must start on its own new line.
|
||||
1. one to four short lines;
|
||||
2. each line must use the format `(<kind>): <text>`;
|
||||
3. `<kind>` must be a short change type such as `fix`, `new`, `tests`, `doc`, `refactor`, or `ci/cd`;
|
||||
4. `<text>` must be a concise 1-5 word description of the change or function;
|
||||
5. each line must start on its own new line;
|
||||
6. when multiple lines are present, kinds must be ordered from top to bottom by this priority: `new`, `fix`, `refactor`, `ci/cd`, `tests`, `doc`.
|
||||
- The agent must output the commit message as a plain multiline block that the user can copy directly.
|
||||
- Do not collapse the lines into a paragraph, bullet list, or wrapped prose explanation.
|
||||
- Keep commit text concise and high-signal.
|
||||
|
||||
@@ -4,9 +4,12 @@
|
||||
|
||||
### Changed
|
||||
- Bot loggers now apply the configured token replacer consistently across the main bot logger, request logger, internal API and uploader loggers, webhook logger, and auto-managed plugin loggers, so bot tokens stay masked in both stdout and file-backed logs.
|
||||
- JSON `BotOpts` files now write `version`, reject newer unsupported config versions, keep older unversioned files loadable, and preserve the loaded file version in `BotOpts.FileConfigVersion`.
|
||||
- Active scenes now support scene-local callback payload handlers through `Scene.OnPayload(...)`, including observer lifecycle events for scene payload execution.
|
||||
|
||||
### Tests
|
||||
- Added regression coverage proving token masking still applies after `initLoggers(...)` switches loggers to file-backed writers and that auto-managed plugin loggers inherit token masking as well.
|
||||
- Added regression coverage for JSON config version handling and scene-local payload routing, including observer lifecycle events and callback fallthrough behavior.
|
||||
|
||||
## v1.0.0-rc.15
|
||||
|
||||
|
||||
@@ -209,6 +209,16 @@ func NewBot[T any](opts *BotOpts) (*Bot[T], error) {
|
||||
}
|
||||
bot.initLoggers(opts)
|
||||
|
||||
if opts.FileConfigVersion > 0 && opts.FileConfigVersion < ConfigVersion {
|
||||
bot.logger.Warnln(
|
||||
fmt.Sprintf(
|
||||
"Config file version %d is older than library version %d; please update your config file to access new features and avoid compatibility issues",
|
||||
opts.FileConfigVersion,
|
||||
ConfigVersion,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
// Fetch bot info to validate token and get username
|
||||
u, err := api.GetMe()
|
||||
if err != nil {
|
||||
|
||||
+8
-1
@@ -62,6 +62,12 @@ type BotOpts struct {
|
||||
|
||||
// MaxWorkers is the maximum number of update handlers that may run concurrently.
|
||||
MaxWorkers int
|
||||
|
||||
// FileConfigVersion stores the version declared by the config file used to
|
||||
// load these options.
|
||||
//
|
||||
// It is zero when the options were not loaded from a versioned file.
|
||||
FileConfigVersion int
|
||||
}
|
||||
|
||||
// LoadOptsFromEnv loads BotOpts from environment variables.
|
||||
@@ -125,7 +131,8 @@ func LoadOptsFromEnv() *BotOpts {
|
||||
DropRLOverflow: os.Getenv("DROP_RL_OVERFLOW") == "true",
|
||||
StrictPayloadType: os.Getenv("STRICT_PAYLOAD_TYPE") == "true",
|
||||
|
||||
MaxWorkers: maxWorkers,
|
||||
MaxWorkers: maxWorkers,
|
||||
FileConfigVersion: ConfigVersion,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ package laniakea
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"regexp"
|
||||
@@ -9,8 +10,16 @@ import (
|
||||
"git.scuroneko.dev/scuroneko/laniakea/tgapi"
|
||||
)
|
||||
|
||||
// ConfigVersion is the current version of the built-in JSON BotOpts file format.
|
||||
const ConfigVersion = 1
|
||||
|
||||
// ErrConfigVersionMismatch reports that a config file declares a newer version
|
||||
// than this library knows how to decode.
|
||||
var ErrConfigVersionMismatch = fmt.Errorf("config version mismatch: expected %d", ConfigVersion)
|
||||
|
||||
// BotOptsFileJson is the JSON file representation of BotOpts.
|
||||
type BotOptsFileJson struct {
|
||||
Version int `json:"version"`
|
||||
Token string `json:"token"`
|
||||
UpdateTypes []tgapi.UpdateType `json:"update_types"`
|
||||
Debug bool `json:"debug"`
|
||||
@@ -41,6 +50,9 @@ func (codec BotOptsFileJsonCodec) FromBytes(data []byte) (*BotOpts, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if fileOpts.Version > ConfigVersion {
|
||||
return nil, ErrConfigVersionMismatch
|
||||
}
|
||||
opts := &BotOpts{
|
||||
Token: fileOpts.Token,
|
||||
UpdateTypes: fileOpts.UpdateTypes,
|
||||
@@ -59,6 +71,8 @@ func (codec BotOptsFileJsonCodec) FromBytes(data []byte) (*BotOpts, error) {
|
||||
|
||||
StrictPayloadType: fileOpts.StrictPayloadType,
|
||||
MaxWorkers: fileOpts.MaxWorkers,
|
||||
|
||||
FileConfigVersion: fileOpts.Version,
|
||||
}
|
||||
return opts, nil
|
||||
}
|
||||
@@ -66,6 +80,7 @@ func (codec BotOptsFileJsonCodec) FromBytes(data []byte) (*BotOpts, error) {
|
||||
// ToBytes encodes BotOpts into JSON file bytes.
|
||||
func (codec BotOptsFileJsonCodec) ToBytes(opts *BotOpts) ([]byte, error) {
|
||||
fileOpts := &BotOptsFileJson{
|
||||
Version: ConfigVersion,
|
||||
Token: opts.Token,
|
||||
UpdateTypes: opts.UpdateTypes,
|
||||
Debug: opts.Debug,
|
||||
|
||||
+30
-7
@@ -1,6 +1,7 @@
|
||||
package laniakea
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
@@ -26,6 +27,7 @@ func TestBotOptsFileJsonCodecRoundTrip(t *testing.T) {
|
||||
DropRLOverflow: true,
|
||||
StrictPayloadType: true,
|
||||
MaxWorkers: 64,
|
||||
FileConfigVersion: ConfigVersion,
|
||||
}
|
||||
|
||||
data, err := codec.ToBytes(want)
|
||||
@@ -74,6 +76,9 @@ func TestLoadBotOptsFileExpandsEnvPlaceholders(t *testing.T) {
|
||||
if got.ErrorTemplate != "Error: %s" {
|
||||
t.Fatalf("unexpected error template: got %q", got.ErrorTemplate)
|
||||
}
|
||||
if got.FileConfigVersion != 0 {
|
||||
t.Fatalf("unexpected file config version: got %d want 0", got.FileConfigVersion)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadBotOptsFileReturnsDecodeError(t *testing.T) {
|
||||
@@ -92,13 +97,14 @@ func TestSaveBotOptsFileWritesEncodedData(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
filename := filepath.Join(dir, "config.json")
|
||||
want := &BotOpts{
|
||||
Token: "TOKEN",
|
||||
UpdateTypes: []tgapi.UpdateType{tgapi.UpdateTypeMessage},
|
||||
ErrorTemplate: "Error: %s",
|
||||
Prefixes: []string{"/"},
|
||||
APIUrl: "https://api.example.invalid",
|
||||
RateLimit: 30,
|
||||
MaxWorkers: 32,
|
||||
Token: "TOKEN",
|
||||
UpdateTypes: []tgapi.UpdateType{tgapi.UpdateTypeMessage},
|
||||
ErrorTemplate: "Error: %s",
|
||||
Prefixes: []string{"/"},
|
||||
APIUrl: "https://api.example.invalid",
|
||||
RateLimit: 30,
|
||||
MaxWorkers: 32,
|
||||
FileConfigVersion: ConfigVersion,
|
||||
}
|
||||
|
||||
if err := SaveBotOptsFile(BotOptsFileJsonCodec{}, filename, want); err != nil {
|
||||
@@ -114,3 +120,20 @@ func TestSaveBotOptsFileWritesEncodedData(t *testing.T) {
|
||||
t.Fatalf("saved file mismatch:\n got: %#v\nwant: %#v", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadBotOptsFileRejectsFutureConfigVersion(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
filename := filepath.Join(dir, "config.json")
|
||||
data := []byte(`{
|
||||
"version": 2,
|
||||
"token": "TOKEN"
|
||||
}`)
|
||||
if err := os.WriteFile(filename, data, 0o644); err != nil {
|
||||
t.Fatalf("WriteFile returned error: %v", err)
|
||||
}
|
||||
|
||||
_, err := LoadBotOptsFile(BotOptsFileJsonCodec{}, filename)
|
||||
if !errors.Is(err, ErrConfigVersionMismatch) {
|
||||
t.Fatalf("expected ErrConfigVersionMismatch, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
+4
-6
@@ -204,13 +204,11 @@ func cloneScene[T AppData](scene *Scene[T]) *Scene[T] {
|
||||
cloned := *scene
|
||||
cloned.steps = make(map[string]SceneHandler[T], len(scene.steps))
|
||||
cloned.commands = make(map[string]SceneHandler[T], len(scene.commands))
|
||||
cloned.payloads = make(map[string]SceneHandler[T], len(scene.payloads))
|
||||
|
||||
for name, handler := range scene.steps {
|
||||
cloned.steps[name] = handler
|
||||
}
|
||||
for name, handler := range scene.commands {
|
||||
cloned.commands[name] = handler
|
||||
}
|
||||
maps.Copy(cloned.steps, scene.steps)
|
||||
maps.Copy(cloned.commands, scene.commands)
|
||||
maps.Copy(cloned.payloads, scene.payloads)
|
||||
|
||||
return &cloned
|
||||
}
|
||||
|
||||
@@ -30,6 +30,8 @@ const (
|
||||
HandlerSceneStepKind HandlerEventKind = "scene_step"
|
||||
// HandlerSceneCommandKind identifies a scene-local command handler.
|
||||
HandlerSceneCommandKind HandlerEventKind = "scene_command"
|
||||
// HandlerScenePayloadKind identifies a scene-local callback payload handler.
|
||||
HandlerScenePayloadKind HandlerEventKind = "scene_payload"
|
||||
// HandlerSceneMessageKind identifies a scene message fallback handler.
|
||||
HandlerSceneMessageKind HandlerEventKind = "scene_message"
|
||||
)
|
||||
|
||||
@@ -21,6 +21,7 @@ type Scene[T any] struct {
|
||||
|
||||
steps map[string]SceneHandler[T]
|
||||
commands map[string]SceneHandler[T]
|
||||
payloads map[string]SceneHandler[T]
|
||||
message SceneHandler[T]
|
||||
}
|
||||
|
||||
@@ -32,6 +33,7 @@ func NewScene[T any](name string) *Scene[T] {
|
||||
Entry: "",
|
||||
steps: make(map[string]SceneHandler[T]),
|
||||
commands: make(map[string]SceneHandler[T]),
|
||||
payloads: make(map[string]SceneHandler[T]),
|
||||
message: nil,
|
||||
}
|
||||
}
|
||||
@@ -65,6 +67,12 @@ func (s *Scene[T]) OnCommand(cmd string, handler SceneHandler[T]) *Scene[T] {
|
||||
return s
|
||||
}
|
||||
|
||||
// OnPayload registers a callback payload handler active while the scene is running.
|
||||
func (s *Scene[T]) OnPayload(cmd string, handler SceneHandler[T]) *Scene[T] {
|
||||
s.payloads[cmd] = handler
|
||||
return s
|
||||
}
|
||||
|
||||
// OnMessage registers a fallback handler used when no scene command or step matches.
|
||||
func (s *Scene[T]) OnMessage(handler SceneHandler[T]) *Scene[T] {
|
||||
s.message = handler
|
||||
@@ -79,6 +87,14 @@ func (s *Scene[T]) executeCommand(cmd string, ctx *SceneContext, db T) (SceneRes
|
||||
result, err := handler(ctx, db)
|
||||
return result, true, err
|
||||
}
|
||||
func (s *Scene[T]) executePayload(cmd string, ctx *SceneContext, db T) (SceneResult, bool, error) {
|
||||
handler, ok := s.payloads[cmd]
|
||||
if !ok {
|
||||
return SceneResult{}, false, nil
|
||||
}
|
||||
result, err := handler(ctx, db)
|
||||
return result, true, err
|
||||
}
|
||||
func (s *Scene[T]) executeStep(step string, ctx *SceneContext, db T) (SceneResult, bool, error) {
|
||||
handler, ok := s.steps[step]
|
||||
if !ok {
|
||||
|
||||
@@ -86,6 +86,41 @@ func (bot *Bot[T]) executeScene(ctx *SceneContext, scene *Scene[T]) (bool, error
|
||||
// instead of also triggering the active scene step or fallback handler.
|
||||
return false, nil
|
||||
}
|
||||
|
||||
query := ctx.Update.CallbackQuery
|
||||
if query != nil {
|
||||
data, err := bot.decodePayload(query.Data)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
ctx.Args = data.Args
|
||||
cmd := data.Command
|
||||
if _, ok := scene.payloads[cmd]; ok {
|
||||
startTime := time.Now()
|
||||
bot.emitSceneStarted(ctx, scene, HandlerScenePayloadKind, cmd)
|
||||
res, _, err := scene.executePayload(cmd, ctx, bot.appData)
|
||||
if err != nil {
|
||||
bot.emitSceneFinished(ctx, scene, HandlerScenePayloadKind, cmd, startTime, err)
|
||||
bot.emitSceneError(ctx, scene, HandlerScenePayloadKind, cmd, err)
|
||||
return false, err
|
||||
}
|
||||
from := ctx.sess.Step
|
||||
ok, err := bot.applySceneResult(scene, ctx, res)
|
||||
bot.emitSceneFinished(ctx, scene, HandlerScenePayloadKind, cmd, startTime, err)
|
||||
if err != nil {
|
||||
bot.emitSceneError(ctx, scene, HandlerScenePayloadKind, cmd, err)
|
||||
}
|
||||
if ok {
|
||||
bot.emitSceneTransition(ctx, scene, from, res)
|
||||
}
|
||||
return ok, err
|
||||
}
|
||||
|
||||
// Unmatched payloads should not trigger the active scene step or fallback handler.
|
||||
// This allows using payloads for other bot features like pagination without interfering with active scenes.
|
||||
return false, nil
|
||||
}
|
||||
|
||||
ctx.Text = text
|
||||
ctx.Args = nil
|
||||
ctx.Prefix = ""
|
||||
|
||||
+198
@@ -430,6 +430,204 @@ func TestSceneMessageObserverEmitsLifecycleEvents(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestScenePayloadHandlerRunsBeforeStep(t *testing.T) {
|
||||
payloadCalled := 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
|
||||
}).
|
||||
OnPayload("confirm", func(ctx *SceneContext, db NoData) (SceneResult, error) {
|
||||
payloadCalled = true
|
||||
if got, want := ctx.Args, []string{"7", "ok"}; len(got) != len(want) || got[0] != want[0] || got[1] != want[1] {
|
||||
t.Fatalf("unexpected payload args: got %v want %v", got, want)
|
||||
}
|
||||
if ctx.Text != "" {
|
||||
t.Fatalf("callback flow must not populate Text, got %q", ctx.Text)
|
||||
}
|
||||
return ctx.Exit(), nil
|
||||
})
|
||||
|
||||
bot := &Bot[NoData]{
|
||||
logger: slog.CreateLogger(),
|
||||
payloadType: BotPayloadJson,
|
||||
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)
|
||||
}
|
||||
|
||||
data, err := encodeJsonPayload(CallbackData{Command: "confirm", Args: []string{"7", "ok"}})
|
||||
if err != nil {
|
||||
t.Fatalf("encodeJsonPayload returned error: %v", err)
|
||||
}
|
||||
|
||||
bot.handle(context.Background(), &tgapi.Update{
|
||||
UpdateID: 25,
|
||||
Type: tgapi.UpdateTypeCallbackQuery,
|
||||
CallbackQuery: &tgapi.CallbackQuery{
|
||||
ID: "cb-scene",
|
||||
Data: data,
|
||||
From: tgapi.User{ID: 42},
|
||||
Message: &tgapi.Message{
|
||||
MessageID: 12,
|
||||
Chat: &tgapi.Chat{ID: 100, Type: tgapi.ChatTypePrivate},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
if !payloadCalled {
|
||||
t.Fatal("expected scene payload handler to be called")
|
||||
}
|
||||
if stepCalled {
|
||||
t.Fatal("expected scene payload to short-circuit the active step")
|
||||
}
|
||||
}
|
||||
|
||||
func TestScenePayloadObserverEmitsLifecycleEvents(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
|
||||
}).
|
||||
OnPayload("confirm", func(ctx *SceneContext, db NoData) (SceneResult, error) {
|
||||
return ctx.Exit(), nil
|
||||
})
|
||||
|
||||
bot := &Bot[NoData]{
|
||||
logger: slog.CreateLogger(),
|
||||
payloadType: BotPayloadJson,
|
||||
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)
|
||||
}
|
||||
|
||||
data, err := encodeJsonPayload(CallbackData{Command: "confirm"})
|
||||
if err != nil {
|
||||
t.Fatalf("encodeJsonPayload returned error: %v", err)
|
||||
}
|
||||
|
||||
bot.handle(context.Background(), &tgapi.Update{
|
||||
UpdateID: 26,
|
||||
Type: tgapi.UpdateTypeCallbackQuery,
|
||||
CallbackQuery: &tgapi.CallbackQuery{
|
||||
ID: "cb-scene",
|
||||
Data: data,
|
||||
From: tgapi.User{ID: 42},
|
||||
Message: &tgapi.Message{
|
||||
MessageID: 13,
|
||||
Chat: &tgapi.Chat{ID: 100, Type: tgapi.ChatTypePrivate},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
if len(observer.started) != 1 {
|
||||
t.Fatalf("expected one scene started event, got %d", len(observer.started))
|
||||
}
|
||||
if got := observer.started[0]; got.HandlerKind != HandlerScenePayloadKind || got.HandlerName != "confirm" || got.Plugin != "wizard" {
|
||||
t.Fatalf("unexpected scene payload 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 != HandlerScenePayloadKind || got.HandlerName != "confirm" || got.Plugin != "wizard" || got.Err != nil {
|
||||
t.Fatalf("unexpected scene payload finished event: %#v", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSceneUnmatchedPayloadFallsThroughWithoutRunningStep(t *testing.T) {
|
||||
stepCalled := false
|
||||
|
||||
plugin := NewPlugin[NoData]("wizard")
|
||||
plugin.NewPayload(func(ctx *MsgContext, db NoData) error { return nil }, "ping")
|
||||
plugin.NewScene("signup").
|
||||
SetEntry("start").
|
||||
OnStep("start", func(ctx *SceneContext, db NoData) (SceneResult, error) {
|
||||
stepCalled = true
|
||||
return ctx.Stay(), nil
|
||||
})
|
||||
|
||||
bot := &Bot[NoData]{
|
||||
logger: slog.CreateLogger(),
|
||||
payloadType: BotPayloadJson,
|
||||
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")
|
||||
}
|
||||
|
||||
data, err := encodeJsonPayload(CallbackData{Command: "ping"})
|
||||
if err != nil {
|
||||
t.Fatalf("encodeJsonPayload returned error: %v", err)
|
||||
}
|
||||
|
||||
bot.handle(context.Background(), &tgapi.Update{
|
||||
UpdateID: 27,
|
||||
Type: tgapi.UpdateTypeCallbackQuery,
|
||||
CallbackQuery: &tgapi.CallbackQuery{
|
||||
ID: "cb-global",
|
||||
Data: data,
|
||||
From: tgapi.User{ID: 42},
|
||||
Message: &tgapi.Message{
|
||||
MessageID: 14,
|
||||
Chat: &tgapi.Chat{ID: 100, Type: tgapi.ChatTypePrivate},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
if stepCalled {
|
||||
t.Fatal("scene step must not run for an unmatched payload")
|
||||
}
|
||||
|
||||
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 payload fallback: %#v", after)
|
||||
}
|
||||
}
|
||||
|
||||
func TestScenePassDoesNotPersistSessionData(t *testing.T) {
|
||||
commandCalled := false
|
||||
|
||||
|
||||
Reference in New Issue
Block a user