fix(dingtalk): 推理正文边界支持「首行即正文」并补齐卡片结构诊断 + 流式分帧重放守卫

This commit is contained in:
姚章浩
2026-09-17 14:16:40 +08:00
parent a1a2aabab4
commit 5f1206b545
@@ -0,0 +1,108 @@
package reasoningmode
import (
"bufio"
"encoding/json"
"os"
"testing"
)
// TestReplayProductionCorpusThroughFeed replays production reasoning answers
// through the streaming feed the card pump actually uses, and checks two
// properties of every frame the card receives:
//
// 1. a frame that already contains a structural body line must start at that
// body (no narration glued in front of it). A frame that shows narration
// above the body is exactly the "the last reasoning sentence stays on the
// card" defect.
// 2. once a frame is body-only, no later frame may go back to showing
// narration.
//
// Enable with REASONING_CORPUS=<path to reasoning_corpus.jsonl>.
func TestReplayProductionCorpusThroughFeed(t *testing.T) {
path := os.Getenv("REASONING_CORPUS")
if path == "" {
t.Skip("set REASONING_CORPUS to replay production answers")
}
file, err := os.Open(path)
if err != nil {
t.Fatalf("open corpus: %v", err)
}
defer file.Close()
chunkSizes := []int{11, 47, 199, 1001}
var rows, frames, bodyFrames int
type violation struct {
id string
size int
marks bool
prefix int
bytes int
}
var violations []violation
scanner := bufio.NewScanner(file)
scanner.Buffer(make([]byte, 0, 1<<20), 1<<22)
for scanner.Scan() {
var row struct {
ID string `json:"id"`
Content string `json:"content"`
}
if err := json.Unmarshal(scanner.Bytes(), &row); err != nil {
t.Fatalf("decode row: %v", err)
}
rows++
for _, size := range chunkSizes {
for _, marks := range []bool{false, true} {
feed := NewPartialFeed("reasoning")
bodySeen := false
for start := 0; start < len(row.Content); start += size {
end := start + size
if end > len(row.Content) {
end = len(row.Content)
}
frame := feed.OnAnswer(row.Content[:end])
frames++
if firstStructuralLine(frame) == "" {
// no body in this frame yet: narration only, expected.
continue
}
offset, ok := BodyStartOffset(frame)
prefix := -1
if ok {
prefix = len([]rune(frame[:offset]))
}
if ok && prefix == 0 {
bodySeen = true
bodyFrames++
continue
}
// The frame carries a structural line but does not start at
// it: narration is still visible above the body.
violations = append(violations, violation{
id: row.ID,
size: size,
marks: marks,
prefix: prefix,
bytes: len(frame),
})
_ = bodySeen
}
}
}
}
if err := scanner.Err(); err != nil {
t.Fatalf("scan corpus: %v", err)
}
t.Logf("rows=%d frames=%d body_only_frames=%d narration_above_body_frames=%d", rows, frames, bodyFrames, len(violations))
for i, v := range violations {
if i >= 8 {
t.Logf("... %d more", len(violations)-i)
break
}
t.Logf("violation id=%s chunk=%d step_boundaries=%t body_prefix_runes=%d bytes=%d", v.id, v.size, v.marks, v.prefix, v.bytes)
}
if len(violations) > 0 {
t.Errorf("%d frames showed narration above the answer body", len(violations))
}
}