feat(channelbridge): 冷避期唯一的放行口——高价值关键词穿透(不解除冷却)
塔 2026-10-15 例外口径定稿落地。M18 主体(冷避优先于「@她必回」)保持不动, 本提交只补冷却期内的例外与它的守卫。 - engage.go:`coldShoulderLocked` 在冷却期内放行**高价值关键词**这一条 (真故障不该被「让她少闲聊」一并压掉),且**不解除冷却**(不动 state / cooldownUntil); 同句既有负面态度又有高价值关键词时**负面态度优先**(仍进冷却、仍静默); 例外与顺序写进函数注释,并写明 owner 车道为什么不需要单独写例外(它在 shouldRespond 第一步就分岔,写在这里是死代码)。文档面 design/18 §5/§9 由塔同步。 - light.go:shouldRespond 的判定顺序与冷避闸注释同步该口径。 - 测试:新增 engage_coldshoulder_exception_test.go(冷却期例外矩阵:真故障放行、 @她/闲聊不穿透、穿透不解除冷却、同句负面态度优先;以及「冷却之外行为与基线一致」 的护栏),并按文件 ≤500 行拆出该文件;engage_coldshoulder_test.go 增共享夹具。 红前绿证据(参照 = 本提交的父 M18 提交):例外矩阵两条「真故障放行」行在我的改动 之前为红(冷避闸会把它们静默),其余三行与基线护栏在父提交上已绿,已如实标注为护栏; 「同句负面态度优先」一条另做变异自证(把例外插到负面态度判定之前 ⇒ 该行立刻变红)。 验收:go build ./... + go vet ./... + go test -count=1 ./... 全绿; channelbridge / channel / cmd/cyrene 单独跑绿;darwin/arm64 交叉编译通过。
This commit is contained in:
1 parent
fe53e029ea
commit
13a0519115
4 files changed
+177
-2
No files matched your search
@@ -0,0 +1,126 @@
|
||||
package channelbridge
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.yeij.top/AskaEth/Cyrene_v2/internal/channel/adapter"
|
||||
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/loop"
|
||||
)
|
||||
|
||||
// 本文件覆盖冷避**冷却期内的例外口径**(塔 2026-10-15 定稿;文档面 design/18 §5/§9
|
||||
// 由塔同步,这里只钉行为):
|
||||
//
|
||||
// ① 同句负面态度优先于例外:一句里既有「安静点」又有「报错」⇒ 仍进冷却、仍静默;
|
||||
// ② 高价值关键词(真故障)**穿透**冷却 —— 只放行这一条,且**不解除**冷却;
|
||||
// ③ @她 / @全体 **不穿透**(那正是主人 2026-10-14「给」的实质);
|
||||
// ④ 主人无需例外:owner 车道在 shouldRespond 第一步就分岔,走不到冷避闸。
|
||||
//
|
||||
// 冷避闸本体的顺序修正(M18 主体)与登记/词表/可见性等用例见
|
||||
// engage_coldshoulder_test.go(同包,共享 coldShoulderMention / coldShoulderBridge 夹具)。
|
||||
|
||||
// TestColdShoulderCooldownMatrix 是塔 2026-10-15 例外口径定稿后的**冷却期矩阵**:
|
||||
// 逐个钉住「谁能穿透、谁不能」,行序刻意把「穿透之后仍在冷却」紧排在高价值关键词行之后。
|
||||
//
|
||||
// 每行对「本轮修前」= M18 提交 fe53e02 的性质用 bite 标出(红前红 / 护栏)——
|
||||
// 不得把护栏吹成证据。
|
||||
func TestColdShoulderCooldownMatrix(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
cases := []struct {
|
||||
name string
|
||||
msg func() adapter.UnifiedMessage
|
||||
wantReply bool
|
||||
bite string
|
||||
}{
|
||||
{
|
||||
name: "fault keyword pierces the cooldown",
|
||||
msg: func() adapter.UnifiedMessage { return nonOwnerGroupMsg("服务报错了,谁知道怎么回事") },
|
||||
wantReply: true,
|
||||
bite: "本轮红前红(fe53e02 的冷避闸会把它静默掉,即「让她漏掉真故障」)",
|
||||
},
|
||||
{
|
||||
name: "fault keyword addressed to her pierces too",
|
||||
msg: func() adapter.UnifiedMessage { return coldShoulderMention("系统报错了,快看看") },
|
||||
wantReply: true,
|
||||
bite: "本轮红前红;例外②按**正文**判定,@她 不在排除项里(与裁定 ①的差别在于本条含真故障)",
|
||||
},
|
||||
{
|
||||
name: "cooldown is NOT lifted by the exception",
|
||||
msg: func() adapter.UnifiedMessage { return coldShoulderMention("晚上有空吗") },
|
||||
wantReply: false,
|
||||
bite: "护栏(fe53e02 亦绿):钉住穿透只放行那一条,不撤诉 ---- 本片的命根子",
|
||||
},
|
||||
{
|
||||
name: "plain chatter stays silenced",
|
||||
msg: func() adapter.UnifiedMessage { return nonOwnerGroupMsg("今晚吃点什么") },
|
||||
wantReply: false,
|
||||
bite: "护栏(fe53e02 亦绿)",
|
||||
},
|
||||
{
|
||||
name: "negative attitude wins inside the same message",
|
||||
msg: func() adapter.UnifiedMessage { return nonOwnerGroupMsg("别刷屏了,线上报错了") },
|
||||
wantReply: false,
|
||||
bite: "护栏(fe53e02 亦绿):钉住例外②不得被插到负面态度判定之前(对 base 是红)",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
tc := tc
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// 每行一张干净的桥接器、各自由真机原话①触发冷却,使各行的前状态完全一致。
|
||||
now := time.Date(2026, 10, 14, 0, 27, 0, 0, time.UTC)
|
||||
chat := &stubChatter{res: &loop.Result{Text: "好嘛"}}
|
||||
hist := &captureHistory{}
|
||||
b := coldShoulderBridge(t, chat, hist, func() time.Time { return now })
|
||||
|
||||
b.process(context.Background(), coldShoulderMention("减少文本输出量"))
|
||||
if chat.sessionID != "" {
|
||||
t.Fatalf("未能进入冷却:循环被驱动 = %q", chat.sessionID)
|
||||
}
|
||||
before := len(hist.records())
|
||||
|
||||
b.process(context.Background(), tc.msg())
|
||||
|
||||
if got := chat.sessionID != ""; got != tc.wantReply {
|
||||
t.Fatalf("冷却期内循环被驱动 = %v, want %v(%s)", got, tc.wantReply, tc.bite)
|
||||
}
|
||||
// 静默 ≠ 丢弃:本条必须已登记(被放行的那条在回合里另有一份落库)。
|
||||
if len(hist.records()) <= before {
|
||||
t.Fatalf("本条没有被登记:records 停在 %d", before)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestColdShoulderHighValueKeywordBaselineOutsideCooldown 是反例护栏(塔要求 2 的第 4 条):
|
||||
// 冷却期**已过**后,高价值关键词与普通消息的行为与基线完全一致 —— 前者照旧必回
|
||||
// (ruleLightJudge 的高价值关键词路径),后者照旧只登记(未装配引擎时失败关闭)。
|
||||
//
|
||||
// 诚实标注:本条对 base 与本轮修前**都是绿的**,它是**护栏而非红前红** ——
|
||||
// 它守的是「例外只改冷却期内,不碰冷却之外的既有语义」。
|
||||
func TestColdShoulderHighValueKeywordBaselineOutsideCooldown(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
now := time.Date(2026, 10, 14, 0, 27, 0, 0, time.UTC)
|
||||
chat := &stubChatter{res: &loop.Result{Text: "我看看"}}
|
||||
b := coldShoulderBridge(t, chat, &captureHistory{}, func() time.Time { return now })
|
||||
|
||||
b.process(context.Background(), coldShoulderMention("减少文本输出量")) // 进冷却
|
||||
now = now.Add(defaultCooldownDuration + time.Minute) // 推过冷却
|
||||
|
||||
chat.sessionID = ""
|
||||
b.process(context.Background(), nonOwnerGroupMsg("服务报错了"))
|
||||
if chat.sessionID != "sess-g" {
|
||||
t.Fatalf("冷却过后的高价值关键词未回:session = %q, want sess-g", chat.sessionID)
|
||||
}
|
||||
|
||||
chat.sessionID = ""
|
||||
b.process(context.Background(), nonOwnerGroupMsg("今晚吃点什么"))
|
||||
if chat.sessionID != "" {
|
||||
t.Fatalf("冷却过后的普通消息不该被驱动:session = %q", chat.sessionID)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user