package safew import ( "context" "errors" "sync/atomic" "testing" "time" "aiaa-notification-service/internal/adapter" ) func TestRefreshMergesAndAdvancesOffset(t *testing.T) { st := NewMemStore() poll := func(token string, offset int64, timeout int) ([]adapter.SafewChat, int64, error) { if timeout != 0 { return nil, offset, nil } if offset != 0 { t.Fatalf("offset=%d", offset) } return []adapter.SafewChat{{ID: "10000778141", Type: "group", Title: "测试AI"}}, 11, nil } w := NewWatcher(st, poll) ctx := context.Background() if err := w.Refresh(ctx, "tok"); err != nil { t.Fatal(err) } list, err := w.List(ctx, "tok", "测试") if err != nil { t.Fatal(err) } if len(list) != 1 || list[0].ID != "10000778141" { t.Fatalf("%#v", list) } off, _ := st.GetSafewOffset(ctx, "tok") if off != 11 { t.Fatalf("offset=%d", off) } w.Stop() } func TestRefreshAuthError(t *testing.T) { st := NewMemStore() w := NewWatcher(st, func(string, int64, int) ([]adapter.SafewChat, int64, error) { return nil, 0, &adapter.SafewAuthError{Description: "BOT_TOKEN_INVALID"} }) err := w.Refresh(context.Background(), "bad") if err == nil { t.Fatal("expected auth error") } if _, ok := err.(*adapter.SafewAuthError); !ok { t.Fatalf("%T", err) } w.Stop() } type alwaysLockStore struct{ *MemStore } func (a *alwaysLockStore) TrySafewPollLock(context.Context, string, time.Duration) (bool, error) { return true, nil } func (a *alwaysLockStore) UnlockSafewPoll(context.Context, string) error { return nil } func TestRefreshAndEnsureDoNotOverlapPolls(t *testing.T) { st := &alwaysLockStore{MemStore: NewMemStore()} var inflight int32 var overlap int32 poll := func(token string, offset int64, timeout int) ([]adapter.SafewChat, int64, error) { n := atomic.AddInt32(&inflight, 1) if n > 1 { atomic.AddInt32(&overlap, 1) } time.Sleep(40 * time.Millisecond) atomic.AddInt32(&inflight, -1) return []adapter.SafewChat{{ID: "1", Type: "group", Title: "g"}}, offset + 1, nil } w := NewWatcher(st, poll) w.bgIdle = 20 * time.Millisecond ctx := context.Background() errCh := make(chan error, 1) go func() { errCh <- w.Refresh(ctx, "tok") }() w.Ensure("tok") if err := <-errCh; err != nil { t.Fatal(err) } time.Sleep(80 * time.Millisecond) w.Stop() if atomic.LoadInt32(&overlap) > 0 { t.Fatalf("overlapping getUpdates: %d", overlap) } } func TestEnsurePollsInBackground(t *testing.T) { st := NewMemStore() got := make(chan int, 1) w := NewWatcher(st, func(token string, offset int64, timeout int) ([]adapter.SafewChat, int64, error) { if timeout != 30 { return nil, offset, nil } select { case got <- timeout: default: } return []adapter.SafewChat{{ID: "1", Type: "group", Title: "g"}}, offset + 1, nil }) w.bgIdle = 10 * time.Millisecond w.Ensure("tok") select { case <-got: case <-time.After(2 * time.Second): t.Fatal("background poll not called") } w.Stop() } func TestNewSafewChatsSkipsKnownIDs(t *testing.T) { known := map[string]struct{}{"1": {}} got := newSafewChats(known, []adapter.SafewChat{ {ID: "1", Title: "old"}, {ID: "2", Title: "测试AI", Type: "group"}, {ID: "2", Title: "dup"}, }) if len(got) != 1 || got[0].ID != "2" || got[0].Title != "测试AI" { t.Fatalf("%#v", got) } } func TestPollConflictIsRetryable(t *testing.T) { err := errors.New("safew getUpdates status 400: BAD_REQUEST: Conflict: terminated by other getUpdates request; make sure that only one bot instance is running") if !isGetUpdatesConflict(err) { t.Fatal("expected conflict") } }