agent_patch
runs/2026-02-28__13-38-10__gpt-5-4/flux-pr-1351/flux-pr-1351.1-of-1.2026-02-28__13-38-10__gpt-5-4/agent-logs/agent.patch
20258 bytes
diff --git a/tmp/agent-patch-flux-pr-1351.1-of-1.2026-02-28__13-38-10__gpt-5-4/app/v2/pkg/engine/resolve/authorization_test.go b/app/v2/pkg/engine/resolve/authorization_test.go
index 263724a..4e6a286 100644
--- a/tmp/agent-patch-flux-pr-1351.1-of-1.2026-02-28__13-38-10__gpt-5-4/app/v2/pkg/engine/resolve/authorization_test.go
+++ b/app/v2/pkg/engine/resolve/authorization_test.go
@@ -213,7 +213,7 @@ func TestAuthorization(t *testing.T) {
assert.Equal(t, int64(0), authorizer.(*testAuthorizer).objectFieldCalls.Load())
var subgraphError *SubgraphError
- require.ErrorAs(t, resolveCtx.subgraphErrors, &subgraphError)
+ require.ErrorAs(t, resolveCtx.SubgraphErrorsForDataSource(DataSourceInfo{Name: "users"}), &subgraphError)
require.Equal(t, "users", subgraphError.DataSourceInfo.Name)
require.Equal(t, "query", subgraphError.Path)
require.Equal(t, "Not allowed to fetch from users Subgraph", subgraphError.Reason)
@@ -244,7 +244,7 @@ func TestAuthorization(t *testing.T) {
assert.Equal(t, int64(0), authorizer.(*testAuthorizer).objectFieldCalls.Load())
var subgraphError *SubgraphError
- require.ErrorAs(t, resolveCtx.subgraphErrors, &subgraphError)
+ require.ErrorAs(t, resolveCtx.SubgraphErrorsForDataSource(DataSourceInfo{Name: "users"}), &subgraphError)
require.Equal(t, "users", subgraphError.DataSourceInfo.Name)
require.Equal(t, "query", subgraphError.Path)
require.Equal(t, "", subgraphError.Reason)
@@ -276,11 +276,11 @@ func TestAuthorization(t *testing.T) {
assert.Equal(t, int64(2), authorizer.(*testAuthorizer).preFetchCalls.Load())
assert.Equal(t, int64(4), authorizer.(*testAuthorizer).objectFieldCalls.Load())
- require.NotEmpty(t, resolveCtx.subgraphErrors)
- require.EqualError(t, resolveCtx.subgraphErrors, "Failed to fetch from Subgraph 'products' at Path: 'query.me.reviews.@.product', Reason: Not allowed to fetch from products Subgraph.")
+ require.NotEmpty(t, resolveCtx.SubgraphErrorsForDataSource(DataSourceInfo{Name: "products"}))
+ require.EqualError(t, resolveCtx.SubgraphErrorsForDataSource(DataSourceInfo{Name: "products"}), "Failed to fetch from Subgraph 'products' at Path: 'query.me.reviews.@.product', Reason: Not allowed to fetch from products Subgraph.")
var subgraphError *SubgraphError
- require.ErrorAs(t, resolveCtx.subgraphErrors, &subgraphError)
+ require.ErrorAs(t, resolveCtx.SubgraphErrorsForDataSource(DataSourceInfo{Name: "products"}), &subgraphError)
require.Equal(t, "products", subgraphError.DataSourceInfo.Name)
require.Equal(t, "query.me.reviews.@.product", subgraphError.Path)
require.Equal(t, "Not allowed to fetch from products Subgraph", subgraphError.Reason)
@@ -311,7 +311,7 @@ func TestAuthorization(t *testing.T) {
assert.Equal(t, int64(4), authorizer.(*testAuthorizer).objectFieldCalls.Load())
var subgraphError *SubgraphError
- require.ErrorAs(t, resolveCtx.subgraphErrors, &subgraphError)
+ require.ErrorAs(t, resolveCtx.SubgraphErrorsForDataSource(DataSourceInfo{Name: "products"}), &subgraphError)
require.Equal(t, "products", subgraphError.DataSourceInfo.Name)
require.Equal(t, "Query.me.reviews.product.data.name", subgraphError.Path)
require.Equal(t, "Not allowed to fetch name on Product", subgraphError.Reason)
@@ -344,7 +344,7 @@ func TestAuthorization(t *testing.T) {
assert.Equal(t, int64(4), authorizer.(*testAuthorizer).objectFieldCalls.Load())
var subgraphError *SubgraphError
- require.ErrorAs(t, resolveCtx.subgraphErrors, &subgraphError)
+ require.ErrorAs(t, resolveCtx.SubgraphErrorsForDataSource(DataSourceInfo{Name: "products"}), &subgraphError)
require.Equal(t, "products", subgraphError.DataSourceInfo.Name)
require.Equal(t, "query.me.reviews.@.product", subgraphError.Path)
require.Equal(t, "Not allowed to fetch from products Subgraph", subgraphError.Reason)
@@ -388,7 +388,7 @@ func TestAuthorization(t *testing.T) {
assert.Equal(t, int64(4), authorizer.(*testAuthorizer).objectFieldCalls.Load())
var subgraphError *SubgraphError
- require.ErrorAs(t, resolveCtx.subgraphErrors, &subgraphError)
+ require.ErrorAs(t, resolveCtx.SubgraphErrorsForDataSource(DataSourceInfo{Name: "reviews"}), &subgraphError)
require.Equal(t, "reviews", subgraphError.DataSourceInfo.Name)
require.Equal(t, "Query.me.reviews.body", subgraphError.Path)
require.Equal(t, "Not allowed to fetch body on Review", subgraphError.Reason)
@@ -417,7 +417,7 @@ func TestAuthorization(t *testing.T) {
assert.Equal(t, int64(4), authorizer.(*testAuthorizer).objectFieldCalls.Load())
var subgraphError *SubgraphError
- require.ErrorAs(t, resolveCtx.subgraphErrors, &subgraphError)
+ require.ErrorAs(t, resolveCtx.SubgraphErrorsForDataSource(DataSourceInfo{Name: "reviews"}), &subgraphError)
require.Equal(t, "reviews", subgraphError.DataSourceInfo.Name)
require.Equal(t, "Query.me.reviews.body", subgraphError.Path)
require.Equal(t, "", subgraphError.Reason)
@@ -448,7 +448,7 @@ func TestAuthorization(t *testing.T) {
assert.Equal(t, int64(4), authorizer.(*testAuthorizer).objectFieldCalls.Load())
var subgraphError *SubgraphError
- require.ErrorAs(t, resolveCtx.subgraphErrors, &subgraphError)
+ require.ErrorAs(t, resolveCtx.SubgraphErrorsForDataSource(DataSourceInfo{Name: "products"}), &subgraphError)
require.Equal(t, "products", subgraphError.DataSourceInfo.Name)
require.Equal(t, "query.me.reviews.@.product", subgraphError.Path)
require.Equal(t, "Not allowed to fetch name on Product", subgraphError.Reason)
@@ -479,7 +479,7 @@ func TestAuthorization(t *testing.T) {
assert.Equal(t, int64(4), authorizer.(*testAuthorizer).objectFieldCalls.Load())
var subgraphError *SubgraphError
- require.ErrorAs(t, resolveCtx.subgraphErrors, &subgraphError)
+ require.ErrorAs(t, resolveCtx.SubgraphErrorsForDataSource(DataSourceInfo{Name: "products"}), &subgraphError)
require.Equal(t, "products", subgraphError.DataSourceInfo.Name)
require.Equal(t, "Query.me.reviews.product.data.name", subgraphError.Path)
require.Equal(t, "Not allowed to fetch name on Product", subgraphError.Reason)
diff --git a/tmp/agent-patch-flux-pr-1351.1-of-1.2026-02-28__13-38-10__gpt-5-4/app/v2/pkg/engine/resolve/context.go b/app/v2/pkg/engine/resolve/context.go
index 65d2d6b..72fb3cd 100644
--- a/tmp/agent-patch-flux-pr-1351.1-of-1.2026-02-28__13-38-10__gpt-5-4/app/v2/pkg/engine/resolve/context.go
+++ b/app/v2/pkg/engine/resolve/context.go
@@ -3,7 +3,6 @@ package resolve
import (
"context"
"encoding/json"
- "errors"
"io"
"net/http"
"time"
@@ -31,7 +30,7 @@ type Context struct {
rateLimiter RateLimiter
fieldRenderer FieldValueRenderer
- subgraphErrors error
+ subgraphErrors *SubgraphErrors
}
type ExecutionOptions struct {
@@ -138,11 +137,31 @@ func (c *Context) SetRateLimiter(limiter RateLimiter) {
}
func (c *Context) SubgraphErrors() error {
+ if c.subgraphErrors == nil {
+ return nil
+ }
return c.subgraphErrors
}
-func (c *Context) appendSubgraphErrors(errs ...error) {
- c.subgraphErrors = errors.Join(c.subgraphErrors, errors.Join(errs...))
+func (c *Context) SubgraphErrorsBySubgraph() map[string]error {
+ if c.subgraphErrors == nil {
+ return nil
+ }
+ return c.subgraphErrors.BySubgraph()
+}
+
+func (c *Context) SubgraphErrorsForDataSource(ds DataSourceInfo) error {
+ if c.subgraphErrors == nil {
+ return nil
+ }
+ return c.subgraphErrors.ForDataSource(ds)
+}
+
+func (c *Context) appendSubgraphErrors(ds DataSourceInfo, errs ...error) {
+ if c.subgraphErrors == nil {
+ c.subgraphErrors = NewSubgraphErrors()
+ }
+ c.subgraphErrors.Add(ds, errs...)
}
type Request struct {
diff --git a/tmp/agent-patch-flux-pr-1351.1-of-1.2026-02-28__13-38-10__gpt-5-4/app/v2/pkg/engine/resolve/errors_test.go b/app/v2/pkg/engine/resolve/errors_test.go
index 3f9daf5..b2d8f14 100644
--- a/tmp/agent-patch-flux-pr-1351.1-of-1.2026-02-28__13-38-10__gpt-5-4/app/v2/pkg/engine/resolve/errors_test.go
+++ b/app/v2/pkg/engine/resolve/errors_test.go
@@ -1,6 +1,7 @@
package resolve
import (
+ "errors"
"testing"
"github.com/stretchr/testify/require"
@@ -59,3 +60,37 @@ func TestRateLimitError(t *testing.T) {
require.EqualError(t, e, "Rate limit rejected for Subgraph 'subgraphName' at Path 'path', Reason: limit.")
})
}
+
+func TestSubgraphErrors(t *testing.T) {
+ t.Run("groups errors by subgraph", func(t *testing.T) {
+ errs := NewSubgraphErrors()
+ errs.Add(DataSourceInfo{Name: "products", ID: "1"},
+ errors.New("transport failed"),
+ NewSubgraphError(DataSourceInfo{Name: "products", ID: "1"}, "query.topProducts", "", 500),
+ )
+ errs.Add(DataSourceInfo{Name: "reviews", ID: "2"},
+ NewSubgraphError(DataSourceInfo{Name: "reviews", ID: "2"}, "query.topProducts.@.reviews", "bad upstream", 500),
+ )
+
+ bySubgraph := errs.BySubgraph()
+ require.Len(t, bySubgraph, 2)
+ require.EqualError(t, bySubgraph["products"], "Failed to fetch from Subgraph 'products' at Path: 'query.topProducts'.\ntransport failed")
+ require.EqualError(t, bySubgraph["reviews"], "Failed to fetch from Subgraph 'reviews' at Path: 'query.topProducts.@.reviews', Reason: bad upstream.")
+
+ require.EqualError(t, errs.ForDataSource(DataSourceInfo{Name: "products", ID: "1"}), "Failed to fetch from Subgraph 'products' at Path: 'query.topProducts'.\ntransport failed")
+ require.EqualError(t, errs, "Failed to fetch from Subgraph 'products' at Path: 'query.topProducts'.\ntransport failed\nFailed to fetch from Subgraph 'reviews' at Path: 'query.topProducts.@.reviews', Reason: bad upstream.")
+ })
+
+ t.Run("keeps combined error ordering deterministic", func(t *testing.T) {
+ first := NewSubgraphErrors()
+ first.Add(DataSourceInfo{Name: "reviews", ID: "2"}, errors.New("reviews error"))
+ first.Add(DataSourceInfo{Name: "products", ID: "1"}, errors.New("products error"))
+
+ second := NewSubgraphErrors()
+ second.Add(DataSourceInfo{Name: "products", ID: "1"}, errors.New("products error"))
+ second.Add(DataSourceInfo{Name: "reviews", ID: "2"}, errors.New("reviews error"))
+
+ require.EqualError(t, first, "products error\nreviews error")
+ require.EqualError(t, second, "products error\nreviews error")
+ })
+}
diff --git a/tmp/agent-patch-flux-pr-1351.1-of-1.2026-02-28__13-38-10__gpt-5-4/app/v2/pkg/engine/resolve/loader.go b/app/v2/pkg/engine/resolve/loader.go
index 7a14d61..e59b01d 100644
--- a/tmp/agent-patch-flux-pr-1351.1-of-1.2026-02-28__13-38-10__gpt-5-4/app/v2/pkg/engine/resolve/loader.go
+++ b/app/v2/pkg/engine/resolve/loader.go
@@ -243,7 +243,7 @@ func (l *Loader) resolveParallel(nodes []*FetchTreeNode) error {
if l.ctx.LoaderHooks != nil && results[i].nestedMergeItems[j].loaderHookContext != nil {
l.ctx.LoaderHooks.OnFinished(results[i].nestedMergeItems[j].loaderHookContext,
results[i].nestedMergeItems[j].ds,
- newResponseInfo(results[i].nestedMergeItems[j], l.ctx.subgraphErrors))
+ newResponseInfo(results[i].nestedMergeItems[j], l.ctx.SubgraphErrorsForDataSource(results[i].nestedMergeItems[j].ds)))
}
if err != nil {
return errors.WithStack(err)
@@ -252,7 +252,7 @@ func (l *Loader) resolveParallel(nodes []*FetchTreeNode) error {
} else {
err = l.mergeResult(nodes[i].Item, results[i], itemsItems[i])
if l.ctx.LoaderHooks != nil {
- l.ctx.LoaderHooks.OnFinished(results[i].loaderHookContext, results[i].ds, newResponseInfo(results[i], l.ctx.subgraphErrors))
+ l.ctx.LoaderHooks.OnFinished(results[i].loaderHookContext, results[i].ds, newResponseInfo(results[i], l.ctx.SubgraphErrorsForDataSource(results[i].ds)))
}
if err != nil {
return errors.WithStack(err)
@@ -289,7 +289,7 @@ func (l *Loader) resolveSingle(item *FetchItem) error {
}
err = l.mergeResult(item, res, items)
if l.ctx.LoaderHooks != nil {
- l.ctx.LoaderHooks.OnFinished(res.loaderHookContext, res.ds, newResponseInfo(res, l.ctx.subgraphErrors))
+ l.ctx.LoaderHooks.OnFinished(res.loaderHookContext, res.ds, newResponseInfo(res, l.ctx.SubgraphErrorsForDataSource(res.ds)))
}
return err
@@ -303,7 +303,7 @@ func (l *Loader) resolveSingle(item *FetchItem) error {
}
err = l.mergeResult(item, res, items)
if l.ctx.LoaderHooks != nil {
- l.ctx.LoaderHooks.OnFinished(res.loaderHookContext, res.ds, newResponseInfo(res, l.ctx.subgraphErrors))
+ l.ctx.LoaderHooks.OnFinished(res.loaderHookContext, res.ds, newResponseInfo(res, l.ctx.SubgraphErrorsForDataSource(res.ds)))
}
return err
case *EntityFetch:
@@ -316,7 +316,7 @@ func (l *Loader) resolveSingle(item *FetchItem) error {
}
err = l.mergeResult(item, res, items)
if l.ctx.LoaderHooks != nil {
- l.ctx.LoaderHooks.OnFinished(res.loaderHookContext, res.ds, newResponseInfo(res, l.ctx.subgraphErrors))
+ l.ctx.LoaderHooks.OnFinished(res.loaderHookContext, res.ds, newResponseInfo(res, l.ctx.SubgraphErrorsForDataSource(res.ds)))
}
return err
case *ParallelListItemFetch:
@@ -349,7 +349,7 @@ func (l *Loader) resolveSingle(item *FetchItem) error {
for i := range results {
err = l.mergeResult(item, results[i], items[i:i+1])
if l.ctx.LoaderHooks != nil {
- l.ctx.LoaderHooks.OnFinished(results[i].loaderHookContext, results[i].ds, newResponseInfo(results[i], l.ctx.subgraphErrors))
+ l.ctx.LoaderHooks.OnFinished(results[i].loaderHookContext, results[i].ds, newResponseInfo(results[i], l.ctx.SubgraphErrorsForDataSource(results[i].ds)))
}
if err != nil {
return errors.WithStack(err)
@@ -740,7 +740,7 @@ func (l *Loader) appendSubgraphError(res *result, fetchItem *FetchItem, value *a
subgraphError.AppendDownstreamError(&gErr)
}
- l.ctx.appendSubgraphErrors(res.err, subgraphError)
+ l.ctx.appendSubgraphErrors(res.ds, res.err, subgraphError)
return nil
}
@@ -1088,7 +1088,7 @@ func (l *Loader) renderErrorsFailedDeps(fetchItem *FetchItem, res *result) error
}
func (l *Loader) renderErrorsFailedToFetch(fetchItem *FetchItem, res *result, reason string) error {
- l.ctx.appendSubgraphErrors(res.err, NewSubgraphError(res.ds, fetchItem.ResponsePath, reason, res.statusCode))
+ l.ctx.appendSubgraphErrors(res.ds, res.err, NewSubgraphError(res.ds, fetchItem.ResponsePath, reason, res.statusCode))
errorObject, err := astjson.ParseWithoutCache(l.renderSubgraphBaseError(res.ds, fetchItem.ResponsePath, reason))
if err != nil {
return err
@@ -1104,7 +1104,7 @@ func (l *Loader) renderErrorsStatusFallback(fetchItem *FetchItem, res *result, s
reason += fmt.Sprintf(": %s", statusText)
}
- l.ctx.appendSubgraphErrors(res.err, NewSubgraphError(res.ds, fetchItem.ResponsePath, reason, res.statusCode))
+ l.ctx.appendSubgraphErrors(res.ds, res.err, NewSubgraphError(res.ds, fetchItem.ResponsePath, reason, res.statusCode))
errorObject, err := astjson.ParseWithoutCache(fmt.Sprintf(`{"message":"%s"}`, reason))
if err != nil {
@@ -1133,7 +1133,7 @@ func (l *Loader) renderSubgraphBaseError(ds DataSourceInfo, path, reason string)
func (l *Loader) renderAuthorizationRejectedErrors(fetchItem *FetchItem, res *result) error {
for i := range res.authorizationRejectedReasons {
- l.ctx.appendSubgraphErrors(res.err, NewSubgraphError(res.ds, fetchItem.ResponsePath, res.authorizationRejectedReasons[i], res.statusCode))
+ l.ctx.appendSubgraphErrors(res.ds, res.err, NewSubgraphError(res.ds, fetchItem.ResponsePath, res.authorizationRejectedReasons[i], res.statusCode))
}
pathPart := l.renderAtPathErrorPart(fetchItem.ResponsePath)
extensionErrorCode := fmt.Sprintf(`"extensions":{"code":"%s"}`, errorcodes.UnauthorizedFieldOrType)
@@ -1174,7 +1174,7 @@ func (l *Loader) renderAuthorizationRejectedErrors(fetchItem *FetchItem, res *re
}
func (l *Loader) renderRateLimitRejectedErrors(fetchItem *FetchItem, res *result) error {
- l.ctx.appendSubgraphErrors(res.err, NewRateLimitError(res.ds.Name, fetchItem.ResponsePath, res.rateLimitRejectedReason))
+ l.ctx.appendSubgraphErrors(res.ds, res.err, NewRateLimitError(res.ds.Name, fetchItem.ResponsePath, res.rateLimitRejectedReason))
pathPart := l.renderAtPathErrorPart(fetchItem.ResponsePath)
var (
err error
diff --git a/tmp/agent-patch-flux-pr-1351.1-of-1.2026-02-28__13-38-10__gpt-5-4/app/v2/pkg/engine/resolve/resolvable.go b/app/v2/pkg/engine/resolve/resolvable.go
index 5219c91..13df1db 100644
--- a/tmp/agent-patch-flux-pr-1351.1-of-1.2026-02-28__13-38-10__gpt-5-4/app/v2/pkg/engine/resolve/resolvable.go
+++ b/app/v2/pkg/engine/resolve/resolvable.go
@@ -758,7 +758,7 @@ func (r *Resolvable) addRejectFieldError(reason string, ds DataSourceInfo, field
} else {
errorMessage = fmt.Sprintf("Unauthorized to load field '%s', Reason: %s.", fieldPath, reason)
}
- r.ctx.appendSubgraphErrors(errors.New(errorMessage),
+ r.ctx.appendSubgraphErrors(ds, errors.New(errorMessage),
NewSubgraphError(ds, fieldPath, reason, 0))
fastjsonext.AppendErrorWithExtensionsCodeToArray(r.astjsonArena, r.errors, errorMessage, errorcodes.UnauthorizedFieldOrType, r.path)
r.popNodePathElement(nodePath)
diff --git a/app/v2/pkg/engine/resolve/subgraph_errors.go b/app/v2/pkg/engine/resolve/subgraph_errors.go
new file mode 100644
index 0000000..c5d6f82
--- /dev/null
+++ b/app/v2/pkg/engine/resolve/subgraph_errors.go
@@ -0,0 +1,153 @@
+package resolve
+
+import (
+ "errors"
+ "slices"
+ "sort"
+ "sync"
+)
+
+type subgraphErrorKey struct {
+ ID string
+ Name string
+}
+
+func (k subgraphErrorKey) mapKey() string {
+ if k.Name != "" {
+ return k.Name
+ }
+ if k.ID != "" {
+ return k.ID
+ }
+ return ""
+}
+
+type SubgraphErrors struct {
+ mu sync.RWMutex
+ byKey map[subgraphErrorKey][]error
+}
+
+func NewSubgraphErrors() *SubgraphErrors {
+ return &SubgraphErrors{
+ byKey: make(map[subgraphErrorKey][]error),
+ }
+}
+
+func (s *SubgraphErrors) Add(ds DataSourceInfo, errs ...error) {
+ if len(errs) == 0 {
+ return
+ }
+
+ key := subgraphErrorKey{ID: ds.ID, Name: ds.Name}
+
+ s.mu.Lock()
+ defer s.mu.Unlock()
+
+ for _, err := range errs {
+ if err == nil {
+ continue
+ }
+ s.byKey[key] = append(s.byKey[key], err)
+ }
+}
+
+func (s *SubgraphErrors) ForDataSource(ds DataSourceInfo) error {
+ s.mu.RLock()
+ defer s.mu.RUnlock()
+
+ if err := joinErrorsDeterministic(s.byKey[subgraphErrorKey{ID: ds.ID, Name: ds.Name}]); err != nil {
+ return err
+ }
+
+ if ds.ID == "" && ds.Name != "" {
+ matching := make([]error, 0)
+ for key, errs := range s.byKey {
+ if key.Name == ds.Name {
+ if err := joinErrorsDeterministic(errs); err != nil {
+ matching = append(matching, err)
+ }
+ }
+ }
+ return joinErrorsDeterministic(matching)
+ }
+
+ return nil
+}
+
+func (s *SubgraphErrors) BySubgraph() map[string]error {
+ s.mu.RLock()
+ defer s.mu.RUnlock()
+
+ out := make(map[string]error, len(s.byKey))
+ for key, errs := range s.byKey {
+ out[key.mapKey()] = joinErrorsDeterministic(errs)
+ }
+ return out
+}
+
+func (s *SubgraphErrors) Error() string {
+ err := joinErrorsDeterministic(s.unwrap())
+ if err == nil {
+ return ""
+ }
+ return err.Error()
+}
+
+func (s *SubgraphErrors) Unwrap() []error {
+ return s.unwrap()
+}
+
+func (s *SubgraphErrors) unwrap() []error {
+ s.mu.RLock()
+ defer s.mu.RUnlock()
+
+ keys := make([]subgraphErrorKey, 0, len(s.byKey))
+ for key := range s.byKey {
+ keys = append(keys, key)
+ }
+
+ sort.Slice(keys, func(i, j int) bool {
+ if keys[i].Name != keys[j].Name {
+ return keys[i].Name < keys[j].Name
+ }
+ return keys[i].ID < keys[j].ID
+ })
+
+ out := make([]error, 0, len(keys))
+ for _, key := range keys {
+ if err := joinErrorsDeterministic(s.byKey[key]); err != nil {
+ out = append(out, err)
+ }
+ }
+ return out
+}
+
+func joinErrorsDeterministic(errs []error) error {
+ filtered := make([]error, 0, len(errs))
+ for _, err := range errs {
+ if err != nil {
+ filtered = append(filtered, err)
+ }
+ }
+ if len(filtered) == 0 {
+ return nil
+ }
+ if len(filtered) == 1 {
+ return filtered[0]
+ }
+
+ slices.SortStableFunc(filtered, func(a, b error) int {
+ ae := a.Error()
+ be := b.Error()
+ switch {
+ case ae < be:
+ return -1
+ case ae > be:
+ return 1
+ default:
+ return 0
+ }
+ })
+
+ return errors.Join(filtered...)
+}