fix(t213): 診斷檔遮蔽引擎錯誤訊息裡的本機絕對路徑
engine.detail/engine.last_error 在同步引擎沒在跑時,會把 collector 開機 橫幅的完整原文帶出來(direct.go RunDirect 的「監看 %s → %s」那行),其中 含監看資料夾的絕對路徑(如 /Users/xxx/Desktop/...)。這是上一輪(第二/ 三輪)已發現但標記「附帶發現、未修」的洞——首頁在同一狀態下本來就會顯示 同一句話(不動,留給另案判斷),但診斷檔現在會被匯出成檔案交給外部人看, 風險層級跟留在托盤畫面上不同,這輪把它遮掉。 新增 redactLocalPaths(),只在 buildDiagnosticsPayload() 組裝 engine 欄位時 套用:把訊息裡看起來像本機絕對路徑的片段換成「…/<路徑最後一截>」, http(s) URL 先放行原文(避免 URL 裡的 / 被誤判成本機路徑)。只動 diagnostics_export.go 這一端,不改 describeStatus()/collectorFailure()/ direct.go 的訊息本身——那些同時是首頁托盤畫面在用的同一組憑據。 擴充 diagnostics_engine_e2e_test.go(真執行檔+真 supervisor 子行程,零 網路零帳號):監看資料夾改用真實絕對路徑,證明子行程停擺後整份匯出 JSON 找不到該路徑,同時 last_error/detail 仍讀得出「引擎沒在跑、以及為什麼」 (含監看資料夾的名字)。另加 8 個 redactLocalPaths 純函式單測。 go build/go vet/go test ./...(collector+arcrun-app+supervisor 三個 package)全綠;diagnostics_stage_manual_test.go 維持預設 SKIP,本輪未碰 網路/stage。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -217,3 +217,105 @@ func TestMergeDiagnostics_NoLastSync_OmitsSecondsSinceLastSync(t *testing.T) {
|
||||
t.Fatalf("omitempty 沒生效:%s", raw)
|
||||
}
|
||||
}
|
||||
|
||||
// ── t213 phase 4(leo 08-08 追加規則④):engine.detail/engine.last_error 不准帶絕對路徑 ──
|
||||
|
||||
// containsAbsPath 是這批測試共用的「還有沒有絕對路徑」判準:不是只查 `/Users/`
|
||||
// (那只是巧合常見的 macOS 家目錄),是查任何看起來像「本機路徑起手式」的樣子——
|
||||
// Unix 開頭的 `/`(前面是邊界字元或字串開頭)、Windows 磁碟代號 `X:\`;
|
||||
// 跟 redactLocalPaths 一樣先放行 http(s) URL(`https://host/path` 的 `/` 不算本機路徑),
|
||||
// 用同一組邊界原語(urlSpanAt/absPathPrefixLen)掃,不是重寫第二套判準。
|
||||
func containsAbsPath(s string) bool {
|
||||
r := []rune(s)
|
||||
for i := 0; i < len(r); {
|
||||
if j, ok := urlSpanAt(r, i); ok {
|
||||
i = j // 整段 URL 跳過,不是只跳一個字元——URL 內部的 `/` 不算路徑起點
|
||||
continue
|
||||
}
|
||||
if absPathPrefixLen(r, i) > 0 {
|
||||
return true
|
||||
}
|
||||
i++
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func TestRedactLocalPaths_StartupBannerHidesWatchedFolder(t *testing.T) {
|
||||
// 這是真實會撞到的那句話(direct.go RunDirect 的開機橫幅),也是這輪要修的洞。
|
||||
in := `collector direct daemon 啟動:監看 /Users/leo/Desktop/知識庫 → https://arcrun-cypher-executor.youlin-hsieh-dev.workers.dev/webhooks/named/ns/rag_ingest/trigger(每 5s 掃一輪)`
|
||||
got := redactLocalPaths(in)
|
||||
|
||||
if containsAbsPath(got) {
|
||||
t.Fatalf("redactLocalPaths 沒有把絕對路徑遮掉:%q", got)
|
||||
}
|
||||
// 遮蔽不能把資訊遮成啞巴——資料夾最後一截、目的地 URL、輪詢間隔都還要讀得出來。
|
||||
for _, want := range []string{"知識庫", "webhooks/named/ns/rag_ingest/trigger", "每 5s 掃一輪", "監看"} {
|
||||
if !strings.Contains(got, want) {
|
||||
t.Fatalf("redactLocalPaths 把資訊遮成啞巴了,找不到 %q:%q", want, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRedactLocalPaths_MultipleFoldersAllHidden(t *testing.T) {
|
||||
in := `監看 /Users/leo/kb1、/Users/leo/kb2 → https://x.dev/a、https://x.dev/b(每 5s 掃一輪)`
|
||||
got := redactLocalPaths(in)
|
||||
if containsAbsPath(got) {
|
||||
t.Fatalf("多資料夾時仍有絕對路徑殘留:%q", got)
|
||||
}
|
||||
if !strings.Contains(got, "…/kb1") || !strings.Contains(got, "…/kb2") {
|
||||
t.Fatalf("每個資料夾的最後一截應保留(只是加上佔位前綴):%q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRedactLocalPaths_WindowsDrivePath(t *testing.T) {
|
||||
in := `collector direct: config JSON 解析失敗:C:\Users\Leo\AppData\Roaming\arcrun-rag\config.json: unexpected end of JSON input`
|
||||
got := redactLocalPaths(in)
|
||||
if containsAbsPath(got) {
|
||||
t.Fatalf("Windows 路徑沒有被遮掉:%q", got)
|
||||
}
|
||||
if !strings.Contains(got, "config.json") {
|
||||
t.Fatalf("檔名本身應保留:%q", got)
|
||||
}
|
||||
if !strings.Contains(got, "unexpected end of JSON input") {
|
||||
t.Fatalf("錯誤原因不該被路徑遮蔽波及:%q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRedactLocalPaths_OSPathError(t *testing.T) {
|
||||
// os.PathError 的標準形狀:「<動作> <路徑>: <原因>」。
|
||||
in := `status 寫入失敗(不擋看守):open /Users/leo/.arcrun-rag/status.json: permission denied`
|
||||
got := redactLocalPaths(in)
|
||||
if containsAbsPath(got) {
|
||||
t.Fatalf("os.PathError 形狀的訊息沒有被遮掉:%q", got)
|
||||
}
|
||||
if !strings.Contains(got, "status.json") || !strings.Contains(got, "permission denied") {
|
||||
t.Fatalf("檔名與原因應保留:%q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRedactLocalPaths_NoPathPassthroughUnchanged(t *testing.T) {
|
||||
for _, in := range []string{"", "exit status 1", "config JSON 解析失敗", "已自動重試 3 次都失敗,所以重新開啟也沒有用。"} {
|
||||
if got := redactLocalPaths(in); got != in {
|
||||
t.Fatalf("沒有路徑的字串不該被改動:in=%q got=%q", in, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRedactLocalPaths_URLNotMistakenForLocalPath(t *testing.T) {
|
||||
in := `連不上你的知識庫:Get "https://arcrun-cypher-executor.example.workers.dev/webhooks/named/ns/trigger": dial tcp: no such host`
|
||||
got := redactLocalPaths(in)
|
||||
if got != in {
|
||||
t.Fatalf("純 URL(無本機路徑)不該被改動:got %q want %q", got, in)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMergeDiagnostics_EngineDoesNotRedact(t *testing.T) {
|
||||
// 對照組:redactLocalPaths 是 buildDiagnosticsPayload 的事,mergeDiagnostics 本身
|
||||
// 仍要「原樣接住」呼叫端已經處理好的字串(同 TestMergeDiagnostics_EngineCarriesLiveStateVerbatim
|
||||
// 的精神)——已經遮過的字串進來,出去也要一字不改。
|
||||
engine := engineDiagnostics{Alive: false, LastError: "…/kb(已遮蔽)", Detail: "原因:…/kb(已遮蔽)"}
|
||||
out := mergeDiagnostics(syncStatus{}, nil, "dev", UpdateInfo{}, engine, nil)
|
||||
if out.Local.Engine.LastError != engine.LastError || out.Local.Engine.Detail != engine.Detail {
|
||||
t.Fatalf("mergeDiagnostics 不該再動已經處理好的 engine 字串:%+v", out.Local.Engine)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user