From 8c87a23c5e77144d36bd1f4095d2c758f60d70d7 Mon Sep 17 00:00:00 2001 From: David Pine <7679720+IEvangelist@users.noreply.github.com> Date: Thu, 12 Nov 2026 06:32:28 -0600 Subject: [PATCH 1/3] Go codegen: address review feedback from #22057 - FormatError finds joined errors behind fmt.Errorf %w wrappers - DTO callbacks bind the registering client so tokens/handles work - Union parameters with handle members propagate Err() before sending - Forwarded callback tokens reuse the host-issued ID - Builder Err API summary reflects aggregated errors Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../AtsGoCodeGenerator.ApiProjection.cs | 9 +- .../AtsGoCodeGenerator.cs | 19 +- .../Resources/transport.go | 40 ++- .../AtsGoCodeGeneratorTests.cs | 59 ++++ .../Snapshots/AtsGeneratedAspire.verified.go | 58 ++- ...EscapedMetadataGeneratedAspire.verified.go | 4 +- ...ApiExportTests.GoOnlySurface.verified.json | 2 +- ...TwoPassScanningGeneratedAspire.verified.go | 330 +++++++++++++++++- 8 files changed, 490 insertions(+), 31 deletions(-) diff --git a/src/Aspire.Hosting.CodeGeneration.Go/AtsGoCodeGenerator.ApiProjection.cs b/src/Aspire.Hosting.CodeGeneration.Go/AtsGoCodeGenerator.ApiProjection.cs index d9f76498d5c..65bd864b3c8 100644 --- a/src/Aspire.Hosting.CodeGeneration.Go/AtsGoCodeGenerator.ApiProjection.cs +++ b/src/Aspire.Hosting.CodeGeneration.Go/AtsGoCodeGenerator.ApiProjection.cs @@ -1012,7 +1012,14 @@ private List ProjectSynthesizedMembers(ApiExportScope scope, string members.AddRange(ProjectMembers(interfaceName, helpers.Where(helper => helper.Kind == GoDeclarationKind.Method), GoApiErrorModel.Returned)); } - members.AddRange(ProjectMembers(interfaceName, [scope.ErrMethod], GoApiErrorModel.None)); + var errMembers = ProjectMembers(interfaceName, [scope.ErrMethod], GoApiErrorModel.None); + if (string.Equals(typeId, AtsConstants.BuilderTypeId, StringComparison.Ordinal)) + { + // The builder's generated Err overrides the promoted handle Err and reports every client error. + errMembers = errMembers.Select(err => err with { Summary = BuilderErrSummary }).ToList(); + } + + members.AddRange(errMembers); return members; } diff --git a/src/Aspire.Hosting.CodeGeneration.Go/AtsGoCodeGenerator.cs b/src/Aspire.Hosting.CodeGeneration.Go/AtsGoCodeGenerator.cs index 7b1f7a86286..54c34654827 100644 --- a/src/Aspire.Hosting.CodeGeneration.Go/AtsGoCodeGenerator.cs +++ b/src/Aspire.Hosting.CodeGeneration.Go/AtsGoCodeGenerator.cs @@ -2078,6 +2078,15 @@ private void EmitHandleParamErrorChecks(string indent, AtsCapabilityInfo capabil var paramName = GetLocalIdentifier(p.Name); WriteLine($"{indent}if {paramName} != nil {{ if err := {paramName}.Err(); err != nil {{ {errorAction} }} }}"); } + + // Handle-valued unions are plain `any` locals, so the wrapper's Err is read through + // an interface assertion. Without it serializeValue turns an errored wrapper into nil. + foreach (var p in GetUnionParameters(capability)) + { + if (p.Type!.UnionTypes is not { } members || !members.Any(m => m.Category == AtsTypeCategory.Handle)) { continue; } + var paramName = GetLocalIdentifier(p.Name); + WriteLine($"{indent}if e, ok := {paramName}.(interface{{ Err() error }}); ok {{ if err := e.Err(); err != nil {{ {errorAction} }} }}"); + } } private void EmitArgsConstruction( @@ -2186,8 +2195,8 @@ private void EmitCallbackRegistration(string indent, AtsParameterInfo p, string // Emits a strongly-typed DTO callback property into the DTO's ToMap output. Method parameters // pre-register their callback (putting a string id into reqArgs), but DTO properties instead - // embed the shim func directly in the map; the client's marshalTransportValue walks the - // serialized args, finds the func(...any) any value, and registers it. This keeps the public + // embed a clientCallback in the map; the client's marshalTransportValue walks the + // serialized args, binds itself to that shim, and registers it. This keeps the public // DTO field strongly typed (e.g. func(ctx InputsDialogValidationContext)) while reusing the // exact arg-decoding/return/writeback behavior of method-parameter callbacks. private void EmitDtoCallbackToMap(AtsDtoPropertyInfo property, string propertyName) @@ -2195,9 +2204,9 @@ private void EmitDtoCallbackToMap(AtsDtoPropertyInfo property, string propertyNa var p = DtoPropertyToParameterInfo(property); WriteLine($"\tif d.{propertyName} != nil {{"); WriteLine($"\t\tcb := d.{propertyName}"); - WriteLine($"\t\tm[{GoStringLiteral(JsonNamingPolicy.CamelCase.ConvertName(property.Name))}] = func(args ...any) any {{"); - EmitGoCallbackShimBody("\t\t\t", p, clientExpression: null); - WriteLine($"\t\t}}"); + WriteLine($"\t\tm[{GoStringLiteral(JsonNamingPolicy.CamelCase.ConvertName(property.Name))}] = clientCallback(func(c *client, args ...any) any {{"); + EmitGoCallbackShimBody("\t\t\t", p, clientExpression: "c"); + WriteLine($"\t\t}})"); WriteLine($"\t}}"); } diff --git a/src/Aspire.Hosting.CodeGeneration.Go/Resources/transport.go b/src/Aspire.Hosting.CodeGeneration.Go/Resources/transport.go index 3adbfc0a06f..2b67ac9f544 100644 --- a/src/Aspire.Hosting.CodeGeneration.Go/Resources/transport.go +++ b/src/Aspire.Hosting.CodeGeneration.Go/Resources/transport.go @@ -95,6 +95,9 @@ func (e *CapabilityError) Error() string { return e.err.Error() } // } // // Non-SDK errors are returned via err.Error() unchanged. +// +// Callers commonly add context with fmt.Errorf("...: %w", err); the joined +// failures are still found underneath, and the added context is dropped. func FormatError(err error) string { if err == nil { return "" @@ -103,9 +106,9 @@ func FormatError(err error) string { if !errors.As(err, &capErr) { return err.Error() } - if joined, ok := err.(interface{ Unwrap() []error }); ok { + if joined := findJoinedErrors(err); joined != nil { var b strings.Builder - for _, child := range joined.Unwrap() { + for _, child := range joined { formatted := FormatError(child) if formatted == "" { continue @@ -146,6 +149,22 @@ func FormatError(err error) string { return b.String() } +// findJoinedErrors walks single-error wrappers (fmt.Errorf with one %w) down to +// the first error that wraps several, such as the result of errors.Join. +func findJoinedErrors(err error) []error { + for err != nil { + switch u := err.(type) { + case interface{ Unwrap() []error }: + return u.Unwrap() + case interface{ Unwrap() error }: + err = u.Unwrap() + default: + return nil + } + } + return nil +} + // CancellationToken wraps a context.Context to provide cooperative cancellation. // Use NewCancellationToken to create one; call Cancel() to cancel it. // Pass token.Context() to any standard Go library that accepts a context. @@ -154,6 +173,10 @@ type CancellationToken struct { cancel context.CancelFunc cancelOnce sync.Once notify func() + // remoteID is the host-issued ID of a token received in a callback. Forwarding + // the token to another capability reuses it so the host links the nested + // operation to the original cancellation source. + remoteID string } // NewCancellationToken creates a new cancellation token backed by a cancellable @@ -171,6 +194,7 @@ func newCancellationTokenFrom(parent context.Context) *CancellationToken { func newRemoteCancellationToken(c *client, id string) *CancellationToken { token := newCancellationTokenFrom(context.Background()) + token.remoteID = id token.notify = func() { go c.cancelToken(id) } @@ -750,6 +774,11 @@ func (c *client) registerCancellation(token *CancellationToken) string { if token == nil { return "" } + // The host already owns this ID, so there is nothing to register or watch locally: + // Cancel on the remote token notifies the host itself. + if token.remoteID != "" { + return token.remoteID + } id := fmt.Sprintf("ct_%d_%d", c.cancellationCounter.Add(1), time.Now().UnixMilli()) c.cancellationsMu.Lock() c.cancellations[id] = &cancellationRegistration{token: token} @@ -875,6 +904,11 @@ func (c *client) invokeCapability(ctx context.Context, capabilityID string, args return wrapIfHandle(result, c) } +// clientCallback is a callback shim that needs the registering client, for +// example to build remote cancellation tokens. DTO ToMap methods have no client +// in scope, so they emit this type and marshalTransportValue binds the client. +type clientCallback func(c *client, args ...any) any + func (c *client) marshalTransportValue(value any) any { if callback, ok := value.(func(...any) any); ok { return c.registerCallback(callback) @@ -884,6 +918,8 @@ func (c *client) marshalTransportValue(value any) any { switch v := serialized.(type) { case func(...any) any: return c.registerCallback(v) + case clientCallback: + return c.registerCallback(func(args ...any) any { return v(c, args...) }) case map[string]any: result := make(map[string]any, len(v)) for key, nestedValue := range v { diff --git a/tests/Aspire.Hosting.CodeGeneration.Go.Tests/AtsGoCodeGeneratorTests.cs b/tests/Aspire.Hosting.CodeGeneration.Go.Tests/AtsGoCodeGeneratorTests.cs index ae05040fe23..cf8691164fd 100644 --- a/tests/Aspire.Hosting.CodeGeneration.Go.Tests/AtsGoCodeGeneratorTests.cs +++ b/tests/Aspire.Hosting.CodeGeneration.Go.Tests/AtsGoCodeGeneratorTests.cs @@ -1709,6 +1709,65 @@ func TestPropagatedChildErrorIsNotRecordedAgain(t *testing.T) { } } + func TestFormatErrorIncludesJoinedErrorsBehindWrappers(t *testing.T) { + first := &CapabilityError{err: atsError{Code: "FIRST", Message: "first failed", Capability: "addFirst"}} + second := &CapabilityError{err: atsError{Code: "SECOND", Message: "second failed", Capability: "addSecond"}} + + want := FormatError(errors.Join(first, second)) + wrapped := fmt.Errorf("building app: %w", fmt.Errorf("resources: %w", errors.Join(first, second))) + if got := FormatError(wrapped); got != want { + t.Errorf("FormatError() = %q, want %q", got, want) + } + } + + func TestForwardedCallbackTokenReusesHostID(t *testing.T) { + c := newClient("") + remote := newRemoteCancellationToken(c, "ct_host") + if id := c.registerCancellation(remote); id != "ct_host" { + t.Fatalf("registerCancellation() = %q, want the host-issued ID", id) + } + c.cancellationsMu.Lock() + _, registered := c.cancellations["ct_host"] + c.cancellationsMu.Unlock() + if registered { + t.Fatal("a host-owned token ID was registered as a local cancellation") + } + } + + func TestClientCallbackIsBoundToRegisteringClient(t *testing.T) { + c := newClient("") + var got *client + marshaled, _ := c.marshalTransportValue(map[string]any{ + "cb": clientCallback(func(cl *client, args ...any) any { got = cl; return nil }), + }).(map[string]any) + id, _ := marshaled["cb"].(string) + c.callbacksMu.Lock() + callback := c.callbacks[id] + c.callbacksMu.Unlock() + if callback == nil { + t.Fatalf("callback %q was not registered", id) + } + callback() + if got != c { + t.Fatalf("callback received client %p, want %p", got, c) + } + } + + func TestErroredHandleInUnionIsNotSent(t *testing.T) { + c, received := connectChildErrorHost(t, "addTestRedis") + builder := newBuilderOn(c) + + failed := builder.AddTestRedis("failed") + target := newTestRedisResourceFromHandle(&handle{HandleID: "target", TypeID: testRedisTypeID}, c) + target.WithUnionDependency(failed) + if !errors.Is(target.Err(), failed.Err()) { + t.Errorf("target.Err() = %v, want the dependency's error %v", target.Err(), failed.Err()) + } + if got, want := received(), []string{"addTestRedis"}; !slices.Equal(got, want) { + t.Errorf("the AppHost received %v, want %v (withUnionDependency must not be sent)", got, want) + } + } + func TestFailedWithCallOnChildReachesBuilder(t *testing.T) { c, _ := connectChildErrorHost(t, "withConnectionStringDirect") builder := newBuilderOn(c) diff --git a/tests/Aspire.Hosting.CodeGeneration.Go.Tests/Snapshots/AtsGeneratedAspire.verified.go b/tests/Aspire.Hosting.CodeGeneration.Go.Tests/Snapshots/AtsGeneratedAspire.verified.go index 03f17d60837..02120177328 100644 --- a/tests/Aspire.Hosting.CodeGeneration.Go.Tests/Snapshots/AtsGeneratedAspire.verified.go +++ b/tests/Aspire.Hosting.CodeGeneration.Go.Tests/Snapshots/AtsGeneratedAspire.verified.go @@ -803,6 +803,12 @@ func (s *aspire_Hosting_CodeGeneration_Go_TestsTestVaultResource) WithUnionDepen if s.Err() != nil { return s } + if e, ok := dependency.(interface{ Err() error }); ok { + if err := e.Err(); err != nil { + s.setPropagatedErr(err) + return s + } + } switch dependency.(type) { case string, ResourceWithConnectionString: default: @@ -1952,6 +1958,12 @@ func (s *testDatabaseResource) WithUnionDependency(dependency any) TestDatabaseR if s.Err() != nil { return s } + if e, ok := dependency.(interface{ Err() error }); ok { + if err := e.Err(); err != nil { + s.setPropagatedErr(err) + return s + } + } switch dependency.(type) { case string, ResourceWithConnectionString: default: @@ -3332,6 +3344,12 @@ func (s *testRedisResource) WithUnionDependency(dependency any) TestRedisResourc if s.Err() != nil { return s } + if e, ok := dependency.(interface{ Err() error }); ok { + if err := e.Err(); err != nil { + s.setPropagatedErr(err) + return s + } + } switch dependency.(type) { case string, ResourceWithConnectionString: default: @@ -5578,6 +5596,9 @@ func (e *CapabilityError) Error() string { return e.err.Error() } // } // // Non-SDK errors are returned via err.Error() unchanged. +// +// Callers commonly add context with fmt.Errorf("...: %w", err); the joined +// failures are still found underneath, and the added context is dropped. func FormatError(err error) string { if err == nil { return "" @@ -5586,9 +5607,9 @@ func FormatError(err error) string { if !errors.As(err, &capErr) { return err.Error() } - if joined, ok := err.(interface{ Unwrap() []error }); ok { + if joined := findJoinedErrors(err); joined != nil { var b strings.Builder - for _, child := range joined.Unwrap() { + for _, child := range joined { formatted := FormatError(child) if formatted == "" { continue @@ -5629,6 +5650,22 @@ func FormatError(err error) string { return b.String() } +// findJoinedErrors walks single-error wrappers (fmt.Errorf with one %w) down to +// the first error that wraps several, such as the result of errors.Join. +func findJoinedErrors(err error) []error { + for err != nil { + switch u := err.(type) { + case interface{ Unwrap() []error }: + return u.Unwrap() + case interface{ Unwrap() error }: + err = u.Unwrap() + default: + return nil + } + } + return nil +} + // CancellationToken wraps a context.Context to provide cooperative cancellation. // Use NewCancellationToken to create one; call Cancel() to cancel it. // Pass token.Context() to any standard Go library that accepts a context. @@ -5637,6 +5674,10 @@ type CancellationToken struct { cancel context.CancelFunc cancelOnce sync.Once notify func() + // remoteID is the host-issued ID of a token received in a callback. Forwarding + // the token to another capability reuses it so the host links the nested + // operation to the original cancellation source. + remoteID string } // NewCancellationToken creates a new cancellation token backed by a cancellable @@ -5654,6 +5695,7 @@ func newCancellationTokenFrom(parent context.Context) *CancellationToken { func newRemoteCancellationToken(c *client, id string) *CancellationToken { token := newCancellationTokenFrom(context.Background()) + token.remoteID = id token.notify = func() { go c.cancelToken(id) } @@ -6233,6 +6275,11 @@ func (c *client) registerCancellation(token *CancellationToken) string { if token == nil { return "" } + // The host already owns this ID, so there is nothing to register or watch locally: + // Cancel on the remote token notifies the host itself. + if token.remoteID != "" { + return token.remoteID + } id := fmt.Sprintf("ct_%d_%d", c.cancellationCounter.Add(1), time.Now().UnixMilli()) c.cancellationsMu.Lock() c.cancellations[id] = &cancellationRegistration{token: token} @@ -6358,6 +6405,11 @@ func (c *client) invokeCapability(ctx context.Context, capabilityID string, args return wrapIfHandle(result, c) } +// clientCallback is a callback shim that needs the registering client, for +// example to build remote cancellation tokens. DTO ToMap methods have no client +// in scope, so they emit this type and marshalTransportValue binds the client. +type clientCallback func(c *client, args ...any) any + func (c *client) marshalTransportValue(value any) any { if callback, ok := value.(func(...any) any); ok { return c.registerCallback(callback) @@ -6367,6 +6419,8 @@ func (c *client) marshalTransportValue(value any) any { switch v := serialized.(type) { case func(...any) any: return c.registerCallback(v) + case clientCallback: + return c.registerCallback(func(args ...any) any { return v(c, args...) }) case map[string]any: result := make(map[string]any, len(v)) for key, nestedValue := range v { diff --git a/tests/Aspire.Hosting.CodeGeneration.Go.Tests/Snapshots/EscapedMetadataGeneratedAspire.verified.go b/tests/Aspire.Hosting.CodeGeneration.Go.Tests/Snapshots/EscapedMetadataGeneratedAspire.verified.go index a9f81707200..4eadce95204 100644 --- a/tests/Aspire.Hosting.CodeGeneration.Go.Tests/Snapshots/EscapedMetadataGeneratedAspire.verified.go +++ b/tests/Aspire.Hosting.CodeGeneration.Go.Tests/Snapshots/EscapedMetadataGeneratedAspire.verified.go @@ -29,10 +29,10 @@ func (d *EscapedSettings) ToMap() map[string]any { m["back\\slash"] = serializeValue(d.Back_slash) if d.Cal_lback != nil { cb := d.Cal_lback - m["cal\"lback"] = func(args ...any) any { + m["cal\"lback"] = clientCallback(func(c *client, args ...any) any { cb(callbackArg[string](args, 0)) return nil - } + }) } return m } diff --git a/tests/Aspire.Hosting.CodeGeneration.Go.Tests/Snapshots/GoApiExportTests.GoOnlySurface.verified.json b/tests/Aspire.Hosting.CodeGeneration.Go.Tests/Snapshots/GoApiExportTests.GoOnlySurface.verified.json index bfd8845c6dc..b25fea6cb35 100644 --- a/tests/Aspire.Hosting.CodeGeneration.Go.Tests/Snapshots/GoApiExportTests.GoOnlySurface.verified.json +++ b/tests/Aspire.Hosting.CodeGeneration.Go.Tests/Snapshots/GoApiExportTests.GoOnlySurface.verified.json @@ -86,7 +86,7 @@ "declaration": "Err() error", "returnType": "error", "errorModel": "none", - "summary": "Err returns the first error recorded on this wrapper. Returns nil if the wrapper resolved successfully and no chained operation has failed.", + "summary": "Err returns an error combining every failure recorded by this builder or by any handle created from it.", "synthesized": true } ] diff --git a/tests/Aspire.Hosting.CodeGeneration.Go.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.go b/tests/Aspire.Hosting.CodeGeneration.Go.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.go index 0a38705fe2d..2b57c97cdfc 100644 --- a/tests/Aspire.Hosting.CodeGeneration.Go.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.go +++ b/tests/Aspire.Hosting.CodeGeneration.Go.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.go @@ -979,10 +979,10 @@ func (d *InteractionInputsDialogOptions) ToMap() map[string]any { } if d.ValidationCallback != nil { cb := d.ValidationCallback - m["validationCallback"] = func(args ...any) any { - cb(callbackArg[InputsDialogValidationContext](args, 0)) + m["validationCallback"] = clientCallback(func(c *client, args ...any) any { + cb(callbackHandleArg(c, args, 0, newInputsDialogValidationContextFromHandle)) return nil - } + }) } return m } @@ -1015,10 +1015,10 @@ func (d *InteractionProgressOptions) ToMap() map[string]any { } if d.Work != nil { cb := d.Work - m["work"] = func(args ...any) any { - cb(callbackArg[ProgressContext](args, 0)) + m["work"] = clientCallback(func(c *client, args ...any) any { + cb(callbackHandleArg(c, args, 0, newProgressContextFromHandle)) return nil - } + }) } return m } @@ -1281,10 +1281,10 @@ func (d *CommandOptions) ToMap() map[string]any { } if d.ValidateArguments != nil { cb := d.ValidateArguments - m["validateArguments"] = func(args ...any) any { - cb(callbackArg[InputsDialogValidationContext](args, 0)) + m["validateArguments"] = clientCallback(func(c *client, args ...any) any { + cb(callbackHandleArg(c, args, 0, newInputsDialogValidationContextFromHandle)) return nil - } + }) } m["visibility"] = serializeValue(d.Visibility) if d.ConfirmationMessage != nil { @@ -1299,9 +1299,9 @@ func (d *CommandOptions) ToMap() map[string]any { m["isHighlighted"] = serializeValue(d.IsHighlighted) if d.UpdateState != nil { cb := d.UpdateState - m["updateState"] = func(args ...any) any { - return cb(callbackArg[UpdateCommandStateContext](args, 0)) - } + m["updateState"] = clientCallback(func(c *client, args ...any) any { + return cb(callbackHandleArg(c, args, 0, newUpdateCommandStateContextFromHandle)) + }) } if d.Progress != nil { m["progress"] = serializeValue(d.Progress) @@ -1403,9 +1403,9 @@ func (d *HttpCommandExportOptions) ToMap() map[string]any { } if d.PrepareRequest != nil { cb := d.PrepareRequest - m["prepareRequest"] = func(args ...any) any { - return cb(callbackArg[HttpCommandPrepareRequestContext](args, 0)) - } + m["prepareRequest"] = clientCallback(func(c *client, args ...any) any { + return cb(callbackHandleArg(c, args, 0, newHttpCommandPrepareRequestContextFromHandle)) + }) } m["resultMode"] = serializeValue(d.ResultMode) return m @@ -1606,9 +1606,9 @@ func (d *ProcessCommandExportOptions) ToMap() map[string]any { } if d.CreateProcessSpec != nil { cb := d.CreateProcessSpec - m["createProcessSpec"] = func(args ...any) any { - return cb(callbackArg[ExecuteCommandContext](args, 0)) - } + m["createProcessSpec"] = clientCallback(func(c *client, args ...any) any { + return cb(callbackHandleArg(c, args, 0, newExecuteCommandContextFromHandle)) + }) } if d.CommandOptions != nil { m["commandOptions"] = serializeValue(d.CommandOptions) @@ -4274,6 +4274,12 @@ func (s *aspire_Hosting_CodeGeneration_Go_TestsTestVaultResource) WithBuildArg(n if s.Err() != nil { return s } + if e, ok := value.(interface{ Err() error }); ok { + if err := e.Err(); err != nil { + s.setPropagatedErr(err) + return s + } + } switch value.(type) { case string, ParameterResource: default: @@ -5018,6 +5024,12 @@ func (s *aspire_Hosting_CodeGeneration_Go_TestsTestVaultResource) WithEnvironmen if s.Err() != nil { return s } + if e, ok := value.(interface{ Err() error }); ok { + if err := e.Err(); err != nil { + s.setPropagatedErr(err) + return s + } + } switch value.(type) { case string, *ReferenceExpression, EndpointReference, ParameterResource, ExternalServiceResource, ResourceWithConnectionString, ExpressionValue: default: @@ -6060,6 +6072,12 @@ func (s *aspire_Hosting_CodeGeneration_Go_TestsTestVaultResource) WithReference( if s.Err() != nil { return s } + if e, ok := source.(interface{ Err() error }); ok { + if err := e.Err(); err != nil { + s.setPropagatedErr(err) + return s + } + } switch source.(type) { case Resource, EndpointReference, string: default: @@ -6377,6 +6395,12 @@ func (s *aspire_Hosting_CodeGeneration_Go_TestsTestVaultResource) WithUnionDepen if s.Err() != nil { return s } + if e, ok := dependency.(interface{ Err() error }); ok { + if err := e.Err(); err != nil { + s.setPropagatedErr(err) + return s + } + } switch dependency.(type) { case string, ResourceWithConnectionString: default: @@ -6403,6 +6427,12 @@ func (s *aspire_Hosting_CodeGeneration_Go_TestsTestVaultResource) WithUrl(url an if s.Err() != nil { return s } + if e, ok := url.(interface{ Err() error }); ok { + if err := e.Err(); err != nil { + s.setPropagatedErr(err) + return s + } + } switch url.(type) { case string, *ReferenceExpression: default: @@ -8872,6 +8902,12 @@ func (s *cSharpAppResource) WithEnvironment(name string, value any) CSharpAppRes if s.Err() != nil { return s } + if e, ok := value.(interface{ Err() error }); ok { + if err := e.Err(); err != nil { + s.setPropagatedErr(err) + return s + } + } switch value.(type) { case string, *ReferenceExpression, EndpointReference, ParameterResource, ExternalServiceResource, ResourceWithConnectionString, ExpressionValue: default: @@ -9807,6 +9843,12 @@ func (s *cSharpAppResource) WithReference(source any, options ...WithReferenceOp if s.Err() != nil { return s } + if e, ok := source.(interface{ Err() error }); ok { + if err := e.Err(); err != nil { + s.setPropagatedErr(err) + return s + } + } switch source.(type) { case Resource, EndpointReference, string: default: @@ -10141,6 +10183,12 @@ func (s *cSharpAppResource) WithUnionDependency(dependency any) CSharpAppResourc if s.Err() != nil { return s } + if e, ok := dependency.(interface{ Err() error }); ok { + if err := e.Err(); err != nil { + s.setPropagatedErr(err) + return s + } + } switch dependency.(type) { case string, ResourceWithConnectionString: default: @@ -10167,6 +10215,12 @@ func (s *cSharpAppResource) WithUrl(url any, options ...WithUrlOption) CSharpApp if s.Err() != nil { return s } + if e, ok := url.(interface{ Err() error }); ok { + if err := e.Err(); err != nil { + s.setPropagatedErr(err) + return s + } + } switch url.(type) { case string, *ReferenceExpression: default: @@ -10478,6 +10532,11 @@ func (s *commandLineArgsEditor) Add(value any) error { if err := s.Err(); err != nil { return err } + if e, ok := value.(interface{ Err() error }); ok { + if err := e.Err(); err != nil { + return err + } + } switch value.(type) { case string, *ReferenceExpression, EndpointReference, ParameterResource, ResourceWithConnectionString, ExpressionValue: default: @@ -13847,6 +13906,12 @@ func (s *containerRegistryResource) WithUnionDependency(dependency any) Containe if s.Err() != nil { return s } + if e, ok := dependency.(interface{ Err() error }); ok { + if err := e.Err(); err != nil { + s.setPropagatedErr(err) + return s + } + } switch dependency.(type) { case string, ResourceWithConnectionString: default: @@ -13873,6 +13938,12 @@ func (s *containerRegistryResource) WithUrl(url any, options ...WithUrlOption) C if s.Err() != nil { return s } + if e, ok := url.(interface{ Err() error }); ok { + if err := e.Err(); err != nil { + s.setPropagatedErr(err) + return s + } + } switch url.(type) { case string, *ReferenceExpression: default: @@ -15800,6 +15871,12 @@ func (s *containerResource) WithBuildArg(name string, value any) ContainerResour if s.Err() != nil { return s } + if e, ok := value.(interface{ Err() error }); ok { + if err := e.Err(); err != nil { + s.setPropagatedErr(err) + return s + } + } switch value.(type) { case string, ParameterResource: default: @@ -16544,6 +16621,12 @@ func (s *containerResource) WithEnvironment(name string, value any) ContainerRes if s.Err() != nil { return s } + if e, ok := value.(interface{ Err() error }); ok { + if err := e.Err(); err != nil { + s.setPropagatedErr(err) + return s + } + } switch value.(type) { case string, *ReferenceExpression, EndpointReference, ParameterResource, ExternalServiceResource, ResourceWithConnectionString, ExpressionValue: default: @@ -17586,6 +17669,12 @@ func (s *containerResource) WithReference(source any, options ...WithReferenceOp if s.Err() != nil { return s } + if e, ok := source.(interface{ Err() error }); ok { + if err := e.Err(); err != nil { + s.setPropagatedErr(err) + return s + } + } switch source.(type) { case Resource, EndpointReference, string: default: @@ -17903,6 +17992,12 @@ func (s *containerResource) WithUnionDependency(dependency any) ContainerResourc if s.Err() != nil { return s } + if e, ok := dependency.(interface{ Err() error }); ok { + if err := e.Err(); err != nil { + s.setPropagatedErr(err) + return s + } + } switch dependency.(type) { case string, ResourceWithConnectionString: default: @@ -17929,6 +18024,12 @@ func (s *containerResource) WithUrl(url any, options ...WithUrlOption) Container if s.Err() != nil { return s } + if e, ok := url.(interface{ Err() error }); ok { + if err := e.Err(); err != nil { + s.setPropagatedErr(err) + return s + } + } switch url.(type) { case string, *ReferenceExpression: default: @@ -18519,6 +18620,11 @@ func (s *distributedApplicationBuilder) AddContainerRegistry(name string, endpoi if err := s.resourceBuilderBase.Err(); err != nil { return &containerRegistryResource{resourceBuilderBase: newPropagatedErroredResourceBuilder(err, s.client)} } + if e, ok := endpoint.(interface{ Err() error }); ok { + if err := e.Err(); err != nil { + return &containerRegistryResource{resourceBuilderBase: newPropagatedErroredResourceBuilder(err, s.client)} + } + } switch endpoint.(type) { case string, ParameterResource: default: @@ -18755,6 +18861,11 @@ func (s *distributedApplicationBuilder) AddExternalService(name string, url any) if err := s.resourceBuilderBase.Err(); err != nil { return &externalServiceResource{resourceBuilderBase: newPropagatedErroredResourceBuilder(err, s.client)} } + if e, ok := url.(interface{ Err() error }); ok { + if err := e.Err(); err != nil { + return &externalServiceResource{resourceBuilderBase: newPropagatedErroredResourceBuilder(err, s.client)} + } + } switch url.(type) { case string, ParameterResource: default: @@ -22515,6 +22626,12 @@ func (s *dotnetToolResource) WithEnvironment(name string, value any) DotnetToolR if s.Err() != nil { return s } + if e, ok := value.(interface{ Err() error }); ok { + if err := e.Err(); err != nil { + s.setPropagatedErr(err) + return s + } + } switch value.(type) { case string, *ReferenceExpression, EndpointReference, ParameterResource, ExternalServiceResource, ResourceWithConnectionString, ExpressionValue: default: @@ -23495,6 +23612,12 @@ func (s *dotnetToolResource) WithReference(source any, options ...WithReferenceO if s.Err() != nil { return s } + if e, ok := source.(interface{ Err() error }); ok { + if err := e.Err(); err != nil { + s.setPropagatedErr(err) + return s + } + } switch source.(type) { case Resource, EndpointReference, string: default: @@ -23908,6 +24031,12 @@ func (s *dotnetToolResource) WithUnionDependency(dependency any) DotnetToolResou if s.Err() != nil { return s } + if e, ok := dependency.(interface{ Err() error }); ok { + if err := e.Err(); err != nil { + s.setPropagatedErr(err) + return s + } + } switch dependency.(type) { case string, ResourceWithConnectionString: default: @@ -23934,6 +24063,12 @@ func (s *dotnetToolResource) WithUrl(url any, options ...WithUrlOption) DotnetTo if s.Err() != nil { return s } + if e, ok := url.(interface{ Err() error }); ok { + if err := e.Err(); err != nil { + s.setPropagatedErr(err) + return s + } + } switch url.(type) { case string, *ReferenceExpression: default: @@ -25355,6 +25490,11 @@ func (s *environmentEditor) Set(name string, value any) error { if err := s.Err(); err != nil { return err } + if e, ok := value.(interface{ Err() error }); ok { + if err := e.Err(); err != nil { + return err + } + } switch value.(type) { case string, *ReferenceExpression, EndpointReference, ParameterResource, ResourceWithConnectionString, ExpressionValue: default: @@ -27620,6 +27760,12 @@ func (s *executableResource) WithEnvironment(name string, value any) ExecutableR if s.Err() != nil { return s } + if e, ok := value.(interface{ Err() error }); ok { + if err := e.Err(); err != nil { + s.setPropagatedErr(err) + return s + } + } switch value.(type) { case string, *ReferenceExpression, EndpointReference, ParameterResource, ExternalServiceResource, ResourceWithConnectionString, ExpressionValue: default: @@ -28600,6 +28746,12 @@ func (s *executableResource) WithReference(source any, options ...WithReferenceO if s.Err() != nil { return s } + if e, ok := source.(interface{ Err() error }); ok { + if err := e.Err(); err != nil { + s.setPropagatedErr(err) + return s + } + } switch source.(type) { case Resource, EndpointReference, string: default: @@ -28917,6 +29069,12 @@ func (s *executableResource) WithUnionDependency(dependency any) ExecutableResou if s.Err() != nil { return s } + if e, ok := dependency.(interface{ Err() error }); ok { + if err := e.Err(); err != nil { + s.setPropagatedErr(err) + return s + } + } switch dependency.(type) { case string, ResourceWithConnectionString: default: @@ -28943,6 +29101,12 @@ func (s *executableResource) WithUrl(url any, options ...WithUrlOption) Executab if s.Err() != nil { return s } + if e, ok := url.(interface{ Err() error }); ok { + if err := e.Err(); err != nil { + s.setPropagatedErr(err) + return s + } + } switch url.(type) { case string, *ReferenceExpression: default: @@ -31369,6 +31533,12 @@ func (s *externalServiceResource) WithUnionDependency(dependency any) ExternalSe if s.Err() != nil { return s } + if e, ok := dependency.(interface{ Err() error }); ok { + if err := e.Err(); err != nil { + s.setPropagatedErr(err) + return s + } + } switch dependency.(type) { case string, ResourceWithConnectionString: default: @@ -31395,6 +31565,12 @@ func (s *externalServiceResource) WithUrl(url any, options ...WithUrlOption) Ext if s.Err() != nil { return s } + if e, ok := url.(interface{ Err() error }); ok { + if err := e.Err(); err != nil { + s.setPropagatedErr(err) + return s + } + } switch url.(type) { case string, *ReferenceExpression: default: @@ -36083,6 +36259,12 @@ func (s *parameterResource) WithUnionDependency(dependency any) ParameterResourc if s.Err() != nil { return s } + if e, ok := dependency.(interface{ Err() error }); ok { + if err := e.Err(); err != nil { + s.setPropagatedErr(err) + return s + } + } switch dependency.(type) { case string, ResourceWithConnectionString: default: @@ -36109,6 +36291,12 @@ func (s *parameterResource) WithUrl(url any, options ...WithUrlOption) Parameter if s.Err() != nil { return s } + if e, ok := url.(interface{ Err() error }); ok { + if err := e.Err(); err != nil { + s.setPropagatedErr(err) + return s + } + } switch url.(type) { case string, *ReferenceExpression: default: @@ -39269,6 +39457,12 @@ func (s *projectResource) WithEnvironment(name string, value any) ProjectResourc if s.Err() != nil { return s } + if e, ok := value.(interface{ Err() error }); ok { + if err := e.Err(); err != nil { + s.setPropagatedErr(err) + return s + } + } switch value.(type) { case string, *ReferenceExpression, EndpointReference, ParameterResource, ExternalServiceResource, ResourceWithConnectionString, ExpressionValue: default: @@ -40204,6 +40398,12 @@ func (s *projectResource) WithReference(source any, options ...WithReferenceOpti if s.Err() != nil { return s } + if e, ok := source.(interface{ Err() error }); ok { + if err := e.Err(); err != nil { + s.setPropagatedErr(err) + return s + } + } switch source.(type) { case Resource, EndpointReference, string: default: @@ -40538,6 +40738,12 @@ func (s *projectResource) WithUnionDependency(dependency any) ProjectResource { if s.Err() != nil { return s } + if e, ok := dependency.(interface{ Err() error }); ok { + if err := e.Err(); err != nil { + s.setPropagatedErr(err) + return s + } + } switch dependency.(type) { case string, ResourceWithConnectionString: default: @@ -40564,6 +40770,12 @@ func (s *projectResource) WithUrl(url any, options ...WithUrlOption) ProjectReso if s.Err() != nil { return s } + if e, ok := url.(interface{ Err() error }); ok { + if err := e.Err(); err != nil { + s.setPropagatedErr(err) + return s + } + } switch url.(type) { case string, *ReferenceExpression: default: @@ -41706,6 +41918,12 @@ func (s *resourceCommandService) ExecuteCommandAsync(resource any, commandName s var zero *ExecuteCommandResult return zero, err } + if e, ok := resource.(interface{ Err() error }); ok { + if err := e.Err(); err != nil { + var zero *ExecuteCommandResult + return zero, err + } + } switch resource.(type) { case string, Resource: default: @@ -42399,6 +42617,11 @@ func (s *resourceUrlsEditor) Add(url any, options ...AddOption) error { if err := s.Err(); err != nil { return err } + if e, ok := url.(interface{ Err() error }); ok { + if err := e.Err(); err != nil { + return err + } + } switch url.(type) { case string, *ReferenceExpression: default: @@ -42438,6 +42661,11 @@ func (s *resourceUrlsEditor) AddForEndpoint(endpoint EndpointReference, url any, return err } } + if e, ok := url.(interface{ Err() error }); ok { + if err := e.Err(); err != nil { + return err + } + } switch url.(type) { case string, *ReferenceExpression: default: @@ -44861,6 +45089,12 @@ func (s *testDatabaseResource) WithBuildArg(name string, value any) TestDatabase if s.Err() != nil { return s } + if e, ok := value.(interface{ Err() error }); ok { + if err := e.Err(); err != nil { + s.setPropagatedErr(err) + return s + } + } switch value.(type) { case string, ParameterResource: default: @@ -45605,6 +45839,12 @@ func (s *testDatabaseResource) WithEnvironment(name string, value any) TestDatab if s.Err() != nil { return s } + if e, ok := value.(interface{ Err() error }); ok { + if err := e.Err(); err != nil { + s.setPropagatedErr(err) + return s + } + } switch value.(type) { case string, *ReferenceExpression, EndpointReference, ParameterResource, ExternalServiceResource, ResourceWithConnectionString, ExpressionValue: default: @@ -46647,6 +46887,12 @@ func (s *testDatabaseResource) WithReference(source any, options ...WithReferenc if s.Err() != nil { return s } + if e, ok := source.(interface{ Err() error }); ok { + if err := e.Err(); err != nil { + s.setPropagatedErr(err) + return s + } + } switch source.(type) { case Resource, EndpointReference, string: default: @@ -46964,6 +47210,12 @@ func (s *testDatabaseResource) WithUnionDependency(dependency any) TestDatabaseR if s.Err() != nil { return s } + if e, ok := dependency.(interface{ Err() error }); ok { + if err := e.Err(); err != nil { + s.setPropagatedErr(err) + return s + } + } switch dependency.(type) { case string, ResourceWithConnectionString: default: @@ -46990,6 +47242,12 @@ func (s *testDatabaseResource) WithUrl(url any, options ...WithUrlOption) TestDa if s.Err() != nil { return s } + if e, ok := url.(interface{ Err() error }); ok { + if err := e.Err(); err != nil { + s.setPropagatedErr(err) + return s + } + } switch url.(type) { case string, *ReferenceExpression: default: @@ -49571,6 +49829,12 @@ func (s *testRedisResource) WithBuildArg(name string, value any) TestRedisResour if s.Err() != nil { return s } + if e, ok := value.(interface{ Err() error }); ok { + if err := e.Err(); err != nil { + s.setPropagatedErr(err) + return s + } + } switch value.(type) { case string, ParameterResource: default: @@ -49811,6 +50075,12 @@ func (s *testRedisResource) WithConnectionProperty(name string, value any) TestR if s.Err() != nil { return s } + if e, ok := value.(interface{ Err() error }); ok { + if err := e.Err(); err != nil { + s.setPropagatedErr(err) + return s + } + } switch value.(type) { case string, *ReferenceExpression: default: @@ -50433,6 +50703,12 @@ func (s *testRedisResource) WithEnvironment(name string, value any) TestRedisRes if s.Err() != nil { return s } + if e, ok := value.(interface{ Err() error }); ok { + if err := e.Err(); err != nil { + s.setPropagatedErr(err) + return s + } + } switch value.(type) { case string, *ReferenceExpression, EndpointReference, ParameterResource, ExternalServiceResource, ResourceWithConnectionString, ExpressionValue: default: @@ -51600,6 +51876,12 @@ func (s *testRedisResource) WithReference(source any, options ...WithReferenceOp if s.Err() != nil { return s } + if e, ok := source.(interface{ Err() error }); ok { + if err := e.Err(); err != nil { + s.setPropagatedErr(err) + return s + } + } switch source.(type) { case Resource, EndpointReference, string: default: @@ -51950,6 +52232,12 @@ func (s *testRedisResource) WithUnionDependency(dependency any) TestRedisResourc if s.Err() != nil { return s } + if e, ok := dependency.(interface{ Err() error }); ok { + if err := e.Err(); err != nil { + s.setPropagatedErr(err) + return s + } + } switch dependency.(type) { case string, ResourceWithConnectionString: default: @@ -51976,6 +52264,12 @@ func (s *testRedisResource) WithUrl(url any, options ...WithUrlOption) TestRedis if s.Err() != nil { return s } + if e, ok := url.(interface{ Err() error }); ok { + if err := e.Err(); err != nil { + s.setPropagatedErr(err) + return s + } + } switch url.(type) { case string, *ReferenceExpression: default: From fa259195b7eb96b7dcc1c37ce74812479766ebe7 Mon Sep 17 00:00:00 2001 From: David Pine <7679720+IEvangelist@users.noreply.github.com> Date: Thu, 12 Nov 2026 06:36:54 -0600 Subject: [PATCH 2/3] Go codegen: keep wrapper context in FormatError for joined errors Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../Resources/transport.go | 22 +++++++++++++------ .../AtsGoCodeGeneratorTests.cs | 4 ++-- .../Snapshots/AtsGeneratedAspire.verified.go | 22 +++++++++++++------ 3 files changed, 32 insertions(+), 16 deletions(-) diff --git a/src/Aspire.Hosting.CodeGeneration.Go/Resources/transport.go b/src/Aspire.Hosting.CodeGeneration.Go/Resources/transport.go index 2b67ac9f544..3c6f1c477c6 100644 --- a/src/Aspire.Hosting.CodeGeneration.Go/Resources/transport.go +++ b/src/Aspire.Hosting.CodeGeneration.Go/Resources/transport.go @@ -97,7 +97,8 @@ func (e *CapabilityError) Error() string { return e.err.Error() } // Non-SDK errors are returned via err.Error() unchanged. // // Callers commonly add context with fmt.Errorf("...: %w", err); the joined -// failures are still found underneath, and the added context is dropped. +// failures are still found underneath, and the added context is printed on its +// own line before them. func FormatError(err error) string { if err == nil { return "" @@ -106,8 +107,14 @@ func FormatError(err error) string { if !errors.As(err, &capErr) { return err.Error() } - if joined := findJoinedErrors(err); joined != nil { + if aggregate, joined := findJoinedErrors(err); joined != nil { var b strings.Builder + // "building app: " + aggregate.Error() -> "building app" + if prefix, ok := strings.CutSuffix(err.Error(), aggregate.Error()); ok { + if prefix = strings.TrimSuffix(prefix, ": "); prefix != "" { + b.WriteString(prefix) + } + } for _, child := range joined { formatted := FormatError(child) if formatted == "" { @@ -150,19 +157,20 @@ func FormatError(err error) string { } // findJoinedErrors walks single-error wrappers (fmt.Errorf with one %w) down to -// the first error that wraps several, such as the result of errors.Join. -func findJoinedErrors(err error) []error { +// the first error that wraps several, such as the result of errors.Join. It +// returns that aggregate error and the errors it wraps. +func findJoinedErrors(err error) (error, []error) { for err != nil { switch u := err.(type) { case interface{ Unwrap() []error }: - return u.Unwrap() + return err, u.Unwrap() case interface{ Unwrap() error }: err = u.Unwrap() default: - return nil + return nil, nil } } - return nil + return nil, nil } // CancellationToken wraps a context.Context to provide cooperative cancellation. diff --git a/tests/Aspire.Hosting.CodeGeneration.Go.Tests/AtsGoCodeGeneratorTests.cs b/tests/Aspire.Hosting.CodeGeneration.Go.Tests/AtsGoCodeGeneratorTests.cs index cf8691164fd..49a42a56923 100644 --- a/tests/Aspire.Hosting.CodeGeneration.Go.Tests/AtsGoCodeGeneratorTests.cs +++ b/tests/Aspire.Hosting.CodeGeneration.Go.Tests/AtsGoCodeGeneratorTests.cs @@ -1715,8 +1715,8 @@ func TestFormatErrorIncludesJoinedErrorsBehindWrappers(t *testing.T) { want := FormatError(errors.Join(first, second)) wrapped := fmt.Errorf("building app: %w", fmt.Errorf("resources: %w", errors.Join(first, second))) - if got := FormatError(wrapped); got != want { - t.Errorf("FormatError() = %q, want %q", got, want) + if got, wantWrapped := FormatError(wrapped), "building app: resources\n"+want; got != wantWrapped { + t.Errorf("FormatError() = %q, want %q", got, wantWrapped) } } diff --git a/tests/Aspire.Hosting.CodeGeneration.Go.Tests/Snapshots/AtsGeneratedAspire.verified.go b/tests/Aspire.Hosting.CodeGeneration.Go.Tests/Snapshots/AtsGeneratedAspire.verified.go index 02120177328..a8d328c9582 100644 --- a/tests/Aspire.Hosting.CodeGeneration.Go.Tests/Snapshots/AtsGeneratedAspire.verified.go +++ b/tests/Aspire.Hosting.CodeGeneration.Go.Tests/Snapshots/AtsGeneratedAspire.verified.go @@ -5598,7 +5598,8 @@ func (e *CapabilityError) Error() string { return e.err.Error() } // Non-SDK errors are returned via err.Error() unchanged. // // Callers commonly add context with fmt.Errorf("...: %w", err); the joined -// failures are still found underneath, and the added context is dropped. +// failures are still found underneath, and the added context is printed on its +// own line before them. func FormatError(err error) string { if err == nil { return "" @@ -5607,8 +5608,14 @@ func FormatError(err error) string { if !errors.As(err, &capErr) { return err.Error() } - if joined := findJoinedErrors(err); joined != nil { + if aggregate, joined := findJoinedErrors(err); joined != nil { var b strings.Builder + // "building app: " + aggregate.Error() -> "building app" + if prefix, ok := strings.CutSuffix(err.Error(), aggregate.Error()); ok { + if prefix = strings.TrimSuffix(prefix, ": "); prefix != "" { + b.WriteString(prefix) + } + } for _, child := range joined { formatted := FormatError(child) if formatted == "" { @@ -5651,19 +5658,20 @@ func FormatError(err error) string { } // findJoinedErrors walks single-error wrappers (fmt.Errorf with one %w) down to -// the first error that wraps several, such as the result of errors.Join. -func findJoinedErrors(err error) []error { +// the first error that wraps several, such as the result of errors.Join. It +// returns that aggregate error and the errors it wraps. +func findJoinedErrors(err error) (error, []error) { for err != nil { switch u := err.(type) { case interface{ Unwrap() []error }: - return u.Unwrap() + return err, u.Unwrap() case interface{ Unwrap() error }: err = u.Unwrap() default: - return nil + return nil, nil } } - return nil + return nil, nil } // CancellationToken wraps a context.Context to provide cooperative cancellation. From c65cc0c8117ffa81bdef41e5d80603a461f6883f Mon Sep 17 00:00:00 2001 From: David Pine <7679720+IEvangelist@users.noreply.github.com> Date: Thu, 12 Nov 2026 06:58:16 -0600 Subject: [PATCH 3/3] Go codegen: scope forwarded token IDs to their client and assert handleReference in unions Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../AtsGoCodeGenerator.cs | 6 +- .../Resources/transport.go | 11 +- .../AtsGoCodeGeneratorTests.cs | 16 +++ .../Snapshots/AtsGeneratedAspire.verified.go | 17 +-- ...TwoPassScanningGeneratedAspire.verified.go | 100 +++++++++--------- 5 files changed, 89 insertions(+), 61 deletions(-) diff --git a/src/Aspire.Hosting.CodeGeneration.Go/AtsGoCodeGenerator.cs b/src/Aspire.Hosting.CodeGeneration.Go/AtsGoCodeGenerator.cs index 54c34654827..b5c0a72d3d1 100644 --- a/src/Aspire.Hosting.CodeGeneration.Go/AtsGoCodeGenerator.cs +++ b/src/Aspire.Hosting.CodeGeneration.Go/AtsGoCodeGenerator.cs @@ -2080,12 +2080,14 @@ private void EmitHandleParamErrorChecks(string indent, AtsCapabilityInfo capabil } // Handle-valued unions are plain `any` locals, so the wrapper's Err is read through - // an interface assertion. Without it serializeValue turns an errored wrapper into nil. + // an interface assertion on handleReference so unrelated values with an Err method + // (for example a context.Context) still reach union validation. Without the guard + // serializeValue turns an errored wrapper into nil. foreach (var p in GetUnionParameters(capability)) { if (p.Type!.UnionTypes is not { } members || !members.Any(m => m.Category == AtsTypeCategory.Handle)) { continue; } var paramName = GetLocalIdentifier(p.Name); - WriteLine($"{indent}if e, ok := {paramName}.(interface{{ Err() error }}); ok {{ if err := e.Err(); err != nil {{ {errorAction} }} }}"); + WriteLine($"{indent}if e, ok := {paramName}.(handleReference); ok {{ if err := e.Err(); err != nil {{ {errorAction} }} }}"); } } diff --git a/src/Aspire.Hosting.CodeGeneration.Go/Resources/transport.go b/src/Aspire.Hosting.CodeGeneration.Go/Resources/transport.go index 3c6f1c477c6..b838dcc1638 100644 --- a/src/Aspire.Hosting.CodeGeneration.Go/Resources/transport.go +++ b/src/Aspire.Hosting.CodeGeneration.Go/Resources/transport.go @@ -185,6 +185,9 @@ type CancellationToken struct { // the token to another capability reuses it so the host links the nested // operation to the original cancellation source. remoteID string + // remoteClient is the connection that issued remoteID; cancellation IDs are + // only meaningful on that connection. + remoteClient *client } // NewCancellationToken creates a new cancellation token backed by a cancellable @@ -203,6 +206,7 @@ func newCancellationTokenFrom(parent context.Context) *CancellationToken { func newRemoteCancellationToken(c *client, id string) *CancellationToken { token := newCancellationTokenFrom(context.Background()) token.remoteID = id + token.remoteClient = c token.notify = func() { go c.cancelToken(id) } @@ -782,9 +786,10 @@ func (c *client) registerCancellation(token *CancellationToken) string { if token == nil { return "" } - // The host already owns this ID, so there is nothing to register or watch locally: - // Cancel on the remote token notifies the host itself. - if token.remoteID != "" { + // The host already owns this ID on this connection, so there is nothing to register + // or watch locally: Cancel on the remote token notifies the host itself. A token + // forwarded through a different client takes the normal local registration below. + if token.remoteID != "" && token.remoteClient == c { return token.remoteID } id := fmt.Sprintf("ct_%d_%d", c.cancellationCounter.Add(1), time.Now().UnixMilli()) diff --git a/tests/Aspire.Hosting.CodeGeneration.Go.Tests/AtsGoCodeGeneratorTests.cs b/tests/Aspire.Hosting.CodeGeneration.Go.Tests/AtsGoCodeGeneratorTests.cs index 49a42a56923..dc84f393420 100644 --- a/tests/Aspire.Hosting.CodeGeneration.Go.Tests/AtsGoCodeGeneratorTests.cs +++ b/tests/Aspire.Hosting.CodeGeneration.Go.Tests/AtsGoCodeGeneratorTests.cs @@ -1359,6 +1359,7 @@ package aspire import ( "bufio" "bytes" + "context" "encoding/json" "errors" "fmt" @@ -1720,6 +1721,14 @@ func TestFormatErrorIncludesJoinedErrorsBehindWrappers(t *testing.T) { } } + func TestForwardedCallbackTokenFromOtherClientIsRegisteredLocally(t *testing.T) { + other := newClient("") + remote := newRemoteCancellationToken(newClient(""), "ct_host") + if id := other.registerCancellation(remote); id == "ct_host" || id == "" { + t.Fatalf("registerCancellation() = %q, want a locally issued ID", id) + } + } + func TestForwardedCallbackTokenReusesHostID(t *testing.T) { c := newClient("") remote := newRemoteCancellationToken(c, "ct_host") @@ -1754,6 +1763,13 @@ var got *client } func TestErroredHandleInUnionIsNotSent(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + cancel() + unrelated := newTestRedisResourceFromHandle(&handle{HandleID: "u", TypeID: testRedisTypeID}, newClient("")) + unrelated.WithUnionDependency(ctx) + if err := unrelated.Err(); err == nil || !strings.Contains(err.Error(), "must be one of") { + t.Errorf("a canceled context got %v, want the union guard error", err) + } c, received := connectChildErrorHost(t, "addTestRedis") builder := newBuilderOn(c) diff --git a/tests/Aspire.Hosting.CodeGeneration.Go.Tests/Snapshots/AtsGeneratedAspire.verified.go b/tests/Aspire.Hosting.CodeGeneration.Go.Tests/Snapshots/AtsGeneratedAspire.verified.go index a8d328c9582..681bfde3a7d 100644 --- a/tests/Aspire.Hosting.CodeGeneration.Go.Tests/Snapshots/AtsGeneratedAspire.verified.go +++ b/tests/Aspire.Hosting.CodeGeneration.Go.Tests/Snapshots/AtsGeneratedAspire.verified.go @@ -803,7 +803,7 @@ func (s *aspire_Hosting_CodeGeneration_Go_TestsTestVaultResource) WithUnionDepen if s.Err() != nil { return s } - if e, ok := dependency.(interface{ Err() error }); ok { + if e, ok := dependency.(handleReference); ok { if err := e.Err(); err != nil { s.setPropagatedErr(err) return s @@ -1958,7 +1958,7 @@ func (s *testDatabaseResource) WithUnionDependency(dependency any) TestDatabaseR if s.Err() != nil { return s } - if e, ok := dependency.(interface{ Err() error }); ok { + if e, ok := dependency.(handleReference); ok { if err := e.Err(); err != nil { s.setPropagatedErr(err) return s @@ -3344,7 +3344,7 @@ func (s *testRedisResource) WithUnionDependency(dependency any) TestRedisResourc if s.Err() != nil { return s } - if e, ok := dependency.(interface{ Err() error }); ok { + if e, ok := dependency.(handleReference); ok { if err := e.Err(); err != nil { s.setPropagatedErr(err) return s @@ -5686,6 +5686,9 @@ type CancellationToken struct { // the token to another capability reuses it so the host links the nested // operation to the original cancellation source. remoteID string + // remoteClient is the connection that issued remoteID; cancellation IDs are + // only meaningful on that connection. + remoteClient *client } // NewCancellationToken creates a new cancellation token backed by a cancellable @@ -5704,6 +5707,7 @@ func newCancellationTokenFrom(parent context.Context) *CancellationToken { func newRemoteCancellationToken(c *client, id string) *CancellationToken { token := newCancellationTokenFrom(context.Background()) token.remoteID = id + token.remoteClient = c token.notify = func() { go c.cancelToken(id) } @@ -6283,9 +6287,10 @@ func (c *client) registerCancellation(token *CancellationToken) string { if token == nil { return "" } - // The host already owns this ID, so there is nothing to register or watch locally: - // Cancel on the remote token notifies the host itself. - if token.remoteID != "" { + // The host already owns this ID on this connection, so there is nothing to register + // or watch locally: Cancel on the remote token notifies the host itself. A token + // forwarded through a different client takes the normal local registration below. + if token.remoteID != "" && token.remoteClient == c { return token.remoteID } id := fmt.Sprintf("ct_%d_%d", c.cancellationCounter.Add(1), time.Now().UnixMilli()) diff --git a/tests/Aspire.Hosting.CodeGeneration.Go.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.go b/tests/Aspire.Hosting.CodeGeneration.Go.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.go index 2b57c97cdfc..7782d21e8eb 100644 --- a/tests/Aspire.Hosting.CodeGeneration.Go.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.go +++ b/tests/Aspire.Hosting.CodeGeneration.Go.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.go @@ -4274,7 +4274,7 @@ func (s *aspire_Hosting_CodeGeneration_Go_TestsTestVaultResource) WithBuildArg(n if s.Err() != nil { return s } - if e, ok := value.(interface{ Err() error }); ok { + if e, ok := value.(handleReference); ok { if err := e.Err(); err != nil { s.setPropagatedErr(err) return s @@ -5024,7 +5024,7 @@ func (s *aspire_Hosting_CodeGeneration_Go_TestsTestVaultResource) WithEnvironmen if s.Err() != nil { return s } - if e, ok := value.(interface{ Err() error }); ok { + if e, ok := value.(handleReference); ok { if err := e.Err(); err != nil { s.setPropagatedErr(err) return s @@ -6072,7 +6072,7 @@ func (s *aspire_Hosting_CodeGeneration_Go_TestsTestVaultResource) WithReference( if s.Err() != nil { return s } - if e, ok := source.(interface{ Err() error }); ok { + if e, ok := source.(handleReference); ok { if err := e.Err(); err != nil { s.setPropagatedErr(err) return s @@ -6395,7 +6395,7 @@ func (s *aspire_Hosting_CodeGeneration_Go_TestsTestVaultResource) WithUnionDepen if s.Err() != nil { return s } - if e, ok := dependency.(interface{ Err() error }); ok { + if e, ok := dependency.(handleReference); ok { if err := e.Err(); err != nil { s.setPropagatedErr(err) return s @@ -6427,7 +6427,7 @@ func (s *aspire_Hosting_CodeGeneration_Go_TestsTestVaultResource) WithUrl(url an if s.Err() != nil { return s } - if e, ok := url.(interface{ Err() error }); ok { + if e, ok := url.(handleReference); ok { if err := e.Err(); err != nil { s.setPropagatedErr(err) return s @@ -8902,7 +8902,7 @@ func (s *cSharpAppResource) WithEnvironment(name string, value any) CSharpAppRes if s.Err() != nil { return s } - if e, ok := value.(interface{ Err() error }); ok { + if e, ok := value.(handleReference); ok { if err := e.Err(); err != nil { s.setPropagatedErr(err) return s @@ -9843,7 +9843,7 @@ func (s *cSharpAppResource) WithReference(source any, options ...WithReferenceOp if s.Err() != nil { return s } - if e, ok := source.(interface{ Err() error }); ok { + if e, ok := source.(handleReference); ok { if err := e.Err(); err != nil { s.setPropagatedErr(err) return s @@ -10183,7 +10183,7 @@ func (s *cSharpAppResource) WithUnionDependency(dependency any) CSharpAppResourc if s.Err() != nil { return s } - if e, ok := dependency.(interface{ Err() error }); ok { + if e, ok := dependency.(handleReference); ok { if err := e.Err(); err != nil { s.setPropagatedErr(err) return s @@ -10215,7 +10215,7 @@ func (s *cSharpAppResource) WithUrl(url any, options ...WithUrlOption) CSharpApp if s.Err() != nil { return s } - if e, ok := url.(interface{ Err() error }); ok { + if e, ok := url.(handleReference); ok { if err := e.Err(); err != nil { s.setPropagatedErr(err) return s @@ -10532,7 +10532,7 @@ func (s *commandLineArgsEditor) Add(value any) error { if err := s.Err(); err != nil { return err } - if e, ok := value.(interface{ Err() error }); ok { + if e, ok := value.(handleReference); ok { if err := e.Err(); err != nil { return err } @@ -13906,7 +13906,7 @@ func (s *containerRegistryResource) WithUnionDependency(dependency any) Containe if s.Err() != nil { return s } - if e, ok := dependency.(interface{ Err() error }); ok { + if e, ok := dependency.(handleReference); ok { if err := e.Err(); err != nil { s.setPropagatedErr(err) return s @@ -13938,7 +13938,7 @@ func (s *containerRegistryResource) WithUrl(url any, options ...WithUrlOption) C if s.Err() != nil { return s } - if e, ok := url.(interface{ Err() error }); ok { + if e, ok := url.(handleReference); ok { if err := e.Err(); err != nil { s.setPropagatedErr(err) return s @@ -15871,7 +15871,7 @@ func (s *containerResource) WithBuildArg(name string, value any) ContainerResour if s.Err() != nil { return s } - if e, ok := value.(interface{ Err() error }); ok { + if e, ok := value.(handleReference); ok { if err := e.Err(); err != nil { s.setPropagatedErr(err) return s @@ -16621,7 +16621,7 @@ func (s *containerResource) WithEnvironment(name string, value any) ContainerRes if s.Err() != nil { return s } - if e, ok := value.(interface{ Err() error }); ok { + if e, ok := value.(handleReference); ok { if err := e.Err(); err != nil { s.setPropagatedErr(err) return s @@ -17669,7 +17669,7 @@ func (s *containerResource) WithReference(source any, options ...WithReferenceOp if s.Err() != nil { return s } - if e, ok := source.(interface{ Err() error }); ok { + if e, ok := source.(handleReference); ok { if err := e.Err(); err != nil { s.setPropagatedErr(err) return s @@ -17992,7 +17992,7 @@ func (s *containerResource) WithUnionDependency(dependency any) ContainerResourc if s.Err() != nil { return s } - if e, ok := dependency.(interface{ Err() error }); ok { + if e, ok := dependency.(handleReference); ok { if err := e.Err(); err != nil { s.setPropagatedErr(err) return s @@ -18024,7 +18024,7 @@ func (s *containerResource) WithUrl(url any, options ...WithUrlOption) Container if s.Err() != nil { return s } - if e, ok := url.(interface{ Err() error }); ok { + if e, ok := url.(handleReference); ok { if err := e.Err(); err != nil { s.setPropagatedErr(err) return s @@ -18620,7 +18620,7 @@ func (s *distributedApplicationBuilder) AddContainerRegistry(name string, endpoi if err := s.resourceBuilderBase.Err(); err != nil { return &containerRegistryResource{resourceBuilderBase: newPropagatedErroredResourceBuilder(err, s.client)} } - if e, ok := endpoint.(interface{ Err() error }); ok { + if e, ok := endpoint.(handleReference); ok { if err := e.Err(); err != nil { return &containerRegistryResource{resourceBuilderBase: newPropagatedErroredResourceBuilder(err, s.client)} } @@ -18861,7 +18861,7 @@ func (s *distributedApplicationBuilder) AddExternalService(name string, url any) if err := s.resourceBuilderBase.Err(); err != nil { return &externalServiceResource{resourceBuilderBase: newPropagatedErroredResourceBuilder(err, s.client)} } - if e, ok := url.(interface{ Err() error }); ok { + if e, ok := url.(handleReference); ok { if err := e.Err(); err != nil { return &externalServiceResource{resourceBuilderBase: newPropagatedErroredResourceBuilder(err, s.client)} } @@ -22626,7 +22626,7 @@ func (s *dotnetToolResource) WithEnvironment(name string, value any) DotnetToolR if s.Err() != nil { return s } - if e, ok := value.(interface{ Err() error }); ok { + if e, ok := value.(handleReference); ok { if err := e.Err(); err != nil { s.setPropagatedErr(err) return s @@ -23612,7 +23612,7 @@ func (s *dotnetToolResource) WithReference(source any, options ...WithReferenceO if s.Err() != nil { return s } - if e, ok := source.(interface{ Err() error }); ok { + if e, ok := source.(handleReference); ok { if err := e.Err(); err != nil { s.setPropagatedErr(err) return s @@ -24031,7 +24031,7 @@ func (s *dotnetToolResource) WithUnionDependency(dependency any) DotnetToolResou if s.Err() != nil { return s } - if e, ok := dependency.(interface{ Err() error }); ok { + if e, ok := dependency.(handleReference); ok { if err := e.Err(); err != nil { s.setPropagatedErr(err) return s @@ -24063,7 +24063,7 @@ func (s *dotnetToolResource) WithUrl(url any, options ...WithUrlOption) DotnetTo if s.Err() != nil { return s } - if e, ok := url.(interface{ Err() error }); ok { + if e, ok := url.(handleReference); ok { if err := e.Err(); err != nil { s.setPropagatedErr(err) return s @@ -25490,7 +25490,7 @@ func (s *environmentEditor) Set(name string, value any) error { if err := s.Err(); err != nil { return err } - if e, ok := value.(interface{ Err() error }); ok { + if e, ok := value.(handleReference); ok { if err := e.Err(); err != nil { return err } @@ -27760,7 +27760,7 @@ func (s *executableResource) WithEnvironment(name string, value any) ExecutableR if s.Err() != nil { return s } - if e, ok := value.(interface{ Err() error }); ok { + if e, ok := value.(handleReference); ok { if err := e.Err(); err != nil { s.setPropagatedErr(err) return s @@ -28746,7 +28746,7 @@ func (s *executableResource) WithReference(source any, options ...WithReferenceO if s.Err() != nil { return s } - if e, ok := source.(interface{ Err() error }); ok { + if e, ok := source.(handleReference); ok { if err := e.Err(); err != nil { s.setPropagatedErr(err) return s @@ -29069,7 +29069,7 @@ func (s *executableResource) WithUnionDependency(dependency any) ExecutableResou if s.Err() != nil { return s } - if e, ok := dependency.(interface{ Err() error }); ok { + if e, ok := dependency.(handleReference); ok { if err := e.Err(); err != nil { s.setPropagatedErr(err) return s @@ -29101,7 +29101,7 @@ func (s *executableResource) WithUrl(url any, options ...WithUrlOption) Executab if s.Err() != nil { return s } - if e, ok := url.(interface{ Err() error }); ok { + if e, ok := url.(handleReference); ok { if err := e.Err(); err != nil { s.setPropagatedErr(err) return s @@ -31533,7 +31533,7 @@ func (s *externalServiceResource) WithUnionDependency(dependency any) ExternalSe if s.Err() != nil { return s } - if e, ok := dependency.(interface{ Err() error }); ok { + if e, ok := dependency.(handleReference); ok { if err := e.Err(); err != nil { s.setPropagatedErr(err) return s @@ -31565,7 +31565,7 @@ func (s *externalServiceResource) WithUrl(url any, options ...WithUrlOption) Ext if s.Err() != nil { return s } - if e, ok := url.(interface{ Err() error }); ok { + if e, ok := url.(handleReference); ok { if err := e.Err(); err != nil { s.setPropagatedErr(err) return s @@ -36259,7 +36259,7 @@ func (s *parameterResource) WithUnionDependency(dependency any) ParameterResourc if s.Err() != nil { return s } - if e, ok := dependency.(interface{ Err() error }); ok { + if e, ok := dependency.(handleReference); ok { if err := e.Err(); err != nil { s.setPropagatedErr(err) return s @@ -36291,7 +36291,7 @@ func (s *parameterResource) WithUrl(url any, options ...WithUrlOption) Parameter if s.Err() != nil { return s } - if e, ok := url.(interface{ Err() error }); ok { + if e, ok := url.(handleReference); ok { if err := e.Err(); err != nil { s.setPropagatedErr(err) return s @@ -39457,7 +39457,7 @@ func (s *projectResource) WithEnvironment(name string, value any) ProjectResourc if s.Err() != nil { return s } - if e, ok := value.(interface{ Err() error }); ok { + if e, ok := value.(handleReference); ok { if err := e.Err(); err != nil { s.setPropagatedErr(err) return s @@ -40398,7 +40398,7 @@ func (s *projectResource) WithReference(source any, options ...WithReferenceOpti if s.Err() != nil { return s } - if e, ok := source.(interface{ Err() error }); ok { + if e, ok := source.(handleReference); ok { if err := e.Err(); err != nil { s.setPropagatedErr(err) return s @@ -40738,7 +40738,7 @@ func (s *projectResource) WithUnionDependency(dependency any) ProjectResource { if s.Err() != nil { return s } - if e, ok := dependency.(interface{ Err() error }); ok { + if e, ok := dependency.(handleReference); ok { if err := e.Err(); err != nil { s.setPropagatedErr(err) return s @@ -40770,7 +40770,7 @@ func (s *projectResource) WithUrl(url any, options ...WithUrlOption) ProjectReso if s.Err() != nil { return s } - if e, ok := url.(interface{ Err() error }); ok { + if e, ok := url.(handleReference); ok { if err := e.Err(); err != nil { s.setPropagatedErr(err) return s @@ -41918,7 +41918,7 @@ func (s *resourceCommandService) ExecuteCommandAsync(resource any, commandName s var zero *ExecuteCommandResult return zero, err } - if e, ok := resource.(interface{ Err() error }); ok { + if e, ok := resource.(handleReference); ok { if err := e.Err(); err != nil { var zero *ExecuteCommandResult return zero, err @@ -42617,7 +42617,7 @@ func (s *resourceUrlsEditor) Add(url any, options ...AddOption) error { if err := s.Err(); err != nil { return err } - if e, ok := url.(interface{ Err() error }); ok { + if e, ok := url.(handleReference); ok { if err := e.Err(); err != nil { return err } @@ -42661,7 +42661,7 @@ func (s *resourceUrlsEditor) AddForEndpoint(endpoint EndpointReference, url any, return err } } - if e, ok := url.(interface{ Err() error }); ok { + if e, ok := url.(handleReference); ok { if err := e.Err(); err != nil { return err } @@ -45089,7 +45089,7 @@ func (s *testDatabaseResource) WithBuildArg(name string, value any) TestDatabase if s.Err() != nil { return s } - if e, ok := value.(interface{ Err() error }); ok { + if e, ok := value.(handleReference); ok { if err := e.Err(); err != nil { s.setPropagatedErr(err) return s @@ -45839,7 +45839,7 @@ func (s *testDatabaseResource) WithEnvironment(name string, value any) TestDatab if s.Err() != nil { return s } - if e, ok := value.(interface{ Err() error }); ok { + if e, ok := value.(handleReference); ok { if err := e.Err(); err != nil { s.setPropagatedErr(err) return s @@ -46887,7 +46887,7 @@ func (s *testDatabaseResource) WithReference(source any, options ...WithReferenc if s.Err() != nil { return s } - if e, ok := source.(interface{ Err() error }); ok { + if e, ok := source.(handleReference); ok { if err := e.Err(); err != nil { s.setPropagatedErr(err) return s @@ -47210,7 +47210,7 @@ func (s *testDatabaseResource) WithUnionDependency(dependency any) TestDatabaseR if s.Err() != nil { return s } - if e, ok := dependency.(interface{ Err() error }); ok { + if e, ok := dependency.(handleReference); ok { if err := e.Err(); err != nil { s.setPropagatedErr(err) return s @@ -47242,7 +47242,7 @@ func (s *testDatabaseResource) WithUrl(url any, options ...WithUrlOption) TestDa if s.Err() != nil { return s } - if e, ok := url.(interface{ Err() error }); ok { + if e, ok := url.(handleReference); ok { if err := e.Err(); err != nil { s.setPropagatedErr(err) return s @@ -49829,7 +49829,7 @@ func (s *testRedisResource) WithBuildArg(name string, value any) TestRedisResour if s.Err() != nil { return s } - if e, ok := value.(interface{ Err() error }); ok { + if e, ok := value.(handleReference); ok { if err := e.Err(); err != nil { s.setPropagatedErr(err) return s @@ -50075,7 +50075,7 @@ func (s *testRedisResource) WithConnectionProperty(name string, value any) TestR if s.Err() != nil { return s } - if e, ok := value.(interface{ Err() error }); ok { + if e, ok := value.(handleReference); ok { if err := e.Err(); err != nil { s.setPropagatedErr(err) return s @@ -50703,7 +50703,7 @@ func (s *testRedisResource) WithEnvironment(name string, value any) TestRedisRes if s.Err() != nil { return s } - if e, ok := value.(interface{ Err() error }); ok { + if e, ok := value.(handleReference); ok { if err := e.Err(); err != nil { s.setPropagatedErr(err) return s @@ -51876,7 +51876,7 @@ func (s *testRedisResource) WithReference(source any, options ...WithReferenceOp if s.Err() != nil { return s } - if e, ok := source.(interface{ Err() error }); ok { + if e, ok := source.(handleReference); ok { if err := e.Err(); err != nil { s.setPropagatedErr(err) return s @@ -52232,7 +52232,7 @@ func (s *testRedisResource) WithUnionDependency(dependency any) TestRedisResourc if s.Err() != nil { return s } - if e, ok := dependency.(interface{ Err() error }); ok { + if e, ok := dependency.(handleReference); ok { if err := e.Err(); err != nil { s.setPropagatedErr(err) return s @@ -52264,7 +52264,7 @@ func (s *testRedisResource) WithUrl(url any, options ...WithUrlOption) TestRedis if s.Err() != nil { return s } - if e, ok := url.(interface{ Err() error }); ok { + if e, ok := url.(handleReference); ok { if err := e.Err(); err != nil { s.setPropagatedErr(err) return s