agent_patch
runs/2026-02-28__13-38-10__gpt-5-3-codex/flux-pr-1155/flux-pr-1155.1-of-1.2026-02-28__13-38-10__gpt-5-3-codex/agent-logs/agent.patch
20343 bytes
diff --git a/tmp/agent-patch-flux-pr-1155.1-of-1.2026-02-28__13-38-10__gpt-5-3-codex/app/v2/pkg/engine/datasource/graphql_datasource/configuration.go b/app/v2/pkg/engine/datasource/graphql_datasource/configuration.go
index f876776..fe219cb 100644
--- a/tmp/agent-patch-flux-pr-1155.1-of-1.2026-02-28__13-38-10__gpt-5-3-codex/app/v2/pkg/engine/datasource/graphql_datasource/configuration.go
+++ b/app/v2/pkg/engine/datasource/graphql_datasource/configuration.go
@@ -96,7 +96,7 @@ func (c *Configuration) FederationConfiguration() *FederationConfiguration {
}
func (c *Configuration) IsGRPC() bool {
- return c.grpc != nil
+ return c.grpc != nil && !c.grpc.Disabled
}
type SingleTypeField struct {
diff --git a/tmp/agent-patch-flux-pr-1155.1-of-1.2026-02-28__13-38-10__gpt-5-3-codex/app/v2/pkg/engine/datasource/graphql_datasource/graphql_datasource.go b/app/v2/pkg/engine/datasource/graphql_datasource/graphql_datasource.go
index 1d45769..bbdf117 100644
--- a/tmp/agent-patch-flux-pr-1155.1-of-1.2026-02-28__13-38-10__gpt-5-3-codex/app/v2/pkg/engine/datasource/graphql_datasource/graphql_datasource.go
+++ b/app/v2/pkg/engine/datasource/graphql_datasource/graphql_datasource.go
@@ -317,7 +317,8 @@ func (p *Planner[T]) createInputForQuery() (input, operation []byte) {
}
func (p *Planner[T]) ConfigureFetch() resolve.FetchConfiguration {
- if p.config.fetch == nil && p.config.grpc == nil {
+ grpcEnabled := p.config.grpc != nil && !p.config.grpc.Disabled
+ if p.config.fetch == nil && !grpcEnabled {
p.stopWithError(errors.WithStack(errors.New("ConfigureFetch: fetch and grpc configuration is empty")))
return resolve.FetchConfiguration{}
}
@@ -353,8 +354,22 @@ func (p *Planner[T]) ConfigureFetch() resolve.FetchConfiguration {
dataSource = &Source{httpClient: p.fetchClient}
- if p.config.grpc != nil {
+ if grpcEnabled {
var err error
+ grpcClient := p.grpcClient
+ if p.config.grpc.Client != nil {
+ grpcClient = p.config.grpc.Client
+ } else if p.config.grpc.ClientFactory != nil {
+ grpcClient, err = p.config.grpc.ClientFactory()
+ if err != nil {
+ p.stopWithError(errors.WithStack(fmt.Errorf("failed to create gRPC client from factory: %w", err)))
+ return resolve.FetchConfiguration{}
+ }
+ }
+ if grpcClient == nil {
+ p.stopWithError(errors.WithStack(errors.New("failed to create gRPC datasource: grpc client is required")))
+ return resolve.FetchConfiguration{}
+ }
opDocument, opReport := astparser.ParseGraphqlDocumentBytes(operation)
if opReport.HasErrors() {
@@ -362,7 +377,7 @@ func (p *Planner[T]) ConfigureFetch() resolve.FetchConfiguration {
return resolve.FetchConfiguration{}
}
- dataSource, err = grpcdatasource.NewDataSource(p.grpcClient, grpcdatasource.DataSourceConfig{
+ dataSource, err = grpcdatasource.NewDataSource(grpcClient, grpcdatasource.DataSourceConfig{
Operation: &opDocument,
Definition: p.config.schemaConfiguration.upstreamSchemaAst,
Mapping: p.config.grpc.Mapping,
@@ -1736,10 +1751,6 @@ func NewFactoryGRPC(executionContext context.Context, grpcClient grpc.ClientConn
return nil, fmt.Errorf("execution context is required")
}
- if grpcClient == nil {
- return nil, fmt.Errorf("grpc client is required")
- }
-
return &Factory[Configuration]{
executionContext: executionContext,
grpcClient: grpcClient,
diff --git a/tmp/agent-patch-flux-pr-1155.1-of-1.2026-02-28__13-38-10__gpt-5-3-codex/app/v2/pkg/engine/datasource/graphql_datasource/graphql_datasource_test.go b/app/v2/pkg/engine/datasource/graphql_datasource/graphql_datasource_test.go
index eff6a10..fbd4c0d 100644
--- a/tmp/agent-patch-flux-pr-1155.1-of-1.2026-02-28__13-38-10__gpt-5-3-codex/app/v2/pkg/engine/datasource/graphql_datasource/graphql_datasource_test.go
+++ b/app/v2/pkg/engine/datasource/graphql_datasource/graphql_datasource_test.go
@@ -23,6 +23,7 @@ import (
"github.com/stretchr/testify/require"
"github.com/wundergraph/graphql-go-tools/v2/pkg/ast"
+ grpcdatasource "github.com/wundergraph/graphql-go-tools/v2/pkg/engine/datasource/grpc_datasource"
"github.com/wundergraph/graphql-go-tools/v2/pkg/engine/datasource/httpclient"
. "github.com/wundergraph/graphql-go-tools/v2/pkg/engine/datasourcetesting"
"github.com/wundergraph/graphql-go-tools/v2/pkg/engine/plan"
@@ -65,6 +66,17 @@ func mustDataSourceConfigurationWithHttpClient(t *testing.T, id string, metadata
return dsCfg
}
+func TestConfiguration_IsGRPC_Disabled(t *testing.T) {
+ cfg := mustCustomConfiguration(t, ConfigurationInput{
+ SchemaConfiguration: mustSchema(t, nil, "type Query { hello: String }"),
+ GRPC: &grpcdatasource.GRPCConfiguration{
+ Disabled: true,
+ },
+ })
+
+ require.False(t, cfg.IsGRPC())
+}
+
func TestGraphQLDataSource(t *testing.T) {
t.Run("query with double nested fragments with fragment on union", func(t *testing.T) {
definition := `
diff --git a/tmp/agent-patch-flux-pr-1155.1-of-1.2026-02-28__13-38-10__gpt-5-3-codex/app/v2/pkg/engine/datasource/grpc_datasource/compiler.go b/app/v2/pkg/engine/datasource/grpc_datasource/compiler.go
index ed8b887..166c9f6 100644
--- a/tmp/agent-patch-flux-pr-1155.1-of-1.2026-02-28__13-38-10__gpt-5-3-codex/app/v2/pkg/engine/datasource/grpc_datasource/compiler.go
+++ b/app/v2/pkg/engine/datasource/grpc_datasource/compiler.go
@@ -400,16 +400,42 @@ func (p *RPCCompiler) buildProtoMessage(inputMessage Message, rpcMessage *RPCMes
// Get a mutable reference to the list field
list := message.Mutable(fd.ByName(protoref.Name(field.Name))).List()
- // Extract the array elements from the JSON data
- elements := data.Get(rpcField.JSONPath).Array()
+ // Extract the array elements from the JSON data. If a scalar is provided for a list field,
+ // treat it as a single-item list to match GraphQL list coercion behavior.
+ elements := p.listInputElements(data.Get(rpcField.JSONPath))
if len(elements) == 0 {
continue
}
// Process each element and append to the list
for _, element := range elements {
- fieldMsg := p.buildProtoMessage(p.doc.Messages[field.MessageRef], rpcField.Message, element)
- list.Append(protoref.ValueOfMessage(fieldMsg))
+ if field.MessageRef >= 0 {
+ fieldMsg := p.buildProtoMessage(p.doc.Messages[field.MessageRef], rpcField.Message, element)
+ if fieldMsg == nil {
+ continue
+ }
+
+ list.Append(protoref.ValueOfMessage(fieldMsg))
+ continue
+ }
+
+ if field.Type == DataTypeEnum {
+ enum, ok := p.doc.EnumByName(rpcField.EnumName)
+ if !ok {
+ p.report.AddInternalError(fmt.Errorf("enum %s not found in document", rpcField.EnumName))
+ continue
+ }
+
+ enumNumber, ok := p.resolveEnumNumber(enum, element.String())
+ if !ok {
+ continue
+ }
+
+ list.Append(protoref.ValueOfEnum(protoref.EnumNumber(enumNumber)))
+ continue
+ }
+
+ list.Append(p.setValueForKind(field.Type, element))
}
continue
@@ -430,17 +456,16 @@ func (p *RPCCompiler) buildProtoMessage(inputMessage Message, rpcMessage *RPCMes
continue
}
- for _, enumValue := range enum.Values {
- if enumValue.GraphqlValue == data.Get(rpcField.JSONPath).String() {
- message.Set(
- fd.ByName(protoref.Name(field.Name)),
- protoref.ValueOfEnum(protoref.EnumNumber(enumValue.Number)),
- )
-
- break
- }
+ enumNumber, ok := p.resolveEnumNumber(enum, data.Get(rpcField.JSONPath).String())
+ if !ok {
+ continue
}
+ message.Set(
+ fd.ByName(protoref.Name(field.Name)),
+ protoref.ValueOfEnum(protoref.EnumNumber(enumNumber)),
+ )
+
continue
}
@@ -463,20 +488,44 @@ func (p *RPCCompiler) setValueForKind(kind DataType, data gjson.Result) protoref
case DataTypeInt64:
return protoref.ValueOfInt64(data.Int())
case DataTypeUint32:
- return protoref.ValueOfUint32(uint32(data.Int()))
+ return protoref.ValueOfUint32(uint32(data.Uint()))
case DataTypeUint64:
- return protoref.ValueOfUint64(uint64(data.Int()))
+ return protoref.ValueOfUint64(data.Uint())
case DataTypeFloat:
return protoref.ValueOfFloat32(float32(data.Float()))
case DataTypeDouble:
return protoref.ValueOfFloat64(data.Float())
case DataTypeBool:
return protoref.ValueOfBool(data.Bool())
+ case DataTypeBytes:
+ return protoref.ValueOfBytes([]byte(data.String()))
}
return protoref.Value{}
}
+func (p *RPCCompiler) listInputElements(value gjson.Result) []gjson.Result {
+ if !value.Exists() || value.Type == gjson.Null {
+ return nil
+ }
+
+ if value.IsArray() {
+ return value.Array()
+ }
+
+ return []gjson.Result{value}
+}
+
+func (p *RPCCompiler) resolveEnumNumber(enum Enum, graphqlValue string) (int32, bool) {
+ for _, enumValue := range enum.Values {
+ if enumValue.GraphqlValue == graphqlValue || enumValue.Name == graphqlValue {
+ return enumValue.Number, true
+ }
+ }
+
+ return 0, false
+}
+
// parseEnum extracts information from a protobuf enum descriptor.
func (p *RPCCompiler) parseEnum(e protoref.EnumDescriptor, mapping *GRPCMapping) Enum {
var enumValueMappings []EnumValueMapping
diff --git a/tmp/agent-patch-flux-pr-1155.1-of-1.2026-02-28__13-38-10__gpt-5-3-codex/app/v2/pkg/engine/datasource/grpc_datasource/compiler_test.go b/app/v2/pkg/engine/datasource/grpc_datasource/compiler_test.go
index 5e098c3..ecad543 100644
--- a/tmp/agent-patch-flux-pr-1155.1-of-1.2026-02-28__13-38-10__gpt-5-3-codex/app/v2/pkg/engine/datasource/grpc_datasource/compiler_test.go
+++ b/app/v2/pkg/engine/datasource/grpc_datasource/compiler_test.go
@@ -6,6 +6,7 @@ import (
"github.com/stretchr/testify/require"
"github.com/tidwall/gjson"
"github.com/wundergraph/graphql-go-tools/v2/pkg/grpctest"
+ protoref "google.golang.org/protobuf/reflect/protoreflect"
)
// Complete valid protobuf definition with service and message definitions
@@ -291,3 +292,56 @@ func TestBuildProtoMessage(t *testing.T) {
require.Equal(t, 1, len(invocations))
}
+
+func TestBuildProtoMessage_RepeatedScalarWithScalarInput(t *testing.T) {
+ schema := `
+syntax = "proto3";
+package test.v1;
+
+service TestService {
+ rpc QueryTags(QueryTagsRequest) returns (QueryTagsResponse) {}
+}
+
+message QueryTagsRequest {
+ repeated string tags = 1;
+}
+
+message QueryTagsResponse {}
+`
+ compiler, err := NewProtoCompiler(schema, nil)
+ require.NoError(t, err)
+
+ executionPlan := &RPCExecutionPlan{
+ Calls: []RPCCall{
+ {
+ MethodName: "QueryTags",
+ Request: RPCMessage{
+ Name: "QueryTagsRequest",
+ Fields: []RPCField{
+ {
+ Name: "kinds",
+ TypeName: string(DataTypeString),
+ JSONPath: "tags",
+ Repeated: true,
+ },
+ },
+ },
+ Response: RPCMessage{Name: "QueryTagsResponse"},
+ },
+ },
+ }
+
+ executionPlan.Calls[0].Request.Fields[0].Name = "tags"
+
+ variables := []byte(`{"variables":{"tags":"single"}}`)
+ invocations, err := compiler.Compile(executionPlan, gjson.ParseBytes(variables).Get("variables"))
+ require.NoError(t, err)
+ require.Len(t, invocations, 1)
+
+ fd := invocations[0].Input.Descriptor().Fields().ByName(protoref.Name("tags"))
+ require.NotNil(t, fd)
+
+ list := invocations[0].Input.Get(fd).List()
+ require.Equal(t, 1, list.Len())
+ require.Equal(t, "single", list.Get(0).String())
+}
diff --git a/tmp/agent-patch-flux-pr-1155.1-of-1.2026-02-28__13-38-10__gpt-5-3-codex/app/v2/pkg/engine/datasource/grpc_datasource/configuration.go b/app/v2/pkg/engine/datasource/grpc_datasource/configuration.go
index 4dbd48b..ead8fe8 100644
--- a/tmp/agent-patch-flux-pr-1155.1-of-1.2026-02-28__13-38-10__gpt-5-3-codex/app/v2/pkg/engine/datasource/grpc_datasource/configuration.go
+++ b/app/v2/pkg/engine/datasource/grpc_datasource/configuration.go
@@ -1,5 +1,7 @@
package grpcdatasource
+import "google.golang.org/grpc"
+
type (
// RPCConfigMap is a map of RPC names to RPC configurations
RPCConfigMap map[string]RPCConfig
@@ -32,6 +34,13 @@ type EnumValueMapping struct {
type GRPCConfiguration struct {
Mapping *GRPCMapping
Compiler *RPCCompiler
+
+ // Disabled allows switching off gRPC execution while keeping the config attached.
+ Disabled bool
+ // Client allows overriding the default factory-level gRPC client for this datasource.
+ Client grpc.ClientConnInterface
+ // ClientFactory allows creating a datasource-specific client dynamically.
+ ClientFactory func() (grpc.ClientConnInterface, error)
}
// RPCConfig defines the configuration for a specific RPC operation
diff --git a/tmp/agent-patch-flux-pr-1155.1-of-1.2026-02-28__13-38-10__gpt-5-3-codex/app/v2/pkg/engine/datasource/grpc_datasource/grpc_datasource.go b/app/v2/pkg/engine/datasource/grpc_datasource/grpc_datasource.go
index 940d501..2df4490 100644
--- a/tmp/agent-patch-flux-pr-1155.1-of-1.2026-02-28__13-38-10__gpt-5-3-codex/app/v2/pkg/engine/datasource/grpc_datasource/grpc_datasource.go
+++ b/app/v2/pkg/engine/datasource/grpc_datasource/grpc_datasource.go
@@ -17,6 +17,7 @@ import (
"github.com/wundergraph/graphql-go-tools/v2/pkg/engine/datasource/httpclient"
"github.com/wundergraph/graphql-go-tools/v2/pkg/engine/resolve"
"google.golang.org/grpc"
+ "google.golang.org/grpc/status"
protoref "google.golang.org/protobuf/reflect/protoreflect"
)
@@ -123,7 +124,11 @@ func (d *DataSource) LoadWithFiles(ctx context.Context, input []byte, files []*h
func (d *DataSource) marshalResponseJSON(arena *astjson.Arena, message *RPCMessage, data protoref.Message) (*astjson.Value, error) {
if message == nil {
- return nil, nil
+ return arena.NewNull(), nil
+ }
+
+ if !data.IsValid() {
+ return arena.NewNull(), nil
}
root := arena.NewObject()
@@ -161,20 +166,35 @@ func (d *DataSource) marshalResponseJSON(arena *astjson.Arena, message *RPCMessa
arr := arena.NewArray()
root.Set(field.JSONPath, arr)
list := data.Get(fd).List()
+ if !list.IsValid() {
+ root.Set(field.JSONPath, arena.NewNull())
+ continue
+ }
+
for i := 0; i < list.Len(); i++ {
- message := list.Get(i).Message()
- value, err := d.marshalResponseJSON(arena, field.Message, message)
- if err != nil {
- return nil, err
+ item := list.Get(i)
+ if field.Message != nil || fd.Kind() == protoref.MessageKind {
+ value, err := d.marshalResponseJSON(arena, field.Message, item.Message())
+ if err != nil {
+ return nil, err
+ }
+
+ arr.SetArrayItem(i, value)
+ continue
}
- arr.SetArrayItem(i, value)
+ arr.SetArrayItem(i, d.jsonValueForKind(arena, data, fd, item))
}
continue
}
if fd.Kind() == protoref.MessageKind {
+ if !data.Has(fd) {
+ root.Set(field.JSONPath, arena.NewNull())
+ continue
+ }
+
msg := data.Get(fd).Message()
value, err := d.marshalResponseJSON(arena, field.Message, msg)
if err != nil {
@@ -193,47 +213,47 @@ func (d *DataSource) marshalResponseJSON(arena *astjson.Arena, message *RPCMessa
continue
}
- d.setJSONValue(arena, root, field.JSONPath, data, fd)
+ root.Set(field.JSONPath, d.jsonValueForKind(arena, data, fd, data.Get(fd)))
}
return root, nil
}
-func (d *DataSource) setJSONValue(arena *astjson.Arena, root *astjson.Value, name string, data protoref.Message, fd protoref.FieldDescriptor) {
+func (d *DataSource) jsonValueForKind(arena *astjson.Arena, data protoref.Message, fd protoref.FieldDescriptor, value protoref.Value) *astjson.Value {
switch fd.Kind() {
case protoref.BoolKind:
- boolValue := data.Get(fd).Bool()
+ boolValue := value.Bool()
if boolValue {
- root.Set(name, arena.NewTrue())
+ return arena.NewTrue()
} else {
- root.Set(name, arena.NewFalse())
+ return arena.NewFalse()
}
case protoref.StringKind:
- root.Set(name, arena.NewString(data.Get(fd).String()))
+ return arena.NewString(value.String())
case protoref.Int32Kind, protoref.Int64Kind:
- root.Set(name, arena.NewNumberInt(int(data.Get(fd).Int())))
+ return arena.NewNumberInt(int(value.Int()))
case protoref.Uint32Kind, protoref.Uint64Kind:
- root.Set(name, arena.NewNumberString(fmt.Sprintf("%d", data.Get(fd).Uint())))
+ return arena.NewNumberString(fmt.Sprintf("%d", value.Uint()))
case protoref.FloatKind, protoref.DoubleKind:
- root.Set(name, arena.NewNumberFloat64(data.Get(fd).Float()))
+ return arena.NewNumberFloat64(value.Float())
case protoref.BytesKind:
- root.Set(name, arena.NewStringBytes(data.Get(fd).Bytes()))
+ return arena.NewStringBytes(value.Bytes())
case protoref.EnumKind:
enumDesc := fd.Enum()
- enumValueDesc := enumDesc.Values().ByNumber(data.Get(fd).Enum())
+ enumValueDesc := enumDesc.Values().ByNumber(value.Enum())
if enumValueDesc == nil {
- root.Set(name, arena.NewNull())
- return
+ return arena.NewNull()
}
graphqlValue, ok := d.mapping.ResolveEnumValue(string(enumDesc.Name()), string(enumValueDesc.Name()))
if !ok {
- root.Set(name, arena.NewNull())
- return
+ return arena.NewNull()
}
- root.Set(name, arena.NewString(graphqlValue))
+ return arena.NewString(graphqlValue)
}
+
+ return arena.NewNull()
}
func writeErrorBytes(err error) []byte {
@@ -244,6 +264,14 @@ func writeErrorBytes(err error) []byte {
errorItem := a.NewObject()
errorItem.Set("message", a.NewString(err.Error()))
+
+ if st, ok := status.FromError(err); ok {
+ extensions := a.NewObject()
+ extensions.Set("grpcStatusCode", a.NewNumberInt(int(st.Code())))
+ extensions.Set("grpcStatusName", a.NewString(st.Code().String()))
+ errorItem.Set("extensions", extensions)
+ }
+
errorArray.SetArrayItem(0, errorItem)
return errorRoot.MarshalTo(nil)
diff --git a/tmp/agent-patch-flux-pr-1155.1-of-1.2026-02-28__13-38-10__gpt-5-3-codex/app/v2/pkg/engine/datasource/grpc_datasource/grpc_datasource_test.go b/app/v2/pkg/engine/datasource/grpc_datasource/grpc_datasource_test.go
index a14f5a1..0d62661 100644
--- a/tmp/agent-patch-flux-pr-1155.1-of-1.2026-02-28__13-38-10__gpt-5-3-codex/app/v2/pkg/engine/datasource/grpc_datasource/grpc_datasource_test.go
+++ b/app/v2/pkg/engine/datasource/grpc_datasource/grpc_datasource_test.go
@@ -425,6 +425,8 @@ func Test_DataSource_Load_WithGrpcError(t *testing.T) {
// Verify the error message
require.Contains(t, response.Errors[0].Message, "user not found: error-user")
+ require.Contains(t, responseJson, `"grpcStatusCode":5`)
+ require.Contains(t, responseJson, `"grpcStatusName":"NotFound"`)
}
func TestMarshalResponseJSON(t *testing.T) {
@@ -493,6 +495,62 @@ func TestMarshalResponseJSON(t *testing.T) {
require.Equal(t, `{"_entities":[{"__typename":"Product","id":"123","name_different":"test","price_different":123.45}]}`, responseJSON.String())
}
+func TestMarshalResponseJSON_RepeatedScalar(t *testing.T) {
+ schema := `
+syntax = "proto3";
+package test.v1;
+
+service TestService {
+ rpc QueryTags(QueryTagsRequest) returns (QueryTagsResponse) {}
+}
+
+message QueryTagsRequest {}
+message QueryTagsResponse {
+ repeated string tags = 1;
+ repeated int32 counts = 2;
+}
+`
+ compiler, err := NewProtoCompiler(schema, nil)
+ require.NoError(t, err)
+
+ response := RPCMessage{
+ Name: "QueryTagsResponse",
+ Fields: []RPCField{
+ {
+ Name: "tags",
+ TypeName: string(DataTypeString),
+ JSONPath: "tags",
+ Repeated: true,
+ },
+ {
+ Name: "counts",
+ TypeName: string(DataTypeInt32),
+ JSONPath: "counts",
+ Repeated: true,
+ },
+ },
+ }
+
+ responseMessageDesc := compiler.doc.MessageByName("QueryTagsResponse").Desc
+ responseMessage := dynamicpb.NewMessage(responseMessageDesc)
+
+ tagsField := responseMessageDesc.Fields().ByName("tags")
+ tagsList := responseMessage.Mutable(tagsField).List()
+ tagsList.Append(protoref.ValueOfString("one"))
+ tagsList.Append(protoref.ValueOfString("two"))
+
+ countsField := responseMessageDesc.Fields().ByName("counts")
+ countsList := responseMessage.Mutable(countsField).List()
+ countsList.Append(protoref.ValueOfInt32(1))
+ countsList.Append(protoref.ValueOfInt32(2))
+
+ ds := &DataSource{}
+ arena := astjson.Arena{}
+ responseJSON, err := ds.marshalResponseJSON(&arena, &response, responseMessage)
+ require.NoError(t, err)
+ require.Equal(t, `{"tags":["one","two"],"counts":[1,2]}`, responseJSON.String())
+}
+
// TODO test interface types
// Test_DataSource_Load_WithAnimalInterface tests the datasource with Animal interface types (Cat/Dog)
// using a bufconn connection to the mock service