(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
|
## 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
|
### Changed
|
||||||
- Updated `slog` to `v2`.
|
- Updated `pond/v2` to `v2.7.1`.
|
||||||
- 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.
|
- `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`.
|
- 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.
|
- `Bot.RunWithContext(...)` treats `context.DeadlineExceeded` like `context.Canceled` and exits polling without retry logging.
|
||||||
- Updated Go initialism names for JSON, URL, ID, and API helpers.
|
- 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
|
### 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.
|
- Added regression coverage proving polling startup preserves an enabled request logger.
|
||||||
- Updated file logger regression coverage for the current `sneklog` text prefix format.
|
- 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.
|
- 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.
|
- 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:
|
default:
|
||||||
updates, err := bot.Updates(ctx)
|
updates, err := bot.Updates(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, context.Canceled) {
|
if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
bot.logger.Errorln("failed to fetch updates:", err)
|
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 {
|
type pollingRetryObserver struct {
|
||||||
recordingObserver
|
recordingObserver
|
||||||
cancel context.CancelFunc
|
cancel context.CancelFunc
|
||||||
|
cancelAfter int
|
||||||
}
|
}
|
||||||
|
|
||||||
func (o *pollingRetryObserver) OnPollingRetry(ctx context.Context, ev PollingRetryEvent) {
|
func (o *pollingRetryObserver) OnPollingRetry(ctx context.Context, ev PollingRetryEvent) {
|
||||||
o.recordingObserver.OnPollingRetry(ctx, ev)
|
o.recordingObserver.OnPollingRetry(ctx, ev)
|
||||||
if o.cancel != nil {
|
if o.cancel != nil && (o.cancelAfter == 0 || len(o.retries) >= o.cancelAfter) {
|
||||||
o.cancel()
|
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) {
|
func TestBotConfigurationFreezesAfterRunStarts(t *testing.T) {
|
||||||
type testDB struct{ Name string }
|
type testDB struct{ Name string }
|
||||||
|
|
||||||
|
|||||||
+20
@@ -3,6 +3,7 @@ package laniakea
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"iter"
|
||||||
|
|
||||||
"git.scuroneko.dev/scuroneko/laniakea/tgapi"
|
"git.scuroneko.dev/scuroneko/laniakea/tgapi"
|
||||||
)
|
)
|
||||||
@@ -67,3 +68,22 @@ func (bot *Bot[T]) Updates(ctx context.Context) ([]tgapi.Update, error) {
|
|||||||
}
|
}
|
||||||
return updates, err
|
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