@@ -18,6 +18,7 @@ import (
1818 "github.com/azure/azure-dev/cli/azd/pkg/alpha"
1919 "github.com/azure/azure-dev/cli/azd/pkg/auth"
2020 "github.com/azure/azure-dev/cli/azd/pkg/azapi"
21+ "github.com/azure/azure-dev/cli/azd/pkg/azdext"
2122 "github.com/azure/azure-dev/cli/azd/pkg/config"
2223 "github.com/azure/azure-dev/cli/azd/pkg/environment"
2324 "github.com/azure/azure-dev/cli/azd/pkg/errorhandler"
@@ -231,6 +232,108 @@ func Test_ErrorMiddleware_PatternMatchingSuggestion(t *testing.T) {
231232 require .NotEmpty (t , suggestionErr .Links , "Expected reference links" )
232233}
233234
235+ // Test_ErrorMiddleware_ExtensionErrorWithSuggestion_BypassesPipeline verifies that
236+ // when an extension-supplied error (LocalError or ServiceError) already carries a
237+ // Suggestion, the YAML error-suggestion pipeline is short-circuited so it doesn't
238+ // override the extension's specific guidance with a generic one. Regression test
239+ // for https://github.com/Azure/azure-dev/issues/7706.
240+ func Test_ErrorMiddleware_ExtensionErrorWithSuggestion_BypassesPipeline (t * testing.T ) {
241+ t .Parallel ()
242+
243+ mockContext := mocks .NewMockContext (context .Background ())
244+ cfg := config .NewEmptyConfig ()
245+ featureManager := alpha .NewFeaturesManagerWithConfig (cfg )
246+ global := & internal.GlobalCommandOptions {
247+ NoPrompt : true , // skip agentic handling
248+ }
249+ userConfigManager := config .NewUserConfigManager (mockContext .ConfigManager )
250+ errorPipeline := errorhandler .NewErrorHandlerPipeline (nil )
251+ middleware := NewErrorMiddleware (
252+ & Options {Name : "test" },
253+ mockContext .Console ,
254+ nil ,
255+ global ,
256+ featureManager ,
257+ userConfigManager ,
258+ errorPipeline ,
259+ )
260+
261+ // "QuotaExceeded" matches a known pattern in error_suggestions.yaml that would
262+ // otherwise wrap into an *ErrorWithSuggestion (proven by
263+ // Test_ErrorMiddleware_PatternMatchingSuggestion). The middleware must skip
264+ // that wrapping when the extension already supplied a suggestion of its own.
265+ extErr := & azdext.LocalError {
266+ Message : "Deployment failed: QuotaExceeded for resource" ,
267+ Code : "quota_exceeded" ,
268+ Category : azdext .LocalErrorCategoryUser ,
269+ Suggestion : "Extension-provided guidance: request a quota increase via the portal." ,
270+ }
271+ nextFn := func (ctx context.Context ) (* actions.ActionResult , error ) {
272+ return nil , extErr
273+ }
274+
275+ result , err := middleware .Run (* mockContext .Context , nextFn )
276+
277+ require .Error (t , err )
278+ require .Nil (t , result )
279+
280+ // The original LocalError must be returned unchanged — not wrapped in
281+ // *ErrorWithSuggestion by the YAML pipeline.
282+ var wrapped * internal.ErrorWithSuggestion
283+ require .False (
284+ t ,
285+ errors .As (err , & wrapped ),
286+ "YAML pipeline should not wrap an extension error that already has a Suggestion" ,
287+ )
288+ returned , ok := errors.AsType [* azdext.LocalError ](err )
289+ require .True (t , ok , "expected the original *LocalError to be returned" )
290+ require .Same (t , extErr , returned )
291+ require .Equal (t , extErr .Suggestion , azdext .ErrorSuggestion (err ))
292+ }
293+
294+ func Test_ErrorMiddleware_StructuredExtensionErrorWithoutSuggestion_BypassesPipeline (t * testing.T ) {
295+ t .Parallel ()
296+
297+ mockContext := mocks .NewMockContext (context .Background ())
298+ cfg := config .NewEmptyConfig ()
299+ featureManager := alpha .NewFeaturesManagerWithConfig (cfg )
300+ global := & internal.GlobalCommandOptions {
301+ NoPrompt : true ,
302+ }
303+ userConfigManager := config .NewUserConfigManager (mockContext .ConfigManager )
304+ errorPipeline := errorhandler .NewErrorHandlerPipeline (nil )
305+ middleware := NewErrorMiddleware (
306+ & Options {Name : "test" },
307+ mockContext .Console ,
308+ nil ,
309+ global ,
310+ featureManager ,
311+ userConfigManager ,
312+ errorPipeline ,
313+ )
314+
315+ extErr := & azdext.LocalError {
316+ Message : "Deployment failed: QuotaExceeded for resource" ,
317+ Code : "quota_exceeded" ,
318+ Category : azdext .LocalErrorCategoryUser ,
319+ }
320+ nextFn := func (ctx context.Context ) (* actions.ActionResult , error ) {
321+ return nil , extErr
322+ }
323+
324+ result , err := middleware .Run (* mockContext .Context , nextFn )
325+
326+ require .Error (t , err )
327+ require .Nil (t , result )
328+
329+ var wrapped * internal.ErrorWithSuggestion
330+ require .False (t , errors .As (err , & wrapped ))
331+ returned , ok := errors.AsType [* azdext.LocalError ](err )
332+ require .True (t , ok )
333+ require .Same (t , extErr , returned )
334+ require .Empty (t , azdext .ErrorSuggestion (err ))
335+ }
336+
234337func Test_ErrorMiddleware_NoPatternMatch (t * testing.T ) {
235338 t .Parallel ()
236339 mockContext := mocks .NewMockContext (context .Background ())
0 commit comments