Expose configuration metadata and findings
This commit is contained in:
@@ -297,8 +297,9 @@ example config scopes
|
|||||||
example config scopes --format yaml
|
example config scopes --format yaml
|
||||||
```
|
```
|
||||||
|
|
||||||
Text output uses one `scope: operation, operation` line per scope. YAML uses a
|
Text output uses one `scope: operation, operation` line per scope and lists any
|
||||||
`scopes` sequence with explicit `name` and `operations` fields. Scope order is
|
declared optional features. Structured output uses a `scopes` sequence with
|
||||||
|
`name`, `operations`, and any supported `features`. Scope order is
|
||||||
`app`, then `promptkit`, then other names alphabetically. A metadata-only scope
|
`app`, then `promptkit`, then other names alphabetically. A metadata-only scope
|
||||||
can be listed without an acquisition callback. Discovery does not construct a
|
can be listed without an acquisition callback. Discovery does not construct a
|
||||||
runtime context or acquire configuration. It reports the intersection between
|
runtime context or acquire configuration. It reports the intersection between
|
||||||
|
|||||||
@@ -249,7 +249,10 @@ adapts a typed ConfigKit loader without calling it during construction.
|
|||||||
The `config` family provides scope discovery, validation, safe effective configuration display,
|
The `config` family provides scope discovery, validation, safe effective configuration display,
|
||||||
source inventory, path explanation, and ConfigKit-backed comparison for the
|
source inventory, path explanation, and ConfigKit-backed comparison for the
|
||||||
capabilities declared by its scopes. The [CLI reference](../cli.md#configuration-scopes)
|
capabilities declared by its scopes. The [CLI reference](../cli.md#configuration-scopes)
|
||||||
owns operator syntax and report details.
|
owns operator syntax and report details. `Scope.Features` declares optional modes; acquired
|
||||||
|
`View.Metadata` and `View.Findings` carry native path metadata and diagnostic
|
||||||
|
records when available. `Findings.Messages` contains presentations associated
|
||||||
|
with those records, while `Acquired.Diagnostics` carries independent messages.
|
||||||
|
|
||||||
## PromptKit configuration, definitions, and requirements
|
## PromptKit configuration, definitions, and requirements
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# Shared reporting and PromptKit v0.20.0 implementation plan
|
# Shared reporting and PromptKit v0.20.0 implementation plan
|
||||||
|
|
||||||
Status: implementation in progress; Stages 1–13 complete.
|
Status: implementation in progress; Stages 1–14 complete.
|
||||||
|
|
||||||
## Scope, authority, and execution
|
## Scope, authority, and execution
|
||||||
|
|
||||||
@@ -491,6 +491,11 @@ conflicts. Metadata/help/completion remain independent of engine acquisition.
|
|||||||
|
|
||||||
## Stage 14: Metadata and failed-acquisition evidence facets
|
## Stage 14: Metadata and failed-acquisition evidence facets
|
||||||
|
|
||||||
|
**Complete.** Scope feature discovery and native metadata, findings, and
|
||||||
|
failed-load source snapshots are available to subsequent operation modes.
|
||||||
|
The root, ckconfig, and ckprompt workspace suites passed with Go 1.27.0,
|
||||||
|
including wrapped failures and independent diagnostic delivery.
|
||||||
|
|
||||||
Extend [ckconfig contracts](../../integrations/ckconfig/contracts.go) with
|
Extend [ckconfig contracts](../../integrations/ckconfig/contracts.go) with
|
||||||
`Scope.Features ScopeFeatures`: `PathDiscovery`, `PathShow`, and `Overrides`
|
`Scope.Features ScopeFeatures`: `PathDiscovery`, `PathShow`, and `Overrides`
|
||||||
booleans. These describe optional modes within existing commands, not new
|
booleans. These describe optional modes within existing commands, not new
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ func FromConfigKit[I, S, T any](
|
|||||||
Name: name,
|
Name: name,
|
||||||
Summary: summary,
|
Summary: summary,
|
||||||
Operations: []Operation{Validate, Show, Sources, Explain, Diff},
|
Operations: []Operation{Validate, Show, Sources, Explain, Diff},
|
||||||
|
Features: ScopeFeatures{PathDiscovery: true, PathShow: true},
|
||||||
}
|
}
|
||||||
if load == nil {
|
if load == nil {
|
||||||
return WithScopeError(scope, fmt.Errorf("ConfigKit scope %q loader must not be nil", name))
|
return WithScopeError(scope, fmt.Errorf("ConfigKit scope %q loader must not be nil", name))
|
||||||
@@ -34,22 +35,25 @@ func FromConfigKit[I, S, T any](
|
|||||||
scope.Acquire = func(ctx context.Context, request Request, input I, state S) (commandkit.Acquired[View], error) {
|
scope.Acquire = func(ctx context.Context, request Request, input I, state S) (commandkit.Acquired[View], error) {
|
||||||
result, err := load(ctx, request.Selection, input, state)
|
result, err := load(ctx, request.Selection, input, state)
|
||||||
acquired := commandkit.Acquired[View]{}
|
acquired := commandkit.Acquired[View]{}
|
||||||
if result != nil {
|
|
||||||
acquired.Value = View{Effective: result, Sources: result, Explanation: result}
|
|
||||||
acquired.Diagnostics, acquired.Explanation = configKitDiagnostics(name, result.Sources(), result.Diagnostics())
|
|
||||||
}
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
var loadError *configkit.LoadError
|
var loadError *configkit.LoadError
|
||||||
if errors.As(err, &loadError) && loadError != nil {
|
if errors.As(err, &loadError) && loadError != nil {
|
||||||
warnings, explanation := configKitDiagnostics(name, loadError.Sources(), loadError.Diagnostics())
|
records := loadError.Diagnostics()
|
||||||
acquired.Diagnostics = append(acquired.Diagnostics, warnings...)
|
messages, explanation := configKitDiagnostics(name, loadError.Sources(), records)
|
||||||
acquired.Explanation = appendConfigKitExplanation(acquired.Explanation, explanation)
|
acquired.Value.Sources = loadError
|
||||||
|
acquired.Value.Findings = &Findings{Records: records, Messages: messages}
|
||||||
|
acquired.Explanation = explanation
|
||||||
}
|
}
|
||||||
return acquired, err
|
return acquired, err
|
||||||
}
|
}
|
||||||
if result == nil {
|
if result == nil {
|
||||||
return commandkit.Acquired[View]{}, fmt.Errorf("ConfigKit scope %q returned a nil result", name)
|
return commandkit.Acquired[View]{}, fmt.Errorf("ConfigKit scope %q returned a nil result", name)
|
||||||
}
|
}
|
||||||
|
metadata := result.Metadata()
|
||||||
|
records := result.Diagnostics()
|
||||||
|
messages, explanation := configKitDiagnostics(name, result.Sources(), records)
|
||||||
|
acquired.Value = View{Effective: result, Sources: result, Explanation: result, Metadata: &metadata, Findings: &Findings{Records: records, Messages: messages}}
|
||||||
|
acquired.Explanation = explanation
|
||||||
return acquired, nil
|
return acquired, nil
|
||||||
}
|
}
|
||||||
scope.Compare = func(ctx context.Context, request ComparisonRequest[I], state S) (commandkit.Acquired[[]configkit.Difference], error) {
|
scope.Compare = func(ctx context.Context, request ComparisonRequest[I], state S) (commandkit.Acquired[[]configkit.Difference], error) {
|
||||||
@@ -99,11 +103,6 @@ func loadComparisonSide[I, S, T any](
|
|||||||
result, err := load(ctx, request.Selection, request.Input, state)
|
result, err := load(ctx, request.Selection, request.Input, state)
|
||||||
var diagnostics []commandkit.Diagnostic
|
var diagnostics []commandkit.Diagnostic
|
||||||
var explanation commandkit.Explanation
|
var explanation commandkit.Explanation
|
||||||
if result != nil {
|
|
||||||
warnings, terminal := configKitComparisonDiagnostics(scope, side, result.Sources(), result.Diagnostics())
|
|
||||||
diagnostics = append(diagnostics, warnings...)
|
|
||||||
explanation = appendConfigKitExplanation(explanation, terminal)
|
|
||||||
}
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
var loadError *configkit.LoadError
|
var loadError *configkit.LoadError
|
||||||
if errors.As(err, &loadError) && loadError != nil {
|
if errors.As(err, &loadError) && loadError != nil {
|
||||||
@@ -116,6 +115,7 @@ func loadComparisonSide[I, S, T any](
|
|||||||
if result == nil {
|
if result == nil {
|
||||||
return nil, diagnostics, explanation, fmt.Errorf("ConfigKit scope %q %s loader returned a nil result", scope, side)
|
return nil, diagnostics, explanation, fmt.Errorf("ConfigKit scope %q %s loader returned a nil result", scope, side)
|
||||||
}
|
}
|
||||||
|
diagnostics, explanation = configKitComparisonDiagnostics(scope, side, result.Sources(), result.Diagnostics())
|
||||||
return result, diagnostics, explanation, nil
|
return result, diagnostics, explanation, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -83,6 +83,29 @@ type View struct {
|
|||||||
Effective EffectiveWriter
|
Effective EffectiveWriter
|
||||||
Sources SourceReader
|
Sources SourceReader
|
||||||
Explanation ExplanationReader
|
Explanation ExplanationReader
|
||||||
|
// Metadata supplies native configuration path metadata when available.
|
||||||
|
Metadata *configkit.Metadata
|
||||||
|
// Findings supplies native diagnostics and their ordinary presentations.
|
||||||
|
// Nil means unavailable; a nonnil value may contain no records.
|
||||||
|
Findings *Findings
|
||||||
|
}
|
||||||
|
|
||||||
|
// Findings associates native diagnostic records with their ordinary messages.
|
||||||
|
type Findings struct {
|
||||||
|
// Records contains the native diagnostic records, which may be empty.
|
||||||
|
Records []configkit.Diagnostic
|
||||||
|
// Messages contains ordinary presentations associated with Records.
|
||||||
|
Messages []commandkit.Diagnostic
|
||||||
|
}
|
||||||
|
|
||||||
|
// ScopeFeatures describes optional modes within configuration operations.
|
||||||
|
type ScopeFeatures struct {
|
||||||
|
// PathDiscovery indicates support for metadata-backed path discovery.
|
||||||
|
PathDiscovery bool
|
||||||
|
// PathShow indicates support for displaying a selected effective path.
|
||||||
|
PathShow bool
|
||||||
|
// Overrides indicates that the loader accepts explicit configuration overrides.
|
||||||
|
Overrides bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// Scope declares the operations and providers for one named configuration.
|
// Scope declares the operations and providers for one named configuration.
|
||||||
@@ -97,6 +120,8 @@ type Scope[I, S any] struct {
|
|||||||
Summary string
|
Summary string
|
||||||
// Operations selects the standard handlers exposed for this scope.
|
// Operations selects the standard handlers exposed for this scope.
|
||||||
Operations []Operation
|
Operations []Operation
|
||||||
|
// Features describes optional modes supported by this scope.
|
||||||
|
Features ScopeFeatures
|
||||||
// Acquire loads the selected configuration for one ordinary inspection
|
// Acquire loads the selected configuration for one ordinary inspection
|
||||||
// invocation. It returns independent diagnostics and terminal context
|
// invocation. It returns independent diagnostics and terminal context
|
||||||
// separately from the native value and optional operation error.
|
// separately from the native value and optional operation error.
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
"reflect"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
@@ -94,7 +95,7 @@ func TestFromConfigKitPassesInvocationValuesAndNativeFacets(t *testing.T) {
|
|||||||
if gotContext != ctx || gotSelection != selection || gotInput.Label != "typed" || gotState != stateValue || stateValue.Calls != 1 {
|
if gotContext != ctx || gotSelection != selection || gotInput.Label != "typed" || gotState != stateValue || stateValue.Calls != 1 {
|
||||||
t.Fatalf("loader arguments/context/state = %v, %#v, %#v, %p, %d", gotContext, gotSelection, gotInput, gotState, stateValue.Calls)
|
t.Fatalf("loader arguments/context/state = %v, %#v, %#v, %p, %d", gotContext, gotSelection, gotInput, gotState, stateValue.Calls)
|
||||||
}
|
}
|
||||||
if acquired.Value.Effective != result || acquired.Value.Sources != result || acquired.Value.Explanation != result {
|
if acquired.Value.Effective != result || acquired.Value.Sources != result || acquired.Value.Explanation != result || acquired.Value.Metadata == nil || acquired.Value.Findings == nil {
|
||||||
t.Fatal("adapter did not expose the native result facets directly")
|
t.Fatal("adapter did not expose the native result facets directly")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -150,14 +151,14 @@ func TestFromConfigKitRetainsLoadErrorAndDiagnostics(t *testing.T) {
|
|||||||
if !errors.As(gotErr, &typed) {
|
if !errors.As(gotErr, &typed) {
|
||||||
t.Fatalf("load error type was not retained: %T", gotErr)
|
t.Fatalf("load error type was not retained: %T", gotErr)
|
||||||
}
|
}
|
||||||
if len(acquired.Diagnostics) != 1 || acquired.Diagnostics[0].Severity != commandkit.SeverityWarning {
|
if acquired.Value.Effective != nil || acquired.Value.Metadata != nil || acquired.Value.Sources == nil || acquired.Value.Findings == nil || len(acquired.Value.Findings.Messages) != 1 || acquired.Value.Findings.Messages[0].Severity != commandkit.SeverityWarning {
|
||||||
t.Fatalf("failure diagnostics = %#v", acquired.Diagnostics)
|
t.Fatalf("failure evidence = %#v", acquired.Value)
|
||||||
}
|
}
|
||||||
if acquired.Explanation.Message != "" || len(acquired.Explanation.Details) != 1 {
|
if acquired.Explanation.Message != "" || len(acquired.Explanation.Details) != 1 {
|
||||||
t.Fatalf("failure explanation = %#v", acquired.Explanation)
|
t.Fatalf("failure explanation = %#v", acquired.Explanation)
|
||||||
}
|
}
|
||||||
var warning, terminal bytes.Buffer
|
var warning, terminal bytes.Buffer
|
||||||
if err := acquired.Diagnostics[0].Write(&warning); err != nil {
|
if err := acquired.Value.Findings.Messages[0].Write(&warning); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
if err := acquired.Explanation.Details[0](&terminal); err != nil {
|
if err := acquired.Explanation.Details[0](&terminal); err != nil {
|
||||||
@@ -174,7 +175,7 @@ func TestFromConfigKitRetainsLoadErrorAndDiagnostics(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestFromConfigKitPreservesValueAndDiagnosticsAlongsideError(t *testing.T) {
|
func TestFromConfigKitDoesNotExposeResultFromFailedLoad(t *testing.T) {
|
||||||
result, err := adapterSettingsLoad(configkit.Mapping("partial source", map[string]any{"greeting": "Hello"}))
|
result, err := adapterSettingsLoad(configkit.Mapping("partial source", map[string]any{"greeting": "Hello"}))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
@@ -188,11 +189,8 @@ func TestFromConfigKitPreservesValueAndDiagnosticsAlongsideError(t *testing.T) {
|
|||||||
if gotErr != loadErr {
|
if gotErr != loadErr {
|
||||||
t.Fatalf("load error identity = %v, want %v", gotErr, loadErr)
|
t.Fatalf("load error identity = %v, want %v", gotErr, loadErr)
|
||||||
}
|
}
|
||||||
if acquired.Value.Effective != result || acquired.Value.Sources != result || acquired.Value.Explanation != result {
|
if acquired.Value.Effective != nil || acquired.Value.Metadata != nil || acquired.Value.Sources != nil || acquired.Value.Explanation != nil || acquired.Value.Findings != nil {
|
||||||
t.Fatalf("acquired value facets were lost: %#v", acquired.Value)
|
t.Fatalf("failed load exposed result facets: %#v", acquired.Value)
|
||||||
}
|
|
||||||
if len(acquired.Diagnostics) != 1 || acquired.Diagnostics[0].Severity != commandkit.SeverityWarning {
|
|
||||||
t.Fatalf("independent diagnostics = %#v", acquired.Diagnostics)
|
|
||||||
}
|
}
|
||||||
if len(acquired.Explanation.Details) != 0 {
|
if len(acquired.Explanation.Details) != 0 {
|
||||||
t.Fatalf("warning-only result acquired terminal context: %#v", acquired.Explanation)
|
t.Fatalf("warning-only result acquired terminal context: %#v", acquired.Explanation)
|
||||||
@@ -274,3 +272,23 @@ func (shortWriter) Write(value []byte) (int, error) { return len(value) - 1, nil
|
|||||||
type failingWriter struct{ err error }
|
type failingWriter struct{ err error }
|
||||||
|
|
||||||
func (w failingWriter) Write([]byte) (int, error) { return 0, w.err }
|
func (w failingWriter) Write([]byte) (int, error) { return 0, w.err }
|
||||||
|
|
||||||
|
func TestFromConfigKitUsesFailureSnapshotInsteadOfReturnedResult(t *testing.T) {
|
||||||
|
result, err := adapterSettingsLoad(configkit.Mapping("successful", map[string]any{"greeting": "Hello"}))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
_, failure := configkit.Load[map[string]any](configkit.Bytes("failed", []byte("broken: [")))
|
||||||
|
var snapshot *configkit.LoadError
|
||||||
|
if !errors.As(failure, &snapshot) {
|
||||||
|
t.Fatalf("load failure: %v", failure)
|
||||||
|
}
|
||||||
|
wrapped := fmt.Errorf("loading: %w", failure)
|
||||||
|
scope := FromConfigKit[struct{}, struct{}, adapterSettings]("app", "", func(context.Context, Selection, struct{}, struct{}) (*configkit.Result[adapterSettings], error) {
|
||||||
|
return result, wrapped
|
||||||
|
})
|
||||||
|
acquired, gotErr := scope.Acquire(context.Background(), Request{}, struct{}{}, struct{}{})
|
||||||
|
if !errors.Is(gotErr, failure) || acquired.Value.Effective != nil || acquired.Value.Metadata != nil || acquired.Value.Explanation != nil || acquired.Value.Sources == nil || acquired.Value.Findings == nil || !reflect.DeepEqual(acquired.Value.Sources.Sources(), snapshot.Sources()) || !reflect.DeepEqual(acquired.Value.Findings.Records, snapshot.Diagnostics()) || len(acquired.Diagnostics) != 0 {
|
||||||
|
t.Fatalf("failed acquisition = %#v, %v", acquired, gotErr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ func runConfigExplanation[I, S any](invocation commandkit.Invocation, plan *conf
|
|||||||
Operation: Explain,
|
Operation: Explain,
|
||||||
Selection: plan.selection,
|
Selection: plan.selection,
|
||||||
}, selected.input, state)
|
}, selected.input, state)
|
||||||
queueConfigDiagnostics(reporter, acquired.Diagnostics)
|
queueConfigAcquisition(reporter, acquired)
|
||||||
if acquireErr != nil {
|
if acquireErr != nil {
|
||||||
return configFailure(fmt.Sprintf("Unable to explain configuration scope %q.", name), acquired.Explanation, acquireErr)
|
return configFailure(fmt.Sprintf("Unable to explain configuration scope %q.", name), acquired.Explanation, acquireErr)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import (
|
|||||||
type scopeMetadata struct {
|
type scopeMetadata struct {
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Operations []Operation `json:"operations"`
|
Operations []Operation `json:"operations"`
|
||||||
|
Features []string `json:"features,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type scopeInventory struct {
|
type scopeInventory struct {
|
||||||
@@ -188,6 +189,7 @@ func captureSuite[I, S any](suite Suite[I, S]) *preparedSuite[I, S] {
|
|||||||
prepared.inventory.Scopes = append(prepared.inventory.Scopes, scopeMetadata{
|
prepared.inventory.Scopes = append(prepared.inventory.Scopes, scopeMetadata{
|
||||||
Name: scope.scope.Name,
|
Name: scope.scope.Name,
|
||||||
Operations: append([]Operation(nil), scope.scope.Operations...),
|
Operations: append([]Operation(nil), scope.scope.Operations...),
|
||||||
|
Features: featureNames(scope.scope.Features),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
return prepared
|
return prepared
|
||||||
@@ -253,12 +255,32 @@ func joinNames(names []string) string {
|
|||||||
return strings.Join(ordered, ", ")
|
return strings.Join(ordered, ", ")
|
||||||
}
|
}
|
||||||
|
|
||||||
var scopeInventoryTemplate = template.Must(commandkit.ParseTextTemplate("scopes", `{{range .Scopes}}{{.Name}}: {{range $index, $operation := .Operations}}{{if $index}}, {{end}}{{$operation}}{{end}}
|
func featureNames(features ScopeFeatures) []string {
|
||||||
|
var names []string
|
||||||
|
if features.PathDiscovery {
|
||||||
|
names = append(names, "path_discovery")
|
||||||
|
}
|
||||||
|
if features.PathShow {
|
||||||
|
names = append(names, "path_show")
|
||||||
|
}
|
||||||
|
if features.Overrides {
|
||||||
|
names = append(names, "overrides")
|
||||||
|
}
|
||||||
|
return names
|
||||||
|
}
|
||||||
|
|
||||||
|
var scopeInventoryTemplate = template.Must(commandkit.ParseTextTemplate("scopes", `{{range .Scopes}}{{.Name}}: {{range $index, $operation := .Operations}}{{if $index}}, {{end}}{{$operation}}{{end}}{{if .Features}} (features: {{range $index, $feature := .Features}}{{if $index}}, {{end}}{{$feature}}{{end}}){{end}}
|
||||||
{{end}}`, nil))
|
{{end}}`, nil))
|
||||||
|
|
||||||
func newConfigScopesCommand[I, S any](prepared *preparedSuite[I, S]) commandkit.Command {
|
func newConfigScopesCommand[I, S any](prepared *preparedSuite[I, S]) commandkit.Command {
|
||||||
|
var description strings.Builder
|
||||||
|
for _, scope := range prepared.inventory.Scopes {
|
||||||
|
if len(scope.Features) > 0 {
|
||||||
|
fmt.Fprintf(&description, "%s: %s\n", scope.Name, strings.Join(scope.Features, ", "))
|
||||||
|
}
|
||||||
|
}
|
||||||
return commandkit.NewCommand(commandkit.CommandSpec[struct{}]{
|
return commandkit.NewCommand(commandkit.CommandSpec[struct{}]{
|
||||||
Name: "scopes", Summary: "List configuration scopes and supported operations.",
|
Name: "scopes", Summary: "List configuration scopes, operations, and supported features.", Description: description.String(),
|
||||||
Run: func(invocation commandkit.Invocation, _ struct{}) commandkit.Outcome {
|
Run: func(invocation commandkit.Invocation, _ struct{}) commandkit.Outcome {
|
||||||
_ = invocation.Reporter().Report(commandkit.Report{Value: prepared.inventory, TextTemplate: scopeInventoryTemplate})
|
_ = invocation.Reporter().Report(commandkit.Report{Value: prepared.inventory, TextTemplate: scopeInventoryTemplate})
|
||||||
return commandkit.Success()
|
return commandkit.Success()
|
||||||
|
|||||||
@@ -593,3 +593,30 @@ func newConfigTestApp(t *testing.T, command commandkit.Command) *commandkit.App
|
|||||||
}
|
}
|
||||||
return app
|
return app
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestScopeFeaturesAndAcquisitionMessages(t *testing.T) {
|
||||||
|
calls := 0
|
||||||
|
message := func(text string) commandkit.Diagnostic {
|
||||||
|
return commandkit.Diagnostic{Severity: commandkit.SeverityWarning, Write: func(w io.Writer) error { _, err := io.WriteString(w, text); return err }}
|
||||||
|
}
|
||||||
|
scope := Scope[struct{}, struct{}]{Name: "app", Operations: []Operation{Validate}, Features: ScopeFeatures{PathDiscovery: true, PathShow: true}, Acquire: func(context.Context, Request, struct{}, struct{}) (commandkit.Acquired[View], error) {
|
||||||
|
calls++
|
||||||
|
return commandkit.Acquired[View]{Value: View{Findings: &Findings{Messages: []commandkit.Diagnostic{message("native warning\n")}}}, Diagnostics: []commandkit.Diagnostic{message("application warning\n")}}, nil
|
||||||
|
}}
|
||||||
|
app := newConfigTestApp(t, Command(Suite[struct{}, struct{}]{Scopes: []Scope[struct{}, struct{}]{scope}}))
|
||||||
|
var out, diagnostic bytes.Buffer
|
||||||
|
result := app.Execute(context.Background(), []string{"config", "scopes", "--format=json"}, commandkit.Streams{Out: &out, Err: &diagnostic})
|
||||||
|
if result.Kind != commandkit.OutcomeSuccess || calls != 0 || !strings.Contains(out.String(), "path_discovery") || !strings.Contains(out.String(), "path_show") {
|
||||||
|
t.Fatalf("discovery = %#v %q", result, out.String())
|
||||||
|
}
|
||||||
|
out.Reset()
|
||||||
|
result = app.Execute(context.Background(), []string{"config", "scopes", "--help"}, commandkit.Streams{Out: &out, Err: &diagnostic})
|
||||||
|
if result.Kind != commandkit.OutcomeSuccess || calls != 0 || !strings.Contains(out.String(), "path_discovery") {
|
||||||
|
t.Fatalf("help = %#v %q", result, out.String())
|
||||||
|
}
|
||||||
|
out.Reset()
|
||||||
|
result = app.Execute(context.Background(), []string{"config", "validate", "--verbose"}, commandkit.Streams{Out: &out, Err: &diagnostic})
|
||||||
|
if result.Kind != commandkit.OutcomeSuccess || calls != 1 || strings.Count(diagnostic.String(), "native warning") != 1 || strings.Count(diagnostic.String(), "application warning") != 1 {
|
||||||
|
t.Fatalf("diagnostics = %#v %q", result, diagnostic.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -54,6 +54,13 @@ func queueConfigDiagnostics(reporter *commandkit.Reporter, diagnostics []command
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func queueConfigAcquisition(reporter *commandkit.Reporter, acquired commandkit.Acquired[View]) {
|
||||||
|
queueConfigDiagnostics(reporter, acquired.Diagnostics)
|
||||||
|
if acquired.Value.Findings != nil {
|
||||||
|
queueConfigDiagnostics(reporter, acquired.Value.Findings.Messages)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func configFailure(message string, explanation commandkit.Explanation, cause error) commandkit.Outcome {
|
func configFailure(message string, explanation commandkit.Explanation, cause error) commandkit.Outcome {
|
||||||
if explanation.Message != "" || len(explanation.Details) > 0 {
|
if explanation.Message != "" || len(explanation.Details) > 0 {
|
||||||
message = explanation.Message
|
message = explanation.Message
|
||||||
@@ -71,7 +78,7 @@ func configAcquisitionFailure(
|
|||||||
acquired commandkit.Acquired[View],
|
acquired commandkit.Acquired[View],
|
||||||
cause error,
|
cause error,
|
||||||
) commandkit.Outcome {
|
) commandkit.Outcome {
|
||||||
queueConfigDiagnostics(reporter, acquired.Diagnostics)
|
queueConfigAcquisition(reporter, acquired)
|
||||||
message := fmt.Sprintf("Unable to %s configuration scope %q.", operation, scope)
|
message := fmt.Sprintf("Unable to %s configuration scope %q.", operation, scope)
|
||||||
return configFailure(message, acquired.Explanation, cause)
|
return configFailure(message, acquired.Explanation, cause)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ func runConfigShow[I, S any](invocation commandkit.Invocation, plan *configOpera
|
|||||||
if acquireErr != nil {
|
if acquireErr != nil {
|
||||||
return configAcquisitionFailure(reporter, "show", selected.scope.scope.Name, acquired, acquireErr)
|
return configAcquisitionFailure(reporter, "show", selected.scope.scope.Name, acquired, acquireErr)
|
||||||
}
|
}
|
||||||
queueConfigDiagnostics(reporter, acquired.Diagnostics)
|
queueConfigAcquisition(reporter, acquired)
|
||||||
if err := invocation.Context.Err(); err != nil {
|
if err := invocation.Context.Err(); err != nil {
|
||||||
return configFailure("Configuration show was canceled.", acquired.Explanation, err)
|
return configFailure("Configuration show was canceled.", acquired.Explanation, err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -68,7 +68,7 @@ func runConfigSources[I, S any](invocation commandkit.Invocation, plan *configOp
|
|||||||
Operation: Sources,
|
Operation: Sources,
|
||||||
Selection: plan.selection,
|
Selection: plan.selection,
|
||||||
}, selected.input, state)
|
}, selected.input, state)
|
||||||
queueConfigDiagnostics(reporter, acquired.Diagnostics)
|
queueConfigAcquisition(reporter, acquired)
|
||||||
if acquireErr != nil {
|
if acquireErr != nil {
|
||||||
return configFailure(fmt.Sprintf("Unable to list sources for configuration scope %q.", name), acquired.Explanation, acquireErr)
|
return configFailure(fmt.Sprintf("Unable to list sources for configuration scope %q.", name), acquired.Explanation, acquireErr)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ func runConfigValidation[I, S any](invocation commandkit.Invocation, plan *confi
|
|||||||
outcome = configAcquisitionFailure(reporter, "validate", name, acquired, err)
|
outcome = configAcquisitionFailure(reporter, "validate", name, acquired, err)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
queueConfigDiagnostics(reporter, acquired.Diagnostics)
|
queueConfigAcquisition(reporter, acquired)
|
||||||
if err := invocation.Context.Err(); err != nil {
|
if err := invocation.Context.Err(); err != nil {
|
||||||
report.Scopes = append(report.Scopes, validationScope{Scope: name, Outcome: "canceled"})
|
report.Scopes = append(report.Scopes, validationScope{Scope: name, Outcome: "canceled"})
|
||||||
outcome = configFailure("Configuration validation was canceled.", acquired.Explanation, err)
|
outcome = configFailure("Configuration validation was canceled.", acquired.Explanation, err)
|
||||||
|
|||||||
@@ -267,6 +267,7 @@ func ConfigScope[I, S any](load func(context.Context, ckconfig.Selection, I, S)
|
|||||||
Name: "promptkit",
|
Name: "promptkit",
|
||||||
Summary: "PromptKit engine configuration.",
|
Summary: "PromptKit engine configuration.",
|
||||||
Operations: []ckconfig.Operation{ckconfig.Validate, ckconfig.Show, ckconfig.Sources, ckconfig.Explain, ckconfig.Diff},
|
Operations: []ckconfig.Operation{ckconfig.Validate, ckconfig.Show, ckconfig.Sources, ckconfig.Explain, ckconfig.Diff},
|
||||||
|
Features: ckconfig.ScopeFeatures{PathDiscovery: true, PathShow: true},
|
||||||
}
|
}
|
||||||
if load == nil {
|
if load == nil {
|
||||||
return ckconfig.WithScopeError(scope, errors.New("PromptKit configuration loader is nil"))
|
return ckconfig.WithScopeError(scope, errors.New("PromptKit configuration loader is nil"))
|
||||||
@@ -275,15 +276,25 @@ func ConfigScope[I, S any](load func(context.Context, ckconfig.Selection, I, S)
|
|||||||
scope.Acquire = func(ctx context.Context, request ckconfig.Request, input I, state S) (commandkit.Acquired[ckconfig.View], error) {
|
scope.Acquire = func(ctx context.Context, request ckconfig.Request, input I, state S) (commandkit.Acquired[ckconfig.View], error) {
|
||||||
_, inspection, messages, err := loadPromptKitEngine(ctx, request.Selection, input, state, "", load)
|
_, inspection, messages, err := loadPromptKitEngine(ctx, request.Selection, input, state, "", load)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return commandkit.Acquired[ckconfig.View]{Diagnostics: messages.diagnostics, Explanation: messages.explanation}, err
|
acquired := commandkit.Acquired[ckconfig.View]{Explanation: messages.explanation}
|
||||||
|
var loadError *configkit.LoadError
|
||||||
|
if errors.As(err, &loadError) && loadError != nil {
|
||||||
|
acquired.Value.Sources = loadError
|
||||||
|
acquired.Value.Findings = &ckconfig.Findings{Records: loadError.Diagnostics(), Messages: messages.diagnostics}
|
||||||
|
} else {
|
||||||
|
acquired.Diagnostics = messages.diagnostics
|
||||||
|
}
|
||||||
|
return acquired, err
|
||||||
}
|
}
|
||||||
|
metadata := inspection.Metadata()
|
||||||
return commandkit.Acquired[ckconfig.View]{
|
return commandkit.Acquired[ckconfig.View]{
|
||||||
Value: ckconfig.View{
|
Value: ckconfig.View{
|
||||||
Effective: inspection,
|
Effective: inspection,
|
||||||
Sources: inspection,
|
Sources: inspection,
|
||||||
Explanation: inspection,
|
Explanation: inspection,
|
||||||
|
Metadata: &metadata,
|
||||||
|
Findings: &ckconfig.Findings{Records: inspection.Diagnostics(), Messages: messages.diagnostics},
|
||||||
},
|
},
|
||||||
Diagnostics: messages.diagnostics,
|
|
||||||
Explanation: messages.explanation,
|
Explanation: messages.explanation,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -288,6 +288,9 @@ func TestConfigScopeUsesNativeInspectionFacetsAndInvocationValues(t *testing.T)
|
|||||||
if err != nil || loaderCalls != 1 {
|
if err != nil || loaderCalls != 1 {
|
||||||
t.Fatalf("scope acquisition = (%#v, %v), calls=%d", acquired, err, loaderCalls)
|
t.Fatalf("scope acquisition = (%#v, %v), calls=%d", acquired, err, loaderCalls)
|
||||||
}
|
}
|
||||||
|
if acquired.Value.Metadata == nil || acquired.Value.Findings == nil || !scope.Features.PathDiscovery || !scope.Features.PathShow {
|
||||||
|
t.Fatalf("missing native metadata or findings: %#v", acquired.Value)
|
||||||
|
}
|
||||||
var effective bytes.Buffer
|
var effective bytes.Buffer
|
||||||
if err := acquired.Value.Effective.WriteEffective(&effective); err != nil || !strings.Contains(effective.String(), "id: briefing") {
|
if err := acquired.Value.Effective.WriteEffective(&effective); err != nil || !strings.Contains(effective.String(), "id: briefing") {
|
||||||
t.Fatalf("native effective report = %q, %v", effective.String(), err)
|
t.Fatalf("native effective report = %q, %v", effective.String(), err)
|
||||||
@@ -931,3 +934,25 @@ type failingDiagnosticWriter struct{ err error }
|
|||||||
func (writer failingDiagnosticWriter) Write([]byte) (int, error) { return 0, writer.err }
|
func (writer failingDiagnosticWriter) Write([]byte) (int, error) { return 0, writer.err }
|
||||||
|
|
||||||
var _ io.Writer = failingDiagnosticWriter{}
|
var _ io.Writer = failingDiagnosticWriter{}
|
||||||
|
|
||||||
|
func TestConfigScopeRetainsFailedLoadSnapshot(t *testing.T) {
|
||||||
|
_, loadErr := configkit.Load[map[string]any](configkit.Bytes("broken", []byte("broken: [")))
|
||||||
|
if loadErr == nil {
|
||||||
|
t.Fatal("fixture unexpectedly loaded")
|
||||||
|
}
|
||||||
|
var snapshot *configkit.LoadError
|
||||||
|
if !errors.As(loadErr, &snapshot) {
|
||||||
|
t.Fatalf("unexpected load error: %v", loadErr)
|
||||||
|
}
|
||||||
|
wrapped := errors.Join(errors.New("loader context"), loadErr)
|
||||||
|
scope := ConfigScope(func(context.Context, ckconfig.Selection, struct{}, struct{}) (*promptkit.Engine, error) {
|
||||||
|
return nil, wrapped
|
||||||
|
})
|
||||||
|
got, err := scope.Acquire(context.Background(), ckconfig.Request{}, struct{}{}, struct{}{})
|
||||||
|
if !errors.Is(err, loadErr) || got.Value.Sources == nil || got.Value.Findings == nil || got.Value.Effective != nil || got.Value.Metadata != nil || got.Value.Explanation != nil || len(got.Diagnostics) != 0 {
|
||||||
|
t.Fatalf("failed acquisition = %#v, %v", got, err)
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(got.Value.Sources.Sources(), snapshot.Sources()) || !reflect.DeepEqual(got.Value.Findings.Records, snapshot.Diagnostics()) {
|
||||||
|
t.Fatalf("failed snapshot = %#v", got.Value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user