package rag import ( "strings" "testing" "unicode/utf8" ) func TestChunkEmpty(t *testing.T) { if got := chunk(" \n\n "); len(got) != 0 { t.Fatalf("空白文本应切出 0 块,得 %d", len(got)) } } func TestChunkShort(t *testing.T) { got := chunk("这是一句很短的话。") if len(got) != 1 || !strings.Contains(got[0], "很短") { t.Fatalf("短文本应为单块,得 %v", got) } } // TestChunkRuneSafe 是核心:中文必须按 rune 切,绝不能切碎出乱码(旧版按字节切的 bug)。 func TestChunkRuneSafe(t *testing.T) { // 1500 个汉字、无标点 → 触发窗口兜底切;每块必须是合法 UTF-8。 text := strings.Repeat("中", 1500) for _, c := range chunk(text) { if !utf8.ValidString(c) { t.Fatalf("切出非法 UTF-8(中文被字节切碎):%q", c) } for _, r := range c { if r != '中' && !strings.ContainsRune("\n ", r) { t.Fatalf("出现意外字符 %q,疑似切碎", r) } } } } func TestChunkSizeBounds(t *testing.T) { // 多段中文,每段以句号结尾。 var sb strings.Builder for i := 0; i < 60; i++ { sb.WriteString("这是用于测试切块大小上界的一个中文句子片段。") } for i, c := range chunk(sb.String()) { if n := runeLen(c); n > chunkTargetRunes+chunkOverlapRunes+1 { t.Fatalf("第 %d 块 %d 字,超过 target+overlap=%d", i, n, chunkTargetRunes+chunkOverlapRunes+1) } } } func TestChunkSentenceBoundary(t *testing.T) { var sb strings.Builder for i := 0; i < 80; i++ { sb.WriteString("第一句话在这里。第二句话也在这里。") } chunks := chunk(sb.String()) if len(chunks) < 2 { t.Fatalf("长文本应切出多块,得 %d", len(chunks)) } // 多数块应以句号收口(允许重叠导致的少量例外)。 endsWell := 0 for _, c := range chunks { if strings.HasSuffix(strings.TrimSpace(c), "。") { endsWell++ } } if endsWell < len(chunks)/2 { t.Fatalf("多数块应在句末收口,仅 %d/%d", endsWell, len(chunks)) } } func TestChunkOverlap(t *testing.T) { var sb strings.Builder for i := 0; i < 60; i++ { sb.WriteString("用于验证相邻块之间存在上下文重叠的中文句子。") } chunks := chunk(sb.String()) if len(chunks) < 2 { t.Skip("未切出多块,跳过重叠校验") } // 第 2 块开头应包含第 1 块尾部的一小段(重叠)。 prevTail := []rune(chunks[0]) tail := string(prevTail[max0(len(prevTail)-chunkOverlapRunes):]) // 取尾部一小片做包含判断(去掉可能的换行)。 probe := strings.TrimSpace(tail) if len(probe) > 10 { probe = probe[len(probe)-10:] } if probe != "" && !strings.Contains(chunks[1], strings.TrimSpace(string([]rune(probe)))) { // 重叠是按 rune 尾部,probe 是字节尾部,宽松校验:第二块前缀应与首块尾部有交集 if !strings.HasPrefix(strings.TrimSpace(chunks[1]), strings.TrimSpace(tail)) { t.Logf("重叠片段:%q\n块2前缀:%q", tail, []rune(chunks[1])[:min0(40, runeLen(chunks[1]))]) t.Fatalf("相邻块未见重叠") } } } func TestChunkOversizedNoPunct(t *testing.T) { text := strings.Repeat("x", 3000) // 无标点超大块 chunks := chunk(text) if len(chunks) < 3 { t.Fatalf("3000 字无标点应窗口切成多块,得 %d", len(chunks)) } for _, c := range chunks { if runeLen(c) > chunkTargetRunes+chunkOverlapRunes+1 { t.Fatalf("窗口切块超界:%d", runeLen(c)) } } } func max0(n int) int { if n < 0 { return 0 } return n } func min0(a, b int) int { if a < b { return a } return b }