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) } }