Merge branch 'feat/m24-flowgate-move-review-off-admission-p' into dev
This commit is contained in:
commit
7ea2850f18
9 files changed
+1129
-150
No files matched your search
@@ -2,10 +2,8 @@ package channelbridge
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"git.yeij.top/AskaEth/Cyrene_v2/internal/channel/adapter"
|
||||
@@ -201,22 +199,14 @@ func truncateText(s string, n int) string {
|
||||
return string(runes[:n]) + "…"
|
||||
}
|
||||
|
||||
// parseKeepIndices 解码审查模型返回的 JSON 索引数组,并按升序
|
||||
// 只保留范围内、去重后的值。越界或重复的
|
||||
// 索引被丢弃,因此模型幻觉出的数字绝不会淘汰真实消息。
|
||||
// parseKeepIndices 解码审查模型返回的编号数组,并按升序只保留范围内、去重后的值。
|
||||
// 越界或重复的索引被丢弃,因此模型幻觉出的数字绝不会淘汰真实消息。
|
||||
//
|
||||
// 解析本身在 adapter.ParseReviewIndices(解析属于闸门的契约,和 Reviewer 类型同一个包),
|
||||
// 它容忍对端回带的东西:**UTF-8 BOM**、首尾空白/换行、Markdown 代码围栏、数组外的解说
|
||||
// 文字。修复前这里是裸 json.Unmarshal,实测 9 天 1,909 次审查里 1,301 次解析失败
|
||||
// (其中 1,288 次错误串逐字是 `invalid character 'ï' looking for beginning of value`),
|
||||
// 每次失败都白烧一次模型调用 —— 见 DevDocs/audit/群聊排队与回旧话-2026-10-24.md。
|
||||
func parseKeepIndices(s string, bound int) ([]int, error) {
|
||||
var idx []int
|
||||
if err := json.Unmarshal([]byte(s), &idx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
seen := make(map[int]bool, len(idx))
|
||||
out := make([]int, 0, len(idx))
|
||||
for _, i := range idx {
|
||||
if i >= 0 && i < bound && !seen[i] {
|
||||
seen[i] = true
|
||||
out = append(out, i)
|
||||
}
|
||||
}
|
||||
sort.Ints(out)
|
||||
return out, nil
|
||||
return adapter.ParseReviewIndices(s, bound)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user