fix(#64): bound purchase spec selection retries
This commit is contained in:
@@ -268,11 +268,22 @@ func (s *Service) SubmitResult(ctx context.Context, taskID uint64, req ResultReq
|
||||
if len(req.ProbedSpecs) == 0 {
|
||||
return TaskPayload{}, fail(CodeInvalidRequest, "规格探测结果无效")
|
||||
}
|
||||
next = models.PurchaseTaskStatusSpecProbePending
|
||||
a.Status = models.PurchaseAttemptStatusCompleted
|
||||
t.SpecSource = "unresolved"
|
||||
t.MappedColorSnapshot = ""
|
||||
t.MappedSizeSnapshot = ""
|
||||
if t.SpecDecisionRequestID != nil {
|
||||
// One slow-path decision has already been frozen for this task. A
|
||||
// second probe means the Agent still could not select that exact
|
||||
// decision. Fail closed instead of clearing the auditable decision
|
||||
// and leaving an active task that can be dispatched forever.
|
||||
next = models.PurchaseTaskStatusFailed
|
||||
a.Status = models.PurchaseAttemptStatusFailed
|
||||
code, message := "PURCHASE_SPEC_NOT_MATCHED", "再次执行仍未能精确选择商品规格,请检查商品规格"
|
||||
t.ErrorCode, t.ErrorMessage = &code, &message
|
||||
} else {
|
||||
next = models.PurchaseTaskStatusSpecProbePending
|
||||
a.Status = models.PurchaseAttemptStatusCompleted
|
||||
t.SpecSource = "unresolved"
|
||||
t.MappedColorSnapshot = ""
|
||||
t.MappedSizeSnapshot = ""
|
||||
}
|
||||
case "rehearsal_completed":
|
||||
if t.ExecutionMode != models.PurchaseExecutionModeRehearsal {
|
||||
return TaskPayload{}, fail(CodeStateConflict, "正式任务不能提交演练结果")
|
||||
@@ -322,7 +333,7 @@ func (s *Service) SubmitResult(ctx context.Context, taskID uint64, req ResultReq
|
||||
}
|
||||
return valuePayload(s, t, a, false)
|
||||
})
|
||||
if err != nil || req.ResultType != "spec_probe_completed" || payload.Replayed {
|
||||
if err != nil || req.ResultType != "spec_probe_completed" || payload.Replayed || payload.Status != models.PurchaseTaskStatusSpecProbePending {
|
||||
return payload, err
|
||||
}
|
||||
return s.resolveProbedSpecs(ctx, taskID, req.TaskAttemptID, req.ProbedSpecs)
|
||||
|
||||
@@ -230,6 +230,48 @@ func TestSlowPathUsesTwoAttemptsAndFreezesDecision(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestSecondSpecProbeFailsClosedWithoutClearingDecision(t *testing.T) {
|
||||
db := testDB(t)
|
||||
f := seed(t, db, liveCaps(), false)
|
||||
s := testService(db)
|
||||
task, err := createLive(t, s, f)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err = s.Claim(context.Background(), task.ID, ActionRequest{RequestID: uuid.NewString()}, f.token); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
first, err := s.Start(context.Background(), task.ID, ActionRequest{RequestID: uuid.NewString()}, f.token)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
probe := json.RawMessage(`{"dimensions":[{"key":"color","values":["黑色"]},{"key":"size","values":["XL"]}]}`)
|
||||
if _, err = s.SubmitResult(context.Background(), task.ID, ResultRequest{RequestID: uuid.NewString(), TaskAttemptID: first.TaskAttemptID, ResultType: "spec_probe_completed", ProbedSpecs: probe}, f.token); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err = s.Claim(context.Background(), task.ID, ActionRequest{RequestID: uuid.NewString()}, f.token); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
second, err := s.Start(context.Background(), task.ID, ActionRequest{RequestID: uuid.NewString()}, f.token)
|
||||
if err != nil || second.Phase != models.PurchaseAttemptPhasePurchase {
|
||||
t.Fatalf("second attempt: %+v %v", second, err)
|
||||
}
|
||||
result, err := s.SubmitResult(context.Background(), task.ID, ResultRequest{RequestID: uuid.NewString(), TaskAttemptID: second.TaskAttemptID, ResultType: "spec_probe_completed", ProbedSpecs: probe}, f.token)
|
||||
if err != nil || result.Status != models.PurchaseTaskStatusFailed {
|
||||
t.Fatalf("second probe must fail closed: %+v %v", result, err)
|
||||
}
|
||||
var saved models.PurchaseTask
|
||||
if err = db.First(&saved, task.ID).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if saved.ErrorCode == nil || *saved.ErrorCode != "PURCHASE_SPEC_NOT_MATCHED" {
|
||||
t.Fatalf("unexpected failure: %v %v", saved.ErrorCode, saved.ErrorMessage)
|
||||
}
|
||||
if saved.MappedColorSnapshot != "黑色" || saved.MappedSizeSnapshot != "XL" || saved.SpecDecisionRequestID == nil {
|
||||
t.Fatalf("the first decision must remain auditable: %+v", saved)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateUsesDeterministicTraditionalAndWeightMatching(t *testing.T) {
|
||||
db := testDB(t)
|
||||
f := seed(t, db, liveCaps(), false)
|
||||
|
||||
Reference in New Issue
Block a user