package automations import ( "bytes" "context" "log/slog" "strings" "testing" "git.yeij.top/AskaEth/Cyrene_v2/internal/bus" ) // 本文件钉住 M40 的自动化侧写日志边界:`AddRuleByName` 造的规则动作曾经把整个 // 触发 payload 用 %v 打进日志。绑到 `ws.chat` / `subagent.result` 这类 subject // 上时,payload 里就是**她的正文** —— 于是「一条自动化触发」的日志会原样抄走一次 // 对话。 // // 现在日志只留触发面的元数据:规则名、订阅的 subject、payload 的类型/体积/指纹。 // 🔴 断言打在**日志输出字节**上(真 slog handler → 缓冲区),不打在调用参数上。 // TestAddRuleByName_DoesNotLogPayloadBody 触发一次事件型自动化,检查日志里 // 没有正文,但仍有可诊断的类型/体积。 func TestAddRuleByName_DoesNotLogPayloadBody(t *testing.T) { const sentinel = "SENTINEL-CHAT-BODY-88ce-她在聊天框里说的话" var logBuf bytes.Buffer log := slog.New(slog.NewTextHandler(&logBuf, &slog.HandlerOptions{})) b := bus.New() t.Cleanup(func() { _ = b.Close() }) svc, err := NewService(Config{}, Deps{Bus: b, Log: log}) if err != nil { t.Fatalf("NewService: %v", err) } id, err := svc.AddRuleByName(context.Background(), "chat-mirror", string(TriggerEvent), "ws.chat") if err != nil { t.Fatalf("AddRuleByName: %v", err) } rule, ok := ruleByID(svc.Engine().Rules(), id) if !ok { t.Fatalf("规则 %q 没有注册到引擎上", id) } payload := map[string]any{"session_id": "s1", "text": sentinel} if _, err := rule.Action.Execute(context.Background(), payload); err != nil { t.Fatalf("Execute: %v", err) } out := logBuf.String() if strings.Contains(out, sentinel) { t.Fatalf("自动化日志里出现了触发 payload 正文:%s", out) } for _, want := range []string{"admin automation fired", "name=chat-mirror", "subject=ws.chat", "payload_bytes=", "payload_sha256="} { if !strings.Contains(out, want) { t.Fatalf("日志 = %q,want 含 %q(脱敏不能把日志变哑)", out, want) } } // 类型名同样是可诊断信息(哪条路上跑的是什么消息)。 if !strings.Contains(out, "payload_type=") || !strings.Contains(out, "map") { t.Fatalf("日志 = %q,want payload_type=map…", out) } } // TestAddRuleByName_NilPayloadStaysQuiet 钉住 cron 侧(payload 为 nil):日志 // 仍成立,不因 nil 而 panic 或退化成 %v 打印。 func TestAddRuleByName_NilPayloadStaysQuiet(t *testing.T) { var logBuf bytes.Buffer log := slog.New(slog.NewTextHandler(&logBuf, nil)) b := bus.New() t.Cleanup(func() { _ = b.Close() }) svc, err := NewService(Config{}, Deps{Bus: b, Log: log}) if err != nil { t.Fatalf("NewService: %v", err) } id, err := svc.AddRuleByName(context.Background(), "nightly", string(TriggerCron), "@daily") if err != nil { t.Fatalf("AddRuleByName: %v", err) } rule, ok := ruleByID(svc.Engine().Rules(), id) if !ok { // cron 规则注册在调度器上,直接按名字从服务侧的动作跑一次即可。 out, err := svc.RunRule(context.Background(), id) if err != nil { t.Fatalf("RunRule: %v", err) } if out == nil { t.Fatal("RunRule 没有返回输出") } } else if _, err := rule.Action.Execute(context.Background(), nil); err != nil { t.Fatalf("Execute: %v", err) } if out := logBuf.String(); !strings.Contains(out, "payload_type=nil") { t.Fatalf("日志 = %q,want payload_type=nil", out) } } // ruleByID 在规则快照里按 id 找一条。 func ruleByID(rules []Rule, id string) (Rule, bool) { for _, r := range rules { if r.ID == id { return r, true } } return Rule{}, false }