STET

agent_patch

runs/2026-02-28__13-38-10__gpt-5-4/flux-pr-1001/flux-pr-1001.1-of-1.2026-02-28__13-38-10__gpt-5-4/agent-logs/agent.patch

13525 bytes

Back to adjudication
diff --git a/tmp/agent-patch-flux-pr-1001.1-of-1.2026-02-28__13-38-10__gpt-5-4/app/v2/pkg/engine/datasource/httpclient/nethttpclient.go b/app/v2/pkg/engine/datasource/httpclient/nethttpclient.go
index 3d7adb6..6cee717 100644
--- a/tmp/agent-patch-flux-pr-1001.1-of-1.2026-02-28__13-38-10__gpt-5-4/app/v2/pkg/engine/datasource/httpclient/nethttpclient.go
+++ b/app/v2/pkg/engine/datasource/httpclient/nethttpclient.go
@@ -70,6 +70,7 @@ type responseContextKey struct{}
 
 type ResponseContext struct {
 	StatusCode int
+	Trace      *TraceHTTP
 }
 
 func InjectResponseContext(ctx context.Context) (context.Context, *ResponseContext) {
@@ -83,6 +84,15 @@ func setResponseStatusCode(ctx context.Context, statusCode int) {
 	}
 }
 
+func setResponseTrace(ctx context.Context, trace *TraceHTTP) {
+	if value, ok := ctx.Value(responseContextKey{}).(*ResponseContext); ok {
+		value.Trace = trace
+		if trace != nil {
+			value.StatusCode = trace.Response.StatusCode
+		}
+	}
+}
+
 var headersToRedact = []string{
 	"authorization",
 	"www-authenticate",
@@ -191,7 +201,19 @@ func makeHTTPRequest(client *http.Client, ctx context.Context, url, method, head
 	}
 	defer response.Body.Close()
 
-	setResponseStatusCode(ctx, response.StatusCode)
+	responseTrace := &TraceHTTP{
+		Request: TraceHTTPRequest{
+			Method:  request.Method,
+			URL:     request.URL.String(),
+			Headers: request.Header.Clone(),
+		},
+		Response: TraceHTTPResponse{
+			StatusCode: response.StatusCode,
+			Status:     response.Status,
+			Headers:    response.Header.Clone(),
+		},
+	}
+	setResponseTrace(ctx, responseTrace)
 
 	respReader, err := respBodyReader(response)
 	if err != nil {
@@ -212,24 +234,15 @@ func makeHTTPRequest(client *http.Client, ctx context.Context, url, method, head
 	if err != nil {
 		return err
 	}
-	responseTrace := TraceHTTP{
-		Request: TraceHTTPRequest{
-			Method:  request.Method,
-			URL:     request.URL.String(),
-			Headers: redactHeaders(request.Header),
-		},
-		Response: TraceHTTPResponse{
-			StatusCode: response.StatusCode,
-			Status:     response.Status,
-			Headers:    redactHeaders(response.Header),
-			BodySize:   len(data),
-		},
-	}
-	trace, err := json.Marshal(responseTrace)
+	trace := *responseTrace
+	trace.Request.Headers = redactHeaders(trace.Request.Headers)
+	trace.Response.Headers = redactHeaders(trace.Response.Headers)
+	trace.Response.BodySize = len(data)
+	traceData, err := json.Marshal(trace)
 	if err != nil {
 		return err
 	}
-	responseWithTraceExtension, err := jsonparser.Set(data, trace, "extensions", "trace")
+	responseWithTraceExtension, err := jsonparser.Set(data, traceData, "extensions", "trace")
 	if err != nil {
 		return err
 	}
diff --git a/tmp/agent-patch-flux-pr-1001.1-of-1.2026-02-28__13-38-10__gpt-5-4/app/v2/pkg/engine/resolve/loader.go b/app/v2/pkg/engine/resolve/loader.go
index 18788ee..643a5c9 100644
--- a/tmp/agent-patch-flux-pr-1001.1-of-1.2026-02-28__13-38-10__gpt-5-4/app/v2/pkg/engine/resolve/loader.go
+++ b/app/v2/pkg/engine/resolve/loader.go
@@ -37,7 +37,12 @@ type LoaderHooks interface {
 	// OnLoad is called before the fetch is executed
 	OnLoad(ctx context.Context, ds DataSourceInfo) context.Context
 	// OnFinished is called after the fetch has been executed and the response has been processed and merged
-	OnFinished(ctx context.Context, statusCode int, ds DataSourceInfo, err error)
+	OnFinished(ctx context.Context, info *LoaderHookResponseInfo, ds DataSourceInfo, err error)
+}
+
+type LoaderHookResponseInfo struct {
+	StatusCode int
+	HTTP       *httpclient.TraceHTTP
 }
 
 func IsIntrospectionDataSource(dataSourceID string) bool {
@@ -118,7 +123,7 @@ func (l *Loader) resolveParallel(nodes []*FetchTreeNode) error {
 			for j := range results[i].nestedMergeItems {
 				err = l.mergeResult(nodes[i].Item, results[i].nestedMergeItems[j], itemsItems[i][j:j+1])
 				if l.ctx.LoaderHooks != nil && results[i].nestedMergeItems[j].loaderHookContext != nil {
-					l.ctx.LoaderHooks.OnFinished(results[i].nestedMergeItems[j].loaderHookContext, results[i].nestedMergeItems[j].statusCode, results[i].nestedMergeItems[j].ds, goerrors.Join(results[i].nestedMergeItems[j].err, l.ctx.subgraphErrors))
+					l.ctx.LoaderHooks.OnFinished(results[i].nestedMergeItems[j].loaderHookContext, results[i].nestedMergeItems[j].loaderHookResponseInfo(), results[i].nestedMergeItems[j].ds, goerrors.Join(results[i].nestedMergeItems[j].err, l.ctx.subgraphErrors))
 				}
 				if err != nil {
 					return errors.WithStack(err)
@@ -127,7 +132,7 @@ func (l *Loader) resolveParallel(nodes []*FetchTreeNode) error {
 		} else {
 			err = l.mergeResult(nodes[i].Item, results[i], itemsItems[i])
 			if l.ctx.LoaderHooks != nil && results[i].loaderHookContext != nil {
-				l.ctx.LoaderHooks.OnFinished(results[i].loaderHookContext, results[i].statusCode, results[i].ds, goerrors.Join(results[i].err, l.ctx.subgraphErrors))
+				l.ctx.LoaderHooks.OnFinished(results[i].loaderHookContext, results[i].loaderHookResponseInfo(), results[i].ds, goerrors.Join(results[i].err, l.ctx.subgraphErrors))
 			}
 			if err != nil {
 				return errors.WithStack(err)
@@ -163,7 +168,7 @@ func (l *Loader) resolveSingle(item *FetchItem) error {
 		}
 		err = l.mergeResult(item, res, items)
 		if l.ctx.LoaderHooks != nil && res.loaderHookContext != nil {
-			l.ctx.LoaderHooks.OnFinished(res.loaderHookContext, res.statusCode, res.ds, goerrors.Join(res.err, l.ctx.subgraphErrors))
+			l.ctx.LoaderHooks.OnFinished(res.loaderHookContext, res.loaderHookResponseInfo(), res.ds, goerrors.Join(res.err, l.ctx.subgraphErrors))
 		}
 		return err
 	case *BatchEntityFetch:
@@ -176,7 +181,7 @@ func (l *Loader) resolveSingle(item *FetchItem) error {
 		}
 		err = l.mergeResult(item, res, items)
 		if l.ctx.LoaderHooks != nil && res.loaderHookContext != nil {
-			l.ctx.LoaderHooks.OnFinished(res.loaderHookContext, res.statusCode, res.ds, goerrors.Join(res.err, l.ctx.subgraphErrors))
+			l.ctx.LoaderHooks.OnFinished(res.loaderHookContext, res.loaderHookResponseInfo(), res.ds, goerrors.Join(res.err, l.ctx.subgraphErrors))
 		}
 		return err
 	case *EntityFetch:
@@ -189,7 +194,7 @@ func (l *Loader) resolveSingle(item *FetchItem) error {
 		}
 		err = l.mergeResult(item, res, items)
 		if l.ctx.LoaderHooks != nil && res.loaderHookContext != nil {
-			l.ctx.LoaderHooks.OnFinished(res.loaderHookContext, res.statusCode, res.ds, goerrors.Join(res.err, l.ctx.subgraphErrors))
+			l.ctx.LoaderHooks.OnFinished(res.loaderHookContext, res.loaderHookResponseInfo(), res.ds, goerrors.Join(res.err, l.ctx.subgraphErrors))
 		}
 		return err
 	case *ParallelListItemFetch:
@@ -222,7 +227,7 @@ func (l *Loader) resolveSingle(item *FetchItem) error {
 		for i := range results {
 			err = l.mergeResult(item, results[i], items[i:i+1])
 			if l.ctx.LoaderHooks != nil && results[i].loaderHookContext != nil {
-				l.ctx.LoaderHooks.OnFinished(results[i].loaderHookContext, results[i].statusCode, results[i].ds, goerrors.Join(results[i].err, l.ctx.subgraphErrors))
+				l.ctx.LoaderHooks.OnFinished(results[i].loaderHookContext, results[i].loaderHookResponseInfo(), results[i].ds, goerrors.Join(results[i].err, l.ctx.subgraphErrors))
 			}
 			if err != nil {
 				return errors.WithStack(err)
@@ -542,6 +547,7 @@ type result struct {
 	statusCode int
 	err        error
 	ds         DataSourceInfo
+	httpTrace  *httpclient.TraceHTTP
 
 	authorizationRejected        bool
 	authorizationRejectedReasons []string
@@ -554,6 +560,16 @@ type result struct {
 	loaderHookContext context.Context
 }
 
+func (r *result) loaderHookResponseInfo() *LoaderHookResponseInfo {
+	if r == nil {
+		return nil
+	}
+	return &LoaderHookResponseInfo{
+		StatusCode: r.statusCode,
+		HTTP:       r.httpTrace,
+	}
+}
+
 func (r *result) init(postProcessing PostProcessingConfiguration, info *FetchInfo) {
 	r.postProcessing = postProcessing
 	if info != nil {
@@ -1533,6 +1549,7 @@ func (l *Loader) executeSourceLoad(ctx context.Context, fetchItem *FetchItem, so
 	}
 
 	res.statusCode = responseContext.StatusCode
+	res.httpTrace = responseContext.Trace
 
 	if l.ctx.TracingOptions.Enable {
 		stats := GetSingleFlightStats(ctx)
diff --git a/tmp/agent-patch-flux-pr-1001.1-of-1.2026-02-28__13-38-10__gpt-5-4/app/v2/pkg/engine/resolve/loader_hooks_test.go b/app/v2/pkg/engine/resolve/loader_hooks_test.go
index d77c614..25bfaff 100644
--- a/tmp/agent-patch-flux-pr-1001.1-of-1.2026-02-28__13-38-10__gpt-5-4/app/v2/pkg/engine/resolve/loader_hooks_test.go
+++ b/app/v2/pkg/engine/resolve/loader_hooks_test.go
@@ -4,6 +4,8 @@ import (
 	"bytes"
 	"context"
 	"io"
+	"net/http"
+	"net/http/httptest"
 	"sync"
 	"sync/atomic"
 	"testing"
@@ -11,12 +13,14 @@ import (
 	"github.com/golang/mock/gomock"
 	"github.com/stretchr/testify/assert"
 	"github.com/wundergraph/graphql-go-tools/v2/pkg/ast"
+	"github.com/wundergraph/graphql-go-tools/v2/pkg/engine/datasource/httpclient"
 )
 
 type TestLoaderHooks struct {
 	preFetchCalls  atomic.Int64
 	postFetchCalls atomic.Int64
 	errors         []error
+	responses      []*LoaderHookResponseInfo
 	mu             sync.Mutex
 }
 
@@ -25,6 +29,7 @@ func NewTestLoaderHooks() LoaderHooks {
 		preFetchCalls:  atomic.Int64{},
 		postFetchCalls: atomic.Int64{},
 		errors:         make([]error, 0),
+		responses:      make([]*LoaderHookResponseInfo, 0),
 		mu:             sync.Mutex{},
 	}
 }
@@ -35,15 +40,26 @@ func (f *TestLoaderHooks) OnLoad(ctx context.Context, ds DataSourceInfo) context
 	return ctx
 }
 
-func (f *TestLoaderHooks) OnFinished(ctx context.Context, statusCode int, ds DataSourceInfo, err error) {
+func (f *TestLoaderHooks) OnFinished(ctx context.Context, info *LoaderHookResponseInfo, ds DataSourceInfo, err error) {
 	f.postFetchCalls.Add(1)
 
 	f.mu.Lock()
 	defer f.mu.Unlock()
 
+	f.responses = append(f.responses, info)
 	f.errors = append(f.errors, err)
 }
 
+type testHTTPDataSource struct{}
+
+func (t *testHTTPDataSource) Load(ctx context.Context, input []byte, out *bytes.Buffer) error {
+	return httpclient.Do(http.DefaultClient, ctx, input, out)
+}
+
+func (t *testHTTPDataSource) LoadWithFiles(ctx context.Context, input []byte, files []httpclient.File, out *bytes.Buffer) error {
+	return httpclient.Do(http.DefaultClient, ctx, input, out)
+}
+
 func TestLoaderHooks_FetchPipeline(t *testing.T) {
 
 	t.Run("simple fetch with simple subgraph error", testFnWithPostEvaluation(func(t *testing.T, ctrl *gomock.Controller) (node *GraphQLResponse, ctx *Context, expectedOutput string, postEvaluation func(t *testing.T)) {
@@ -651,3 +667,88 @@ func TestLoaderHooks_FetchPipeline(t *testing.T) {
 	}))
 
 }
+
+func TestLoaderHooks_OnFinishedProvidesHTTPMetadata(t *testing.T) {
+	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		assert.Equal(t, "POST", r.Method)
+		assert.Equal(t, "value", r.Header.Get("X-Test"))
+		assert.Equal(t, "bar", r.URL.Query().Get("foo"))
+		w.Header().Set("X-Upstream", "ok")
+		w.WriteHeader(http.StatusAccepted)
+		_, err := w.Write([]byte(`{"data":{"name":"Ada"}}`))
+		assert.NoError(t, err)
+	}))
+	defer server.Close()
+
+	hooks := NewTestLoaderHooks()
+	resolveCtx := &Context{
+		ctx:         context.Background(),
+		LoaderHooks: hooks,
+	}
+
+	var input []byte
+	input = httpclient.SetInputMethod(input, []byte("POST"))
+	input = httpclient.SetInputURL(input, []byte(server.URL))
+	input = httpclient.SetInputBody(input, []byte(`{"query":"{name}"}`))
+	input = httpclient.SetInputHeader(input, []byte(`{"X-Test":["value"]}`))
+	input = httpclient.SetInputQueryParams(input, []byte(`[{"name":"foo","value":"bar"}]`))
+
+	resp := &GraphQLResponse{
+		Info: &GraphQLResponseInfo{
+			OperationType: ast.OperationTypeQuery,
+		},
+		Fetches: SingleWithPath(&SingleFetch{
+			FetchConfiguration: FetchConfiguration{
+				DataSource: &testHTTPDataSource{},
+				PostProcessing: PostProcessingConfiguration{
+					SelectResponseDataPath: []string{"data"},
+				},
+			},
+			InputTemplate: InputTemplate{
+				Segments: []TemplateSegment{
+					{
+						SegmentType: StaticSegmentType,
+						Data:        input,
+					},
+				},
+			},
+			Info: &FetchInfo{
+				DataSourceID:   "Users",
+				DataSourceName: "Users",
+			},
+		}, "query"),
+		Data: &Object{
+			Nullable: false,
+			Fields: []*Field{
+				{
+					Name: []byte("name"),
+					Value: &String{
+						Path:     []string{"name"},
+						Nullable: false,
+					},
+				},
+			},
+		},
+	}
+
+	buf := &bytes.Buffer{}
+	rCtx, cancel := context.WithCancel(context.Background())
+	defer cancel()
+	r := New(rCtx, ResolverOptions{MaxConcurrency: 1024})
+	_, err := r.ResolveGraphQLResponse(resolveCtx, resp, nil, buf)
+	assert.NoError(t, err)
+	assert.Equal(t, `{"data":{"name":"Ada"}}`, buf.String())
+
+	loaderHooks := hooks.(*TestLoaderHooks)
+	assert.Len(t, loaderHooks.responses, 1)
+	assert.NotNil(t, loaderHooks.responses[0])
+	assert.Equal(t, http.StatusAccepted, loaderHooks.responses[0].StatusCode)
+	if assert.NotNil(t, loaderHooks.responses[0].HTTP) {
+		assert.Equal(t, "POST", loaderHooks.responses[0].HTTP.Request.Method)
+		assert.Equal(t, server.URL+"?foo=bar", loaderHooks.responses[0].HTTP.Request.URL)
+		assert.Equal(t, []string{"value"}, loaderHooks.responses[0].HTTP.Request.Headers.Values("X-Test"))
+		assert.Equal(t, []string{"ok"}, loaderHooks.responses[0].HTTP.Response.Headers.Values("X-Upstream"))
+		assert.Equal(t, http.StatusAccepted, loaderHooks.responses[0].HTTP.Response.StatusCode)
+		assert.Equal(t, "202 Accepted", loaderHooks.responses[0].HTTP.Response.Status)
+	}
+}