package asynctask_test import ( "testing" "time" "github.com/break/junhong_cmp_fiber/pkg/asynctask" ) func TestCompletedTaskUsesCountsForPartialAndAllItemFailures(t *testing.T) { t.Parallel() now := time.Date(2026, 7, 23, 10, 0, 0, 0, time.UTC) cases := []struct { name string success int failed int }{ {name: "部分成功", success: 7, failed: 3}, {name: "业务项全部失败", success: 0, failed: 10}, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { projection, err := asynctask.NewTerminalProjection(asynctask.TerminalResult{ TaskID: "task-1", Status: asynctask.StatusCompleted, TotalCount: 10, SuccessCount: tc.success, FailedCount: tc.failed, StartedAt: &now, CompletedAt: &now, UpdatedAt: now, }) if err != nil { t.Fatalf("构造任务投影失败:%v", err) } if projection.Status != asynctask.StatusCompleted || projection.StatusName != "已完成" { t.Fatalf("业务项处理到终点必须为已完成:%+v", projection) } if projection.Progress != 100 { t.Fatalf("终态进度必须为 100,得到:%d", projection.Progress) } }) } } func TestTerminalProjectionRejectsBrokenCountInvariant(t *testing.T) { t.Parallel() _, err := asynctask.NewTerminalProjection(asynctask.TerminalResult{ TaskID: "task-2", Status: asynctask.StatusCompleted, TotalCount: 10, SuccessCount: 5, FailedCount: 4, UpdatedAt: time.Now(), }) if err == nil { t.Fatal("终态计数不守恒时应拒绝构造公开投影") } } func TestStructuredPayloadRejectsBytes(t *testing.T) { t.Parallel() if err := asynctask.ValidatePayload([]byte(`{"task_id":1}`)); err == nil { t.Fatal("预序列化 []byte 载荷必须被拒绝") } if err := asynctask.ValidatePayload(struct { TaskID uint `json:"task_id"` }{TaskID: 1}); err != nil { t.Fatalf("结构化载荷应通过校验:%v", err) } } func TestClaimRecoversOnlyExpiredProcessingTask(t *testing.T) { t.Parallel() now := time.Date(2026, 7, 23, 10, 0, 0, 0, time.UTC) activeExpiry := now.Add(time.Minute) active, err := asynctask.Claim(asynctask.LeaseState{ Status: asynctask.StatusProcessing, LeaseOwner: "worker-a", LeaseExpiresAt: &activeExpiry, }, "worker-b", now, time.Minute) if err != nil { t.Fatalf("检查有效租约失败:%v", err) } if active.Claimed { t.Fatal("其他 Worker 不得抢占有效租约") } expiredAt := now.Add(-time.Second) recovered, err := asynctask.Claim(asynctask.LeaseState{ Status: asynctask.StatusProcessing, LeaseOwner: "worker-a", LeaseExpiresAt: &expiredAt, }, "worker-b", now, time.Minute) if err != nil { t.Fatalf("恢复过期任务失败:%v", err) } if !recovered.Claimed || recovered.State.LeaseOwner != "worker-b" { t.Fatalf("过期任务应由新 Worker 恢复:%+v", recovered) } } func TestTerminalTaskCannotBeClaimedOrCancelledAgain(t *testing.T) { t.Parallel() result, err := asynctask.Claim(asynctask.LeaseState{Status: asynctask.StatusCompleted}, "worker-a", time.Now(), time.Minute) if err != nil { t.Fatalf("检查终态任务失败:%v", err) } if result.Claimed || asynctask.CanCancel(asynctask.StatusCompleted) { t.Fatal("终态任务不得被重复消费或取消") } }