Treat nil diagnostic writers as no-output entries

This commit is contained in:
2026-10-07 16:49:56 +00:00
parent b4010c43bd
commit d5f544d14d
3 changed files with 46 additions and 4 deletions

View File

@@ -63,6 +63,9 @@ architecture. The issues below affect behavior within the implemented structure.
- R-06 complete: zero outcomes normalize to success/0 and empty delivery state
normalizes to none without altering explicit domain outcomes.
- R-07 complete: nil diagnostic writers are no-output entries, do not count as
delivery attempts, and cannot suppress cause presentation.
## Findings and priority
P1 means a supported invocation is blocked or its report content is corrupted.

View File

@@ -111,11 +111,12 @@ func (a *App) present(outcome Outcome, path []string, streams Streams) Outcome {
if len(diagnostics) == 0 {
return outcome
}
delivered := false
for _, diagnostic := range diagnostics {
if diagnostic.Write == nil {
outcome = deliveryFailure(outcome, errors.New("diagnostic has no writer"))
return outcome
continue
}
delivered = true
writer := &checkedWriter{writer: streams.Err}
writeErr := diagnostic.Write(writer)
if writer.failure != nil {
@@ -129,7 +130,9 @@ func (a *App) present(outcome Outcome, path []string, streams Streams) Outcome {
return deliveryFailure(outcome, writeErr)
}
}
outcome.DiagnosticDelivery = DeliveryComplete
if delivered {
outcome.DiagnosticDelivery = DeliveryComplete
}
return outcome
}
@@ -148,7 +151,7 @@ func (a *App) usageLine(path []string) string {
func hasErrorDiagnostic(diagnostics []Diagnostic) bool {
for _, diagnostic := range diagnostics {
if diagnostic.Severity == configkit.SeverityError {
if diagnostic.Write != nil && diagnostic.Severity == configkit.SeverityError {
return true
}
}

View File

@@ -469,3 +469,39 @@ func TestExecuteNormalizesOutcomeDefaultsAndPreservesDomainValues(t *testing.T)
}
}
}
func TestNilDiagnosticWritersHaveNoOutputAndDoNotHideCauses(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: configkit.SeverityWarning}, {Severity: configkit.SeverityError}}
command, err := commandkit.NewCommand(commandkit.CommandSpec[struct{}]{Name: "run", Run: func(commandkit.Invocation, struct{}) commandkit.Outcome { return base }})
if err != nil {
t.Fatal(err)
}
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)
}
}
}
}