diff --git a/docs/roadmap/remediation.md b/docs/roadmap/remediation.md index f681c27..f8d336b 100644 --- a/docs/roadmap/remediation.md +++ b/docs/roadmap/remediation.md @@ -60,6 +60,9 @@ architecture. The issues below affect behavior within the implemented structure. - R-05 complete: validation header/record write failures retain `DeliveryError` separately from acquisition failures and combined diagnostic-stream failures. +- R-06 complete: zero outcomes normalize to success/0 and empty delivery state + normalizes to none without altering explicit domain outcomes. + ## Findings and priority P1 means a supported invocation is blocked or its report content is corrupted. diff --git a/execute.go b/execute.go index 84cb5b5..54edd2b 100644 --- a/execute.go +++ b/execute.go @@ -88,6 +88,14 @@ func (a *App) commandAt(path []string) (Command, bool) { } func (a *App) present(outcome Outcome, path []string, streams Streams) Outcome { + if outcome.Kind == "" && outcome.Status == 0 && outcome.Domain == "" && + outcome.Cause == nil && outcome.DeliveryError == nil && + !outcome.ReportIncomplete && len(outcome.Diagnostics) == 0 { + outcome.Kind = OutcomeSuccess + } + if outcome.DiagnosticDelivery == "" { + outcome.DiagnosticDelivery = DeliveryNone + } streams = normalizeStreams(streams) if outcome.DiagnosticDelivery == DeliveryComplete { return outcome diff --git a/execute_external_test.go b/execute_external_test.go index cf24111..06ac243 100644 --- a/execute_external_test.go +++ b/execute_external_test.go @@ -437,3 +437,35 @@ func TestHelpEntryFormsPresentReportAndDiagnosticFailures(t *testing.T) { } } } + +func TestExecuteNormalizesOutcomeDefaultsAndPreservesDomainValues(t *testing.T) { + cause := errors.New("domain cause") + for _, tc := range []struct { + outcome commandkit.Outcome + kind commandkit.OutcomeKind + status int + domain string + cause error + }{ + {commandkit.Outcome{}, commandkit.OutcomeSuccess, 0, "", nil}, + {commandkit.Outcome{Kind: commandkit.OutcomeDomain, Status: 2, Domain: "needs action", Cause: cause}, commandkit.OutcomeDomain, 2, "needs action", cause}, + {commandkit.Outcome{Kind: commandkit.OutcomeDomain, Status: 0, Domain: "complete"}, commandkit.OutcomeDomain, 0, "complete", nil}, + } { + command, err := commandkit.NewCommand(commandkit.CommandSpec[struct{}]{Name: "run", Run: func(commandkit.Invocation, struct{}) commandkit.Outcome { return tc.outcome }}) + if err != nil { + t.Fatal(err) + } + app, err := commandkit.New(commandkit.Identity{Name: "example"}, command) + 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 { + t.Fatalf("result=%#v expected=%#v", result, tc) + } + } +} diff --git a/outcome.go b/outcome.go index d1f3adc..10028e5 100644 --- a/outcome.go +++ b/outcome.go @@ -49,6 +49,7 @@ type Diagnostic struct { // Cause records the operation error. DeliveryError records an error encountered // while presenting diagnostics or reports; it is kept separate from Cause so // both failures remain inspectable. Outcome does not implement error. +// Invocation coordination treats a zero Outcome as success with status 0. type Outcome struct { // Kind classifies the result. Kind OutcomeKind