- 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 四处均补了会随回退变红的用例(变异已实测)。
172 lines
6.5 KiB
Go
172 lines
6.5 KiB
Go
package adapter
|
||
|
||
// 本文件用**对端原样输出**(含 UTF-8 BOM 字节)做夹具,固定审查输出的解析韧性。
|
||
// 生产症状(`~/cy.log`,9 天):`flow gate review output unparsable` 1,301 次,其中
|
||
// 1,288 次的错误串逐字是 `invalid character 'ï' looking for beginning of value` ——
|
||
// 对端在 JSON 数组前带了 BOM,而闸门用的 json.Unmarshal 不接受它。
|
||
|
||
import (
|
||
"encoding/json"
|
||
"strings"
|
||
"testing"
|
||
"unicode/utf8"
|
||
)
|
||
|
||
// bomReviewFixture 是审查模型网关的实际回包形状:UTF-8 BOM(EF BB BF)紧接一个
|
||
// JSON 数组。写死字节而不是用 \ufeff,是为了让「夹具被谁悄悄去掉 BOM」本身就会红。
|
||
const bomReviewFixture = "\xef\xbb\xbf[0, 1, 2, 3, 4, 5]"
|
||
|
||
// TestReviewBOMFixtureStillReproducesTheFailure 先钉住夹具的真实性:它必须以 BOM
|
||
// 开头,且严格的 json.Unmarshal 必须在它上面失败,错误串必须就是生产日志里那条。
|
||
//
|
||
// 变异判据:把夹具里的 BOM 去掉 ⇒ 本用例红(同时也说明 BOM 相关用例已失去意义)。
|
||
func TestReviewBOMFixtureStillReproducesTheFailure(t *testing.T) {
|
||
t.Parallel()
|
||
|
||
if !strings.HasPrefix(bomReviewFixture, "\xef\xbb\xbf") {
|
||
t.Fatal("BOM fixture lost its BOM bytes: it no longer reproduces the production failure")
|
||
}
|
||
var idx []int
|
||
err := json.Unmarshal([]byte(bomReviewFixture), &idx)
|
||
if err == nil {
|
||
t.Fatal("strict json.Unmarshal accepted the BOM fixture: the regression it models is gone")
|
||
}
|
||
if !strings.Contains(err.Error(), "invalid character 'ï'") {
|
||
t.Fatalf("strict parse error = %v, want the production symptom \"invalid character 'ï'\"", err)
|
||
}
|
||
}
|
||
|
||
// TestParseReviewIndices_ToleratesBOM 是修复的正面证据:同一份带 BOM 的原样回包
|
||
// 现在能解析出编号。
|
||
//
|
||
// 变异判据:去掉 ParseReviewIndices 里的 reviewArraySpan(回到裸 json.Unmarshal)
|
||
// ⇒ 本用例红。
|
||
func TestParseReviewIndices_ToleratesBOM(t *testing.T) {
|
||
t.Parallel()
|
||
|
||
keep, err := ParseReviewIndices(bomReviewFixture, 6)
|
||
if err != nil {
|
||
t.Fatalf("ParseReviewIndices(BOM fixture) error = %v, want nil", err)
|
||
}
|
||
want := []int{0, 1, 2, 3, 4, 5}
|
||
if len(keep) != len(want) {
|
||
t.Fatalf("keep = %v, want %v", keep, want)
|
||
}
|
||
for i := range want {
|
||
if keep[i] != want[i] {
|
||
t.Fatalf("keep = %v, want %v", keep, want)
|
||
}
|
||
}
|
||
}
|
||
|
||
// TestParseReviewIndices_ToleratesDecorations 覆盖模型常见的几种外壳:首尾空白与换行、
|
||
// Markdown 代码围栏(带或不带语言标签)、以及围在数组外面的解说文字。
|
||
func TestParseReviewIndices_ToleratesDecorations(t *testing.T) {
|
||
t.Parallel()
|
||
|
||
cases := map[string]string{
|
||
"newlines": "\n\n[0, 2]\n\n",
|
||
"fence": "```\n[0, 2]\n```",
|
||
"fence with lang": "```json\n[0, 2]\n```",
|
||
"prose prefix": "好的,保留以下编号:[0, 2]",
|
||
"prose suffix": "[0, 2] 以上是保留的编号。",
|
||
"bom plus fence": "\xef\xbb\xbf```json\n[0, 2]\n```",
|
||
"spaced values": "[ 0 , 2 ]",
|
||
}
|
||
for name, raw := range cases {
|
||
keep, err := ParseReviewIndices(raw, 3)
|
||
if err != nil {
|
||
t.Fatalf("%s: error = %v, want nil", name, err)
|
||
}
|
||
if len(keep) != 2 || keep[0] != 0 || keep[1] != 2 {
|
||
t.Fatalf("%s: keep = %v, want [0 2]", name, keep)
|
||
}
|
||
}
|
||
}
|
||
|
||
// TestParseReviewIndices_FiltersHallucinations 固定过滤口径与修复前一致:越界、负数、
|
||
// 重复的编号一律丢弃(模型幻觉出的数字绝不能淘汰真实消息),结果升序。
|
||
func TestParseReviewIndices_FiltersHallucinations(t *testing.T) {
|
||
t.Parallel()
|
||
|
||
keep, err := ParseReviewIndices("\xef\xbb\xbf[2, 2, 99, -1, 0]", 3)
|
||
if err != nil {
|
||
t.Fatalf("error = %v, want nil", err)
|
||
}
|
||
if len(keep) != 2 || keep[0] != 0 || keep[1] != 2 {
|
||
t.Fatalf("keep = %v, want [0 2]", keep)
|
||
}
|
||
}
|
||
|
||
// TestParseReviewIndices_EmptyArrayIsNotAnError:模型明确回空数组 = 「一条都不保留」,
|
||
// 这是有效结论(全部当噪声),不是解析失败 —— 失败方向必须留给真正的畸形输出。
|
||
func TestParseReviewIndices_EmptyArrayIsNotAnError(t *testing.T) {
|
||
t.Parallel()
|
||
|
||
keep, err := ParseReviewIndices("\xef\xbb\xbf[]", 5)
|
||
if err != nil {
|
||
t.Fatalf("error = %v, want nil", err)
|
||
}
|
||
if len(keep) != 0 {
|
||
t.Fatalf("keep = %v, want empty", keep)
|
||
}
|
||
}
|
||
|
||
// TestParseReviewIndices_RejectsGarbage:真正畸形的输出仍然报错(调用方据此走
|
||
// 「不额外丢弃、也不提权」的安全方向),错误里带一小段原文便于定位下一次变种。
|
||
func TestParseReviewIndices_RejectsGarbage(t *testing.T) {
|
||
t.Parallel()
|
||
|
||
for name, raw := range map[string]string{
|
||
"no array": "抱歉,我无法判断。",
|
||
"empty": "",
|
||
"array of str": `["a", "b"]`,
|
||
} {
|
||
if _, err := ParseReviewIndices(raw, 5); err == nil {
|
||
t.Fatalf("%s: error = nil, want a decode error", name)
|
||
}
|
||
}
|
||
}
|
||
|
||
// TestReviewExcerpt_TruncatesByRune 钉住「上限按字符计,不按字节计」:审查模型回的中文
|
||
// 每个字 3 字节,按 40 **字节**截会把其中一个字切成半个,错误串因此成为非法 UTF-8
|
||
// (终端显示 U+FFFD、JSON 客户端各自乱码)。
|
||
//
|
||
// 变异判据:把 reviewExcerpt 换回 `s[:reviewExcerptLen] + "…"` 的字节截断 ⇒ 本用例红
|
||
// (utf8.ValidString 为 false)。
|
||
func TestReviewExcerpt_TruncatesByRune(t *testing.T) {
|
||
t.Parallel()
|
||
|
||
got := reviewExcerpt(strings.Repeat("中", reviewExcerptLen*2))
|
||
if !utf8.ValidString(got) {
|
||
t.Fatalf("截断后不是合法 UTF-8(按字节截断了半个汉字):%q", got)
|
||
}
|
||
body := strings.TrimSuffix(got, "…")
|
||
if n := utf8.RuneCountInString(body); n != reviewExcerptLen {
|
||
t.Fatalf("保留 %d 个字符,期望 %d:%q", n, reviewExcerptLen, got)
|
||
}
|
||
if !strings.HasSuffix(got, "…") {
|
||
t.Errorf("超限时应当带省略号:%q", got)
|
||
}
|
||
}
|
||
|
||
// TestReviewExcerpt_RedactsCredentialShapes 钉住「带上限出镜 ≠ 原样出镜」:摘要仍要
|
||
// 过 trace.RedactText,网关回显的凭据形状(sk-… / token=… 等)必须被打码。
|
||
//
|
||
// 变异判据:让 reviewExcerpt 只做截断、不过 trace.RedactTextLimited ⇒ 本用例红。
|
||
func TestReviewExcerpt_RedactsCredentialShapes(t *testing.T) {
|
||
t.Parallel()
|
||
|
||
const secret = "sk-abcdefghijklmnop"
|
||
got := reviewExcerpt("[0, 1] Authorization: Bearer " + secret)
|
||
if strings.Contains(got, secret) {
|
||
t.Fatalf("凭据形状没被打码:%q", got)
|
||
}
|
||
if !strings.Contains(got, "[redacted]") {
|
||
t.Errorf("打码标记缺失:%q", got)
|
||
}
|
||
if !strings.Contains(got, "[0, 1]") {
|
||
t.Errorf("摘要丢了诊断价值(编号数组):%q", got)
|
||
}
|
||
}
|