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

@@ -1,6 +1,6 @@
# Invocation reporting implementation plan
Status: Stages 1–10 complete. Stage 11 is next.
Status: Stages 1–11 complete. Stage 12 is next.
## Objective and authority
@@ -418,7 +418,7 @@ migration; Stage 20 will validate the assembled modules.
## Stage 11: Migrate configuration show and validate
Status: pending.
Status: complete.
Primary files: [show.go](../../integrations/ckconfig/show.go),
[validate.go](../../integrations/ckconfig/validate.go), and
@@ -449,6 +449,15 @@ deferred context ordering, rendering failures, and report write failures.
Update tests of superseded outcome fields to observable reports and
`Presentation`, rather than dropping their original failure protection.
Configuration show and validate now submit provider messages through the shared
reporter and render results through managed output. Tests retain acquisition
ordering, shared state, YAML framing, cancellation with partial validation
output, and cover warning visibility, terminal context, quiet rendering, and
destination failures through `Presentation`. The ckconfig tests remain
unrunnable because `sources.go`, `explain.go`, `diff.go`, and family wiring and
tests still reference APIs scheduled for later migrations; Stage 20 will run the
assembled suites.
## Stage 12: Migrate configuration sources, explain, and diff
Status: pending.

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) {