Files
Leo f10d10747c fix(collector): 萃取產物加 vault 辨識+落卡不再無條件覆蓋(arcrun-rag#60)
事故:daemon 完全沒有 vault 辨識,把萃取卡片寫進使用者的 Logseq vault
(system-dev/wiki/cards/ 對一般資料夾沒事,對 vault=憑空多出頁面,
2026-08-10 leo 實撞:25 個簡體字卡片污染 ~/Documents/KB)。

修法(照票上指示搬現成的,不重新設計):
- collector/vault.go:DetectVaultType/IsVault,判準逐條抄自
  system-dev-template/scripts/install.sh:209-221(先查 logseq/、再查 .obsidian/)。
  已用同一批 fixture 資料夾跑過 install.sh 與這支 Go 版,五種情境(logseq/
  obsidian/一般資料夾/兩者皆有/空資料夾)IS_VAULT 判斷逐一比對一致。
- extract.go:cardsRelDirFor() 依 IsVault 決定卡片相對路徑——非 vault 不變
  (system-dev/wiki/cards/),vault 改落 .arcrun-rag/wiki/cards/(點開頭隱藏
  目錄,Logseq/Obsidian 預設不掃描,跟 daemon 自己 scan.go 的隱藏目錄跳過規則
  一致)。extract_workersai.go/extract_gemma.go/direct.go 的落卡與下架清除
  都改用這個函式,三處對同一個 absRoot 保證同一個答案。
- safewrite.go:safeWriteCard() 取代兩處無條件 os.WriteFile——目標已存在且
  內容不同就先備份成 <dest>.bak-<unixnano> 才覆寫;內容相同則不動(不產生
  垃圾備份);備份失敗就整個中止,不無聲蓋掉使用者機器上已有的東西。

驗證(見 PR/commit 說明附的實測輸出):
- 用 fake Logseq vault fixture 重現舊行為(卡片確實落在 system-dev/wiki/cards/),
  再用同一份 fixture 驗新行為(卡片改落 .arcrun-rag/wiki/cards/,vault 根目錄
  非隱藏 .md 數量不變、journals/ 原稿位元不動)。
- 故意放同名既有卡片,跑完既有內容被備份、新內容確實寫入,未無聲遺失。
- 非 vault 既有測試(extract_gemma_test.go 原有三支)全數不動照過,確認
  一般資料夾行為零改變。

範圍外(留給下一輪):ExtractWithClaude(extract.go 的 claude 路)與其
templatefs/.claude/commands/rag-extract-file.md 技能檔仍硬寫 system-dev/wiki/cards/,
但這條路目前在 direct.go 的 RunDirectOnce 是不支援狀態(cfg.Extractor 只認
workers-ai/gemma),非本次事故的作用路徑,故未動。

不影響:leo 機器上的 daemon(未重啟、未重新打包);此修復要生效還要
①總管審過併 main ②重新打包桌面版裝上他機器 ③總管用新二進位實測,
三件缺一不可(見 issue #60 leo 的重啟條件)。
2026-08-11 12:12:51 +08:00

100 lines
3.3 KiB
Go
Raw Permalink 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.
// vault_test.go — arcrun-rag#60:驗 vault 偵測跟 system-dev-template/scripts/install.sh:209-221
// 的 bash 判準逐條對齊(同一個資料夾兩邊必須得出同一個答案)。
package collector
import (
"os"
"path/filepath"
"testing"
)
func TestDetectVaultType_Logseq(t *testing.T) {
root := t.TempDir()
mustMkdir(t, filepath.Join(root, "logseq"))
mustMkdir(t, filepath.Join(root, "pages"))
mustMkdir(t, filepath.Join(root, "journals"))
if got := DetectVaultType(root); got != VaultLogseq {
t.Fatalf("DetectVaultType=%qwant %q", got, VaultLogseq)
}
if !IsVault(root) {
t.Fatal("IsVault 應為 true")
}
}
func TestDetectVaultType_Obsidian(t *testing.T) {
root := t.TempDir()
mustMkdir(t, filepath.Join(root, ".obsidian"))
if got := DetectVaultType(root); got != VaultObsidian {
t.Fatalf("DetectVaultType=%qwant %q", got, VaultObsidian)
}
if !IsVault(root) {
t.Fatal("IsVault 應為 true")
}
}
func TestDetectVaultType_PlainFolder(t *testing.T) {
root := t.TempDir()
mustMkdir(t, filepath.Join(root, "docs"))
if got := DetectVaultType(root); got != VaultNone {
t.Fatalf("DetectVaultType=%qwant VaultNone(一般資料夾不該被認成 vault", got)
}
if IsVault(root) {
t.Fatal("IsVault 應為 false")
}
}
// 兩者都有時,install.sh 用 if/eliflogseq 優先)——Go 這邊也要一樣的優先序。
func TestDetectVaultType_BothPresent_LogseqWins(t *testing.T) {
root := t.TempDir()
mustMkdir(t, filepath.Join(root, "logseq"))
mustMkdir(t, filepath.Join(root, ".obsidian"))
if got := DetectVaultType(root); got != VaultLogseq {
t.Fatalf("DetectVaultType=%qwant %qinstall.sh 用 if/eliflogseq 優先)", got, VaultLogseq)
}
}
// 空資料夾也不該被誤判成 vault。
func TestDetectVaultType_EmptyFolder(t *testing.T) {
root := t.TempDir()
if got := DetectVaultType(root); got != VaultNone {
t.Fatalf("DetectVaultType=%qwant VaultNone", got)
}
}
// install.sh 判準是「-d logseq」——檔案(非目錄)叫這名字不算數。
func TestDetectVaultType_FileNotDir_DoesNotCount(t *testing.T) {
root := t.TempDir()
if err := os.WriteFile(filepath.Join(root, "logseq"), []byte("not a dir"), 0o644); err != nil {
t.Fatal(err)
}
if got := DetectVaultType(root); got != VaultNone {
t.Fatalf("DetectVaultType=%qwant VaultNonelogseq 是檔案不是目錄,不該算 vault)", got)
}
}
// cardsRelDirFor:非 vault 行為不變(回歸),vault 改落隱藏目錄。
func TestCardsRelDirFor(t *testing.T) {
plain := t.TempDir()
if got := cardsRelDirFor(plain); got != cardsRelDir {
t.Fatalf("非 vaultcardsRelDirFor=%qwant %q(既有行為不能變)", got, cardsRelDir)
}
vault := t.TempDir()
mustMkdir(t, filepath.Join(vault, "logseq"))
if got := cardsRelDirFor(vault); got != vaultCardsRelDir {
t.Fatalf("vaultcardsRelDirFor=%qwant %q", got, vaultCardsRelDir)
}
// vault 目的地必須是隱藏(點開頭)路徑——這是「Logseq/Obsidian/daemon 自己的 scan.go
// 都不掃描」的唯一保證來源,不能改掉。
if filepath.Base(filepath.Dir(vaultCardsRelDir))[0] != '.' && vaultCardsRelDir[0] != '.' {
t.Fatalf("vaultCardsRelDir=%q 不是隱藏路徑,vault 頁面數保護會失效", vaultCardsRelDir)
}
}
func mustMkdir(t *testing.T, p string) {
t.Helper()
if err := os.MkdirAll(p, 0o755); err != nil {
t.Fatal(err)
}
}