@@ -19,11 +19,7 @@ const (
1919 LongActionTime = 500 * time .Millisecond
2020)
2121
22- // NewDiscardLogger creates a new logger that discards all output
23- // This is useful for tests to prevent log output from cluttering test results
24- func NewDiscardLogger () * slog.Logger {
25- return slog .New (slog .NewTextHandler (io .Discard , nil ))
26- }
22+ // duplicate NewDiscardLogger removed (defined earlier in file)
2723
2824type TestAction struct {
2925 task_engine.BaseAction
@@ -86,11 +82,25 @@ type testResultProvider struct{ v interface{} }
8682func (p testResultProvider ) GetResult () interface {} { return p .v }
8783func (p testResultProvider ) GetError () error { return nil }
8884
89- func (a * AfterExecuteFailingAction ) AfterExecute (ctx context.Context ) error {
90- if a .ShouldFailAfter {
91- return errors .New ("simulated AfterExecute failure" )
85+ // CancelAwareAction returns context error if canceled, otherwise completes after Delay
86+ type CancelAwareAction struct {
87+ task_engine.BaseAction
88+ Delay time.Duration
89+ }
90+
91+ func (a * CancelAwareAction ) Execute (ctx context.Context ) error {
92+ select {
93+ case <- ctx .Done ():
94+ return ctx .Err ()
95+ case <- time .After (a .Delay ):
96+ return nil
9297 }
93- return nil
98+ }
99+
100+ // NewDiscardLogger creates a new logger that discards all output
101+ // This is useful for tests to prevent log output from cluttering test results
102+ func NewDiscardLogger () * slog.Logger {
103+ return slog .New (slog .NewTextHandler (io .Discard , nil ))
94104}
95105
96106var (
@@ -374,6 +384,179 @@ func TestResolveAsGeneric(t *testing.T) {
374384 }
375385}
376386
387+ func TestEntityValueNegativePaths (t * testing.T ) {
388+ gc := task_engine .NewGlobalContext ()
389+
390+ if _ , err := task_engine .EntityValue (gc , "invalid" , "id" , "" ); err == nil {
391+ t .Fatalf ("expected error for invalid entity type" )
392+ }
393+ if _ , err := task_engine .EntityValue (gc , "action" , "missing" , "" ); err == nil {
394+ t .Fatalf ("expected error for missing action" )
395+ }
396+ gc .StoreActionOutput ("a1" , map [string ]interface {}{"k" : 1 })
397+ if _ , err := task_engine .ActionOutputFieldAs [string ](gc , "a1" , "k" ); err == nil {
398+ t .Fatalf ("expected type error for wrong cast" )
399+ }
400+ }
401+
402+ func TestResolveAsNegative (t * testing.T ) {
403+ gc := task_engine .NewGlobalContext ()
404+ gc .StoreActionOutput ("a" , map [string ]interface {}{"x" : "str" })
405+ // wrong type
406+ if _ , err := task_engine .ResolveAs [int ](context .Background (), task_engine .ActionOutputField ("a" , "x" ), gc ); err == nil {
407+ t .Fatalf ("expected type error for ResolveAs" )
408+ }
409+ }
410+
411+ func TestIDHelpers (t * testing.T ) {
412+ if out := task_engine .SanitizeIDPart (" Hello/World _! " ); out == "" {
413+ t .Fatalf ("expected sanitized non-empty id" )
414+ }
415+ id := task_engine .BuildActionID ("prefix" , " Part A " , "B/C" )
416+ if id == "" || id == "action-action" {
417+ t .Fatalf ("unexpected id: %s" , id )
418+ }
419+ }
420+
421+ // Task cancellation should still store task output and task result
422+ func TestTaskCancellationStoresOutputAndResult (t * testing.T ) {
423+ logger := NewDiscardLogger ()
424+ gc := task_engine .NewGlobalContext ()
425+
426+ // Task with a quick action and a cancel-aware long-running action
427+ task := & task_engine.Task {
428+ ID : "cancel-task" ,
429+ Name : "Cancellation Test" ,
430+ Actions : []task_engine.ActionWrapper {
431+ & task_engine.Action [* DelayAction ]{
432+ ID : "quick" ,
433+ Wrapped : & DelayAction {BaseAction : task_engine.BaseAction {Logger : logger }, Delay : 1 * time .Millisecond },
434+ Logger : logger ,
435+ },
436+ & task_engine.Action [* CancelAwareAction ]{
437+ ID : "slow" ,
438+ Wrapped : & CancelAwareAction {BaseAction : task_engine.BaseAction {Logger : logger }, Delay : 2 * time .Second },
439+ Logger : logger ,
440+ },
441+ },
442+ Logger : logger ,
443+ }
444+
445+ ctx , cancel := context .WithCancel (context .Background ())
446+ go func () {
447+ // cancel shortly after start
448+ time .Sleep (5 * time .Millisecond )
449+ cancel ()
450+ }()
451+ _ = task .RunWithContext (ctx , gc )
452+
453+ // Verify task output and result stored
454+ if _ , ok := gc .TaskOutputs [task .ID ]; ! ok {
455+ t .Fatalf ("expected TaskOutputs to contain task output on cancellation" )
456+ }
457+ if _ , ok := gc .TaskResults [task .ID ]; ! ok {
458+ t .Fatalf ("expected TaskResults to contain task result provider on cancellation" )
459+ }
460+ // Check outputs map for success=false
461+ out := gc .TaskOutputs [task .ID ].(map [string ]interface {})
462+ if out ["success" ].(bool ) {
463+ t .Fatalf ("expected success=false on cancellation" )
464+ }
465+ }
466+
467+ // ResultBuilder error should set task error and mark success=false in outputs
468+ func TestTaskResultBuilderErrorPath (t * testing.T ) {
469+ logger := NewDiscardLogger ()
470+ gc := task_engine .NewGlobalContext ()
471+
472+ errSentinel := errors .New ("builder failed" )
473+ builderTask := & task_engine.Task {
474+ ID : "builder-error" ,
475+ Name : "Builder Error" ,
476+ Actions : []task_engine.ActionWrapper {
477+ & task_engine.Action [* DelayAction ]{ID : "noop" , Wrapped : & DelayAction {}, Logger : logger },
478+ },
479+ Logger : logger ,
480+ ResultBuilder : func (ctx * task_engine.TaskContext ) (interface {}, error ) {
481+ return nil , errSentinel
482+ },
483+ }
484+
485+ _ = builderTask .RunWithContext (context .Background (), gc )
486+ out , ok := gc .TaskOutputs [builderTask .ID ]
487+ if ! ok {
488+ t .Fatalf ("expected TaskOutputs to contain output" )
489+ }
490+ outMap := out .(map [string ]interface {})
491+ if outMap ["success" ].(bool ) {
492+ t .Fatalf ("expected success=false when builder fails" )
493+ }
494+ // Result should be from task provider with error surfaced in GetResult map
495+ res , ok := task_engine .TaskResultAs [map [string ]interface {}](gc , builderTask .ID )
496+ if ! ok {
497+ t .Fatalf ("expected typed task result from task provider" )
498+ }
499+ if res ["success" ].(bool ) {
500+ t .Fatalf ("expected task result success=false when builder fails" )
501+ }
502+ }
503+
504+ // Typed helper does not fallback from outputs to results for tasks; verify error
505+ func TestTypedHelperNoFallbackForTaskOutputFieldAs (t * testing.T ) {
506+ gc := task_engine .NewGlobalContext ()
507+ // Only set task result, no task output
508+ gc .StoreTaskResult ("t1" , testResultProvider {v : map [string ]interface {}{"v" : 1 }})
509+ if _ , err := task_engine .TaskOutputFieldAs [int ](gc , "t1" , "v" ); err == nil {
510+ t .Fatalf ("expected error since TaskOutputFieldAs should not fallback to results" )
511+ }
512+ // But EntityValue should fallback to results and succeed (full result)
513+ if v , err := task_engine .EntityValue (gc , "task" , "t1" , "" ); err != nil {
514+ t .Fatalf ("expected EntityValue to return fallback result, err=%v" , err )
515+ } else {
516+ if m , ok := v .(map [string ]interface {}); ! ok || m ["v" ].(int ) != 1 {
517+ t .Fatalf ("unexpected result fallback: %v" , v )
518+ }
519+ }
520+ // And with a key, EntityValue should read from result map
521+ if v , err := task_engine .EntityValue (gc , "task" , "t1" , "v" ); err != nil || v .(int ) != 1 {
522+ t .Fatalf ("expected EntityValue with key to read from result map, got %v, err=%v" , v , err )
523+ }
524+ }
525+
526+ // TaskManager timeout and ResetGlobalContext behavior
527+ func TestTaskManagerTimeoutAndResetGlobalContext (t * testing.T ) {
528+ logger := NewDiscardLogger ()
529+ tm := task_engine .NewTaskManager (logger )
530+
531+ // Long-running task
532+ task := & task_engine.Task {
533+ ID : "timeout-task" ,
534+ Name : "Timeout Task" ,
535+ Actions : []task_engine.ActionWrapper {
536+ & task_engine.Action [* DelayAction ]{ID : "slow" , Wrapped : & DelayAction {Delay : 2 * time .Second }, Logger : logger },
537+ },
538+ Logger : logger ,
539+ }
540+ _ = tm .AddTask (task )
541+ _ = tm .RunTask ("timeout-task" )
542+ // Expect timeout quickly
543+ if err := tm .WaitForAllTasksToComplete (10 * time .Millisecond ); err == nil {
544+ t .Fatalf ("expected timeout error" )
545+ }
546+
547+ // Store something in current global context
548+ gc := tm .GetGlobalContext ()
549+ gc .StoreActionOutput ("a" , "x" )
550+ // Reset and verify cleared
551+ tm .ResetGlobalContext ()
552+ gc2 := tm .GetGlobalContext ()
553+ if gc2 == gc || len (gc2 .ActionOutputs ) != 0 || len (gc2 .TaskOutputs ) != 0 || len (gc2 .ActionResults ) != 0 || len (gc2 .TaskResults ) != 0 {
554+ t .Fatalf ("expected a fresh global context after reset" )
555+ }
556+ // Stop to clean up
557+ _ = tm .StopTask ("timeout-task" )
558+ }
559+
377560func TestTaskWithParameterPassing (t * testing.T ) {
378561 t .Run ("TaskExecutionWithGlobalContext" , func (t * testing.T ) {
379562 logger := NewDiscardLogger ()
0 commit comments