Reject missing prepared requests

This commit is contained in:
2026-11-07 15:41:18 +00:00
parent 1fc6409f95
commit 552b91c0a2
4 changed files with 56 additions and 2 deletions

View File

@@ -1,6 +1,6 @@
# Consolidated audit catalog # Consolidated audit catalog
Status: **24 distinct open findings: two P2 and 22 P3**, consolidated from 30 Status: **23 distinct open findings: one P2 and 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`.
@@ -104,6 +104,11 @@ Also noted in the [documentation cross-references](13-documentation.md#previousl
## F04 — P2: Prepared-request preview can succeed without a prepared result ## F04 — P2: Prepared-request preview can succeed without a prepared result
**Complete.** The command rejects a missing prepared request after provider and
cancellation checks, preserving acquired evidence. Its public-command regression
failed against the original behavior; focused preparation tests pass with valid,
missing, provider-error and canceled acquisitions.
The ckprompt prepared-request handler accepts an empty `Acquired` value with a The ckprompt prepared-request handler accepts an empty `Acquired` value with a
nil error from `PrepareRequest`. It renders `null` and reports success despite nil error from `PrepareRequest`. It renders `null` and reports success despite
having no prepared request. having no prepared request.

View File

@@ -1,6 +1,6 @@
# Audit remediation implementation plan # Audit remediation implementation plan
Status: in progress; Stages 1–3 complete. Status: in progress; Stages 1–4 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
@@ -200,6 +200,12 @@ open for the remaining parsing and binding wrappers.
## Stage 4 — Reject missing prepared-request results ## Stage 4 — Reject missing prepared-request results
**Complete.** Missing prepared requests produce an operational failure while
retaining diagnostics and explanations. The public-command regression reproduced
the original successful null preview; focused preparation tests now pass in the
offline aligned workspace with Go 1.27.0 and read-only module resolution, covering
valid results, provider errors, and cancellation as well.
Findings: F04. Source: ckprompt `preparation.go`. Findings: F04. Source: ckprompt `preparation.go`.
1. After the existing provider-error and cancellation handling in 1. After the existing provider-error and cancellation handling in

View File

@@ -100,6 +100,9 @@ func runRequestPreparation[I, S any](invocation commandkit.Invocation, suite Sui
if err := promptKitContextError(invocation.Context); err != nil { if err := promptKitContextError(invocation.Context); err != nil {
return definitionFailure(fmt.Sprintf("Preparation of the PromptKit request for prompt %q was canceled.", input.promptID), acquired.Explanation, err) return definitionFailure(fmt.Sprintf("Preparation of the PromptKit request for prompt %q was canceled.", input.promptID), acquired.Explanation, err)
} }
if acquired.Value == nil {
return definitionFailure("PromptKit request preparation returned no result.", acquired.Explanation, nil)
}
_ = invocation.Reporter().Report(commandkit.Report{Value: acquired.Value}) _ = invocation.Reporter().Report(commandkit.Report{Value: acquired.Value})
return commandkit.Success() return commandkit.Success()
} }

View File

@@ -105,3 +105,43 @@ func TestNativePreparationBuilderAndErrorIdentity(t *testing.T) {
t.Fatalf("native error = %v", err) t.Fatalf("native error = %v", err)
} }
} }
func TestPreparationCommandRejectsMissingResultAndRetainsEvidence(t *testing.T) {
cause := errors.New("provider failure")
for _, tc := range []struct {
name string
providerErr error
cancel bool
}{
{name: "missing result"}, {name: "provider error", providerErr: cause}, {name: "cancellation", cancel: true},
} {
t.Run(tc.name, func(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
app := newPromptTestApp(t, Command(Suite[struct{}, struct{}]{PrepareRequest: func(context.Context, PreparationRequest, struct{}, struct{}) (commandkit.Acquired[*promptkit.PreparedRun], error) {
if tc.cancel {
cancel()
}
acquired := commandkit.Acquired[*promptkit.PreparedRun]{
Diagnostics: []commandkit.Diagnostic{{Severity: commandkit.SeverityWarning, Write: func(w io.Writer) error { _, err := io.WriteString(w, "provider diagnostic\n"); return err }}},
Explanation: commandkit.Explanation{Message: "provider explanation"},
}
if tc.providerErr != nil {
acquired.Value = &promptkit.PreparedRun{PromptID: "briefing"}
}
return acquired, tc.providerErr
}}))
var output, diagnostics bytes.Buffer
result := app.Execute(ctx, []string{"promptkit", "show", "prepared", "briefing", "--format=json", "--verbose"}, commandkit.Streams{Out: &output, Err: &diagnostics})
if result.Kind != commandkit.OutcomeOperational || output.Len() != 0 || !bytes.Contains(diagnostics.Bytes(), []byte("provider diagnostic")) || !bytes.Contains(diagnostics.Bytes(), []byte("provider explanation")) {
t.Fatalf("result=%#v output=%q diagnostics=%q", result, output.String(), diagnostics.String())
}
if tc.providerErr != nil && !errors.Is(result.Cause, tc.providerErr) {
t.Fatalf("provider cause = %v", result.Cause)
}
if tc.cancel && !errors.Is(result.Cause, context.Canceled) {
t.Fatalf("cancellation cause = %v", result.Cause)
}
})
}
}