STET

agent_patch

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

16007 bytes

Back to adjudication
diff --git a/tmp/agent-patch-flux-pr-891.1-of-1.2026-02-28__13-38-10__gpt-5-1-codex-mini/app/v2/pkg/engine/plan/visitor.go b/app/v2/pkg/engine/plan/visitor.go
index 9ba1abb..2b834ee 100644
--- a/tmp/agent-patch-flux-pr-891.1-of-1.2026-02-28__13-38-10__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.possibleTypeNamesFor(typeDefinitionNode),
 			}
 			v.objects = append(v.objects, object)
 			v.Walker.DefferOnEnterField(func() {
@@ -679,6 +680,57 @@ func (v *Visitor) resolveFieldValue(fieldRef, typeRef int, nullable bool, path [
 	}
 }
 
+func (v *Visitor) possibleTypeNamesFor(typeDefinitionNode ast.Node) [][]byte {
+	switch typeDefinitionNode.Kind {
+	case ast.NodeKindObjectTypeDefinition:
+		name := v.Definition.NodeNameBytes(typeDefinitionNode)
+		if len(name) == 0 {
+			return nil
+		}
+		return [][]byte{name}
+	case ast.NodeKindInterfaceTypeDefinition:
+		typeNames, ok := v.Definition.InterfaceTypeDefinitionImplementedByObjectWithNames(typeDefinitionNode.Ref)
+		if !ok || len(typeNames) == 0 {
+			return nil
+		}
+		return typeNamesToByteSlices(typeNames)
+	case ast.NodeKindUnionTypeDefinition:
+		typeNames, ok := v.Definition.UnionTypeDefinitionMemberTypeNames(typeDefinitionNode.Ref)
+		if !ok || len(typeNames) == 0 {
+			return nil
+		}
+		return typeNamesToByteSlices(typeNames)
+	default:
+		return nil
+	}
+}
+
+func (v *Visitor) rootOperationPossibleTypeNames(operationKind ast.OperationType) [][]byte {
+	var rootType []byte
+	switch operationKind {
+	case ast.OperationTypeQuery:
+		rootType = v.Definition.Index.QueryTypeName
+	case ast.OperationTypeMutation:
+		rootType = v.Definition.Index.MutationTypeName
+	case ast.OperationTypeSubscription:
+		rootType = v.Definition.Index.SubscriptionTypeName
+	default:
+		return nil
+	}
+	if len(rootType) == 0 {
+		return nil
+	}
+	return [][]byte{rootType}
+}
+
+func typeNamesToByteSlices(names []string) [][]byte {
+	out := make([][]byte, len(names))
+	for i := range names {
+		out[i] = []byte(names[i])
+	}
+	return out
+}
+
 func (v *Visitor) resolveFieldExport(fieldRef int) *resolve.FieldExport {
 	if !v.Operation.Fields[fieldRef].HasDirectives {
 		return nil
@@ -764,8 +816,15 @@ func (v *Visitor) EnterOperationDefinition(ref int) {
 
 	v.operationDefinition = ref
 
+	operationKind, _, err := AnalyzePlanKind(v.Operation, v.Definition, v.OperationName)
+	if err != nil {
+		v.Walker.StopWithInternalErr(err)
+		return
+	}
+
 	rootObject := &resolve.Object{
-		Fields: []*resolve.Field{},
+		Fields:            []*resolve.Field{},
+		PossibleTypeNames: v.rootOperationPossibleTypeNames(operationKind),
 	}
 
 	v.objects = append(v.objects, rootObject)
@@ -774,12 +833,6 @@ func (v *Visitor) EnterOperationDefinition(ref int) {
 		popOnField: -1,
 	})
 
-	operationKind, _, err := AnalyzePlanKind(v.Operation, v.Definition, v.OperationName)
-	if err != nil {
-		v.Walker.StopWithInternalErr(err)
-		return
-	}
-
 	graphQLResponse := &resolve.GraphQLResponse{
 		Data: rootObject,
 	}
diff --git a/tmp/agent-patch-flux-pr-891.1-of-1.2026-02-28__13-38-10__gpt-5-1-codex-mini/app/v2/pkg/engine/resolve/node_object.go b/app/v2/pkg/engine/resolve/node_object.go
index 214d237..116bb01 100644
--- a/tmp/agent-patch-flux-pr-891.1-of-1.2026-02-28__13-38-10__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 {
@@ -19,10 +20,11 @@ func (o *Object) Copy() Node {
 		fields[i] = f.Copy()
 	}
 	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: append([][]byte(nil), o.PossibleTypeNames...),
 	}
 }
 
diff --git a/tmp/agent-patch-flux-pr-891.1-of-1.2026-02-28__13-38-10__gpt-5-1-codex-mini/app/v2/pkg/engine/resolve/resolvable.go b/app/v2/pkg/engine/resolve/resolvable.go
index 4512664..d200dca 100644
--- a/tmp/agent-patch-flux-pr-891.1-of-1.2026-02-28__13-38-10__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(),
@@ -468,6 +471,12 @@ func (r *Resolvable) walkObject(obj *Object, parent *astjson.Value) bool {
 	}
 	addComma := false
 	typeName := value.GetStringBytes("__typename")
+	if len(obj.PossibleTypeNames) != 0 && typeName != nil {
+		if !r.isAllowedTypeName(typeName, obj.PossibleTypeNames) {
+			r.addInvalidTypeNameError(typeName, obj.PossibleTypeNames)
+			return r.err()
+		}
+	}
 	r.typeNames = append(r.typeNames, typeName)
 	defer func() {
 		r.typeNames = r.typeNames[:len(r.typeNames)-1]
@@ -618,7 +627,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 +761,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: %q", string(r.marshalBuf)), s.Path)
 		return r.err()
 	}
 	if r.print {
@@ -801,7 +810,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: %q", string(r.marshalBuf)), b.Path)
 		return r.err()
 	}
 	if r.print {
@@ -825,7 +834,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: %q", string(r.marshalBuf)), i.Path)
 		return r.err()
 	}
 	if r.print {
@@ -849,7 +858,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: %q", string(r.marshalBuf)), f.Path)
 		return r.err()
 	}
 	if r.print {
@@ -950,7 +959,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)
+	fastjsonext.AppendErrorToArray(r.astjsonArena, r.errors, errorMessage, r.path)
 	r.popNodePathElement(fieldPath)
 }
 
@@ -976,6 +985,32 @@ 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 (r *Resolvable) isAllowedTypeName(typeName []byte, allowed [][]byte) bool {
+	for i := range allowed {
+		if bytes.Equal(typeName, allowed[i]) {
+			return true
+		}
+	}
+	return false
+}
+
+func (r *Resolvable) addInvalidTypeNameError(typeName []byte, allowed [][]byte) {
+	message := fmt.Sprintf("__typename \"%s\" is not valid for this selection. Supported types: %s.", string(typeName), strings.Join(typeNamesToStrings(allowed), ", "))
+	errorObject := fastjsonext.CreateErrorObjectWithPath(r.astjsonArena, message, r.path)
+	extensionsObj := r.astjsonArena.NewObject()
+	extensionsObj.Set("code", r.astjsonArena.NewString(invalidTypenameExtensionCode))
+	errorObject.Set("extensions", extensionsObj)
+	astjson.AppendToArray(r.errors, errorObject)
+}
+
+func typeNamesToStrings(names [][]byte) []string {
+	out := make([]string, len(names))
+	for i := range names {
+		out[i] = string(names[i])
+	}
+	return out
+}
diff --git a/tmp/agent-patch-flux-pr-891.1-of-1.2026-02-28__13-38-10__gpt-5-1-codex-mini/app/v2/pkg/engine/resolve/resolvable_test.go b/app/v2/pkg/engine/resolve/resolvable_test.go
index 24c1e22..948ad36 100644
--- a/tmp/agent-patch-flux-pr-891.1-of-1.2026-02-28__13-38-10__gpt-5-1-codex-mini/app/v2/pkg/engine/resolve/resolvable_test.go
+++ b/app/v2/pkg/engine/resolve/resolvable_test.go
@@ -155,6 +155,37 @@ 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_InvalidTypeName(t *testing.T) {
+	res := NewResolvable()
+	err := res.Init(&Context{}, []byte(`{"user":{"__typename":"Admin","name":"Alice"}}`), ast.OperationTypeQuery)
+	assert.NoError(t, err)
+	object := &Object{
+		Fields: []*Field{
+			{
+				Name: []byte("user"),
+				Value: &Object{
+					Path:              []string{"user"},
+					Nullable:          true,
+					PossibleTypeNames: [][]byte{[]byte("User")},
+					Fields: []*Field{
+						{
+							Name: []byte("name"),
+							Value: &String{
+								Path: []string{"name"},
+							},
+						},
+					},
+				},
+			},
+		},
+	}
+
+	out := &bytes.Buffer{}
+	err = res.Resolve(context.Background(), object, nil, out)
+	assert.NoError(t, err)
+	assert.Equal(t, `{"errors":[{"message":"__typename \"Admin\" is not valid for this selection. Supported types: User.","path":["user"],"extensions":{"code":"INVALID_TYPENAME"}}],"data":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()
diff --git a/tmp/agent-patch-flux-pr-891.1-of-1.2026-02-28__13-38-10__gpt-5-1-codex-mini/app/v2/pkg/fastjsonext/fastjsonext.go b/app/v2/pkg/fastjsonext/fastjsonext.go
index a555886..ee35dd5 100644
--- a/tmp/agent-patch-flux-pr-891.1-of-1.2026-02-28__13-38-10__gpt-5-1-codex-mini/app/v2/pkg/fastjsonext/fastjsonext.go
+++ b/app/v2/pkg/fastjsonext/fastjsonext.go
@@ -1,17 +1,14 @@
 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 := CreateErrorObjectWithPath(arena, msg, path)
 	items, _ := v.Array()
 	v.SetArrayItem(len(items), errorObject)
 }
@@ -21,18 +18,21 @@ type PathElement struct {
 	Idx  int
 }
 
-func CreateErrorObjectWithPath(message string, path []PathElement) *astjson.Value {
-	errorObject := astjson.MustParse(fmt.Sprintf(`{"message":"%s"}`, message))
+func CreateErrorObjectWithPath(arena *astjson.Arena, message string, path []PathElement) *astjson.Value {
+	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 item *astjson.Value
 		if path[i].Name != "" {
-			errorPath.SetArrayItem(i, astjson.MustParse(fmt.Sprintf(`"%s"`, path[i].Name)))
+			item = arena.NewString(path[i].Name)
 		} else {
-			errorPath.SetArrayItem(i, astjson.MustParse(strconv.FormatInt(int64(path[i].Idx), 10)))
+			item = arena.NewNumberInt(path[i].Idx)
 		}
+		errorPath.SetArrayItem(i, item)
 	}
 	errorObject.Set("path", errorPath)
 	return errorObject
diff --git a/tmp/agent-patch-flux-pr-891.1-of-1.2026-02-28__13-38-10__gpt-5-1-codex-mini/app/v2/pkg/fastjsonext/fastjsonext_test.go b/app/v2/pkg/fastjsonext/fastjsonext_test.go
index 3991fbe..8a4eb75 100644
--- a/tmp/agent-patch-flux-pr-891.1-of-1.2026-02-28__13-38-10__gpt-5-1-codex-mini/app/v2/pkg/fastjsonext/fastjsonext_test.go
+++ b/app/v2/pkg/fastjsonext/fastjsonext_test.go
@@ -19,30 +19,32 @@ func TestGetArray(t *testing.T) {
 }
 
 func TestAppendErrorWithMessage(t *testing.T) {
+	arena := &astjson.Arena{}
 	a := astjson.MustParse(`[]`)
-	AppendErrorToArray(a, "error", nil)
+	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"},
 	})