agent_patch
runs/2026-02-28__13-38-10__gpt-5-4/flux-pr-891/flux-pr-891.1-of-1.2026-02-28__13-38-10__gpt-5-4/agent-logs/agent.patch
15442 bytes
diff --git a/tmp/agent-patch-flux-pr-891.1-of-1.2026-02-28__13-38-10__gpt-5-4/app/v2/pkg/engine/plan/visitor.go b/app/v2/pkg/engine/plan/visitor.go
index 9ba1abb..eca0a16 100644
--- a/tmp/agent-patch-flux-pr-891.1-of-1.2026-02-28__13-38-10__gpt-5-4/app/v2/pkg/engine/plan/visitor.go
+++ b/app/v2/pkg/engine/plan/visitor.go
@@ -663,6 +663,7 @@ func (v *Visitor) resolveFieldValue(fieldRef, typeRef int, nullable bool, path [
Path: path,
Fields: []*resolve.Field{},
}
+ resolve.SetObjectPossibleTypeNames(object, v.resolvePossibleTypeNames(typeDefinitionNode))
v.objects = append(v.objects, object)
v.Walker.DefferOnEnterField(func() {
v.currentFields = append(v.currentFields, objectFields{
@@ -679,6 +680,35 @@ func (v *Visitor) resolveFieldValue(fieldRef, typeRef int, nullable bool, path [
}
}
+func (v *Visitor) resolvePossibleTypeNames(typeDefinitionNode ast.Node) [][]byte {
+ switch typeDefinitionNode.Kind {
+ case ast.NodeKindObjectTypeDefinition:
+ return [][]byte{v.Config.Types.RenameTypeNameOnMatchBytes(typeDefinitionNode.NameBytes(v.Definition))}
+ case ast.NodeKindInterfaceTypeDefinition:
+ typeNames, ok := v.Definition.InterfaceTypeDefinitionImplementedByObjectWithNames(typeDefinitionNode.Ref)
+ if !ok {
+ return nil
+ }
+ possibleTypeNames := make([][]byte, 0, len(typeNames))
+ for i := range typeNames {
+ possibleTypeNames = append(possibleTypeNames, v.Config.Types.RenameTypeNameOnMatchBytes([]byte(typeNames[i])))
+ }
+ return possibleTypeNames
+ case ast.NodeKindUnionTypeDefinition:
+ typeNames, ok := v.Definition.UnionTypeDefinitionMemberTypeNames(typeDefinitionNode.Ref)
+ if !ok {
+ return nil
+ }
+ possibleTypeNames := make([][]byte, 0, len(typeNames))
+ for i := range typeNames {
+ possibleTypeNames = append(possibleTypeNames, v.Config.Types.RenameTypeNameOnMatchBytes([]byte(typeNames[i])))
+ }
+ return possibleTypeNames
+ default:
+ return nil
+ }
+}
+
func (v *Visitor) resolveFieldExport(fieldRef int) *resolve.FieldExport {
if !v.Operation.Fields[fieldRef].HasDirectives {
return nil
diff --git a/tmp/agent-patch-flux-pr-891.1-of-1.2026-02-28__13-38-10__gpt-5-4/app/v2/pkg/engine/resolve/const.go b/app/v2/pkg/engine/resolve/const.go
index 7fb406d..82cd0bc 100644
--- a/tmp/agent-patch-flux-pr-891.1-of-1.2026-02-28__13-38-10__gpt-5-4/app/v2/pkg/engine/resolve/const.go
+++ b/app/v2/pkg/engine/resolve/const.go
@@ -45,8 +45,9 @@ var (
)
const (
- errorsMessagePathIndex = 0
- errorsLocationsPathIndex = 1
- errorsPathPathIndex = 2
- errorsExtensionsPathIndex = 3
+ errorsMessagePathIndex = 0
+ errorsLocationsPathIndex = 1
+ errorsPathPathIndex = 2
+ errorsExtensionsPathIndex = 3
+ errorCodeInvalidGraphqlResponse = "INVALID_GRAPHQL_RESPONSE"
)
diff --git a/tmp/agent-patch-flux-pr-891.1-of-1.2026-02-28__13-38-10__gpt-5-4/app/v2/pkg/engine/resolve/node_object.go b/app/v2/pkg/engine/resolve/node_object.go
index 214d237..30d9fca 100644
--- a/tmp/agent-patch-flux-pr-891.1-of-1.2026-02-28__13-38-10__gpt-5-4/app/v2/pkg/engine/resolve/node_object.go
+++ b/app/v2/pkg/engine/resolve/node_object.go
@@ -3,8 +3,11 @@ package resolve
import (
"bytes"
"slices"
+ "sync"
)
+var objectPossibleTypeNames sync.Map
+
type Object struct {
Nullable bool
Path []string
@@ -18,12 +21,16 @@ func (o *Object) Copy() Node {
for i, f := range o.Fields {
fields[i] = f.Copy()
}
- return &Object{
+ out := &Object{
Nullable: o.Nullable,
Path: o.Path,
Fields: fields,
Fetches: o.Fetches,
}
+ if typeNames := ObjectPossibleTypeNames(o); len(typeNames) > 0 {
+ SetObjectPossibleTypeNames(out, typeNames)
+ }
+ return out
}
func (_ *Object) NodeKind() NodeKind {
@@ -62,6 +69,27 @@ func (o *Object) Equals(n Node) bool {
return true
}
+func SetObjectPossibleTypeNames(object *Object, typeNames [][]byte) {
+ if object == nil {
+ return
+ }
+ if len(typeNames) == 0 {
+ objectPossibleTypeNames.Delete(object)
+ return
+ }
+ objectPossibleTypeNames.Store(object, typeNames)
+}
+
+func ObjectPossibleTypeNames(object *Object) [][]byte {
+ if object == nil {
+ return nil
+ }
+ if value, ok := objectPossibleTypeNames.Load(object); ok {
+ return value.([][]byte)
+ }
+ return nil
+}
+
type EmptyObject struct{}
func (_ *EmptyObject) NodeKind() NodeKind {
diff --git a/tmp/agent-patch-flux-pr-891.1-of-1.2026-02-28__13-38-10__gpt-5-4/app/v2/pkg/engine/resolve/resolvable.go b/app/v2/pkg/engine/resolve/resolvable.go
index 4512664..c73ffb5 100644
--- a/tmp/agent-patch-flux-pr-891.1-of-1.2026-02-28__13-38-10__gpt-5-4/app/v2/pkg/engine/resolve/resolvable.go
+++ b/app/v2/pkg/engine/resolve/resolvable.go
@@ -6,6 +6,7 @@ import (
goerrors "errors"
"fmt"
"io"
+ "strings"
"github.com/cespare/xxhash/v2"
"github.com/goccy/go-json"
@@ -455,13 +456,20 @@ func (r *Resolvable) walkObject(obj *Object, parent *astjson.Value) bool {
r.addNonNullableFieldError(obj.Path, parent)
return r.err()
}
- r.pushNodePathElement(obj.Path)
- isRoot := r.depth < 2
- defer r.popNodePathElement(obj.Path)
if value.Type() != astjson.TypeObject {
r.addError("Object cannot represent non-object value.", obj.Path)
return r.err()
}
+ if !r.validateObjectTypeName(obj, value) {
+ if obj.Nullable && len(obj.Path) > 0 {
+ astjson.SetNull(parent, obj.Path...)
+ return false
+ }
+ return r.err()
+ }
+ r.pushNodePathElement(obj.Path)
+ isRoot := r.depth < 2
+ defer r.popNodePathElement(obj.Path)
if r.print && !isRoot {
r.printBytes(lBrace)
r.ctx.Stats.ResolvedObjects++
@@ -752,7 +760,7 @@ func (r *Resolvable) walkString(s *String, value *astjson.Value) bool {
}
if value.Type() != astjson.TypeString {
r.marshalBuf = value.MarshalTo(r.marshalBuf[:0])
- r.addError(fmt.Sprintf("String cannot represent non-string value: \\\"%s\\\"", string(r.marshalBuf)), s.Path)
+ r.addError(fmt.Sprintf("String cannot represent non-string value: \"%s\"", string(r.marshalBuf)), s.Path)
return r.err()
}
if r.print {
@@ -801,7 +809,7 @@ func (r *Resolvable) walkBoolean(b *Boolean, value *astjson.Value) bool {
}
if value.Type() != astjson.TypeTrue && value.Type() != astjson.TypeFalse {
r.marshalBuf = value.MarshalTo(r.marshalBuf[:0])
- r.addError(fmt.Sprintf("Bool cannot represent non-boolean value: \\\"%s\\\"", string(r.marshalBuf)), b.Path)
+ r.addError(fmt.Sprintf("Bool cannot represent non-boolean value: \"%s\"", string(r.marshalBuf)), b.Path)
return r.err()
}
if r.print {
@@ -825,7 +833,7 @@ func (r *Resolvable) walkInteger(i *Integer, value *astjson.Value) bool {
}
if value.Type() != astjson.TypeNumber {
r.marshalBuf = value.MarshalTo(r.marshalBuf[:0])
- r.addError(fmt.Sprintf("Int cannot represent non-integer value: \\\"%s\\\"", string(r.marshalBuf)), i.Path)
+ r.addError(fmt.Sprintf("Int cannot represent non-integer value: \"%s\"", string(r.marshalBuf)), i.Path)
return r.err()
}
if r.print {
@@ -849,7 +857,7 @@ func (r *Resolvable) walkFloat(f *Float, value *astjson.Value) bool {
}
if value.Type() != astjson.TypeNumber {
r.marshalBuf = value.MarshalTo(r.marshalBuf[:0])
- r.addError(fmt.Sprintf("Float cannot represent non-float value: \\\"%s\\\"", string(r.marshalBuf)), f.Path)
+ r.addError(fmt.Sprintf("Float cannot represent non-float value: \"%s\"", string(r.marshalBuf)), f.Path)
return r.err()
}
if r.print {
@@ -950,7 +958,7 @@ func (r *Resolvable) addNonNullableFieldError(fieldPath []string, parent *astjso
}
r.pushNodePathElement(fieldPath)
errorMessage := fmt.Sprintf("Cannot return null for non-nullable field '%s'.", r.renderFieldPath())
- fastjsonext.AppendErrorToArray(r.errors, errorMessage, r.path)
+ r.addResolvedError(errorMessage, r.path, nil)
r.popNodePathElement(fieldPath)
}
@@ -976,6 +984,81 @@ func (r *Resolvable) renderFieldPath() string {
func (r *Resolvable) addError(message string, fieldPath []string) {
r.pushNodePathElement(fieldPath)
- fastjsonext.AppendErrorToArray(r.errors, message, r.path)
+ r.addResolvedError(message, r.path, nil)
+ r.popNodePathElement(fieldPath)
+}
+
+func (r *Resolvable) addErrorWithCode(message string, fieldPath []string, code string) {
+ r.pushNodePathElement(fieldPath)
+ r.addResolvedError(message, r.path, map[string]string{
+ "code": code,
+ })
+ r.popNodePathElement(fieldPath)
+}
+
+func (r *Resolvable) addResolvedError(message string, path []fastjsonext.PathElement, extensions map[string]string) {
+ if r.errors == nil || r.errors.Type() != astjson.TypeArray {
+ return
+ }
+ errorObject := r.astjsonArena.NewObject()
+ errorObject.Set("message", r.astjsonArena.NewString(message))
+ if len(path) > 0 {
+ errorPath := r.astjsonArena.NewArray()
+ for i := range path {
+ if path[i].Name != "" {
+ errorPath.SetArrayItem(i, r.astjsonArena.NewString(path[i].Name))
+ } else {
+ errorPath.SetArrayItem(i, r.astjsonArena.NewNumberInt(path[i].Idx))
+ }
+ }
+ errorObject.Set("path", errorPath)
+ }
+ if len(extensions) > 0 {
+ extensionsObject := r.astjsonArena.NewObject()
+ for key, value := range extensions {
+ extensionsObject.Set(key, r.astjsonArena.NewString(value))
+ }
+ errorObject.Set("extensions", extensionsObject)
+ }
+ astjson.AppendToArray(r.errors, errorObject)
+}
+
+func (r *Resolvable) validateObjectTypeName(obj *Object, value *astjson.Value) bool {
+ possibleTypeNames := ObjectPossibleTypeNames(obj)
+ if len(possibleTypeNames) == 0 {
+ return true
+ }
+ typeNameValue := value.Get("__typename")
+ if typeNameValue == nil || typeNameValue.Type() == astjson.TypeNull {
+ return true
+ }
+ fieldPath := r.renderPathWithField(obj.Path)
+ if typeNameValue.Type() != astjson.TypeString {
+ r.marshalBuf = typeNameValue.MarshalTo(r.marshalBuf[:0])
+ r.addErrorWithCode(fmt.Sprintf("Invalid __typename value %s for field '%s'; expected one of [%s].", string(r.marshalBuf), fieldPath, r.renderPossibleTypeNames(possibleTypeNames)), obj.Path, errorCodeInvalidGraphqlResponse)
+ return false
+ }
+ typeName := typeNameValue.GetStringBytes()
+ for i := range possibleTypeNames {
+ if bytes.Equal(typeName, possibleTypeNames[i]) {
+ return true
+ }
+ }
+ r.addErrorWithCode(fmt.Sprintf("Invalid __typename '%s' for field '%s'; expected one of [%s].", string(typeName), fieldPath, r.renderPossibleTypeNames(possibleTypeNames)), obj.Path, errorCodeInvalidGraphqlResponse)
+ return false
+}
+
+func (r *Resolvable) renderPossibleTypeNames(typeNames [][]byte) string {
+ values := make([]string, 0, len(typeNames))
+ for i := range typeNames {
+ values = append(values, string(typeNames[i]))
+ }
+ return strings.Join(values, ", ")
+}
+
+func (r *Resolvable) renderPathWithField(fieldPath []string) string {
+ r.pushNodePathElement(fieldPath)
+ path := r.renderFieldPath()
r.popNodePathElement(fieldPath)
+ return path
}
diff --git a/tmp/agent-patch-flux-pr-891.1-of-1.2026-02-28__13-38-10__gpt-5-4/app/v2/pkg/engine/resolve/resolvable_test.go b/app/v2/pkg/engine/resolve/resolvable_test.go
index 24c1e22..fce29a7 100644
--- a/tmp/agent-patch-flux-pr-891.1-of-1.2026-02-28__13-38-10__gpt-5-4/app/v2/pkg/engine/resolve/resolvable_test.go
+++ b/app/v2/pkg/engine/resolve/resolvable_test.go
@@ -155,6 +155,96 @@ func TestResolvable_ResolveWithTypeMismatch(t *testing.T) {
assert.Equal(t, `{"errors":[{"message":"String cannot represent non-string value: \"true\"","path":["topProducts",0,"reviews",0,"author","name"]}],"data":{"topProducts":[{"name":"Table","stock":8,"reviews":[{"body":"Love Table!","author":null},{"body":"Prefer other Table.","author":{"name":"user-2"}}]},{"name":"Couch","stock":2,"reviews":[{"body":"Couch Too expensive.","author":{"name":"user-1"}}]},{"name":"Chair","stock":5,"reviews":[{"body":"Chair Could be better.","author":{"name":"user-2"}}]}]}}`, out.String())
}
+func TestResolvable_ResolveWithInvalidTypenameOnObject(t *testing.T) {
+ topProducts := `{"topProducts":[{"__typename":"Product","reviews":[{"author":{"__typename":"Admin","name":"user-1"}}]}]}`
+ res := NewResolvable()
+ ctx := &Context{}
+ err := res.Init(ctx, []byte(topProducts), ast.OperationTypeQuery)
+ assert.NoError(t, err)
+
+ object := &Object{
+ Fields: []*Field{
+ {
+ Name: []byte("topProducts"),
+ Value: &Array{
+ Path: []string{"topProducts"},
+ Item: &Object{
+ Fields: []*Field{
+ {
+ Name: []byte("reviews"),
+ Value: &Array{
+ Path: []string{"reviews"},
+ Item: &Object{
+ Fields: []*Field{
+ {
+ Name: []byte("author"),
+ Value: &Object{
+ Path: []string{"author"},
+ Nullable: true,
+ Fields: []*Field{
+ {
+ Name: []byte("name"),
+ Value: &String{
+ Path: []string{"name"},
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ }
+ SetObjectPossibleTypeNames(object.Fields[0].Value.(*Array).Item.(*Object), [][]byte{[]byte("Product")})
+ SetObjectPossibleTypeNames(object.Fields[0].Value.(*Array).Item.(*Object).Fields[0].Value.(*Array).Item.(*Object).Fields[0].Value.(*Object), [][]byte{[]byte("User")})
+
+ out := &bytes.Buffer{}
+ err = res.Resolve(context.Background(), object, nil, out)
+ assert.NoError(t, err)
+ assert.Equal(t, `{"errors":[{"message":"Invalid __typename 'Admin' for field 'Query.topProducts.reviews.author'; expected one of [User].","path":["topProducts",0,"reviews",0,"author"],"extensions":{"code":"INVALID_GRAPHQL_RESPONSE"}}],"data":{"topProducts":[{"reviews":[{"author":null}]}]}}`, out.String())
+}
+
+func TestResolvable_ResolveWithInvalidTypenameOnAbstractObject(t *testing.T) {
+ data := `{"node":{"__typename":"Comment","name":"Ada"}}`
+ res := NewResolvable()
+ ctx := &Context{}
+ err := res.Init(ctx, []byte(data), ast.OperationTypeQuery)
+ assert.NoError(t, err)
+
+ object := &Object{
+ Fields: []*Field{
+ {
+ Name: []byte("node"),
+ Value: &Object{
+ Path: []string{"node"},
+ Nullable: true,
+ Fields: []*Field{
+ {
+ Name: []byte("name"),
+ Value: &String{
+ Path: []string{"name"},
+ Nullable: true,
+ },
+ },
+ },
+ },
+ },
+ },
+ }
+ SetObjectPossibleTypeNames(object.Fields[0].Value.(*Object), [][]byte{[]byte("User"), []byte("Admin")})
+
+ out := &bytes.Buffer{}
+ err = res.Resolve(context.Background(), object, nil, out)
+ assert.NoError(t, err)
+ assert.Equal(t, `{"errors":[{"message":"Invalid __typename 'Comment' for field 'Query.node'; expected one of [User, Admin].","path":["node"],"extensions":{"code":"INVALID_GRAPHQL_RESPONSE"}}],"data":{"node":null}}`, out.String())
+}
+
func TestResolvable_ResolveWithErrorBubbleUp(t *testing.T) {
topProducts := `{"topProducts":[{"name":"Table","__typename":"Product","upc":"1","reviews":[{"body":"Love Table!","author":{"__typename":"User","id":"1"}},{"body":"Prefer other Table.","author":{"__typename":"User","id":"2","name":"user-2"}}],"stock":8},{"name":"Couch","__typename":"Product","upc":"2","reviews":[{"body":"Couch Too expensive.","author":{"__typename":"User","id":"1","name":"user-1"}}],"stock":2},{"name":"Chair","__typename":"Product","upc":"3","reviews":[{"body":"Chair Could be better.","author":{"__typename":"User","id":"2","name":"user-2"}}],"stock":5}]}`
res := NewResolvable()