Files
Cyrene_v2/internal/services/channelbridge/engage_session_test.go
T
AskaEth 7d9b2132bb test(testhealth): 替身竞态加锁 + 墙钟断言硬度(M33)
四处测试健康债务,只改 *_test.go,不动生产代码、不降并发:

- channelbridge:stubChatter / stubSessions 的跨 goroutine 字段一律经 mu 访问
  (Run/GetOrCreate 在车道 worker 上写、测试 goroutine 读,两者无 happens-before;
  负向断言「等一会儿再看循环有没有被驱动」尤其如此)。读写走访问器,Messages()
  返回副本;t.Parallel 与并发度不变。
- plugins/tools/io:claimProbeTimeout 2s(真实时间预算)→ 60s,并由 claimProbeWait
  按 t.Deadline 收缩。探针只等 goroutine 被调度(无时间语义),放大余量把
  「机器快慢」从断言里去掉,死锁仍以断言失败暴露。
- backthink:TestSelfWakeGate_WindowExpiry 窗口 120ms → 6s(给 waitFor 的 4s 上界
  留 2s 余量),到期等待改为按首次受理时刻推算 window+250ms,不再盲睡 3×窗口。
  另修 TestSelfWakeStatus_MatchesReceiptAndTracksPendingSlot:worker 先 take 再进
  Think,卡 Think 并不占待跑槽 —— 改为「上一轮把 worker 占住再断言 Pending」。
- memory:墙钟实测用例显式给闸 5s 预算(measureGateBudget),不再因默认 150ms
  超时 fail-open 打炸「每轮都真扫真判」前置断言;并把 GateFailed 纳入该断言。
2026-10-24 22:24:14 +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())
}
}