From a6c0fb5b0d97e23f728b31ea6f4840faa1d62793 Mon Sep 17 00:00:00 2001 From: Eric Rakestraw Date: Mon, 26 Oct 2026 03:51:14 +0000 Subject: [PATCH] Reconcile reporting tests and boundaries --- debug_events_test.go | 4 +- docs/roadmap/implementation.md | 17 +++- execute_external_test.go | 150 ++++++++++++++------------------- parser_reporting_test.go | 30 ++++--- performance_dispatch_test.go | 2 +- reporter_test.go | 25 ++++++ route_test.go | 2 +- 7 files changed, 124 insertions(+), 106 deletions(-) diff --git a/debug_events_test.go b/debug_events_test.go index c736de3..753c721 100644 --- a/debug_events_test.go +++ b/debug_events_test.go @@ -32,7 +32,7 @@ func TestDebugEventsFollowModeAndFinalizationOrder(t *testing.T) { for _, fragment := range []string{ "resolved command alias \"shortcut\" to \"run\"", "selected debug reporting mode using --trace (alias for --debug)", - "INFO: deferred summary", + "deferred summary", "handler completed for \"run\" with operational outcome and status 1", "ERROR: operation failed", } { @@ -46,7 +46,7 @@ func TestDebugEventsFollowModeAndFinalizationOrder(t *testing.T) { ordered := []string{ "resolved command alias", "selected debug reporting mode", - "INFO: deferred summary", + "deferred summary", "handler completed", "ERROR: operation failed", } diff --git a/docs/roadmap/implementation.md b/docs/roadmap/implementation.md index 9148326..11f4d4a 100644 --- a/docs/roadmap/implementation.md +++ b/docs/roadmap/implementation.md @@ -1,6 +1,6 @@ # 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 @@ -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 -Status: pending. +Status: complete. 1. Find and migrate remaining callers of removed constructors, raw invocation 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 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 Status: pending. diff --git a/execute_external_test.go b/execute_external_test.go index 85b4002..42a2d0f 100644 --- a/execute_external_test.go +++ b/execute_external_test.go @@ -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) } } diff --git a/parser_reporting_test.go b/parser_reporting_test.go index 0ddc6a9..2afdaf4 100644 --- a/parser_reporting_test.go +++ b/parser_reporting_test.go @@ -21,6 +21,12 @@ func newReportingParseApp(t *testing.T) *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) { for _, test := range []struct { name string @@ -37,7 +43,7 @@ func TestReportingModeReductionFollowsFlagOccurrenceOrder(t *testing.T) { {"disabled quiet leaves verbose", []string{"run", "--verbose", "--quiet=false"}, commandmeta.ReportingModeVerbose}, } { 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 { 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}, } { 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 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) @@ -71,9 +77,9 @@ func TestProgressSelectionIsIndependentAndUsesLastOccurrence(t *testing.T) { func TestReportingParseRetainsAliasesAndConsumedPrefix(t *testing.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 { - t.Fatalf("aliased selection = %#v", result) + t.Fatalf("aliased selection = %#v, failure=%+v", result, result.Failure) } want := []commandmeta.FlagOccurrence{ {Name: "verbose", Spelling: "--verbose", RawValue: "true"}, @@ -83,12 +89,12 @@ func TestReportingParseRetainsAliasesAndConsumedPrefix(t *testing.T) { 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 { 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 { t.Fatalf("invalid boolean partial selection = %#v", result) } @@ -96,7 +102,7 @@ func TestReportingParseRetainsAliasesAndConsumedPrefix(t *testing.T) { func TestReportingParsePreservesValuesHelpAndTerminator(t *testing.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"}) { 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) } - 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" { 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 { t.Fatalf("valid false help boolean changed handling: %#v", result) } - result = app.parse([]string{"missing", "--quiet"}, commandmeta.ParseOptions{}) - if result.Failure == nil || len(result.FlagOccurrences) != 0 { - t.Fatalf("malformed path produced flag occurrences: %#v", result) + result = parseReportingArgs(app, []string{"missing", "--quiet"}) + if result.Failure == nil || result.Reporting.Mode != commandmeta.ReportingModeQuiet || len(result.FlagOccurrences) != 1 || result.FlagOccurrences[0].Name != "quiet" { + t.Fatalf("routing failure did not retain reporting selection: %#v", result) } } diff --git a/performance_dispatch_test.go b/performance_dispatch_test.go index fdf17ee..ddfa743 100644 --- a/performance_dispatch_test.go +++ b/performance_dispatch_test.go @@ -124,7 +124,7 @@ func performanceFlatCommands(tb testing.TB, count int, prefix string) []commandk run := func(commandkit.Invocation, struct{}) commandkit.Outcome { return commandkit.Success() } if prefix == "cmd" && index == 4 { 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}) diff --git a/reporter_test.go b/reporter_test.go index c0f5843..0249d43 100644 --- a/reporter_test.go +++ b/reporter_test.go @@ -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) { var destination bytes.Buffer reporter := newReporter(Streams{Out: &destination}) diff --git a/route_test.go b/route_test.go index 85b987d..ede3747 100644 --- a/route_test.go +++ b/route_test.go @@ -1020,7 +1020,7 @@ func TestAliasHelpTargetsGroupsAndDiscoveryViews(t *testing.T) { var diagnostics bytes.Buffer 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()) } if inputBuilds != 0 || promptRuns != 0 {