Files
arcrun-collector/cmd/arcrun-app/diagnostics_engine_e2e_test.go
T
Leo 0c6fb43117 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>
2026-08-08 16:36:20 +08:00

152 lines
7.4 KiB
Go
Raw 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.
package main
// diagnostics_engine_e2e_test.go — t213 phase 3 端到端驗證(考卷 Q2 缺口)。
//
// 舊版診斷檔看得到 pending=0,但分不出「真的做完了」還是「daemon 早就掛了沒人知道」——
// 因為完全沒有時間資訊。這支測試用**真的執行檔+真的 supervisor 子行程**(同 App 在真機上
// 做的事,見 supervise.go startSupervisor),證明新加的 local.engine 這組欄位不是憑空編出來
// 的假象,而是真的跟著子行程死活變動:
//
// 情境一(存活中):子行程剛拉起、跑過至少一輪 → engine.alive=true、
// seconds_since_last_sync 是幾秒內的新鮮心跳。
// 情境二(考卷 Q2 的核心,刻意製造「同步停擺」):把子行程停掉(模擬同步引擎掛掉/
// 停止——不是「整個 App 被關掉」那種情況,那種情況下 App 根本不會被打開來按
// 「匯出診斷檔」,不在本檔驗證範圍)→ engine.alive=false、headline 講人話說明、
// last_sync 凍結在停止前那一刻不再前進、seconds_since_last_sync 隨等待時間持續變大。
//
// 刻意零帳號、零網路:這支測試只驗「心跳(LastSync)跟著子行程死活走」這條機制本身,
// 不需要任何雲端憑證,任何機器上都能跑、都穩定。
import (
"encoding/json"
"os"
"os/exec"
"path/filepath"
"strings"
"testing"
"time"
"arcrun-rag/collector/supervisor"
)
func TestDiagnosticsEngine_ReflectsRealSupervisorLifecycle(t *testing.T) {
if testing.Short() {
t.Skip("拉真子行程+等真心跳,跑起來要十幾秒,-short 跳過")
}
// 🔴 先編再改 HOME(同 freshinstall_e2e_test.go 的教訓:Go 模組快取會寫進 HOME,
// 先改 HOME 再 build 會把唯讀快取寫進暫存目錄,測試結束清不掉)。
bin := filepath.Join(t.TempDir(), "arcrun-app-test")
if out, err := exec.Command("go", "build", "-o", bin, ".").CombinedOutput(); err != nil {
t.Fatalf("編不出執行檔:%v\n%s", err, out)
}
home := t.TempDir()
t.Setenv("HOME", home)
t.Setenv("USERPROFILE", home)
// t213 phase 4:監看資料夾要是一個**真的絕對路徑**,才能重現「collector 開機橫幅把
// 監看路徑印進 stderr → supervisor 記成 LastError → 診斷檔把它原樣帶出去」這個洞
// (見 direct.go RunDirect 的「監看 %s → %s」那行;leo 08-08 附帶發現、本輪要修的問題)。
watchDir := filepath.Join(home, "知識庫")
if err := os.MkdirAll(watchDir, 0o755); err != nil {
t.Fatalf("建監看資料夾失敗:%v", err)
}
// 需要至少一個帳號:LoadDirectConfig 會拒絕零帳號的設定(「缺必填欄位:accounts」,
// 連迴圈都進不去),但這個帳號**不必連得上**——cypher_url 刻意指向一個解析不出來的
// 網址,驗證的是「心跳(LastSync)跟著子行程死活走」這條機制,不是雲端連線本身。
cfg := &directConfig{
Manifest: filepath.Join(home, ".arcrun-rag", "manifest.json"),
Accounts: []accountCfg{{
CypherURL: "https://example.invalid.test", Namespace: "test", APIKey: "test",
WatchFolders: []string{watchDir},
}},
}
if err := saveCfg(cfg); err != nil {
t.Fatalf("存設定失敗:%v", err)
}
sup = supervisor.New(bin, configPath())
sup.ArgPrefix = []string{collectorModeFlag}
t.Cleanup(func() { sup.Stop() })
sup.Start()
// ── 情境一:等到子行程真的活著、心跳過至少一輪 ──
deadline := time.Now().Add(15 * time.Second)
for {
s := loadSyncStatus()
if collectorAlive() && s.LastSync != "" {
break
}
if time.Now().After(deadline) {
t.Fatalf("等 15 秒子行程都沒完成第一輪心跳(alive=%v, LastSync=%q", collectorAlive(), s.LastSync)
}
time.Sleep(200 * time.Millisecond)
}
app := &App{}
aliveOut := app.buildDiagnosticsPayload()
aliveJSON, _ := json.MarshalIndent(aliveOut, "", " ")
t.Logf("── 情境一(存活中)完整診斷檔 ──\n%s", aliveJSON)
if !aliveOut.Local.Engine.Alive {
t.Fatalf("情境一:engine.alive 應為 true,得到 %+v", aliveOut.Local.Engine)
}
if aliveOut.Local.Engine.SecondsSinceLastSync == nil || *aliveOut.Local.Engine.SecondsSinceLastSync > 10 {
t.Fatalf("情境一:last_sync 應該是幾秒內的新鮮心跳,得到 seconds_since_last_sync=%v",
aliveOut.Local.Engine.SecondsSinceLastSync)
}
// ── 情境二:停掉子行程,模擬同步引擎掛了/被停止(考卷 Q2)──
frozenLastSync := aliveOut.Local.Engine.LastSync
sup.Stop()
// 等一段「真的有時間流逝」,讓 seconds_since_last_sync 明顯拉開(不是量測誤差)。
time.Sleep(6 * time.Second)
stoppedOut := app.buildDiagnosticsPayload()
stoppedJSON, _ := json.MarshalIndent(stoppedOut, "", " ")
t.Logf("── 情境二(已停擺)完整診斷檔 ──\n%s", stoppedJSON)
if stoppedOut.Local.Engine.Alive {
t.Fatal("情境二:子行程已經 Stop()engine.alive 應為 false")
}
if stoppedOut.Local.Engine.LastSync != frozenLastSync {
t.Fatalf("情境二:子行程停了之後不該再有新心跳,last_sync 卻變了:停前 %q,停後 %q",
frozenLastSync, stoppedOut.Local.Engine.LastSync)
}
if stoppedOut.Local.Engine.SecondsSinceLastSync == nil || *stoppedOut.Local.Engine.SecondsSinceLastSync < 5 {
t.Fatalf("情境二:心跳停了 6 秒以上,seconds_since_last_sync 應該 >=5,得到 %v",
stoppedOut.Local.Engine.SecondsSinceLastSync)
}
if stoppedOut.Local.Engine.Headline == "" {
t.Fatal("情境二:alive=false 時 headline 不該是空的——這是給人看的『還在不在跑』答案")
}
t.Logf("✅ Q2 證據鏈完整:pending 不變的情況下,alivelast_syncseconds_since_last_sync 三者都反映了子行程真的停了")
// ── t213 phase 4leo 08-08 附帶發現、本輪要修)──
// alive=false 這個狀態下,engine.detailengine.last_error 過去會帶出 collector 開機
// 橫幅的完整原文,其中含監看資料夾的絕對路徑(見 direct.go RunDirect 的
// 「監看 %s → %s」那行)。這裡的 watchDir 就是刻意設成真實絕對路徑,用來重現這個洞:
// 驗證整份匯出 JSON 裡找不到它,同時確認「引擎沒在跑、以及為什麼」仍然讀得懂
// (不能把資訊遮成啞巴——資料夾最後一截「知識庫」要還在)。
if strings.Contains(string(stoppedJSON), watchDir) {
t.Fatalf("情境二:整份診斷檔仍然帶出監看資料夾的絕對路徑 %q:%s", watchDir, stoppedJSON)
}
if strings.Contains(string(stoppedJSON), home) {
t.Fatalf("情境二:整份診斷檔仍然帶出本機家目錄路徑 %q:%s", home, stoppedJSON)
}
if containsAbsPath(string(stoppedJSON)) {
t.Fatalf("情境二:整份診斷檔(掃全文,不只 engine 兩個欄位)仍有本機絕對路徑殘留:%s", stoppedJSON)
}
reason := stoppedOut.Local.Engine.LastError + stoppedOut.Local.Engine.Detail
if reason == "" {
t.Fatal("情境二:alive=false 時 last_errordetail 不該兩個都是空的——『為什麼沒在跑』答不出來了")
}
if !strings.Contains(reason, "知識庫") {
t.Fatalf("情境二:遮蔽把資訊遮成啞巴了——連監看資料夾叫什麼名字都看不出來:last_error=%q detail=%q",
stoppedOut.Local.Engine.LastError, stoppedOut.Local.Engine.Detail)
}
t.Logf("✅ t213 phase 4:路徑遮掉了,但『引擎沒在跑、以及為什麼(監看的是「知識庫」資料夾)』還讀得懂")
}