// relevance_test.go -- 相关性评分的单元测试. // // 覆盖场景: // - Score 函数基本评分 // - 完全匹配高分 // - 完全不相关低分 // - 空输入返回 0 // - SelectRelevant 选出 top-N // - tokenize 分词 // - tokenWeight 权重 // - partialMatch 部分匹配 // - isCJK 中文字符判断 package memory import ( "testing" "time" ) // TestScore_ExactMatch 测试完全匹配场景 func TestScore_ExactMatch(t *testing.T) { score := Score("golang testing", "golang testing") if score < 0.8 { t.Errorf("完全匹配的分数应 >= 0.8, 实际: %f", score) } } // TestScore_NoMatch 测试完全不相关 func TestScore_NoMatch(t *testing.T) { score := Score("golang testing", "piano music concert") if score > 0.3 { t.Errorf("不相关内容的分数应 < 0.3, 实际: %f", score) } } // TestScore_PartialMatch 测试部分匹配 func TestScore_PartialMatch(t *testing.T) { score := Score("golang", "golang testing framework") if score < 0.1 { t.Errorf("部分匹配分数应 > 0.1, 实际: %f", score) } } // TestScore_EmptyInput 测试空输入 func TestScore_EmptyInput(t *testing.T) { if Score("", "something") != 0 { t.Error("空 query 应返回 0") } if Score("something", "") != 0 { t.Error("空 description 应返回 0") } if Score("", "") != 0 { t.Error("双空应返回 0") } } // TestScore_Ordering 测试分数排序正确 func TestScore_Ordering(t *testing.T) { query := "config file loading" exact := Score(query, "config file loading and parsing") partial := Score(query, "config management system") unrelated := Score(query, "database migration tool") if exact <= partial { t.Errorf("更精确的匹配应有更高分数: exact=%f, partial=%f", exact, partial) } if partial <= unrelated { t.Errorf("部分匹配应高于不相关: partial=%f, unrelated=%f", partial, unrelated) } } // TestSelectRelevant 测试选出 top-N 相关记忆 func TestSelectRelevant(t *testing.T) { headers := []MemoryHeader{ {Frontmatter: Frontmatter{Name: "golang-setup", Description: "go project setup and configuration"}, ModTime: time.Now()}, {Frontmatter: Frontmatter{Name: "python-ml", Description: "python machine learning workflow"}, ModTime: time.Now()}, {Frontmatter: Frontmatter{Name: "go-testing", Description: "golang testing patterns and best practices"}, ModTime: time.Now()}, {Frontmatter: Frontmatter{Name: "docker-deploy", Description: "docker deployment workflow"}, ModTime: time.Now()}, } result := SelectRelevant("golang testing", headers, 2) if len(result) > 2 { t.Errorf("期望最多 2 个结果, 实际 %d", len(result)) } if len(result) == 0 { t.Fatal("应有相关结果") } // 第一个结果应该是最相关的(go-testing) if result[0].Frontmatter.Name != "go-testing" { t.Errorf("最相关的应为 'go-testing', 实际: %q", result[0].Frontmatter.Name) } } // TestSelectRelevant_EmptyQuery 测试空查询 func TestSelectRelevant_EmptyQuery(t *testing.T) { headers := []MemoryHeader{ {Frontmatter: Frontmatter{Name: "test", Description: "test desc"}, ModTime: time.Now()}, } result := SelectRelevant("", headers, 5) if len(result) != 0 { t.Errorf("空查询不应有结果, 实际 %d 个", len(result)) } } // TestSelectRelevant_DefaultLimit 测试默认 limit func TestSelectRelevant_DefaultLimit(t *testing.T) { headers := make([]MemoryHeader, 10) for i := range headers { headers[i] = MemoryHeader{ Frontmatter: Frontmatter{Name: "test", Description: "test matching description"}, ModTime: time.Now(), } } result := SelectRelevant("test matching", headers, 0) if len(result) > defaultRelevanceLimit { t.Errorf("默认 limit 应为 %d, 实际返回 %d", defaultRelevanceLimit, len(result)) } } // TestTokenize 测试分词 func TestTokenize(t *testing.T) { tests := []struct { name string text string minLen int // 期望的最小 token 数 }{ {"英文文本", "hello world testing", 3}, {"含短词", "a b cd efg", 2}, {"空文本", "", 0}, {"中文文本", "你好世界", 1}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { tokens := tokenize(tt.text) if len(tokens) < tt.minLen { t.Errorf("token 数: %d, 期望至少 %d\n tokens: %v", len(tokens), tt.minLen, tokens) } }) } } // TestTokenWeight 测试 token 权重 func TestTokenWeight(t *testing.T) { // 短 token 低权重 short := tokenWeight("go") // 中等 token 正常权重 medium := tokenWeight("test") // 长 token 高权重 long := tokenWeight("configuration") if short >= medium { t.Errorf("短 token 权重应 < 中等: %f >= %f", short, medium) } if medium >= long { t.Errorf("中等 token 权重应 < 长: %f >= %f", medium, long) } } // TestPartialMatch 测试部分匹配 func TestPartialMatch(t *testing.T) { // 子串匹配给部分分数 score := partialMatch("config", []string{"configuration", "setup"}) if score == 0 { t.Error("'config' 应部分匹配 'configuration'") } // 太短的 token 不做子串匹配 score = partialMatch("ab", []string{"abc", "def"}) if score != 0 { t.Error("2 字符 token 不应做子串匹配") } // 无匹配 score = partialMatch("xyz", []string{"abc", "def"}) if score != 0 { t.Error("无匹配应返回 0") } } // TestIsCJK 测试 CJK 字符判断 func TestIsCJK(t *testing.T) { if !isCJK('你') { t.Error("中文字符应为 CJK") } if !isCJK('世') { t.Error("中文字符应为 CJK") } if isCJK('a') { t.Error("英文字母不应为 CJK") } if isCJK('1') { t.Error("数字不应为 CJK") } }