test(channelbridge): 锁住审查解析的接线(BOM 韧性必须在生产侧生效)

- 新增 TestParseKeepIndices_ToleratesBOMWiring:喂对端**原样字节**(\xef\xbb\xbf + JSON 数组)
  给生产路径的 parseKeepIndices,必须解析成功且升序;同时断言夹具真的以 BOM 开头、且裸
  json.Unmarshal 在它上面仍然失败 —— 把委派改回裸 json.Unmarshal 即红。
- 新增 TestParseKeepIndices_StillFiltersAfterDelegation:委派之后过滤口径不变(越界/负数/
  重复编号丢弃、结果升序)。
- 新增 TestNewLLMReviewer_ToleratesBOMOutput:走整条生产路径(NewLLMReviewer 产出的
  Reviewer),回带 BOM 时给出保留编号而不是退回 FIFO。
- 注释里写明 M24 已确认的语义代价:审查移出准入路径后 review_pruned 不再从门口丢消息
  (41–74 条/日 会进流水线),而 rate_limited 门口丢与 FIFO 上限修剪一字不改(由既有用例钉住)。

只加用例与必要注释(含一个为断言所需的 encoding/json import),未改动既有用例的任何断言。
This commit is contained in:
AskaEth committed 2026-10-24 19:49:58 +08:00
1 parent 76d972f0f2
commit a25e0c8870
1 file changed
+86
@@ -3,6 +3,7 @@ package channelbridge
import (
"bytes"
"context"
"encoding/json"
"log/slog"
"strings"
"testing"
@@ -329,3 +330,88 @@ func TestParseKeepIndices(t *testing.T) {
t.Fatal("expected error for non-JSON")
}
}
// ── M24:审查解析的**接线**(BOM 韧性只有在这里才算真的生效)────────────────
//
// parseKeepIndices 现在是 adapter.ParseReviewIndices 的一行委派;adapter 侧有自己的
// 用例,但「委派还在不在」只有本文件的用例能锁住 —— 谁把它改回裸 json.Unmarshal,
// 下面这几条就会红。生产症状(`~/cy.log`,9 天):1,909 次审查里 1,301 次解析失败,
// 其中 1,288 次错误串逐字是 `invalid character 'ï' looking for beginning of value`
// (对端在数组前回带了 UTF-8 BOM),每一次都白烧一次模型调用。
//
// 另一条已确认接受的语义代价(M24,塔裁决):审查移出准入路径后 review_pruned 不再
// 从门口丢消息(实测 41–74 条/日 现在会进流水线)—— 这是「不阻塞读循环」的必然代价。
// rate_limited 的门口丢与 FIFO 上限修剪**一字不改**:前者由本文件既有的
// TestFlowGate_RateLimitDropsNonOwnerAndAudits 钉住,后者由 adapter 侧的
// TestFlowGate_CapStillEvictsOnAdmission 钉住。
// bomReviewOutput 是审查模型网关的**原样**回包:UTF-8 BOM 紧接 JSON 数组。写死字节
// (而不是 \ufeff 转义或运行时拼装),使「夹具被谁悄悄去掉 BOM」本身就会红。
const bomReviewOutput = "\xef\xbb\xbf[0, 1, 2, 3, 4, 5]"
// TestParseKeepIndices_ToleratesBOMWiring 锁住接线:生产路径的 parseKeepIndices 必须
// 吃下带 BOM 的对端回包。
func TestParseKeepIndices_ToleratesBOMWiring(t *testing.T) {
t.Parallel()
// 夹具真实性:它必须以 BOM 字节开头,且裸 json.Unmarshal 在它上面必须失败 ——
// 否则这条用例就不再模拟任何生产故障。
if !strings.HasPrefix(bomReviewOutput, "\xef\xbb\xbf") {
t.Fatal("fixture lost its BOM bytes: this case no longer models the production failure")
}
var raw []int
if err := json.Unmarshal([]byte(bomReviewOutput), &raw); err == nil {
t.Fatal("strict json.Unmarshal accepted the BOM fixture; the case is meaningless now")
}
got, err := parseKeepIndices(bomReviewOutput, 6)
if err != nil {
t.Fatalf("parseKeepIndices(BOM) error = %v, want nil (delegation to adapter.ParseReviewIndices lost?)", err)
}
if len(got) != 6 {
t.Fatalf("keep = %v, want [0 1 2 3 4 5]", got)
}
for i := range got {
if got[i] != i {
t.Fatalf("keep = %v, want ascending [0 1 2 3 4 5]", got)
}
}
}
// TestParseKeepIndices_StillFiltersAfterDelegation 委派之后过滤口径不变:越界、负数、
// 重复的编号仍被丢弃,结果按升序(模型幻觉出的数字绝不能淘汰真实消息)。
func TestParseKeepIndices_StillFiltersAfterDelegation(t *testing.T) {
t.Parallel()
got, err := parseKeepIndices("\xef\xbb\xbf[3, -1, 3, 1, 99]", 4)
if err != nil {
t.Fatalf("parse: %v", err)
}
if len(got) != 2 || got[0] != 1 || got[1] != 3 {
t.Fatalf("keep = %v, want [1 3] (out-of-range/negative/dup dropped, ascending)", got)
}
}
// TestNewLLMReviewer_ToleratesBOMOutput 走**整条生产路径**:审查模型回带 BOM ⇒
// NewLLMReviewer 产出的 Reviewer 必须给出保留编号,而不是报 unparsable 退回 FIFO。
// (它证明的是密封在函数里的 wiring,而不只是 parseKeepIndices 这一个符号。)
func TestNewLLMReviewer_ToleratesBOMOutput(t *testing.T) {
t.Parallel()
review := NewLLMReviewer(&mockLLM{content: bomReviewOutput}, nil)
msgs := make([]adapter.UnifiedMessage, 0, 6)
for i := 0; i < 6; i++ {
msgs = append(msgs, adapter.UnifiedMessage{
SenderID: "u",
Content: "m",
ContentType: adapter.ContentText,
})
}
keep, err := review(context.Background(), msgs)
if err != nil {
t.Fatalf("review with BOM output: %v, want nil error", err)
}
if len(keep) != 6 {
t.Fatalf("keep = %v, want all six indices", keep)
}
}