feat(tenant): supersede unknown publish tasks on manual retry
Replace the in-place requeue of submit-uncertain unknown publish tasks with an explicit supersede-and-create-new flow: manual retries mark the old unknown task failed (manual_retry_superseded) and create a fresh task carrying manual_retry / superseded_unknown_task_ids in its payload. Also fold title-length validation into definitivePublishFailurePattern and check the pattern before the publish_submit_uncertain guard so those failures are released from the dedup set. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -155,62 +155,6 @@ func TestPublishTargetPlatformsSortsAndDedupes(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestShouldRequeuePublishTaskInsteadOfCreatingNewOnlyForUnknownPublish(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
if !shouldRequeuePublishTaskInsteadOfCreatingNew(&repository.DesktopTask{
|
||||
Kind: "publish",
|
||||
Status: "unknown",
|
||||
Error: []byte(`{"publish_submit_uncertain":true,"message":"network interrupted after submit"}`),
|
||||
}) {
|
||||
t.Fatal("submit-uncertain unknown publish tasks should be retried by requeueing the existing task")
|
||||
}
|
||||
if shouldRequeuePublishTaskInsteadOfCreatingNew(&repository.DesktopTask{
|
||||
Kind: "publish",
|
||||
Status: "unknown",
|
||||
Error: []byte(`{"error_code":403,"error_msg":"CSRF token invalid"}`),
|
||||
}) {
|
||||
t.Fatal("definitive failed unknown publish tasks should create a new retry record")
|
||||
}
|
||||
if shouldRequeuePublishTaskInsteadOfCreatingNew(&repository.DesktopTask{
|
||||
Kind: "publish",
|
||||
Status: "failed",
|
||||
}) {
|
||||
t.Fatal("failed publish tasks should keep create-new retry semantics")
|
||||
}
|
||||
if shouldRequeuePublishTaskInsteadOfCreatingNew(&repository.DesktopTask{
|
||||
Kind: "monitor",
|
||||
Status: "unknown",
|
||||
}) {
|
||||
t.Fatal("monitor retry semantics are owned by desktop task reconcile")
|
||||
}
|
||||
if shouldRequeuePublishTaskInsteadOfCreatingNew(nil) {
|
||||
t.Fatal("nil tasks should not be requeued")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreatePublishJobResponseForRequeuedTaskReportsRequeuedTaskID(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
taskID := uuid.MustParse("55555555-5555-5555-5555-555555555555")
|
||||
got := createPublishJobResponseForRequeuedTask(&repository.DesktopTask{DesktopID: taskID})
|
||||
if got.JobID != "" {
|
||||
t.Fatalf("JobID = %q, want empty for requeued task", got.JobID)
|
||||
}
|
||||
if len(got.TaskIDs) != 1 || got.TaskIDs[0] != taskID.String() {
|
||||
t.Fatalf("TaskIDs = %v, want [%s]", got.TaskIDs, taskID)
|
||||
}
|
||||
if len(got.CreatedTaskIDs) != 0 {
|
||||
t.Fatalf("CreatedTaskIDs = %v, want empty", got.CreatedTaskIDs)
|
||||
}
|
||||
if len(got.ExistingTaskIDs) != 0 {
|
||||
t.Fatalf("ExistingTaskIDs = %v, want empty", got.ExistingTaskIDs)
|
||||
}
|
||||
if len(got.RequeuedTaskIDs) != 1 || got.RequeuedTaskIDs[0] != taskID.String() {
|
||||
t.Fatalf("RequeuedTaskIDs = %v, want [%s]", got.RequeuedTaskIDs, taskID)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPublishUnknownTaskHasDefinitiveFailureReleasesCSRF403(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
@@ -237,6 +181,19 @@ func TestPublishUnknownTaskHasDefinitiveFailureKeepsSubmitUncertain(t *testing.T
|
||||
}
|
||||
}
|
||||
|
||||
func TestPublishUnknownTaskHasDefinitiveFailureReleasesTitleLengthValidation(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
task := &repository.DesktopTask{
|
||||
Kind: "publish",
|
||||
Status: "unknown",
|
||||
Error: []byte(`{"publish_submit_uncertain":true,"code":"toutiaohao_publish_failed","message":"标题长度应该在2-30字之间"}`),
|
||||
}
|
||||
if !publishUnknownTaskHasDefinitiveFailure(task) {
|
||||
t.Fatal("title length validation should be treated as definitive failed")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadExistingPublishRecordTargetsDedupeUnknownTasks(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
@@ -298,6 +255,53 @@ func TestLockDesktopPublishDedupKeysSkipsEmptyList(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestReleaseManuallySupersededUnknownPublishTasksLocksUnknownDedupSet(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tx := &capturePublishDedupLookupTx{rows: emptyPublishDedupRows{}}
|
||||
outcomes, superseded, err := releaseManuallySupersededUnknownPublishTasks(
|
||||
context.Background(),
|
||||
tx,
|
||||
1,
|
||||
2,
|
||||
[]string{"dedup-a", "dedup-b"},
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("releaseManuallySupersededUnknownPublishTasks() error = %v", err)
|
||||
}
|
||||
if len(outcomes) != 0 {
|
||||
t.Fatalf("outcomes = %v, want empty", outcomes)
|
||||
}
|
||||
if len(superseded) != 0 {
|
||||
t.Fatalf("superseded = %v, want empty", superseded)
|
||||
}
|
||||
|
||||
normalizedSQL := normalizeSQLWhitespace(tx.sql)
|
||||
for _, fragment := range []string{
|
||||
"FROM desktop_tasks",
|
||||
"tenant_id = $1",
|
||||
"workspace_id = $2",
|
||||
"kind = 'publish'",
|
||||
"status = 'unknown'",
|
||||
"dedup_key = ANY($3::text[])",
|
||||
"FOR UPDATE",
|
||||
} {
|
||||
if !strings.Contains(normalizedSQL, fragment) {
|
||||
t.Fatalf("manual supersede lookup missing %q: %s", fragment, tx.sql)
|
||||
}
|
||||
}
|
||||
if !strings.Contains(normalizedSQL, "COALESCE(dedup_key, '')") {
|
||||
t.Fatalf("manual supersede lookup should return dedup keys for new-task payload: %s", tx.sql)
|
||||
}
|
||||
if len(tx.args) != 3 || tx.args[0] != int64(1) || tx.args[1] != int64(2) {
|
||||
t.Fatalf("manual supersede lookup args = %#v, want tenant/workspace/dedup keys", tx.args)
|
||||
}
|
||||
gotKeys, ok := tx.args[2].([]string)
|
||||
if !ok || len(gotKeys) != 2 || gotKeys[0] != "dedup-a" || gotKeys[1] != "dedup-b" {
|
||||
t.Fatalf("manual supersede lookup dedup keys = %#v, want [dedup-a dedup-b]", tx.args[2])
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsPostgresUniqueViolationMatchesConstraint(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user