主人 2026-10-13 裁决「乙 C」= 默认按甲(群里不对她说就不开口)+ 可调旋钮。 - inbound.go: owner 不再无条件短路参与闸(去掉 `!actor.IsOwner()` 门), 主人群消息改走 shouldRespond 的 owner 车道分支;主人私聊恒回。 - ownerlane.go(新): owner 车道结构化闸 + 判据 `msg.AddressedToMe || (msg.SelfID == "" && len(msg.Mentions) > 0)` (未接线渠道退回旧判据,保护 kook/webhook 的群 @);指向她本人必回(硬保留); 非指向消息按每群旋钮——默认只进上下文不回(registerSilenced 落隔离会话)。 「少说点」类关键词落成结构化降频配置,带 source 日志。 - channelbridge.go: Options.OwnerLaneFor/OwnerLaneSet 读写接缝 + Bridge 状态。 - engage.go: 冷避词表并入「少说点」(与既有「少说两句」同义)。 - runtimeconfig/channels_ownerlane.go(新): owner_lane.json 落盘面 (默认档 + 每群覆盖,可读可改,面板可写)。 - wire_channels.go: 注入 owner_lane.json 存储($DATA_DIR/config/)。 - 测试:不对她说就不回(进上下文零回复)/ @她必回(即使降频)/ 未接线退回 旧判据 / 降频生效且可恢复 + 频率闸 / 旋钮读写接缝 / 归属不变量闭合 / 陌生人私聊与主人私聊回归。变异自证:恢复 owner 短路 ⇒ 5 条用例红。
82 lines
2.9 KiB
Go
82 lines
2.9 KiB
Go
package runtimeconfig
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
)
|
|
|
|
// TestOwnerLaneStoreDefaultsWhenFileMissing 钉住默认档(裁决「甲」):
|
|
// 文件不存在时读到的就是最保守的一档 —— 不对她说就不开口,且没有额外频率闸。
|
|
func TestOwnerLaneStoreDefaultsWhenFileMissing(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
path := filepath.Join(t.TempDir(), "config", "owner_lane.json")
|
|
s := NewOwnerLaneStore(path, nil)
|
|
|
|
got := s.PolicyFor("qq-obv11", "672432767")
|
|
if got.ReplyToUnaddressed || got.MinReplyGapSec != 0 {
|
|
t.Fatalf("默认档 = %+v, want 零值(不对她说就不开口)", got)
|
|
}
|
|
}
|
|
|
|
// TestOwnerLaneStoreSetPersistsAndReloads 钉住「可读可改」:Set 落盘后,新建的
|
|
// store 能读回同一个每群覆盖;未覆盖的群仍走全局 default。
|
|
func TestOwnerLaneStoreSetPersistsAndReloads(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
path := filepath.Join(t.TempDir(), "config", "owner_lane.json")
|
|
s := NewOwnerLaneStore(path, nil)
|
|
want := OwnerLanePolicy{ReplyToUnaddressed: true, MinReplyGapSec: 120}
|
|
if err := s.Set("qq-obv11", "672432767", want); err != nil {
|
|
t.Fatalf("Set: %v", err)
|
|
}
|
|
if _, err := os.Stat(path); err != nil {
|
|
t.Fatalf("owner_lane.json 未落盘:%v", err)
|
|
}
|
|
|
|
reloaded := NewOwnerLaneStore(path, nil)
|
|
if got := reloaded.PolicyFor("qq-obv11", "672432767"); got != want {
|
|
t.Fatalf("重载后该群策略 = %+v, want %+v", got, want)
|
|
}
|
|
// 未被覆盖的群仍走全局 default(零值)。
|
|
if got := reloaded.PolicyFor("qq-obv11", "another-group"); got != DefaultOwnerLanePolicy() {
|
|
t.Fatalf("未覆盖群策略 = %+v, want 默认档", got)
|
|
}
|
|
}
|
|
|
|
// TestOwnerLaneStoreGlobalDefault 钉住面板可改的「全局 default」那一档:
|
|
// groupID 为空即写默认,所有未单独覆盖的群都跟着变。
|
|
func TestOwnerLaneStoreGlobalDefault(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
s := NewOwnerLaneStore(filepath.Join(t.TempDir(), "config", "owner_lane.json"), nil)
|
|
global := OwnerLanePolicy{ReplyToUnaddressed: true, MinReplyGapSec: 60}
|
|
if err := s.Set("", "", global); err != nil {
|
|
t.Fatalf("Set default: %v", err)
|
|
}
|
|
if got := s.PolicyFor("telegram", "-20"); got != global {
|
|
t.Fatalf("全局默认生效值 = %+v, want %+v", got, global)
|
|
}
|
|
}
|
|
|
|
// TestOwnerLaneStoreCorruptFileFallsBackToDefaults 钉住「配置坏了也不阻断」:
|
|
// 畸形文件只退回默认档并留 WARN,绝不 panic、绝不返回错误阻断渠道启动。
|
|
func TestOwnerLaneStoreCorruptFileFallsBackToDefaults(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
dir := t.TempDir()
|
|
path := filepath.Join(dir, "config", "owner_lane.json")
|
|
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
|
|
t.Fatalf("mkdir: %v", err)
|
|
}
|
|
if err := os.WriteFile(path, []byte("{ this is not json "), 0o600); err != nil {
|
|
t.Fatalf("write: %v", err)
|
|
}
|
|
|
|
s := NewOwnerLaneStore(path, nil)
|
|
if got := s.PolicyFor("qq-obv11", "g"); got != DefaultOwnerLanePolicy() {
|
|
t.Fatalf("坏配置下策略 = %+v, want 默认档", got)
|
|
}
|
|
}
|