(new): sneklog migration (refactor): idiomatic names (fix): polling lifecycle (tests): polling regressions (doc): rc16 changelog
This commit is contained in:
+23
-5
@@ -2,17 +2,35 @@
|
||||
|
||||
## v1.0.0-rc.16
|
||||
|
||||
### Breaking Changes
|
||||
- Replaced `git.scuroneko.dev/scuroneko/slog` with `git.scuroneko.dev/scuroneko/sneklog/v2` across public logger APIs, including `AppDataLogger`, logger getters, and custom logger setters.
|
||||
- Renamed exported `Json`, `Url`, and `Id` identifiers to idiomatic `JSON`, `URL`, and `ID` spellings, including `BotOpts.APIURL`, `BotOpts.SetAPIURL(...)`, `tgapi.APIOpts.SetAPIURL(...)`, `BotOptsFileJSONCodec`, `BotPayloadJSON`, and related README examples.
|
||||
- Made the request logger field internal; use `Bot.SetRequestLogger(...)` and `Bot.GetRequestLogger()` instead of accessing `Bot.RequestLogger` directly.
|
||||
|
||||
### Added
|
||||
- Added `Bot.UpdatesIter(...)` as an iterator wrapper around a single `Bot.Updates(...)` call, including error delivery through the iterator.
|
||||
- Added scene-local callback payload handlers through `Scene.OnPayload(...)`, including observer lifecycle events for scene payload execution.
|
||||
- Added configurable logger output through `BotOpts.LogFormat`, `BotOpts.SetLogFormat(...)`, `BotOpts.SetLogFormatter(...)`, `tgapi.APIOpts.SetLogFormat(...)`, and `tgapi.APIOpts.SetLogFormatter(...)`.
|
||||
- Added JSON BotOpts file format versioning through `ConfigVersion`, `ErrConfigVersionMismatch`, and `BotOpts.FileConfigVersion`.
|
||||
- Added `Bot.SetLogger(...)`, `Bot.SetRequestLogger(...)`, `Bot.SetWebHookLogger(...)`, `Bot.GetRequestLogger()`, and `Bot.GetWebHookLogger()` helpers for explicit logger customization.
|
||||
|
||||
### Changed
|
||||
- Updated `slog` to `v2`.
|
||||
- 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.
|
||||
- Updated `pond/v2` to `v2.7.1`.
|
||||
- `Bot.RunWithContext(...)` now closes an explicitly set request logger when `UseRequestLogger` is false and closes webhook loggers before long-polling startup.
|
||||
- Bot loggers now apply the configured token replacer consistently across the main bot logger, request logger, internal API and uploader loggers, webhook logger, app-data logger writers, and auto-managed plugin loggers.
|
||||
- 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.
|
||||
- Updated Go initialism names for JSON, URL, ID, and API helpers.
|
||||
- `Bot.RunWithContext(...)` treats `context.DeadlineExceeded` like `context.Canceled` and exits polling without retry logging.
|
||||
- README and README_RU now use the current `JSON`, `URL`, and `ID` public API names.
|
||||
|
||||
### Fixed
|
||||
- Fixed the go-lint workflow file to end with a newline.
|
||||
|
||||
### Tests
|
||||
- Added regression coverage for `Bot.UpdatesIter(...)` error delivery and early iterator stop behavior.
|
||||
- Added regression coverage proving `Bot.RunWithContext(...)` preserves polling retry attempts and backoff delays across repeated getUpdates failures.
|
||||
- Added regression coverage proving polling startup preserves an enabled request logger.
|
||||
- Updated file logger regression coverage for the current `sneklog` text prefix format.
|
||||
- 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 proving token masking still applies after `initLoggers(...)` switches loggers to file-backed writers and that auto-managed plugin loggers inherit token masking.
|
||||
- Added regression coverage for JSON config version handling and scene-local payload routing, including observer lifecycle events and callback fallthrough behavior.
|
||||
- Updated logger helper tests for the explicit log format and formatter parameters.
|
||||
|
||||
|
||||
@@ -442,7 +442,7 @@ func (bot *Bot[T]) RunWithContext(ctx context.Context) error {
|
||||
default:
|
||||
updates, err := bot.Updates(ctx)
|
||||
if err != nil {
|
||||
if errors.Is(err, context.Canceled) {
|
||||
if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {
|
||||
return
|
||||
}
|
||||
bot.logger.Errorln("failed to fetch updates:", err)
|
||||
|
||||
+52
-2
@@ -24,12 +24,13 @@ func (f pollingRoundTripFunc) RoundTrip(req *http.Request) (*http.Response, erro
|
||||
|
||||
type pollingRetryObserver struct {
|
||||
recordingObserver
|
||||
cancel context.CancelFunc
|
||||
cancel context.CancelFunc
|
||||
cancelAfter int
|
||||
}
|
||||
|
||||
func (o *pollingRetryObserver) OnPollingRetry(ctx context.Context, ev PollingRetryEvent) {
|
||||
o.recordingObserver.OnPollingRetry(ctx, ev)
|
||||
if o.cancel != nil {
|
||||
if o.cancel != nil && (o.cancelAfter == 0 || len(o.retries) >= o.cancelAfter) {
|
||||
o.cancel()
|
||||
}
|
||||
}
|
||||
@@ -510,6 +511,55 @@ func TestRunWithContextEmitsPollingRetryAndErrorEvents(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunWithContextPreservesPollingRetryBackoff(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
observer := &pollingRetryObserver{cancel: cancel, cancelAfter: 2}
|
||||
|
||||
client := &http.Client{
|
||||
Transport: pollingRoundTripFunc(func(r *http.Request) (*http.Response, error) {
|
||||
return &http.Response{
|
||||
StatusCode: http.StatusOK,
|
||||
Header: http.Header{"Content-Type": []string{"application/json"}},
|
||||
Body: io.NopCloser(strings.NewReader(`{"ok":false,"error_code":500,"description":"boom"}`)),
|
||||
}, nil
|
||||
}),
|
||||
}
|
||||
|
||||
api := tgapi.NewAPI(
|
||||
tgapi.NewAPIOpts("token").
|
||||
SetAPIURL("http://example.invalid").
|
||||
SetHTTPClient(client),
|
||||
)
|
||||
defer func() {
|
||||
_ = api.Close()
|
||||
}()
|
||||
|
||||
bot := &Bot[NoData]{
|
||||
logger: sneklog.NewLogger(),
|
||||
api: api,
|
||||
prefixes: []string{"/"},
|
||||
plugins: []Plugin[NoData]{{name: "demo"}},
|
||||
updateQueue: make(chan *tgapi.Update, 1),
|
||||
maxWorkers: 1,
|
||||
observer: observer,
|
||||
}
|
||||
|
||||
if err := bot.RunWithContext(ctx); err != nil {
|
||||
t.Fatalf("RunWithContext returned error: %v", err)
|
||||
}
|
||||
|
||||
if len(observer.retries) != 2 {
|
||||
t.Fatalf("expected two polling retry events, got %d", len(observer.retries))
|
||||
}
|
||||
if got := observer.retries[0]; got.Attempt != 1 || got.Delay != time.Second {
|
||||
t.Fatalf("unexpected first retry event: %#v", got)
|
||||
}
|
||||
if got := observer.retries[1]; got.Attempt != 2 || got.Delay != 2*time.Second {
|
||||
t.Fatalf("unexpected second retry event: %#v", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBotConfigurationFreezesAfterRunStarts(t *testing.T) {
|
||||
type testDB struct{ Name string }
|
||||
|
||||
|
||||
+20
@@ -3,6 +3,7 @@ package laniakea
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"iter"
|
||||
|
||||
"git.scuroneko.dev/scuroneko/laniakea/tgapi"
|
||||
)
|
||||
@@ -67,3 +68,22 @@ func (bot *Bot[T]) Updates(ctx context.Context) ([]tgapi.Update, error) {
|
||||
}
|
||||
return updates, err
|
||||
}
|
||||
|
||||
// UpdatesIter fetches updates once and yields each update in order.
|
||||
//
|
||||
// If fetching updates fails, the iterator yields the error once with a zero
|
||||
// update and then stops.
|
||||
func (bot *Bot[T]) UpdatesIter(ctx context.Context) iter.Seq2[tgapi.Update, error] {
|
||||
return func(yield func(tgapi.Update, error) bool) {
|
||||
updates, err := bot.Updates(ctx)
|
||||
if err != nil {
|
||||
yield(tgapi.Update{}, err)
|
||||
return
|
||||
}
|
||||
for _, u := range updates {
|
||||
if !yield(u, nil) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
package laniakea
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"git.scuroneko.dev/scuroneko/laniakea/tgapi"
|
||||
)
|
||||
|
||||
func TestUpdatesIterYieldsFetchError(t *testing.T) {
|
||||
bot := newUpdatesIterTestBot(t, `{"ok":false,"error_code":500,"description":"boom"}`)
|
||||
|
||||
var gotErr error
|
||||
var gotUpdates int
|
||||
bot.UpdatesIter(context.Background())(func(update tgapi.Update, err error) bool {
|
||||
gotUpdates++
|
||||
if update.UpdateID != 0 {
|
||||
t.Fatalf("expected zero update on error, got %d", update.UpdateID)
|
||||
}
|
||||
gotErr = err
|
||||
return true
|
||||
})
|
||||
|
||||
if gotUpdates != 1 {
|
||||
t.Fatalf("expected one yielded error, got %d yields", gotUpdates)
|
||||
}
|
||||
if gotErr == nil {
|
||||
t.Fatal("expected fetch error")
|
||||
}
|
||||
if !strings.Contains(gotErr.Error(), "boom") {
|
||||
t.Fatalf("expected Telegram error description, got %v", gotErr)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdatesIterStopsWhenYieldReturnsFalse(t *testing.T) {
|
||||
bot := newUpdatesIterTestBot(t, `{"ok":true,"result":[{"update_id":11},{"update_id":12}]}`)
|
||||
|
||||
var gotIDs []int
|
||||
bot.UpdatesIter(context.Background())(func(update tgapi.Update, err error) bool {
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
gotIDs = append(gotIDs, update.UpdateID)
|
||||
return false
|
||||
})
|
||||
|
||||
if len(gotIDs) != 1 || gotIDs[0] != 11 {
|
||||
t.Fatalf("expected only first update, got %v", gotIDs)
|
||||
}
|
||||
}
|
||||
|
||||
func newUpdatesIterTestBot(t *testing.T, response string) *Bot[NoData] {
|
||||
t.Helper()
|
||||
|
||||
api := tgapi.NewAPI(
|
||||
tgapi.NewAPIOpts("token").
|
||||
SetAPIURL("https://example.test").
|
||||
SetHTTPClient(&http.Client{
|
||||
Transport: pollingRoundTripFunc(func(req *http.Request) (*http.Response, error) {
|
||||
return &http.Response{
|
||||
StatusCode: http.StatusOK,
|
||||
Header: http.Header{"Content-Type": []string{"application/json"}},
|
||||
Body: io.NopCloser(strings.NewReader(response)),
|
||||
}, nil
|
||||
}),
|
||||
}),
|
||||
)
|
||||
t.Cleanup(func() {
|
||||
if err := api.Close(); err != nil {
|
||||
t.Fatalf("Close returned error: %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
return &Bot[NoData]{api: api}
|
||||
}
|
||||
Reference in New Issue
Block a user