package session import ( "context" "testing" "time" "git.yeij.top/AskaEth/Cyrene_v2/internal/channel/adapter" ) func capsProactive() adapter.Capabilities { return adapter.Capabilities{Proactive: true} } func capsNoProactive() adapter.Capabilities { return adapter.Capabilities{Proactive: false} } func TestProactivePolicyCapabilityGate(t *testing.T) { t.Parallel() p := NewProactivePolicy(ProactiveOptions{}) // A channel that does not declare proactive support is denied regardless of // how fresh the last message is. if p.Allow(context.Background(), capsNoProactive(), time.Now()) { t.Error("Allow() = true with Proactive=false, want false") } } func TestProactivePolicyWindowWithin(t *testing.T) { t.Parallel() p := NewProactivePolicy(ProactiveOptions{Window: time.Hour}) last := time.Now().Add(-30 * time.Minute) if !p.Allow(context.Background(), capsProactive(), last) { t.Error("Allow() = false within window, want true") } } func TestProactivePolicyWindowBeyond(t *testing.T) { t.Parallel() p := NewProactivePolicy(ProactiveOptions{Window: time.Hour}) last := time.Now().Add(-90 * time.Minute) if p.Allow(context.Background(), capsProactive(), last) { t.Error("Allow() = true beyond window, want false") } } func TestProactivePolicyWindowZeroLastMessage(t *testing.T) { t.Parallel() p := NewProactivePolicy(ProactiveOptions{Window: time.Hour}) if p.Allow(context.Background(), capsProactive(), time.Time{}) { t.Error("Allow() = true with no last message, want false") } } func TestProactivePolicyRateLimit(t *testing.T) { t.Parallel() p := NewProactivePolicy(ProactiveOptions{RateLimit: 3, RateWindow: 10 * time.Second}) ctx := context.Background() for i := 1; i <= 3; i++ { if !p.Allow(ctx, capsProactive(), time.Now()) { t.Fatalf("Allow() denied on attempt %d, want allowed under limit", i) } } if p.Allow(ctx, capsProactive(), time.Now()) { t.Error("Allow() allowed past burst limit, want denied") } } func TestProactivePolicyUnrestrictedChannel(t *testing.T) { t.Parallel() // telegram/webhook-like channel: proactive capability on, no window, no // rate limit → always allowed. p := NewProactivePolicy(ProactiveOptions{}) old := time.Now().Add(-48 * time.Hour) for i := 0; i < 5; i++ { if !p.Allow(context.Background(), capsProactive(), old) { t.Fatalf("Allow() denied on unrestricted channel, want true") } } } func TestProactivePolicyForPlatform(t *testing.T) { t.Parallel() ctx := context.Background() iLink := ProactivePolicyForPlatform("weixin-ilink") now := time.Now() if !iLink.Allow(ctx, capsProactive(), now.Add(-23*time.Hour)) { t.Error("weixin-ilink allowed only within 24h; 23h old should allow") } if iLink.Allow(ctx, capsProactive(), now.Add(-25*time.Hour)) { t.Error("weixin-ilink 25h old last message should deny") } oneBot := ProactivePolicyForPlatform("qq-obv11") for i := 0; i < oneBotBurstLimit; i++ { if !oneBot.Allow(ctx, capsProactive(), now) { t.Fatalf("onebot denied on attempt %d, want allowed", i+1) } } if oneBot.Allow(ctx, capsProactive(), now) { t.Error("onebot allowed past burst limit, want denied") } tg := ProactivePolicyForPlatform("telegram") if !tg.Allow(ctx, capsProactive(), now.Add(-72*time.Hour)) { t.Error("telegram must push freely, want true") } } // TestProactivePolicyForUnknownPlatformIsConservative 是裁决 06.4.4 的断言:未知平台 // (能力未声明、不在平台清单里)**不得**拿到「无限制」——默认给最严格的限频。 func TestProactivePolicyForUnknownPlatformIsConservative(t *testing.T) { t.Parallel() ctx := context.Background() now := time.Now() for _, platform := range []string{"mystery-im", "discord", ""} { p := ProactivePolicyForPlatform(platform) if p == nil { t.Fatalf("ProactivePolicyForPlatform(%q) = nil", platform) } if p.limiter == nil { t.Fatalf("未知平台 %q 拿到了无限制策略,违反裁决 06.4.4", platform) } for i := 0; i < unknownBurstLimit; i++ { if !p.Allow(ctx, capsProactive(), now) { t.Fatalf("未知平台 %q 第 %d 条被拒,want allowed(限频内)", platform, i+1) } } if p.Allow(ctx, capsProactive(), now) { t.Errorf("未知平台 %q 超出保守限频仍被放行", platform) } } } func TestProactivePolicyNilPolicyAndCanceledCtx(t *testing.T) { t.Parallel() var p *ProactivePolicy if p.Allow(context.Background(), capsProactive(), time.Now()) { t.Error("nil policy Allow() = true, want false (fail closed)") } ctx, cancel := context.WithCancel(context.Background()) cancel() policy := NewProactivePolicy(ProactiveOptions{}) if policy.Allow(ctx, capsProactive(), time.Now()) { t.Error("Allow() with canceled ctx = true, want false") } } func TestRateLimiterSlidingWindow(t *testing.T) { t.Parallel() l := NewRateLimiter(2, 50*time.Millisecond) if !l.Allow() { t.Fatal("first Allow() = false, want true") } if !l.Allow() { t.Fatal("second Allow() = false, want true") } if l.Allow() { t.Fatal("third Allow() = true, want false (limit reached)") } time.Sleep(60 * time.Millisecond) if !l.Allow() { t.Fatal("Allow() after window elapsed = false, want true") } } func TestRateLimiterDisabledConfig(t *testing.T) { t.Parallel() // A limiter with no limit/window configured is the safe permissive default. configs := []*RateLimiter{ NewRateLimiter(0, 0), NewRateLimiter(-1, time.Second), NewRateLimiter(0, time.Second), } for i, l := range configs { if !l.Allow() || !l.Allow() { t.Errorf("config %d: Allow() = false, want true (unlimited)", i) } } if !(*RateLimiter)(nil).Allow() { t.Error("nil rate limiter Allow() = false, want true (permissive)") } }