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
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()