Files
Leo 5a140faf1f fix(collector): 機器寫進筆記庫的檔案一律帶前綴,不再跟使用者的頁面撞名(arcrun-rag#60)
leo 2026-08-12:「我的 Logseq 又被覆蓋⋯⋯**不只是加上 journal,可能所有的檔案都加一個前後綴,比如「wiki」**。」

不是資料被蓋掉,是機器產出用了跟他一樣的命名空間(status.md、日期檔那些)
⇒ 他打開資料夾分不出哪些是自己的。**心理上的覆蓋跟實際覆蓋一樣糟。**
2026-08-12 14:57:32 +08:00

103 lines
4.4 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.
// extract_test.go — task 3 claude 萃取路(用 stub 執行檔驗呼叫契約,不需真 claude)。
package collector
import (
"fmt"
"os"
"path/filepath"
"runtime"
"testing"
)
// writeStubClaude 產生一支假 claude:驗證收到的參數與 cwd,並寫一張卡進 cards/。
func writeStubClaude(t *testing.T, dir string, script string) string {
t.Helper()
if runtime.GOOS == "windows" {
t.Skip("stub 腳本測試僅跑 unix")
}
p := filepath.Join(dir, "claude")
if err := os.WriteFile(p, []byte("#!/bin/sh\n"+script), 0o755); err != nil {
t.Fatal(err)
}
return p
}
// 正常路:stub 收到 -p "/rag-extract-file <檔>"、cwd=監看根,寫卡後被 diff 偵測到。
func TestExtractWithClaudeHappyPath(t *testing.T) {
root := t.TempDir()
if err := os.MkdirAll(filepath.Join(root, "system-dev", "wiki", "cards"), 0o755); err != nil {
t.Fatal(err)
}
stub := writeStubClaude(t, t.TempDir(), fmt.Sprintf(`
[ "$1" = "-p" ] || { echo "第一參數應為 -pgot $1" >&2; exit 9; }
case "$2" in "/rag-extract-file 我的筆記.md") ;; *) echo "prompt 錯:$2" >&2; exit 9;; esac
[ "$(pwd)" = "%s" ] || { echo "cwd 錯:$(pwd)" >&2; exit 9; }
mkdir -p system-dev/wiki/cards
printf '# 我的筆記\n\n## 摘要\nstub 卡\n' > "system-dev/wiki/cards/我的筆記.md"
`, root))
cards, err := ExtractWithClaude(stub, root, "我的筆記.md")
if err != nil {
t.Fatal(err)
}
// stub(模擬 /rag-extract-file skill)寫的是**沒有前綴**的檔名——這正是實況:
// 那條路的檔名由 prompt 決定,不歸 Go 管。enforceCardMarks 必須把它補正,
// 讓 claude 路與 gemma/workers-ai 路交出同一種名字(arcrun-rag#60 紅線:只准一種)。
if len(cards) != 1 || cards[0] != "system-dev/wiki/cards/arcrun-我的筆記.md" {
t.Fatalf("cards=%vwant [system-dev/wiki/cards/arcrun-我的筆記.md]", cards)
}
if _, err := os.Stat(filepath.Join(root, "system-dev", "wiki", "cards", "arcrun-我的筆記.md")); err != nil {
t.Fatalf("補正後的卡片檔不存在:%v", err)
}
if _, err := os.Stat(filepath.Join(root, "system-dev", "wiki", "cards", "我的筆記.md")); err == nil {
t.Fatal("沒帶前綴的原檔還在——補正應該是改名,不是複製一份")
}
}
// arcrun-rag#60 第二輪:vault 上,skill 仍會把卡寫進**可見的** system-dev/wiki/cards/
// (那個位置寫死在 prompt 裡,Go 改不到它)⇒ Go 這一側必須把它歸位到隱藏目錄並補標記。
// 這是「破口在 prompt 裡、只能在 Go 收口」的那一類,不補就等於 claude 路照樣污染筆記庫。
func TestExtractWithClaude_VaultRelocatesAndMarks(t *testing.T) {
root := t.TempDir()
mustMkdir(t, filepath.Join(root, "logseq")) // 這是個 Logseq vault
stub := writeStubClaude(t, t.TempDir(), `
mkdir -p system-dev/wiki/cards
printf '# 我的筆記\n\n## 摘要\nstub 卡\n' > "system-dev/wiki/cards/我的筆記.md"
`)
cards, err := ExtractWithClaude(stub, root, "我的筆記.md")
if err != nil {
t.Fatal(err)
}
want := ".arcrun-rag/wiki/cards/arcrun-我的筆記.md"
if len(cards) != 1 || cards[0] != want {
t.Fatalf("cards=%vwant [%s]", cards, want)
}
if _, err := os.Stat(filepath.Join(root, filepath.FromSlash(want))); err != nil {
t.Fatalf("歸位後的卡片不存在:%v", err)
}
// 可見目錄裡不准留下任何東西——vault 的頁面數不能因為 daemon 跑過而增加。
if _, err := os.Stat(filepath.Join(root, "system-dev", "wiki", "cards", "我的筆記.md")); err == nil {
t.Fatal("卡片還留在可見的 system-dev/wiki/cards/vault 會多出一頁")
}
}
// claude 跑完但沒寫卡=誠實報錯(不假綠)。
func TestExtractWithClaudeNoCard(t *testing.T) {
root := t.TempDir()
stub := writeStubClaude(t, t.TempDir(), "exit 0\n")
if _, err := ExtractWithClaude(stub, root, "x.md"); err == nil {
t.Fatal("沒寫卡應報錯")
}
}
// 找不到執行檔=引導改用 gemma 路的錯誤訊息。
// t92:覆蓋 fallback 清單為空,確保即使本機有 claude 安裝,測試仍能驗到「找不到」路徑。
func TestExtractWithClaudeMissingBin(t *testing.T) {
orig := claudeFallbackPaths
claudeFallbackPaths = nil // 停用 fallback,讓找不到 /no/such/claude-bin 就直接報錯
defer func() { claudeFallbackPaths = orig }()
if _, err := ExtractWithClaude("/no/such/claude-bin", t.TempDir(), "x.md"); err == nil {
t.Fatal("缺執行檔應報錯")
}
}