Files
code-utils/sync_docs_test.go

142 lines
4.9 KiB
Go
Raw Normal View History

2026-08-14 07:52:01 +08:00
package main
import (
"path/filepath"
"strings"
"testing"
)
// 项目/规则文档的生成与套用(不依赖 MySQL 的本地部分)。
// v2 语义:身份文档只按 name 匹配更新,不再从文档落库项目;
// 项目落地依赖 paths:<machineID> 行applyMachinePaths或用户手动绑定。
func TestProjectsDocRoundTrip(t *testing.T) {
dir := t.TempDir()
s, e := OpenStore(filepath.Join(dir, "docs.db"))
if e != nil {
t.Fatal(e)
}
defer s.db.Close()
a := NewApp()
a.store = s
doc, e := a.projectsDoc()
if e != nil || doc != "[]" {
t.Fatalf("empty doc want [], got %q err=%v", doc, e)
}
// 远端身份文档v1 旧格式,元素带 path本机没有这些项目 → 全部进待绑定,不落库。
p1 := filepath.Join(dir, "alpha")
p2 := filepath.Join(dir, "beta")
remote := `[{"name":"Alpha","path":` + jsonStr(p1) + `,"description":"d1","group":"Work","favorite":true},` +
`{"name":"Beta","path":` + jsonStr(p2) + `}]`
if e := a.applyProjectsDoc(remote); e != nil {
t.Fatal(e)
}
var n int
_ = s.db.QueryRow(`SELECT COUNT(*) FROM projects`).Scan(&n)
if n != 0 {
t.Fatalf("identity doc must not insert projects, got %d", n)
}
if pending, _ := a.ListCloudPendingProjects(); len(pending) != 2 {
t.Fatalf("expect 2 pending cloud projects, got %d", len(pending))
}
// 本机 paths 行到达(同机换库场景)→ 项目落库,随后身份文档补齐分组/收藏。
if e := a.applyMachinePaths(`{"Alpha":` + jsonStr(p1) + `,"Beta":` + jsonStr(p2) + `}`); e != nil {
t.Fatal(e)
}
if e := a.applyProjectsDoc(remote); e != nil {
t.Fatal(e)
}
var gid int64
if e := s.db.QueryRow(`SELECT group_id FROM projects WHERE path=?`, p1).Scan(&gid); e != nil || gid == 1 {
t.Fatalf("alpha should be in new group, gid=%d err=%v", gid, e)
}
var gname string
_ = s.db.QueryRow(`SELECT name FROM project_groups WHERE id=?`, gid).Scan(&gname)
if gname != "Work" {
t.Fatalf("group name want Work, got %q", gname)
}
var fav int
_ = s.db.QueryRow(`SELECT COUNT(*) FROM favorites f JOIN projects p ON p.id=f.project_id WHERE p.path=?`, p1).Scan(&fav)
if fav != 1 {
t.Fatal("alpha should be favorite")
}
if pending, _ := a.ListCloudPendingProjects(); len(pending) != 0 {
t.Fatalf("pending should be empty after paths applied, got %d", len(pending))
}
// 再生成文档应包含两个项目且不带 path重复套用幂等。
doc, e = a.projectsDoc()
if e != nil || !strings.Contains(doc, "Alpha") || !strings.Contains(doc, "Beta") {
t.Fatalf("doc missing entries: %q err=%v", doc, e)
}
if strings.Contains(doc, `"path"`) {
t.Fatalf("v2 doc must not contain path: %q", doc)
}
if e := a.applyProjectsDoc(remote); e != nil {
t.Fatal(e)
}
_ = s.db.QueryRow(`SELECT COUNT(*) FROM projects`).Scan(&n)
if n != 2 {
t.Fatalf("apply should be idempotent, projects=%d", n)
}
// 更新改名v1 文档按 path 匹配到本地项目)+ 取消收藏;本地多出的项目不受影响(只增改不删)。
remote2 := `[{"name":"Alpha2","path":` + jsonStr(p1) + `,"description":"d2","group":"Work","favorite":false}]`
if e := a.applyProjectsDoc(remote2); e != nil {
t.Fatal(e)
}
var name string
_ = s.db.QueryRow(`SELECT name FROM projects WHERE path=?`, p1).Scan(&name)
if name != "Alpha2" {
t.Fatalf("want renamed Alpha2, got %q", name)
}
_ = s.db.QueryRow(`SELECT COUNT(*) FROM favorites f JOIN projects p ON p.id=f.project_id WHERE p.path=?`, p1).Scan(&fav)
if fav != 0 {
t.Fatal("favorite should be removed")
}
_ = s.db.QueryRow(`SELECT COUNT(*) FROM projects`).Scan(&n)
if n != 2 {
t.Fatalf("pull must not delete local projects, got %d", n)
}
}
func TestRulesDocRoundTrip(t *testing.T) {
s, e := OpenStore(filepath.Join(t.TempDir(), "rules.db"))
if e != nil {
t.Fatal(e)
}
defer s.db.Close()
a := NewApp()
a.store = s
if _, e := s.AddRule("*.bak", "custom"); e != nil {
t.Fatal(e)
}
doc, e := a.rulesDoc()
if e != nil || !strings.Contains(doc, "*.bak") {
t.Fatalf("doc should contain custom rule: %q err=%v", doc, e)
}
var builtinBefore int
_ = s.db.QueryRow(`SELECT COUNT(*) FROM exclusion_rules WHERE builtin=1`).Scan(&builtinBefore)
// 远端替换自定义部分:*.bak 消失,*.tmp 出现,内置规则数不变。
if e := a.applyRulesDoc(`[{"pattern":"*.tmp","category":"custom"}]`); e != nil {
t.Fatal(e)
}
var nBak, nTmp, builtinAfter int
_ = s.db.QueryRow(`SELECT COUNT(*) FROM exclusion_rules WHERE pattern='*.bak'`).Scan(&nBak)
_ = s.db.QueryRow(`SELECT COUNT(*) FROM exclusion_rules WHERE pattern='*.tmp' AND builtin=0`).Scan(&nTmp)
_ = s.db.QueryRow(`SELECT COUNT(*) FROM exclusion_rules WHERE builtin=1`).Scan(&builtinAfter)
if nBak != 0 || nTmp != 1 || builtinAfter != builtinBefore {
t.Fatalf("replace failed: bak=%d tmp=%d builtin %d->%d", nBak, nTmp, builtinBefore, builtinAfter)
}
}
// jsonStr 把路径安全编码为 JSON 字符串Windows 反斜杠)。
func jsonStr(s string) string {
b := strings.ReplaceAll(s, `\`, `\\`)
return `"` + b + `"`
}