package asynctask_test import ( "context" "testing" "time" "gorm.io/gorm" storepkg "github.com/break/junhong_cmp_fiber/internal/infrastructure/asynctask" "github.com/break/junhong_cmp_fiber/internal/testutil" contract "github.com/break/junhong_cmp_fiber/pkg/asynctask" ) func TestPostgresTaskTransitionsAreConditionalAndRecoverExpiredLease(t *testing.T) { db := testutil.NewPostgresTransaction(t) createTaskContractTable(t, db) store, err := storepkg.NewStore(db, taskDefinition()) if err != nil { t.Fatalf("创建任务契约 Store 失败:%v", err) } now := time.Date(2026, 7, 23, 10, 0, 0, 0, time.UTC) if err := db.Exec("INSERT INTO test_async_contract_task (id, status, updated_at) VALUES (1, 1, ?)", now).Error; err != nil { t.Fatalf("准备待处理任务失败:%v", err) } claimed, err := store.Claim(context.Background(), 1, "worker-a", now, time.Minute) if err != nil || !claimed { t.Fatalf("领取待处理任务失败:%v,领取:%v", err, claimed) } claimed, err = store.Claim(context.Background(), 1, "worker-b", now.Add(30*time.Second), time.Minute) if err != nil || claimed { t.Fatalf("不得抢占有效租约:%v,领取:%v", err, claimed) } claimed, err = store.Claim(context.Background(), 1, "worker-b", now.Add(2*time.Minute), time.Minute) if err != nil || !claimed { t.Fatalf("过期任务应由新 Worker 恢复:%v,领取:%v", err, claimed) } renewed, err := store.Renew(context.Background(), 1, "worker-a", now.Add(150*time.Second), 2*time.Minute) if err != nil || renewed { t.Fatalf("旧租约所有者不得续租:%v,续租:%v", err, renewed) } renewed, err = store.Renew(context.Background(), 1, "worker-b", now.Add(150*time.Second), 2*time.Minute) if err != nil || !renewed { t.Fatalf("有效租约所有者续租失败:%v,续租:%v", err, renewed) } claimed, err = store.Claim(context.Background(), 1, "worker-c", now.Add(3*time.Minute), time.Minute) if err != nil || claimed { t.Fatalf("续租后不得被其他 Worker 领取:%v,领取:%v", err, claimed) } finished, err := store.Finish(context.Background(), 1, "worker-a", contract.TerminalResult{ TaskID: "1", Status: contract.StatusCompleted, TotalCount: 10, SuccessCount: 7, FailedCount: 3, UpdatedAt: now, }, now.Add(3*time.Minute)) if err != nil || finished { t.Fatalf("旧租约所有者不得完成任务:%v,完成:%v", err, finished) } finished, err = store.Finish(context.Background(), 1, "worker-b", contract.TerminalResult{ TaskID: "1", Status: contract.StatusCompleted, TotalCount: 10, SuccessCount: 7, FailedCount: 3, UpdatedAt: now, }, now.Add(3*time.Minute)) if err != nil || !finished { t.Fatalf("当前租约所有者完成任务失败:%v,完成:%v", err, finished) } claimed, err = store.Claim(context.Background(), 1, "worker-c", now.Add(4*time.Minute), time.Minute) if err != nil || claimed { t.Fatalf("终态重复消费必须无副作用:%v,领取:%v", err, claimed) } cancelled, err := store.Cancel(context.Background(), 1, now.Add(4*time.Minute)) if err != nil || cancelled { t.Fatalf("终态任务不得再次取消:%v,取消:%v", err, cancelled) } var row struct { Status int Total int Success int Failed int } if err := db.Table("test_async_contract_task").Select("status, total_count AS total, success_count AS success, failed_count AS failed").Where("id = 1").Scan(&row).Error; err != nil { t.Fatalf("读取任务终态失败:%v", err) } if row.Status != contract.StatusCompleted || row.Total != 10 || row.Success != 7 || row.Failed != 3 { t.Fatalf("任务终态计数错误:%+v", row) } } func TestPostgresTaskContractSupportsWholeFailureAndCancellation(t *testing.T) { db := testutil.NewPostgresTransaction(t) createTaskContractTable(t, db) store, _ := storepkg.NewStore(db, taskDefinition()) now := time.Now().UTC() if err := db.Exec("INSERT INTO test_async_contract_task (id, status, updated_at) VALUES (2, 1, ?), (3, 1, ?)", now, now).Error; err != nil { t.Fatalf("准备任务失败:%v", err) } claimed, _ := store.Claim(context.Background(), 2, "worker", now, time.Minute) if !claimed { t.Fatal("整体失败任务领取失败") } finished, err := store.Finish(context.Background(), 2, "worker", contract.TerminalResult{ TaskID: "2", Status: contract.StatusFailed, ErrorCode: "FILE_PARSE_FAILED", ErrorSummary: "文件无法解析", UpdatedAt: now, }, now) if err != nil || !finished { t.Fatalf("整体失败终态更新失败:%v", err) } cancelled, err := store.Cancel(context.Background(), 3, now) if err != nil || !cancelled { t.Fatalf("待处理任务取消失败:%v", err) } } func taskDefinition() storepkg.Definition { return storepkg.Definition{ Table: "test_async_contract_task", IDColumn: "id", StatusColumn: "status", LeaseOwnerColumn: "lease_owner", LeaseExpiresColumn: "lease_expires_at", TotalColumn: "total_count", SuccessColumn: "success_count", FailedColumn: "failed_count", ProgressColumn: "progress", ErrorCodeColumn: "error_code", ErrorSummaryColumn: "error_summary", StartedAtColumn: "started_at", CompletedAtColumn: "completed_at", UpdatedAtColumn: "updated_at", } } func createTaskContractTable(t *testing.T, db *gorm.DB) { t.Helper() if err := db.Exec(`CREATE TEMP TABLE test_async_contract_task ( id bigint PRIMARY KEY, status integer NOT NULL, total_count integer NOT NULL DEFAULT 0, success_count integer NOT NULL DEFAULT 0, failed_count integer NOT NULL DEFAULT 0, progress integer NOT NULL DEFAULT 0, error_code varchar(100) NOT NULL DEFAULT '', error_summary varchar(500) NOT NULL DEFAULT '', lease_owner varchar(100), lease_expires_at timestamptz, started_at timestamptz, completed_at timestamptz, updated_at timestamptz NOT NULL ) ON COMMIT DROP`).Error; err != nil { t.Fatalf("创建任务契约测试表失败:%v", err) } }