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