Retain validation report delivery errors in outcomes
This commit is contained in:
@@ -639,7 +639,7 @@ func TestConfigValidationYAMLWarningsAndShortWrites(t *testing.T) {
|
||||
}
|
||||
|
||||
outcome = app.Execute(context.Background(), []string{"config", "validate"}, commandkit.Streams{Out: configKitPartialWriter{}, Err: io.Discard})
|
||||
if outcome.Kind != commandkit.OutcomeOperational || !outcome.ReportIncomplete || !errors.Is(outcome.Cause, io.ErrShortWrite) {
|
||||
if outcome.Kind != commandkit.OutcomeOperational || !outcome.ReportIncomplete || !errors.Is(outcome.Cause, io.ErrShortWrite) || !errors.Is(outcome.DeliveryError, io.ErrShortWrite) {
|
||||
t.Fatalf("report short write outcome = %#v", outcome)
|
||||
}
|
||||
var delivered bytes.Buffer
|
||||
@@ -657,3 +657,64 @@ func (configKitPartialWriter) Write(data []byte) (int, error) {
|
||||
}
|
||||
return len(data) - 1, nil
|
||||
}
|
||||
|
||||
func TestValidationReportDeliveryErrorsAtEachWriteBoundary(t *testing.T) {
|
||||
reportErr := errors.New("report failed")
|
||||
diagnosticErr := errors.New("diagnostic failed")
|
||||
app, _ := commandkit.New(commandkit.Identity{Name: "example"})
|
||||
acquire := func(context.Context, commandkit.ConfigRequest, struct{}) (commandkit.Acquired[commandkit.ConfigView], error) {
|
||||
return commandkit.Acquired[commandkit.ConfigView]{}, nil
|
||||
}
|
||||
if err := commandkit.RegisterConfig(app, commandkit.ConfigSuite[struct{}]{Scopes: []commandkit.ConfigScope[struct{}]{
|
||||
{Name: "app", Operations: []commandkit.ConfigOperation{commandkit.ConfigValidate}, Acquire: acquire},
|
||||
{Name: "promptkit", Operations: []commandkit.ConfigOperation{commandkit.ConfigValidate}, Acquire: acquire},
|
||||
}}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, format := range []string{"text", "yaml"} {
|
||||
writes := 2
|
||||
if format == "yaml" {
|
||||
writes = 3
|
||||
}
|
||||
for at := 1; at <= writes; at++ {
|
||||
for _, brokenErr := range []bool{false, true} {
|
||||
out := &validationBoundaryWriter{failAt: at, err: reportErr}
|
||||
errStream := io.Writer(io.Discard)
|
||||
if brokenErr {
|
||||
errStream = failingWriter{diagnosticErr}
|
||||
}
|
||||
result := app.Execute(context.Background(), []string{"config", "validate", "--scope", "all", "--format", format}, commandkit.Streams{Out: out, Err: errStream})
|
||||
if result.Kind != commandkit.OutcomeOperational || !result.ReportIncomplete || !errors.Is(result.Cause, reportErr) || !errors.Is(result.DeliveryError, reportErr) || (brokenErr && !errors.Is(result.DeliveryError, diagnosticErr)) {
|
||||
t.Fatalf("format=%s at=%d brokenErr=%t result=%#v", format, at, brokenErr, result)
|
||||
}
|
||||
if at > 1 && out.written.Len() == 0 {
|
||||
t.Fatal("lost completed prefix")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
failedApp, _ := commandkit.New(commandkit.Identity{Name: "example"})
|
||||
if err := commandkit.RegisterConfig(failedApp, commandkit.ConfigSuite[struct{}]{Scopes: []commandkit.ConfigScope[struct{}]{{Name: "app", Operations: []commandkit.ConfigOperation{commandkit.ConfigValidate}, Acquire: func(context.Context, commandkit.ConfigRequest, struct{}) (commandkit.Acquired[commandkit.ConfigView], error) {
|
||||
return commandkit.Acquired[commandkit.ConfigView]{}, reportErr
|
||||
}}}}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
result := failedApp.Execute(context.Background(), []string{"config", "validate"}, commandkit.Streams{})
|
||||
if !errors.Is(result.Cause, reportErr) || result.DeliveryError != nil {
|
||||
t.Fatalf("acquisition failure=%#v", result)
|
||||
}
|
||||
}
|
||||
|
||||
type validationBoundaryWriter struct {
|
||||
failAt, calls int
|
||||
err error
|
||||
written bytes.Buffer
|
||||
}
|
||||
|
||||
func (w *validationBoundaryWriter) Write(p []byte) (int, error) {
|
||||
w.calls++
|
||||
if w.calls == w.failAt {
|
||||
return 0, w.err
|
||||
}
|
||||
return w.written.Write(p)
|
||||
}
|
||||
|
||||
@@ -57,6 +57,9 @@ architecture. The issues below affect behavior within the implemented structure.
|
||||
- R-04 complete: every help entry form finalizes report failures through the
|
||||
common diagnostic presentation path once.
|
||||
|
||||
- R-05 complete: validation header/record write failures retain `DeliveryError`
|
||||
separately from acquisition failures and combined diagnostic-stream failures.
|
||||
|
||||
## Findings and priority
|
||||
|
||||
P1 means a supported invocation is blocked or its report content is corrupted.
|
||||
|
||||
@@ -68,11 +68,15 @@ func runConfigValidation[C any](invocation Invocation, suite ConfigSuite[C], inp
|
||||
}
|
||||
if input.Format == "yaml" && !writtenRecord {
|
||||
if err := writeReport(invocation.Streams.Out, []byte("scopes:\n")); err != nil {
|
||||
return validationFailure(err, diagnostics, true)
|
||||
outcome := validationFailure(err, diagnostics, true)
|
||||
outcome.DeliveryError = err
|
||||
return outcome
|
||||
}
|
||||
}
|
||||
if err := writeReport(invocation.Streams.Out, record); err != nil {
|
||||
return validationFailure(err, diagnostics, true)
|
||||
outcome := validationFailure(err, diagnostics, true)
|
||||
outcome.DeliveryError = err
|
||||
return outcome
|
||||
}
|
||||
writtenRecord = true
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user