package jobs import ( "context" "encoding/json" "errors" "fmt" "strings" "testing" ) func TestRunnerDispatchesRegisteredHandler(t *testing.T) { runner := NewRunner() called := false var receivedPayload json.RawMessage testKind := Kind("test_job") runner.Register(testKind, func(ctx context.Context, payload json.RawMessage) error { called = true receivedPayload = payload return nil }) job := Job{ ID: "job-1", Kind: testKind, Payload: json.RawMessage(`{"key":"value"}`), } err := runner.Execute(context.Background(), job) if err != nil { t.Fatalf("unexpected error: %v", err) } if !called { t.Error("expected handler to be called, but it was not") } if string(receivedPayload) != `{"key":"value"}` { t.Errorf("expected payload %q, got %q", `{"key":"value"}`, string(receivedPayload)) } } func TestRunnerRejectsUnknownKind(t *testing.T) { runner := NewRunner() job := Job{ ID: "job-2", Kind: Kind("non_existent"), } err := runner.Execute(context.Background(), job) if err == nil { t.Fatal("expected error for unknown job kind, got nil") } } func TestRunnerRespectsCanceledContext(t *testing.T) { runner := NewRunner() testKind := Kind("cancellation_test") runner.Register(testKind, func(ctx context.Context, payload json.RawMessage) error { return nil }) ctx, cancel := context.WithCancel(context.Background()) cancel() // Cancel context immediately job := Job{ ID: "job-3", Kind: testKind, } err := runner.Execute(ctx, job) if err == nil { t.Fatal("expected error due to canceled context, got nil") } if !errors.Is(err, context.Canceled) { t.Errorf("expected error %v, got %v", context.Canceled, err) } } func TestRunnerHandlesPanic(t *testing.T) { runner := NewRunner() panicKind := Kind("panic_job") runner.Register(panicKind, func(ctx context.Context, payload json.RawMessage) error { panic("something went critically wrong") }) job := Job{ ID: "job-4", Kind: panicKind, } err := runner.Execute(context.Background(), job) if err == nil { t.Fatal("expected error from panic recovery, got nil") } if err.Error() != "job panicked: something went critically wrong" { t.Errorf("unexpected error message: %v", err) } } func TestRegisterBuiltins(t *testing.T) { runner := NewRunner() RegisterBuiltins(runner) expectedCount := 3 if runner.Len() != expectedCount { t.Errorf("expected %d registered built-in handlers, got %d", expectedCount, runner.Len()) } } // --- ExecuteWithResult tests --- func TestRunnerExecuteWithResultSuccess(t *testing.T) { runner := NewRunner() testKind := Kind("ewr_success") var payload json.RawMessage runner.Register(testKind, func(ctx context.Context, p json.RawMessage) error { payload = p return nil }) runner.RegisterWithResult(testKind, func(ctx context.Context, p json.RawMessage) (Result, error) { return Result{Bars: 42, Instruments: 3}, nil }) job := Job{ ID: "ewr-1", Kind: testKind, Payload: json.RawMessage(`{"key":"value"}`), } res, hasResult, err := runner.ExecuteWithResult(context.Background(), job) if err != nil { t.Fatalf("ExecuteWithResult: unexpected error: %v", err) } if !hasResult { t.Fatal("ExecuteWithResult: expected hasResult=true, got false") } if res.Bars != 42 || res.Instruments != 3 { t.Errorf("ExecuteWithResult: expected {Bars:42,Instruments:3}, got %+v", res) } // Verify the handler was called with the correct payload. _ = payload // payload is captured by the Register handler; assertion is above. } func TestRunnerExecuteWithResultMissingHandler(t *testing.T) { runner := NewRunner() testKind := Kind("ewr_missing") // Only register Handler, not WithResultHandler runner.Register(testKind, func(ctx context.Context, p json.RawMessage) error { return nil }) job := Job{ ID: "ewr-2", Kind: testKind, } res, hasResult, err := runner.ExecuteWithResult(context.Background(), job) if err != nil { t.Fatalf("ExecuteWithResult: expected no error for missing handler, got: %v", err) } if hasResult { t.Fatal("ExecuteWithResult: expected hasResult=false for missing handler, got true") } if res.Bars != 0 { t.Errorf("ExecuteWithResult: expected zero result for missing handler, got %+v", res) } } func TestRunnerExecuteWithResultHandlerError(t *testing.T) { runner := NewRunner() testKind := Kind("ewr_error") wantErr := fmt.Errorf("import failed: connection reset") runner.Register(testKind, func(ctx context.Context, p json.RawMessage) error { return nil }) runner.RegisterWithResult(testKind, func(ctx context.Context, p json.RawMessage) (Result, error) { return Result{}, wantErr }) job := Job{ ID: "ewr-3", Kind: testKind, } res, hasResult, err := runner.ExecuteWithResult(context.Background(), job) if err == nil { t.Fatal("ExecuteWithResult: expected error from handler, got nil") } if err != wantErr { t.Errorf("ExecuteWithResult: expected error %v, got %v", wantErr, err) } if hasResult { t.Fatal("ExecuteWithResult: expected hasResult=false on error, got true") } if res.Bars != 0 { t.Errorf("ExecuteWithResult: expected zero result on error, got %+v", res) } } func TestRunnerExecuteWithResultCanceledContext(t *testing.T) { runner := NewRunner() testKind := Kind("ewr_cancel") runner.Register(testKind, func(ctx context.Context, p json.RawMessage) error { return nil }) runner.RegisterWithResult(testKind, func(ctx context.Context, p json.RawMessage) (Result, error) { return Result{Bars: 10}, nil }) ctx, cancel := context.WithCancel(context.Background()) cancel() // Cancel immediately job := Job{ ID: "ewr-4", Kind: testKind, } res, hasResult, err := runner.ExecuteWithResult(ctx, job) if err == nil { t.Fatal("ExecuteWithResult: expected context.Canceled error, got nil") } if !errors.Is(err, context.Canceled) { t.Errorf("ExecuteWithResult: expected context.Canceled, got %v", err) } if hasResult { t.Fatal("ExecuteWithResult: expected hasResult=false on cancel, got true") } if res.Bars != 0 { t.Errorf("ExecuteWithResult: expected zero result on cancel, got %+v", res) } } func TestRunnerExecuteWithResultPanic(t *testing.T) { runner := NewRunner() testKind := Kind("ewr_panic") runner.Register(testKind, func(ctx context.Context, p json.RawMessage) error { return nil }) runner.RegisterWithResult(testKind, func(ctx context.Context, p json.RawMessage) (Result, error) { panic("boom in handler") }) job := Job{ ID: "ewr-5", Kind: testKind, } res, hasResult, err := runner.ExecuteWithResult(context.Background(), job) if err == nil { t.Fatal("ExecuteWithResult: expected panic error, got nil") } if !strings.Contains(err.Error(), "panicked") { t.Errorf("ExecuteWithResult: expected 'panicked' in error message, got: %v", err) } if hasResult { t.Fatal("ExecuteWithResult: expected hasResult=false on panic, got true") } if res.Bars != 0 { t.Errorf("ExecuteWithResult: expected zero result on panic, got %+v", res) } }