Define invocation outcome reporting types

This commit is contained in:
2026-10-26 02:37:19 +00:00
parent 2a1096bd1f
commit e56537690b
3 changed files with 211 additions and 69 deletions

View File

@@ -2,39 +2,130 @@ package commandkit
import (
"errors"
"io"
"strings"
"testing"
)
func TestOutcomeConstructors(t *testing.T) {
cause := errors.New("operation failed")
type outcomeTestCause struct{ message string }
func (e *outcomeTestCause) Error() string { return e.message }
func TestOutcomeConstructorsAndModifiers(t *testing.T) {
var calls int
detail := Detail(func(io.Writer) error {
calls++
return nil
})
cause := &outcomeTestCause{message: "operation failed"}
wrapped := errors.Join(errors.New("context"), cause)
tests := []struct {
name string
got Outcome
kind OutcomeKind
status int
domain string
wantCause error
name string
got Outcome
kind OutcomeKind
status int
domain string
}{
{name: "success", got: Success(), kind: OutcomeSuccess, status: 0},
{name: "usage", got: Usage(cause), kind: OutcomeUsage, status: 2, wantCause: cause},
{name: "failure", got: Failure(cause), kind: OutcomeOperational, status: 1, wantCause: cause},
{name: "domain", got: Domain("record", 7, cause), kind: OutcomeDomain, status: 7, domain: "record", wantCause: cause},
{name: "usage", got: Usage("invalid input", detail), kind: OutcomeUsage, status: 2},
{name: "failure", got: Failure("operation failed", detail), kind: OutcomeOperational, status: 1},
{name: "domain", got: Domain("record", 7), kind: OutcomeDomain, status: 7, domain: "record"},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
if test.got.Kind != test.kind {
t.Fatalf("Kind = %q, want %q", test.got.Kind, test.kind)
if test.got.Kind != test.kind || test.got.Status != test.status || test.got.Domain != test.domain {
t.Fatalf("outcome = %#v, want kind %q, status %d, domain %q", test.got, test.kind, test.status, test.domain)
}
if test.got.Status != test.status {
t.Fatalf("Status = %d, want %d", test.got.Status, test.status)
})
}
if calls != 0 {
t.Fatalf("constructors called detail renderer %d times", calls)
}
base := Domain("validation", 3).WithCause(wrapped)
updated := base.WithExplanation("invalid record", detail)
if updated.Kind != base.Kind || updated.Status != base.Status || updated.Domain != base.Domain {
t.Fatalf("WithExplanation changed classification: base %#v, updated %#v", base, updated)
}
if !errors.Is(updated.Cause, cause) {
t.Fatalf("WithExplanation lost cause identity: %v", updated.Cause)
}
var gotCause *outcomeTestCause
if !errors.As(updated.Cause, &gotCause) || gotCause != cause {
t.Fatalf("WithExplanation lost typed cause: %#v", gotCause)
}
if updated.Explanation.Message != "invalid record" || len(updated.Explanation.Details) != 1 {
t.Fatalf("WithExplanation = %#v", updated.Explanation)
}
if calls != 0 {
t.Fatalf("WithExplanation called detail renderer %d times", calls)
}
changedCause := updated.WithCause(errors.New("replacement"))
if changedCause.Kind != updated.Kind || changedCause.Status != updated.Status || changedCause.Domain != updated.Domain {
t.Fatalf("WithCause changed classification: %#v", changedCause)
}
if changedCause.Explanation.Message != updated.Explanation.Message || len(changedCause.Explanation.Details) != 1 {
t.Fatalf("WithCause changed explanation: %#v", changedCause.Explanation)
}
}
func TestOutcomeCopiesDetailSlices(t *testing.T) {
one := Detail(func(w io.Writer) error { _, err := io.WriteString(w, "one"); return err })
two := Detail(func(w io.Writer) error { _, err := io.WriteString(w, "two"); return err })
tests := []struct {
name string
make func([]Detail) Outcome
}{
{name: "failure", make: func(details []Detail) Outcome { return Failure("failed", details...) }},
{name: "usage", make: func(details []Detail) Outcome { return Usage("invalid", details...) }},
{name: "modifier", make: func(details []Detail) Outcome { return Success().WithExplanation("failed", details...) }},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
supplied := []Detail{one}
outcome := test.make(supplied)
supplied[0] = two
supplied = append(supplied, two)
if len(outcome.Explanation.Details) != 1 || outcome.Explanation.Details[0] == nil {
t.Fatalf("captured details = %#v", outcome.Explanation.Details)
}
if test.got.Cause != test.wantCause {
t.Fatalf("Cause = %v, want original cause %v", test.got.Cause, test.wantCause)
var rendered strings.Builder
if err := outcome.Explanation.Details[0](&rendered); err != nil {
t.Fatal(err)
}
if test.got.Domain != test.domain {
t.Fatalf("Domain = %q, want %q", test.got.Domain, test.domain)
if rendered.String() != "one" {
t.Fatalf("captured callback rendered %q, want original callback", rendered.String())
}
})
}
}
func TestWithCausePreservesErrorIdentity(t *testing.T) {
cause := &outcomeTestCause{message: "typed failure"}
outcome := Failure("failed").WithCause(cause)
if !errors.Is(outcome.Cause, cause) {
t.Fatalf("errors.Is(%v, cause) = false", outcome.Cause)
}
var got *outcomeTestCause
if !errors.As(outcome.Cause, &got) || got != cause {
t.Fatalf("errors.As cause = %#v", got)
}
}
func TestMessageDiagnosticIsPassiveData(t *testing.T) {
diagnostic := MessageDiagnostic(SeverityWarning, "review needed\n")
if diagnostic.Severity != SeverityWarning || diagnostic.Write == nil {
t.Fatalf("diagnostic = %#v", diagnostic)
}
var rendered strings.Builder
if err := diagnostic.Write(&rendered); err != nil {
t.Fatal(err)
}
if got := rendered.String(); got != "review needed\n" {
t.Fatalf("rendered diagnostic = %q", got)
}
}