① light.go 的 2 处 "light judge: " + d.Reason 与 engage.go 同形(model reason → 返回值 → coalesce_prepare 的 Debug)。把上一轮的 engageReason 提成包内共用的 judgeReason(judge_reason.go,RedactTextLimited + strings.Fields,60 rune), engage.go 7 处 + light.go 2 处 + notice.go 的 "notice review failed: "+err.Error() 统一走它。控制流零改动:只换「进去的那份文本」,判定条件与返回值拼接结构未动。 ② MCP 的 Logger 面先探针后修:go-sdk v1.8.0 的 keepalive 会把对端 JSON-RPC error 原样打进日志(实测泄漏,见 sdklog_probe_test.go 的输出),tools/call 路径不打日志。 新增 sdklog.go:redactingLogger 交给 SDK,wire 错误(链上有 *jsonrpc.Error)只留 code + message_bytes(复用 upstreamError),本地错误保留原因,消息/属性压平+打码+300 rune。 ③ errortoken.go 的口径注释纠正:从「包外不得调用」改为按字段类别判定(协议枚举可守、 自由文本默认不带),并把 cdp.go 这条 M51 用法作为例子写进去。
115 lines
4.8 KiB
Go
115 lines
4.8 KiB
Go
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)
|
|
}
|