diff --git a/docs/roadmap/audit/catalog.md b/docs/roadmap/audit/catalog.md index 9ebf3bd..fc950ea 100644 --- a/docs/roadmap/audit/catalog.md +++ b/docs/roadmap/audit/catalog.md @@ -1,6 +1,6 @@ # 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: `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 +**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 nil error from `PrepareRequest`. It renders `null` and reports success despite having no prepared request. diff --git a/docs/roadmap/implementation.md b/docs/roadmap/implementation.md index 54b442b..3596db8 100644 --- a/docs/roadmap/implementation.md +++ b/docs/roadmap/implementation.md @@ -1,6 +1,6 @@ # Audit remediation implementation plan -Status: in progress; Stages 1–3 complete. +Status: in progress; Stages 1–4 complete. Baseline: `d492c7747175c01a53261206c2b4356e53453621`. Before this sprint, 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 +**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`. 1. After the existing provider-error and cancellation handling in diff --git a/integrations/ckprompt/preparation.go b/integrations/ckprompt/preparation.go index a7dda5f..ee8fb09 100644 --- a/integrations/ckprompt/preparation.go +++ b/integrations/ckprompt/preparation.go @@ -100,6 +100,9 @@ func runRequestPreparation[I, S any](invocation commandkit.Invocation, suite Sui 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) } + if acquired.Value == nil { + return definitionFailure("PromptKit request preparation returned no result.", acquired.Explanation, nil) + } _ = invocation.Reporter().Report(commandkit.Report{Value: acquired.Value}) return commandkit.Success() } diff --git a/integrations/ckprompt/preparation_test.go b/integrations/ckprompt/preparation_test.go index eca9dfe..7a48c54 100644 --- a/integrations/ckprompt/preparation_test.go +++ b/integrations/ckprompt/preparation_test.go @@ -105,3 +105,43 @@ func TestNativePreparationBuilderAndErrorIdentity(t *testing.T) { 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) + } + }) + } +}