Restore cancellation checks before report delivery

This commit is contained in:
2026-10-26 14:32:03 +00:00
parent 13a4d4eacd
commit de2266af75
9 changed files with 193 additions and 57 deletions

View File

@@ -414,15 +414,9 @@ func runCatalogList[T any, I, S any](
return definitionFailure("PromptKit catalog listing was canceled.", acquired.Explanation, err)
}
}
_ = invocation.Reporter().RenderOutput(func(writer io.Writer) error {
output, err := encode(acquired.Value)
if err != nil {
return err
}
_, err = writer.Write(output)
return err
return renderPreparedPromptReport(invocation, acquired.Explanation, func() ([]byte, error) {
return encode(acquired.Value)
})
return commandkit.Success()
}
func runDefinitionInspection[I, S any](invocation commandkit.Invocation, suite Suite[I, S], input *definitionInput[I], prompt bool) commandkit.Outcome {
@@ -456,7 +450,7 @@ func runDefinitionInspection[I, S any](invocation commandkit.Invocation, suite S
return definitionFailure(fmt.Sprintf("Inspection of PromptKit prompt %q was canceled.", input.id), acquired.Explanation, err)
}
}
return renderPromptReport(invocation, acquired.Value, input.format)
return renderPromptReport(invocation, acquired.Value, input.format, acquired.Explanation)
}
request := ProfileRequest{Selection: input.selection, ID: input.id}
@@ -473,7 +467,7 @@ func runDefinitionInspection[I, S any](invocation commandkit.Invocation, suite S
return definitionFailure(fmt.Sprintf("Inspection of PromptKit profile %q was canceled.", input.id), acquired.Explanation, err)
}
}
return renderProfileReport(invocation, acquired.Value, input.format)
return renderProfileReport(invocation, acquired.Value, input.format, acquired.Explanation)
}
type promptInputReport struct {
@@ -659,37 +653,50 @@ func runRequirementsInspection[I, S any](invocation commandkit.Invocation, suite
return definitionFailure(fmt.Sprintf("Requirements inspection for PromptKit prompt %q was canceled.", input.inspection.PromptID), acquired.Explanation, err)
}
}
_ = invocation.Reporter().RenderOutput(func(writer io.Writer) error {
return encodePromptYAML(writer, requirementsReportFromInspection(acquired.Value))
return renderPreparedPromptReport(invocation, acquired.Explanation, func() ([]byte, error) {
return encodePromptYAMLBytes(requirementsReportFromInspection(acquired.Value))
})
return commandkit.Success()
}
func renderPromptReport(invocation commandkit.Invocation, inspection *promptkit.PromptInspection, format string) commandkit.Outcome {
_ = invocation.Reporter().RenderOutput(func(writer io.Writer) error {
func renderPromptReport(invocation commandkit.Invocation, inspection *promptkit.PromptInspection, format string, explanation commandkit.Explanation) commandkit.Outcome {
return renderPreparedPromptReport(invocation, explanation, func() ([]byte, error) {
report := promptReportFromInspection(inspection)
if format == "yaml" {
return encodePromptYAML(writer, report)
return encodePromptYAMLBytes(report)
}
_, err := io.WriteString(writer, formatPromptText(report))
return err
return []byte(formatPromptText(report)), nil
})
return commandkit.Success()
}
func renderProfileReport(invocation commandkit.Invocation, inspection *promptkit.ProfileInspection, format string) commandkit.Outcome {
_ = invocation.Reporter().RenderOutput(func(writer io.Writer) error {
func renderProfileReport(invocation commandkit.Invocation, inspection *promptkit.ProfileInspection, format string, explanation commandkit.Explanation) commandkit.Outcome {
return renderPreparedPromptReport(invocation, explanation, func() ([]byte, error) {
report := profileReportFromInspection(inspection)
if format == "yaml" {
return encodePromptYAML(writer, report)
return encodePromptYAMLBytes(report)
}
text, err := formatProfileText(report)
return []byte(text), err
})
}
// These reports were already prepared before delivery. Keep that boundary
// inside managed rendering, with cancellation owned by the command outcome.
func renderPreparedPromptReport(invocation commandkit.Invocation, explanation commandkit.Explanation, prepare func() ([]byte, error)) commandkit.Outcome {
var canceled error
_ = invocation.Reporter().RenderOutput(func(writer io.Writer) error {
output, err := prepare()
if err != nil {
return err
}
_, err = io.WriteString(writer, text)
if canceled = promptKitContextError(invocation.Context); canceled != nil {
return nil
}
_, err = writer.Write(output)
return err
})
if canceled != nil {
return definitionFailure("PromptKit report preparation was canceled.", explanation, canceled)
}
return commandkit.Success()
}

View File

@@ -0,0 +1,50 @@
package ckprompt
import (
"context"
"errors"
"testing"
commandkit "gitea.maximumdirect.net/eric/CommandKit"
promptkit "gitea.maximumdirect.net/eric/promptkit"
)
type cancelDuringYAML struct{ cancel context.CancelFunc }
func (value cancelDuringYAML) MarshalYAML() (any, error) {
value.cancel()
return "prepared", nil
}
func TestInspectionCancellationDuringPreparationPreventsDelivery(t *testing.T) {
for _, format := range []string{"text", "yaml"} {
for _, mode := range []string{"ordinary", "quiet"} {
t.Run(format+"/"+mode, func(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
app := newPromptTestApp(t, Command(Suite[struct{}, struct{}]{
InspectProfile: func(context.Context, ProfileRequest, struct{}, struct{}) (commandkit.Acquired[*promptkit.ProfileInspection], error) {
return commandkit.Acquired[*promptkit.ProfileInspection]{Value: &promptkit.ProfileInspection{
ProfileID: "demo",
EffectiveModelParams: promptkit.ExecutionTarget{ExtraParams: map[string]any{
"controlled": cancelDuringYAML{cancel},
}},
}}, nil
},
}))
args := []string{"promptkit", "show", "profile", "demo", "--format", format}
if mode == "quiet" {
args = append(args, "--quiet")
}
output := &countingWriter{}
got := app.Execute(ctx, args, commandkit.Streams{Out: output})
if got.Kind != commandkit.OutcomeOperational || got.Status != 1 || !errors.Is(got.Cause, context.Canceled) || output.calls != 0 {
t.Fatalf("outcome=%#v writes=%d", got, output.calls)
}
if got.Presentation.Output.RenderError != nil || got.Presentation.Output.WriteError != nil {
t.Fatalf("operation cancellation became a presentation failure: %#v", got.Presentation.Output)
}
})
}
}
}

View File

@@ -3,7 +3,6 @@ package ckprompt
import (
"errors"
"fmt"
"io"
commandkit "gitea.maximumdirect.net/eric/CommandKit"
promptkit "gitea.maximumdirect.net/eric/promptkit"
@@ -122,8 +121,7 @@ func runRequestValidation[I, S any](invocation commandkit.Invocation, suite Suit
if err := promptKitContextError(invocation.Context); err != nil {
return definitionFailure(fmt.Sprintf("Validation of the PromptKit request for prompt %q was canceled.", input.promptID), acquired.Explanation, err)
}
_ = invocation.Reporter().RenderOutput(func(writer io.Writer) error {
return encodePromptYAML(writer, requestValidationReport{PromptID: input.promptID, Valid: true})
return renderPreparedPromptReport(invocation, acquired.Explanation, func() ([]byte, error) {
return encodePromptYAMLBytes(requestValidationReport{PromptID: input.promptID, Valid: true})
})
return commandkit.Success()
}