From 5a2be87f658d01bc5b3b7ecdba92ac69bf09dcf7 Mon Sep 17 00:00:00 2001
From: ai_xiaopei <xiaopei@aisim.cn>
Date: Thu, 03 Sep 2026 15:15:37 +0800
Subject: [PATCH] fix(search): 搜索准确性四连修——FTS5 MATCH 引号包裹(连字符不再被解析成 MINUS);KeywordSearch 截断按命中关键词数排序(高频 bigram 不再挤出强相关文档);原词奖励档(完整短语压过泛化 bigram);tags 字段真实参与评分(此前误用 Section)

---
 internal/index/reconcile_test.go |  175 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 1 files changed, 175 insertions(+), 0 deletions(-)

diff --git a/internal/index/reconcile_test.go b/internal/index/reconcile_test.go
new file mode 100644
index 0000000..677015f
--- /dev/null
+++ b/internal/index/reconcile_test.go
@@ -0,0 +1,175 @@
+package index
+
+import (
+	"os"
+	"path/filepath"
+	"testing"
+	"time"
+)
+
+func TestReconcileAdd(t *testing.T) {
+	dir := t.TempDir()
+	if err := os.MkdirAll(filepath.Join(dir, "FAQ"), 0755); err != nil {
+		t.Fatal(err)
+	}
+	content := "---\ntitle: 测试文档\ntags: [t1]\nstatus: 已解决\n---\n正文\n"
+	if err := os.WriteFile(filepath.Join(dir, "FAQ", "001-测试文档.md"), []byte(content), 0644); err != nil {
+		t.Fatal(err)
+	}
+
+	store, err := Open(filepath.Join(t.TempDir(), "kb.db"))
+	if err != nil {
+		t.Fatal(err)
+	}
+	defer store.Close()
+
+	res, err := Reconcile(store, dir)
+	if err != nil {
+		t.Fatal(err)
+	}
+	if res.Added != 1 || res.Unchanged != 0 {
+		t.Errorf("新增场景: %+v", res)
+	}
+	var title, status string
+	if err := store.db.QueryRow(`SELECT title, status FROM nodes WHERE path='FAQ/001-测试文档.md'`).Scan(&title, &status); err != nil {
+		t.Fatal(err)
+	}
+	if title != "测试文档" || status != "已解决" {
+		t.Errorf("节点字段错误: %s / %s", title, status)
+	}
+}
+
+func TestReconcileModify(t *testing.T) {
+	dir := t.TempDir()
+	if err := os.MkdirAll(filepath.Join(dir, "FAQ"), 0755); err != nil {
+		t.Fatal(err)
+	}
+	p := filepath.Join(dir, "FAQ", "001-测试文档.md")
+	if err := os.WriteFile(p, []byte("---\ntitle: 旧标题\n---\n旧内容\n"), 0644); err != nil {
+		t.Fatal(err)
+	}
+
+	store, err := Open(filepath.Join(t.TempDir(), "kb.db"))
+	if err != nil {
+		t.Fatal(err)
+	}
+	defer store.Close()
+	if _, err := Reconcile(store, dir); err != nil {
+		t.Fatal(err)
+	}
+
+	// 修改内容(title 变新标题,内容加"补气")
+	time.Sleep(1100 * time.Millisecond) // mtime 秒级精度,确保 mtime 变化
+	if err := os.WriteFile(p, []byte("---\ntitle: 新标题\n---\n新内容补气\n"), 0644); err != nil {
+		t.Fatal(err)
+	}
+
+	res, err := Reconcile(store, dir)
+	if err != nil {
+		t.Fatal(err)
+	}
+	if res.Modified != 1 || res.Added != 0 {
+		t.Errorf("修改场景: %+v", res)
+	}
+	var title string
+	if err := store.db.QueryRow(`SELECT title FROM nodes WHERE path='FAQ/001-测试文档.md'`).Scan(&title); err != nil {
+		t.Fatal(err)
+	}
+	if title != "新标题" {
+		t.Errorf("标题未更新: %s", title)
+	}
+}
+
+func TestReconcileDelete(t *testing.T) {
+	dir := t.TempDir()
+	if err := os.MkdirAll(filepath.Join(dir, "FAQ"), 0755); err != nil {
+		t.Fatal(err)
+	}
+	p := filepath.Join(dir, "FAQ", "001-测试文档.md")
+	if err := os.WriteFile(p, []byte("---\ntitle: 测试文档\n---\n内容\n"), 0644); err != nil {
+		t.Fatal(err)
+	}
+
+	store, err := Open(filepath.Join(t.TempDir(), "kb.db"))
+	if err != nil {
+		t.Fatal(err)
+	}
+	defer store.Close()
+	if _, err := Reconcile(store, dir); err != nil {
+		t.Fatal(err)
+	}
+	if err := os.Remove(p); err != nil {
+		t.Fatal(err)
+	}
+
+	res, err := Reconcile(store, dir)
+	if err != nil {
+		t.Fatal(err)
+	}
+	if res.Deleted != 1 {
+		t.Errorf("删除场景: %+v", res)
+	}
+	var n int
+	if err := store.db.QueryRow(`SELECT COUNT(*) FROM nodes`).Scan(&n); err != nil {
+		t.Fatal(err)
+	}
+	if n != 0 {
+		t.Errorf("节点未删净: %d", n)
+	}
+}
+
+func TestReconcileUnchanged(t *testing.T) {
+	dir := t.TempDir()
+	if err := os.MkdirAll(filepath.Join(dir, "FAQ"), 0755); err != nil {
+		t.Fatal(err)
+	}
+	if err := os.WriteFile(filepath.Join(dir, "FAQ", "001-测试文档.md"),
+		[]byte("---\ntitle: 测试文档\n---\n内容\n"), 0644); err != nil {
+		t.Fatal(err)
+	}
+
+	store, err := Open(filepath.Join(t.TempDir(), "kb.db"))
+	if err != nil {
+		t.Fatal(err)
+	}
+	defer store.Close()
+	if _, err := Reconcile(store, dir); err != nil {
+		t.Fatal(err)
+	}
+
+	res, err := Reconcile(store, dir)
+	if err != nil {
+		t.Fatal(err)
+	}
+	if res.Unchanged != 1 || res.Added != 0 || res.Modified != 0 {
+		t.Errorf("未变场景: %+v", res)
+	}
+}
+
+func TestQuickCheck(t *testing.T) {
+	dir := t.TempDir()
+	if err := os.MkdirAll(filepath.Join(dir, "FAQ"), 0755); err != nil {
+		t.Fatal(err)
+	}
+	if err := os.WriteFile(filepath.Join(dir, "FAQ", "001-测试文档.md"),
+		[]byte("---\ntitle: 测试文档\n---\n内容\n"), 0644); err != nil {
+		t.Fatal(err)
+	}
+
+	store, err := Open(filepath.Join(t.TempDir(), "kb.db"))
+	if err != nil {
+		t.Fatal(err)
+	}
+	defer store.Close()
+	if _, err := Reconcile(store, dir); err != nil {
+		t.Fatal(err)
+	}
+
+	dirty, err := QuickCheck(store, dir)
+	if err != nil {
+		t.Fatal(err)
+	}
+	if dirty {
+		t.Error("无差异时 QuickCheck 应 false")
+	}
+}

--
Gitblit v1.10.0