Render ConfigKit show and validate through reporter

This commit is contained in:
2026-10-26 03:16:02 +00:00
parent 9a4d1db955
commit 5f37aac70f
5 changed files with 235 additions and 110 deletions

View File

@@ -161,7 +161,7 @@ func TestValidationPreparesIndependentScopeInputsAndOneSharedState(t *testing.T)
}
app := newConfigTestApp(t, Command(suite))
var output, diagnostic bytes.Buffer
outcome := app.Execute(context.Background(), []string{"config", "validate", "--scope", "all", "--config", "chosen.yaml", "--campaign", "spring"}, commandkit.Streams{Out: &output, Err: &diagnostic})
outcome := app.Execute(context.Background(), []string{"config", "validate", "--scope", "all", "--config", "chosen.yaml", "--campaign", "spring", "--verbose"}, commandkit.Streams{Out: &output, Err: &diagnostic})
if outcome.Kind != commandkit.OutcomeSuccess || !strings.Contains(diagnostic.String(), "app warning") || !strings.Contains(diagnostic.String(), "promptkit warning") {
t.Fatalf("validation outcome=%#v diagnostics=%q", outcome, diagnostic.String())
}
@@ -289,7 +289,7 @@ func TestShowAcquiresAllViewsBeforeRenderingAndFramesNativeYAML(t *testing.T) {
makeScope("promptkit", "---\nname: promptkit\n", providerErr),
}}))
outcome := app.Execute(context.Background(), []string{"config", "show", "--scope", "all"}, commandkit.Streams{Out: &output, Err: io.Discard})
if outcome.Kind != commandkit.OutcomeOperational || outcome.Cause != providerErr || output.Len() != 0 {
if outcome.Kind != commandkit.OutcomeOperational || outcome.Cause != providerErr || output.Len() != 0 || outcome.Presentation.Output.State != commandkit.DeliveryNone {
t.Fatalf("aggregate provider failure outcome=%#v output=%q", outcome, output.String())
}
if len(acquisitionOutputLengths) != 2 || acquisitionOutputLengths[0] != 0 || acquisitionOutputLengths[1] != 0 {
@@ -303,7 +303,7 @@ func TestShowAcquiresAllViewsBeforeRenderingAndFramesNativeYAML(t *testing.T) {
makeScope("promptkit", "name: promptkit\n", nil),
}}))
outcome = app.Execute(context.Background(), []string{"config", "show", "--scope", "all"}, commandkit.Streams{Out: &output, Err: io.Discard})
if outcome.Kind != commandkit.OutcomeSuccess {
if outcome.Kind != commandkit.OutcomeSuccess || outcome.Presentation.Output.State != commandkit.DeliveryComplete {
t.Fatalf("aggregate show outcome = %#v", outcome)
}
want := "---\n# scope: app\nname: app\n---\n# scope: promptkit\nname: promptkit\n"
@@ -312,6 +312,109 @@ func TestShowAcquiresAllViewsBeforeRenderingAndFramesNativeYAML(t *testing.T) {
}
}
func TestConfigShowSeparatesWarningsFromTerminalContextAcrossModes(t *testing.T) {
providerErr := errors.New("private provider cause")
app := newConfigTestApp(t, Command(Suite[struct{}, struct{}]{Scopes: []Scope[struct{}, struct{}]{{
Name: "app", Operations: []Operation{Show}, Acquire: func(context.Context, Request, struct{}, struct{}) (commandkit.Acquired[View], error) {
return commandkit.Acquired[View]{
Diagnostics: []commandkit.Diagnostic{commandkit.MessageDiagnostic(commandkit.SeverityWarning, "independent warning")},
Explanation: commandkit.Explanation{Details: []commandkit.Detail{func(writer io.Writer) error {
_, err := io.WriteString(writer, "native terminal context\n")
return err
}}},
}, providerErr
},
}}}))
for _, test := range []struct {
name string
flags []string
wantWarning bool
wantTerminal bool
}{
{name: "ordinary", wantTerminal: true},
{name: "verbose", flags: []string{"--verbose"}, wantWarning: true, wantTerminal: true},
{name: "quiet", flags: []string{"--quiet"}, wantTerminal: true},
} {
t.Run(test.name, func(t *testing.T) {
args := append([]string{"config", "show"}, test.flags...)
var output, diagnostic bytes.Buffer
outcome := app.Execute(context.Background(), args, commandkit.Streams{Out: &output, Err: &diagnostic})
if outcome.Kind != commandkit.OutcomeOperational || outcome.Cause != providerErr {
t.Fatalf("provider failure outcome = %#v", outcome)
}
if got := strings.Contains(diagnostic.String(), "independent warning"); got != test.wantWarning {
t.Fatalf("warning visibility = %v, output %q", got, diagnostic.String())
}
if got := strings.Contains(diagnostic.String(), "native terminal context"); got != test.wantTerminal {
t.Fatalf("terminal context visibility = %v, output %q", got, diagnostic.String())
}
if strings.Contains(diagnostic.String(), providerErr.Error()) {
t.Fatalf("cause text was duplicated into presentation: %q", diagnostic.String())
}
if test.wantWarning && strings.Index(diagnostic.String(), "independent warning") > strings.Index(diagnostic.String(), "native terminal context") {
t.Fatalf("deferred warning did not precede terminal context: %q", diagnostic.String())
}
})
}
}
func TestConfigShowAndValidationUseManagedRendering(t *testing.T) {
t.Run("quiet show still records native render errors", func(t *testing.T) {
renderErr := errors.New("native renderer failed")
app := newConfigTestApp(t, Command(Suite[struct{}, struct{}]{Scopes: []Scope[struct{}, struct{}]{{
Name: "app", Operations: []Operation{Show}, Acquire: func(context.Context, Request, struct{}, struct{}) (commandkit.Acquired[View], error) {
return commandkit.Acquired[View]{Value: View{Effective: failedEffective{err: renderErr}}}, nil
},
}}}))
var output, diagnostic bytes.Buffer
outcome := app.Execute(context.Background(), []string{"config", "show", "--quiet"}, commandkit.Streams{Out: &output, Err: &diagnostic})
if outcome.Kind != commandkit.OutcomeOperational || !errors.Is(outcome.Presentation.Output.RenderError, renderErr) || outcome.Cause != nil || output.Len() != 0 {
t.Fatalf("quiet render failure outcome=%#v output=%q", outcome, output.String())
}
})
for _, operation := range []Operation{Show, Validate} {
t.Run(string(operation)+" write failure", func(t *testing.T) {
writeErr := errors.New("report destination failed")
app := newConfigTestApp(t, Command(Suite[struct{}, struct{}]{Scopes: []Scope[struct{}, struct{}]{{
Name: "app",
Operations: []Operation{operation},
Acquire: func(context.Context, Request, struct{}, struct{}) (commandkit.Acquired[View], error) {
return commandkit.Acquired[View]{Value: View{Effective: effectiveValue("value: ready\n")}}, nil
},
}}}))
commandName := "show"
if operation == Validate {
commandName = "validate"
}
outcome := app.Execute(context.Background(), []string{"config", commandName}, commandkit.Streams{Out: failingOutput{err: writeErr}, Err: io.Discard})
if outcome.Kind != commandkit.OutcomeOperational || !errors.Is(outcome.Presentation.Output.WriteError, writeErr) || outcome.Cause != nil {
t.Fatalf("report write failure outcome = %#v", outcome)
}
})
t.Run(string(operation)+" quiet suppression", func(t *testing.T) {
writeErr := errors.New("quiet destination should not be used")
app := newConfigTestApp(t, Command(Suite[struct{}, struct{}]{Scopes: []Scope[struct{}, struct{}]{{
Name: "app",
Operations: []Operation{operation},
Acquire: func(context.Context, Request, struct{}, struct{}) (commandkit.Acquired[View], error) {
return commandkit.Acquired[View]{Value: View{Effective: effectiveValue("value: ready\n")}}, nil
},
}}}))
commandName := "show"
if operation == Validate {
commandName = "validate"
}
outcome := app.Execute(context.Background(), []string{"config", commandName, "--quiet"}, commandkit.Streams{Out: failingOutput{err: writeErr}, Err: io.Discard})
if outcome.Kind != commandkit.OutcomeSuccess || outcome.Presentation.Output.State != commandkit.DeliverySuppressed || outcome.Presentation.Output.WriteError != nil || outcome.Cause != nil {
t.Fatalf("quiet report outcome = %#v", outcome)
}
})
}
}
func TestSourcesReadsNativeInventoryOnceAndPreservesZeroValues(t *testing.T) {
reader := &sourceInventory{values: []configkit.SourceInfo{{
ID: "synthetic", Label: "", Path: "", Layer: configkit.Layer("base"), Kind: configkit.SourceKind("mapping"), UnderlayIndex: 0,
@@ -394,7 +497,7 @@ func TestReportWriteFailureTracksIncompleteDeliveryAndCancellation(t *testing.T)
}}))
var output bytes.Buffer
outcome = app.Execute(ctx, []string{"config", "validate", "--scope", "all"}, commandkit.Streams{Out: &output, Err: io.Discard})
if !errors.Is(outcome.Cause, context.Canceled) || !outcome.ReportIncomplete || calls != 2 || output.String() != "app: valid\n" {
if !errors.Is(outcome.Cause, context.Canceled) || outcome.Presentation.Output.State != commandkit.DeliveryComplete || calls != 2 || output.String() != "app: valid\n" {
t.Fatalf("cancelled aggregate validation outcome=%#v calls=%d output=%q", outcome, calls, output.String())
}
}
@@ -406,6 +509,12 @@ func (value effectiveValue) WriteEffective(writer io.Writer) error {
return err
}
type failedEffective struct{ err error }
func (value failedEffective) WriteEffective(io.Writer) error {
return value.err
}
type sourceInventory struct {
values []configkit.SourceInfo
calls int

View File

@@ -1,7 +1,6 @@
package ckconfig
import (
"errors"
"fmt"
commandkit "gitea.maximumdirect.net/eric/CommandKit"
@@ -9,10 +8,10 @@ import (
func runConfigOperation[I, S any](invocation commandkit.Invocation, operation Operation, input *configCommandInput[I, S]) commandkit.Outcome {
if input == nil || input.plan == nil {
return commandkit.Failure(errors.New("configuration operation was not prepared"))
return commandkit.Failure("Configuration operation was not prepared.")
}
if err := invocation.Context.Err(); err != nil {
return commandkit.Failure(err)
return configFailure("Configuration operation was canceled.", commandkit.Explanation{}, err)
}
plan := input.plan
var state S
@@ -31,19 +30,47 @@ func runConfigOperation[I, S any](invocation commandkit.Invocation, operation Op
case Diff:
return runConfigDiff(invocation, plan, state)
default:
return commandkit.Failure(fmt.Errorf("unsupported configuration operation %q", operation))
return commandkit.Failure(fmt.Sprintf("Unsupported configuration operation %q.", operation))
}
}
func configOperationFailure(cause error, diagnostics []commandkit.Diagnostic, reportStarted bool) commandkit.Outcome {
outcome := commandkit.Failure(cause)
outcome.Diagnostics = diagnostics
outcome.ReportIncomplete = reportStarted
func queueConfigDiagnostics(reporter *commandkit.Reporter, diagnostics []commandkit.Diagnostic) {
deferred := reporter.AtEnd()
for _, diagnostic := range diagnostics {
if diagnostic.Write == nil {
continue
}
switch diagnostic.Severity {
case commandkit.SeverityInfo:
_ = deferred.Info("", diagnostic.Write)
case commandkit.SeverityDebug:
_ = deferred.Debug("", diagnostic.Write)
case commandkit.SeverityProgress:
_ = reporter.Progress("", diagnostic.Write)
default:
_ = deferred.Warn("", diagnostic.Write)
}
}
}
func configFailure(message string, explanation commandkit.Explanation, cause error) commandkit.Outcome {
if explanation.Message != "" || len(explanation.Details) > 0 {
message = explanation.Message
}
outcome := commandkit.Failure(message, explanation.Details...)
if cause != nil {
outcome = outcome.WithCause(cause)
}
return outcome
}
func configDeliveryFailure(cause error, diagnostics []commandkit.Diagnostic, reportStarted bool) commandkit.Outcome {
outcome := configOperationFailure(cause, diagnostics, reportStarted)
outcome.DeliveryError = cause
return outcome
func configAcquisitionFailure(
reporter *commandkit.Reporter,
operation, scope string,
acquired commandkit.Acquired[View],
cause error,
) commandkit.Outcome {
queueConfigDiagnostics(reporter, acquired.Diagnostics)
message := fmt.Sprintf("Unable to %s configuration scope %q.", operation, scope)
return configFailure(message, acquired.Explanation, cause)
}

View File

@@ -2,7 +2,6 @@ package ckconfig
import (
"bytes"
"errors"
"fmt"
"io"
"strconv"
@@ -18,101 +17,76 @@ type effectiveDocument struct {
func runConfigShow[I, S any](invocation commandkit.Invocation, plan *configOperationPlan[I, S], state S) commandkit.Outcome {
documents := make([]effectiveDocument, 0, len(plan.scopes))
diagnostics := make([]commandkit.Diagnostic, 0)
reporter := invocation.Reporter()
for _, selected := range plan.scopes {
if err := invocation.Context.Err(); err != nil {
return configOperationFailure(err, diagnostics, false)
return configFailure("Configuration show was canceled.", commandkit.Explanation{}, err)
}
if selected.scope.scope.Acquire == nil {
return configOperationFailure(fmt.Errorf("configuration scope %q has no acquisition callback", selected.scope.scope.Name), diagnostics, false)
message := fmt.Sprintf("Configuration scope %q has no acquisition callback.", selected.scope.scope.Name)
return commandkit.Failure(message)
}
acquired, acquireErr := selected.scope.scope.Acquire(invocation.Context, Request{
Operation: Show,
Selection: plan.selection,
}, selected.input, state)
diagnostics = append(diagnostics, acquired.Diagnostics...)
if acquireErr != nil {
return configOperationFailure(acquireErr, diagnostics, false)
return configAcquisitionFailure(reporter, "show", selected.scope.scope.Name, acquired, acquireErr)
}
queueConfigDiagnostics(reporter, acquired.Diagnostics)
if err := invocation.Context.Err(); err != nil {
return configOperationFailure(err, diagnostics, false)
return configFailure("Configuration show was canceled.", acquired.Explanation, err)
}
if acquired.Value.Effective == nil {
return configOperationFailure(fmt.Errorf("configuration scope %q did not provide an effective configuration writer", selected.scope.scope.Name), diagnostics, false)
message := fmt.Sprintf("Configuration scope %q did not provide an effective configuration writer.", selected.scope.scope.Name)
return commandkit.Failure(message)
}
documents = append(documents, effectiveDocument{name: selected.scope.scope.Name, view: acquired.Value})
}
if err := invocation.Context.Err(); err != nil {
return configOperationFailure(err, diagnostics, false)
return configFailure("Configuration show was canceled.", commandkit.Explanation{}, err)
}
if len(documents) == 1 && plan.scopeName != "all" {
checked := &checkedWriter{writer: invocation.Streams.Out}
renderErr := documents[0].view.Effective.WriteEffective(checked)
if checked.failure != nil {
if renderErr == nil {
renderErr = checked.failure
} else {
renderErr = errors.Join(renderErr, checked.failure)
err := reporter.RenderOutput(func(writer io.Writer) error {
return documents[0].view.Effective.WriteEffective(writer)
})
if err != nil && invocation.Context.Err() != nil {
return configFailure("Configuration show was canceled.", commandkit.Explanation{}, invocation.Context.Err())
}
return commandkit.Success()
}
err := reporter.RenderOutput(func(writer io.Writer) error {
for index, document := range documents {
if err := invocation.Context.Err(); err != nil {
return err
}
var native bytes.Buffer
if err := document.view.Effective.WriteEffective(&native); err != nil {
return err
}
if err := invocation.Context.Err(); err != nil {
return err
}
framed, hasDirective := frameEffectiveYAML(document.name, native.Bytes())
if index > 0 && hasDirective {
if err := writeConfigReport(writer, []byte("...\n")); err != nil {
return err
}
}
if index < len(documents)-1 && len(framed) > 0 && framed[len(framed)-1] != '\n' {
framed = append(framed, '\n')
}
if err := writeConfigReport(writer, framed); err != nil {
return err
}
}
if renderErr != nil {
outcome := configOperationFailure(renderErr, diagnostics, true)
outcome.DeliveryError = checked.failure
return outcome
}
outcome := commandkit.Success()
outcome.Diagnostics = diagnostics
return outcome
return nil
})
if err != nil && invocation.Context.Err() != nil {
return configFailure("Configuration show was canceled.", commandkit.Explanation{}, invocation.Context.Err())
}
written := false
for index, document := range documents {
if err := invocation.Context.Err(); err != nil {
return configOperationFailure(err, diagnostics, written)
}
var native bytes.Buffer
if err := document.view.Effective.WriteEffective(&native); err != nil {
return configOperationFailure(err, diagnostics, written)
}
if err := invocation.Context.Err(); err != nil {
return configOperationFailure(err, diagnostics, written)
}
framed, hasDirective := frameEffectiveYAML(document.name, native.Bytes())
if index > 0 && hasDirective {
if err := writeConfigReport(invocation.Streams.Out, []byte("...\n")); err != nil {
return configDeliveryFailure(err, diagnostics, true)
}
}
if index < len(documents)-1 && len(framed) > 0 && framed[len(framed)-1] != '\n' {
framed = append(framed, '\n')
}
if err := writeConfigReport(invocation.Streams.Out, framed); err != nil {
return configDeliveryFailure(err, diagnostics, true)
}
written = true
}
outcome := commandkit.Success()
outcome.Diagnostics = diagnostics
return outcome
}
type checkedWriter struct {
writer io.Writer
failure error
}
func (writer *checkedWriter) Write(content []byte) (int, error) {
if writer.failure != nil {
return 0, writer.failure
}
written, err := writer.writer.Write(content)
if err == nil && written != len(content) {
err = io.ErrShortWrite
}
if err != nil {
writer.failure = err
}
return written, err
return commandkit.Success()
}
type yamlPreamble struct {

View File

@@ -3,6 +3,7 @@ package ckconfig
import (
"bytes"
"fmt"
"io"
"strings"
commandkit "gitea.maximumdirect.net/eric/CommandKit"
@@ -10,44 +11,49 @@ import (
)
func runConfigValidation[I, S any](invocation commandkit.Invocation, plan *configOperationPlan[I, S], state S) commandkit.Outcome {
diagnostics := make([]commandkit.Diagnostic, 0)
reporter := invocation.Reporter()
writtenRecord := false
for _, selected := range plan.scopes {
if err := invocation.Context.Err(); err != nil {
return configOperationFailure(err, diagnostics, writtenRecord)
return configFailure("Configuration validation was canceled.", commandkit.Explanation{}, err)
}
if selected.scope.scope.Acquire == nil {
return configOperationFailure(fmt.Errorf("configuration scope %q has no acquisition callback", selected.scope.scope.Name), diagnostics, writtenRecord)
message := fmt.Sprintf("Configuration scope %q has no acquisition callback.", selected.scope.scope.Name)
return commandkit.Failure(message)
}
acquired, acquireErr := selected.scope.scope.Acquire(invocation.Context, Request{
Operation: Validate,
Selection: plan.selection,
}, selected.input, state)
diagnostics = append(diagnostics, acquired.Diagnostics...)
if acquireErr != nil {
return configOperationFailure(acquireErr, diagnostics, writtenRecord)
return configAcquisitionFailure(reporter, "validate", selected.scope.scope.Name, acquired, acquireErr)
}
queueConfigDiagnostics(reporter, acquired.Diagnostics)
if err := invocation.Context.Err(); err != nil {
return configOperationFailure(err, diagnostics, writtenRecord)
return configFailure("Configuration validation was canceled.", acquired.Explanation, err)
}
record, err := validationSuccessRecord(selected.scope.scope.Name, plan.format)
if err != nil {
return configOperationFailure(err, diagnostics, writtenRecord)
}
if plan.format == "yaml" && !writtenRecord {
if err := writeConfigReport(invocation.Streams.Out, []byte("scopes:\n")); err != nil {
return configDeliveryFailure(err, diagnostics, true)
err := reporter.RenderOutput(func(writer io.Writer) error {
record, err := validationSuccessRecord(selected.scope.scope.Name, plan.format)
if err != nil {
return err
}
}
if err := writeConfigReport(invocation.Streams.Out, record); err != nil {
return configDeliveryFailure(err, diagnostics, true)
if plan.format == "yaml" && !writtenRecord {
if err := writeConfigReport(writer, []byte("scopes:\n")); err != nil {
return err
}
}
return writeConfigReport(writer, record)
})
if err != nil {
if ctxErr := invocation.Context.Err(); ctxErr != nil {
return configFailure("Configuration validation was canceled.", acquired.Explanation, ctxErr)
}
return commandkit.Success()
}
writtenRecord = true
}
outcome := commandkit.Success()
outcome.Diagnostics = diagnostics
return outcome
return commandkit.Success()
}
func validationSuccessRecord(scope, format string) ([]byte, error) {