Files
HomeAgent/internal/knowledge/knowledge_test.go
JianFeeeee 685ed7e4a0 fix(knowledge): 覆盖同名条目时摘掉旧向量
从一次真实的知识库更新里发现:在线实例更新一个已有条目之后,
knowledge_count=32 而 vector_count=33——多出来的那一条是上一版的副本。

成因:vector.Store.Insert 是**追加**语义(s.docs = append + index.Add),不按 id 去重;
而 Store.Add 走的是「写 content.md + 覆盖 items[id] + Insert 向量」。
文件与内存条目都被正确替换了,只有向量索引多留了一份。

危害不在于多占内存:**检索可能命中已被替换掉的旧内容**,而且完全静默——
条目数看起来是对的,只有向量数比条目数多。

修法:Insert 之前先 s.vec.Remove(id)(Remove 已按 id 过滤 docs 与倒排索引)。
回归测试 TestAddOverwriteReplacesVector 钉住 knowledge_count / vector_count /
content.md 三者都必须只剩新版。

注:该文件在 origin/main 上本就有 32 行 gofmt 差异(结构体字段注释对齐),
不属本次改动,按纪律不做整体重排。
2026-09-12 08:30:22 +08:00

191 lines
4.3 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package knowledge
import (
"os"
"testing"
)
func TestNewStore(t *testing.T) {
dir, err := os.MkdirTemp("", "know_test_*")
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(dir)
s := NewStore(dir)
if err := s.Start(); err != nil {
t.Fatal(err)
}
defer s.Stop()
if s == nil {
t.Fatal("store should not be nil")
}
}
func TestAddAndSearch(t *testing.T) {
dir, err := os.MkdirTemp("", "know_add_*")
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(dir)
s := NewStore(dir)
s.Start()
defer s.Stop()
if err := s.Add("coffee", "咖啡是一种饮品,含有咖啡因"); err != nil {
t.Fatal(err)
}
results := s.Search("咖啡", 5)
if len(results) == 0 {
t.Fatal("expected results for '咖啡'")
}
}
// 覆盖同名条目必须把旧向量摘掉,而不是再插一份。
//
// 这条是从一次真实的知识库更新里发现的:在线上实例更新一个已有条目后,
// knowledge_count=32 但 vector_count=33 ——多出来的那一条是上一版的副本。
// 成因是 vector.Store.Insert 为追加语义s.docs = append + index.Add不按 id 去重。
// 危害不在于多占一份内存:检索可能命中**已被替换掉的旧内容**。
func TestAddOverwriteReplacesVector(t *testing.T) {
dir, err := os.MkdirTemp("", "know_overwrite_*")
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(dir)
s := NewStore(dir)
s.Start()
defer s.Stop()
if err := s.Add("recent", "第一版内容:旧的多模态描述式索引"); err != nil {
t.Fatal(err)
}
if got := s.Stats()["vector_count"].(int); got != 1 {
t.Fatalf("首次写入后 vector_count 应为 1实为 %d", got)
}
if err := s.Add("recent", "第二版内容:媒体已成为图记忆的一等节点"); err != nil {
t.Fatal(err)
}
if n := s.Stats()["knowledge_count"].(int); n != 1 {
t.Fatalf("同名覆盖后 knowledge_count 应为 1实为 %d", n)
}
if n := s.Stats()["vector_count"].(int); n != 1 {
t.Fatalf("同名覆盖后 vector_count 应为 1多了就是旧版没被摘掉实为 %d", n)
}
// 目录里也只应有一份内容,且是新的那份
b, err := os.ReadFile(dir + "/recent/content.md")
if err != nil {
t.Fatal(err)
}
if string(b) != "第二版内容:媒体已成为图记忆的一等节点" {
t.Fatalf("content.md 未被新内容覆盖,实为 %q", string(b))
}
}
func TestList(t *testing.T) {
dir, err := os.MkdirTemp("", "know_list_*")
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(dir)
s := NewStore(dir)
s.Start()
defer s.Stop()
s.Add("topic1", "内容一")
s.Add("topic2", "内容二")
list := s.List()
if len(list) != 2 {
t.Errorf("expected 2 items, got %d", len(list))
}
}
func TestRemove(t *testing.T) {
dir, err := os.MkdirTemp("", "know_rm_*")
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(dir)
s := NewStore(dir)
s.Start()
defer s.Stop()
s.Add("test", "测试内容")
if err := s.Remove("test"); err != nil {
t.Fatal(err)
}
list := s.List()
if len(list) != 0 {
t.Errorf("expected 0 items after remove, got %d", len(list))
}
}
func TestRemoveNotFound(t *testing.T) {
dir, err := os.MkdirTemp("", "know_notfound_*")
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(dir)
s := NewStore(dir)
s.Start()
defer s.Stop()
if err := s.Remove("nonexistent"); err != nil {
t.Errorf("remove nonexistent should not error, got: %v", err)
}
}
func TestStats(t *testing.T) {
dir, err := os.MkdirTemp("", "know_stats_*")
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(dir)
s := NewStore(dir)
s.Start()
defer s.Stop()
s.Add("a", "内容A")
s.Add("b", "内容B")
stats := s.Stats()
if stats["knowledge_count"].(int) != 2 {
t.Errorf("expected knowledge_count 2, got %v", stats["knowledge_count"])
}
if stats["vector_count"] == nil {
t.Error("expected vector_count in stats")
}
}
func TestSearchNoMatch(t *testing.T) {
dir, err := os.MkdirTemp("", "know_nomatch_*")
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(dir)
s := NewStore(dir)
s.Start()
defer s.Stop()
s.Add("math", "加减乘除是基本运算")
// "电电电电电" 中的字符 "电" 不在文档 "math 加减乘除是基本运算" 的任意 unigram 中
results := s.Search("电电电电电", 5)
if len(results) != 0 {
t.Errorf("expected 0 results for non-matching query, got %d", len(results))
}
}