diff --git a/config_test.go b/config_test.go index c6b5005..7171943 100644 --- a/config_test.go +++ b/config_test.go @@ -255,3 +255,32 @@ func TestRequiredConfigSelectorOnlyAppliesToSelectedScopes(t *testing.T) { } } } + +func TestUnknownConfigScopeReportsChoicesWithoutRuntimeWork(t *testing.T) { + for _, tc := range []struct { + names []string + scope []string + choices string + }{ + {[]string{"promptkit"}, nil, "available: promptkit"}, + {[]string{"zeta", "alpha", "promptkit", "app"}, []string{"--scope", "unknown"}, "available: app, promptkit, alpha, zeta"}, + } { + runtimeCalls := 0 + suite := ConfigSuite[struct{}]{NewContext: func() struct{} { runtimeCalls++; return struct{}{} }} + for _, name := range tc.names { + suite.Scopes = append(suite.Scopes, ConfigScope[struct{}]{Name: name, Operations: []ConfigOperation{ConfigValidate}, Acquire: func(context.Context, ConfigRequest, struct{}) (Acquired[ConfigView], error) { + runtimeCalls++ + return Acquired[ConfigView]{}, nil + }}) + } + app, _ := New(Identity{Name: "example"}) + if err := RegisterConfig(app, suite); err != nil { + t.Fatal(err) + } + var out, diagnostics bytes.Buffer + result := app.Execute(context.Background(), append([]string{"config", "validate"}, tc.scope...), Streams{Out: &out, Err: &diagnostics}) + if result.Kind != OutcomeUsage || runtimeCalls != 0 || out.Len() != 0 || !strings.Contains(diagnostics.String(), tc.choices) { + t.Fatalf("result=%#v calls=%d out=%q diagnostics=%q", result, runtimeCalls, out.String(), diagnostics.String()) + } + } +} diff --git a/docs/roadmap/remediation.md b/docs/roadmap/remediation.md index 6033fc5..a25a2a3 100644 --- a/docs/roadmap/remediation.md +++ b/docs/roadmap/remediation.md @@ -66,6 +66,9 @@ architecture. The issues below affect behavior within the implemented structure. - R-07 complete: nil diagnostic writers are no-output entries, do not count as delivery attempts, and cannot suppress cause presentation. +- R-08 complete: unknown scopes and absent default app scopes report available + choices in standard scope order without runtime acquisition. + ## Findings and priority P1 means a supported invocation is blocked or its report content is corrupted. diff --git a/selectors.go b/selectors.go index b564986..385daad 100644 --- a/selectors.go +++ b/selectors.go @@ -118,7 +118,11 @@ func selectConfigScopes[C any](suite ConfigSuite[C], input configInput[C], opera } else { scope, ok := findConfigScope(suite.Scopes, input.Scope) if !ok { - return nil, fmt.Errorf("unknown configuration scope %q", input.Scope) + choices := make([]string, 0, len(suite.Scopes)) + for _, available := range orderedConfigScopes(suite.Scopes) { + choices = append(choices, available.Name) + } + return nil, fmt.Errorf("unknown configuration scope %q (available: %s)", input.Scope, joinStrings(choices)) } if !containsOperation(scope.Operations, operation) { return nil, unsupportedConfigOperation(scope, operation)