package channelbridge // 本文件钉住 judgeReason 的三个使用者(engage.go / light.go / notice.go)都真的走它。 // engage.go 那 7 处由 engage_reason_test.go 覆盖;这里覆盖后两处与 helper 本身。 // // 变异判据(每条都对应一个会红的实现): // - 把 judgeReason 换成恒等返回(摘掉 trace.RedactTextLimited)⇒ 三条用例都红; // - 只做 RedactText 不做 strings.Fields 压平 ⇒ helper 那条与 light 那条红(换行仍在); // - 把 light.go / notice.go 的调用点改回原文(`d.Reason` / `err.Error()`)⇒ 对应用例红。 import ( "context" "errors" "strings" "testing" "git.yeij.top/AskaEth/Cyrene_v2/internal/channel/adapter" "git.yeij.top/AskaEth/Cyrene_v2/internal/logger" "git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/loop" "git.yeij.top/AskaEth/Cyrene_v2/internal/user" ) // judgeReasonSecret 是模型/上游「回显」进 reason 的凭据形状。 const judgeReasonSecret = "sk-live-0123456789abcdef" // judgeReasonRaw 是原始 reason:短语 + 回显的凭据 + 换行(想在按行聚合的日志里伪造 // 一条新记录)。 const judgeReasonRaw = "第一行:群里有 " + judgeReasonSecret + "\n第二行:伪造一条日志" // judgeReasonWant 是 judgeReason 的期望产物。 const judgeReasonWant = "第一行:群里有 [redacted] 第二行:伪造一条日志" // assertSanitizedReason 断言一段出镜文本既不含凭据原文,也不含换行。 func assertSanitizedReason(t *testing.T, what, got string) { t.Helper() if strings.Contains(got, "sk-live") { t.Fatalf("%s 里出现了回显的凭据:%q", what, got) } if !strings.Contains(got, "[redacted]") { t.Errorf("%s 里没有打码标记:%q", what, got) } if strings.Contains(got, "\n") { t.Fatalf("%s 里出现了换行(应被压平):%q", what, got) } } // TestJudgeReason_RedactsFlattensAndBounds 钉住 helper 本身的三件事。 func TestJudgeReason_RedactsFlattensAndBounds(t *testing.T) { t.Parallel() if got := judgeReason(judgeReasonRaw); got != judgeReasonWant { t.Fatalf("judgeReason = %q,期望 %q", got, judgeReasonWant) } // 上限按 rune 计:60 个汉字必须原样保留(不能被截),61 个才截断。 if got := judgeReason(strings.Repeat("中", judgeReasonRunes)); got != strings.Repeat("中", judgeReasonRunes) { t.Fatalf("恰好 %d rune 不该被截断:%q", judgeReasonRunes, got) } if got := judgeReason(strings.Repeat("中", judgeReasonRunes+1)); len([]rune(got)) != judgeReasonRunes+1 { t.Fatalf("超限时应保留 %d rune + 省略号,实际 %q", judgeReasonRunes, got) } } // TestLegacyLightJudge_ReasonSanitized 覆盖 light.go 的两处返回值(旧轻量判定车道): // reason 出镜前必须打码压平(它随后被 coalesce_prepare 的 Debug 落日志)。 func TestLegacyLightJudge_ReasonSanitized(t *testing.T) { t.Parallel() for _, respond := range []bool{true, false} { judge := &stubLightJudge{resp: LightDecision{Respond: respond, Reason: judgeReasonRaw}} b, err := New(Options{ Log: logger.New(logger.Options{Level: "debug", Out: &strings.Builder{}}), Loop: &stubChatter{res: &loop.Result{Text: "x"}}, Sessions: &stubSessions{sess: nonOwnerSession()}, Owner: &user.OwnerProfile{}, Light: judge, TriggerWords: []string{"昔涟"}, }) if err != nil { t.Fatalf("New: %v", err) } msg := nonOwnerGroupMsg("提一句昔涟") got, reason := b.legacyLightJudge(context.Background(), msg, msg.Sender, false) b.Stop(context.Background()) if got != respond { t.Fatalf("legacyLightJudge respond = %v, 期望 %v", got, respond) } assertSanitizedReason(t, "light judge 返回值", reason) if !strings.Contains(reason, "light judge: "+judgeReasonWant) { t.Fatalf("返回值 = %q,期望含 %q", reason, "light judge: "+judgeReasonWant) } } } // TestNoticeReviewFailureReasonSanitized 覆盖 notice.go 的失败原因: // 审查模型(或它背后的上游)的 error 文本进 noticeOutcome.Reason 前同样要打码压平。 func TestNoticeReviewFailureReasonSanitized(t *testing.T) { t.Parallel() review := func(context.Context, []adapter.UnifiedMessage) ([]int, error) { return nil, errors.New(judgeReasonRaw) } ad := ¬iceAdapter{notice: adapter.SystemNotice{Kind: adapter.NoticeOther, SafeText: "群/客户端系统事件"}, ok: true} b, _, _, _ := noticeFixture(t, ad, review) msg := groupNoticeMessage() out := b.preEntryReview(context.Background(), &msg, ad) if out.Proceed { t.Fatalf("审查失败必须走 fail-safe 静默,实际放行了:reason=%q", out.Reason) } if !strings.HasPrefix(out.Reason, "notice review failed: ") { t.Fatalf("原因前缀被改动了:%q", out.Reason) } assertSanitizedReason(t, "notice 审查失败原因", out.Reason) }