Files
arcrun-collector/cmd/arcrun-app/diagnostics_export_test.go
T
Leo e5d8cee58b feat(t213): 匯出診斷檔搬進 arcrun-app 本機端(phase 2,接手 t210 落地後的 Progress/FailureBreakdown)
t210(commit 5effcb2)已把「總量進度」「失敗分類」算好並寫進 status.json(現況快照,
不隨閒置歸零)——本次直接讀那兩個欄位,不另算一套(首頁與診斷檔同一組數字)。

新增:
- collector/cmd/arcrun-app/diagnostics_export.go
  - App.ExportDiagnostics():合併本機(daemon 版本/自我更新狀態/Progress/
    FailureBreakdown/略過檔案樣本)與雲端(每帳號打新端點 GET /portal/daemon/diagnostics,
    X-Arcrun-API-Key 認證)成一份 JSON,彈系統存檔對話框存下來
  - mergeDiagnostics 抽成無 IO 純函式,方便測試
- collector/cmd/arcrun-app/diagnostics_export_test.go:5 個離線測試,涵蓋
  同首頁數字/分類名稱原樣照抄不自己判斷/失敗檔名 basename-only/略過清單省略欄位/
  帳號層 cloud 與 cloud_error 互斥
- frontend/src/main.js:「版本與更新」頁加「疑難排解」卡片+匯出按鈕

守住的規則:
- Progress/FailureBreakdown 原樣接住 status.json,不重掃 manifest
- 分類名稱字串只認 collector/progress.go 的 ClassifyFailure,本檔不判斷任何分類
- 失敗檔名 basename-only:借用既有 buildSkipped() 輸出(本來就是 basename+白話標籤)

驗證(實跑,非設計稿):go build/vet/gofmt 全乾淨;go test ./...(collector+
arcrun-app)全過;check-cis.sh/check-render.sh 視覺機械閘全過;wails build 成功
產生含 ExportDiagnostics 的 bindings;拿本機真實 config.json(2 個真帳號)+真網路
跑 buildDiagnosticsPayload():真拿到 update_check.latest=v0.18.23、真列出本機
略過檔案(basename)、兩個真帳號誠實回 cloud_error(線上新端點還沒部署,與總管
實測結果一致)。未在真實 GUI 點過按鈕(本機正跑 production Arcrun.app,避免干擾)。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-08 13:09:48 +08:00

126 lines
5.8 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_export_test.go — t213 phase 2mergeDiagnostics 純函式測試(無網路/無磁碟),
// 涵蓋 leo 08-08 三條規則:①同首頁數字 ②分類名稱只認 ClassifyFailure(本檔不自己判斷)
// ③失敗檔名 basename-only。
import (
"encoding/json"
"strings"
"testing"
collector "arcrun-rag/collector"
)
func TestMergeDiagnostics_SameNumbersAsHomeScreen(t *testing.T) {
// leo 規則①:ProgressFailureBreakdown 必須原樣接住 status.json 裡 t210 已算好的值,
// 不是本檔重新掃 manifest 算出來的——這裡直接餵一組跟首頁會看到的一模一樣的 syncStatus,
// 斷言輸出的 local.progresslocal.failure_breakdown 逐欄位相等。
sync := syncStatus{
Progress: collector.SyncProgress{Total: 9000, Done: 8879, Pending: 101, Stuck: 15, Unreadable: 5},
FailureBreakdown: collector.FailureBreakdown{
Total: 20,
Groups: []collector.FailureGroup{
{Category: collector.FailQuotaExhausted, Count: 12},
{Category: collector.FailNoTextInFile, Count: 5},
{Category: collector.FailOther, Count: 3},
},
},
}
out := mergeDiagnostics(sync, nil, "0.18.23", UpdateInfo{Current: "0.18.23"}, nil)
if out.Local.Progress != sync.Progress {
t.Fatalf("progress 沒有原樣接住:got %+v want %+v", out.Local.Progress, sync.Progress)
}
if out.Local.FailureBreakdown.Total != 20 {
t.Fatalf("failure_breakdown.total = %d, want 20", out.Local.FailureBreakdown.Total)
}
if len(out.Local.FailureBreakdown.Groups) != 3 {
t.Fatalf("failure_breakdown.groups 數量跑掉:got %d want 3", len(out.Local.FailureBreakdown.Groups))
}
// 分母(Q29000 檔 vs 雲端 101 張卡)就是 Progress.Total,必須是真的總量,不是本輪計數。
if out.Local.Progress.Total != 9000 {
t.Fatalf("Q2 的分母跑掉:Total = %d, want 9000", out.Local.Progress.Total)
}
}
func TestMergeDiagnostics_CategoryNamesPassThroughVerbatim(t *testing.T) {
// leo 規則②:分類名稱字串只准住在 collector/progress.go 的 ClassifyFailure。
// 這裡故意餵一個「本檔完全沒見過」的假分類名,驗證 mergeDiagnostics 原樣照抄、
// 不會因為認不得而過濾掉或改寫——它不准對分類名稱做任何判斷。
sync := syncStatus{
FailureBreakdown: collector.FailureBreakdown{
Total: 1,
Groups: []collector.FailureGroup{{Category: "未來才會新增的假分類", Count: 1}},
},
}
out := mergeDiagnostics(sync, nil, "dev", UpdateInfo{}, nil)
if len(out.Local.FailureBreakdown.Groups) != 1 || out.Local.FailureBreakdown.Groups[0].Category != "未來才會新增的假分類" {
t.Fatalf("分類名稱沒有原樣照抄:%+v", out.Local.FailureBreakdown.Groups)
}
}
func TestMergeDiagnostics_SkippedNamesAreBasenameOnly(t *testing.T) {
// leo 08-08 補的紅線:失敗檔名只出 basename,不出完整路徑。
// buildSkipped() 本來就只輸出 filepath.Base()+白話標籤(見 app.go),這裡驗證
// mergeDiagnostics 原樣帶出這個既有保證、且序列化後的 JSON 真的看不到路徑分隔符/使用者名稱。
skipped := &UISkipped{
Files: []string{"教材授權書-Leov2.pagesPages", "舊版報告.doc(舊版 Word"},
More: 2,
}
out := mergeDiagnostics(syncStatus{}, skipped, "dev", UpdateInfo{}, nil)
if len(out.Local.SkippedSample) != 2 {
t.Fatalf("skipped_sample 數量跑掉:%v", out.Local.SkippedSample)
}
for _, name := range out.Local.SkippedSample {
if strings.ContainsAny(name, "/\\") {
t.Fatalf("skipped_sample 洩漏了路徑分隔符(疑似夾帶完整路徑):%q", name)
}
}
if out.Local.SkippedMore != 2 {
t.Fatalf("skipped_more = %d, want 2", out.Local.SkippedMore)
}
// 序列化後再檢查一次(防禦:就算欄位邏輯對,JSON tag 打錯字也可能悄悄漏東西進去)。
raw, err := json.Marshal(out)
if err != nil {
t.Fatal(err)
}
if strings.Contains(string(raw), "/Users/") || strings.Contains(string(raw), `C:\`) {
t.Fatalf("整份 JSON 不該出現絕對路徑:%s", raw)
}
}
func TestMergeDiagnostics_NoSkipped_OmitsSampleFields(t *testing.T) {
// 沒有任何檔案被略過(skipped == nil,同 buildSkipped() 的既有語意)→ 不該生出空陣列佔畫面。
out := mergeDiagnostics(syncStatus{}, nil, "dev", UpdateInfo{}, nil)
if out.Local.SkippedSample != nil {
t.Fatalf("沒有略過任何檔案時 SkippedSample 應為 nil,得到 %v", out.Local.SkippedSample)
}
raw, _ := json.Marshal(out)
if strings.Contains(string(raw), "skipped_sample") {
t.Fatalf("omitempty 沒生效,空狀態不該出現 skipped_sample 欄位:%s", raw)
}
}
func TestMergeDiagnostics_AccountsCarryCloudOrError(t *testing.T) {
// 帳號層:雲端查得到 → cloud 有值;查不到(如目前線上 404,見 t213 部署備註)→ cloud_error
// 誠實帶出原因,兩者互斥,且都要附上是哪個帳號/實例(不然多帳號時分不出是誰的狀態)。
accounts := []accountDiagnostics{
{InstanceName: "youlin.hsieh.dev", Host: "arcrun-cypher-executor.youlin-hsieh-dev.workers.dev",
Cloud: map[string]any{"library_count": float64(3), "triplet_count": float64(191)}},
{InstanceName: "geek6688", Host: "arcrun-cypher-executor.arcrun-fc9490d5.workers.dev",
CloudError: "你的知識庫還是舊版(沒有雲端診斷功能)⇒ 請到 portal 按「立即更新」重裝一次"},
}
out := mergeDiagnostics(syncStatus{}, nil, "dev", UpdateInfo{}, accounts)
if len(out.Accounts) != 2 {
t.Fatalf("accounts 數量跑掉:%d", len(out.Accounts))
}
if out.Accounts[0].Cloud == nil || out.Accounts[0].CloudError != "" {
t.Fatalf("第一個帳號應該只有 cloud、沒有 cloud_error%+v", out.Accounts[0])
}
if out.Accounts[1].Cloud != nil || out.Accounts[1].CloudError == "" {
t.Fatalf("第二個帳號應該只有 cloud_error、沒有 cloud%+v", out.Accounts[1])
}
}