Files
huya-go/internal/sniper/sniper_test.go
T
yml2213 7dadcd1644 feat(role): 增加用户绑定角色信息查询与作战表展示
- 逆向并实现虎牙活动绑角色接口 webActUI.checkUserBindGameAccount,支持精准解析游戏角色名、区服和平台
- 增加本地持久化 roles.json 存储,启动即回显角色信息无需重复请求
- 前端账号作战表与 CK 管理列表整合角色展示([微信-安卓] 角色名),直观区分账号归属
- 工具栏增加「查询角色」按钮,并以兑换记录中的历史账号作为双重兜底
- 升级版本至 v0.5.5 并成功交叉编译 Windows 产物
2026-10-19 12:09:24 +08:00

690 lines
22 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package sniper
import (
"context"
"errors"
"sync"
"sync/atomic"
"testing"
"time"
"huya-go/internal/huya"
)
type fakeActivityClient struct {
mu sync.Mutex
listCount int
exchangeCount int
prewarmCount int
scoreCount int
recordCount int
listResp *huya.GetActPrizeListResp
exchangeFn func(uid int64, sid, pid int32) (*huya.ScoreExchangePrizeResp, error)
exchangeCtxFn func(ctx context.Context, uid int64, sid, pid int32) (*huya.ScoreExchangePrizeResp, error)
}
func (f *fakeActivityClient) GetActPrizeList(ctx context.Context, uid int64, cookie string, sid int32) (*huya.GetActPrizeListResp, error) {
f.mu.Lock()
f.listCount++
f.mu.Unlock()
if f.listResp != nil {
return f.listResp, nil
}
return &huya.GetActPrizeListResp{}, nil
}
func (f *fakeActivityClient) QueryUserScore(ctx context.Context, uid int64, cookie string, sid int32) (*huya.GetUserScoreResp, error) {
f.mu.Lock()
f.scoreCount++
f.mu.Unlock()
return &huya.GetUserScoreResp{Status: 200, Msg: "请求成功"}, nil
}
func (f *fakeActivityClient) GetUserPrizeRecords(ctx context.Context, uid int64, cookie string, sid int32) (*huya.GetUserPrizeRecordsResp, error) {
f.mu.Lock()
f.recordCount++
f.mu.Unlock()
return &huya.GetUserPrizeRecordsResp{Status: 200, Msg: "请求成功"}, nil
}
func (f *fakeActivityClient) ScoreExchangePrize(ctx context.Context, uid int64, cookie string, sid, pid int32) (*huya.ScoreExchangePrizeResp, error) {
f.mu.Lock()
f.exchangeCount++
f.mu.Unlock()
if f.exchangeCtxFn != nil {
return f.exchangeCtxFn(ctx, uid, sid, pid)
}
if f.exchangeFn != nil {
return f.exchangeFn(uid, sid, pid)
}
return &huya.ScoreExchangePrizeResp{Status: 200, Msg: "请求成功"}, nil
}
func (f *fakeActivityClient) CheckUserBindGameAccount(ctx context.Context, uid int64, cookie string, bActID int32) (*huya.CheckUserBindGameAccountResp, error) {
return &huya.CheckUserBindGameAccountResp{
Status: 200,
Msg: "请求成功",
AccountData: huya.GameAccountData{
IsBindAccount: 1,
IsBindRole: 1,
GameRole: huya.GameRole{
RoleName: "测试角色",
AreaName: "微信",
PlatName: "安卓",
},
},
GameName: "和平精英",
}, nil
}
func (f *fakeActivityClient) PrewarmActivity(ctx context.Context, uid int64, cookie string, sid int32) error {
f.mu.Lock()
f.prewarmCount++
f.mu.Unlock()
return nil
}
func (f *fakeActivityClient) counts() (int, int, int) {
f.mu.Lock()
defer f.mu.Unlock()
return f.listCount, f.exchangeCount, f.prewarmCount
}
func TestClassifyExchange(t *testing.T) {
tests := []struct {
name string
resp *huya.ScoreExchangePrizeResp
err error
want string
}{
{name: "success", resp: &huya.ScoreExchangePrizeResp{Status: 200, Msg: "请求成功"}, want: CategorySuccess},
{name: "frequent", resp: &huya.ScoreExchangePrizeResp{Status: 500, Msg: "操作频繁,请稍后再试"}, want: CategoryTooFrequent},
{name: "not open", resp: &huya.ScoreExchangePrizeResp{Status: 500, Msg: "奖品暂未开放兑换"}, want: CategoryNotOpen},
{name: "not enough", resp: &huya.ScoreExchangePrizeResp{Status: 500, Msg: "兑换货币不足"}, want: CategoryNotEnough},
{name: "already", resp: &huya.ScoreExchangePrizeResp{Status: 500, Msg: "已兑换该奖品"}, want: CategoryAlreadyDone},
{name: "off shelf", resp: &huya.ScoreExchangePrizeResp{Status: 500, Msg: "奖品已下架"}, want: CategoryOffShelf},
{name: "sold out", resp: &huya.ScoreExchangePrizeResp{Status: 500, Msg: "库存不足"}, want: CategorySoldOut},
{name: "error", err: errors.New("timeout"), want: CategoryError},
{
name: "success with order id",
resp: &huya.ScoreExchangePrizeResp{Status: 200, OrderID: "sOrderId_2203_12889_1_1783328404_957"},
want: CategorySuccess,
},
{
name: "not enough in precondition tip",
resp: &huya.ScoreExchangePrizeResp{
Status: 500,
Msg: "兑换失败",
ActPreCondition: huya.ActPreCondition{Tip: "积分不足,无法兑换"},
},
want: CategoryNotEnough,
},
{
name: "not enough in condition params",
resp: &huya.ScoreExchangePrizeResp{
Status: 500,
Msg: "兑换失败",
ActPreCondition: huya.ActPreCondition{ConditionParams: map[string]string{"tip": "余额不足"}},
},
want: CategoryNotEnough,
},
{
name: "condition gate without text",
resp: &huya.ScoreExchangePrizeResp{
Status: 500,
ActPreCondition: huya.ActPreCondition{ConditionType: 2},
},
want: CategoryCondition,
},
{name: "empty response", resp: &huya.ScoreExchangePrizeResp{Status: 500}, want: CategoryFailed},
{name: "nil response", resp: nil, want: CategoryFailed},
{
name: "text wins over order id",
resp: &huya.ScoreExchangePrizeResp{Status: 200, OrderID: "order", Msg: "库存不足"},
want: CategorySoldOut,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := ClassifyExchange(tt.resp, tt.err); got != tt.want {
t.Fatalf("ClassifyExchange()=%s, want %s", got, tt.want)
}
})
}
}
func TestRunTargetsDirectExchangeSkipsListAndStopsPerAccount(t *testing.T) {
client := &fakeActivityClient{}
runner := Runner{Client: client}
accounts := []Account{{UID: 1, Cookie: "yyuid=1"}, {UID: 2, Cookie: "yyuid=2"}}
err := runner.RunTargets(context.Background(), accounts, []Target{{PID: 12889}}, Config{
SID: 2203,
maxAttempts: 5,
Delay: time.Millisecond,
Concurrency: 2,
StopAccountAfterSuccess: true,
}, nil)
if err != nil {
t.Fatal(err)
}
listCount, exchangeCount, _ := client.counts()
if listCount != 0 {
t.Fatalf("listCount=%d, want 0", listCount)
}
if exchangeCount != len(accounts) {
t.Fatalf("exchangeCount=%d, want %d", exchangeCount, len(accounts))
}
}
func TestRunTargetsTerminalNotEnoughStopsAccountTarget(t *testing.T) {
client := &fakeActivityClient{
exchangeFn: func(uid int64, sid, pid int32) (*huya.ScoreExchangePrizeResp, error) {
return &huya.ScoreExchangePrizeResp{Status: 500, Msg: "兑换货币不足"}, nil
},
}
runner := Runner{Client: client}
err := runner.RunTargets(context.Background(), []Account{{UID: 1, Cookie: "yyuid=1"}}, []Target{{PID: 12889}}, Config{
SID: 2203,
maxAttempts: 4,
Delay: time.Millisecond,
}, nil)
if err != nil {
t.Fatal(err)
}
_, exchangeCount, _ := client.counts()
if exchangeCount != 1 {
t.Fatalf("exchangeCount=%d, want 1", exchangeCount)
}
}
func TestRunTargetsTooFrequentBackoffDelaysActualAttempts(t *testing.T) {
client := &fakeActivityClient{
exchangeFn: func(uid int64, sid, pid int32) (*huya.ScoreExchangePrizeResp, error) {
return &huya.ScoreExchangePrizeResp{Status: 500, Msg: "操作频繁,请稍后再试"}, nil
},
}
runner := Runner{Client: client}
started := time.Now()
err := runner.RunTargets(context.Background(), []Account{{UID: 1, Cookie: "yyuid=1"}}, []Target{{PID: 12889}}, Config{
SID: 2203,
maxAttempts: 2,
Delay: time.Millisecond,
TooFrequentBackoff: 50 * time.Millisecond,
}, nil)
if err != nil {
t.Fatal(err)
}
_, exchangeCount, _ := client.counts()
if exchangeCount != 2 {
t.Fatalf("exchangeCount=%d, want 2(maxAttempts 应统计真实尝试)", exchangeCount)
}
if elapsed := time.Since(started); elapsed < 45*time.Millisecond {
t.Fatalf("退避没有生效,耗时=%v", elapsed)
}
}
func TestRunTargetsSharedListForNameTargets(t *testing.T) {
client := &fakeActivityClient{
listResp: &huya.GetActPrizeListResp{
Status: 200,
Msg: "请求成功",
Prizes: []huya.PrizeInfo{{ID: 12889, SID: 2203, Name: "套装-潮萌小黄鸭", NewScore: 988, LeftNum: 10}},
},
}
runner := Runner{Client: client}
accounts := []Account{{UID: 1, Cookie: "yyuid=1"}, {UID: 2, Cookie: "yyuid=2"}}
err := runner.RunTargets(context.Background(), accounts, []Target{{Name: "小黄鸭"}}, Config{
SID: 2203,
maxAttempts: 1,
Concurrency: 2,
StopAccountAfterSuccess: true,
ListBeforeExchange: true,
SharePrizeList: true,
}, nil)
if err != nil {
t.Fatal(err)
}
listCount, exchangeCount, _ := client.counts()
if listCount != 1 {
t.Fatalf("listCount=%d, want 1", listCount)
}
if exchangeCount != len(accounts) {
t.Fatalf("exchangeCount=%d, want %d", exchangeCount, len(accounts))
}
}
func TestRunTargetsAutoSelectPrefersPrizeWithLeft(t *testing.T) {
var gotPID int32
client := &fakeActivityClient{
listResp: &huya.GetActPrizeListResp{
Status: 200,
Msg: "请求成功",
Prizes: []huya.PrizeInfo{
{ID: 1, SID: 2203, Name: "无剩余", NewScore: 10, LeftNum: 0},
{ID: 2, SID: 2203, Name: "有剩余", NewScore: 20, LeftNum: 5},
},
},
exchangeFn: func(uid int64, sid, pid int32) (*huya.ScoreExchangePrizeResp, error) {
gotPID = pid
return &huya.ScoreExchangePrizeResp{Status: 200, Msg: "请求成功"}, nil
},
}
runner := Runner{Client: client}
err := runner.RunTargets(context.Background(), []Account{{UID: 1, Cookie: "yyuid=1"}}, []Target{{Auto: true}}, Config{
SID: 2203,
maxAttempts: 1,
StopAccountAfterSuccess: true,
}, nil)
if err != nil {
t.Fatal(err)
}
if gotPID != 2 {
t.Fatalf("gotPID=%d, want 2", gotPID)
}
}
// TestRunTargetsCategoryFilter 验证按类目限定后只会在该类的商品里成交。
func TestRunTargetsCategoryFilter(t *testing.T) {
var gotPID int32
client := &fakeActivityClient{
listResp: &huya.GetActPrizeListResp{
Status: 200,
Msg: "请求成功",
Tags: []huya.PrizeTag{
{ID: 101, Name: "精英专享", Weight: 2},
{ID: 99, Name: "限量返场", Weight: 1},
},
Prizes: []huya.PrizeInfo{
{ID: 13415, Name: "套装-雪山精英", NewScore: 600, LeftNum: 100, Tags: "101"},
{ID: 13433, Name: "怪诞乔克服装礼包", NewScore: 8000, LeftNum: 50, Tags: "99"},
},
},
exchangeFn: func(uid int64, sid, pid int32) (*huya.ScoreExchangePrizeResp, error) {
gotPID = pid
return &huya.ScoreExchangePrizeResp{Status: 200, Msg: "请求成功"}, nil
},
}
runner := Runner{Client: client}
err := runner.RunTargets(context.Background(), []Account{{UID: 1, Cookie: "yyuid=1"}}, []Target{{Auto: true}}, Config{
SID: 2277,
Category: "限量返场",
maxAttempts: 1,
StopAccountAfterSuccess: true,
}, nil)
if err != nil {
t.Fatal(err)
}
if gotPID != 13433 {
t.Fatalf("gotPID=%d, want 13433(应只在限量返场里选品)", gotPID)
}
}
// TestRunTargetsCategoryFilterMisses 验证类目里没有目标时给出带类目的未命中提示。
func TestRunTargetsCategoryFilterMisses(t *testing.T) {
client := &fakeActivityClient{
listResp: &huya.GetActPrizeListResp{
Status: 200,
Msg: "请求成功",
Tags: []huya.PrizeTag{{ID: 99, Name: "限量返场", Weight: 1}},
Prizes: []huya.PrizeInfo{{ID: 13415, Name: "套装-雪山精英", NewScore: 600, Tags: "101"}},
},
}
results := make([]Result, 0, 4)
runner := Runner{Client: client}
err := runner.RunTargets(context.Background(), []Account{{UID: 1, Cookie: "yyuid=1"}}, []Target{{Name: "雪山"}}, Config{
SID: 2277,
Category: "限量返场",
maxAttempts: 1,
ListBeforeExchange: true,
}, func(result Result) {
results = append(results, result)
})
if err != nil {
t.Fatal(err)
}
if len(results) != 1 || results[0].Category != CategoryNotFound {
t.Fatalf("results=%+v, want 单条 not_found", results)
}
if results[0].Message != "类目[限量返场]内未查询到所需物品,继续监控" {
t.Fatalf("Message=%q", results[0].Message)
}
_, exchangeCount, _ := client.counts()
if exchangeCount != 0 {
t.Fatalf("exchangeCount=%d, want 0", exchangeCount)
}
}
func TestRunTargetsRefreshAfterSuccessEmitsRefreshResults(t *testing.T) {
client := &fakeActivityClient{}
runner := Runner{Client: client}
results := make(chan Result, 8)
err := runner.RunTargets(context.Background(), []Account{{UID: 1, Cookie: "yyuid=1"}}, []Target{{PID: 12889}}, Config{
SID: 2203,
maxAttempts: 1,
StopAccountAfterSuccess: true,
RefreshAfterSuccess: true,
}, func(result Result) {
results <- result
})
if err != nil {
t.Fatal(err)
}
seenScore := false
seenRecords := false
deadline := time.After(time.Second)
for !seenScore || !seenRecords {
select {
case result := <-results:
seenScore = seenScore || result.Action == "refresh_score"
seenRecords = seenRecords || result.Action == "refresh_records"
case <-deadline:
t.Fatalf("未收到刷新结果: score=%v records=%v", seenScore, seenRecords)
}
}
}
func TestRunTargetsConditionGateKeepsRetrying(t *testing.T) {
client := &fakeActivityClient{
exchangeFn: func(uid int64, sid, pid int32) (*huya.ScoreExchangePrizeResp, error) {
return &huya.ScoreExchangePrizeResp{
Status: 500,
ActPreCondition: huya.ActPreCondition{ConditionType: 3},
}, nil
},
}
runner := Runner{Client: client}
err := runner.RunTargets(context.Background(), []Account{{UID: 1, Cookie: "yyuid=1"}}, []Target{{PID: 12889}}, Config{
SID: 2203,
maxAttempts: 3,
Delay: 10 * time.Millisecond,
ErrorBackoff: time.Millisecond,
}, nil)
if err != nil {
t.Fatal(err)
}
_, exchangeCount, _ := client.counts()
if exchangeCount != 3 {
t.Fatalf("exchangeCount=%d, want 3", exchangeCount)
}
}
func TestNormalizeTargetsDropsDuplicatePrizeTargets(t *testing.T) {
targets := normalizeTargets([]Target{
{PID: 13409, Name: "幸运币礼包(小)"},
{PID: 13409},
{Name: "幸运币礼包(小)"},
{PID: 12889, Name: "套装-潮萌小黄鸭"},
})
if len(targets) != 2 {
t.Fatalf("targets=%d, want 2: %+v", len(targets), targets)
}
if targets[0].PID != 13409 || targets[1].PID != 12889 {
t.Fatalf("unexpected targets: %+v", targets)
}
}
func TestRunTargetsExchangesPrizeOnceWhenTargetsDuplicated(t *testing.T) {
client := &fakeActivityClient{}
runner := Runner{Client: client}
err := runner.RunTargets(context.Background(), []Account{{UID: 1, Cookie: "yyuid=1"}}, []Target{
{PID: 13409, Name: "幸运币礼包(小)"},
{PID: 13409},
}, Config{
SID: 2277,
maxAttempts: 3,
Delay: time.Millisecond,
StopAccountAfterSuccess: true,
}, nil)
if err != nil {
t.Fatal(err)
}
_, exchangeCount, _ := client.counts()
if exchangeCount != 1 {
t.Fatalf("exchangeCount=%d, want 1(同一奖品不应重复兑换)", exchangeCount)
}
}
func TestRunTargetsWaitsForRefreshBeforeReturn(t *testing.T) {
client := &fakeActivityClient{}
runner := Runner{Client: client}
var mu sync.Mutex
seen := map[string]bool{}
err := runner.RunTargets(context.Background(), []Account{{UID: 1, Cookie: "yyuid=1"}}, []Target{{PID: 13409}}, Config{
SID: 2277,
maxAttempts: 1,
StopAccountAfterSuccess: true,
RefreshAfterSuccess: true,
}, func(result Result) {
mu.Lock()
seen[result.Action] = true
mu.Unlock()
})
if err != nil {
t.Fatal(err)
}
mu.Lock()
defer mu.Unlock()
if !seen["refresh_score"] || !seen["refresh_records"] {
t.Fatalf("RunTargets 返回时异步刷新还没结束: %v", seen)
}
}
func TestRunTargetsDoesNotExchangeSamePrizeTwiceViaNameMatch(t *testing.T) {
client := &fakeActivityClient{
listResp: &huya.GetActPrizeListResp{
Status: 200,
Prizes: []huya.PrizeInfo{{ID: 13409, Name: "幸运币礼包(小)", NewScore: 20}},
},
}
runner := Runner{Client: client}
// 两个不同名称的 target 都会匹配到同一个奖品。
err := runner.RunTargets(context.Background(), []Account{{UID: 1, Cookie: "yyuid=1"}}, []Target{
{Name: "幸运币"},
{Name: "礼包"},
}, Config{
SID: 2277,
maxAttempts: 2,
Delay: time.Millisecond,
DirectExchange: false,
StopAccountAfterSuccess: true,
}, nil)
if err != nil {
t.Fatal(err)
}
_, exchangeCount, _ := client.counts()
if exchangeCount != 1 {
t.Fatalf("exchangeCount=%d, want 1(同一奖品不应重复兑换)", exchangeCount)
}
}
// TestRunTargetsSoldOutKeepsProbingUntilSuccess 验证「已抢完」不再默认终止:
// 库存可能因为别人取消 / 锁单过期回流,窗口内要继续探测到成功。
func TestRunTargetsSoldOutKeepsProbingUntilSuccess(t *testing.T) {
var mu sync.Mutex
calls := 0
client := &fakeActivityClient{
exchangeFn: func(uid int64, sid, pid int32) (*huya.ScoreExchangePrizeResp, error) {
mu.Lock()
calls++
current := calls
mu.Unlock()
if current <= 2 {
return &huya.ScoreExchangePrizeResp{Status: 500, Msg: "该奖品已抢完"}, nil
}
return &huya.ScoreExchangePrizeResp{Status: 200, Msg: "请求成功"}, nil
},
}
runner := Runner{Client: client}
err := runner.RunTargets(context.Background(), []Account{{UID: 1, Cookie: "yyuid=1"}}, []Target{{PID: 12889}}, Config{
SID: 2203,
maxAttempts: 6,
Delay: time.Millisecond,
ErrorBackoff: time.Millisecond,
StopAccountAfterSuccess: true,
}, nil)
if err != nil {
t.Fatal(err)
}
_, exchangeCount, _ := client.counts()
if exchangeCount != 3 {
t.Fatalf("exchangeCount=%d, want 3(售罄后应继续探测直到成功)", exchangeCount)
}
}
// TestRunTargetsStopOnSoldOutTerminatesTarget 验证显式开启 StopOnSoldOut 时售罄即停。
func TestRunTargetsStopOnSoldOutTerminatesTarget(t *testing.T) {
client := &fakeActivityClient{
exchangeFn: func(uid int64, sid, pid int32) (*huya.ScoreExchangePrizeResp, error) {
return &huya.ScoreExchangePrizeResp{Status: 500, Msg: "该奖品已抢完"}, nil
},
}
runner := Runner{Client: client}
err := runner.RunTargets(context.Background(), []Account{{UID: 1, Cookie: "yyuid=1"}}, []Target{{PID: 12889}}, Config{
SID: 2203,
maxAttempts: 5,
Delay: time.Millisecond,
ErrorBackoff: time.Millisecond,
StopOnSoldOut: true,
}, nil)
if err != nil {
t.Fatal(err)
}
_, exchangeCount, _ := client.counts()
if exchangeCount != 1 {
t.Fatalf("exchangeCount=%d, want 1(StopOnSoldOut 时售罄即停)", exchangeCount)
}
}
// TestRunTargetsOffShelfTerminatesTarget 验证下架/不存在仍然直接终止该 target。
func TestRunTargetsOffShelfTerminatesTarget(t *testing.T) {
client := &fakeActivityClient{
exchangeFn: func(uid int64, sid, pid int32) (*huya.ScoreExchangePrizeResp, error) {
return &huya.ScoreExchangePrizeResp{Status: 500, Msg: "奖品已下架"}, nil
},
}
runner := Runner{Client: client}
err := runner.RunTargets(context.Background(), []Account{{UID: 1, Cookie: "yyuid=1"}}, []Target{{PID: 12889}}, Config{
SID: 2203,
maxAttempts: 4,
Delay: time.Millisecond,
ErrorBackoff: time.Millisecond,
}, nil)
if err != nil {
t.Fatal(err)
}
_, exchangeCount, _ := client.counts()
if exchangeCount != 1 {
t.Fatalf("exchangeCount=%d, want 1(下架应终止该 target)", exchangeCount)
}
}
// TestRunTargetsConcurrencyLimitsInFlightRequests 验证并发额度作用在单次请求上:
// 在途请求数不会超过 Concurrency,但多个账号确实能同时在途。
func TestRunTargetsConcurrencyLimitsInFlightRequests(t *testing.T) {
var inFlight, peak int32
client := &fakeActivityClient{
exchangeFn: func(uid int64, sid, pid int32) (*huya.ScoreExchangePrizeResp, error) {
current := atomic.AddInt32(&inFlight, 1)
for {
observed := atomic.LoadInt32(&peak)
if current <= observed || atomic.CompareAndSwapInt32(&peak, observed, current) {
break
}
}
time.Sleep(10 * time.Millisecond)
atomic.AddInt32(&inFlight, -1)
return &huya.ScoreExchangePrizeResp{Status: 500, Msg: "操作频繁,请稍后再试"}, nil
},
}
runner := Runner{Client: client}
accounts := make([]Account, 0, 6)
for uid := int64(1); uid <= 6; uid++ {
accounts = append(accounts, Account{UID: uid, Cookie: "yyuid=1"})
}
err := runner.RunTargets(context.Background(), accounts, []Target{{PID: 12889}}, Config{
SID: 2203,
maxAttempts: 1,
Delay: time.Millisecond,
Concurrency: 2,
TooFrequentBackoff: 10 * time.Millisecond,
}, nil)
if err != nil {
t.Fatal(err)
}
if got := atomic.LoadInt32(&peak); got != 2 {
t.Fatalf("peak in-flight=%d, want 2(并发额度应精确限制在途请求数)", got)
}
}
// TestRunTargetsEmitsSummary 验证任务结束时汇总请求次数与结局分布。
func TestRunTargetsEmitsSummary(t *testing.T) {
client := &fakeActivityClient{}
var summary RunSummary
runner := Runner{
Client: client,
OnSummary: func(item RunSummary) { summary = item },
}
accounts := []Account{{UID: 1, Cookie: "yyuid=1"}, {UID: 2, Cookie: "yyuid=2"}}
// 异步刷新只在有 emit 回调时才跑(没地方上报就不发请求),这里给个空回调。
err := runner.RunTargets(context.Background(), accounts, []Target{{PID: 12889}}, Config{
SID: 2203,
maxAttempts: 3,
Delay: time.Millisecond,
Concurrency: 2,
StopAccountAfterSuccess: true,
RefreshAfterSuccess: true,
}, func(Result) {})
if err != nil {
t.Fatal(err)
}
if summary.Accounts != len(accounts) || summary.Targets != 1 {
t.Fatalf("summary=%+v", summary)
}
if summary.ExchangeReqs != len(accounts) {
t.Fatalf("ExchangeReqs=%d, want %d", summary.ExchangeReqs, len(accounts))
}
if summary.ScoreReqs != len(accounts) || summary.RecordReqs != len(accounts) {
t.Fatalf("刷新请求未计入汇总: score=%d records=%d", summary.ScoreReqs, summary.RecordReqs)
}
if summary.SuccessUIDs != len(accounts) || summary.Outcomes[CategorySuccess] != len(accounts) {
t.Fatalf("成功统计不正确: %+v", summary)
}
if summary.Duration <= 0 {
t.Fatalf("Duration=%v, want > 0", summary.Duration)
}
}
// TestJitterDurationAddsUpToQuarter 验证抖动只往上加、最多 25%,且不退化为 0。
func TestJitterDurationAddsUpToQuarter(t *testing.T) {
base := 400 * time.Millisecond
for i := 0; i < 200; i++ {
got := jitterDuration(base)
if got < base || got > base+base/4 {
t.Fatalf("jitterDuration(%v)=%v, want [%v,%v]", base, got, base, base+base/4)
}
}
if got := jitterDuration(0); got != 0 {
t.Fatalf("jitterDuration(0)=%v, want 0", got)
}
if got := jitterDuration(time.Nanosecond); got != time.Nanosecond {
t.Fatalf("jitterDuration(1ns)=%v, want 1ns", got)
}
}