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
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"},
})