e5d8cee58b
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>
126 lines
5.8 KiB
Go
126 lines
5.8 KiB
Go
package main
|
||
|
||
// diagnostics_export_test.go — t213 phase 2:mergeDiagnostics 純函式測試(無網路/無磁碟),
|
||
// 涵蓋 leo 08-08 三條規則:①同首頁數字 ②分類名稱只認 ClassifyFailure(本檔不自己判斷)
|
||
// ③失敗檔名 basename-only。
|
||
import (
|
||
"encoding/json"
|
||
"strings"
|
||
"testing"
|
||
|
||
collector "arcrun-rag/collector"
|
||
)
|
||
|
||
func TestMergeDiagnostics_SameNumbersAsHomeScreen(t *testing.T) {
|
||
// leo 規則①:Progress/FailureBreakdown 必須原樣接住 status.json 裡 t210 已算好的值,
|
||
// 不是本檔重新掃 manifest 算出來的——這裡直接餵一組跟首頁會看到的一模一樣的 syncStatus,
|
||
// 斷言輸出的 local.progress/local.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))
|
||
}
|
||
// 分母(Q2:9000 檔 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.pages(Pages)", "舊版報告.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])
|
||
}
|
||
}
|