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

@@ -19,7 +19,7 @@ func TestExecuteRunsTypedCommandsWithFreshInvocationState(t *testing.T) {
}
var inputs []input
var contexts []context.Context
var streams []commandkit.Streams
var reporters []*commandkit.Reporter
firstContext := context.WithValue(context.Background(), struct{}{}, "first")
secondContext := context.WithValue(context.Background(), struct{}{}, "second")
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 {
inputs = append(inputs, value)
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()
},
})
@@ -52,11 +57,11 @@ func TestExecuteRunsTypedCommandsWithFreshInvocationState(t *testing.T) {
if inputs[1].name.Set || inputs[1].name.Value != "" || inputs[1].ids.Set {
t.Fatalf("second invocation inherited state: %#v", inputs[1])
}
if streams[2].Out != io.Discard || streams[2].Err != io.Discard {
t.Fatalf("nil streams were not replaced with io.Discard: %#v", streams[2])
if reporters[0] == nil || reporters[1] == nil || reporters[2] == nil || reporters[0] == reporters[1] || reporters[0] == reporters[2] || reporters[1] == reporters[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 {
t.Fatalf("handler invocation contexts or streams were not forwarded independently")
if firstOut.String() != "first\n" || secondOut.String() != "\n" || contexts[0] != firstContext || contexts[1] != secondContext {
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",
Run: func(_ commandkit.Invocation, value *int) commandkit.Outcome {
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()
},
@@ -136,9 +141,9 @@ func TestExecutePresentsHandlerUsageAfterRegisteringCommand(t *testing.T) {
Run: func(commandkit.Invocation, struct{}) commandkit.Outcome {
calls++
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)
@@ -195,12 +200,11 @@ func TestExecuteReturnsUsageForParsingRequirementsAndInputChecks(t *testing.T) {
func TestExecutePreservesDomainOutcomesAndWarningDiagnostics(t *testing.T) {
domainCause := errors.New("domain rejected the request")
warning := commandkit.MessageDiagnostic(commandkit.SeverityWarning, "review needed")
command := commandkit.NewCommand(commandkit.CommandSpec[struct{}]{
Name: "run",
Run: func(commandkit.Invocation, struct{}) commandkit.Outcome {
outcome := commandkit.Domain("sample", 2, domainCause)
outcome.Diagnostics = []commandkit.Diagnostic{warning}
Run: func(invocation commandkit.Invocation, _ struct{}) commandkit.Outcome {
_ = invocation.Reporter().Warn("review needed")
outcome := commandkit.Domain("sample", 2).WithExplanation("The request was rejected.").WithCause(domainCause)
return outcome
},
})
@@ -209,19 +213,18 @@ func TestExecutePreservesDomainOutcomesAndWarningDiagnostics(t *testing.T) {
t.Fatal(err)
}
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) {
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)
}
warningOnly := commandkit.NewCommand(commandkit.CommandSpec[struct{}]{
Name: "warn",
Run: func(commandkit.Invocation, struct{}) commandkit.Outcome {
result := commandkit.Success()
result.Diagnostics = []commandkit.Diagnostic{warning}
return result
Run: func(invocation commandkit.Invocation, _ struct{}) commandkit.Outcome {
_ = invocation.Reporter().Warn("review needed")
return commandkit.Success()
},
})
app, err = commandkit.New(commandkit.Identity{Name: "example"}, warningOnly)
@@ -229,23 +232,18 @@ func TestExecutePreservesDomainOutcomesAndWarningDiagnostics(t *testing.T) {
t.Fatal(err)
}
diagnostic.Reset()
outcome = app.Execute(context.Background(), []string{"warn"}, commandkit.Streams{Err: &diagnostic})
if outcome.Kind != commandkit.OutcomeSuccess || diagnostic.String() != "review needed\n" {
outcome = app.Execute(context.Background(), []string{"warn", "--verbose"}, commandkit.Streams{Err: &diagnostic})
if outcome.Kind != commandkit.OutcomeSuccess || diagnostic.String() != "WARN: review needed\n" {
t.Fatalf("warning-only outcome = %#v, diagnostics %q", outcome, diagnostic.String())
}
}
func TestExecuteDoesNotRepeatCompletedDiagnosticsOrPresentedCauses(t *testing.T) {
cause := errors.New("already shown")
func TestExecuteDoesNotPresentOperationCauseWithoutExplanation(t *testing.T) {
cause := errors.New("private operation detail")
command := commandkit.NewCommand(commandkit.CommandSpec[struct{}]{
Name: "run",
Run: func(commandkit.Invocation, struct{}) commandkit.Outcome {
outcome := commandkit.Domain("sample", 2, cause)
outcome.Diagnostics = []commandkit.Diagnostic{{Severity: commandkit.SeverityError, Write: func(writer io.Writer) error {
_, err := io.WriteString(writer, "presented\n")
return err
}}}
return outcome
return commandkit.Domain("sample", 2).WithCause(cause)
},
})
app, err := commandkit.New(commandkit.Identity{Name: "example"}, command)
@@ -254,21 +252,11 @@ func TestExecuteDoesNotRepeatCompletedDiagnosticsOrPresentedCauses(t *testing.T)
}
var diagnostic bytes.Buffer
outcome := app.Execute(context.Background(), []string{"run"}, commandkit.Streams{Err: &diagnostic})
if outcome.Kind != commandkit.OutcomeDomain || diagnostic.String() != "presented\n" {
t.Fatalf("presented cause was duplicated: %#v, %q", outcome, diagnostic.String())
if outcome.Kind != commandkit.OutcomeDomain || !errors.Is(outcome.Cause, cause) {
t.Fatalf("domain outcome changed: %#v", outcome)
}
diagnostic.Reset()
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())
if strings.Contains(diagnostic.String(), cause.Error()) || !strings.Contains(diagnostic.String(), "ERROR: Command failed.") {
t.Fatalf("cause was presented instead of the fallback explanation: %#v, %q", outcome, diagnostic.String())
}
}
@@ -278,9 +266,7 @@ func TestExecuteRetainsOperationAndDiagnosticDeliveryFailures(t *testing.T) {
command := commandkit.NewCommand(commandkit.CommandSpec[struct{}]{
Name: "run",
Run: func(commandkit.Invocation, struct{}) commandkit.Outcome {
outcome := commandkit.Failure(operationFailure)
outcome.ReportIncomplete = true
return outcome
return commandkit.Failure("The operation was canceled.").WithCause(operationFailure)
},
})
app, err := commandkit.New(commandkit.Identity{Name: "example"}, command)
@@ -288,12 +274,12 @@ func TestExecuteRetainsOperationAndDiagnosticDeliveryFailures(t *testing.T) {
t.Fatal(err)
}
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)
}
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)
}
}
@@ -339,7 +325,7 @@ func TestExecuteRunsApplicationOwnedNestedHelp(t *testing.T) {
)},
Run: func(_ commandkit.Invocation, value input) commandkit.Outcome {
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)
@@ -463,7 +449,7 @@ func TestHelpRendersRegisteredMetadataAndResolvesExactPaths(t *testing.T) {
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})
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)
}
out.Reset()
@@ -503,7 +489,7 @@ func TestVersionCommandFormatsApplicationAndCommandKitIdentity(t *testing.T) {
t.Fatalf("invalid version scope = %#v, output %q", invalid, out.String())
}
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)
}
}
@@ -532,11 +518,11 @@ func TestHelpEntryFormsPresentReportAndDiagnosticFailures(t *testing.T) {
for _, args := range [][]string{nil, {"notes"}, {"--help"}, {"-h"}, {"notes", "--help"}, {"help"}, {"help", "notes"}} {
var diagnostics bytes.Buffer
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())
}
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)
}
}
@@ -560,46 +546,34 @@ func TestExecuteNormalizesOutcomeDefaultsAndPreservesDomainValues(t *testing.T)
if err != nil {
t.Fatal(err)
}
result := app.Execute(context.Background(), []string{"run"}, commandkit.Streams{})
wantDelivery := commandkit.DeliveryNone
if tc.cause != nil {
wantDelivery = commandkit.DeliveryComplete
}
if result.Kind != tc.kind || result.Status != tc.status || result.Domain != tc.domain || result.Cause != tc.cause || result.DiagnosticDelivery != wantDelivery {
var diagnostic bytes.Buffer
result := app.Execute(context.Background(), []string{"run"}, commandkit.Streams{Err: &diagnostic})
if result.Kind != tc.kind || result.Status != tc.status || result.Domain != tc.domain || result.Cause != tc.cause {
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")
for _, base := range []commandkit.Outcome{commandkit.Success(), commandkit.Domain("needs action", 2, nil), commandkit.Failure(cause)} {
base.Diagnostics = []commandkit.Diagnostic{{}, {Severity: commandkit.SeverityWarning}, {Severity: commandkit.SeverityError}}
command := commandkit.NewCommand(commandkit.CommandSpec[struct{}]{Name: "run", Run: func(commandkit.Invocation, struct{}) commandkit.Outcome { return base }})
app, err := commandkit.New(commandkit.Identity{Name: "example"}, command)
if err != nil {
t.Fatal(err)
}
var diagnostics bytes.Buffer
result := app.Execute(context.Background(), []string{"run"}, commandkit.Streams{Err: &diagnostics})
want := commandkit.DeliveryNone
if base.Cause != nil {
want = commandkit.DeliveryComplete
}
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)
}
}
command := commandkit.NewCommand(commandkit.CommandSpec[struct{}]{
Name: "run",
Run: func(commandkit.Invocation, struct{}) commandkit.Outcome {
return commandkit.Failure("The operation failed.").WithCause(cause)
},
})
app, err := commandkit.New(commandkit.Identity{Name: "example"}, command)
if err != nil {
t.Fatal(err)
}
result := app.Execute(context.Background(), []string{"run"}, commandkit.Streams{})
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)
}
}