STET

agent_patch

runs/2026-02-21__03-36-16__gpt-5-1-codex-mini/flux-pr-891/flux-pr-891.1-of-1.2026-03-05__17-48-26__gpt-5-1-codex-mini/agent-logs/agent.patch

14984 bytes

Back to adjudication
diff --git a/tmp/agent-patch-flux-pr-891.1-of-1.2026-03-05__17-48-26__gpt-5-1-codex-mini/app/v2/pkg/engine/plan/visitor.go b/app/v2/pkg/engine/plan/visitor.go
index 9ba1abb..85baa84 100644
--- a/tmp/agent-patch-flux-pr-891.1-of-1.2026-03-05__17-48-26__gpt-5-1-codex-mini/app/v2/pkg/engine/plan/visitor.go
+++ b/app/v2/pkg/engine/plan/visitor.go
@@ -659,9 +659,10 @@ func (v *Visitor) resolveFieldValue(fieldRef, typeRef int, nullable bool, path [
 			}
 		case ast.NodeKindObjectTypeDefinition, ast.NodeKindInterfaceTypeDefinition, ast.NodeKindUnionTypeDefinition:
 			object := &resolve.Object{
-				Nullable: nullable,
-				Path:     path,
-				Fields:   []*resolve.Field{},
+				Nullable:          nullable,
+				Path:              path,
+				Fields:            []*resolve.Field{},
+				PossibleTypeNames: v.resolvePossibleTypeNames(typeDefinitionNode),
 			}
 			v.objects = append(v.objects, object)
 			v.Walker.DefferOnEnterField(func() {
@@ -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.Definition.ObjectTypeDefinitionNameBytes(typeDefinitionNode.Ref)}
+	case ast.NodeKindInterfaceTypeDefinition:
+		typeNames, ok := v.Definition.InterfaceTypeDefinitionImplementedByObjectWithNames(typeDefinitionNode.Ref)
+		if !ok {
+			return nil
+		}
+		return toByteSlices(typeNames)
+	case ast.NodeKindUnionTypeDefinition:
+		typeNames, ok := v.Definition.UnionTypeDefinitionMemberTypeNames(typeDefinitionNode.Ref)
+		if !ok {
+			return nil
+		}
+		return toByteSlices(typeNames)
+	default:
+		return nil
+	}
+}
+
+func toByteSlices(names []string) [][]byte {
+	out := make([][]byte, len(names))
+	for i, name := range names {
+		out[i] = []byte(name)
+	}
+	return out
+}
+
 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-03-05__17-48-26__gpt-5-1-codex-mini/app/v2/pkg/engine/resolve/node_object.go b/app/v2/pkg/engine/resolve/node_object.go
index 214d237..307fa60 100644
--- a/tmp/agent-patch-flux-pr-891.1-of-1.2026-03-05__17-48-26__gpt-5-1-codex-mini/app/v2/pkg/engine/resolve/node_object.go
+++ b/app/v2/pkg/engine/resolve/node_object.go
@@ -6,11 +6,12 @@ import (
 )
 
 type Object struct {
-	Nullable bool
-	Path     []string
-	Fields   []*Field
-	Fetches  []Fetch
-	Fetch    Fetch
+	Nullable          bool
+	Path              []string
+	Fields            []*Field
+	Fetches           []Fetch
+	Fetch             Fetch
+	PossibleTypeNames [][]byte
 }
 
 func (o *Object) Copy() Node {
@@ -18,11 +19,16 @@ func (o *Object) Copy() Node {
 	for i, f := range o.Fields {
 		fields[i] = f.Copy()
 	}
+	possibleTypeNames := make([][]byte, len(o.PossibleTypeNames))
+	for i := range o.PossibleTypeNames {
+		possibleTypeNames[i] = append([]byte(nil), o.PossibleTypeNames[i]...)
+	}
 	return &Object{
-		Nullable: o.Nullable,
-		Path:     o.Path,
-		Fields:   fields,
-		Fetches:  o.Fetches,
+		Nullable:          o.Nullable,
+		Path:              o.Path,
+		Fields:            fields,
+		Fetches:           o.Fetches,
+		PossibleTypeNames: possibleTypeNames,
 	}
 }
 
@@ -57,6 +63,15 @@ func (o *Object) Equals(n Node) bool {
 		return false
 	}
 
+	if len(o.PossibleTypeNames) != len(other.PossibleTypeNames) {
+		return false
+	}
+	for i := range o.PossibleTypeNames {
+		if !bytes.Equal(o.PossibleTypeNames[i], other.PossibleTypeNames[i]) {
+			return false
+		}
+	}
+
 	// We ignore fetches in comparison, because we compare shape of the response nodes
 
 	return true
diff --git a/tmp/agent-patch-flux-pr-891.1-of-1.2026-03-05__17-48-26__gpt-5-1-codex-mini/app/v2/pkg/engine/resolve/resolvable.go b/app/v2/pkg/engine/resolve/resolvable.go
index 4512664..387bbe7 100644
--- a/tmp/agent-patch-flux-pr-891.1-of-1.2026-03-05__17-48-26__gpt-5-1-codex-mini/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"
@@ -49,6 +50,8 @@ type Resolvable struct {
 	marshalBuf []byte
 }
 
+const invalidTypenameExtensionCode = "INVALID_TYPENAME"
+
 func NewResolvable() *Resolvable {
 	return &Resolvable{
 		xxh:                xxhash.New(),
@@ -472,6 +475,10 @@ func (r *Resolvable) walkObject(obj *Object, parent *astjson.Value) bool {
 	defer func() {
 		r.typeNames = r.typeNames[:len(r.typeNames)-1]
 	}()
+	if typeName != nil && len(obj.PossibleTypeNames) > 0 && !containsTypeName(obj.PossibleTypeNames, typeName) {
+		r.addInvalidTypenameError(typeName, obj.PossibleTypeNames)
+		return r.err()
+	}
 	for i := range obj.Fields {
 		if obj.Fields[i].SkipDirectiveDefined {
 			if r.skipField(obj.Fields[i].SkipVariableName) {
@@ -618,7 +625,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)
+	fastjsonext.AppendErrorToArray(r.astjsonArena, r.errors, errorMessage, r.path)
 	r.popNodePathElement(nodePath)
 }
 
@@ -752,7 +759,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 +808,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 +832,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 +856,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,10 +957,35 @@ 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)
+	fastjsonext.AppendErrorToArray(r.astjsonArena, r.errors, errorMessage, r.path)
 	r.popNodePathElement(fieldPath)
 }
 
+func (r *Resolvable) addInvalidTypenameError(typeName []byte, allowed [][]byte) {
+	errorMessage := fmt.Sprintf("Subgraph returned invalid __typename '%s' for '%s', expected one of %s.", string(typeName), r.renderFieldPath(), formatAllowedTypeNames(allowed))
+	errorObject := fastjsonext.CreateErrorObjectWithPathWithArena(r.astjsonArena, errorMessage, r.path)
+	extensions := r.astjsonArena.NewObject()
+	extensions.Set("code", r.astjsonArena.NewString(invalidTypenameExtensionCode))
+	errorObject.Set("extensions", extensions)
+	astjson.AppendToArray(r.errors, errorObject)
+}
+
+func formatAllowedTypeNames(names [][]byte) string {
+	if len(names) == 0 {
+		return ""
+	}
+	var sb strings.Builder
+	for i := range names {
+		if i != 0 {
+			sb.WriteString(", ")
+		}
+		sb.WriteByte('\'')
+		sb.Write(names[i])
+		sb.WriteByte('\'')
+	}
+	return sb.String()
+}
+
 func (r *Resolvable) renderFieldPath() string {
 	buf := pool.BytesBuffer.Get()
 	defer pool.BytesBuffer.Put(buf)
@@ -976,6 +1008,15 @@ func (r *Resolvable) renderFieldPath() string {
 
 func (r *Resolvable) addError(message string, fieldPath []string) {
 	r.pushNodePathElement(fieldPath)
-	fastjsonext.AppendErrorToArray(r.errors, message, r.path)
+	fastjsonext.AppendErrorToArray(r.astjsonArena, r.errors, message, r.path)
 	r.popNodePathElement(fieldPath)
 }
+
+func containsTypeName(names [][]byte, name []byte) bool {
+	for i := range names {
+		if bytes.Equal(names[i], name) {
+			return true
+		}
+	}
+	return false
+}
diff --git a/tmp/agent-patch-flux-pr-891.1-of-1.2026-03-05__17-48-26__gpt-5-1-codex-mini/app/v2/pkg/engine/resolve/resolve_test.go b/app/v2/pkg/engine/resolve/resolve_test.go
index be9cf73..282d0da 100644
--- a/tmp/agent-patch-flux-pr-891.1-of-1.2026-03-05__17-48-26__gpt-5-1-codex-mini/app/v2/pkg/engine/resolve/resolve_test.go
+++ b/app/v2/pkg/engine/resolve/resolve_test.go
@@ -1940,6 +1940,42 @@ func TestResolver_ResolveGraphQLResponse(t *testing.T) {
 				},
 			}, `{"data":{"user":{"id":1,"name":"Jannik","__typename":"namespaced_User","aliased":"namespaced_User","rewritten":"namespaced_User"}}}`
 	}))
+	t.Run("invalid __typename", testFn(func(t *testing.T, ctrl *gomock.Controller) (node *GraphQLResponse, ctx Context, expectedOutput string) {
+		return &GraphQLResponse{
+			Fetches: Single(&SingleFetch{
+				FetchConfiguration: FetchConfiguration{
+					DataSource: FakeDataSource(`{"pet":{"__typename":"Dog","name":"Woofie"}}`),
+				},
+			}),
+			Data: &Object{
+				Fields: []*Field{
+					{
+						Name: []byte("pet"),
+						Value: &Object{
+							Path:              []string{"pet"},
+							PossibleTypeNames: [][]byte{[]byte("Cat")},
+							Fields: []*Field{
+								{
+									Name: []byte("__typename"),
+									Value: &String{
+										Path:       []string{"__typename"},
+										Nullable:   false,
+										IsTypeName: true,
+									},
+								},
+								{
+									Name: []byte("name"),
+									Value: &String{
+										Path: []string{"name"},
+									},
+								},
+							},
+						},
+					},
+				},
+			},
+		}, Context{ctx: context.Background()}, `{"errors":[{"message":"Subgraph returned invalid __typename 'Dog' for 'Query.pet', expected one of 'Cat'.","path":["pet"],"extensions":{"code":"INVALID_TYPENAME"}}],"data":null}`
+	}))
 	t.Run("empty graphql response for non-nullable object query field", testFn(func(t *testing.T, ctrl *gomock.Controller) (node *GraphQLResponse, ctx Context, expectedOutput string) {
 		return &GraphQLResponse{
 			Data: &Object{
diff --git a/tmp/agent-patch-flux-pr-891.1-of-1.2026-03-05__17-48-26__gpt-5-1-codex-mini/app/v2/pkg/fastjsonext/fastjsonext.go b/app/v2/pkg/fastjsonext/fastjsonext.go
index a555886..f51300a 100644
--- a/tmp/agent-patch-flux-pr-891.1-of-1.2026-03-05__17-48-26__gpt-5-1-codex-mini/app/v2/pkg/fastjsonext/fastjsonext.go
+++ b/app/v2/pkg/fastjsonext/fastjsonext.go
@@ -1,17 +1,16 @@
 package fastjsonext
 
 import (
-	"fmt"
 	"strconv"
 
 	"github.com/wundergraph/astjson"
 )
 
-func AppendErrorToArray(v *astjson.Value, msg string, path []PathElement) {
+func AppendErrorToArray(arena *astjson.Arena, v *astjson.Value, msg string, path []PathElement) {
 	if v.Type() != astjson.TypeArray {
 		return
 	}
-	errorObject := CreateErrorObjectWithPath(msg, path)
+	errorObject := CreateErrorObjectWithPathWithArena(arena, msg, path)
 	items, _ := v.Array()
 	v.SetArrayItem(len(items), errorObject)
 }
@@ -22,17 +21,27 @@ type PathElement struct {
 }
 
 func CreateErrorObjectWithPath(message string, path []PathElement) *astjson.Value {
-	errorObject := astjson.MustParse(fmt.Sprintf(`{"message":"%s"}`, message))
+	return CreateErrorObjectWithPathWithArena(nil, message, path)
+}
+
+func CreateErrorObjectWithPathWithArena(arena *astjson.Arena, message string, path []PathElement) *astjson.Value {
+	if arena == nil {
+		arena = &astjson.Arena{}
+	}
+	errorObject := arena.NewObject()
+	errorObject.Set("message", arena.NewString(message))
 	if len(path) == 0 {
 		return errorObject
 	}
-	errorPath := astjson.MustParse(`[]`)
+	errorPath := arena.NewArray()
 	for i := range path {
+		var value *astjson.Value
 		if path[i].Name != "" {
-			errorPath.SetArrayItem(i, astjson.MustParse(fmt.Sprintf(`"%s"`, path[i].Name)))
+			value = arena.NewString(path[i].Name)
 		} else {
-			errorPath.SetArrayItem(i, astjson.MustParse(strconv.FormatInt(int64(path[i].Idx), 10)))
+			value = arena.NewNumberString(strconv.FormatInt(int64(path[i].Idx), 10))
 		}
+		errorPath.SetArrayItem(i, value)
 	}
 	errorObject.Set("path", errorPath)
 	return errorObject
diff --git a/tmp/agent-patch-flux-pr-891.1-of-1.2026-03-05__17-48-26__gpt-5-1-codex-mini/app/v2/pkg/fastjsonext/fastjsonext_test.go b/app/v2/pkg/fastjsonext/fastjsonext_test.go
index 3991fbe..aa917d3 100644
--- a/tmp/agent-patch-flux-pr-891.1-of-1.2026-03-05__17-48-26__gpt-5-1-codex-mini/app/v2/pkg/fastjsonext/fastjsonext_test.go
+++ b/app/v2/pkg/fastjsonext/fastjsonext_test.go
@@ -20,29 +20,31 @@ func TestGetArray(t *testing.T) {
 
 func TestAppendErrorWithMessage(t *testing.T) {
 	a := astjson.MustParse(`[]`)
-	AppendErrorToArray(a, "error", nil)
+	arena := &astjson.Arena{}
+	AppendErrorToArray(arena, a, "error", nil)
 	out := a.MarshalTo(nil)
 	require.Equal(t, `[{"message":"error"}]`, string(out))
 
-	AppendErrorToArray(a, "error2", []PathElement{{Name: "a"}})
+	AppendErrorToArray(arena, a, "error2", []PathElement{{Name: "a"}})
 	out = a.MarshalTo(nil)
 	require.Equal(t, `[{"message":"error"},{"message":"error2","path":["a"]}]`, string(out))
 }
 
 func TestCreateErrorObjectWithPath(t *testing.T) {
-	v := CreateErrorObjectWithPath("my error message", []PathElement{
+	arena := &astjson.Arena{}
+	v := CreateErrorObjectWithPath(arena, "my error message", []PathElement{
 		{Name: "a"},
 	})
 	out := v.MarshalTo(nil)
 	require.Equal(t, `{"message":"my error message","path":["a"]}`, string(out))
-	v = CreateErrorObjectWithPath("my error message", []PathElement{
+	v = CreateErrorObjectWithPath(arena, "my error message", []PathElement{
 		{Name: "a"},
 		{Idx: 1},
 		{Name: "b"},
 	})
 	out = v.MarshalTo(nil)
 	require.Equal(t, `{"message":"my error message","path":["a",1,"b"]}`, string(out))
-	v = CreateErrorObjectWithPath("my error message", []PathElement{
+	v = CreateErrorObjectWithPath(arena, "my error message", []PathElement{
 		{Name: "a"},
 		{Name: "b"},
 	})