// 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]) } } // t39:config 的 `~/` 由 daemon 端展開——安裝器成功頁下載的 config.json 用 // `~/.arcrun-rag/manifest.json` 這種人看的寫法,Go 不展開波浪號=會去找字面上叫 "~" 的資料夾。 func TestLoadDirectConfigExpandsHome(t *testing.T) { home, err := os.UserHomeDir() if err != nil || home == "" { t.Skip("拿不到家目錄,跳過") } dir := t.TempDir() cfgPath := filepath.Join(dir, "config.json") body := `{ "watch_folders": ["~/Documents/知識庫", "/tmp/絕對路徑不動"], "manifest": "~/.arcrun-rag/manifest.json", "cypher_url": "https://cypher.example.workers.dev", "namespace": "ns1" }` if err := os.WriteFile(cfgPath, []byte(body), 0o644); err != nil { t.Fatal(err) } cfg, err := LoadDirectConfig(cfgPath) if err != nil { t.Fatalf("LoadDirectConfig: %v", err) } wantManifest := filepath.Join(home, ".arcrun-rag/manifest.json") if cfg.Manifest != wantManifest { t.Errorf("manifest 未展開:got %q want %q", cfg.Manifest, wantManifest) } folders := cfg.Folders() wantFolder := filepath.Join(home, "Documents/知識庫") if folders[0] != wantFolder { t.Errorf("watch_folders[0] 未展開:got %q want %q", folders[0], wantFolder) } if folders[1] != "/tmp/絕對路徑不動" { t.Errorf("絕對路徑不該被動:got %q", folders[1]) } } func TestExpandHomeEdgeCases(t *testing.T) { home, err := os.UserHomeDir() if err != nil || home == "" { t.Skip("拿不到家目錄,跳過") } cases := map[string]string{ "~": home, "~/a/b": filepath.Join(home, "a/b"), "/abs/path": "/abs/path", "relative/dir": "relative/dir", "": "", "~notme/x": "~notme/x", // 只認 `~/`;`~user` 形式不猜(Go 無法解析他人家目錄) } for in, want := range cases { if got := expandHome(in); got != want { t.Errorf("expandHome(%q)=%q,want %q", in, got, want) } } }