STET

agent_patch

runs/2026-02-28__13-38-10__gpt-5-3-codex/flux-pr-891/flux-pr-891.1-of-1.2026-02-28__13-38-10__gpt-5-3-codex/agent-logs/agent.patch

13670 bytes

Back to adjudication
diff --git a/tmp/agent-patch-flux-pr-891.1-of-1.2026-02-28__13-38-10__gpt-5-3-codex/app/v2/pkg/engine/plan/visitor.go b/app/v2/pkg/engine/plan/visitor.go
index 9ba1abb..3ab7008 100644
--- a/tmp/agent-patch-flux-pr-891.1-of-1.2026-02-28__13-38-10__gpt-5-3-codex/app/v2/pkg/engine/plan/visitor.go
+++ b/app/v2/pkg/engine/plan/visitor.go
@@ -658,11 +658,13 @@ func (v *Visitor) resolveFieldValue(fieldRef, typeRef int, nullable bool, path [
 				UnescapeResponseJson: unescapeResponseJson,
 			}
 		case ast.NodeKindObjectTypeDefinition, ast.NodeKindInterfaceTypeDefinition, ast.NodeKindUnionTypeDefinition:
+			possibleTypeNames := v.resolvePossibleTypeNames(typeDefinitionNode, typeName)
 			object := &resolve.Object{
 				Nullable: nullable,
 				Path:     path,
 				Fields:   []*resolve.Field{},
 			}
+			resolve.SetObjectTypeNameInfo(object, typeName, possibleTypeNames)
 			v.objects = append(v.objects, object)
 			v.Walker.DefferOnEnterField(func() {
 				v.currentFields = append(v.currentFields, objectFields{
@@ -679,6 +681,35 @@ func (v *Visitor) resolveFieldValue(fieldRef, typeRef int, nullable bool, path [
 	}
 }
 
+func (v *Visitor) resolvePossibleTypeNames(typeDefinitionNode ast.Node, typeName string) [][]byte {
+	switch typeDefinitionNode.Kind {
+	case ast.NodeKindObjectTypeDefinition:
+		return [][]byte{v.Config.Types.RenameTypeNameOnMatchBytes([]byte(typeName))}
+	case ast.NodeKindInterfaceTypeDefinition:
+		possibleTypes, ok := v.Definition.InterfaceTypeDefinitionImplementedByObjectWithNames(typeDefinitionNode.Ref)
+		if !ok {
+			return nil
+		}
+		out := make([][]byte, 0, len(possibleTypes))
+		for i := range possibleTypes {
+			out = append(out, v.Config.Types.RenameTypeNameOnMatchBytes([]byte(possibleTypes[i])))
+		}
+		return out
+	case ast.NodeKindUnionTypeDefinition:
+		memberTypes, ok := v.Definition.UnionTypeDefinitionMemberTypeNames(typeDefinitionNode.Ref)
+		if !ok {
+			return nil
+		}
+		out := make([][]byte, 0, len(memberTypes))
+		for i := range memberTypes {
+			out = append(out, v.Config.Types.RenameTypeNameOnMatchBytes([]byte(memberTypes[i])))
+		}
+		return out
+	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-3-codex/app/v2/pkg/engine/resolve/node_object.go b/app/v2/pkg/engine/resolve/node_object.go
index 214d237..5694368 100644
--- a/tmp/agent-patch-flux-pr-891.1-of-1.2026-02-28__13-38-10__gpt-5-3-codex/app/v2/pkg/engine/resolve/node_object.go
+++ b/app/v2/pkg/engine/resolve/node_object.go
@@ -18,12 +18,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 info, ok := getObjectTypeNameInfo(o); ok {
+		SetObjectTypeNameInfo(out, info.typeName, info.possibleTypeNames)
+	}
+	return out
 }
 
 func (_ *Object) NodeKind() NodeKind {
diff --git a/app/v2/pkg/engine/resolve/object_typename_info.go b/app/v2/pkg/engine/resolve/object_typename_info.go
new file mode 100644
index 0000000..1ceb15f
--- /dev/null
+++ b/app/v2/pkg/engine/resolve/object_typename_info.go
@@ -0,0 +1,43 @@
+package resolve
+
+import "sync"
+
+type objectTypeNameInfo struct {
+	typeName          string
+	possibleTypeNames [][]byte
+}
+
+var objectTypeNameInfos sync.Map
+
+func SetObjectTypeNameInfo(object *Object, typeName string, possibleTypeNames [][]byte) {
+	if object == nil || len(possibleTypeNames) == 0 {
+		return
+	}
+
+	names := make([][]byte, 0, len(possibleTypeNames))
+	for i := range possibleTypeNames {
+		if len(possibleTypeNames[i]) == 0 {
+			continue
+		}
+		nameCopy := make([]byte, len(possibleTypeNames[i]))
+		copy(nameCopy, possibleTypeNames[i])
+		names = append(names, nameCopy)
+	}
+	if len(names) == 0 {
+		return
+	}
+
+	objectTypeNameInfos.Store(object, objectTypeNameInfo{
+		typeName:          typeName,
+		possibleTypeNames: names,
+	})
+}
+
+func getObjectTypeNameInfo(object *Object) (info objectTypeNameInfo, ok bool) {
+	raw, ok := objectTypeNameInfos.Load(object)
+	if !ok {
+		return objectTypeNameInfo{}, false
+	}
+	info, ok = raw.(objectTypeNameInfo)
+	return info, ok
+}
diff --git a/tmp/agent-patch-flux-pr-891.1-of-1.2026-02-28__13-38-10__gpt-5-3-codex/app/v2/pkg/engine/resolve/resolvable.go b/app/v2/pkg/engine/resolve/resolvable.go
index 4512664..0563e25 100644
--- a/tmp/agent-patch-flux-pr-891.1-of-1.2026-02-28__13-38-10__gpt-5-3-codex/app/v2/pkg/engine/resolve/resolvable.go
+++ b/app/v2/pkg/engine/resolve/resolvable.go
@@ -6,6 +6,8 @@ import (
 	goerrors "errors"
 	"fmt"
 	"io"
+	"slices"
+	"strings"
 
 	"github.com/cespare/xxhash/v2"
 	"github.com/goccy/go-json"
@@ -62,6 +64,8 @@ var (
 	parsers = &astjson.ParserPool{}
 )
 
+const invalidSubgraphTypenameExtensionCode = "INVALID_SUBGRAPH_TYPENAME"
+
 func (r *Resolvable) parseJSON(data []byte) (*astjson.Value, error) {
 	parser := parsers.Get()
 	r.parsers = append(r.parsers, parser)
@@ -462,6 +466,13 @@ func (r *Resolvable) walkObject(obj *Object, parent *astjson.Value) bool {
 		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()
+	}
 	if r.print && !isRoot {
 		r.printBytes(lBrace)
 		r.ctx.Stats.ResolvedObjects++
@@ -618,7 +629,7 @@ func (r *Resolvable) addRejectFieldError(reason string, ds DataSourceInfo, field
 	}
 	r.ctx.appendSubgraphError(goerrors.Join(errors.New(errorMessage),
 		NewSubgraphError(ds, fieldPath, reason, 0)))
-	fastjsonext.AppendErrorToArray(r.errors, errorMessage, r.path)
+	r.appendError(errorMessage, r.path, "")
 	r.popNodePathElement(nodePath)
 }
 
@@ -752,7 +763,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 +812,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 +836,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 +860,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 +961,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.appendError(errorMessage, r.path, "")
 	r.popNodePathElement(fieldPath)
 }
 
@@ -976,6 +987,89 @@ func (r *Resolvable) renderFieldPath() string {
 
 func (r *Resolvable) addError(message string, fieldPath []string) {
 	r.pushNodePathElement(fieldPath)
-	fastjsonext.AppendErrorToArray(r.errors, message, r.path)
+	r.appendError(message, r.path, "")
 	r.popNodePathElement(fieldPath)
 }
+
+func (r *Resolvable) validateObjectTypeName(obj *Object, value *astjson.Value) bool {
+	typeInfo, ok := getObjectTypeNameInfo(obj)
+	if !ok || len(typeInfo.possibleTypeNames) == 0 {
+		return false
+	}
+	typeNameValue := value.Get("__typename")
+	if typeNameValue == nil {
+		return false
+	}
+
+	var typeName []byte
+	if typeNameValue.Type() == astjson.TypeString {
+		typeName = typeNameValue.GetStringBytes()
+	}
+
+	if typeName != nil {
+		for i := range typeInfo.possibleTypeNames {
+			if bytes.Equal(typeName, typeInfo.possibleTypeNames[i]) {
+				return false
+			}
+		}
+	}
+
+	typeNameRepr := "<non-string>"
+	if typeName != nil {
+		typeNameRepr = fmt.Sprintf("%q", typeName)
+	} else {
+		r.marshalBuf = typeNameValue.MarshalTo(r.marshalBuf[:0])
+		typeNameRepr = string(r.marshalBuf)
+	}
+	possibleTypes := r.renderPossibleTypeNames(typeInfo.possibleTypeNames)
+	message := fmt.Sprintf(
+		"Invalid __typename %s for GraphQL type '%s' at path '%s'; expected one of [%s].",
+		typeNameRepr,
+		typeInfo.typeName,
+		r.renderFieldPath(),
+		possibleTypes,
+	)
+
+	r.pushNodePathElement([]string{"__typename"})
+	r.appendError(message, r.path, invalidSubgraphTypenameExtensionCode)
+	r.popNodePathElement([]string{"__typename"})
+
+	return true
+}
+
+func (r *Resolvable) renderPossibleTypeNames(typeNames [][]byte) string {
+	if len(typeNames) == 0 {
+		return ""
+	}
+	names := make([]string, 0, len(typeNames))
+	for i := range typeNames {
+		names = append(names, string(typeNames[i]))
+	}
+	slices.Sort(names)
+	return strings.Join(names, ", ")
+}
+
+func (r *Resolvable) appendError(message string, path []fastjsonext.PathElement, extensionCode string) {
+	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 extensionCode != "" {
+		extensionsObject := r.astjsonArena.NewObject()
+		extensionsObject.Set("code", r.astjsonArena.NewString(extensionCode))
+		errorObject.Set("extensions", extensionsObject)
+	}
+
+	astjson.AppendToArray(r.errors, errorObject)
+}
diff --git a/tmp/agent-patch-flux-pr-891.1-of-1.2026-02-28__13-38-10__gpt-5-3-codex/app/v2/pkg/engine/resolve/resolvable_test.go b/app/v2/pkg/engine/resolve/resolvable_test.go
index 24c1e22..897f88f 100644
--- a/tmp/agent-patch-flux-pr-891.1-of-1.2026-02-28__13-38-10__gpt-5-3-codex/app/v2/pkg/engine/resolve/resolvable_test.go
+++ b/app/v2/pkg/engine/resolve/resolvable_test.go
@@ -155,6 +155,44 @@ 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_ResolveWithInvalidTypeName(t *testing.T) {
+	responseData := `{"search":{"__typename":"Starship","name":"R2-D2"}}`
+	res := NewResolvable()
+	ctx := &Context{
+		Variables: nil,
+	}
+	err := res.Init(ctx, []byte(responseData), ast.OperationTypeQuery)
+	assert.NoError(t, err)
+	assert.NotNil(t, res)
+
+	object := &Object{
+		Fields: []*Field{
+			{
+				Name: []byte("search"),
+				Value: &Object{
+					Path:     []string{"search"},
+					Nullable: true,
+					Fields: []*Field{
+						{
+							Name: []byte("name"),
+							Value: &String{
+								Path: []string{"name"},
+							},
+						},
+					},
+				},
+			},
+		},
+	}
+	searchObject := object.Fields[0].Value.(*Object)
+	SetObjectTypeNameInfo(searchObject, "Character", [][]byte{[]byte("Droid"), []byte("Human")})
+
+	out := &bytes.Buffer{}
+	err = res.Resolve(context.Background(), object, nil, out)
+	assert.NoError(t, err)
+	assert.Equal(t, `{"errors":[{"message":"Invalid __typename \"Starship\" for GraphQL type 'Character' at path 'Query.search'; expected one of [Droid, Human].","path":["search","__typename"],"extensions":{"code":"INVALID_SUBGRAPH_TYPENAME"}}],"data":{"search":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()