// direct_multi_test.go — daemon-beta task 1 多資料夾 config(單數相容/manifest 分根/多根掃描)。 package main import ( "encoding/json" "os" "path/filepath" "testing" ) func writeDirectConfig(t *testing.T, dir string, cfg map[string]any) string { t.Helper() data, _ := json.Marshal(cfg) p := filepath.Join(dir, "config.json") if err := os.WriteFile(p, data, 0o600); err != nil { t.Fatal(err) } return p } // 單數舊制照舊可載入;Folders() 正規化=一根。 func TestLoadDirectConfigSingularCompat(t *testing.T) { dir := t.TempDir() p := writeDirectConfig(t, dir, map[string]any{ "watch_folder": "/tmp/kb", "manifest": filepath.Join(dir, "m.json"), "cypher_url": "https://x.example", "namespace": "demo", }) c, err := LoadDirectConfig(p) if err != nil { t.Fatal(err) } if got := c.Folders(); len(got) != 1 || got[0] != "/tmp/kb" { t.Fatalf("Folders()=%v", got) } // 單根 manifest 沿用原路徑(升級不丟狀態) if mp := c.manifestPathFor("/tmp/kb"); mp != c.Manifest { t.Fatalf("單根 manifest 應沿用原路徑,got %s", mp) } } // 只填 watch_folders 也合法;單數與清單並存時去重保序。 func TestLoadDirectConfigMulti(t *testing.T) { dir := t.TempDir() p := writeDirectConfig(t, dir, map[string]any{ "watch_folder": "/tmp/a", "watch_folders": []string{"/tmp/b", "/tmp/a", "/tmp/c"}, "manifest": filepath.Join(dir, "m.json"), "cypher_url": "https://x.example", "namespace": "demo", }) c, err := LoadDirectConfig(p) if err != nil { t.Fatal(err) } got := c.Folders() want := []string{"/tmp/a", "/tmp/b", "/tmp/c"} if len(got) != len(want) { t.Fatalf("Folders()=%v want %v", got, want) } for i := range want { if got[i] != want[i] { t.Fatalf("Folders()[%d]=%s want %s", i, got[i], want[i]) } } // 多根=每根 manifest 各一份、彼此不同、且尾碼對同一路徑穩定 m1 := c.manifestPathFor("/tmp/a") m2 := c.manifestPathFor("/tmp/b") if m1 == m2 || m1 == c.Manifest { t.Fatalf("多根 manifest 應分根:%s vs %s", m1, m2) } if m1 != c.manifestPathFor("/tmp/a") { t.Fatal("同路徑尾碼應穩定") } } // 兩欄都空=缺必填。 func TestLoadDirectConfigMissingFolders(t *testing.T) { dir := t.TempDir() p := writeDirectConfig(t, dir, map[string]any{ "manifest": filepath.Join(dir, "m.json"), "cypher_url": "https://x.example", "namespace": "demo", }) if _, err := LoadDirectConfig(p); err == nil { t.Fatal("兩欄皆空應報缺必填") } } // 多根 dry-run:各根的檔案都被掃到、事件標 Root、manifest 各自獨立。 func TestRunDirectOnceMultiRootDryRun(t *testing.T) { base := t.TempDir() rootA := filepath.Join(base, "rootA") rootB := filepath.Join(base, "rootB") for _, d := range []string{rootA, rootB} { if err := os.MkdirAll(d, 0o755); err != nil { t.Fatal(err) } } if err := os.WriteFile(filepath.Join(rootA, "a.md"), []byte("# A"), 0o644); err != nil { t.Fatal(err) } if err := os.WriteFile(filepath.Join(rootB, "b.md"), []byte("# B"), 0o644); err != nil { t.Fatal(err) } cfg := &DirectConfig{ WatchFolders: []string{rootA, rootB}, Manifest: filepath.Join(base, "manifest.json"), CypherURL: "https://x.example", Namespace: "demo", MaxRemoved: DefaultMaxRemovedRatio, } results, exit, _ := RunDirectOnce(cfg, true) if exit != 0 { t.Fatalf("dry-run 不應失敗:%+v", results) } byRoot := map[string][]string{} for _, r := range results { if r.Status != "planned" { t.Fatalf("dry-run 事件應為 planned:%+v", r) } byRoot[r.Root] = append(byRoot[r.Root], r.Path) } if len(byRoot[rootA]) != 1 || byRoot[rootA][0] != "a.md" { t.Fatalf("rootA 事件錯:%v", byRoot[rootA]) } if len(byRoot[rootB]) != 1 || byRoot[rootB][0] != "b.md" { t.Fatalf("rootB 事件錯:%v", byRoot[rootB]) } }