Skip to content

Commit 1c486f0

Browse files
authored
Standardize no-prompt required input behavior (#7825)
Previously, `no-prompt` handling was inconsistent throughout the system. This change aligns handling to a standard behavior, and provides failed cases with consistent human-readable and JSON output. `no-prompt` now operates strictly and is better suited for non-interactive work: CI and agents. - added a shared `input.PromptRequiredError` for no-prompt failures - updated prompt-service, provisioning, prompt-model, and asker paths to use the shared error instead of ad hoc strings - adjusted behavior in `pkg/prompt/prompt_service.go` to require input for missing subscription, location, and resource group selection - adjusted behavior in `pkg/infra/provisioning/manager.go` to require input for missing subscription and location selection
1 parent 181537b commit 1c486f0

17 files changed

Lines changed: 888 additions & 510 deletions

cli/azd/cmd/deeper_coverage3_test.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,10 @@ func (m *mockPrompter) PromptResourceGroupFrom(
137137
return args.String(0), args.Error(1)
138138
}
139139

140+
func (m *mockPrompter) IsNoPromptMode() bool {
141+
return false
142+
}
143+
140144
type mockEnvSetSecretSubscriptionResolver struct {
141145
mock.Mock
142146
}

cli/azd/internal/grpcserver/prompt_interactive_coverage3_test.go

Lines changed: 53 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,7 @@ func TestPromptService_Confirm_NoPrompt_NoDefault(t *testing.T) {
122122
},
123123
})
124124
require.Error(t, err)
125-
require.Contains(t, err.Error(), "no default response")
125+
requirePromptRequiredError(t, err, "continue?")
126126
}
127127

128128
// --- Select tests ---
@@ -169,7 +169,7 @@ func TestPromptService_Select_NoPrompt_NoDefault(t *testing.T) {
169169
},
170170
})
171171
require.Error(t, err)
172-
require.Contains(t, err.Error(), "no default selection")
172+
requirePromptRequiredError(t, err, "choose:")
173173
}
174174

175175
// --- MultiSelect tests ---
@@ -247,7 +247,7 @@ func TestPromptService_Prompt_NoPrompt_RequiredNoDefault(t *testing.T) {
247247
},
248248
})
249249
require.Error(t, err)
250-
require.Contains(t, err.Error(), "no default response")
250+
requirePromptRequiredError(t, err, "enter:")
251251
}
252252

253253
func TestPromptService_Prompt_NoPrompt_RequiredWithDefault(t *testing.T) {
@@ -302,6 +302,14 @@ func TestPromptService_PromptSubscription_Error(t *testing.T) {
302302
require.Contains(t, err.Error(), "no subscriptions")
303303
}
304304

305+
func TestPromptService_PromptSubscription_NoPrompt_DefaultMessage(t *testing.T) {
306+
t.Parallel()
307+
svc := newTestPromptService(&mockPromptService{}, true)
308+
_, err := svc.PromptSubscription(t.Context(), &azdext.PromptSubscriptionRequest{})
309+
require.Error(t, err)
310+
requirePromptRequiredError(t, err, "Select subscription")
311+
}
312+
305313
// --- PromptLocation tests ---
306314

307315
func TestPromptService_PromptLocation_NilAzureContext(t *testing.T) {
@@ -382,6 +390,14 @@ func TestPromptService_PromptLocation_Error(t *testing.T) {
382390
require.Error(t, err)
383391
}
384392

393+
func TestPromptService_PromptLocation_NoPrompt(t *testing.T) {
394+
t.Parallel()
395+
svc := newTestPromptService(&mockPromptService{}, true)
396+
_, err := svc.PromptLocation(t.Context(), &azdext.PromptLocationRequest{})
397+
require.Error(t, err)
398+
requirePromptRequiredError(t, err, "Select location")
399+
}
400+
385401
// --- PromptResourceGroup tests ---
386402

387403
func TestPromptService_PromptResourceGroup_NilAzureContext(t *testing.T) {
@@ -438,6 +454,14 @@ func TestPromptService_PromptResourceGroup_Error(t *testing.T) {
438454
require.Error(t, err)
439455
}
440456

457+
func TestPromptService_PromptResourceGroup_NoPrompt_DefaultMessage(t *testing.T) {
458+
t.Parallel()
459+
svc := newTestPromptService(&mockPromptService{}, true)
460+
_, err := svc.PromptResourceGroup(t.Context(), &azdext.PromptResourceGroupRequest{})
461+
require.Error(t, err)
462+
requirePromptRequiredError(t, err, "Select resource group")
463+
}
464+
441465
// --- PromptSubscriptionResource tests ---
442466

443467
func TestPromptService_PromptSubscriptionResource_NilAzureContext(t *testing.T) {
@@ -503,6 +527,18 @@ func TestPromptService_PromptSubscriptionResource_Error(t *testing.T) {
503527
require.Error(t, err)
504528
}
505529

530+
func TestPromptService_PromptSubscriptionResource_NoPrompt_DefaultResourceMessage(t *testing.T) {
531+
t.Parallel()
532+
svc := newTestPromptService(&mockPromptService{}, true)
533+
_, err := svc.PromptSubscriptionResource(t.Context(), &azdext.PromptSubscriptionResourceRequest{
534+
Options: &azdext.PromptResourceOptions{
535+
ResourceTypeDisplayName: "OpenAI account",
536+
},
537+
})
538+
require.Error(t, err)
539+
requirePromptRequiredError(t, err, "Select OpenAI account")
540+
}
541+
506542
// --- PromptResourceGroupResource tests ---
507543

508544
func TestPromptService_PromptResourceGroupResource_NilAzureContext(t *testing.T) {
@@ -512,6 +548,20 @@ func TestPromptService_PromptResourceGroupResource_NilAzureContext(t *testing.T)
512548
require.Error(t, err)
513549
}
514550

551+
func TestPromptService_PromptResourceGroupResource_NoPrompt_UsesSelectOptionsMessage(t *testing.T) {
552+
t.Parallel()
553+
svc := newTestPromptService(&mockPromptService{}, true)
554+
_, err := svc.PromptResourceGroupResource(t.Context(), &azdext.PromptResourceGroupResourceRequest{
555+
Options: &azdext.PromptResourceOptions{
556+
SelectOptions: &azdext.PromptResourceSelectOptions{
557+
Message: "Select existing web app",
558+
},
559+
},
560+
})
561+
require.Error(t, err)
562+
requirePromptRequiredError(t, err, "Select existing web app")
563+
}
564+
515565
func TestPromptService_PromptResourceGroupResource_Success(t *testing.T) {
516566
t.Parallel()
517567
mock := &mockPromptService{

cli/azd/internal/grpcserver/prompt_service.go

Lines changed: 69 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import (
1616
"github.com/azure/azure-dev/cli/azd/pkg/ai"
1717
"github.com/azure/azure-dev/cli/azd/pkg/azapi"
1818
"github.com/azure/azure-dev/cli/azd/pkg/azdext"
19+
"github.com/azure/azure-dev/cli/azd/pkg/input"
1920
"github.com/azure/azure-dev/cli/azd/pkg/output"
2021
"github.com/azure/azure-dev/cli/azd/pkg/prompt"
2122
"github.com/azure/azure-dev/cli/azd/pkg/ux"
@@ -54,7 +55,9 @@ func (s *promptService) Confirm(ctx context.Context, req *azdext.ConfirmRequest)
5455

5556
if s.globalOptions.NoPrompt {
5657
if req.Options.DefaultValue == nil {
57-
return nil, fmt.Errorf("no default response for prompt '%s'", req.Options.Message)
58+
return nil, &input.PromptRequiredError{
59+
PromptMessage: req.Options.Message,
60+
}
5861
} else {
5962
return &azdext.ConfirmResponse{
6063
Value: req.Options.DefaultValue,
@@ -91,7 +94,9 @@ func (s *promptService) Select(ctx context.Context, req *azdext.SelectRequest) (
9194

9295
if s.globalOptions.NoPrompt {
9396
if req.Options.SelectedIndex == nil {
94-
return nil, fmt.Errorf("no default selection for prompt '%s'", req.Options.Message)
97+
return nil, &input.PromptRequiredError{
98+
PromptMessage: req.Options.Message,
99+
}
95100
} else {
96101
return &azdext.SelectResponse{
97102
Value: req.Options.SelectedIndex,
@@ -196,7 +201,9 @@ func (s *promptService) MultiSelect(
196201
func (s *promptService) Prompt(ctx context.Context, req *azdext.PromptRequest) (*azdext.PromptResponse, error) {
197202
if s.globalOptions.NoPrompt {
198203
if req.Options.Required && req.Options.DefaultValue == "" {
199-
return nil, fmt.Errorf("no default response for prompt '%s'", req.Options.Message)
204+
return nil, &input.PromptRequiredError{
205+
PromptMessage: req.Options.Message,
206+
}
200207
} else {
201208
return &azdext.PromptResponse{
202209
Value: req.Options.DefaultValue,
@@ -235,7 +242,10 @@ func (s *promptService) PromptSubscription(
235242
ctx context.Context,
236243
req *azdext.PromptSubscriptionRequest,
237244
) (*azdext.PromptSubscriptionResponse, error) {
238-
// Delegate to prompt service which handles --no-prompt mode
245+
if s.globalOptions.NoPrompt {
246+
return nil, &input.PromptRequiredError{PromptMessage: promptSubscriptionMessage(req)}
247+
}
248+
239249
release, err := s.acquirePromptLock(ctx)
240250
if err != nil {
241251
return nil, err
@@ -267,7 +277,10 @@ func (s *promptService) PromptLocation(
267277
ctx context.Context,
268278
req *azdext.PromptLocationRequest,
269279
) (*azdext.PromptLocationResponse, error) {
270-
// Delegate to prompt service which handles --no-prompt mode
280+
if s.globalOptions.NoPrompt {
281+
return nil, &input.PromptRequiredError{PromptMessage: "Select location"}
282+
}
283+
271284
release, err := s.acquirePromptLock(ctx)
272285
if err != nil {
273286
return nil, err
@@ -306,7 +319,10 @@ func (s *promptService) PromptResourceGroup(
306319
ctx context.Context,
307320
req *azdext.PromptResourceGroupRequest,
308321
) (*azdext.PromptResourceGroupResponse, error) {
309-
// Delegate to prompt service which handles --no-prompt mode
322+
if s.globalOptions.NoPrompt {
323+
return nil, &input.PromptRequiredError{PromptMessage: promptResourceGroupMessage(req)}
324+
}
325+
310326
release, err := s.acquirePromptLock(ctx)
311327
if err != nil {
312328
return nil, err
@@ -340,7 +356,10 @@ func (s *promptService) PromptSubscriptionResource(
340356
ctx context.Context,
341357
req *azdext.PromptSubscriptionResourceRequest,
342358
) (*azdext.PromptSubscriptionResourceResponse, error) {
343-
// Delegate to prompt service which handles --no-prompt mode
359+
if s.globalOptions.NoPrompt {
360+
return nil, &input.PromptRequiredError{PromptMessage: promptResourceMessage(req.Options)}
361+
}
362+
344363
release, err := s.acquirePromptLock(ctx)
345364
if err != nil {
346365
return nil, err
@@ -374,7 +393,10 @@ func (s *promptService) PromptResourceGroupResource(
374393
ctx context.Context,
375394
req *azdext.PromptResourceGroupResourceRequest,
376395
) (*azdext.PromptResourceGroupResourceResponse, error) {
377-
// Delegate to prompt service which handles --no-prompt mode
396+
if s.globalOptions.NoPrompt {
397+
return nil, &input.PromptRequiredError{PromptMessage: promptResourceMessage(req.Options)}
398+
}
399+
378400
release, err := s.acquirePromptLock(ctx)
379401
if err != nil {
380402
return nil, err
@@ -432,7 +454,7 @@ func (s *promptService) createAzureContext(wire *azdext.AzureContext) (*prompt.A
432454

433455
resourceList := prompt.NewAzureResourceList(s.resourceService, resources)
434456

435-
return prompt.NewAzureContext(s.prompter, scope, resourceList), nil
457+
return prompt.NewAzureContext(s.prompter, scope, resourceList, s.globalOptions.NoPrompt), nil
436458
}
437459

438460
func createResourceOptions(options *azdext.PromptResourceOptions) prompt.ResourceOptions {
@@ -472,6 +494,44 @@ func createResourceOptions(options *azdext.PromptResourceOptions) prompt.Resourc
472494
return resourceOptions
473495
}
474496

497+
func promptSubscriptionMessage(req *azdext.PromptSubscriptionRequest) string {
498+
if req != nil && req.Message != "" {
499+
return req.Message
500+
}
501+
502+
return "Select subscription"
503+
}
504+
505+
func promptResourceGroupMessage(req *azdext.PromptResourceGroupRequest) string {
506+
if req != nil &&
507+
req.Options != nil &&
508+
req.Options.SelectOptions != nil &&
509+
req.Options.SelectOptions.Message != "" {
510+
return req.Options.SelectOptions.Message
511+
}
512+
513+
return "Select resource group"
514+
}
515+
516+
func promptResourceMessage(options *azdext.PromptResourceOptions) string {
517+
if options != nil &&
518+
options.SelectOptions != nil &&
519+
options.SelectOptions.Message != "" {
520+
return options.SelectOptions.Message
521+
}
522+
523+
resourceName := "resource"
524+
if options != nil {
525+
if options.ResourceTypeDisplayName != "" {
526+
resourceName = options.ResourceTypeDisplayName
527+
} else if options.ResourceType != "" {
528+
resourceName = options.ResourceType
529+
}
530+
}
531+
532+
return fmt.Sprintf("Select %s", resourceName)
533+
}
534+
475535
func createResourceGroupOptions(options *azdext.PromptResourceGroupOptions) *prompt.ResourceGroupOptions {
476536
if options == nil || options.SelectOptions == nil {
477537
return nil

cli/azd/internal/grpcserver/prompt_service_test.go

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import (
1515
"github.com/azure/azure-dev/cli/azd/pkg/azapi"
1616
"github.com/azure/azure-dev/cli/azd/pkg/azdext"
1717
"github.com/azure/azure-dev/cli/azd/pkg/extensions"
18+
"github.com/azure/azure-dev/cli/azd/pkg/input"
1819
"github.com/azure/azure-dev/cli/azd/pkg/prompt"
1920
"github.com/azure/azure-dev/cli/azd/pkg/ux"
2021
"github.com/azure/azure-dev/cli/azd/test/mocks/mockprompt"
@@ -51,7 +52,7 @@ func Test_PromptService_Confirm_NoPromptWithoutDefault(t *testing.T) {
5152
})
5253

5354
require.Error(t, err)
54-
require.Contains(t, err.Error(), "no default response")
55+
requirePromptRequiredError(t, err, "Continue?")
5556
}
5657

5758
func Test_PromptService_Select_NoPromptWithDefault(t *testing.T) {
@@ -88,7 +89,7 @@ func Test_PromptService_Select_NoPromptWithoutDefault(t *testing.T) {
8889
})
8990

9091
require.Error(t, err)
91-
require.Contains(t, err.Error(), "no default selection")
92+
requirePromptRequiredError(t, err, "Choose option:")
9293
}
9394

9495
func Test_PromptService_MultiSelect_NoPrompt(t *testing.T) {
@@ -140,7 +141,7 @@ func Test_PromptService_Prompt_NoPromptRequiredWithoutDefault(t *testing.T) {
140141
})
141142

142143
require.Error(t, err)
143-
require.Contains(t, err.Error(), "no default response")
144+
requirePromptRequiredError(t, err, "Enter name:")
144145
}
145146

146147
func Test_PromptService_Prompt_NoPromptNotRequiredWithoutDefault(t *testing.T) {
@@ -158,6 +159,17 @@ func Test_PromptService_Prompt_NoPromptNotRequiredWithoutDefault(t *testing.T) {
158159
require.Equal(t, "", resp.Value)
159160
}
160161

162+
func requirePromptRequiredError(t *testing.T, err error, expectedPromptMessage string) *input.PromptRequiredError {
163+
t.Helper()
164+
165+
promptErr, ok := errors.AsType[*input.PromptRequiredError](err)
166+
require.True(t, ok)
167+
require.Empty(t, promptErr.Inputs)
168+
require.Equal(t, expectedPromptMessage, promptErr.PromptMessage)
169+
170+
return promptErr
171+
}
172+
161173
func Test_PromptService_PromptSubscription(t *testing.T) {
162174
mockPrompter := &mockprompt.MockPromptService{}
163175
globalOptions := &internal.GlobalCommandOptions{NoPrompt: false}

cli/azd/pkg/contracts/envelope.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,3 +16,13 @@ type EventEnvelope struct {
1616
Timestamp time.Time `json:"timestamp"`
1717
Data any `json:"data"`
1818
}
19+
20+
// ErrorEnvelope is the standard envelope for error returns.
21+
type ErrorEnvelope[D any] struct {
22+
// Code is a machine-readable error code.
23+
Code string `json:"code"`
24+
// Message is a human-readable error message.
25+
Message string `json:"message"`
26+
// Details contains additional error details.
27+
Details D `json:"details"`
28+
}

0 commit comments

Comments
 (0)