diff --git a/internal/services/automations/reminder.go b/internal/services/automations/reminder.go index 500ea91..fbbbee1 100644 --- a/internal/services/automations/reminder.go +++ b/internal/services/automations/reminder.go @@ -9,6 +9,7 @@ import ( "time" "git.yeij.top/AskaEth/Cyrene_v2/internal/bus" + "git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/trace" ) const ( @@ -206,6 +207,6 @@ func (rs *ReminderStore) fireDue(ctx context.Context) { rs.log.Error("publish reminder", "id", r.ID, "err", err) continue } - rs.log.Info("reminder fired", "id", r.ID, "at", r.At, "message", r.Message) + rs.log.Info("reminder fired", "id", r.ID, "at", r.At, "message", trace.RedactText(r.Message)) } } diff --git a/internal/services/automations/reminder_test.go b/internal/services/automations/reminder_test.go index 0d23a27..1a8952a 100644 --- a/internal/services/automations/reminder_test.go +++ b/internal/services/automations/reminder_test.go @@ -1,8 +1,11 @@ package automations import ( + "bytes" "context" "errors" + "log/slog" + "strings" "testing" "time" @@ -166,3 +169,70 @@ func TestService_ReminderLifecycle(t *testing.T) { t.Fatalf("CancelReminder: %v", err) } } + +// TestReminder_FireLogIsRedacted 钉住提醒触发日志的出文本口径:Message 是模型/ +// 用户自由文本,可能顺手带着凭据(「把 api_key=… 发我」)。日志面必须打码, +// 而总线上的 payload 是**投递面**,必须原样保留 —— 打码不能改投递内容。 +func TestReminder_FireLogIsRedacted(t *testing.T) { + t.Parallel() + const secret = "sk-live-9f8e7d6c5b4a3210" + msg := "把 api_key=" + secret + " 发到群里" + + var logBuf bytes.Buffer + b := bus.New() + defer b.Close() + + got := make(chan bus.Event, 1) + unsub, err := b.Subscribe(context.Background(), SubjectTrigger, func(_ context.Context, ev bus.Event) error { + got <- ev + return nil + }) + if err != nil { + t.Fatalf("subscribe: %v", err) + } + defer unsub() + + rs, err := NewReminder(Config{ReminderInterval: 10 * time.Millisecond}, Deps{ + Bus: b, + Log: slog.New(slog.NewTextHandler(&logBuf, nil)), + }) + if err != nil { + t.Fatalf("NewReminder: %v", err) + } + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + if err := rs.Add(ctx, Reminder{ID: "leak", At: time.Now().Add(10 * time.Millisecond), Message: msg}); err != nil { + t.Fatalf("Add: %v", err) + } + if err := rs.Start(ctx); err != nil { + t.Fatalf("Start: %v", err) + } + + select { + case ev := <-got: + rem, ok := ev.Payload.(Reminder) + if !ok { + t.Fatalf("payload type = %T, want Reminder", ev.Payload) + } + if rem.Message != msg { + t.Fatalf("总线 payload 的 message 被改动: %q, want %q", rem.Message, msg) + } + case <-time.After(3 * time.Second): + t.Fatal("reminder did not fire") + } + // Stop 等轮询 goroutine 退出,触发日志必然已写完,避免与后台写缓冲竞争。 + if err := rs.Stop(ctx); err != nil { + t.Fatalf("Stop: %v", err) + } + + out := logBuf.String() + if strings.Contains(out, secret) { + t.Fatalf("提醒日志里出现了凭据原文:%s", out) + } + for _, want := range []string{"reminder fired", "[redacted]"} { + if !strings.Contains(out, want) { + t.Fatalf("提醒日志 = %q,want 含 %q(打码不能把日志变哑)", out, want) + } + } +}