| | |
| | | "os" |
| | | "path/filepath" |
| | | "testing" |
| | | |
| | | "github.com/aisim/kb-cli/internal/graph" |
| | | ) |
| | | |
| | | // newV1Store 手工构建一个 v1 schema 的老库(模拟迁移前状态) |
| | |
| | | } |
| | | } |
| | | |
| | | // TestMigrateVersionNotWrittenOnFailure 验证版本写入时序: |
| | | // 若 FTS 重建中途失败,schema_versions 中不得出现 version='2' |
| | | // (否则后续 Open 读到 version=2 直接早退,半迁移状态无法重试)。 |
| | | // 手法:把 v1 库的 nodes.content_fts 列改名,FTS5 external-content |
| | | // 建表引用不存在的列必然真实失败(CreateFTS 第 1 步即报错)。 |
| | | func TestMigrateVersionNotWrittenOnFailure(t *testing.T) { |
| | | store, _ := newV1Store(t) |
| | | defer store.Close() |
| | | |
| | | // 破坏 content 表:content_fts 列改名,使 FTS external-content 建表失败 |
| | | if _, err := store.db.Exec(`ALTER TABLE nodes RENAME COLUMN content_fts TO content_x`); err != nil { |
| | | t.Fatal(err) |
| | | } |
| | | |
| | | if err := store.migrate(); err == nil { |
| | | t.Fatal("migrate 应返回 FTS 建表失败错误, 实际成功") |
| | | } |
| | | |
| | | // 失败后版本号必须未写入(行不存在) |
| | | var v string |
| | | err := store.db.QueryRow(`SELECT value FROM schema_versions WHERE key='version'`).Scan(&v) |
| | | if err == nil { |
| | | t.Fatalf("迁移失败后版本号不应写入, 实际写入了: %s", v) |
| | | } |
| | | if err != sql.ErrNoRows { |
| | | t.Fatalf("查询版本行出错: %v", err) |
| | | } |
| | | } |
| | | |
| | | // TestBackupWALConsistent 验证备份一致性:WAL 模式未 checkpoint 的 |
| | | // 新增数据必须包含在 .bak 里(VACUUM INTO 合并 WAL;旧的 |
| | | // ReadFile 直读主文件做不到)。 |
| | | func TestBackupWALConsistent(t *testing.T) { |
| | | store, dbPath := newV1Store(t) |
| | | defer store.Close() |
| | | |
| | | // 通过活动连接插入一条数据,停留在未 checkpoint 的 WAL 中 |
| | | if _, err := store.db.Exec(`INSERT INTO nodes (path, title, content_fts) |
| | | VALUES ('WAL/002-新节点.md', 'walnode', 'wal数据')`); err != nil { |
| | | t.Fatal(err) |
| | | } |
| | | |
| | | if err := store.backupDB(); err != nil { |
| | | t.Fatalf("backupDB: %v", err) |
| | | } |
| | | // .bak 必须是合法 SQLite 库且包含 WAL 中未 checkpoint 的数据 |
| | | db, err := sql.Open("sqlite3", dbPath+".bak") |
| | | if err != nil { |
| | | t.Fatal(err) |
| | | } |
| | | defer db.Close() |
| | | var n int |
| | | if err := db.QueryRow(`SELECT COUNT(*) FROM nodes WHERE path='WAL/002-新节点.md'`).Scan(&n); err != nil { |
| | | t.Fatal(err) |
| | | } |
| | | if n != 1 { |
| | | t.Fatalf("备份缺少 WAL 未 checkpoint 数据 (count=%d)", n) |
| | | } |
| | | } |
| | | |
| | | // TestFTSTriggers FTS 增删改触发器回归测试: |
| | | // 新建库 → 插节点 → FTS 可查 → UPDATE 改内容 → 新词可查/旧词不可查 → DELETE → 不可查。 |
| | | // 注意:unicode61 分词下 CJK 连续串是整串 token,故用英文词验证触发器行为。 |
| | | func TestFTSTriggers(t *testing.T) { |
| | | store, err := Open(filepath.Join(t.TempDir(), "kb-fts.db")) |
| | | if err != nil { |
| | | t.Fatal(err) |
| | | } |
| | | defer store.Close() |
| | | |
| | | // INSERT 触发器:新插节点立即可被 FTS 查到 |
| | | id, err := store.InsertNode(&graph.Node{ |
| | | Path: "TRG/001.md", |
| | | Title: "trigger node", |
| | | Content: "alpha omega", |
| | | }) |
| | | if err != nil { |
| | | t.Fatal(err) |
| | | } |
| | | if got := store.ftsMatchCount(t, "alpha"); got != 1 { |
| | | t.Fatalf("INSERT 后 FTS 应查到 1 条 alpha, 实际 %d", got) |
| | | } |
| | | |
| | | // UPDATE 触发器:改内容后新词可查、旧词不可查 |
| | | if _, err := store.db.Exec(`UPDATE nodes SET content_fts=? WHERE id=?`, "beta delta", id); err != nil { |
| | | t.Fatal(err) |
| | | } |
| | | if got := store.ftsMatchCount(t, "beta"); got != 1 { |
| | | t.Fatalf("UPDATE 后 FTS 应查到 1 条 beta, 实际 %d", got) |
| | | } |
| | | if got := store.ftsMatchCount(t, "alpha"); got != 0 { |
| | | t.Fatalf("UPDATE 后旧词 alpha 应不可查, 实际 %d 条", got) |
| | | } |
| | | |
| | | // DELETE 触发器:删节点后 FTS 不可查 |
| | | if _, err := store.db.Exec(`DELETE FROM nodes WHERE id=?`, id); err != nil { |
| | | t.Fatal(err) |
| | | } |
| | | if got := store.ftsMatchCount(t, "beta"); got != 0 { |
| | | t.Fatalf("DELETE 后 FTS 不应查到 beta, 实际 %d 条", got) |
| | | } |
| | | } |
| | | |
| | | // ftsMatchCount 统计 FTS 虚拟表中匹配 term 的文档数(测试辅助) |
| | | func (s *Store) ftsMatchCount(t *testing.T, term string) int { |
| | | t.Helper() |
| | | var n int |
| | | if err := s.db.QueryRow(`SELECT COUNT(*) FROM nodes_fts WHERE nodes_fts MATCH ?`, term).Scan(&n); err != nil { |
| | | t.Fatalf("FTS MATCH %q: %v", term, err) |
| | | } |
| | | return n |
| | | } |
| | | |
| | | // 测试辅助:列检查 |
| | | func (s *Store) nodeColumns(t *testing.T) map[string]bool { |
| | | t.Helper() |