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]) } }