github.com/grafana/synthetic-monitoring-agent@v0.64.2/internal/adhoc/adhoc_test.go (about)

     1  package adhoc
     2  
     3  import (
     4  	"context"
     5  	"errors"
     6  	"io"
     7  	"os"
     8  	"testing"
     9  	"time"
    10  
    11  	"github.com/prometheus/client_golang/prometheus"
    12  	"github.com/rs/zerolog"
    13  	"github.com/stretchr/testify/require"
    14  	"google.golang.org/grpc"
    15  	"google.golang.org/grpc/codes"
    16  	"google.golang.org/grpc/connectivity"
    17  	"google.golang.org/grpc/metadata"
    18  	"google.golang.org/grpc/status"
    19  
    20  	"github.com/google/uuid"
    21  	"github.com/grafana/synthetic-monitoring-agent/internal/feature"
    22  	"github.com/grafana/synthetic-monitoring-agent/internal/k6runner"
    23  	"github.com/grafana/synthetic-monitoring-agent/internal/prober"
    24  	"github.com/grafana/synthetic-monitoring-agent/internal/prober/logger"
    25  	"github.com/grafana/synthetic-monitoring-agent/internal/pusher"
    26  	"github.com/grafana/synthetic-monitoring-agent/internal/testhelper"
    27  	sm "github.com/grafana/synthetic-monitoring-agent/pkg/pb/synthetic_monitoring"
    28  )
    29  
    30  func TestNewHandler(t *testing.T) {
    31  	features := feature.NewCollection()
    32  	require.NoError(t, features.Set("adhoc"))
    33  
    34  	opts := HandlerOpts{
    35  		Conn:           nil,
    36  		Logger:         zerolog.New(io.Discard),
    37  		Publisher:      channelPublisher(make(chan pusher.Payload)),
    38  		TenantCh:       make(chan sm.Tenant),
    39  		PromRegisterer: prometheus.NewPedanticRegistry(),
    40  		Features:       features,
    41  	}
    42  
    43  	h, err := NewHandler(opts)
    44  	require.NoError(t, err)
    45  	require.NotNil(t, h)
    46  
    47  	require.Equal(t, opts.Conn, h.api.conn)
    48  	require.Equal(t, opts.Logger, h.logger)
    49  	require.Equal(t, opts.Features, h.features)
    50  	require.Equal(t, opts.Publisher, h.publisher)
    51  	require.NotNil(t, h.runnerFactory)
    52  	require.NotNil(t, h.grpcAdhocChecksClientFactory)
    53  	require.Nil(t, h.probe, "probe should not be set at this point")
    54  	require.NotNil(t, h.metrics.opsCounter)
    55  	require.False(t, h.supportsProtocolSecrets, "default value should be false")
    56  }
    57  
    58  func TestHandlerSupportsProtocolSecrets(t *testing.T) {
    59  	features := feature.NewCollection()
    60  	require.NoError(t, features.Set("adhoc"))
    61  
    62  	var capturedProbeInfo *sm.ProbeInfo
    63  
    64  	testClient := &testClient{
    65  		logger: zerolog.New(io.Discard),
    66  		registerProbeHook: func(info *sm.ProbeInfo) {
    67  			capturedProbeInfo = info
    68  		},
    69  	}
    70  
    71  	opts := HandlerOpts{
    72  		Conn:                    &grpcTestConn{},
    73  		Logger:                  zerolog.New(io.Discard),
    74  		Publisher:               channelPublisher(make(chan pusher.Payload)),
    75  		TenantCh:                make(chan sm.Tenant),
    76  		PromRegisterer:          prometheus.NewPedanticRegistry(),
    77  		Features:                features,
    78  		SupportsProtocolSecrets: true,
    79  		grpcAdhocChecksClientFactory: func(conn ClientConn) (sm.AdHocChecksClient, error) {
    80  			return testClient, nil
    81  		},
    82  	}
    83  
    84  	h, err := NewHandler(opts)
    85  	require.NoError(t, err)
    86  	require.NotNil(t, h)
    87  	require.True(t, h.supportsProtocolSecrets, "should be set to true")
    88  
    89  	ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
    90  	defer cancel()
    91  
    92  	// Run will call RegisterProbe, which should capture the ProbeInfo
    93  	_ = h.Run(ctx)
    94  
    95  	require.NotNil(t, capturedProbeInfo, "RegisterProbe should have been called")
    96  	require.True(t, capturedProbeInfo.SupportsProtocolSecrets, "SupportsProtocolSecrets should be true")
    97  }
    98  
    99  func TestHandlerRun(t *testing.T) {
   100  	features := feature.NewCollection()
   101  	require.NoError(t, features.Set("adhoc"))
   102  
   103  	logger := zerolog.New(io.Discard)
   104  	if testing.Verbose() {
   105  		logger = zerolog.New(os.Stdout)
   106  	}
   107  
   108  	publishCh := make(chan pusher.Payload)
   109  
   110  	zerolog.SetGlobalLevel(zerolog.WarnLevel) // default log level.
   111  
   112  	opts := HandlerOpts{
   113  		Logger:         logger,
   114  		Publisher:      channelPublisher(publishCh),
   115  		TenantCh:       make(chan sm.Tenant),
   116  		PromRegisterer: prometheus.NewPedanticRegistry(),
   117  		Features:       features,
   118  		runnerFactory: func(ctx context.Context, req *sm.AdHocRequest) (*runner, error) {
   119  			return &runner{
   120  				logger: logger,
   121  				prober: &testProber{logger},
   122  				id:     req.AdHocCheck.Id,
   123  				target: req.AdHocCheck.Target,
   124  				probe:  "testProbe",
   125  			}, nil
   126  		},
   127  		grpcAdhocChecksClientFactory: func(conn ClientConn) (sm.AdHocChecksClient, error) {
   128  			return &testClient{logger: logger}, nil
   129  		},
   130  	}
   131  
   132  	h, err := NewHandler(opts)
   133  	require.NoError(t, err)
   134  	require.NotNil(t, h)
   135  
   136  	ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
   137  	defer cancel()
   138  
   139  	runErr := h.Run(ctx)
   140  	require.NoError(t, runErr)
   141  
   142  	payload := <-publishCh
   143  	require.Len(t, payload.Metrics(), 0)
   144  	require.Len(t, payload.Streams(), 1)
   145  
   146  	entry := payload.Streams()[0].Entries[0]
   147  	require.Contains(t, entry.Line, "ad-hoc check done") // log must publish even when verbose is disabled for check tests
   148  	require.Len(t, entry.StructuredMetadata, 1)
   149  	require.Equal(t, "execution_id", entry.StructuredMetadata[0].Name)
   150  	_, err = uuid.Parse(entry.StructuredMetadata[0].Value)
   151  	require.NoError(t, err, "execution_id should be a valid UUID")
   152  }
   153  
   154  type channelPublisher chan pusher.Payload
   155  
   156  func (c channelPublisher) Publish(payload pusher.Payload) {
   157  	c <- payload
   158  }
   159  
   160  type grpcTestError struct {
   161  	code codes.Code
   162  	msg  string
   163  }
   164  
   165  func newGrpcTestError(code codes.Code, msg string) *grpcTestError {
   166  	return &grpcTestError{
   167  		code: code,
   168  		msg:  msg,
   169  	}
   170  }
   171  
   172  func (e grpcTestError) Error() string {
   173  	return e.msg
   174  }
   175  
   176  func (e grpcTestError) GRPCStatus() *status.Status {
   177  	return status.New(e.code, e.msg)
   178  }
   179  
   180  type testBackoff struct{}
   181  
   182  func (testBackoff) Duration() time.Duration {
   183  	return 0
   184  }
   185  
   186  func (testBackoff) Reset() {}
   187  
   188  type timeoutBackoff struct{}
   189  
   190  func (timeoutBackoff) Duration() time.Duration {
   191  	return 100 * time.Millisecond
   192  }
   193  
   194  func (timeoutBackoff) Reset() {}
   195  
   196  type grpcTestConn struct{}
   197  
   198  func (grpcTestConn) GetState() connectivity.State {
   199  	return connectivity.Ready
   200  }
   201  
   202  func (grpcTestConn) Invoke(context.Context, string, any, any, ...grpc.CallOption) error {
   203  	return nil
   204  }
   205  
   206  func (grpcTestConn) NewStream(context.Context, *grpc.StreamDesc, string, ...grpc.CallOption) (grpc.ClientStream, error) {
   207  	return nil, nil
   208  }
   209  
   210  func TestHandlerRunErrors(t *testing.T) {
   211  	features := feature.NewCollection()
   212  	require.NoError(t, features.Set("adhoc"))
   213  
   214  	logger := zerolog.New(io.Discard)
   215  	if testing.Verbose() {
   216  		logger = zerolog.New(os.Stdout)
   217  	}
   218  
   219  	publishCh := make(chan pusher.Payload)
   220  
   221  	testcases := map[string]struct {
   222  		expectError         bool
   223  		registerProbeError  error
   224  		getAdhocChecksError error
   225  	}{
   226  		"canceled": {
   227  			expectError:        false,
   228  			registerProbeError: newGrpcTestError(codes.Canceled, "context canceled"),
   229  		},
   230  		"unimplemented": {
   231  			expectError:        true,
   232  			registerProbeError: newGrpcTestError(codes.Unimplemented, "not implemented"),
   233  		},
   234  		"permission denied": {
   235  			expectError:        true,
   236  			registerProbeError: newGrpcTestError(codes.PermissionDenied, "not authorized"),
   237  		},
   238  		"idle timeout": {
   239  			expectError:        true,
   240  			registerProbeError: newGrpcTestError(codes.Unavailable, "unexpected HTTP status code received from server: 504 (Gateway Timeout); transport: received unexpected content-type \"text/html\""),
   241  		},
   242  		// "transport closing": {
   243  		// 	registerProbeError: newGrpcTestError(codes.Aborted, "transport is closing"),
   244  		// },
   245  		// "other": {
   246  		// 	registerProbeError: errors.New("arbitrary error"),
   247  		// },
   248  	}
   249  
   250  	for name, tc := range testcases {
   251  		t.Run(name, func(t *testing.T) {
   252  			tc := tc
   253  
   254  			// Use timeoutBackoff for idle timeout test to prevent infinite loop
   255  			var backoff Backoffer = testBackoff{}
   256  			if name == "idle timeout" {
   257  				backoff = timeoutBackoff{}
   258  			}
   259  
   260  			opts := HandlerOpts{
   261  				Conn:           &grpcTestConn{},
   262  				Logger:         logger,
   263  				Publisher:      channelPublisher(publishCh),
   264  				Backoff:        backoff,
   265  				TenantCh:       make(chan sm.Tenant),
   266  				PromRegisterer: prometheus.NewPedanticRegistry(),
   267  				Features:       features,
   268  				runnerFactory: func(ctx context.Context, req *sm.AdHocRequest) (*runner, error) {
   269  					return &runner{
   270  						logger: logger,
   271  						prober: &testProber{logger},
   272  						id:     req.AdHocCheck.Id,
   273  						target: req.AdHocCheck.Target,
   274  						probe:  "testProbe",
   275  					}, nil
   276  				},
   277  				grpcAdhocChecksClientFactory: func(conn ClientConn) (sm.AdHocChecksClient, error) {
   278  					return &testClient{logger: logger, registerProbeError: tc.registerProbeError}, nil
   279  				},
   280  			}
   281  
   282  			h, err := NewHandler(opts)
   283  			require.NoError(t, err)
   284  			require.NotNil(t, h)
   285  
   286  			ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
   287  			defer cancel()
   288  
   289  			runErr := h.Run(ctx)
   290  			if tc.expectError {
   291  				require.Error(t, runErr)
   292  			} else {
   293  				require.NoError(t, runErr)
   294  			}
   295  
   296  			select {
   297  			case <-publishCh:
   298  				t.Fail()
   299  			default:
   300  			}
   301  		})
   302  	}
   303  }
   304  
   305  type testClient struct {
   306  	logger              zerolog.Logger
   307  	registerProbeError  error
   308  	getAdhocChecksError error
   309  	registerProbeHook   func(*sm.ProbeInfo)
   310  }
   311  
   312  func (c *testClient) RegisterProbe(ctx context.Context, in *sm.ProbeInfo, opts ...grpc.CallOption) (*sm.RegisterProbeResult, error) {
   313  	c.logger.Info().Str("func", "RegisterProbe").Interface("in", in).Interface("opts", opts).Send()
   314  
   315  	if c.registerProbeHook != nil {
   316  		c.registerProbeHook(in)
   317  	}
   318  
   319  	if c.registerProbeError != nil {
   320  		return nil, c.registerProbeError
   321  	}
   322  
   323  	return &sm.RegisterProbeResult{
   324  		Probe: sm.Probe{
   325  			Id:         1,
   326  			TenantId:   1000,
   327  			Name:       "testProbe",
   328  			Version:    in.Version,
   329  			Commit:     in.Commit,
   330  			Buildstamp: in.Buildstamp,
   331  		},
   332  		Status: sm.Status{
   333  			Code:    sm.StatusCode_OK,
   334  			Message: "OK",
   335  		},
   336  	}, nil
   337  }
   338  
   339  type testGetAdhocChecksClient struct {
   340  	count  int
   341  	logger zerolog.Logger
   342  	ctx    context.Context
   343  }
   344  
   345  func (c *testClient) GetAdHocChecks(ctx context.Context, in *sm.Void, opts ...grpc.CallOption) (sm.AdHocChecks_GetAdHocChecksClient, error) {
   346  	c.logger.Info().Str("func", "GetAdHocChecks").Interface("in", in).Interface("opts", opts).Send()
   347  
   348  	if c.getAdhocChecksError != nil {
   349  		return nil, c.getAdhocChecksError
   350  	}
   351  
   352  	return &testGetAdhocChecksClient{logger: c.logger, ctx: ctx}, nil
   353  }
   354  
   355  func (c *testGetAdhocChecksClient) Recv() (*sm.AdHocRequest, error) {
   356  	c.logger.Info().Str("func", "Recv").Caller(0).Send()
   357  	c.count++
   358  
   359  	switch c.count {
   360  	case 1:
   361  		return &sm.AdHocRequest{
   362  			AdHocCheck: sm.AdHocCheck{
   363  				Id:      "test",
   364  				Target:  "testTarget",
   365  				Timeout: 1,
   366  				Settings: sm.CheckSettings{
   367  					Ping: &sm.PingSettings{},
   368  				},
   369  			},
   370  		}, nil
   371  
   372  	default:
   373  		return nil, io.EOF
   374  	}
   375  }
   376  
   377  func (c *testGetAdhocChecksClient) CloseSend() error {
   378  	c.logger.Info().Str("func", "CloseSend").Caller(0).Send()
   379  	return nil
   380  }
   381  
   382  func (c *testGetAdhocChecksClient) Context() context.Context {
   383  	c.logger.Info().Str("func", "Context").Caller(0).Send()
   384  	return c.ctx
   385  }
   386  
   387  func (c *testGetAdhocChecksClient) Header() (metadata.MD, error) {
   388  	c.logger.Info().Str("func", "Header").Caller(0).Send()
   389  	return nil, nil
   390  }
   391  
   392  func (c *testGetAdhocChecksClient) Trailer() metadata.MD {
   393  	c.logger.Info().Str("func", "Trailer").Caller(0).Send()
   394  	return nil
   395  }
   396  
   397  func (c *testGetAdhocChecksClient) RecvMsg(any) error {
   398  	c.logger.Info().Str("func", "RecvMsg").Caller(0).Send()
   399  	return nil
   400  }
   401  
   402  func (c *testGetAdhocChecksClient) SendMsg(any) error {
   403  	c.logger.Info().Str("func", "SendMsg").Caller(0).Send()
   404  	return nil
   405  }
   406  
   407  type testProber struct {
   408  	logger zerolog.Logger
   409  }
   410  
   411  func (p *testProber) Name() string {
   412  	p.logger.Info().Str("func", "Name").Caller(0).Send()
   413  	return "test"
   414  }
   415  
   416  func (p *testProber) Probe(ctx context.Context, target string, registry *prometheus.Registry, logger logger.Logger, _ string) (bool, float64) {
   417  	p.logger.Info().Str("func", "Probe").Caller(0).Send()
   418  
   419  	g := prometheus.NewGauge(prometheus.GaugeOpts{
   420  		Name: "test",
   421  	})
   422  	g.Set(1)
   423  	registry.MustRegister(g)
   424  
   425  	_ = logger.Log("msg", "test")
   426  
   427  	return true, 1
   428  }
   429  
   430  func TestIdleTimeoutHandling(t *testing.T) {
   431  	features := feature.NewCollection()
   432  	require.NoError(t, features.Set("adhoc"))
   433  
   434  	logger := zerolog.New(io.Discard)
   435  	if testing.Verbose() {
   436  		logger = zerolog.New(os.Stdout)
   437  	}
   438  
   439  	publishCh := make(chan pusher.Payload)
   440  
   441  	opts := HandlerOpts{
   442  		Conn:           &grpcTestConn{},
   443  		Logger:         logger,
   444  		Publisher:      channelPublisher(publishCh),
   445  		Backoff:        testBackoff{},
   446  		TenantCh:       make(chan sm.Tenant),
   447  		PromRegisterer: prometheus.NewPedanticRegistry(),
   448  		Features:       features,
   449  		runnerFactory: func(ctx context.Context, req *sm.AdHocRequest) (*runner, error) {
   450  			return &runner{
   451  				logger: logger,
   452  				prober: &testProber{logger},
   453  				id:     req.AdHocCheck.Id,
   454  				target: req.AdHocCheck.Target,
   455  				probe:  "testProbe",
   456  			}, nil
   457  		},
   458  		grpcAdhocChecksClientFactory: func(conn ClientConn) (sm.AdHocChecksClient, error) {
   459  			return &testClient{
   460  				logger:             logger,
   461  				registerProbeError: newGrpcTestError(codes.Unavailable, "unexpected HTTP status code received from server: 504 (Gateway Timeout); transport: received unexpected content-type \"text/html\""),
   462  			}, nil
   463  		},
   464  	}
   465  
   466  	h, err := NewHandler(opts)
   467  	require.NoError(t, err)
   468  	require.NotNil(t, h)
   469  
   470  	ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
   471  	defer cancel()
   472  
   473  	runErr := h.Run(ctx)
   474  	require.Error(t, runErr)
   475  	require.ErrorIs(t, runErr, context.DeadlineExceeded)
   476  
   477  	// Should not publish anything for idle timeout
   478  	select {
   479  	case <-publishCh:
   480  		t.Fail()
   481  	default:
   482  	}
   483  }
   484  
   485  func TestDefaultRunnerFactory(t *testing.T) {
   486  	t.Parallel()
   487  
   488  	features := feature.NewCollection()
   489  	require.NoError(t, features.Set(feature.K6))
   490  
   491  	logger := zerolog.New(io.Discard)
   492  	if testing.Verbose() {
   493  		logger = zerolog.New(os.Stdout)
   494  	}
   495  
   496  	// Initialize the mockRunner and secretStore
   497  	mockRunner := &testK6Runner{}
   498  	secretStore := &testhelper.TestSecretStore{}
   499  
   500  	testcases := map[string]struct {
   501  		request     *sm.AdHocRequest
   502  		expectError bool
   503  		errCheck    func(error) bool
   504  		shouldPanic bool
   505  	}{
   506  		"ping check": {
   507  			request: &sm.AdHocRequest{
   508  				AdHocCheck: sm.AdHocCheck{
   509  					Id:       "test-ping",
   510  					TenantId: 1000,
   511  					Target:   "example.com",
   512  					Timeout:  1000,
   513  					Settings: sm.CheckSettings{
   514  						Ping: &sm.PingSettings{},
   515  					},
   516  				},
   517  			},
   518  		},
   519  		"http check": {
   520  			request: &sm.AdHocRequest{
   521  				AdHocCheck: sm.AdHocCheck{
   522  					Id:       "test-http",
   523  					TenantId: 1000,
   524  					Target:   "http://example.com",
   525  					Timeout:  1000,
   526  					Settings: sm.CheckSettings{
   527  						Http: &sm.HttpSettings{},
   528  					},
   529  				},
   530  			},
   531  		},
   532  		"dns check": {
   533  			request: &sm.AdHocRequest{
   534  				AdHocCheck: sm.AdHocCheck{
   535  					Id:       "test-dns",
   536  					TenantId: 1000,
   537  					Target:   "example.com",
   538  					Timeout:  1000,
   539  					Settings: sm.CheckSettings{
   540  						Dns: &sm.DnsSettings{},
   541  					},
   542  				},
   543  			},
   544  		},
   545  		"tcp check": {
   546  			request: &sm.AdHocRequest{
   547  				AdHocCheck: sm.AdHocCheck{
   548  					Id:       "test-tcp",
   549  					TenantId: 1000,
   550  					Target:   "example.com:80",
   551  					Timeout:  1000,
   552  					Settings: sm.CheckSettings{
   553  						Tcp: &sm.TcpSettings{},
   554  					},
   555  				},
   556  			},
   557  		},
   558  		"k6 scripted check": {
   559  			request: &sm.AdHocRequest{
   560  				AdHocCheck: sm.AdHocCheck{
   561  					Id:       "test-scripted",
   562  					TenantId: 1000,
   563  					Target:   "test-target",
   564  					Timeout:  1000,
   565  					Settings: sm.CheckSettings{
   566  						Scripted: &sm.ScriptedSettings{},
   567  					},
   568  				},
   569  			},
   570  		},
   571  		"k6 multihttp check": {
   572  			request: &sm.AdHocRequest{
   573  				AdHocCheck: sm.AdHocCheck{
   574  					Id:       "test-multihttp",
   575  					TenantId: 1000,
   576  					Target:   "test-target",
   577  					Timeout:  1000,
   578  					Settings: sm.CheckSettings{
   579  						Multihttp: &sm.MultiHttpSettings{
   580  							Entries: []*sm.MultiHttpEntry{
   581  								{
   582  									Request: &sm.MultiHttpEntryRequest{
   583  										Url: "http://example.com",
   584  									},
   585  								},
   586  							},
   587  						},
   588  					},
   589  				},
   590  			},
   591  		},
   592  		"k6 browser check": {
   593  			request: &sm.AdHocRequest{
   594  				AdHocCheck: sm.AdHocCheck{
   595  					Id:       "test-browser",
   596  					TenantId: 1000,
   597  					Target:   "test-target",
   598  					Timeout:  1000,
   599  					Settings: sm.CheckSettings{
   600  						Browser: &sm.BrowserSettings{},
   601  					},
   602  				},
   603  			},
   604  		},
   605  		"zero timeout": {
   606  			request: &sm.AdHocRequest{
   607  				AdHocCheck: sm.AdHocCheck{
   608  					Id:       "test-zero-timeout",
   609  					TenantId: 1000,
   610  					Target:   "example.com",
   611  					Timeout:  0,
   612  					Settings: sm.CheckSettings{
   613  						Ping: &sm.PingSettings{},
   614  					},
   615  				},
   616  			},
   617  		},
   618  		"empty settings": {
   619  			request: &sm.AdHocRequest{
   620  				AdHocCheck: sm.AdHocCheck{
   621  					Id:       "test-empty-settings",
   622  					TenantId: 1000,
   623  					Target:   "example.com",
   624  					Timeout:  1000,
   625  					Settings: sm.CheckSettings{},
   626  				},
   627  			},
   628  			expectError: true,
   629  			shouldPanic: true,
   630  		},
   631  		"nil request": {
   632  			request:     nil,
   633  			expectError: true,
   634  			errCheck: func(err error) bool {
   635  				return errors.Is(err, errInvalidAdHocRequest)
   636  			},
   637  		},
   638  		"zero tenant": {
   639  			request: &sm.AdHocRequest{
   640  				AdHocCheck: sm.AdHocCheck{
   641  					Id:       "test-zero-tenant",
   642  					TenantId: 0,
   643  					Target:   "example.com",
   644  					Timeout:  1000,
   645  					Settings: sm.CheckSettings{},
   646  				},
   647  			},
   648  			expectError: true,
   649  			errCheck: func(err error) bool {
   650  				return errors.Is(err, errInvalidAdHocRequest)
   651  			},
   652  		},
   653  	}
   654  
   655  	for name, tc := range testcases {
   656  		t.Run(name, func(t *testing.T) {
   657  			t.Parallel()
   658  
   659  			h := &Handler{
   660  				logger:   logger,
   661  				features: features,
   662  				probe: &sm.Probe{
   663  					Name: "test-probe",
   664  				},
   665  				proberFactory: prober.NewProberFactory(mockRunner, 0, features, secretStore),
   666  			}
   667  
   668  			ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
   669  			defer cancel()
   670  
   671  			if tc.shouldPanic {
   672  				require.Panics(t, func() {
   673  					_, _ = h.defaultRunnerFactory(ctx, tc.request)
   674  				})
   675  
   676  				return
   677  			}
   678  
   679  			runner, err := h.defaultRunnerFactory(ctx, tc.request)
   680  			if tc.expectError {
   681  				require.Error(t, err)
   682  
   683  				if tc.errCheck != nil {
   684  					require.True(t, tc.errCheck(err), "unexpected error: %v", err)
   685  				}
   686  
   687  				require.Nil(t, runner)
   688  
   689  				return
   690  			}
   691  
   692  			require.NoError(t, err)
   693  			require.NotNil(t, runner)
   694  
   695  			// Verify runner fields
   696  			require.Equal(t, logger, runner.logger)
   697  			require.NotNil(t, runner.prober)
   698  			require.Equal(t, tc.request.AdHocCheck.Id, runner.id)
   699  			require.Equal(t, "test-probe", runner.probe)
   700  
   701  			// For k6-based checks, verify the grace period is added
   702  			switch tc.request.AdHocCheck.Type() {
   703  			case sm.CheckTypeMultiHttp, sm.CheckTypeScripted, sm.CheckTypeBrowser:
   704  				expectedTimeout := time.Duration(tc.request.AdHocCheck.Timeout)*time.Millisecond + k6AdhocGraceTime
   705  				require.Equal(t, expectedTimeout, runner.timeout)
   706  
   707  			default:
   708  				expectedTimeout := time.Duration(tc.request.AdHocCheck.Timeout) * time.Millisecond
   709  				require.Equal(t, expectedTimeout, runner.timeout)
   710  			}
   711  		})
   712  	}
   713  }
   714  
   715  // Add mock k6runner
   716  type testK6Runner struct{}
   717  
   718  func (r *testK6Runner) WithLogger(logger *zerolog.Logger) k6runner.Runner {
   719  	return r
   720  }
   721  
   722  func (r *testK6Runner) Run(ctx context.Context, script k6runner.Script, secretStore k6runner.SecretStore, _ string) (*k6runner.RunResponse, error) {
   723  	return &k6runner.RunResponse{}, nil
   724  }
   725  
   726  func (*testK6Runner) Versions(_ context.Context) <-chan []string {
   727  	return nil // Blocks forever if read.
   728  }