STET

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

Back to adjudication
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()