Files
Cyrene_v2/internal/services/channelbridge/engage_session_test.go
T
AskaEth af8591b78a fix(channel,user): owner 车道只认白名单,堵住群主/伪造 role 冒充主人(裁决 06.4.2/06.4.3/06.4.4)
- 适配器只描述平台身份: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 条/分钟
2026-10-04 12:07:40 +08:00

82 lines
3.2 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 channelbridge
// 本文件覆盖裁决 06.4.3 在参与度引擎侧的断言:会话状态键与入站节流键是同一个口径,
// 且每个会话的参与度状态互相独立——一个群冷场/参与中不影响另一个群。
import (
"context"
"testing"
"git.yeij.top/AskaEth/Cyrene_v2/internal/channel/adapter"
)
// TestEngageKeyMatchesThrottleKey 固定「成对契约」:参与度引擎的状态键与入站节流键
// (adapter.ConversationKey)必须同口径,否则同一会话会在「计数」与「状态机」里分裂成
// 两个桶(裁决 06.4.3)。
func TestEngageKeyMatchesThrottleKey(t *testing.T) {
t.Parallel()
msg := nonOwnerGroupMsg("hi")
msg.ChannelID, msg.GroupID = "-100", "-100"
actor := adapter.Actor{Role: adapter.RoleMember, IsGroup: true, GroupID: "-100"}
if got, want := engageKey(msg, actor), adapter.ConversationKey(msg); got != want {
t.Errorf("engageKey = %q, 节流键 = %q, 两者必须同口径", got, want)
}
// 群聊缺 group id 时用 actor 上报的群 id 补齐后仍与节流键一致。
bare := nonOwnerGroupMsg("hi")
bare.ChannelID, bare.GroupID = "", ""
patchActor := adapter.Actor{Role: adapter.RoleMember, IsGroup: true, GroupID: "-20"}
patched := bare
patched.GroupID = "-20"
if got, want := engageKey(bare, patchActor), adapter.ConversationKey(patched); got != want {
t.Errorf("缺 group id 时 engageKey = %q, 期望 %q", got, want)
}
// 两个群必须是两个键。
other := nonOwnerGroupMsg("hi")
other.ChannelID, other.GroupID = "-200", "-200"
if engageKey(msg, actor) == engageKey(other, actor) {
t.Error("两个群共用同一个参与度状态键(会话未隔离)")
}
}
// TestEngageStateIsPerConversation 断言两个群并发互不挤占:A 群被要求安静进入 COOLDOWN
// 后,B 群仍能正常触发判定并回复(状态与计数都按会话独立,裁决 06.4.3)。
func TestEngageStateIsPerConversation(t *testing.T) {
t.Parallel()
judge := &engageScriptJudge{responses: []LightDecision{
{WorthReply: true, Interesting: true, Reason: "想聊"},
}}
b, chat, _ := engageTestBridge(t, judge, DefaultEngageConfig(), dayNow(), nil)
ctx := context.Background()
// A 群冷场:一句「太吵了」把 A 群打进 COOLDOWN,且不消耗判定。
cold := nonOwnerGroupMsg("太吵了,闭嘴")
cold.ChannelID, cold.GroupID = "-100", "-100"
b.process(ctx, cold)
if judge.calls != 0 {
t.Fatalf("冷场消息触发了轻判调用 = %d, want 0", judge.calls)
}
// A 群的后续触发消息处于 COOLDOWN:必须不调判定、不回复。
groupA := nonOwnerGroupMsg("昔涟 你来说说")
groupA.ChannelID, groupA.GroupID = "-100", "-100"
b.process(ctx, groupA)
if judge.calls != 0 {
t.Fatalf("A 群 COOLDOWN 期间仍调用了判定 = %d, want 0", judge.calls)
}
// B 群不受 A 群影响:照常 PROBING → 判定 → 回复。
groupB := nonOwnerGroupMsg("昔涟 你来说说")
groupB.ChannelID, groupB.GroupID = "-200", "-200"
b.process(ctx, groupB)
if judge.calls != 1 {
t.Fatalf("B 群判定被 A 群 COOLDOWN 挤占,judge.calls = %d, want 1", judge.calls)
}
if chat.sessionID != "sess-g" {
t.Fatalf("B 群判定为想聊后应驱动 loop,session = %q", chat.sessionID)
}
}