package api import ( "net/http" "net/http/httptest" "testing" "browser.local/platform/domain" "browser.local/platform/dto" ) func TestRunControlAPIHelloHeartbeatWorkflow(t *testing.T) { router := newTestRouter() helloRecorder := performJSON(t, router, http.MethodPost, "/api/v1/run/control/hello", validRunControlHelloRequest()) assertStatus(t, helloRecorder, http.StatusOK) hello := decodeBody[dto.RunControlHelloResponse](t, helloRecorder) if !hello.Accepted || hello.SessionToken == "" || hello.RunEndpointID != "run-local" { t.Fatalf("expected accepted hello response, got %+v", hello) } endpoint := getJSON[dto.RunEndpointResponse](t, router, "/api/v1/run/endpoints/run-local") if endpoint.Status != domain.RunEndpointStatusOnline || len(endpoint.Capabilities) != 2 { t.Fatalf("expected registered endpoint metadata, got %+v", endpoint) } heartbeatRecorder := performJSON(t, router, http.MethodPost, "/api/v1/run/control/heartbeat", dto.RunControlHeartbeatRequest{ RunEndpointID: "run-local", SessionToken: hello.SessionToken, Version: "0.1.1", Status: domain.RunEndpointStatusOnline, CapabilityFingerprint: "cap-v2", Capacity: dto.RunCapacityResponse{ MaxJobs: 4, RunningJobs: 1, }, }) assertStatus(t, heartbeatRecorder, http.StatusOK) heartbeat := decodeBody[dto.RunControlHeartbeatResponse](t, heartbeatRecorder) if !heartbeat.Accepted || !heartbeat.RefreshCapabilities || heartbeat.NextHeartbeatSeconds <= 0 { t.Fatalf("expected accepted heartbeat with refresh, got %+v", heartbeat) } } func TestRunControlAPIReRegistrationRotatesToken(t *testing.T) { router := newTestRouter() first := decodeBody[dto.RunControlHelloResponse](t, performRunControlHello(t, router, validRunControlHelloRequest())) request := validRunControlHelloRequest() request.Version = "0.2.0" second := decodeBody[dto.RunControlHelloResponse](t, performRunControlHello(t, router, request)) if second.SessionToken == first.SessionToken { t.Fatalf("expected new session token after re-registration, got %q", second.SessionToken) } } func TestRunControlAPIErrors(t *testing.T) { router := newTestRouter() invalid := validRunControlHelloRequest() invalid.RegistrationToken = "" invalid.Capacity.RunningJobs = 8 invalid.Capacity.MaxJobs = 4 invalidHello := performJSON(t, router, http.MethodPost, "/api/v1/run/control/hello", invalid) assertErrorResponse(t, invalidHello, http.StatusBadRequest, errorCodeValidation) hello := decodeBody[dto.RunControlHelloResponse](t, performRunControlHello(t, router, validRunControlHelloRequest())) invalidHeartbeat := performJSON(t, router, http.MethodPost, "/api/v1/run/control/heartbeat", dto.RunControlHeartbeatRequest{ RunEndpointID: "run-local", SessionToken: "stale-token", Version: "0.1.1", Status: domain.RunEndpointStatusOnline, CapabilityFingerprint: "cap-v1", Capacity: dto.RunCapacityResponse{MaxJobs: 4}, }) assertErrorResponse(t, invalidHeartbeat, http.StatusBadRequest, errorCodeValidation) validHeartbeat := performJSON(t, router, http.MethodPost, "/api/v1/run/control/heartbeat", dto.RunControlHeartbeatRequest{ RunEndpointID: "run-local", SessionToken: hello.SessionToken, Version: "0.1.1", Status: domain.RunEndpointStatusOnline, CapabilityFingerprint: "cap-v1", Capacity: dto.RunCapacityResponse{MaxJobs: 4}, }) assertStatus(t, validHeartbeat, http.StatusOK) methodFailure := performRaw(t, router, http.MethodGet, "/api/v1/run/control/hello", "") assertErrorResponse(t, methodFailure, http.StatusMethodNotAllowed, errorCodeMethodNotAllowed) } func performRunControlHello(t *testing.T, router http.Handler, request dto.RunControlHelloRequest) *httptest.ResponseRecorder { t.Helper() recorder := performJSON(t, router, http.MethodPost, "/api/v1/run/control/hello", request) assertStatus(t, recorder, http.StatusOK) return recorder } func validRunControlHelloRequest() dto.RunControlHelloRequest { return dto.RunControlHelloRequest{ RegistrationToken: "registration-token", RunEndpointID: "run-local", DisplayName: "Local Run", Version: "0.1.0", Status: domain.RunEndpointStatusOnline, Platform: "darwin/arm64", CapabilityReport: dto.RunCapabilityReport{ Capabilities: []string{"control.hello", "control.heartbeat"}, Fingerprint: "cap-v1", }, Capacity: dto.RunCapacityResponse{MaxJobs: 4}, } }