Files
Cyrene_v2/internal/channel/adapter/flowgate_direct_test.go
T
AskaEth 7ae4298871
CI / Backend (build / vet / test / cross-compile) (push) Has been cancelled
CI / Frontend (tsc + vite build) (push) Has been cancelled
feat(channels): 陌生人私聊正常聊天(跳概率门)+ 强制流控 6条/分与回合槽位 + 安静时段不再硬静默 + 受限车道边界行前置
- 陌生人私聊:不再走「值不值得回」的概率门,唯一闸门 = 每发送者 6 条/分滑窗 + 同会话 1 个在跑回合
- 被限流只登记不回,且留带 source 的可见告警(不静默)
- 安静时段在回复侧只作信号(规则层不再静默),群聊取消夜间静默
- 锁定流控常量:FlowDirectDefault 加字面量钉桩测试(放大它 = 静默关流控,且会让额度循环跑百万轮)
2026-10-05 13:26:15 +08:00

105 lines
3.7 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 adapter
// 本文件覆盖主人 2026-10-05 裁决③的**陌生人私聊流控**两件套:
// - AllowDirect:私聊按发送者(会话键 platform+channel+sender)的每分钟滑窗;
// - AcquireTurn/ReleaseTurn:同一发送者同时最多 1 个在跑的回合。
// 两者都复用同一张 flowSession 表与同一套 ConversationKey 口径,不另造第二套限流。
import (
"testing"
"time"
)
// TestFlowGate_AllowDirectIsPerSender 钉住私聊滑窗按发送者独立计数:
// A 用尽额度不挤占 B;窗口滚过后 A 恢复;群聊消息不经这条通道。
func TestFlowGate_AllowDirectIsPerSender(t *testing.T) {
t.Parallel()
now, clock := fixedClock(time.Unix(2000, 0))
var audited []string
g := NewFlowGate(FlowGateOptions{
DirectMsgPerMin: 2,
Window: time.Minute,
Now: now,
OnPruned: func(_ UnifiedMessage, reason string) { audited = append(audited, reason) },
})
for i := 0; i < 2; i++ {
if !g.AllowDirect(directMsgTo("peer-1", i)) {
t.Fatalf("peer-1 第 %d 条被拒,want allowed", i+1)
}
}
if g.AllowDirect(directMsgTo("peer-1", 2)) {
t.Fatal("peer-1 超出自身额度仍被放行")
}
if len(audited) != 1 || audited[0] != "rate_limited" {
t.Fatalf("拒绝未入审计:%v", audited)
}
if !g.AllowDirect(directMsgTo("peer-2", 0)) {
t.Fatal("peer-2 被 peer-1 的额度挤占(私聊限流未按发送者独立)")
}
// 群聊走 Admit 的群聊滑窗,AllowDirect 对它恒放行。
if !g.AllowDirect(groupMsgIn("-100", 0)) {
t.Fatal("群聊消息不该被私聊限流拦下")
}
*clock = clock.Add(61 * time.Second)
if !g.AllowDirect(directMsgTo("peer-1", 3)) {
t.Fatal("窗口滚过后 peer-1 应恢复放行")
}
}
// TestFlowGate_AllowDirectDisabledByZero 钉住零值 = 关闭:没有配置私聊限流时
// (DirectMsgPerMin 为 0)AllowDirect 恒放行,既有调用行为不变。
func TestFlowGate_AllowDirectDisabledByZero(t *testing.T) {
t.Parallel()
g := NewFlowGate(FlowGateOptions{})
for i := 0; i < 50; i++ {
if !g.AllowDirect(directMsgTo("peer-1", i)) {
t.Fatalf("零值额度下第 %d 条被拒,want allowed", i+1)
}
}
}
// TestFlowDirectDefaultIsPinned 用**字面量 6** 钉住保护性默认值(与 FlowLimitDefault
// 的既有钉桩同口径)。不这么钉的后果:`stranger_test.go` 的额度循环以该常量为上界,
// 一旦有人把它改成近似无限,测试不会断言失败,而是**跑一百万轮把整包测试拖到超时**
// ——看起来像「偶发卡死」,实际是流控被静默关掉。主人 2026-10-05 裁决③要求陌生人
// 私聊**必须有**流控,因此这条默认值属于契约,不允许随手放大。
func TestFlowDirectDefaultIsPinned(t *testing.T) {
t.Parallel()
if FlowDirectDefault != 6 {
t.Fatalf("FlowDirectDefault = %d, want 6(每发送者每分钟 6 条的保护性默认;"+
"放大它等于静默关闭陌生人私聊流控)", FlowDirectDefault)
}
}
// TestFlowGate_AcquireTurnSinglePerConversation 钉住「同一会话同时最多 1 个在跑的回合」:
// 第一个占位成功、第二个被拒;换一个发送者互不影响;释放后又能占。
func TestFlowGate_AcquireTurnSinglePerConversation(t *testing.T) {
t.Parallel()
g := NewFlowGate(FlowGateOptions{})
a := directMsgTo("peer-1", 0)
b := directMsgTo("peer-2", 0)
if !g.AcquireTurn(a) {
t.Fatal("首个回合应能占到槽位")
}
if g.AcquireTurn(a) {
t.Fatal("同一发送者的第二个回合不该同时占到槽位")
}
if !g.AcquireTurn(b) {
t.Fatal("另一个发送者的槽位被挤占")
}
g.ReleaseTurn(a)
if !g.AcquireTurn(a) {
t.Fatal("释放后应能重新占到槽位")
}
g.ReleaseTurn(a)
g.ReleaseTurn(b)
}