sync: collector/ 同步自 inkstone/arcrun-rag@4982b50(桌面小幫手 0.18.43)

This commit is contained in:
2026-08-28 01:48:25 +08:00
parent 3d1cb83110
commit ef5fe1df6f
16 changed files with 1026 additions and 45 deletions
+118
View File
@@ -0,0 +1,118 @@
// cardloop_test.go — 「卡片自我繁殖」迴歸網(inkstone/Arcrun#134 → comment 5025)。
//
// 🔴 2026-08-28 在 leo 的 `~/Desktop/youlinhsieh-test1` 實測到的現象:
// 一份 20 KB 的劇本原稿,`.wiki/` 只落了 4 張卡;而 `system-dev/wiki/cards/` 底下
// 卻在 20 分鐘內從 9 個檔長到 55 個,出現 `arcrun-換柱(arcrun-換柱).md`
// 這種第三代檔名。那不是萃取品質問題,是**產物被自己的整理程序拖回原稿區**:
//
// ① 萃取寫 `<node>/.wiki/換柱.md`(隱藏目錄,Scan 整棵跳過 ⇒ 不會被當原稿)
// ② MigrateCardNames 每輪自動跑,遞迴掃 `system-dev/wiki/cards/`——
// **它沒有跳過隱藏目錄**,於是把 `cards/.wiki/換柱.md`
// 「歸位」成 `cards/arcrun-換柱.md`
// ③ 那個位置 Scan 看得見 ⇒ 下一輪當成新原稿再萃一次 ⇒ 回到 ①
//
// 每一圈都燒一次 Workers AI 額度、往雲端推一批「卡片的卡片」,而且不會停。
//
// 四支測試各釘住迴圈的一個環節(tidy 自動路/tidy 手動路/收檔判準/claude 路),
// 缺一個就會復發。
package collector
import (
"os"
"path/filepath"
"testing"
)
// TestMigrateCardNamesLeavesWikiDirAlone`.wiki/` 是卡片的家,不是「位置不對的舊卡」。
// MigrateCardNames 一個檔都不准動它——動了就等於把產物送回原稿區(迴圈的第②環)。
func TestMigrateCardNamesLeavesWikiDirAlone(t *testing.T) {
root := t.TempDir()
// 卡片產物區底下的 `.wiki/`:萃取正常的落點。
wikiDir := filepath.Join(root, filepath.FromSlash(cardsRelDir), wikiRelDir)
if err := os.MkdirAll(wikiDir, 0o755); err != nil {
t.Fatal(err)
}
card := filepath.Join(wikiDir, "換柱.md")
if err := os.WriteFile(card, []byte("# 換柱\n"), 0o644); err != nil {
t.Fatal(err)
}
mig := MigrateCardNames(root)
if _, err := os.Stat(card); err != nil {
t.Fatalf("`.wiki/` 裡的卡被搬走了(MigrateCardNames 回報 Moved=%d)——"+
"它下一輪就會被當成新原稿再萃一次,迴圈就是這樣長出來的", mig.Moved)
}
dragged := filepath.Join(root, filepath.FromSlash(cardsRelDir), MarkName("換柱.md"))
if _, err := os.Stat(dragged); err == nil {
t.Fatalf("卡片被拖回原稿區:%s", dragged)
}
if mig.Moved != 0 {
t.Fatalf("Moved = %d,期望 0`.wiki/` 不該有任何東西被搬)", mig.Moved)
}
}
// TestTidyLeavesWikiDirAlone:手動 `collector tidy --apply` 走的是另一條路,同樣不准碰。
func TestTidyLeavesWikiDirAlone(t *testing.T) {
root := t.TempDir()
wikiDir := filepath.Join(root, filepath.FromSlash(cardsRelDir), wikiRelDir)
if err := os.MkdirAll(wikiDir, 0o755); err != nil {
t.Fatal(err)
}
card := filepath.Join(wikiDir, "官架子.md")
if err := os.WriteFile(card, []byte("# 官架子\n"), 0o644); err != nil {
t.Fatal(err)
}
rep, err := Tidy(root, true)
if err != nil {
t.Fatal(err)
}
if _, err := os.Stat(card); err != nil {
t.Fatalf("tidy --apply 搬走了 `.wiki/` 裡的卡;報告列了 %d 項", len(rep.Items))
}
for _, it := range rep.Items {
if it.Kind == TidyKindCard && filepath.Base(filepath.Dir(it.Rel)) == wikiRelDir {
t.Fatalf("tidy 把 `.wiki/` 裡的卡列成待整理項:%s → %s", it.Rel, it.To)
}
}
}
// TestMachineOwnedCardsAreNotIngestedAsSourcecurated-wiki 模式收的是「他自己寫的 wiki」,
// 但 `system-dev/wiki/cards/arcrun-*.md` 是 **daemon 上一版親手產的卡**
// (檔名帶 MachineMark 就是它的身分證)。把自己的產物當原稿再萃一次
// =迴圈的第③環,也是這次「卡片的卡片」的第一代來源。
func TestMachineOwnedCardsAreNotIngestedAsSource(t *testing.T) {
plan := IngestPlan{Mode: IngestCuratedWiki, WikiRelDir: "system-dev/wiki"}
userCard := "system-dev/wiki/cards/我自己寫的.md"
if !plan.KeepsFile(userCard) {
t.Fatalf("使用者自己寫的卡被排除了:%s(curated-wiki 要收的就是這個)", userCard)
}
machineCard := "system-dev/wiki/cards/" + MarkName("換柱.md")
if plan.KeepsFile(machineCard) {
t.Fatalf("daemon 自己產的卡被當成原稿收進去:%s——"+
"它會被再萃一次,產出「卡片的卡片」", machineCard)
}
}
// TestSnapshotCardsIgnoresWikiDirclaude 路的同一個洞。
// snapshotCards → diffCards → enforceCardMarks 會把「這一輪新出現的卡」歸位到
// cardRelFor(搬出 `.wiki/`、加前綴)。`.wiki/` 不進快照,就沒有東西可以被搬。
func TestSnapshotCardsIgnoresWikiDir(t *testing.T) {
root := t.TempDir()
wikiDir := filepath.Join(root, filepath.FromSlash(cardsRelDir), wikiRelDir)
if err := os.MkdirAll(wikiDir, 0o755); err != nil {
t.Fatal(err)
}
before := snapshotCards(root)
if err := os.WriteFile(filepath.Join(wikiDir, "Pixel手機.md"), []byte("# Pixel手機\n"), 0o644); err != nil {
t.Fatal(err)
}
changed := diffCards(before, snapshotCards(root))
if len(changed) != 0 {
t.Fatalf("`.wiki/` 裡的新卡進了快照差集:%v——enforceCardMarks 會把它搬出隱藏目錄", changed)
}
}