sync: collector/ 同步自 inkstone/arcrun-rag@7c21a3a(桌面小幫手 0.18.42)

This commit is contained in:
2026-08-27 21:30:21 +08:00
parent 6e80296362
commit 3d1cb83110
18 changed files with 737 additions and 61 deletions
+123
View File
@@ -0,0 +1,123 @@
package collector
import (
"os"
"path/filepath"
"strings"
"testing"
"time"
)
// testOrigin=測試用的三件式(機器/庫/庫內路徑)。
// 內容不重要,重要的是**每個呼叫端都必須交出它**——`inkstone/Arcrun#167` 的教訓是
// 「出處寫的是什麼」不能靠預設值,得由知道自己在哪一台、哪個庫的那一端說出來。
func testOrigin() SourceOrigin {
return SourceOrigin{MachineLabel: "test@Machine", Library: "test-lib", LibraryPath: "測試文件.md"}
}
// wsOriginwikishape 測試用:庫名/機器固定,庫內路徑跟著受測的 relPath 走
//(子資料夾的檔要驗「資料夾沒被弄丟」,所以這一格不能寫死)。
func wsOrigin(relPath string) SourceOrigin {
return SourceOrigin{MachineLabel: "test@Machine", Library: "test-lib", LibraryPath: relPath}
}
// TestSourceOriginNeverEmitsInternalPath 是 `inkstone/Arcrun#167` 的迴歸閘。
//
// leo 2026-08-27 用 n8n 實測:問「原文 wiki 的位置在哪裏?」,AI 照著卡片內文答
// `../小果被AFTEE詐貸.pdf`——**使用者拿著它走不到任何地方**。
// 這裡把那個形狀鎖死:不管三件式缺哪一格,輸出都不准出現 `../`。
func TestSourceOriginNeverEmitsInternalPath(t *testing.T) {
cases := []struct {
name string
origin SourceOrigin
want []string
}{
{
"三件齊全",
SourceOrigin{MachineLabel: "youlinhsieh@Leo-MBA", Library: "youlinhsieh-test1", LibraryPath: "小果被AFTEE詐貸.pdf"},
[]string{"youlinhsieh@Leo-MBA youlinhsieh-test1 小果被AFTEE詐貸.pdf", "`youlinhsieh-test1/小果被AFTEE詐貸.pdf`"},
},
{
// 🔴 票上的驗收條件三:子資料夾裡的檔要定位得到。
// 舊寫法 `../<檔名>` 會把整段資料夾弄丟(實據:inkstoneco 庫的
// system-dev/wiki/trees/2026-08-17-today.md 出處只剩 `../2026-08-17-today.md`)。
"子資料夾不准被弄丟",
SourceOrigin{MachineLabel: "youlinhsieh@Leo-MBA", Library: "inkstoneco", LibraryPath: "system-dev/wiki/trees/2026-08-17-today.md"},
[]string{"inkstoneco system-dev/wiki/trees/2026-08-17-today.md", "`inkstoneco/system-dev/wiki/trees/2026-08-17-today.md`"},
},
{
"機器未知也要誠實,不得退回相對路徑",
SourceOrigin{Library: "kb", LibraryPath: "a/b.md"},
[]string{"機器(未知) kb a/b.md"},
},
{
"庫未知",
SourceOrigin{MachineLabel: "m", LibraryPath: "a/b.md"},
[]string{"m 知識庫(未知) a/b.md", "`a/b.md`"},
},
{
"路徑未知",
SourceOrigin{MachineLabel: "m", Library: "kb"},
[]string{"m kb 庫內路徑(未知)"},
},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
var b strings.Builder
renderSourceLine(&b, c.origin, "某卡")
got := b.String()
if strings.Contains(got, "../") {
t.Fatalf("出處帶內部相對路徑:\n%s", got)
}
for _, w := range c.want {
if !strings.Contains(got, w) {
t.Fatalf("出處少了「%s」:\n%s", w, got)
}
}
// 人話那行不准變成三元組(`## 關聯` 的解析只收含兩個分隔符的行)。
human := strings.Split(strings.TrimSpace(got), "\n")[1]
if strings.Contains(human, triSep) {
t.Fatalf("人話位置那行含三元組分隔符,會被當成一條邊:%s", human)
}
})
}
}
// TestBuildWikiDocSourceLocatesSubfolderDoc 走完整落地路徑(不是只測 renderer):
// 子資料夾裡的原稿,產出的文件卡與概念卡都要寫得出「庫內完整路徑」。
func TestBuildWikiDocSourceLocatesSubfolderDoc(t *testing.T) {
root := t.TempDir()
rel := "專案/會議/週會.md"
if err := os.MkdirAll(filepath.Join(root, "專案", "會議"), 0o755); err != nil {
t.Fatal(err)
}
src := "# 週會\n\n甲和乙討論了進度。\n"
if err := os.WriteFile(filepath.Join(root, filepath.FromSlash(rel)), []byte(src), 0o644); err != nil {
t.Fatal(err)
}
ex := &DocExtract{
Gloss: "週會記錄", Summary: "甲乙討論進度", Points: []string{"討論了進度"},
Concepts: []WikiConcept{{Name: "進度", Gloss: "專案進度", Summary: "專案進度的討論"}},
}
origin := SourceOrigin{MachineLabel: "youlinhsieh@Leo-MBA", Library: "inkstoneco", LibraryPath: rel}
cards, err := BuildWikiDoc(root, rel, src, ex, origin, time.Date(2026, 8, 27, 0, 0, 0, 0, time.UTC))
if err != nil {
t.Fatal(err)
}
if len(cards) == 0 {
t.Fatal("沒產出卡")
}
for _, c := range cards {
body, rerr := os.ReadFile(filepath.Join(root, filepath.FromSlash(c)))
if rerr != nil {
t.Fatal(rerr)
}
card := string(body)
if strings.Contains(card, "../") {
t.Fatalf("%s 仍帶 `../`\n%s", c, card)
}
if !strings.Contains(card, "inkstoneco 專案/會議/週會.md") {
t.Fatalf("%s 的出處定位不到子資料夾:\n%s", c, card)
}
}
}