Files
AskaEth d6493d79f4 fix(upstream): M51 收敛剩余上游自由文本出口 - CDP/MCP/flowgate/channelbridge WARN
- 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

四处均补了会随回退变红的用例(变异已实测)。
2026-10-26 19:44:22 +08:00

115 lines
4.3 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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)
}
}