From 174ecfb2bf51ae2550540e00ad73565610593367 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 26 Mar 2026 08:03:04 +0000 Subject: [PATCH 1/3] Initial plan From 4bad2be5619e87483dfeb1cfe4a0ab5b55b287b7 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 26 Mar 2026 08:12:46 +0000 Subject: [PATCH 2/3] Add elapsed_ms to step.http_call output for latency measurement Co-authored-by: intel352 <77607+intel352@users.noreply.github.com> Agent-Logs-Url: https://github.com/GoCodeAlone/workflow/sessions/e8305985-82c3-4d25-b099-2808597e0880 --- module/pipeline_step_http_call.go | 6 +++++ module/pipeline_step_http_call_test.go | 32 ++++++++++++++++++++++++++ schema/step_schema_builtins.go | 1 + schema/step_schema_test.go | 2 +- 4 files changed, 40 insertions(+), 1 deletion(-) diff --git a/module/pipeline_step_http_call.go b/module/pipeline_step_http_call.go index a0325a4a..bb357085 100644 --- a/module/pipeline_step_http_call.go +++ b/module/pipeline_step_http_call.go @@ -470,6 +470,7 @@ func (s *HTTPCallStep) Execute(ctx context.Context, pc *PipelineContext) (*StepR return nil, err } + start := time.Now() resp, err := s.httpClient.Do(req) //nolint:gosec // G107: URL is user-configured if err != nil { return nil, fmt.Errorf("http_call step %q: request failed: %w", s.name, err) @@ -477,6 +478,7 @@ func (s *HTTPCallStep) Execute(ctx context.Context, pc *PipelineContext) (*StepR defer resp.Body.Close() respBody, err := io.ReadAll(resp.Body) + elapsedMS := time.Since(start).Milliseconds() if err != nil { return nil, fmt.Errorf("http_call step %q: failed to read response: %w", s.name, err) } @@ -511,6 +513,7 @@ func (s *HTTPCallStep) Execute(ctx context.Context, pc *PipelineContext) (*StepR return nil, buildErr } + retryStart := time.Now() retryResp, doErr := s.httpClient.Do(retryReq) //nolint:gosec // G107: URL is user-configured if doErr != nil { return nil, fmt.Errorf("http_call step %q: retry request failed: %w", s.name, doErr) @@ -518,11 +521,13 @@ func (s *HTTPCallStep) Execute(ctx context.Context, pc *PipelineContext) (*StepR defer retryResp.Body.Close() respBody, err = io.ReadAll(retryResp.Body) + retryElapsedMS := time.Since(retryStart).Milliseconds() if err != nil { return nil, fmt.Errorf("http_call step %q: failed to read retry response: %w", s.name, err) } output := parseHTTPResponse(retryResp, respBody) + output["elapsed_ms"] = retryElapsedMS if instanceURL := s.oauthEntry.getInstanceURL(); instanceURL != "" { output["instance_url"] = instanceURL } @@ -533,6 +538,7 @@ func (s *HTTPCallStep) Execute(ctx context.Context, pc *PipelineContext) (*StepR } output := parseHTTPResponse(resp, respBody) + output["elapsed_ms"] = elapsedMS if s.auth != nil { if instanceURL := s.oauthEntry.getInstanceURL(); instanceURL != "" { output["instance_url"] = instanceURL diff --git a/module/pipeline_step_http_call_test.go b/module/pipeline_step_http_call_test.go index 0397ab0f..c5e4a5cf 100644 --- a/module/pipeline_step_http_call_test.go +++ b/module/pipeline_step_http_call_test.go @@ -1076,3 +1076,35 @@ func TestHTTPCallStep_BodyFrom_NilValue(t *testing.T) { t.Errorf("expected empty body for nil body_from, got %q", string(gotBody)) } } + +func TestHTTPCallStep_ElapsedMS(t *testing.T) { + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte(`{}`)) + })) + defer srv.Close() + + factory := NewHTTPCallStepFactory() + step, err := factory("elapsed-test", map[string]any{ + "url": srv.URL, + "method": "GET", + }, nil) + if err != nil { + t.Fatalf("factory error: %v", err) + } + step.(*HTTPCallStep).httpClient = srv.Client() + + pc := NewPipelineContext(nil, nil) + result, err := step.Execute(context.Background(), pc) + if err != nil { + t.Fatalf("execute error: %v", err) + } + + elapsedMS, ok := result.Output["elapsed_ms"].(int64) + if !ok { + t.Fatalf("expected elapsed_ms to be int64, got %T (%v)", result.Output["elapsed_ms"], result.Output["elapsed_ms"]) + } + if elapsedMS < 0 { + t.Errorf("expected elapsed_ms >= 0, got %d", elapsedMS) + } +} diff --git a/schema/step_schema_builtins.go b/schema/step_schema_builtins.go index fd20c754..b1f88b31 100644 --- a/schema/step_schema_builtins.go +++ b/schema/step_schema_builtins.go @@ -85,6 +85,7 @@ func (r *StepSchemaRegistry) registerBuiltins() { {Key: "status", Type: "number", Description: "HTTP response status code"}, {Key: "body", Type: "any", Description: "Response body (parsed as JSON if Content-Type is application/json)"}, {Key: "headers", Type: "map", Description: "Response headers"}, + {Key: "elapsed_ms", Type: "number", Description: "Request duration in milliseconds (wall-clock time from send to response fully read)"}, }, }) diff --git a/schema/step_schema_test.go b/schema/step_schema_test.go index 37c331fb..99a7a539 100644 --- a/schema/step_schema_test.go +++ b/schema/step_schema_test.go @@ -255,7 +255,7 @@ func TestInferStepOutputs_Fallback(t *testing.T) { for _, o := range outputs { keys[o.Key] = true } - if !keys["status"] || !keys["body"] || !keys["headers"] { + if !keys["status"] || !keys["body"] || !keys["headers"] || !keys["elapsed_ms"] { t.Errorf("expected static outputs for step.http_call, got %v", outputs) } } From 7ff8266a0dc88326a9dad53ea8d84860d212036a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 26 Mar 2026 08:35:44 +0000 Subject: [PATCH 3/3] Fix schema outputs and add retry-path elapsed_ms test per review feedback Co-authored-by: intel352 <77607+intel352@users.noreply.github.com> Agent-Logs-Url: https://github.com/GoCodeAlone/workflow/sessions/c86221d6-c717-44a1-9cb1-a4d24a72dbe9 --- module/pipeline_step_http_call_test.go | 7 +++++++ schema/step_schema_builtins.go | 3 ++- schema/step_schema_test.go | 2 +- 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/module/pipeline_step_http_call_test.go b/module/pipeline_step_http_call_test.go index c5e4a5cf..ab24a722 100644 --- a/module/pipeline_step_http_call_test.go +++ b/module/pipeline_step_http_call_test.go @@ -272,6 +272,13 @@ func TestHTTPCallStep_OAuth2_Retry401(t *testing.T) { if result.Output["status_code"] != http.StatusOK { t.Errorf("expected 200 after retry, got %v", result.Output["status_code"]) } + retryElapsedMS, ok := result.Output["elapsed_ms"].(int64) + if !ok { + t.Fatalf("expected elapsed_ms to be int64 on retry path, got %T (%v)", result.Output["elapsed_ms"], result.Output["elapsed_ms"]) + } + if retryElapsedMS < 0 { + t.Errorf("expected elapsed_ms >= 0 on retry path, got %d", retryElapsedMS) + } if atomic.LoadInt32(&tokenRequests) != 2 { t.Errorf("expected 2 token requests, got %d", atomic.LoadInt32(&tokenRequests)) } diff --git a/schema/step_schema_builtins.go b/schema/step_schema_builtins.go index b1f88b31..cc870ac7 100644 --- a/schema/step_schema_builtins.go +++ b/schema/step_schema_builtins.go @@ -82,7 +82,8 @@ func (r *StepSchemaRegistry) registerBuiltins() { {Key: "auth", Type: FieldTypeMap, Description: "Authentication config (type, token, client_id, client_secret, token_url for OAuth2)"}, }, Outputs: []StepOutputDef{ - {Key: "status", Type: "number", Description: "HTTP response status code"}, + {Key: "status_code", Type: "number", Description: "HTTP response status code (e.g. 200)"}, + {Key: "status", Type: "string", Description: "HTTP response status text (e.g. \"200 OK\")"}, {Key: "body", Type: "any", Description: "Response body (parsed as JSON if Content-Type is application/json)"}, {Key: "headers", Type: "map", Description: "Response headers"}, {Key: "elapsed_ms", Type: "number", Description: "Request duration in milliseconds (wall-clock time from send to response fully read)"}, diff --git a/schema/step_schema_test.go b/schema/step_schema_test.go index 99a7a539..4e650f41 100644 --- a/schema/step_schema_test.go +++ b/schema/step_schema_test.go @@ -255,7 +255,7 @@ func TestInferStepOutputs_Fallback(t *testing.T) { for _, o := range outputs { keys[o.Key] = true } - if !keys["status"] || !keys["body"] || !keys["headers"] || !keys["elapsed_ms"] { + if !keys["status_code"] || !keys["body"] || !keys["headers"] || !keys["elapsed_ms"] { t.Errorf("expected static outputs for step.http_call, got %v", outputs) } }