package zset import ( "context" "fmt" "testing" "time" "git.5iot.com/iotlcm-public/iotlcm-worker-svr/internal/testutil" ) func newCoveredRun(t *testing.T, prefix, run string, count int) (*Store, context.Context) { t.Helper() store, err := New(testutil.Redis(t), prefix) if err != nil { t.Fatal(err) } ctx := context.Background() if err := store.CreateRun(ctx, RunMeta{RunID: run}); err != nil { t.Fatal(err) } tasks := make([]Task, count) for i := range tasks { tasks[i] = Task{RunID: run, TaskID: fmt.Sprintf("%s:%d", run, i), CardID: int64(i + 1), ChanKey: "channel", TempKey: "sim_yd_v5", ICCID: fmt.Sprintf("card-%d", i)} } if n, err := store.EnqueueRunBatch(ctx, run, "channel", tasks, time.Now()); err != nil || n != count { t.Fatalf("enqueue %d: %v", n, err) } if err := store.MarkProducerDone(ctx, run); err != nil { t.Fatal(err) } return store, ctx } // TestCoveredOutcomeStaysOutOfFailurePool pins the queue-side contract of a // covered result: the task leaves the round like a success, the run counts it // as completed work, and nothing is written to the failure hash or failedCount. // Code 14000 ("carrier has no record of this card") reaches the queue through // this path, so a regression here restores the 4900-entry failure pool seen in // production on 2026-10-20. func TestCoveredOutcomeStaysOutOfFailurePool(t *testing.T) { store, ctx := newCoveredRun(t, "covered:", "run", 3) now := time.Now() claims, err := store.ClaimRun(ctx, "run", "channel", now, now.Add(time.Minute), 3) if err != nil || len(claims) != 3 { t.Fatalf("claim: %d %v", len(claims), err) } outcomes := []TaskOutcome{ {Success: true}, {Covered: true, ErrorClass: "no_record", Code: 14000, Message: "系统无记录,请检查输入参数是否正确"}, {Covered: true, ErrorClass: "data_invalid", Message: "carrier returned no imei value"}, } if n, err := store.FinalizeRunTasks(ctx, claims, outcomes); err != nil || n != 3 { t.Fatalf("finalize: %d %v", n, err) } failureKey, _ := store.FailedKey("run") size, err := store.RedisClient().Do(ctx, "HLEN", failureKey) if err != nil || size.Int64() != 0 { t.Fatalf("failure pool entries = %d err=%v, want 0", size.Int64(), err) } meta, err := store.GetRunMeta(ctx, "run") if err != nil { t.Fatal(err) } if meta.FailedCount != 0 || meta.SuccessCount != 3 { t.Fatalf("counters: success=%d failed=%d, want 3/0", meta.SuccessCount, meta.FailedCount) } // The round was still advanced: covered tasks must free the capacity they // claimed, otherwise the run can never complete. if meta.ActiveCount != 0 || meta.InflightCount != 0 || meta.NextRoundCount != 0 { t.Fatalf("queue capacity not released: %+v", meta) } stateKey, _ := store.TaskStateKey("run") state, err := store.RedisClient().Do(ctx, "HGET", stateKey, "run:1") if err != nil { t.Fatal(err) } values := state.MapStrAny() if values["status"] != taskStatusCovered || fmt.Sprint(values["lastCode"]) != "14000" { t.Fatalf("covered task state = %v", values) } completed, err := store.CompleteRunIfIdle(ctx, "run") if err != nil || !completed { t.Fatalf("complete: %t %v", completed, err) } if meta, err = store.GetRunMeta(ctx, "run"); err != nil || meta.Status != "completed" { t.Fatalf("status = %q err=%v, want completed (no failures)", meta.Status, err) } } // TestFailureOutcomeStillCountsAsFailure is the guard rail for the test above: // only Covered is exempt. An ordinary terminal failure keeps the historical // failure-pool behaviour. func TestFailureOutcomeStillCountsAsFailure(t *testing.T) { store, ctx := newCoveredRun(t, "covered-fail:", "run", 2) now := time.Now() claims, err := store.ClaimRun(ctx, "run", "channel", now, now.Add(time.Minute), 2) if err != nil || len(claims) != 2 { t.Fatalf("claim: %d %v", len(claims), err) } outcomes := []TaskOutcome{ {Covered: true, ErrorClass: "no_record", Code: 14000}, {ErrorClass: "business", FailureType: "business", Code: 11011, Message: "invalid card"}, } if n, err := store.FinalizeRunTasks(ctx, claims, outcomes); err != nil || n != 2 { t.Fatalf("finalize: %d %v", n, err) } failureKey, _ := store.FailedKey("run") size, err := store.RedisClient().Do(ctx, "HLEN", failureKey) if err != nil || size.Int64() != 1 { t.Fatalf("failure pool entries = %d err=%v, want 1", size.Int64(), err) } exists, err := store.RedisClient().Do(ctx, "HEXISTS", failureKey, "run:1") if err != nil || exists.Int() != 1 { t.Fatalf("the business failure is missing from the pool: %v %v", exists, err) } meta, err := store.GetRunMeta(ctx, "run") if err != nil || meta.FailedCount != 1 || meta.SuccessCount != 1 { t.Fatalf("counters: %+v err=%v", meta, err) } }