- Admit 不再同步调用审查模型:触发时只把审查**派发**到后台 goroutine,准入只读 当前已有结论。三重上界:同一适配器至多一次在飞(reviewInFlight)、同一会话每窗口 至多一次(lastReview)、整个适配器每窗口至多 defaultReviewPerWindow=6 次(reviewHits, 写成可测断言)。 - st.lastReview 改为在**派发时**置位 ⇒ 成功与失败两条路径同样被节流。修复前它只在 err == nil 分支置位,而实测 68% 的审查因对端回带 BOM 解析失败 ⇒ 缓冲满后每条入站帧 都付一次 ~6s 的模型调用(qq-ws/kook-ws/weixin-poll 三路单读 goroutine 全被钉住)。 - 新增 adapter.ParseReviewIndices:容忍 UTF-8 BOM、首尾空白/换行、Markdown 代码围栏、 数组外解说文字;越界/重复编号照旧丢弃。channelbridge.parseKeepIndices 改为委派它 (仅函数体 + 注释,未动限流/丢弃/审查触发语义)。 - 语义差异(已写进 flowgate_review.go 文件头):rate_limited 的门口丢与 FIFO 上限 一字不改;审查修剪降级为后台积压去噪,只修剪它当时看到的那批历史条目 —— review_pruned 不再从门口丢消息(实测 41–74 条/日),完整性↑、噪声↑,是否开口仍由 engage_judge/冷避/owner 闸判(三处判据未动)。 - cmd/cyrene:逐账号启动日志如实报出生效的入口流控(塔裁决 A:零行为改动、不翻默认), 让「没配流控键的账号闸门仍开着」一眼可见。 - 测试:4s 慢审查替身证明入站帧不被阻塞(变异回同步 ⇒ 红)、节流在失败路径生效 (摘掉派发时置位 ⇒ 红)、每适配器窗口预算上界(摘掉预算 ⇒ 红)、同适配器单飞 (摘掉 in-flight ⇒ 红)、在飞期间到达的消息不被连带修剪、带 BOM 字节的夹具 (夹具去掉 BOM ⇒ 红)。闸门用例 -race 通过。
129 lines
4.7 KiB
Go
129 lines
4.7 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"
|
||
)
|
||
|
||
// 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)
|
||
}
|
||
}
|
||
}
|