@@ -184,6 +184,42 @@ func expectQueueTasksQuery(queueName string, limit int64) func(string, []driver.
184184 }
185185}
186186
187+ func expectFailedQueueTasksQuery (queueName string , limit int64 , terminal bool ) func (string , []driver.NamedValue ) error {
188+ rTable := fmt .Sprintf (`FROM absurd.%q r` , "r_" + queueName )
189+ tTable := fmt .Sprintf (`JOIN absurd.%q t ON t.task_id = r.task_id` , "t_" + queueName )
190+ return func (query string , args []driver.NamedValue ) error {
191+ for _ , required := range []string {
192+ rTable ,
193+ tTable ,
194+ "r.state = $1" ,
195+ "ORDER BY r.run_id DESC" ,
196+ } {
197+ if ! strings .Contains (query , required ) {
198+ return fmt .Errorf ("query %q missing %q" , query , required )
199+ }
200+ }
201+ for _ , terminalClause := range []string {"t.state = 'failed'" , "r.run_id = t.last_attempt_run" } {
202+ contains := strings .Contains (query , terminalClause )
203+ if terminal && ! contains {
204+ return fmt .Errorf ("query %q missing %q" , query , terminalClause )
205+ }
206+ if ! terminal && contains {
207+ return fmt .Errorf ("query %q unexpectedly contains %q" , query , terminalClause )
208+ }
209+ }
210+ if len (args ) != 2 {
211+ return fmt .Errorf ("expected 2 args, got %d" , len (args ))
212+ }
213+ if args [0 ].Value != "failed" {
214+ return fmt .Errorf ("status arg = %#v, want failed" , args [0 ].Value )
215+ }
216+ if args [1 ].Value != limit {
217+ return fmt .Errorf ("limit arg = %#v, want %#v" , args [1 ].Value , limit )
218+ }
219+ return nil
220+ }
221+ }
222+
187223func expectRecentTaskNamesQuery (queueName string , limit int64 ) func (string , []driver.NamedValue ) error {
188224 rTable := fmt .Sprintf (`FROM absurd.%q` , "r_" + queueName )
189225 tTable := fmt .Sprintf (`JOIN absurd.%q t ON t.task_id = r.task_id` , "t_" + queueName )
@@ -352,6 +388,171 @@ func TestHandleTasksFailsFastOnQueueQueryDeadline(t *testing.T) {
352388 }
353389}
354390
391+ func TestHandleTasksTerminalFailedScopeFiltersLastFailedRun (t * testing.T ) {
392+ now := time .Now ().UTC ()
393+ taskID := uuid .NewString ()
394+ runID := uuid .NewString ()
395+
396+ db := newScriptedDB (t , []scriptedQuery {
397+ {
398+ match : expectContains (`SELECT queue_name FROM absurd.queues ORDER BY queue_name` ),
399+ columns : []string {"queue_name" },
400+ rows : [][]driver.Value {{"alpha" }},
401+ },
402+ {
403+ match : expectRecentTaskNamesQuery ("alpha" , 5000 ),
404+ columns : []string {"task_name" },
405+ rows : [][]driver.Value {{"process-webhook" }},
406+ },
407+ {
408+ match : expectFailedQueueTasksQuery ("alpha" , 27 , true ),
409+ columns : []string {
410+ "task_id" ,
411+ "run_id" ,
412+ "queue_name" ,
413+ "task_name" ,
414+ "state" ,
415+ "attempt" ,
416+ "max_attempts" ,
417+ "created_at" ,
418+ "updated_at" ,
419+ "completed_at" ,
420+ "claimed_by" ,
421+ "params" ,
422+ },
423+ rows : [][]driver.Value {
424+ {
425+ taskID ,
426+ runID ,
427+ "alpha" ,
428+ "process-webhook" ,
429+ "failed" ,
430+ int64 (3 ),
431+ int64 (3 ),
432+ now ,
433+ now ,
434+ nil ,
435+ nil ,
436+ nil ,
437+ },
438+ },
439+ },
440+ })
441+
442+ srv := & Server {db : db }
443+
444+ req := httptest .NewRequest (http .MethodGet , "/api/tasks?status=failed&failedRunScope=terminal&page=1&perPage=25" , nil )
445+ resp := httptest .NewRecorder ()
446+
447+ srv .handleTasks (resp , req )
448+
449+ if resp .Code != http .StatusOK {
450+ t .Fatalf ("status = %d, want %d (body=%q)" , resp .Code , http .StatusOK , resp .Body .String ())
451+ }
452+
453+ body := resp .Body .String ()
454+ if ! strings .Contains (body , runID ) {
455+ t .Fatalf ("expected terminal failed run in response body, got %q" , body )
456+ }
457+ if ! strings .Contains (body , `"total":1` ) {
458+ t .Fatalf ("expected total=1 in response body, got %q" , body )
459+ }
460+ }
461+
462+ func TestHandleTasksAllFailedScopeKeepsRetriedFailures (t * testing.T ) {
463+ now := time .Now ().UTC ()
464+ taskID := uuid .NewString ()
465+ runID := uuid .NewString ()
466+
467+ db := newScriptedDB (t , []scriptedQuery {
468+ {
469+ match : expectContains (`SELECT queue_name FROM absurd.queues ORDER BY queue_name` ),
470+ columns : []string {"queue_name" },
471+ rows : [][]driver.Value {{"alpha" }},
472+ },
473+ {
474+ match : expectRecentTaskNamesQuery ("alpha" , 5000 ),
475+ columns : []string {"task_name" },
476+ rows : [][]driver.Value {{"process-webhook" }},
477+ },
478+ {
479+ match : expectFailedQueueTasksQuery ("alpha" , 27 , false ),
480+ columns : []string {
481+ "task_id" ,
482+ "run_id" ,
483+ "queue_name" ,
484+ "task_name" ,
485+ "state" ,
486+ "attempt" ,
487+ "max_attempts" ,
488+ "created_at" ,
489+ "updated_at" ,
490+ "completed_at" ,
491+ "claimed_by" ,
492+ "params" ,
493+ },
494+ rows : [][]driver.Value {
495+ {
496+ taskID ,
497+ runID ,
498+ "alpha" ,
499+ "process-webhook" ,
500+ "failed" ,
501+ int64 (1 ),
502+ int64 (3 ),
503+ now ,
504+ now ,
505+ nil ,
506+ nil ,
507+ nil ,
508+ },
509+ },
510+ },
511+ })
512+
513+ srv := & Server {db : db }
514+
515+ req := httptest .NewRequest (http .MethodGet , "/api/tasks?status=failed&failedRunScope=all&page=1&perPage=25" , nil )
516+ resp := httptest .NewRecorder ()
517+
518+ srv .handleTasks (resp , req )
519+
520+ if resp .Code != http .StatusOK {
521+ t .Fatalf ("status = %d, want %d (body=%q)" , resp .Code , http .StatusOK , resp .Body .String ())
522+ }
523+
524+ body := resp .Body .String ()
525+ if ! strings .Contains (body , runID ) {
526+ t .Fatalf ("expected failed run in response body, got %q" , body )
527+ }
528+ }
529+
530+ func TestHandleTasksRejectsInvalidFailedRunScope (t * testing.T ) {
531+ db := newScriptedDB (t , []scriptedQuery {
532+ {
533+ match : expectContains (`SELECT queue_name FROM absurd.queues ORDER BY queue_name` ),
534+ columns : []string {"queue_name" },
535+ rows : [][]driver.Value {{"alpha" }},
536+ },
537+ })
538+
539+ srv := & Server {db : db }
540+
541+ req := httptest .NewRequest (http .MethodGet , "/api/tasks?failedRunScope=sometimes&page=1&perPage=25" , nil )
542+ resp := httptest .NewRecorder ()
543+
544+ srv .handleTasks (resp , req )
545+
546+ if resp .Code != http .StatusOK {
547+ t .Fatalf ("status = %d, want %d (body=%q)" , resp .Code , http .StatusOK , resp .Body .String ())
548+ }
549+
550+ body := resp .Body .String ()
551+ if ! strings .Contains (body , `"items":[]` ) || ! strings .Contains (body , `"total":0` ) {
552+ t .Fatalf ("expected empty task list response, got %q" , body )
553+ }
554+ }
555+
355556func TestHandleRetryTaskSuccess (t * testing.T ) {
356557 taskID := uuid .NewString ()
357558 runID := uuid .NewString ()
0 commit comments