(new): v1.2 release
Golang lint / lint (push) Successful in 11m32s

This commit is contained in:
2026-08-19 14:58:25 +03:00
parent f03a081ed6
commit 29b208eeec
79 changed files with 7301 additions and 2060 deletions
+144 -3
View File
@@ -3,6 +3,9 @@ package laniakea
import (
"context"
"errors"
"io"
"net/http"
"strings"
"testing"
"git.scuroneko.dev/scuroneko/laniakea/tgapi"
@@ -1378,14 +1381,14 @@ func TestHandlePanicEmitsErrorEvent(t *testing.T) {
name: "error value",
panicWith: errors.New("boom"),
matchErr: func(err error) bool {
return err != nil && err.Error() == "boom"
return errors.Is(err, ErrHandlerPanic) && strings.Contains(err.Error(), "boom")
},
},
{
name: "string value",
panicWith: "kaboom",
matchErr: func(err error) bool {
return err != nil && err.Error() == "kaboom"
return errors.Is(err, ErrHandlerPanic) && strings.Contains(err.Error(), "kaboom")
},
},
}
@@ -1419,7 +1422,7 @@ func TestHandlePanicEmitsErrorEvent(t *testing.T) {
panicEvent := (*ErrorEvent)(nil)
for i := range observer.errors {
ev := observer.errors[i]
if ev.Plugin == "" && ev.HandlerKind == "" && ev.UpdateID == 100 {
if ev.Plugin == "test" && ev.HandlerKind == HandlerCommandKind && ev.UpdateID == 100 {
panicEvent = &ev
break
}
@@ -1436,10 +1439,148 @@ func TestHandlePanicEmitsErrorEvent(t *testing.T) {
if !tt.matchErr(panicEvent.Err) {
t.Fatalf("unexpected panic Err: %v", panicEvent.Err)
}
if len(observer.finished) != 1 || !errors.Is(observer.finished[0].Err, ErrHandlerPanic) {
t.Fatalf("expected balanced HandlerFinishedEvent, got %#v", observer.finished)
}
if len(observer.handled) != 1 || !observer.handled[0].Handled {
t.Fatalf("expected completed handled update, got %#v", observer.handled)
}
})
}
}
func TestSceneUserErrorIsDeliveredOnce(t *testing.T) {
failing := func(*SceneContext, NoData) (SceneResult, error) {
return SceneResult{}, AsUserError(errors.New("try again"))
}
tests := []struct {
name string
step string
kind HandlerEventKind
setup func(*Scene[NoData])
update func() tgapi.Update
}{
{
name: "step", step: "start", kind: HandlerSceneStepKind,
setup: func(scene *Scene[NoData]) { scene.OnStep("start", failing) },
update: func() tgapi.Update { return sceneMessageUpdate(101, "hello") },
},
{
name: "command", step: "start", kind: HandlerSceneCommandKind,
setup: func(scene *Scene[NoData]) { scene.OnCommand("fail", failing) },
update: func() tgapi.Update { return sceneMessageUpdate(102, "/fail") },
},
{
name: "payload", step: "start", kind: HandlerScenePayloadKind,
setup: func(scene *Scene[NoData]) { scene.OnPayload("fail", failing) },
update: func() tgapi.Update {
data, err := encodeJSONPayload(CallbackData{Command: "fail"})
if err != nil {
t.Fatalf("encodeJSONPayload returned error: %v", err)
}
return tgapi.Update{
UpdateID: 103,
Type: tgapi.UpdateTypeCallbackQuery,
CallbackQuery: &tgapi.CallbackQuery{
ID: "callback", Data: data, From: tgapi.User{ID: 1},
Message: &tgapi.Message{MessageID: 1, Chat: &tgapi.Chat{ID: 42, Type: tgapi.ChatTypePrivate}},
},
}
},
},
{
name: "message fallback", step: "missing", kind: HandlerSceneMessageKind,
setup: func(scene *Scene[NoData]) { scene.OnMessage(failing) },
update: func() tgapi.Update { return sceneMessageUpdate(104, "hello") },
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
requests := 0
client := &http.Client{Transport: roundTripFunc(func(req *http.Request) (*http.Response, error) {
requests++
result := `true`
if !strings.HasSuffix(req.URL.Path, "/answerCallbackQuery") {
result = `{"message_id":1,"date":0,"chat":{"id":42,"type":"private"}}`
}
return &http.Response{
StatusCode: http.StatusOK,
Body: io.NopCloser(strings.NewReader(`{"ok":true,"result":` + result + `}`)),
Header: make(http.Header),
}, nil
})}
api := tgapi.NewAPI(tgapi.NewAPIOpts("token").SetAPIURL("https://example.test").SetHTTPClient(client))
defer func() { _ = api.Close() }()
scene := NewScene[NoData]("signup")
tt.setup(scene)
plugin := NewPlugin[NoData]("scene-plugin")
plugin.AddScene(scene)
store := NewMemorySessionStore()
if err := store.Set("user_id:1:chat_id:42", SceneSession{Scene: "signup", Step: tt.step}); err != nil {
t.Fatalf("Set returned error: %v", err)
}
observer := &recordingObserver{}
bot := &Bot[NoData]{
api: api, logger: sneklog.NewLogger(), errorTemplate: "error: %s",
plugins: []Plugin[NoData]{clonePlugin(plugin)}, observer: observer,
sessionStore: store, sceneScopePriority: []SceneScope{SceneScopeUserChat},
prefixes: []string{"/"}, payloadType: BotPayloadJSON,
}
defer func() { _ = bot.logger.Close() }()
update := tt.update()
bot.handle(context.Background(), &update)
if requests != 1 {
t.Fatalf("expected one user-facing reply, got %d requests", requests)
}
if len(observer.errors) != 1 {
t.Fatalf("expected one ErrorEvent, got %#v", observer.errors)
}
if event := observer.errors[0]; event.Plugin != "scene-plugin" || event.HandlerKind != tt.kind || !event.UserFacing {
t.Fatalf("unexpected scene ErrorEvent: %#v", event)
}
})
}
}
func TestPolicyEventsUseConfiguredEmitter(t *testing.T) {
directObserver := &recordingObserver{}
emitted := 0
ctx := &MessageContext{
observer: directObserver,
eventEmitter: func(_ context.Context, event Event) {
if _, ok := event.(PolicyCheckedEvent); !ok {
t.Fatalf("unexpected event type %T", event)
}
emitted++
},
}
ctx.emitPolicyChecked(PolicyCheckedEvent{Name: "admin", Passed: true})
if emitted != 1 {
t.Fatalf("emitter call count = %d, want 1", emitted)
}
if len(directObserver.policies) != 0 {
t.Fatalf("policy bypassed configured emitter: %#v", directObserver.policies)
}
}
func sceneMessageUpdate(updateID int, text string) tgapi.Update {
return tgapi.Update{
UpdateID: updateID,
Type: tgapi.UpdateTypeMessage,
Message: &tgapi.Message{
MessageID: 1,
Text: text,
From: &tgapi.User{ID: 1},
Chat: &tgapi.Chat{ID: 42, Type: tgapi.ChatTypePrivate},
},
}
}
func TestHandleCallbackObserverEmitsDecodeErrors(t *testing.T) {
observer := &recordingObserver{}
bot := &Bot[NoData]{