Files
Cyrene_v2/internal/channel/adapter/flowgate_parse_test.go
T
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

172 lines
6.5 KiB
Go
Raw 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 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)
}
}