Download internal/scheduler/activity_test.go from a3216/gcli2api: direct link, hf CLI and curl.
- Browser
- Download file 21.5 kB
-
https://huggingface.co/spaces/a3216/gcli2api/resolve/main/internal/scheduler/activity_test.go
- Command line
-
hf download hf://spaces/a3216/gcli2api/internal/scheduler/activity_test.go
-
curl -L -o activity_test.go https://huggingface.co/spaces/a3216/gcli2api/resolve/main/internal/scheduler/activity_test.go
21.5 kB
| package scheduler | |
| import ( | |
| "context" | |
| "fmt" | |
| "net/http" | |
| "net/http/httptest" | |
| "sync/atomic" | |
| "testing" | |
| "time" | |
| "github.com/linguo2625469/workbuddy2api-panel/internal/auth" | |
| "github.com/linguo2625469/workbuddy2api-panel/internal/pool" | |
| "github.com/linguo2625469/workbuddy2api-panel/internal/upstream" | |
| ) | |
| // fastActivity 关闭活跃上报账号间限速,避免测试白等 800ms。 | |
| func fastActivity(t *testing.T) { | |
| t.Helper() | |
| old := activityAccountDelay | |
| activityAccountDelay = 0 | |
| t.Cleanup(func() { activityAccountDelay = old }) | |
| } | |
| // reportStub 记录 /v2/report 调用次数与 userId。 | |
| type reportStub struct { | |
| calls atomic.Int32 | |
| uids atomic.Int32 | |
| bodies atomic.Int32 | |
| } | |
| func (s *reportStub) handler() http.Handler { | |
| return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | |
| if r.URL.Path == "/v2/report" { | |
| s.calls.Add(1) | |
| uid := r.Header.Get("X-User-Id") | |
| if uid != "" { | |
| s.uids.Add(1) | |
| } | |
| s.bodies.Add(1) // 标记收到 body(断言数组含 userId 在 upstream 包单测覆盖) | |
| w.Write([]byte(`{"code":0,"msg":"OK"}`)) | |
| return | |
| } | |
| http.Error(w, "not found", 404) | |
| }) | |
| } | |
| // TestRunActivityNowReportsEachAccount 遍历池内每个可用账号上报一次。 | |
| func TestRunActivityNowReportsEachAccount(t *testing.T) { | |
| fastActivity(t) | |
| stub := &reportStub{} | |
| srv := httptest.NewServer(stub.handler()) | |
| defer srv.Close() | |
| p := pool.New("") | |
| p.Add(&auth.Auth{UID: "u1", AccessToken: "at", RefreshToken: "rt", ExpiresAt: 9999999999}) | |
| p.Add(&auth.Auth{UID: "u2", AccessToken: "at", RefreshToken: "rt", ExpiresAt: 9999999999}) | |
| up := &upstream.Client{HTTP: srv.Client(), ChatBaseCN: srv.URL, BillingBaseCN: srv.URL} | |
| s := New(Config{Pool: p, Upstream: up}) | |
| s.RunActivityNow() | |
| if n := stub.calls.Load(); n != 2 { | |
| t.Errorf("report calls=%d want 2(每号上报一次)", n) | |
| } | |
| if n := stub.uids.Load(); n != 2 { | |
| t.Errorf("report with X-User-Id=%d want 2(每号必带 userId)", n) | |
| } | |
| } | |
| // TestRunActivityNowSkipsDisabledAndNoToken 禁用账号与无 token 账号跳过。 | |
| func TestRunActivityNowSkipsDisabledAndNoToken(t *testing.T) { | |
| fastActivity(t) | |
| stub := &reportStub{} | |
| srv := httptest.NewServer(stub.handler()) | |
| defer srv.Close() | |
| p := pool.New("") | |
| p.Add(&auth.Auth{UID: "ok", AccessToken: "at", RefreshToken: "rt", ExpiresAt: 9999999999}) | |
| p.Add(&auth.Auth{UID: "dis", AccessToken: "at", RefreshToken: "rt", ExpiresAt: 9999999999}) | |
| p.Add(&auth.Auth{UID: "notoken", AccessToken: "", RefreshToken: "", ExpiresAt: 9999999999}) | |
| p.Disable("dis", "test") | |
| up := &upstream.Client{HTTP: srv.Client(), ChatBaseCN: srv.URL, BillingBaseCN: srv.URL} | |
| s := New(Config{Pool: p, Upstream: up}) | |
| s.RunActivityNow() | |
| if n := stub.calls.Load(); n != 1 { | |
| t.Errorf("report calls=%d want 1(仅 ok 账号)", n) | |
| } | |
| } | |
| // TestRunActivityNowErrorDoesNotAbort 单账号上报失败不影响后续遍历。 | |
| func TestRunActivityNowErrorDoesNotAbort(t *testing.T) { | |
| fastActivity(t) | |
| var okCalls atomic.Int32 | |
| srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | |
| if r.URL.Path != "/v2/report" { | |
| // streak 自检等非 report 请求直接回 200(不带计数,只统计 /v2/report)。 | |
| w.Write([]byte(`{"code":0,"data":{}}`)) | |
| return | |
| } | |
| uid := r.Header.Get("X-User-Id") | |
| if uid == "fail" { | |
| w.WriteHeader(500) | |
| w.Write([]byte(`boom`)) | |
| return | |
| } | |
| okCalls.Add(1) | |
| w.Write([]byte(`{"code":0}`)) | |
| })) | |
| defer srv.Close() | |
| p := pool.New("") | |
| p.Add(&auth.Auth{UID: "fail", AccessToken: "at", RefreshToken: "rt", ExpiresAt: 9999999999}) | |
| p.Add(&auth.Auth{UID: "ok", AccessToken: "at", RefreshToken: "rt", ExpiresAt: 9999999999}) | |
| up := &upstream.Client{HTTP: srv.Client(), ChatBaseCN: srv.URL, BillingBaseCN: srv.URL} | |
| s := New(Config{Pool: p, Upstream: up}) | |
| s.RunActivityNow() // 不应 panic | |
| if n := okCalls.Load(); n != 1 { | |
| t.Errorf("ok account report calls=%d want 1(失败账号不影响后续遍历)", n) | |
| } | |
| } | |
| // --------------------------------------------------------------------------- | |
| // P1:活跃上报后回读 streak 自检 | |
| // --------------------------------------------------------------------------- | |
| // activityStreakStub 模拟 /v2/report(200 成功)+ /activity/growth/streak(days 可配)。 | |
| type activityStreakStub struct { | |
| reportCalls atomic.Int32 | |
| days int // streak 返回的连登天数 | |
| streakErr bool // 让 streak 返回 500 | |
| noUserId bool // 待测:上报不带 userId(服务端 200 但静默丢弃) | |
| streakHits atomic.Int32 | |
| } | |
| func (s *activityStreakStub) handler() http.Handler { | |
| return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | |
| switch r.URL.Path { | |
| case "/v2/report": | |
| s.reportCalls.Add(1) | |
| w.Write([]byte(`{"code":0,"msg":"OK"}`)) | |
| case "/activity/growth/streak": | |
| s.streakHits.Add(1) | |
| if s.streakErr { | |
| w.WriteHeader(500) | |
| w.Write([]byte(`boom`)) | |
| return | |
| } | |
| fmt.Fprintf(w, `{"code":0,"data":{"streak":{"days":%d}}}`, s.days) | |
| default: | |
| http.Error(w, "not found", 404) | |
| } | |
| }) | |
| } | |
| // activityStreakScheduler 构造带 streak 自检 stub 的调度器。 | |
| func activityStreakScheduler(t *testing.T, srv *httptest.Server) (*Scheduler, *pool.Pool) { | |
| t.Helper() | |
| p := pool.New("") | |
| p.Add(&auth.Auth{UID: "u1", AccessToken: "at", RefreshToken: "rt", ExpiresAt: 9999999999}) | |
| up := &upstream.Client{HTTP: srv.Client(), ChatBaseCN: srv.URL, BillingBaseCN: srv.URL} | |
| return New(Config{Pool: p, Upstream: up}), p | |
| } | |
| // TestRunActivityNowSelfCheckDaysNormal 上报成功后回读 streak:days>=1 → 无告警。 | |
| func TestRunActivityNowSelfCheckDaysNormal(t *testing.T) { | |
| fastActivity(t) | |
| stub := &activityStreakStub{days: 3} | |
| srv := httptest.NewServer(stub.handler()) | |
| defer srv.Close() | |
| p := pool.New("") | |
| p.Add(&auth.Auth{UID: "u1", AccessToken: "at", RefreshToken: "rt", ExpiresAt: 9999999999}) | |
| up := &upstream.Client{HTTP: srv.Client(), ChatBaseCN: srv.URL, BillingBaseCN: srv.URL} | |
| s := New(Config{Pool: p, Upstream: up}) | |
| s.RunActivityNow() | |
| if stub.reportCalls.Load() != 1 || stub.streakHits.Load() != 1 { | |
| t.Errorf("report_calls=%d streak_hits=%d want 1/1", stub.reportCalls.Load(), stub.streakHits.Load()) | |
| } | |
| // days>=1:checkActivityStreak 返回 false(无可疑)。 | |
| if s.checkActivityStreak(p.AuthByUID("u1")) { | |
| t.Fatal("days>=1 不告警") | |
| } | |
| } | |
| // TestRunActivityNowSelfCheckSilentDrop 上报 200 但 streak.days=0 → 告警(silent drop?)。 | |
| func TestRunActivityNowSelfCheckSilentDrop(t *testing.T) { | |
| fastActivity(t) | |
| stub := &activityStreakStub{days: 0} | |
| srv := httptest.NewServer(stub.handler()) | |
| defer srv.Close() | |
| p := pool.New("") | |
| p.Add(&auth.Auth{UID: "u1", AccessToken: "at", RefreshToken: "rt", ExpiresAt: 9999999999}) | |
| up := &upstream.Client{HTTP: srv.Client(), ChatBaseCN: srv.URL, BillingBaseCN: srv.URL} | |
| s := New(Config{Pool: p, Upstream: up}) | |
| if !s.checkActivityStreak(p.AuthByUID("u1")) { | |
| t.Fatal("days=0 应告警(上报 200 但 silent drop?)") | |
| } | |
| } | |
| // TestRunActivityNowSelfCheckGETFailure 回读 GET 失败 → 告警但不影响主流程(上报已成功)。 | |
| func TestRunActivityNowSelfCheckGETFailure(t *testing.T) { | |
| fastActivity(t) | |
| stub := &activityStreakStub{days: 1, streakErr: true} | |
| srv := httptest.NewServer(stub.handler()) | |
| defer srv.Close() | |
| p := pool.New("") | |
| p.Add(&auth.Auth{UID: "u1", AccessToken: "at", RefreshToken: "rt", ExpiresAt: 9999999999}) | |
| up := &upstream.Client{HTTP: srv.Client(), ChatBaseCN: srv.URL, BillingBaseCN: srv.URL} | |
| s := New(Config{Pool: p, Upstream: up}) | |
| // 直接断言 checkActivityStreak:GET 失败 → 告警。 | |
| if !s.checkActivityStreak(p.AuthByUID("u1")) { | |
| t.Fatal("streak GET 失败应告警(reported but unverifiable)") | |
| } | |
| // 回读是只读 oracle:GET 失败不影响已发生的上报本轮走通(遍历继续)。 | |
| if stub.streakHits.Load() != 1 { | |
| t.Errorf("streak_hits=%d want 1(GET 失败也打了 streak 请求)", stub.streakHits.Load()) | |
| } | |
| } | |
| // TestRunActivityNowSkipsSelfCheckOnReportFail 上报失败 → 不跑自检(SKIP,无意义回读)。 | |
| func TestRunActivityNowSkipsSelfCheckOnReportFail(t *testing.T) { | |
| fastActivity(t) | |
| var streakHits atomic.Int32 | |
| srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | |
| switch r.URL.Path { | |
| case "/v2/report": | |
| w.WriteHeader(500) | |
| w.Write([]byte(`boom`)) | |
| case "/activity/growth/streak": | |
| streakHits.Add(1) | |
| w.Write([]byte(`{"code":0,"data":{"streak":{"days":1}}}`)) | |
| } | |
| })) | |
| defer srv.Close() | |
| p := pool.New("") | |
| p.Add(&auth.Auth{UID: "u1", AccessToken: "at", RefreshToken: "rt", ExpiresAt: 9999999999}) | |
| up := &upstream.Client{HTTP: srv.Client(), ChatBaseCN: srv.URL, BillingBaseCN: srv.URL} | |
| s := New(Config{Pool: p, Upstream: up}) | |
| s.RunActivityNow() // 不上报成功 → 无自检 | |
| if streakHits.Load() != 0 { | |
| t.Errorf("streak hits=%d want 0(上报失败不跑自检)", streakHits.Load()) | |
| } | |
| } | |
| // TestRunCheckinDoesNotTriggerTravel 签到收尾不再跑旅行(旅行已剥离为独立排程)。 | |
| func TestRunCheckinDoesNotTriggerTravel(t *testing.T) { | |
| fastTravel(t) | |
| stub := &travelStub{buddy: "null"} | |
| srv := billingAndGrowthServer(stub) | |
| defer srv.Close() | |
| s, _ := newTravelScheduler(t, srv, "u1") | |
| s.RunCheckinNow() | |
| // 签到不再顺带跑旅行:buddy/info 不应被调用。 | |
| if n := stub.infoCalls.Load(); n != 0 { | |
| t.Errorf("buddy/info calls=%d want 0(旅行已从签到剥离)", n) | |
| } | |
| } | |
| // TestNextWakeTravelIndependent 旅行有独立时点,与签到互不影响。 | |
| func TestNextWakeTravelIndependent(t *testing.T) { | |
| s := New(Config{ | |
| CheckinHours: []int{21}, | |
| TravelHours: []int{9}, | |
| ActivityHours: []int{10}, | |
| KeepaliveHours: []int{22}, | |
| }) | |
| at, kinds := s.nextWake(time.Date(2026, 9, 11, 8, 0, 0, 0, time.Local)) | |
| if want := time.Date(2026, 9, 11, 9, 0, 0, 0, time.Local); !at.Equal(want) { | |
| t.Errorf("next=%v want %v(旅行 09:00 独立时点)", at, want) | |
| } | |
| if len(kinds) != 1 || kinds[0] != taskTravel { | |
| t.Errorf("kinds=%v want [travel]", kinds) | |
| } | |
| } | |
| // TestNextWakeActivityIndependent 活跃上报有独立时点。 | |
| func TestNextWakeActivityIndependent(t *testing.T) { | |
| s := New(Config{ | |
| CheckinHours: []int{21}, | |
| TravelHours: []int{9}, | |
| ActivityHours: []int{10}, | |
| KeepaliveHours: []int{22}, | |
| }) | |
| at, kinds := s.nextWake(time.Date(2026, 9, 11, 9, 30, 0, 0, time.Local)) | |
| if want := time.Date(2026, 9, 11, 10, 0, 0, 0, time.Local); !at.Equal(want) { | |
| t.Errorf("next=%v want %v(活跃 10:00 独立时点)", at, want) | |
| } | |
| if len(kinds) != 1 || kinds[0] != taskActivity { | |
| t.Errorf("kinds=%v want [activity]", kinds) | |
| } | |
| } | |
| // TestNextWakeTravelDisabled 旅行禁用后排程里不再有旅行时点(签到照常)。 | |
| func TestNextWakeTravelDisabled(t *testing.T) { | |
| s := New(Config{ | |
| CheckinHours: []int{9, 21}, | |
| TravelHours: []int{9}, | |
| TravelDisabled: true, | |
| KeepaliveHours: []int{22}, | |
| }) | |
| at, kinds := s.nextWake(time.Date(2026, 9, 11, 8, 0, 0, 0, time.Local)) | |
| // 旅行禁用 → 09:00 旅行时点不应出现,最近的是 09:00 签到(同小时但签到未禁用)。 | |
| if want := time.Date(2026, 9, 11, 9, 0, 0, 0, time.Local); !at.Equal(want) { | |
| t.Errorf("next=%v want %v", at, want) | |
| } | |
| if !hasKind(kinds, taskCheckin) { | |
| t.Errorf("kinds=%v want 含 checkin", kinds) | |
| } | |
| if hasKind(kinds, taskTravel) { | |
| t.Errorf("kinds=%v 不应含 travel(已禁用)", kinds) | |
| } | |
| } | |
| // TestNextWakeActivityDisabled 活跃上报禁用后排程里不再有活跃时点。 | |
| func TestNextWakeActivityDisabled(t *testing.T) { | |
| s := New(Config{ | |
| CheckinHours: []int{9, 21}, | |
| ActivityHours: []int{10}, | |
| ActivityDisabled: true, | |
| KeepaliveHours: []int{22}, | |
| }) | |
| at, kinds := s.nextWake(time.Date(2026, 9, 11, 9, 30, 0, 0, time.Local)) | |
| if want := time.Date(2026, 9, 11, 21, 0, 0, 0, time.Local); !at.Equal(want) { | |
| t.Errorf("next=%v want %v(活跃禁用 → 跳过 10:00)", at, want) | |
| } | |
| if hasKind(kinds, taskActivity) { | |
| t.Errorf("kinds=%v 不应含 activity(已禁用)", kinds) | |
| } | |
| } | |
| // TestCheckinDisabledTravelStillRuns 签到禁用时旅行/活跃照跑(验收标准 2)。 | |
| func TestCheckinDisabledTravelStillRuns(t *testing.T) { | |
| s := New(Config{ | |
| CheckinHours: []int{9, 21}, | |
| CheckinDisabled: true, | |
| TravelHours: []int{9}, | |
| ActivityHours: []int{10}, | |
| KeepaliveHours: []int{22}, | |
| }) | |
| at, kinds := s.nextWake(time.Date(2026, 9, 11, 8, 0, 0, 0, time.Local)) | |
| if want := time.Date(2026, 9, 11, 9, 0, 0, 0, time.Local); !at.Equal(want) { | |
| t.Errorf("next=%v want %v(签到禁用,旅行 09:00 照跑)", at, want) | |
| } | |
| if hasKind(kinds, taskCheckin) { | |
| t.Errorf("kinds=%v 不应含 checkin(已禁用)", kinds) | |
| } | |
| if !hasKind(kinds, taskTravel) { | |
| t.Errorf("kinds=%v 应含 travel(签到禁用但旅行独立)", kinds) | |
| } | |
| } | |
| // TestAllFourDisabledNoSpin 四类任务全禁用:Run 不空转。 | |
| func TestAllFourDisabledNoSpin(t *testing.T) { | |
| s := New(Config{ | |
| CheckinDisabled: true, | |
| TravelDisabled: true, | |
| ActivityDisabled: true, | |
| KeepaliveDisabled: true, | |
| BlackcatDisabled: true, | |
| CheckinHours: []int{9, 21}, | |
| TravelHours: []int{9}, | |
| ActivityHours: []int{10}, | |
| KeepaliveHours: []int{22}, | |
| }) | |
| at, kinds := s.nextWake(time.Now()) | |
| if !at.IsZero() || len(kinds) != 0 { | |
| t.Errorf("at=%v kinds=%v want zero/nil(五类全禁用)", at, kinds) | |
| } | |
| } | |
| // TestNextWakeSameHourTravelAndCheckin 旅行与签到配到同一小时时两类任务都要执行。 | |
| func TestNextWakeSameHourTravelAndCheckin(t *testing.T) { | |
| s := New(Config{ | |
| CheckinHours: []int{9, 21}, | |
| TravelHours: []int{9}, | |
| ActivityHours: []int{10}, | |
| KeepaliveHours: []int{22}, | |
| }) | |
| at, kinds := s.nextWake(time.Date(2026, 9, 11, 8, 0, 0, 0, time.Local)) | |
| if want := time.Date(2026, 9, 11, 9, 0, 0, 0, time.Local); !at.Equal(want) { | |
| t.Errorf("next=%v want %v", at, want) | |
| } | |
| if !hasKind(kinds, taskCheckin) || !hasKind(kinds, taskTravel) { | |
| t.Errorf("kinds=%v want 含 checkin+travel(同 09:00 两任务)", kinds) | |
| } | |
| } | |
| // TestRunDispatchesActivityAndTravel Run 到点分发 activity 与 travel(不真打上游,用空池)。 | |
| func TestRunDispatchesActivityAndTravel(t *testing.T) { | |
| fastActivity(t) | |
| fastTravel(t) | |
| // 空 pool → RunActivityNow/RunTravelNow 遍历 0 账号即返回,不阻塞。 | |
| p := pool.New("") | |
| up := &upstream.Client{} | |
| s := New(Config{ | |
| Pool: p, | |
| Upstream: up, | |
| CheckinHours: []int{}, | |
| TravelHours: []int{}, | |
| ActivityHours: []int{}, | |
| KeepaliveHours: []int{}, | |
| }) | |
| // 四类全空 hours → nextWake 回落默认 → 会构造 timer,ctx 取消即返回。 | |
| ctx, cancel := context.WithCancel(context.Background()) | |
| done := make(chan struct{}) | |
| go func() { s.Run(ctx); close(done) }() | |
| time.Sleep(20 * time.Millisecond) | |
| cancel() | |
| select { | |
| case <-done: | |
| case <-time.After(2 * time.Second): | |
| t.Fatal("Run 未在 ctx 取消后返回") | |
| } | |
| } | |
| // --------------------------------------------------------------------------- | |
| // 国际版活跃上报:global 账号不再被跳过(上游 a190252 同口径) | |
| // --------------------------------------------------------------------------- | |
| // withGlobalSwitch 临时打开 global realm 开关并复位(生产缺省即开,此辅助确保测试隔离)。 | |
| func withGlobalSwitch(t *testing.T) { | |
| t.Helper() | |
| old := auth.GlobalEnabled() | |
| auth.SetGlobalEnabled(true) | |
| t.Cleanup(func() { auth.SetGlobalEnabled(old) }) | |
| } | |
| // globalAccount 构造一个 realm=global 的账号(BackfillRealmFor 落显式标识)。 | |
| func globalAccount(t *testing.T, uid string) *auth.Auth { | |
| t.Helper() | |
| a := &auth.Auth{UID: uid, AccessToken: "at", RefreshToken: "rt", ExpiresAt: 9999999999} | |
| if _, err := auth.BackfillRealmFor(a, "global"); err != nil { | |
| t.Fatalf("BackfillRealmFor(global): %v", err) | |
| } | |
| if !a.IsGlobal() { | |
| t.Fatalf("%s IsGlobal()=false want true", uid) | |
| } | |
| return a | |
| } | |
| // TestRunActivityNowReportsGlobalAccount 纯 global 池:账号必须上报(曾因 | |
| // 「D4 门控:global 无活跃体系」被跳过 → 国际版永远点不亮连登、拿不到每日积分)。 | |
| func TestRunActivityNowReportsGlobalAccount(t *testing.T) { | |
| withGlobalSwitch(t) | |
| fastActivity(t) | |
| var cnHits, globalHits atomic.Int32 | |
| cnSrv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | |
| if r.URL.Path == "/v2/report" { | |
| cnHits.Add(1) | |
| } | |
| w.Write([]byte(`{"code":0,"msg":"OK"}`)) | |
| })) | |
| defer cnSrv.Close() | |
| globalSrv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | |
| if r.URL.Path == "/v2/report" { | |
| globalHits.Add(1) | |
| } | |
| w.Write([]byte(`{"code":0,"msg":"OK"}`)) | |
| })) | |
| defer globalSrv.Close() | |
| p := pool.New("") | |
| p.Add(globalAccount(t, "g1")) | |
| // global base 单独指向另一台 server:断言路由按 realm 切,不碰 CN 端点。 | |
| up := &upstream.Client{ | |
| HTTP: cnSrv.Client(), | |
| GlobalEnabled: true, | |
| ChatBaseCN: cnSrv.URL, | |
| BillingBaseCN: cnSrv.URL, | |
| ChatBaseGlobal: globalSrv.URL, | |
| BillingBaseGlobal: globalSrv.URL, | |
| } | |
| s := New(Config{Pool: p, Upstream: up}) | |
| s.RunActivityNow() | |
| if n := globalHits.Load(); n == 0 { | |
| t.Error("global 账号未上报:/v2/report 未打到 global base(国际版将永远点不亮连登)") | |
| } | |
| if n := cnHits.Load(); n != 0 { | |
| t.Errorf("CN base hits=%d want 0(global 账号不应打到 CN 端点)", n) | |
| } | |
| } | |
| // TestRunActivityNowMixedPoolReportsBoth 混池:CN 与 global 各上报一次, | |
| // 且分别路由到各自 realm 的 base。 | |
| func TestRunActivityNowMixedPoolReportsBoth(t *testing.T) { | |
| withGlobalSwitch(t) | |
| fastActivity(t) | |
| var cnHits, globalHits atomic.Int32 | |
| cnSrv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | |
| if r.URL.Path == "/v2/report" { | |
| cnHits.Add(1) | |
| } | |
| w.Write([]byte(`{"code":0,"msg":"OK"}`)) | |
| })) | |
| defer cnSrv.Close() | |
| globalSrv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | |
| if r.URL.Path == "/v2/report" { | |
| globalHits.Add(1) | |
| } | |
| w.Write([]byte(`{"code":0,"msg":"OK"}`)) | |
| })) | |
| defer globalSrv.Close() | |
| p := pool.New("") | |
| p.Add(&auth.Auth{UID: "cn1", AccessToken: "at", RefreshToken: "rt", ExpiresAt: 9999999999}) | |
| p.Add(globalAccount(t, "g1")) | |
| up := &upstream.Client{ | |
| HTTP: cnSrv.Client(), | |
| GlobalEnabled: true, | |
| ChatBaseCN: cnSrv.URL, | |
| BillingBaseCN: cnSrv.URL, | |
| ChatBaseGlobal: globalSrv.URL, | |
| BillingBaseGlobal: globalSrv.URL, | |
| } | |
| s := New(Config{Pool: p, Upstream: up}) | |
| s.RunActivityNow() | |
| if n := cnHits.Load(); n != 1 { | |
| t.Errorf("CN report hits=%d want 1", n) | |
| } | |
| if n := globalHits.Load(); n != 1 { | |
| t.Errorf("global report hits=%d want 1", n) | |
| } | |
| } | |
| // TestRunActivityNowGlobalErrorDoesNotSkipCN global report 失败(404)时 | |
| // CN 账号照常上报:单账号失败只 break 该号,不影响遍历。 | |
| func TestRunActivityNowGlobalErrorDoesNotSkipCN(t *testing.T) { | |
| withGlobalSwitch(t) | |
| fastActivity(t) | |
| var cnHits atomic.Int32 | |
| cnSrv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | |
| if r.URL.Path == "/v2/report" { | |
| cnHits.Add(1) | |
| } | |
| w.Write([]byte(`{"code":0,"msg":"OK"}`)) | |
| })) | |
| defer cnSrv.Close() | |
| // global 侧一律 404:模拟国际版端点不可用。 | |
| globalSrv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | |
| http.Error(w, "not found", 404) | |
| })) | |
| defer globalSrv.Close() | |
| p := pool.New("") | |
| p.Add(globalAccount(t, "g1")) | |
| p.Add(&auth.Auth{UID: "cn1", AccessToken: "at", RefreshToken: "rt", ExpiresAt: 9999999999}) | |
| up := &upstream.Client{ | |
| HTTP: cnSrv.Client(), | |
| GlobalEnabled: true, | |
| ChatBaseCN: cnSrv.URL, | |
| BillingBaseCN: cnSrv.URL, | |
| ChatBaseGlobal: globalSrv.URL, | |
| BillingBaseGlobal: globalSrv.URL, | |
| } | |
| s := New(Config{Pool: p, Upstream: up}) | |
| s.RunActivityNow() // 不应 panic | |
| if n := cnHits.Load(); n != 1 { | |
| t.Errorf("CN report hits=%d want 1(global 失败不应影响 CN 账号)", n) | |
| } | |
| } | |
| // TestCheckinStillSkipsGlobal 回归护栏:本次只放开活跃上报,签到仍跳过 global | |
| // (上游 a190252 明确「checkin/travel 门控不动」)。误删该 gate 会让国际版去打 | |
| // CN 专属签到端点。 | |
| func TestCheckinStillSkipsGlobal(t *testing.T) { | |
| withGlobalSwitch(t) | |
| var hits atomic.Int32 | |
| srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | |
| hits.Add(1) | |
| w.Write([]byte(`{"code":0,"data":{}}`)) | |
| })) | |
| defer srv.Close() | |
| p := pool.New("") | |
| p.Add(globalAccount(t, "g1")) | |
| up := &upstream.Client{ | |
| HTTP: srv.Client(), | |
| GlobalEnabled: true, | |
| ChatBaseCN: srv.URL, | |
| BillingBaseCN: srv.URL, | |
| ChatBaseGlobal: srv.URL, | |
| BillingBaseGlobal: srv.URL, | |
| } | |
| s := New(Config{Pool: p, Upstream: up}) | |
| s.RunCheckinNow() | |
| if n := hits.Load(); n != 0 { | |
| t.Errorf("checkin global 上游调用=%d want 0(签到仍应跳过 global)", n) | |
| } | |
| } | |