package gui import ( "testing" "time" "huya-go/internal/logger" ) func TestSplitCookieLinesNormalizesLineBreaks(t *testing.T) { tests := []struct { name string input string want []string }{ { name: "crlf_and_cr", input: "ck-a\r\nck-b\rck-c", want: []string{"ck-a", "ck-b", "ck-c"}, }, { name: "escaped_line_breaks", input: `ck-a\r\nck-b\rck-c\nck-d`, want: []string{"ck-a", "ck-b", "ck-c", "ck-d"}, }, { name: "slash_r_line_breaks", input: "ck-a/r/nck-b/rck-c/nck-d", want: []string{"ck-a", "ck-b", "ck-c", "ck-d"}, }, { name: "slash_n_only_is_preserved", input: "ck-a/nck-b", want: []string{"ck-a/nck-b"}, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { lines := splitCookieLines(tt.input) if len(lines) != len(tt.want) { t.Fatalf("len(splitCookieLines()) = %d, want %d", len(lines), len(tt.want)) } for i, line := range lines { if line.text != tt.want[i] { t.Fatalf("line[%d] = %q, want %q", i, line.text, tt.want[i]) } } }) } } func TestParseAccountsRequiresExplicitSelection(t *testing.T) { cfg := Config{ CookieText: "udb_uid=1; yyuid=1\nudb_uid=2; yyuid=2", } if _, err := parseAccounts(cfg); err == nil { t.Fatal("空选择不应回退为全部账号") } cfg.SelectedAccountLines = []int{2} accounts, err := parseAccounts(cfg) if err != nil { t.Fatalf("parseAccounts() error = %v", err) } if len(accounts) != 1 || accounts[0].UID != 2 { t.Fatalf("accounts=%+v, want 仅第 2 行账号", accounts) } } func TestDefaultConfigUsesWindowMode(t *testing.T) { cfg := (&App{}).DefaultConfig() if cfg.LeadMS != 0 { t.Fatalf("LeadMS=%d, want 0(动态提前量)", cfg.LeadMS) } if cfg.Concurrency != 2 { t.Fatalf("Concurrency=%d, want 2(降低多开时的请求洪峰)", cfg.Concurrency) } if !cfg.Prewarm { t.Fatal("Prewarm 应默认开启(提前 10 秒异步预热,不阻塞发令枪)") } } func TestParseStartTimeAcceptsClockOnly(t *testing.T) { t.Run("future_today", func(t *testing.T) { now := time.Date(2026, 7, 6, 10, 20, 30, 0, time.Local) got := nextClockTime(now, 12, 5, 6) want := time.Date(2026, 7, 6, 12, 5, 6, 0, time.Local) if !got.Equal(want) { t.Fatalf("nextClockTime() = %s, want %s", got, want) } }) t.Run("past_tomorrow", func(t *testing.T) { now := time.Date(2026, 7, 6, 10, 20, 30, 0, time.Local) got := nextClockTime(now, 9, 5, 6) want := time.Date(2026, 7, 7, 9, 5, 6, 0, time.Local) if !got.Equal(want) { t.Fatalf("nextClockTime() = %s, want %s", got, want) } }) t.Run("parse_hms", func(t *testing.T) { got, err := parseStartTime("23:59:58") if err != nil { t.Fatalf("parseStartTime() error = %v", err) } if got.Hour() != 23 || got.Minute() != 59 || got.Second() != 58 { t.Fatalf("parseStartTime() = %s, want 23:59:58", got) } }) } func TestSetLogLevelNormalizesAndFilters(t *testing.T) { dir := t.TempDir() store := logger.NewStore(20) closeLog, err := logger.Init(store, dir, "测试实例", "test", dir) if err != nil { t.Fatalf("logger.Init() error = %v", err) } defer closeLog() defer logger.SetMinLevel(logger.LevelInfo) app := &App{logStore: store} if got := app.SetLogLevel("这不是合法等级"); got != logger.LevelInfo { t.Fatalf("非法等级应回退为 info, got %q", got) } if got := app.SetLogLevel("debug"); got != logger.LevelDebug { t.Fatalf("SetLogLevel(debug) = %q", got) } logger.LogEvent("网络", logger.LevelDebug, "debug 应被保留") app.SetLogLevel("error") logger.LogEvent("网络", logger.LevelDebug, "debug 应被过滤") for _, entry := range store.Since(0) { if entry.Message == "debug 应被过滤" { t.Fatal("低于最低等级的日志不应进入内存缓冲") } } } func TestParseTargetsAppliesPriceAndInventoryFilters(t *testing.T) { targets := parseTargets(Config{ PID: 12889, Name: "小黄鸭", TargetText: "13001\nauto", AutoSelect: true, MaxPrice: 1000, MinLeft: 2, }) if len(targets) != 4 { t.Fatalf("targets=%d, want 4", len(targets)) } for _, target := range targets { if target.MaxPrice != 1000 { t.Fatalf("target %+v maxPrice=%d, want 1000", target, target.MaxPrice) } if target.MinLeft != 2 { t.Fatalf("target %+v minLeft=%d, want 2", target, target.MinLeft) } } } func TestParseTargetsWithoutFiltersKeepsZeroValues(t *testing.T) { targets := parseTargets(Config{PID: 12889}) if len(targets) != 1 { t.Fatalf("targets=%d, want 1", len(targets)) } if targets[0].MaxPrice != 0 || targets[0].MinLeft != 0 { t.Fatalf("unexpected filters: %+v", targets[0]) } } // TestRunnerConfigMapsDirectExchangeToExchangeMode 验证界面「直接兑换」开关能真正 // 切换到「先查列表」模式:引擎在 ListBeforeExchange=false 时会强制回落到直兑, // 所以关闭直接兑换必须同时置 ListBeforeExchange,否则开关形同失效。 func TestRunnerConfigMapsDirectExchangeToExchangeMode(t *testing.T) { direct := runnerConfig(Config{ SID: 2203, DelayMS: 120, Concurrency: 5, DirectExchange: true, }) if !direct.DirectExchange || direct.ListBeforeExchange { t.Fatalf("直接兑换模式映射错误: %+v", direct) } if direct.SID != 2203 || direct.Delay != 120*time.Millisecond || direct.Concurrency != 5 { t.Fatalf("基础字段未透传: %+v", direct) } list := runnerConfig(Config{SID: 2203, DirectExchange: false}) if list.DirectExchange || !list.ListBeforeExchange { t.Fatalf("关闭直接兑换后应改为先查列表: %+v", list) } }