- channelbridge: 拨号失败的 WARN 不再带原始 dial 错误(含 ?access_token=), URLCause 剥壳 + 内嵌地址 MaskQuery 打码 + HostOf 保留目标来源;控制流零改动 - browser/cdp.go: CDP error.message 只有像协议枚举(ErrorToken)才出镜,其余只留字节数 - mcp: 第三方 server 的错误文本默认不带,只留 server/工具/JSON-RPC 码/message 长度, 分类链(errors.Is/As、context.Canceled)经 Unwrap 保留 - channel/adapter/flowgate: 审查输出摘要由 40 字节改 40 rune(防切 UTF-8)并过 RedactText 四处均补了会随回退变红的用例(变异已实测)。
115 lines
4.3 KiB
Go
115 lines
4.3 KiB
Go
package channelbridge
|
||
|
||
// 本文件钉住「拨号失败的 WARN 不带凭据」这条红线(connectlog.go)。
|
||
//
|
||
// 生产症状:qq-obv11 的 OneBot 端点按惯例把凭据写成 `?access_token=…`,拨号失败时
|
||
// 错误串里就是整条地址;原样 `"error", err` 落 WARN 等于把平台凭据写进日志。
|
||
//
|
||
// 变异判据(每条都对应一个会红的实现):
|
||
// - 把 connectFailureAttrs 换回 `"error", err` ⇒ SECRET 出现在日志里,用例红;
|
||
// - 只做 trace.RedactText(不做 MaskQuery)⇒ 短 token 用例红(形状级匹配要求
|
||
// 赋值 ≥6 字符,3 字符的 token 逃得过去);
|
||
// - 把 host 字段摘掉 ⇒ 「目标 host 仍在」断言红(打码不能把诊断一起打没)。
|
||
|
||
import (
|
||
"bytes"
|
||
"context"
|
||
"errors"
|
||
"net/url"
|
||
"strings"
|
||
"testing"
|
||
|
||
"git.yeij.top/AskaEth/Cyrene_v2/internal/channel/session"
|
||
"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"
|
||
)
|
||
|
||
// dialFailureAdapter 是「Connect 以给定错误失败」的适配器:其余能力沿用 fakeAdapter。
|
||
type dialFailureAdapter struct {
|
||
*fakeAdapter
|
||
err error
|
||
}
|
||
|
||
func (a *dialFailureAdapter) Connect(context.Context) error { return a.err }
|
||
|
||
// newLogBridge 构建一个把日志写进 out 的桥接器(其余依赖用测试桩)。
|
||
func newLogBridge(t *testing.T, out *bytes.Buffer) *Bridge {
|
||
t.Helper()
|
||
b, err := New(Options{
|
||
Log: logger.New(logger.Options{Format: string(logger.FormatJSON), Out: out}),
|
||
Loop: &stubChatter{res: &loop.Result{Text: "hi"}},
|
||
Sessions: &stubSessions{sess: session.Session{SessionID: "s", Owner: true, MemWritable: true}},
|
||
Owner: &user.OwnerProfile{},
|
||
})
|
||
if err != nil {
|
||
t.Fatalf("New: %v", err)
|
||
}
|
||
t.Cleanup(func() { b.Stop(context.Background()) })
|
||
return b
|
||
}
|
||
|
||
// qqDialAdapter 返回一个「配置完整、拨号必失败」的 qq-obv11 适配器。
|
||
func qqDialAdapter(err error) *dialFailureAdapter {
|
||
return &dialFailureAdapter{
|
||
fakeAdapter: &fakeAdapter{
|
||
name: "qq-obv11:bot1",
|
||
cfg: map[string]any{"mode": "client", "remoteURL": "wss://onebot.example.test/open"},
|
||
},
|
||
err: err,
|
||
}
|
||
}
|
||
|
||
// startAndCapture 启动桥接器(连接同步完成)并返回它写下的日志。
|
||
func startAndCapture(t *testing.T, err error) string {
|
||
t.Helper()
|
||
var out bytes.Buffer
|
||
b := newLogBridge(t, &out)
|
||
b.Add(qqDialAdapter(err))
|
||
b.Start()
|
||
return out.String()
|
||
}
|
||
|
||
// assertConnectWarn 断言 WARN 的三件事:仍然说「连不上」、目标 host 仍在、凭据不在。
|
||
func assertConnectWarn(t *testing.T, logged, secret string) {
|
||
t.Helper()
|
||
if !strings.Contains(logged, "channel connect failed, leaving disconnected") {
|
||
t.Fatalf("日志里没有「连不上」这条 WARN:%s", logged)
|
||
}
|
||
if !strings.Contains(logged, "onebot.example.test") {
|
||
t.Fatalf("日志丢了目标 host(打码不该连诊断一起打没):%s", logged)
|
||
}
|
||
if strings.Contains(logged, secret) {
|
||
t.Fatalf("日志里出现了拨号凭据 %q:%s", secret, logged)
|
||
}
|
||
}
|
||
|
||
// TestConnectFailureWarn_MasksURLErrorToken 覆盖 *url.Error 形状(net/http 与
|
||
// gorilla dial 的真实形态:Error() 会渲染整条地址)。
|
||
func TestConnectFailureWarn_MasksURLErrorToken(t *testing.T) {
|
||
t.Parallel()
|
||
const secret = "SECRET-TOKEN-VALUE"
|
||
dialErr := &url.Error{
|
||
Op: "dial",
|
||
URL: "wss://onebot.example.test/open?access_token=" + secret,
|
||
Err: errors.New("websocket: bad handshake"),
|
||
}
|
||
assertConnectWarn(t, startAndCapture(t, dialErr), secret)
|
||
}
|
||
|
||
// TestConnectFailureWarn_MasksEmbeddedURLToken 覆盖「适配器自己把地址拼进文案」的
|
||
// 形状(qq-obv11 的 `dial onebot ws <remote_url>: <cause>`):这里没有 *url.Error
|
||
// 外壳可剥,只能靠地址级打码 —— 因此 token 刻意取得**短于**形状级匹配的长度,
|
||
// 只做 RedactText 的实现会在这里漏。
|
||
func TestConnectFailureWarn_MasksEmbeddedURLToken(t *testing.T) {
|
||
t.Parallel()
|
||
const secret = "abc"
|
||
dialErr := errors.New("qq-obv11: dial onebot ws wss://onebot.example.test/open?access_token=" +
|
||
secret + ": websocket: bad handshake")
|
||
logged := startAndCapture(t, dialErr)
|
||
assertConnectWarn(t, logged, secret)
|
||
if !strings.Contains(logged, "access_token=***") {
|
||
t.Fatalf("凭据参数的值应被打码成 ***(保留参数名便于排障):%s", logged)
|
||
}
|
||
}
|