Separate show cancellation from rendering errors

This commit is contained in:
2026-11-07 15:42:22 +00:00
parent 552b91c0a2
commit 4346b840db
4 changed files with 56 additions and 3 deletions

View File

@@ -1,6 +1,6 @@
# Consolidated audit catalog # Consolidated audit catalog
Status: **23 distinct open findings: one P2 and 22 P3**, consolidated from 30 Status: **22 distinct open findings: 22 P3**, consolidated from 30
numbered entries in seven audit documents. Reviewed audit state: numbered entries in seven audit documents. Reviewed audit state:
`db732e3a1de55c844f77870e1c6db683c2685d5d`. `db732e3a1de55c844f77870e1c6db683c2685d5d`.
@@ -122,6 +122,11 @@ Also noted in the [documentation cross-references](13-documentation.md#previousl
## F05 — P2: Configuration-show cancellation is also recorded as a rendering failure ## F05 — P2: Configuration-show cancellation is also recorded as a rendering failure
**Complete.** A cancellation checkpoint stops buffered show preparation without
reporting cancellation as a render error. A regression failed before the fix;
focused tests now verify no partial output, cancellation identity, and retained
native rendering errors when both occur.
ckconfig checks cancellation during buffered show preparation inside the ckconfig checks cancellation during buffered show preparation inside the
`RenderOutput` callback. Returning that cancellation from the renderer records `RenderOutput` callback. Returning that cancellation from the renderer records
both the correct operation cause and a false presentation failure, conflating both the correct operation cause and a false presentation failure, conflating

View File

@@ -1,6 +1,6 @@
# Audit remediation implementation plan # Audit remediation implementation plan
Status: in progress; Stages 1–4 complete. Status: in progress; Stages 1–5 complete.
Baseline: `d492c7747175c01a53261206c2b4356e53453621`. Before this sprint, Baseline: `d492c7747175c01a53261206c2b4356e53453621`. Before this sprint,
intervening changes recorded decisions and retired the completed older intervening changes recorded decisions and retired the completed older
@@ -223,6 +223,12 @@ Complete when empty successful acquisition cannot masquerade as prepared preview
## Stage 5 — Separate show preparation cancellation from rendering failures ## Stage 5 — Separate show preparation cancellation from rendering failures
**Complete.** Buffered show preparation carries cancellation outside managed
rendering without emitting the incomplete report. The two-scope regression
failed against the original behavior and now passes, including simultaneous
native rendering failure; focused show, buffered cancellation and write-failure
tests passed in the offline aligned Go 1.27.0 workspace with `-mod=readonly`.
Findings: F05. Source: ckconfig `show.go`; reference `operations.go`'s Findings: F05. Source: ckconfig `show.go`; reference `operations.go`'s
renderPreparedConfigReport checkpoint pattern. renderPreparedConfigReport checkpoint pattern.

View File

@@ -1,6 +1,7 @@
package ckconfig package ckconfig
import ( import (
"bytes"
"context" "context"
"errors" "errors"
"io" "io"
@@ -67,3 +68,39 @@ func TestBufferedReportDoesNotRejectCancellationAfterDeliveryStarts(t *testing.T
t.Fatalf("post-delivery cancellation changed the operation outcome: %#v", got) t.Fatalf("post-delivery cancellation changed the operation outcome: %#v", got)
} }
} }
type effectiveWriterFunc func(io.Writer) error
func (write effectiveWriterFunc) WriteEffective(w io.Writer) error { return write(w) }
func TestShowCancellationDuringBufferedPreparation(t *testing.T) {
renderErr := errors.New("native rendering failed")
for _, tc := range []struct {
name string
renderErr error
}{{name: "cancellation"}, {name: "cancellation and rendering failure", renderErr: renderErr}} {
t.Run(tc.name, func(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
acquired, secondWrites := 0, 0
scope := func(name string, writer EffectiveWriter) Scope[struct{}, struct{}] {
return Scope[struct{}, struct{}]{Name: name, Operations: []Operation{Show}, Acquire: func(context.Context, Request, struct{}, struct{}) (commandkit.Acquired[View], error) {
acquired++
return commandkit.Acquired[View]{Value: View{Effective: writer}}, nil
}}
}
app := newConfigTestApp(t, Command(Suite[struct{}, struct{}]{Scopes: []Scope[struct{}, struct{}]{
scope("app", effectiveWriterFunc(func(w io.Writer) error { _, _ = io.WriteString(w, "name: app\n"); cancel(); return tc.renderErr })),
scope("promptkit", effectiveWriterFunc(func(w io.Writer) error { secondWrites++; _, err := io.WriteString(w, "name: promptkit\n"); return err })),
}}))
var output bytes.Buffer
got := app.Execute(ctx, []string{"config", "show", "--scope=all"}, commandkit.Streams{Out: &output, Err: io.Discard})
if acquired != 2 || secondWrites != 0 || output.Len() != 0 || !errors.Is(got.Cause, context.Canceled) || got.Presentation.Output.WriteError != nil {
t.Fatalf("acquired=%d second=%d output=%q outcome=%#v", acquired, secondWrites, output.String(), got)
}
if tc.renderErr == nil && got.Presentation.Output.RenderError != nil || tc.renderErr != nil && !errors.Is(got.Presentation.Output.RenderError, tc.renderErr) {
t.Fatalf("render evidence = %#v", got.Presentation.Output)
}
})
}
}

View File

@@ -66,11 +66,13 @@ func runConfigShow[I, S any](invocation commandkit.Invocation, plan *configOpera
return configFailure("Configuration show was canceled.", commandkit.Explanation{}, err) return configFailure("Configuration show was canceled.", commandkit.Explanation{}, err)
} }
var value any var value any
var canceled error
err := reporter.RenderOutput(func(io.Writer) error { err := reporter.RenderOutput(func(io.Writer) error {
values := make(map[string]any, len(documents)) values := make(map[string]any, len(documents))
for _, document := range documents { for _, document := range documents {
if err := invocation.Context.Err(); err != nil { if err := invocation.Context.Err(); err != nil {
return err canceled = err
return nil
} }
var native bytes.Buffer var native bytes.Buffer
if err := document.view.Effective.WriteEffective(&native); err != nil { if err := document.view.Effective.WriteEffective(&native); err != nil {
@@ -89,6 +91,9 @@ func runConfigShow[I, S any](invocation commandkit.Invocation, plan *configOpera
} }
return nil return nil
}) })
if canceled != nil {
return configFailure("Configuration show was canceled.", commandkit.Explanation{}, canceled)
}
if err != nil { if err != nil {
if canceled := invocation.Context.Err(); canceled != nil { if canceled := invocation.Context.Err(); canceled != nil {
return configFailure("Configuration show was canceled.", commandkit.Explanation{}, canceled) return configFailure("Configuration show was canceled.", commandkit.Explanation{}, canceled)