Files
arcrun-collector/direct_multi_test.go
T
Claude e548c57efa fix(t39): daemon 端展開 config 的 ~/ 路徑
安裝器成功頁下載的 config.json 把 manifest 寫成 ~/.arcrun-rag/manifest.json——那是給人
看的寫法,Go 不展開波浪號,os.ReadFile 會去找一個字面上叫 "~" 的資料夾。

修在 daemon 端而非前端:用戶自己手打 config、或把設定搬到另一台機器時 ~/ 都該會動,
而前端也拿不到用戶家目錄。manifest 與 watch_folder(s) 全展開;只認 ~/ 與單獨的 ~,
~user 形式不猜(Go 無法解析他人家目錄);拿不到家目錄就原樣返回讓錯誤誠實浮現。

測試 +2(config 展開含 CJK 路徑與絕對路徑不動、expandHome 六種邊界),collector 全套綠。
2026-07-25 08:18:21 +00:00

181 lines
5.6 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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])
}
}
// t39config 的 `~/` 由 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)%qwant %q", in, got, want)
}
}
}