Files
arcrun-collector/progress_test.go
T
Leo 1efc8123f1 feat(t210): 統計層地基——分母與失敗分類(首頁與診斷檔共用同一組數字)
leo 08-08:「我不要枚舉每個檔案可能的問題和解法,應該是統計的」。
SyncProgress 單位裁定為「份檔案」(地端算不出待同步幾張卡)。
不變式 Total == Done+Pending+Stuck+Unreadable 由測試守著。

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

109 lines
4.1 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.
// progress_test.go — t210 統計層。守的是 leo 08-08 定的兩條驗法:
//
// ① 四個數字相加要等於總數(「不准有對不起來的組合」——這整批的起因就是數字對不起來)
// ② 分類收斂:認不出來的一律進「其他」,不准為新錯誤一直長文案
package collector
import "testing"
func mkEntry(content, ingested string, failCount int) *ManifestEntry {
return &ManifestEntry{ContentHash: content, IngestedHash: ingested, FailCount: failCount}
}
// 🔴 leo 08-08 驗法①:相加必須等於總數。
func TestProgress_SumEqualsTotal(t *testing.T) {
m := &Manifest{Entries: map[string]*ManifestEntry{
"done1.md": mkEntry("h1", "h1", 0), // 已完成
"done2.md": mkEntry("h2", "h2", 0), // 已完成
"changed.md": mkEntry("h3new", "h3old", 0), // 改過了,要重送 → 待處理
"fresh.md": mkEntry("h4", "", 0), // 從沒送過 → 待處理
"retrying.md": mkEntry("h5", "", 3), // 退避中,但還沒放棄 → 待處理
"givenup.md": mkEntry("h6", "", MaxFailBeforeSkip), // 已放棄自動重試 → 卡住
}}
p := m.Progress()
if p.Total != 6 {
t.Fatalf("總數應為 6got %d", p.Total)
}
if p.Done != 2 || p.Pending != 3 || p.Stuck != 1 {
t.Fatalf("分佈錯:Done=%d Pending=%d Stuck=%dwant 2/3/1", p.Done, p.Pending, p.Stuck)
}
if sum := p.Done + p.Pending + p.Stuck + p.Unreadable; sum != p.Total {
t.Fatalf("🔴 數字對不起來:%d+%d+%d+%d=%d ≠ 總數 %d\n"+
"(這正是 Evan 2026-08-08 回報的病,不准再發生)",
p.Done, p.Pending, p.Stuck, p.Unreadable, sum, p.Total)
}
}
// 已放棄重試的不可以混進「待處理」——否則使用者會一直等一件永遠不會發生的事。
func TestProgress_GivenUpIsNotPending(t *testing.T) {
m := &Manifest{Entries: map[string]*ManifestEntry{
"givenup.md": mkEntry("h", "", MaxFailBeforeSkip),
}}
p := m.Progress()
if p.Pending != 0 || p.Stuck != 1 {
t.Fatalf("放棄重試的該算 Stuck 不是 PendingPending=%d Stuck=%d", p.Pending, p.Stuck)
}
}
// 多資料夾/多帳號時首頁講的是總量。
func TestProgress_Add(t *testing.T) {
a := SyncProgress{Total: 9000, Done: 101, Pending: 8879, Stuck: 20}
b := SyncProgress{Total: 10, Done: 10}
got := a.Add(b)
if got.Total != 9010 || got.Done != 111 || got.Pending != 8879 || got.Stuck != 20 {
t.Fatalf("累加錯:%+v", got)
}
}
// 分類用的是實撞過的錯誤原文,不是猜的。
func TestClassifyFailure_RealWorldMessages(t *testing.T) {
cases := []struct{ raw, want string }{
{"4006: you have used up your daily free allocation of 10,000 neurons", FailQuotaExhausted},
{"轉檔失敗(a.pdf):檔案裡沒有可抽取的文字", FailNoTextInFile},
{"尚未支援的檔案格式:.pages", FailUnsupportedFormat},
{"上次失敗(第 5 次),5h38m38s 後重試|原因:4006 neurons", FailQuotaExhausted},
{"connection reset by peer", FailOther},
{"", FailOther},
}
for _, c := range cases {
if got := ClassifyFailure(c.raw); got != c.want {
t.Errorf("ClassifyFailure(%q) = %q, want %q", c.raw, got, c.want)
}
}
}
// 統計只出分類與份數;份數 0 的分類不佔畫面。
func TestBuildFailureBreakdown(t *testing.T) {
b := BuildFailureBreakdown([]string{
"4006 neurons", "4006 neurons",
"尚未支援的檔案格式:.pages",
"something we have never seen",
})
if b.Total != 4 {
t.Fatalf("總數應為 4got %d", b.Total)
}
if len(b.Groups) != 3 {
t.Fatalf("應有 3 個非零分類(0 的不出現),got %d%+v", len(b.Groups), b.Groups)
}
// 順序固定,畫面才不會每次重排
want := []FailureGroup{
{FailUnsupportedFormat, 1},
{FailQuotaExhausted, 2},
{FailOther, 1},
}
for i, w := range want {
if b.Groups[i] != w {
t.Fatalf("第 %d 組 = %+v, want %+v(順序須照 FailCategories", i, b.Groups[i], w)
}
}
// 分組份數加起來要等於總數(同「數字對得起來」的不變式)
sum := 0
for _, g := range b.Groups {
sum += g.Count
}
if sum != b.Total {
t.Fatalf("分組加總 %d ≠ 總數 %d", sum, b.Total)
}
}