Reconcile reporting tests and boundaries

This commit is contained in:
2026-10-26 03:51:14 +00:00
parent 6264a87183
commit a6c0fb5b0d
7 changed files with 124 additions and 106 deletions

View File

@@ -32,7 +32,7 @@ func TestDebugEventsFollowModeAndFinalizationOrder(t *testing.T) {
for _, fragment := range []string{ for _, fragment := range []string{
"resolved command alias \"shortcut\" to \"run\"", "resolved command alias \"shortcut\" to \"run\"",
"selected debug reporting mode using --trace (alias for --debug)", "selected debug reporting mode using --trace (alias for --debug)",
"INFO: deferred summary", "deferred summary",
"handler completed for \"run\" with operational outcome and status 1", "handler completed for \"run\" with operational outcome and status 1",
"ERROR: operation failed", "ERROR: operation failed",
} { } {
@@ -46,7 +46,7 @@ func TestDebugEventsFollowModeAndFinalizationOrder(t *testing.T) {
ordered := []string{ ordered := []string{
"resolved command alias", "resolved command alias",
"selected debug reporting mode", "selected debug reporting mode",
"INFO: deferred summary", "deferred summary",
"handler completed", "handler completed",
"ERROR: operation failed", "ERROR: operation failed",
} }

View File

@@ -1,6 +1,6 @@
# Invocation reporting implementation plan # Invocation reporting implementation plan
Status: Stages 1–17 complete. Stage 18 is next. Status: Stages 1–18 complete. Stage 19 is next.
## Objective and authority ## Objective and authority
@@ -676,7 +676,7 @@ ConfigKit, aliased quiet help, and offline PromptKit smoke commands also pass.
## Stage 18: Reconcile the full test suite and architectural boundaries ## Stage 18: Reconcile the full test suite and architectural boundaries
Status: pending. Status: complete.
1. Find and migrate remaining callers of removed constructors, raw invocation 1. Find and migrate remaining callers of removed constructors, raw invocation
streams, terminal `SeverityError`, outcome diagnostic submission, and old streams, terminal `SeverityError`, outcome diagnostic submission, and old
@@ -704,6 +704,19 @@ Validation: the suite should now build as a whole using the local three-module
workspace. Focused reporter race testing is justified by the selected concurrent workspace. Focused reporter race testing is justified by the selected concurrent
reporter contract; it does not establish a new permanent repository-wide gate. reporter contract; it does not establish a new permanent repository-wide gate.
Completion: migrated the remaining core tests and benchmark caller to reporter,
explicit explanation/cause, and presentation-result APIs. Updated reporting
expectations for unprefixed Info, routed flag-alias parsing, quiet selection on
routing failures, and causes remaining separate from terminal explanations.
Reporter tests cover delivery states, retained failures, ignored errors,
finalization, domain statuses, quiet late handles, independent healthy
destinations, complete concurrent operations, and caller-owned writers.
`GOTOOLCHAIN=local go test -count=1 ./...` passes in the root, ckconfig, and
ckprompt modules; `GOTOOLCHAIN=local go test -race -run '^TestReporter' .` passes.
Boundary checks confirm Cobra/pflag imports stay under `internal/cobra`, public
Go documentation exposes no backend or internal types, and core does not depend
on either optional integration module.
## Stage 19: Update canonical documentation and compatibility guidance ## Stage 19: Update canonical documentation and compatibility guidance
Status: pending. Status: pending.

View File

@@ -19,7 +19,7 @@ func TestExecuteRunsTypedCommandsWithFreshInvocationState(t *testing.T) {
} }
var inputs []input var inputs []input
var contexts []context.Context var contexts []context.Context
var streams []commandkit.Streams var reporters []*commandkit.Reporter
firstContext := context.WithValue(context.Background(), struct{}{}, "first") firstContext := context.WithValue(context.Background(), struct{}{}, "first")
secondContext := context.WithValue(context.Background(), struct{}{}, "second") secondContext := context.WithValue(context.Background(), struct{}{}, "second")
var firstOut, secondOut, firstDiagnostic, secondDiagnostic bytes.Buffer var firstOut, secondOut, firstDiagnostic, secondDiagnostic bytes.Buffer
@@ -32,7 +32,12 @@ func TestExecuteRunsTypedCommandsWithFreshInvocationState(t *testing.T) {
Run: func(invocation commandkit.Invocation, value input) commandkit.Outcome { Run: func(invocation commandkit.Invocation, value input) commandkit.Outcome {
inputs = append(inputs, value) inputs = append(inputs, value)
contexts = append(contexts, invocation.Context) contexts = append(contexts, invocation.Context)
streams = append(streams, invocation.Streams) reporter := invocation.Reporter()
reporters = append(reporters, reporter)
_ = reporter.RenderOutput(func(writer io.Writer) error {
_, err := io.WriteString(writer, value.name.Value+"\n")
return err
})
return commandkit.Success() return commandkit.Success()
}, },
}) })
@@ -52,11 +57,11 @@ func TestExecuteRunsTypedCommandsWithFreshInvocationState(t *testing.T) {
if inputs[1].name.Set || inputs[1].name.Value != "" || inputs[1].ids.Set { if inputs[1].name.Set || inputs[1].name.Value != "" || inputs[1].ids.Set {
t.Fatalf("second invocation inherited state: %#v", inputs[1]) t.Fatalf("second invocation inherited state: %#v", inputs[1])
} }
if streams[2].Out != io.Discard || streams[2].Err != io.Discard { if reporters[0] == nil || reporters[1] == nil || reporters[2] == nil || reporters[0] == reporters[1] || reporters[0] == reporters[2] || reporters[1] == reporters[2] {
t.Fatalf("nil streams were not replaced with io.Discard: %#v", streams[2]) t.Fatalf("invocations did not receive fresh reporters: %#v", reporters)
} }
if contexts[0] != firstContext || contexts[1] != secondContext || streams[0].Out != &firstOut || streams[0].Err != &firstDiagnostic || streams[1].Out != &secondOut || streams[1].Err != &secondDiagnostic { if firstOut.String() != "first\n" || secondOut.String() != "\n" || contexts[0] != firstContext || contexts[1] != secondContext {
t.Fatalf("handler invocation contexts or streams were not forwarded independently") t.Fatalf("handler invocation contexts or reporter destinations were not independent: out=%q/%q contexts=%v/%v", firstOut.String(), secondOut.String(), contexts[0], contexts[1])
} }
} }
@@ -65,7 +70,7 @@ func TestOmittedInputFactoryUsesExactPointerZeroValue(t *testing.T) {
Name: "run", Name: "run",
Run: func(_ commandkit.Invocation, value *int) commandkit.Outcome { Run: func(_ commandkit.Invocation, value *int) commandkit.Outcome {
if value != nil { if value != nil {
return commandkit.Failure(errors.New("omitted input factory allocated a pointer")) return commandkit.Failure("omitted input factory allocated a pointer")
} }
return commandkit.Success() return commandkit.Success()
}, },
@@ -136,9 +141,9 @@ func TestExecutePresentsHandlerUsageAfterRegisteringCommand(t *testing.T) {
Run: func(commandkit.Invocation, struct{}) commandkit.Outcome { Run: func(commandkit.Invocation, struct{}) commandkit.Outcome {
calls++ calls++
if err := app.Register(late); err != nil { if err := app.Register(late); err != nil {
return commandkit.Failure(err) return commandkit.Failure(err.Error()).WithCause(err)
} }
return commandkit.Usage(usageCause) return commandkit.Usage("The handler requests usage.").WithCause(usageCause)
}, },
}) })
app, err := commandkit.New(commandkit.Identity{Name: "example"}, command) app, err := commandkit.New(commandkit.Identity{Name: "example"}, command)
@@ -195,12 +200,11 @@ func TestExecuteReturnsUsageForParsingRequirementsAndInputChecks(t *testing.T) {
func TestExecutePreservesDomainOutcomesAndWarningDiagnostics(t *testing.T) { func TestExecutePreservesDomainOutcomesAndWarningDiagnostics(t *testing.T) {
domainCause := errors.New("domain rejected the request") domainCause := errors.New("domain rejected the request")
warning := commandkit.MessageDiagnostic(commandkit.SeverityWarning, "review needed")
command := commandkit.NewCommand(commandkit.CommandSpec[struct{}]{ command := commandkit.NewCommand(commandkit.CommandSpec[struct{}]{
Name: "run", Name: "run",
Run: func(commandkit.Invocation, struct{}) commandkit.Outcome { Run: func(invocation commandkit.Invocation, _ struct{}) commandkit.Outcome {
outcome := commandkit.Domain("sample", 2, domainCause) _ = invocation.Reporter().Warn("review needed")
outcome.Diagnostics = []commandkit.Diagnostic{warning} outcome := commandkit.Domain("sample", 2).WithExplanation("The request was rejected.").WithCause(domainCause)
return outcome return outcome
}, },
}) })
@@ -209,19 +213,18 @@ func TestExecutePreservesDomainOutcomesAndWarningDiagnostics(t *testing.T) {
t.Fatal(err) t.Fatal(err)
} }
var diagnostic bytes.Buffer var diagnostic bytes.Buffer
outcome := app.Execute(context.Background(), []string{"run"}, commandkit.Streams{Err: &diagnostic}) outcome := app.Execute(context.Background(), []string{"run", "--verbose"}, commandkit.Streams{Err: &diagnostic})
if outcome.Kind != commandkit.OutcomeDomain || outcome.Domain != "sample" || outcome.Status != 2 || !errors.Is(outcome.Cause, domainCause) { if outcome.Kind != commandkit.OutcomeDomain || outcome.Domain != "sample" || outcome.Status != 2 || !errors.Is(outcome.Cause, domainCause) {
t.Fatalf("domain outcome changed: %#v", outcome) t.Fatalf("domain outcome changed: %#v", outcome)
} }
if diagnostic.String() != "review needed\ndomain rejected the request\n" || outcome.DiagnosticDelivery != commandkit.DeliveryComplete { if diagnostic.String() != "WARN: review needed\nERROR: The request was rejected.\n" || outcome.Presentation.Diagnostics.State != commandkit.DeliveryComplete {
t.Fatalf("warning delivery = %q, %#v", diagnostic.String(), outcome) t.Fatalf("warning delivery = %q, %#v", diagnostic.String(), outcome)
} }
warningOnly := commandkit.NewCommand(commandkit.CommandSpec[struct{}]{ warningOnly := commandkit.NewCommand(commandkit.CommandSpec[struct{}]{
Name: "warn", Name: "warn",
Run: func(commandkit.Invocation, struct{}) commandkit.Outcome { Run: func(invocation commandkit.Invocation, _ struct{}) commandkit.Outcome {
result := commandkit.Success() _ = invocation.Reporter().Warn("review needed")
result.Diagnostics = []commandkit.Diagnostic{warning} return commandkit.Success()
return result
}, },
}) })
app, err = commandkit.New(commandkit.Identity{Name: "example"}, warningOnly) app, err = commandkit.New(commandkit.Identity{Name: "example"}, warningOnly)
@@ -229,23 +232,18 @@ func TestExecutePreservesDomainOutcomesAndWarningDiagnostics(t *testing.T) {
t.Fatal(err) t.Fatal(err)
} }
diagnostic.Reset() diagnostic.Reset()
outcome = app.Execute(context.Background(), []string{"warn"}, commandkit.Streams{Err: &diagnostic}) outcome = app.Execute(context.Background(), []string{"warn", "--verbose"}, commandkit.Streams{Err: &diagnostic})
if outcome.Kind != commandkit.OutcomeSuccess || diagnostic.String() != "review needed\n" { if outcome.Kind != commandkit.OutcomeSuccess || diagnostic.String() != "WARN: review needed\n" {
t.Fatalf("warning-only outcome = %#v, diagnostics %q", outcome, diagnostic.String()) t.Fatalf("warning-only outcome = %#v, diagnostics %q", outcome, diagnostic.String())
} }
} }
func TestExecuteDoesNotRepeatCompletedDiagnosticsOrPresentedCauses(t *testing.T) { func TestExecuteDoesNotPresentOperationCauseWithoutExplanation(t *testing.T) {
cause := errors.New("already shown") cause := errors.New("private operation detail")
command := commandkit.NewCommand(commandkit.CommandSpec[struct{}]{ command := commandkit.NewCommand(commandkit.CommandSpec[struct{}]{
Name: "run", Name: "run",
Run: func(commandkit.Invocation, struct{}) commandkit.Outcome { Run: func(commandkit.Invocation, struct{}) commandkit.Outcome {
outcome := commandkit.Domain("sample", 2, cause) return commandkit.Domain("sample", 2).WithCause(cause)
outcome.Diagnostics = []commandkit.Diagnostic{{Severity: commandkit.SeverityError, Write: func(writer io.Writer) error {
_, err := io.WriteString(writer, "presented\n")
return err
}}}
return outcome
}, },
}) })
app, err := commandkit.New(commandkit.Identity{Name: "example"}, command) app, err := commandkit.New(commandkit.Identity{Name: "example"}, command)
@@ -254,21 +252,11 @@ func TestExecuteDoesNotRepeatCompletedDiagnosticsOrPresentedCauses(t *testing.T)
} }
var diagnostic bytes.Buffer var diagnostic bytes.Buffer
outcome := app.Execute(context.Background(), []string{"run"}, commandkit.Streams{Err: &diagnostic}) outcome := app.Execute(context.Background(), []string{"run"}, commandkit.Streams{Err: &diagnostic})
if outcome.Kind != commandkit.OutcomeDomain || diagnostic.String() != "presented\n" { if outcome.Kind != commandkit.OutcomeDomain || !errors.Is(outcome.Cause, cause) {
t.Fatalf("presented cause was duplicated: %#v, %q", outcome, diagnostic.String()) t.Fatalf("domain outcome changed: %#v", outcome)
} }
if strings.Contains(diagnostic.String(), cause.Error()) || !strings.Contains(diagnostic.String(), "ERROR: Command failed.") {
diagnostic.Reset() t.Fatalf("cause was presented instead of the fallback explanation: %#v, %q", outcome, diagnostic.String())
completed := commandkit.Domain("sample", 2, cause)
completed.DiagnosticDelivery = commandkit.DeliveryComplete
command = commandkit.NewCommand(commandkit.CommandSpec[struct{}]{Name: "done", Run: func(commandkit.Invocation, struct{}) commandkit.Outcome { return completed }})
app, err = commandkit.New(commandkit.Identity{Name: "example"}, command)
if err != nil {
t.Fatal(err)
}
outcome = app.Execute(context.Background(), []string{"done"}, commandkit.Streams{Err: &diagnostic})
if diagnostic.Len() != 0 || outcome.DiagnosticDelivery != commandkit.DeliveryComplete {
t.Fatalf("completed diagnostics were repeated: %#v, %q", outcome, diagnostic.String())
} }
} }
@@ -278,9 +266,7 @@ func TestExecuteRetainsOperationAndDiagnosticDeliveryFailures(t *testing.T) {
command := commandkit.NewCommand(commandkit.CommandSpec[struct{}]{ command := commandkit.NewCommand(commandkit.CommandSpec[struct{}]{
Name: "run", Name: "run",
Run: func(commandkit.Invocation, struct{}) commandkit.Outcome { Run: func(commandkit.Invocation, struct{}) commandkit.Outcome {
outcome := commandkit.Failure(operationFailure) return commandkit.Failure("The operation was canceled.").WithCause(operationFailure)
outcome.ReportIncomplete = true
return outcome
}, },
}) })
app, err := commandkit.New(commandkit.Identity{Name: "example"}, command) app, err := commandkit.New(commandkit.Identity{Name: "example"}, command)
@@ -288,12 +274,12 @@ func TestExecuteRetainsOperationAndDiagnosticDeliveryFailures(t *testing.T) {
t.Fatal(err) t.Fatal(err)
} }
outcome := app.Execute(context.Background(), []string{"run"}, commandkit.Streams{Err: failingWriter{err: writeFailure}}) outcome := app.Execute(context.Background(), []string{"run"}, commandkit.Streams{Err: failingWriter{err: writeFailure}})
if outcome.Kind != commandkit.OutcomeOperational || outcome.Status != 1 || outcome.DiagnosticDelivery != commandkit.DeliveryIncomplete || !outcome.ReportIncomplete || !errors.Is(outcome.Cause, operationFailure) || !errors.Is(outcome.DeliveryError, writeFailure) { if outcome.Kind != commandkit.OutcomeOperational || outcome.Status != 1 || outcome.Presentation.Diagnostics.State != commandkit.DeliveryIncomplete || !errors.Is(outcome.Cause, operationFailure) || !errors.Is(outcome.Presentation.Diagnostics.WriteError, writeFailure) {
t.Fatalf("combined operation and delivery failure = %#v", outcome) t.Fatalf("combined operation and delivery failure = %#v", outcome)
} }
short := app.Execute(context.Background(), []string{"run"}, commandkit.Streams{Err: shortWriter{}}) short := app.Execute(context.Background(), []string{"run"}, commandkit.Streams{Err: shortWriter{}})
if !errors.Is(short.DeliveryError, io.ErrShortWrite) || short.DiagnosticDelivery != commandkit.DeliveryIncomplete { if !errors.Is(short.Presentation.Diagnostics.WriteError, io.ErrShortWrite) || short.Presentation.Diagnostics.State != commandkit.DeliveryIncomplete {
t.Fatalf("short write was not detected: %#v", short) t.Fatalf("short write was not detected: %#v", short)
} }
} }
@@ -339,7 +325,7 @@ func TestExecuteRunsApplicationOwnedNestedHelp(t *testing.T) {
)}, )},
Run: func(_ commandkit.Invocation, value input) commandkit.Outcome { Run: func(_ commandkit.Invocation, value input) commandkit.Outcome {
received = append(received, value) received = append(received, value)
return commandkit.Domain("session-help", 7, domainCause) return commandkit.Domain("session-help", 7).WithExplanation("Session help is unavailable.").WithCause(domainCause)
}, },
}) })
session := commandkit.Group("session", "Session commands.", help) session := commandkit.Group("session", "Session commands.", help)
@@ -463,7 +449,7 @@ func TestHelpRendersRegisteredMetadataAndResolvesExactPaths(t *testing.T) {
t.Fatalf("version invoked an application command: %#v calls=%d/%d", versionOutcome, factoryCalls, runCalls) t.Fatalf("version invoked an application command: %#v calls=%d/%d", versionOutcome, factoryCalls, runCalls)
} }
failed := app.Execute(context.Background(), []string{"config", "show", "--help"}, commandkit.Streams{Out: shortWriter{}, Err: &diagnostic}) failed := app.Execute(context.Background(), []string{"config", "show", "--help"}, commandkit.Streams{Out: shortWriter{}, Err: &diagnostic})
if failed.Kind != commandkit.OutcomeOperational || !failed.ReportIncomplete || !errors.Is(failed.DeliveryError, io.ErrShortWrite) { if failed.Kind != commandkit.OutcomeOperational || failed.Presentation.Output.State != commandkit.DeliveryIncomplete || !errors.Is(failed.Presentation.Output.WriteError, io.ErrShortWrite) {
t.Fatalf("failed help delivery = %#v", failed) t.Fatalf("failed help delivery = %#v", failed)
} }
out.Reset() out.Reset()
@@ -503,7 +489,7 @@ func TestVersionCommandFormatsApplicationAndCommandKitIdentity(t *testing.T) {
t.Fatalf("invalid version scope = %#v, output %q", invalid, out.String()) t.Fatalf("invalid version scope = %#v, output %q", invalid, out.String())
} }
failed := app.Execute(context.Background(), []string{"version"}, commandkit.Streams{Out: shortWriter{}, Err: &diagnostic}) failed := app.Execute(context.Background(), []string{"version"}, commandkit.Streams{Out: shortWriter{}, Err: &diagnostic})
if failed.Kind != commandkit.OutcomeOperational || !failed.ReportIncomplete || !errors.Is(failed.DeliveryError, io.ErrShortWrite) { if failed.Kind != commandkit.OutcomeOperational || failed.Presentation.Output.State != commandkit.DeliveryIncomplete || !errors.Is(failed.Presentation.Output.WriteError, io.ErrShortWrite) {
t.Fatalf("failed version delivery = %#v", failed) t.Fatalf("failed version delivery = %#v", failed)
} }
} }
@@ -532,11 +518,11 @@ func TestHelpEntryFormsPresentReportAndDiagnosticFailures(t *testing.T) {
for _, args := range [][]string{nil, {"notes"}, {"--help"}, {"-h"}, {"notes", "--help"}, {"help"}, {"help", "notes"}} { for _, args := range [][]string{nil, {"notes"}, {"--help"}, {"-h"}, {"notes", "--help"}, {"help"}, {"help", "notes"}} {
var diagnostics bytes.Buffer var diagnostics bytes.Buffer
result := app.Execute(context.Background(), args, commandkit.Streams{Out: failingWriter{reportErr}, Err: &diagnostics}) result := app.Execute(context.Background(), args, commandkit.Streams{Out: failingWriter{reportErr}, Err: &diagnostics})
if result.Kind != commandkit.OutcomeOperational || !result.ReportIncomplete || !errors.Is(result.Cause, reportErr) || !errors.Is(result.DeliveryError, reportErr) || result.DiagnosticDelivery != commandkit.DeliveryComplete || strings.Count(diagnostics.String(), reportErr.Error()) != 1 { if result.Kind != commandkit.OutcomeOperational || result.Presentation.Output.State != commandkit.DeliveryIncomplete || !errors.Is(result.Presentation.Output.WriteError, reportErr) || result.Presentation.Diagnostics.State != commandkit.DeliveryComplete || !strings.Contains(diagnostics.String(), "ERROR: Command failed.") {
t.Fatalf("%v: result=%#v diagnostics=%q", args, result, diagnostics.String()) t.Fatalf("%v: result=%#v diagnostics=%q", args, result, diagnostics.String())
} }
result = app.Execute(context.Background(), args, commandkit.Streams{Out: failingWriter{reportErr}, Err: failingWriter{diagnosticErr}}) result = app.Execute(context.Background(), args, commandkit.Streams{Out: failingWriter{reportErr}, Err: failingWriter{diagnosticErr}})
if result.DiagnosticDelivery != commandkit.DeliveryIncomplete || !errors.Is(result.Cause, reportErr) || !errors.Is(result.DeliveryError, reportErr) || !errors.Is(result.DeliveryError, diagnosticErr) { if result.Presentation.Output.State != commandkit.DeliveryIncomplete || !errors.Is(result.Presentation.Output.WriteError, reportErr) || result.Presentation.Diagnostics.State != commandkit.DeliveryIncomplete || !errors.Is(result.Presentation.Diagnostics.WriteError, diagnosticErr) {
t.Fatalf("%v: result=%#v", args, result) t.Fatalf("%v: result=%#v", args, result)
} }
} }
@@ -560,46 +546,34 @@ func TestExecuteNormalizesOutcomeDefaultsAndPreservesDomainValues(t *testing.T)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
result := app.Execute(context.Background(), []string{"run"}, commandkit.Streams{}) var diagnostic bytes.Buffer
wantDelivery := commandkit.DeliveryNone result := app.Execute(context.Background(), []string{"run"}, commandkit.Streams{Err: &diagnostic})
if tc.cause != nil { if result.Kind != tc.kind || result.Status != tc.status || result.Domain != tc.domain || result.Cause != tc.cause {
wantDelivery = commandkit.DeliveryComplete
}
if result.Kind != tc.kind || result.Status != tc.status || result.Domain != tc.domain || result.Cause != tc.cause || result.DiagnosticDelivery != wantDelivery {
t.Fatalf("result=%#v expected=%#v", result, tc) t.Fatalf("result=%#v expected=%#v", result, tc)
} }
if tc.status != 0 && !strings.Contains(diagnostic.String(), "ERROR: Command failed.") {
t.Fatalf("failed outcome omitted its fallback explanation: %q", diagnostic.String())
}
if tc.cause != nil && strings.Contains(diagnostic.String(), tc.cause.Error()) {
t.Fatalf("operation cause was presented automatically: %q", diagnostic.String())
}
} }
} }
func TestNilDiagnosticWritersHaveNoOutputAndDoNotHideCauses(t *testing.T) { func TestNilDiagnosticWriterDoesNotHideOperationCause(t *testing.T) {
cause := errors.New("operation failed") cause := errors.New("operation failed")
for _, base := range []commandkit.Outcome{commandkit.Success(), commandkit.Domain("needs action", 2, nil), commandkit.Failure(cause)} { command := commandkit.NewCommand(commandkit.CommandSpec[struct{}]{
base.Diagnostics = []commandkit.Diagnostic{{}, {Severity: commandkit.SeverityWarning}, {Severity: commandkit.SeverityError}} Name: "run",
command := commandkit.NewCommand(commandkit.CommandSpec[struct{}]{Name: "run", Run: func(commandkit.Invocation, struct{}) commandkit.Outcome { return base }}) Run: func(commandkit.Invocation, struct{}) commandkit.Outcome {
app, err := commandkit.New(commandkit.Identity{Name: "example"}, command) return commandkit.Failure("The operation failed.").WithCause(cause)
if err != nil { },
t.Fatal(err) })
} app, err := commandkit.New(commandkit.Identity{Name: "example"}, command)
var diagnostics bytes.Buffer if err != nil {
result := app.Execute(context.Background(), []string{"run"}, commandkit.Streams{Err: &diagnostics}) t.Fatal(err)
want := commandkit.DeliveryNone }
if base.Cause != nil { result := app.Execute(context.Background(), []string{"run"}, commandkit.Streams{})
want = commandkit.DeliveryComplete if result.Kind != commandkit.OutcomeOperational || !errors.Is(result.Cause, cause) || result.Presentation.Diagnostics.State != commandkit.DeliveryComplete {
} t.Fatalf("nil diagnostic destination changed the outcome: %#v", result)
if result.Kind != base.Kind || result.Status != base.Status || result.Domain != base.Domain || result.Cause != base.Cause || result.DeliveryError != nil || result.DiagnosticDelivery != want {
t.Fatalf("result=%#v base=%#v", result, base)
}
if base.Cause == nil && diagnostics.Len() != 0 {
t.Fatalf("unexpected output=%q", diagnostics.String())
}
if base.Cause != nil && strings.Count(diagnostics.String(), cause.Error()) != 1 {
t.Fatalf("cause output=%q", diagnostics.String())
}
if base.Cause != nil {
result = app.Execute(context.Background(), []string{"run"}, commandkit.Streams{Err: failingWriter{io.ErrClosedPipe}})
if !errors.Is(result.Cause, cause) || !errors.Is(result.DeliveryError, io.ErrClosedPipe) || result.DiagnosticDelivery != commandkit.DeliveryIncomplete {
t.Fatalf("failed delivery=%#v", result)
}
}
} }
} }

View File

@@ -21,6 +21,12 @@ func newReportingParseApp(t *testing.T) *App {
return app return app
} }
func parseReportingArgs(app *App, args []string) commandmeta.Result {
route := resolveEnteredRoute(args, app.routeSnapshot())
current := &execution{app: app, route: route}
return current.parse(route.args)
}
func TestReportingModeReductionFollowsFlagOccurrenceOrder(t *testing.T) { func TestReportingModeReductionFollowsFlagOccurrenceOrder(t *testing.T) {
for _, test := range []struct { for _, test := range []struct {
name string name string
@@ -37,7 +43,7 @@ func TestReportingModeReductionFollowsFlagOccurrenceOrder(t *testing.T) {
{"disabled quiet leaves verbose", []string{"run", "--verbose", "--quiet=false"}, commandmeta.ReportingModeVerbose}, {"disabled quiet leaves verbose", []string{"run", "--verbose", "--quiet=false"}, commandmeta.ReportingModeVerbose},
} { } {
t.Run(test.name, func(t *testing.T) { t.Run(test.name, func(t *testing.T) {
result := newReportingParseApp(t).parse(test.args, commandmeta.ParseOptions{}) result := parseReportingArgs(newReportingParseApp(t), test.args)
if result.Failure != nil || result.Reporting.Mode != test.mode { if result.Failure != nil || result.Reporting.Mode != test.mode {
t.Fatalf("parse result = %#v, want mode %d", result, test.mode) t.Fatalf("parse result = %#v, want mode %d", result, test.mode)
} }
@@ -60,7 +66,7 @@ func TestProgressSelectionIsIndependentAndUsesLastOccurrence(t *testing.T) {
{"unset by default", []string{"run", "--verbose"}, commandmeta.ReportingModeVerbose, false, false}, {"unset by default", []string{"run", "--verbose"}, commandmeta.ReportingModeVerbose, false, false},
} { } {
t.Run(test.name, func(t *testing.T) { t.Run(test.name, func(t *testing.T) {
result := newReportingParseApp(t).parse(test.args, commandmeta.ParseOptions{}) result := parseReportingArgs(newReportingParseApp(t), test.args)
selection := result.Reporting selection := result.Reporting
if result.Failure != nil || selection.Mode != test.mode || selection.Progress != test.progress || selection.ProgressSet != test.progressSet { if result.Failure != nil || selection.Mode != test.mode || selection.Progress != test.progress || selection.ProgressSet != test.progressSet {
t.Fatalf("parse selection = %#v, failure = %v", selection, result.Failure) t.Fatalf("parse selection = %#v, failure = %v", selection, result.Failure)
@@ -71,9 +77,9 @@ func TestProgressSelectionIsIndependentAndUsesLastOccurrence(t *testing.T) {
func TestReportingParseRetainsAliasesAndConsumedPrefix(t *testing.T) { func TestReportingParseRetainsAliasesAndConsumedPrefix(t *testing.T) {
app := newReportingParseApp(t) app := newReportingParseApp(t)
result := app.parse([]string{"run", "--verbose", "-q"}, commandmeta.ParseOptions{}) result := parseReportingArgs(app, []string{"run", "--verbose", "-q"})
if result.Failure != nil || result.Reporting.Mode != commandmeta.ReportingModeQuiet { if result.Failure != nil || result.Reporting.Mode != commandmeta.ReportingModeQuiet {
t.Fatalf("aliased selection = %#v", result) t.Fatalf("aliased selection = %#v, failure=%+v", result, result.Failure)
} }
want := []commandmeta.FlagOccurrence{ want := []commandmeta.FlagOccurrence{
{Name: "verbose", Spelling: "--verbose", RawValue: "true"}, {Name: "verbose", Spelling: "--verbose", RawValue: "true"},
@@ -83,12 +89,12 @@ func TestReportingParseRetainsAliasesAndConsumedPrefix(t *testing.T) {
t.Fatalf("occurrences = %#v, want %#v", result.FlagOccurrences, want) t.Fatalf("occurrences = %#v, want %#v", result.FlagOccurrences, want)
} }
result = app.parse([]string{"run", "--verbose", "--unknown", "--quiet"}, commandmeta.ParseOptions{}) result = parseReportingArgs(app, []string{"run", "--verbose", "--unknown", "--quiet"})
if result.Failure == nil || result.Reporting.Mode != commandmeta.ReportingModeVerbose || len(result.FlagOccurrences) != 1 { if result.Failure == nil || result.Reporting.Mode != commandmeta.ReportingModeVerbose || len(result.FlagOccurrences) != 1 {
t.Fatalf("unknown flag partial selection = %#v", result) t.Fatalf("unknown flag partial selection = %#v", result)
} }
result = app.parse([]string{"run", "--verbose", "--quiet=invalid", "--debug"}, commandmeta.ParseOptions{}) result = parseReportingArgs(app, []string{"run", "--verbose", "--quiet=invalid", "--debug"})
if result.Failure == nil || !result.Failure.Usage || result.Reporting.Mode != commandmeta.ReportingModeVerbose || len(result.FlagOccurrences) != 2 { if result.Failure == nil || !result.Failure.Usage || result.Reporting.Mode != commandmeta.ReportingModeVerbose || len(result.FlagOccurrences) != 2 {
t.Fatalf("invalid boolean partial selection = %#v", result) t.Fatalf("invalid boolean partial selection = %#v", result)
} }
@@ -96,7 +102,7 @@ func TestReportingParseRetainsAliasesAndConsumedPrefix(t *testing.T) {
func TestReportingParsePreservesValuesHelpAndTerminator(t *testing.T) { func TestReportingParsePreservesValuesHelpAndTerminator(t *testing.T) {
app := newReportingParseApp(t) app := newReportingParseApp(t)
result := app.parse([]string{"run", "--message=--quiet", "--verbose", "--", "--debug"}, commandmeta.ParseOptions{}) result := parseReportingArgs(app, []string{"run", "--message=--quiet", "--verbose", "--", "--debug"})
if result.Failure != nil || result.Reporting.Mode != commandmeta.ReportingModeVerbose || !reflect.DeepEqual(result.Operands, []string{"--debug"}) { if result.Failure != nil || result.Reporting.Mode != commandmeta.ReportingModeVerbose || !reflect.DeepEqual(result.Operands, []string{"--debug"}) {
t.Fatalf("option-looking value or terminator = %#v", result) t.Fatalf("option-looking value or terminator = %#v", result)
} }
@@ -104,17 +110,17 @@ func TestReportingParsePreservesValuesHelpAndTerminator(t *testing.T) {
t.Fatalf("reporting-like value became a flag occurrence: %#v", result.FlagOccurrences) t.Fatalf("reporting-like value became a flag occurrence: %#v", result.FlagOccurrences)
} }
result = app.parse([]string{"run", "--help=bad", "--quiet"}, commandmeta.ParseOptions{}) result = parseReportingArgs(app, []string{"run", "--help=bad", "--quiet"})
if result.Failure == nil || result.Reporting.Mode != commandmeta.ReportingModeNormal || len(result.FlagOccurrences) != 1 || result.FlagOccurrences[0].Name != "help" { if result.Failure == nil || result.Reporting.Mode != commandmeta.ReportingModeNormal || len(result.FlagOccurrences) != 1 || result.FlagOccurrences[0].Name != "help" {
t.Fatalf("invalid help boolean consumed later flags: %#v", result) t.Fatalf("invalid help boolean consumed later flags: %#v", result)
} }
result = app.parse([]string{"run", "--help=false", "--quiet"}, commandmeta.ParseOptions{}) result = parseReportingArgs(app, []string{"run", "--help=false", "--quiet"})
if result.Failure != nil || result.HelpRequested || result.Reporting.Mode != commandmeta.ReportingModeQuiet { if result.Failure != nil || result.HelpRequested || result.Reporting.Mode != commandmeta.ReportingModeQuiet {
t.Fatalf("valid false help boolean changed handling: %#v", result) t.Fatalf("valid false help boolean changed handling: %#v", result)
} }
result = app.parse([]string{"missing", "--quiet"}, commandmeta.ParseOptions{}) result = parseReportingArgs(app, []string{"missing", "--quiet"})
if result.Failure == nil || len(result.FlagOccurrences) != 0 { if result.Failure == nil || result.Reporting.Mode != commandmeta.ReportingModeQuiet || len(result.FlagOccurrences) != 1 || result.FlagOccurrences[0].Name != "quiet" {
t.Fatalf("malformed path produced flag occurrences: %#v", result) t.Fatalf("routing failure did not retain reporting selection: %#v", result)
} }
} }

View File

@@ -124,7 +124,7 @@ func performanceFlatCommands(tb testing.TB, count int, prefix string) []commandk
run := func(commandkit.Invocation, struct{}) commandkit.Outcome { return commandkit.Success() } run := func(commandkit.Invocation, struct{}) commandkit.Outcome { return commandkit.Success() }
if prefix == "cmd" && index == 4 { if prefix == "cmd" && index == 4 {
run = func(commandkit.Invocation, struct{}) commandkit.Outcome { run = func(commandkit.Invocation, struct{}) commandkit.Outcome {
return commandkit.Usage(performanceUsageCause) return commandkit.Usage("The benchmark command needs usage.").WithCause(performanceUsageCause)
} }
} }
command := commandkit.NewCommand(commandkit.CommandSpec[struct{}]{Name: name, Summary: "Benchmark command.", Flags: flags, Run: run}) command := commandkit.NewCommand(commandkit.CommandSpec[struct{}]{Name: name, Summary: "Benchmark command.", Flags: flags, Run: run})

View File

@@ -49,6 +49,31 @@ func TestReporterOutputSubmitsOnWrite(t *testing.T) {
} }
} }
type callerOwnedWriter struct {
bytes.Buffer
closeCalls int
}
func (w *callerOwnedWriter) Close() error {
w.closeCalls++
return nil
}
func TestReporterLeavesCallerWriterOpen(t *testing.T) {
destination := &callerOwnedWriter{}
reporter := newReporter(Streams{Out: destination})
_, _ = reporter.Output().Write([]byte("report"))
_ = reporter.RenderOutput(func(writer io.Writer) error {
_, err := io.WriteString(writer, " rendered")
return err
})
_ = reporter.finalize(Success(), nil)
if destination.String() != "report rendered" || destination.closeCalls != 0 {
t.Fatalf("destination = %q, Close calls = %d", destination.String(), destination.closeCalls)
}
}
func TestReporterQuietOutputDiscardsSuccessfully(t *testing.T) { func TestReporterQuietOutputDiscardsSuccessfully(t *testing.T) {
var destination bytes.Buffer var destination bytes.Buffer
reporter := newReporter(Streams{Out: &destination}) reporter := newReporter(Streams{Out: &destination})

View File

@@ -1020,7 +1020,7 @@ func TestAliasHelpTargetsGroupsAndDiscoveryViews(t *testing.T) {
var diagnostics bytes.Buffer var diagnostics bytes.Buffer
outcome := app.Execute(context.Background(), []string{"list"}, Streams{Out: io.Discard, Err: &diagnostics}) outcome := app.Execute(context.Background(), []string{"list"}, Streams{Out: io.Discard, Err: &diagnostics})
if outcome.Kind != OutcomeUsage || !strings.Contains(outcome.Cause.Error(), "--help") { if outcome.Kind != OutcomeUsage || !strings.Contains(outcome.Explanation.Message, "--help") {
t.Fatalf("bare discovery prefix = %#v, diagnostics %q; want usage pointing to list --help", outcome, diagnostics.String()) t.Fatalf("bare discovery prefix = %#v, diagnostics %q; want usage pointing to list --help", outcome, diagnostics.String())
} }
if inputBuilds != 0 || promptRuns != 0 { if inputBuilds != 0 || promptRuns != 0 {