- 适配器只描述平台身份:qq_obv11 的 owner/admin 一律上报 RoleMember,不再上报 RoleOwner - session 层唯一判主点只认白名单(owner.yaml Accounts ∪ 每渠道 adminIds),nil 档案 fail-closed 拒绝 - 新增「每渠道管理员层」:channels.json ChannelAccount.adminIds[] → runtimeconfig.AdminIDs → OwnerProfile.SetPlatformAdmins(整体替换语义,热重载后移除的 id 立即失效) - 组合根接线:启动即播种、面板保存先灌白名单再重载适配器(cmd/cyrene/wire_channels.go + 4 条端到端单测) - 06.4.3 节流与参与度状态按会话独立计数(ConversationKey 三处统一,空会话 GC);06.4.4 未知平台主动发言保守默认 1 条/分钟
198 lines
5.6 KiB
Go
198 lines
5.6 KiB
Go
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)")
|
|
}
|
|
}
|