Separate PromptKit messages from terminal context

This commit is contained in:
2026-10-26 03:26:14 +00:00
parent c0e0010683
commit 37dfea5b25
5 changed files with 218 additions and 145 deletions

View File

@@ -1,6 +1,6 @@
# Invocation reporting implementation plan
Status: Stages 1–12 complete. Stage 13 is next.
Status: Stages 1–13 complete. Stage 14 is next.
## Objective and authority
@@ -496,7 +496,7 @@ old invocation API and is scheduled for migration in Stage 17.
## Stage 13: Migrate PromptKit acquisition adapters
Status: pending.
Status: complete.
Primary files: [adapters.go](../../integrations/ckprompt/adapters.go),
[validation.go](../../integrations/ckprompt/validation.go),
@@ -521,6 +521,18 @@ Validation: establish correct forwarding, passive data, separated context,
and supported error identity. Preserve offline fixtures and existing builder/
acquisition lifecycle protections. No live provider or credentials are needed.
PromptKit engine loading now separates independent warning batches from native
terminal records and carries both through catalogs, inspection, requirements,
ConfigScope comparison, concrete request validation, and smoke acquisition.
Native rendering retains scope/side headings, source legends, and diagnostic
locations; original errors and reports returned with errors remain available.
Offline adapter tests cover mixed record separation, writer propagation,
comparison-side context, error identity, and existing request lifetimes. The
ckprompt suite cannot execute yet: compiler diagnostics identify legacy
reporting calls in `command.go`, `smoke_command.go`, and their tests, which are
scheduled for Stages 14 and 15. No compile errors were reported in the migrated
adapter files; Stage 20 will run the assembled suite.
## Stage 14: Migrate PromptKit listing and inspection commands
Status: pending.

View File

@@ -88,26 +88,33 @@ type Suite[I, S any] struct {
// CheckInput validates selected inspection, requirements, validation, smoke,
// or catalog input before state creation.
CheckInput func(ckconfig.Selection, I) error
// InspectPrompt inspects the selected prompt after input and state preparation.
// InspectPrompt inspects the selected prompt after input and state
// preparation. Its acquisition carries independent loader diagnostics and
// terminal context alongside any native inspection result.
InspectPrompt func(context.Context, PromptRequest, I, S) (commandkit.Acquired[*promptkit.PromptInspection], error)
// InspectProfile inspects the selected profile after input and state preparation.
// InspectProfile inspects the selected profile after input and state
// preparation. Its acquisition carries independent loader diagnostics and
// terminal context alongside any native inspection result.
InspectProfile func(context.Context, ProfileRequest, I, S) (commandkit.Acquired[*promptkit.ProfileInspection], error)
// InspectRequirements inspects native PromptKit requirements after input and
// state preparation. It is optional; a nil callback omits
// `promptkit requirements show` without disabling validation or other
// capabilities.
// state preparation. Its acquisition carries independent loader diagnostics
// and terminal context alongside any native inspection result. It is
// optional; a nil callback omits `promptkit requirements show` without
// disabling validation or other capabilities.
InspectRequirements func(context.Context, RequirementsRequest, I, S) (commandkit.Acquired[*promptkit.RequirementInspection], error)
// ValidateRequest validates a concrete PromptKit request after input and
// state preparation. It is optional; a nil callback omits
// `promptkit requirements validate` without disabling discovery or other
// capabilities. The acquisition carries diagnostics and an empty value on
// success; PromptKit errors are returned unchanged where supported.
// capabilities. The acquisition carries independent diagnostics, terminal
// context, and an empty value on success; PromptKit errors are returned
// unchanged where supported.
ValidateRequest func(context.Context, ValidationRequest, I, S) (commandkit.Acquired[struct{}], error)
// SmokeTest runs one concrete native smoke-test operation after input and
// state preparation. It is optional; nil omits only the smoke-test command.
// The acquisition retains a native report even when the callback also
// returns an error. For `test all`, CommandKit calls the captured callback
// sequentially for each selected kind after preparing input and state once.
// The acquisition retains a native report, independent diagnostics, and
// terminal context even when the callback also returns an error. For
// `test all`, CommandKit calls the captured callback sequentially for each
// selected kind after preparing input and state once.
SmokeTest func(context.Context, SmokeRequest, I, S) (commandkit.Acquired[*promptkit.SmokeTestReport], error)
// ListPrompts lists prompt identities after input and state preparation.
// It is optional independently of the other catalogs; nil omits only this
@@ -143,9 +150,10 @@ type Suite[I, S any] struct {
// selection. A nil builder forwards the seed request unchanged. A builder
// failure prevents engine loading and preserves its supported error identity.
// Native validation errors are returned with supported identity, and
// configuration diagnostics from engine loading remain in the acquisition. A
// nil loader is retained as a suite wiring issue and does not install a
// validation callback. Callback references and objects remain caller-owned.
// configuration diagnostics and terminal context from engine loading remain
// separate in the acquisition. A nil loader is retained as a suite wiring
// issue and does not install a validation callback. Callback references and
// objects remain caller-owned.
func WithRequestValidation[I, S any](
suite Suite[I, S],
load func(context.Context, ckconfig.Selection, I, S) (*promptkit.Engine, error),
@@ -175,34 +183,39 @@ func WithRequestValidation[I, S any](
return commandkit.Acquired[struct{}]{}, fmt.Errorf("PromptKit request validation after building: %w", err)
}
engine, _, diagnostics, err := loadPromptKitEngine(ctx, request.Selection, input, state, "", load)
engine, _, messages, err := loadPromptKitEngine(ctx, request.Selection, input, state, "", load)
if err != nil {
return commandkit.Acquired[struct{}]{Diagnostics: diagnostics}, err
return commandkit.Acquired[struct{}]{Diagnostics: messages.diagnostics, Explanation: messages.explanation}, err
}
if err := promptKitContextError(ctx); err != nil {
return commandkit.Acquired[struct{}]{Diagnostics: diagnostics}, fmt.Errorf("PromptKit request validation before native validation: %w", err)
return commandkit.Acquired[struct{}]{Diagnostics: messages.diagnostics, Explanation: messages.explanation}, fmt.Errorf("PromptKit request validation before native validation: %w", err)
}
if err := engine.ValidateRequest(ctx, concreteRequest); err != nil {
return commandkit.Acquired[struct{}]{Diagnostics: diagnostics}, err
return commandkit.Acquired[struct{}]{Diagnostics: messages.diagnostics, Explanation: messages.explanation}, err
}
if err := promptKitContextError(ctx); err != nil {
return commandkit.Acquired[struct{}]{Diagnostics: diagnostics}, fmt.Errorf("PromptKit request validation after native validation: %w", err)
return commandkit.Acquired[struct{}]{Diagnostics: messages.diagnostics, Explanation: messages.explanation}, fmt.Errorf("PromptKit request validation after native validation: %w", err)
}
return commandkit.Acquired[struct{}]{Diagnostics: diagnostics}, nil
return commandkit.Acquired[struct{}]{Diagnostics: messages.diagnostics, Explanation: messages.explanation}, nil
}
return suite
}
type engineLoader[I, S any] func(context.Context, ckconfig.Selection, I, S) (*promptkit.Engine, error)
type promptKitAcquisition struct {
diagnostics []commandkit.Diagnostic
explanation commandkit.Explanation
}
// ConfigScope adapts PromptKit's captured configuration inspection to the
// standard configuration commands, including native comparison when the scope
// declares Diff. The loader is not called during declaration construction or
// installation; it is called only when a config operation selects the
// promptkit scope, with that operation's input and shared state. PromptKit
// diagnostics and supported error identity are returned through ckconfig's
// acquisition and comparison contracts.
// independent diagnostics, terminal context, and supported error identity are
// returned through ckconfig's acquisition and comparison contracts.
func ConfigScope[I, S any](load func(context.Context, ckconfig.Selection, I, S) (*promptkit.Engine, error)) ckconfig.Scope[I, S] {
scope := ckconfig.Scope[I, S]{
Name: "promptkit",
@@ -214,9 +227,9 @@ func ConfigScope[I, S any](load func(context.Context, ckconfig.Selection, I, S)
}
scope.Acquire = func(ctx context.Context, request ckconfig.Request, input I, state S) (commandkit.Acquired[ckconfig.View], error) {
_, inspection, diagnostics, err := loadPromptKitEngine(ctx, request.Selection, input, state, "", load)
_, inspection, messages, err := loadPromptKitEngine(ctx, request.Selection, input, state, "", load)
if err != nil {
return commandkit.Acquired[ckconfig.View]{Diagnostics: diagnostics}, err
return commandkit.Acquired[ckconfig.View]{Diagnostics: messages.diagnostics, Explanation: messages.explanation}, err
}
return commandkit.Acquired[ckconfig.View]{
Value: ckconfig.View{
@@ -224,7 +237,8 @@ func ConfigScope[I, S any](load func(context.Context, ckconfig.Selection, I, S)
Sources: inspection,
Explanation: inspection,
},
Diagnostics: diagnostics,
Diagnostics: messages.diagnostics,
Explanation: messages.explanation,
}, nil
}
scope.Compare = func(ctx context.Context, request ckconfig.ComparisonRequest[I], state S) (commandkit.Acquired[[]configkit.Difference], error) {
@@ -232,31 +246,30 @@ func ConfigScope[I, S any](load func(context.Context, ckconfig.Selection, I, S)
return commandkit.Acquired[[]configkit.Difference]{}, fmt.Errorf("PromptKit scope %q comparison before left load: %w", scope.Name, err)
}
_, leftInspection, leftDiagnostics, err := loadPromptKitEngine(ctx, request.Left.Selection, request.Left.Input, state, "left", load)
diagnostics := append([]commandkit.Diagnostic(nil), leftDiagnostics...)
_, leftInspection, messages, err := loadPromptKitEngine(ctx, request.Left.Selection, request.Left.Input, state, "left", load)
if err != nil {
return commandkit.Acquired[[]configkit.Difference]{Diagnostics: diagnostics}, fmt.Errorf("PromptKit scope %q left comparison load: %w", scope.Name, err)
return commandkit.Acquired[[]configkit.Difference]{Diagnostics: messages.diagnostics, Explanation: messages.explanation}, fmt.Errorf("PromptKit scope %q left comparison load: %w", scope.Name, err)
}
if err := promptKitContextError(ctx); err != nil {
return commandkit.Acquired[[]configkit.Difference]{Diagnostics: diagnostics}, fmt.Errorf("PromptKit scope %q comparison after left load: %w", scope.Name, err)
return commandkit.Acquired[[]configkit.Difference]{Diagnostics: messages.diagnostics, Explanation: messages.explanation}, fmt.Errorf("PromptKit scope %q comparison after left load: %w", scope.Name, err)
}
_, rightInspection, rightDiagnostics, err := loadPromptKitEngine(ctx, request.Right.Selection, request.Right.Input, state, "right", load)
diagnostics = append(diagnostics, rightDiagnostics...)
_, rightInspection, rightMessages, err := loadPromptKitEngine(ctx, request.Right.Selection, request.Right.Input, state, "right", load)
messages = appendPromptKitAcquisition(messages, rightMessages)
if err != nil {
return commandkit.Acquired[[]configkit.Difference]{Diagnostics: diagnostics}, fmt.Errorf("PromptKit scope %q right comparison load: %w", scope.Name, err)
return commandkit.Acquired[[]configkit.Difference]{Diagnostics: messages.diagnostics, Explanation: messages.explanation}, fmt.Errorf("PromptKit scope %q right comparison load: %w", scope.Name, err)
}
if err := promptKitContextError(ctx); err != nil {
return commandkit.Acquired[[]configkit.Difference]{Diagnostics: diagnostics}, fmt.Errorf("PromptKit scope %q comparison before native diff: %w", scope.Name, err)
return commandkit.Acquired[[]configkit.Difference]{Diagnostics: messages.diagnostics, Explanation: messages.explanation}, fmt.Errorf("PromptKit scope %q comparison before native diff: %w", scope.Name, err)
}
differences, err := leftInspection.Diff(rightInspection)
if err != nil {
return commandkit.Acquired[[]configkit.Difference]{Diagnostics: diagnostics}, fmt.Errorf("PromptKit scope %q native configuration diff: %w", scope.Name, err)
return commandkit.Acquired[[]configkit.Difference]{Diagnostics: messages.diagnostics, Explanation: messages.explanation}, fmt.Errorf("PromptKit scope %q native configuration diff: %w", scope.Name, err)
}
return commandkit.Acquired[[]configkit.Difference]{Value: differences, Diagnostics: diagnostics}, nil
return commandkit.Acquired[[]configkit.Difference]{Value: differences, Diagnostics: messages.diagnostics, Explanation: messages.explanation}, nil
}
return scope
}
@@ -280,8 +293,8 @@ func promptKitContextError(ctx context.Context) error {
// shared invocation state to the loader before calling PromptKit's public
// listing or inspection method. Applications can narrow, wrap, or replace these
// callbacks before Command captures them. A nil loader is retained as an
// adapter issue. Each callback returns loader diagnostics with the native
// operation's value and error.
// adapter issue. Each callback returns independent loader diagnostics and
// terminal context alongside the native operation's value and error.
func FromEngine[I, S any](load func(context.Context, ckconfig.Selection, I, S) (*promptkit.Engine, error)) Suite[I, S] {
if load == nil {
return Suite[I, S]{issues: []error{errors.New("PromptKit engine loader is nil")}}
@@ -289,68 +302,68 @@ func FromEngine[I, S any](load func(context.Context, ckconfig.Selection, I, S) (
return Suite[I, S]{
ListPrompts: func(ctx context.Context, request CatalogRequest, input I, state S) (commandkit.Acquired[[]promptkit.PromptIdentity], error) {
engine, _, diagnostics, err := loadPromptKitEngine(ctx, request.Selection, input, state, "", load)
engine, _, messages, err := loadPromptKitEngine(ctx, request.Selection, input, state, "", load)
if err != nil {
return commandkit.Acquired[[]promptkit.PromptIdentity]{Diagnostics: diagnostics}, err
return commandkit.Acquired[[]promptkit.PromptIdentity]{Diagnostics: messages.diagnostics, Explanation: messages.explanation}, err
}
prompts, err := engine.ListPrompts(ctx, request.Filter)
return commandkit.Acquired[[]promptkit.PromptIdentity]{Value: prompts, Diagnostics: diagnostics}, err
return commandkit.Acquired[[]promptkit.PromptIdentity]{Value: prompts, Diagnostics: messages.diagnostics, Explanation: messages.explanation}, err
},
ListProfiles: func(ctx context.Context, request CatalogRequest, input I, state S) (commandkit.Acquired[[]string], error) {
engine, _, diagnostics, err := loadPromptKitEngine(ctx, request.Selection, input, state, "", load)
engine, _, messages, err := loadPromptKitEngine(ctx, request.Selection, input, state, "", load)
if err != nil {
return commandkit.Acquired[[]string]{Diagnostics: diagnostics}, err
return commandkit.Acquired[[]string]{Diagnostics: messages.diagnostics, Explanation: messages.explanation}, err
}
profiles, err := engine.ListProfiles(ctx, request.Filter)
return commandkit.Acquired[[]string]{Value: profiles, Diagnostics: diagnostics}, err
return commandkit.Acquired[[]string]{Value: profiles, Diagnostics: messages.diagnostics, Explanation: messages.explanation}, err
},
ListBackends: func(ctx context.Context, request CatalogRequest, input I, state S) (commandkit.Acquired[[]string], error) {
engine, _, diagnostics, err := loadPromptKitEngine(ctx, request.Selection, input, state, "", load)
engine, _, messages, err := loadPromptKitEngine(ctx, request.Selection, input, state, "", load)
if err != nil {
return commandkit.Acquired[[]string]{Diagnostics: diagnostics}, err
return commandkit.Acquired[[]string]{Diagnostics: messages.diagnostics, Explanation: messages.explanation}, err
}
backends, err := engine.ListBackends(ctx, request.Filter)
return commandkit.Acquired[[]string]{Value: backends, Diagnostics: diagnostics}, err
return commandkit.Acquired[[]string]{Value: backends, Diagnostics: messages.diagnostics, Explanation: messages.explanation}, err
},
ListNamespaces: func(ctx context.Context, request NamespaceRequest, input I, state S) (commandkit.Acquired[[]string], error) {
engine, _, diagnostics, err := loadPromptKitEngine(ctx, request.Selection, input, state, "", load)
engine, _, messages, err := loadPromptKitEngine(ctx, request.Selection, input, state, "", load)
if err != nil {
return commandkit.Acquired[[]string]{Diagnostics: diagnostics}, err
return commandkit.Acquired[[]string]{Diagnostics: messages.diagnostics, Explanation: messages.explanation}, err
}
namespaces, err := engine.ListNamespaces(ctx, request.Kinds...)
return commandkit.Acquired[[]string]{Value: namespaces, Diagnostics: diagnostics}, err
return commandkit.Acquired[[]string]{Value: namespaces, Diagnostics: messages.diagnostics, Explanation: messages.explanation}, err
},
ListCollections: func(ctx context.Context, selection ckconfig.Selection, input I, state S) (commandkit.Acquired[[]promptkit.CollectionInfo], error) {
engine, _, diagnostics, err := loadPromptKitEngine(ctx, selection, input, state, "", load)
engine, _, messages, err := loadPromptKitEngine(ctx, selection, input, state, "", load)
if err != nil {
return commandkit.Acquired[[]promptkit.CollectionInfo]{Diagnostics: diagnostics}, err
return commandkit.Acquired[[]promptkit.CollectionInfo]{Diagnostics: messages.diagnostics, Explanation: messages.explanation}, err
}
collections, err := engine.ListCollections(ctx)
return commandkit.Acquired[[]promptkit.CollectionInfo]{Value: collections, Diagnostics: diagnostics}, err
return commandkit.Acquired[[]promptkit.CollectionInfo]{Value: collections, Diagnostics: messages.diagnostics, Explanation: messages.explanation}, err
},
InspectPrompt: func(ctx context.Context, request PromptRequest, input I, state S) (commandkit.Acquired[*promptkit.PromptInspection], error) {
engine, _, diagnostics, err := loadPromptKitEngine(ctx, request.Selection, input, state, "", load)
engine, _, messages, err := loadPromptKitEngine(ctx, request.Selection, input, state, "", load)
if err != nil {
return commandkit.Acquired[*promptkit.PromptInspection]{Diagnostics: diagnostics}, err
return commandkit.Acquired[*promptkit.PromptInspection]{Diagnostics: messages.diagnostics, Explanation: messages.explanation}, err
}
inspection, err := engine.InspectPrompt(ctx, request.ID)
return commandkit.Acquired[*promptkit.PromptInspection]{Value: inspection, Diagnostics: diagnostics}, err
return commandkit.Acquired[*promptkit.PromptInspection]{Value: inspection, Diagnostics: messages.diagnostics, Explanation: messages.explanation}, err
},
InspectProfile: func(ctx context.Context, request ProfileRequest, input I, state S) (commandkit.Acquired[*promptkit.ProfileInspection], error) {
engine, _, diagnostics, err := loadPromptKitEngine(ctx, request.Selection, input, state, "", load)
engine, _, messages, err := loadPromptKitEngine(ctx, request.Selection, input, state, "", load)
if err != nil {
return commandkit.Acquired[*promptkit.ProfileInspection]{Diagnostics: diagnostics}, err
return commandkit.Acquired[*promptkit.ProfileInspection]{Diagnostics: messages.diagnostics, Explanation: messages.explanation}, err
}
inspection, err := engine.InspectProfile(ctx, request.ID)
return commandkit.Acquired[*promptkit.ProfileInspection]{Value: inspection, Diagnostics: diagnostics}, err
return commandkit.Acquired[*promptkit.ProfileInspection]{Value: inspection, Diagnostics: messages.diagnostics, Explanation: messages.explanation}, err
},
InspectRequirements: func(ctx context.Context, request RequirementsRequest, input I, state S) (commandkit.Acquired[*promptkit.RequirementInspection], error) {
engine, _, diagnostics, err := loadPromptKitEngine(ctx, request.Selection, input, state, "", load)
engine, _, messages, err := loadPromptKitEngine(ctx, request.Selection, input, state, "", load)
if err != nil {
return commandkit.Acquired[*promptkit.RequirementInspection]{Diagnostics: diagnostics}, err
return commandkit.Acquired[*promptkit.RequirementInspection]{Diagnostics: messages.diagnostics, Explanation: messages.explanation}, err
}
inspection, err := engine.InspectRequirements(ctx, request.Inspection)
return commandkit.Acquired[*promptkit.RequirementInspection]{Value: inspection, Diagnostics: diagnostics}, err
return commandkit.Acquired[*promptkit.RequirementInspection]{Value: inspection, Diagnostics: messages.diagnostics, Explanation: messages.explanation}, err
},
}
}
@@ -362,41 +375,44 @@ func loadPromptKitEngine[I, S any](
state S,
side string,
load engineLoader[I, S],
) (*promptkit.Engine, promptkit.ConfigurationInspection, []commandkit.Diagnostic, error) {
) (*promptkit.Engine, promptkit.ConfigurationInspection, promptKitAcquisition, error) {
engine, err := load(ctx, selection, input, state)
if err != nil {
var loadErr *configkit.LoadError
if errors.As(err, &loadErr) {
return nil, promptkit.ConfigurationInspection{}, promptKitDiagnostics("promptkit", side, loadErr.Sources(), loadErr.Diagnostics()), err
messages := promptKitDiagnostics("promptkit", side, loadErr.Sources(), loadErr.Diagnostics())
return nil, promptkit.ConfigurationInspection{}, messages, err
}
return nil, promptkit.ConfigurationInspection{}, nil, err
return nil, promptkit.ConfigurationInspection{}, promptKitAcquisition{}, err
}
if engine == nil {
return nil, promptkit.ConfigurationInspection{}, nil, errors.New("PromptKit configuration loader returned a nil engine")
return nil, promptkit.ConfigurationInspection{}, promptKitAcquisition{}, errors.New("PromptKit configuration loader returned a nil engine")
}
inspection, err := engine.InspectConfiguration()
messages := promptKitDiagnostics("promptkit", side, inspection.Sources(), inspection.Diagnostics())
if err != nil {
return nil, promptkit.ConfigurationInspection{}, nil, err
return nil, inspection, messages, err
}
diagnostics := promptKitDiagnostics("promptkit", side, inspection.Sources(), inspection.Diagnostics())
if ctx != nil {
if err := ctx.Err(); err != nil {
return nil, promptkit.ConfigurationInspection{}, diagnostics, err
return nil, promptkit.ConfigurationInspection{}, messages, err
}
}
return engine, inspection, diagnostics, nil
return engine, inspection, messages, nil
}
func promptKitDiagnostics(scope, side string, sources []configkit.SourceInfo, records []configkit.Diagnostic) []commandkit.Diagnostic {
if len(records) == 0 {
return nil
func appendPromptKitAcquisition(left, right promptKitAcquisition) promptKitAcquisition {
left.diagnostics = append(left.diagnostics, right.diagnostics...)
left.explanation.Details = append(left.explanation.Details, right.explanation.Details...)
if left.explanation.Message == "" {
left.explanation.Message = right.explanation.Message
}
severity := commandkit.SeverityWarning
for _, record := range records {
if record.Severity == configkit.SeverityError {
severity = commandkit.SeverityError
break
}
return left
}
func promptKitDiagnostics(scope, side string, sources []configkit.SourceInfo, records []configkit.Diagnostic) promptKitAcquisition {
if len(records) == 0 {
return promptKitAcquisition{}
}
// Snapshot provider-owned slices before deferring their report to invocation.
sources = append([]configkit.SourceInfo(nil), sources...)
@@ -408,27 +424,48 @@ func promptKitDiagnostics(scope, side string, sources []configkit.SourceInfo, re
}
records[index].Related = append([]configkit.Location(nil), records[index].Related...)
}
return []commandkit.Diagnostic{{
Severity: severity,
Write: func(writer io.Writer) error {
heading := "Configuration scope " + strconv.Quote(scope)
if side != "" {
heading += " (" + side + ")"
}
if _, err := fmt.Fprintf(writer, "%s:\n", heading); err != nil {
var warnings, failures []configkit.Diagnostic
for _, record := range records {
if record.Severity == configkit.SeverityError {
failures = append(failures, record)
} else {
warnings = append(warnings, record)
}
}
messages := promptKitAcquisition{}
if len(warnings) > 0 {
messages.diagnostics = []commandkit.Diagnostic{{
Severity: commandkit.SeverityWarning,
Write: func(writer io.Writer) error {
return writePromptKitDiagnostics(writer, scope, side, sources, warnings)
},
}}
}
if len(failures) > 0 {
messages.explanation = commandkit.Explanation{Details: []commandkit.Detail{func(writer io.Writer) error {
return writePromptKitDiagnostics(writer, scope, side, sources, failures)
}}}
}
return messages
}
func writePromptKitDiagnostics(writer io.Writer, scope, side string, sources []configkit.SourceInfo, records []configkit.Diagnostic) error {
heading := "Configuration scope " + strconv.Quote(scope)
if side != "" {
heading += " (" + side + ")"
}
if _, err := fmt.Fprintf(writer, "%s:\n", heading); err != nil {
return err
}
if len(sources) > 0 {
if _, err := io.WriteString(writer, "Source IDs:\n"); err != nil {
return err
}
for _, source := range sources {
if _, err := fmt.Fprintf(writer, " %s: label=%s, path=%s\n", strconv.Quote(source.ID), strconv.Quote(source.Label), strconv.Quote(source.Path)); err != nil {
return err
}
if len(sources) > 0 {
if _, err := io.WriteString(writer, "Source IDs:\n"); err != nil {
return err
}
for _, source := range sources {
if _, err := fmt.Fprintf(writer, " %s: label=%s, path=%s\n", strconv.Quote(source.ID), strconv.Quote(source.Label), strconv.Quote(source.Path)); err != nil {
return err
}
}
}
return configkit.WriteDiagnostics(writer, records)
},
}}
}
}
return configkit.WriteDiagnostics(writer, records)
}

View File

@@ -239,7 +239,7 @@ func TestWithSmokeTestingRetainsNilLoaderIssueAndChecksCancellationBeforeBuild(t
}
}
func TestWithSmokeTestingRetainsLoaderDiagnosticsAndErrorIdentity(t *testing.T) {
func TestWithSmokeTestingRetainsLoaderExplanationAndErrorIdentity(t *testing.T) {
_, loadErr := configkit.Load[map[string]any](configkit.Bytes("invalid smoke configuration", []byte("broken: [")))
if loadErr == nil {
t.Fatal("invalid configuration unexpectedly loaded")
@@ -249,7 +249,7 @@ func TestWithSmokeTestingRetainsLoaderDiagnosticsAndErrorIdentity(t *testing.T)
}, nil)
acquired, err := suite.SmokeTest(context.Background(), SmokeRequest{}, 0, 0)
var retained *configkit.LoadError
if !errors.Is(err, loadErr) || !errors.As(err, &retained) || retained != loadErr || acquired.Value != nil || len(acquired.Diagnostics) == 0 {
if !errors.Is(err, loadErr) || !errors.As(err, &retained) || retained != loadErr || acquired.Value != nil || len(acquired.Explanation.Details) == 0 {
t.Fatalf("smoke load failure = (%#v, %v)", acquired, err)
}
}
@@ -394,7 +394,7 @@ func TestConfigScopeCompareStopsAfterLeftLoadFailure(t *testing.T) {
}
}
func TestConfigScopeCompareRetainsRightLoadDiagnosticsAndErrorIdentity(t *testing.T) {
func TestConfigScopeCompareRetainsRightLoadExplanationAndErrorIdentity(t *testing.T) {
_, loadErr := configkit.Load[map[string]any](configkit.Bytes("invalid right prompt config", []byte("broken: [")))
if loadErr == nil {
t.Fatal("invalid right configuration unexpectedly loaded")
@@ -415,12 +415,12 @@ func TestConfigScopeCompareRetainsRightLoadDiagnosticsAndErrorIdentity(t *testin
acquired, err := scope.Compare(context.Background(), request, struct{}{})
var retained *configkit.LoadError
if !errors.Is(err, loadErr) || !errors.As(err, &retained) || retained != loadErr || len(calls) != 2 || len(acquired.Diagnostics) == 0 {
if !errors.Is(err, loadErr) || !errors.As(err, &retained) || retained != loadErr || len(calls) != 2 || len(acquired.Explanation.Details) == 0 {
t.Fatalf("right failure = (%#v, %v), calls=%#v", acquired, err, calls)
}
var output bytes.Buffer
if writeErr := acquired.Diagnostics[0].Write(&output); writeErr != nil || !strings.Contains(output.String(), `Configuration scope "promptkit" (right):`) {
t.Fatalf("right diagnostic = %q, %v", output.String(), writeErr)
if writeErr := acquired.Explanation.Details[0](&output); writeErr != nil || !strings.Contains(output.String(), `Configuration scope "promptkit" (right):`) {
t.Fatalf("right explanation = %q, %v", output.String(), writeErr)
}
}
@@ -534,7 +534,7 @@ func TestFromEngineInspectsRequirementsWithNativeRequest(t *testing.T) {
}
}
func TestFromEngineRequirementsRetainLoadDiagnosticsAndSelectionErrors(t *testing.T) {
func TestFromEngineRequirementsRetainLoadExplanationAndSelectionErrors(t *testing.T) {
_, loadErr := configkit.Load[map[string]any](configkit.Bytes("invalid requirements configuration", []byte("broken: [")))
if loadErr == nil {
t.Fatal("invalid configuration unexpectedly loaded")
@@ -544,11 +544,8 @@ func TestFromEngineRequirementsRetainLoadDiagnosticsAndSelectionErrors(t *testin
})
acquired, err := loadFailure.InspectRequirements(context.Background(), RequirementsRequest{}, 0, 0)
var retained *configkit.LoadError
if !errors.As(err, &retained) || retained != loadErr || len(acquired.Diagnostics) == 0 || acquired.Value != nil {
t.Fatalf("requirements load failure = (%#v, %v), want original ConfigKit error and diagnostics", acquired, err)
}
if acquired.Diagnostics[0].Severity != commandkit.SeverityError {
t.Fatalf("load diagnostics = %#v, want error severity", acquired.Diagnostics)
if !errors.As(err, &retained) || retained != loadErr || len(acquired.Explanation.Details) == 0 || acquired.Value != nil {
t.Fatalf("requirements load failure = (%#v, %v), want original ConfigKit error and terminal context", acquired, err)
}
nilEngine := FromEngine[int, int](func(context.Context, ckconfig.Selection, int, int) (*promptkit.Engine, error) {
@@ -651,7 +648,7 @@ func TestFromEngineListsNativeCatalogsWithInvocationValues(t *testing.T) {
}
}
func TestFromEngineCatalogsRetainLoadDiagnosticsAndErrorIdentity(t *testing.T) {
func TestFromEngineCatalogsRetainLoadExplanationAndErrorIdentity(t *testing.T) {
_, loadErr := configkit.Load[map[string]any](configkit.Bytes("invalid catalog configuration", []byte("broken: [")))
if loadErr == nil {
t.Fatal("invalid configuration unexpectedly loaded")
@@ -666,42 +663,42 @@ func TestFromEngineCatalogsRetainLoadDiagnosticsAndErrorIdentity(t *testing.T) {
{name: "prompts", call: func() (int, error) {
acquired, err := suite.ListPrompts(context.Background(), CatalogRequest{}, 0, 0)
var retained *configkit.LoadError
if !errors.As(err, &retained) || retained != loadErr || len(acquired.Diagnostics) == 0 {
if !errors.As(err, &retained) || retained != loadErr || len(acquired.Explanation.Details) == 0 {
t.Errorf("prompt load failure = (%#v, %v)", acquired, err)
}
return len(acquired.Diagnostics), err
return len(acquired.Diagnostics) + len(acquired.Explanation.Details), err
}},
{name: "profiles", call: func() (int, error) {
acquired, err := suite.ListProfiles(context.Background(), CatalogRequest{}, 0, 0)
var retained *configkit.LoadError
if !errors.As(err, &retained) || retained != loadErr || len(acquired.Diagnostics) == 0 {
if !errors.As(err, &retained) || retained != loadErr || len(acquired.Explanation.Details) == 0 {
t.Errorf("profile load failure = (%#v, %v)", acquired, err)
}
return len(acquired.Diagnostics), err
return len(acquired.Diagnostics) + len(acquired.Explanation.Details), err
}},
{name: "backends", call: func() (int, error) {
acquired, err := suite.ListBackends(context.Background(), CatalogRequest{}, 0, 0)
var retained *configkit.LoadError
if !errors.As(err, &retained) || retained != loadErr || len(acquired.Diagnostics) == 0 {
if !errors.As(err, &retained) || retained != loadErr || len(acquired.Explanation.Details) == 0 {
t.Errorf("backend load failure = (%#v, %v)", acquired, err)
}
return len(acquired.Diagnostics), err
return len(acquired.Diagnostics) + len(acquired.Explanation.Details), err
}},
{name: "namespaces", call: func() (int, error) {
acquired, err := suite.ListNamespaces(context.Background(), NamespaceRequest{}, 0, 0)
var retained *configkit.LoadError
if !errors.As(err, &retained) || retained != loadErr || len(acquired.Diagnostics) == 0 {
if !errors.As(err, &retained) || retained != loadErr || len(acquired.Explanation.Details) == 0 {
t.Errorf("namespace load failure = (%#v, %v)", acquired, err)
}
return len(acquired.Diagnostics), err
return len(acquired.Diagnostics) + len(acquired.Explanation.Details), err
}},
{name: "collections", call: func() (int, error) {
acquired, err := suite.ListCollections(context.Background(), ckconfig.Selection{}, 0, 0)
var retained *configkit.LoadError
if !errors.As(err, &retained) || retained != loadErr || len(acquired.Diagnostics) == 0 {
if !errors.As(err, &retained) || retained != loadErr || len(acquired.Explanation.Details) == 0 {
t.Errorf("collection load failure = (%#v, %v)", acquired, err)
}
return len(acquired.Diagnostics), err
return len(acquired.Diagnostics) + len(acquired.Explanation.Details), err
}},
} {
t.Run(test.name, func(t *testing.T) {
@@ -782,7 +779,7 @@ func TestAdapterAcquisitionPreservesLoadAndContextErrors(t *testing.T) {
}
}
func TestFromEngineRetainsConfigKitLoadDiagnostics(t *testing.T) {
func TestFromEngineRetainsConfigKitLoadExplanation(t *testing.T) {
_, loadErr := configkit.Load[map[string]any](configkit.Bytes("invalid PromptKit config", []byte("broken: [")))
if loadErr == nil {
t.Fatal("invalid configuration unexpectedly loaded")
@@ -793,11 +790,8 @@ func TestFromEngineRetainsConfigKitLoadDiagnostics(t *testing.T) {
acquired, err := suite.InspectProfile(context.Background(), ProfileRequest{ID: "default"}, 0, 0)
var retained *configkit.LoadError
if !errors.As(err, &retained) || retained != loadErr || len(acquired.Diagnostics) == 0 {
t.Fatalf("load failure = (%#v, %v), want original ConfigKit error and diagnostics", acquired, err)
}
if acquired.Diagnostics[0].Severity != commandkit.SeverityError {
t.Fatalf("load diagnostics = %#v, want error severity", acquired.Diagnostics)
if !errors.As(err, &retained) || retained != loadErr || len(acquired.Explanation.Details) == 0 {
t.Fatalf("load failure = (%#v, %v), want original ConfigKit error and terminal context", acquired, err)
}
}
@@ -823,25 +817,55 @@ func TestPromptKitDiagnosticsRemainDeferredAndPropagateWriterErrors(t *testing.T
Severity: configkit.SeverityWarning,
Message: "safe warning",
}}
diagnostics := promptKitDiagnostics("promptkit", "", sources, records)
if len(diagnostics) != 1 || diagnostics[0].Severity != commandkit.SeverityWarning {
t.Fatalf("diagnostics = %#v", diagnostics)
messages := promptKitDiagnostics("promptkit", "", sources, records)
if len(messages.diagnostics) != 1 || messages.diagnostics[0].Severity != commandkit.SeverityWarning || len(messages.explanation.Details) != 0 {
t.Fatalf("diagnostic messages = %#v", messages)
}
if err := diagnostics[0].Write(failingDiagnosticWriter{err: writeErr}); !errors.Is(err, writeErr) {
if err := messages.diagnostics[0].Write(failingDiagnosticWriter{err: writeErr}); !errors.Is(err, writeErr) {
t.Fatalf("diagnostic write error = %v", err)
}
var output bytes.Buffer
if err := diagnostics[0].Write(&output); err != nil || !strings.Contains(output.String(), `Configuration scope "promptkit":`) || !strings.Contains(output.String(), "safe warning") || !strings.Contains(output.String(), `"source"`) {
if err := messages.diagnostics[0].Write(&output); err != nil || !strings.Contains(output.String(), `Configuration scope "promptkit":`) || !strings.Contains(output.String(), "safe warning") || !strings.Contains(output.String(), `"source"`) {
t.Fatalf("diagnostic text = %q, %v", output.String(), err)
}
sideDiagnostics := promptKitDiagnostics("promptkit", "left", sources, records)
output.Reset()
if err := sideDiagnostics[0].Write(&output); err != nil || !strings.Contains(output.String(), `Configuration scope "promptkit" (left):`) {
if err := sideDiagnostics.diagnostics[0].Write(&output); err != nil || !strings.Contains(output.String(), `Configuration scope "promptkit" (left):`) {
t.Fatalf("comparison-side diagnostic text = %q, %v", output.String(), err)
}
}
func TestPromptKitDiagnosticsSeparateIndependentWarningsAndTerminalRecords(t *testing.T) {
sources := []configkit.SourceInfo{{ID: "source", Label: "safe label", Path: "config.yml"}}
primary := configkit.Location{SourceID: "source", Line: 3, Column: 2}
related := configkit.Location{SourceID: "base.yml", Line: 7, Column: 4}
messages := promptKitDiagnostics("promptkit", "right", sources, []configkit.Diagnostic{
{Severity: configkit.SeverityWarning, Message: "independent warning", Primary: &primary, Related: []configkit.Location{related}},
{Severity: configkit.SeverityError, Message: "terminal configuration error", Primary: &related, Related: []configkit.Location{primary}},
})
if len(messages.diagnostics) != 1 || len(messages.explanation.Details) != 1 {
t.Fatalf("mixed acquisition messages = %#v", messages)
}
writeErr := errors.New("terminal explanation writer failed")
if err := messages.explanation.Details[0](failingDiagnosticWriter{err: writeErr}); !errors.Is(err, writeErr) {
t.Fatalf("terminal explanation write error = %v", err)
}
var warning, terminal bytes.Buffer
if err := messages.diagnostics[0].Write(&warning); err != nil {
t.Fatal(err)
}
if err := messages.explanation.Details[0](&terminal); err != nil {
t.Fatal(err)
}
if got := warning.String(); !strings.Contains(got, "independent warning") || !strings.Contains(got, `Configuration scope "promptkit" (right):`) || !strings.Contains(got, `"source": label="safe label", path="config.yml"`) || !strings.Contains(got, "source:3:2") || !strings.Contains(got, "related: base.yml:7:4") || strings.Contains(got, "terminal configuration error") {
t.Fatalf("independent warning report = %q", got)
}
if got := terminal.String(); !strings.Contains(got, "terminal configuration error") || !strings.Contains(got, `Configuration scope "promptkit" (right):`) || !strings.Contains(got, `"source": label="safe label", path="config.yml"`) || !strings.Contains(got, "base.yml:7:4") || !strings.Contains(got, "related: source:3:2") || strings.Contains(got, "independent warning") {
t.Fatalf("terminal explanation report = %q", got)
}
}
type failingDiagnosticWriter struct{ err error }
func (writer failingDiagnosticWriter) Write([]byte) (int, error) { return 0, writer.err }

View File

@@ -26,8 +26,8 @@ type SmokeRequest struct {
// invocation context. Its complete returned request is passed to PromptKit;
// without a builder, the seed is forwarded unchanged. Reports and errors from
// PromptKit are both retained. A nil loader is kept as a suite wiring issue and
// does not install a smoke-test callback. The callback also retains engine
// loading diagnostics with the report and error.
// does not install a smoke-test callback. The callback also retains independent
// engine diagnostics and terminal context with the report and error.
func WithSmokeTesting[I, S any](
suite Suite[I, S],
load func(context.Context, ckconfig.Selection, I, S) (*promptkit.Engine, error),
@@ -56,16 +56,16 @@ func WithSmokeTesting[I, S any](
return commandkit.Acquired[*promptkit.SmokeTestReport]{}, fmt.Errorf("PromptKit smoke test after building: %w", err)
}
engine, _, diagnostics, err := loadPromptKitEngine(ctx, request.Selection, input, state, "", load)
engine, _, messages, err := loadPromptKitEngine(ctx, request.Selection, input, state, "", load)
if err != nil {
return commandkit.Acquired[*promptkit.SmokeTestReport]{Diagnostics: diagnostics}, err
return commandkit.Acquired[*promptkit.SmokeTestReport]{Diagnostics: messages.diagnostics, Explanation: messages.explanation}, err
}
if err := promptKitContextError(ctx); err != nil {
return commandkit.Acquired[*promptkit.SmokeTestReport]{Diagnostics: diagnostics}, fmt.Errorf("PromptKit smoke test before native operation: %w", err)
return commandkit.Acquired[*promptkit.SmokeTestReport]{Diagnostics: messages.diagnostics, Explanation: messages.explanation}, fmt.Errorf("PromptKit smoke test before native operation: %w", err)
}
report, err := engine.SmokeTest(ctx, concreteRequest)
return commandkit.Acquired[*promptkit.SmokeTestReport]{Value: report, Diagnostics: diagnostics}, err
return commandkit.Acquired[*promptkit.SmokeTestReport]{Value: report, Diagnostics: messages.diagnostics, Explanation: messages.explanation}, err
}
return suite
}

View File

@@ -213,7 +213,7 @@ func TestWithRequestValidationBuilderOutputReachesNativeValidation(t *testing.T)
}
}
func TestWithRequestValidationRetainsNilEngineAndLoadDiagnostics(t *testing.T) {
func TestWithRequestValidationRetainsNilEngineAndLoadExplanation(t *testing.T) {
request := ValidationRequest{Request: promptkit.RunRequest{PromptID: "empty", ProfileID: "default"}}
nilEngineSuite := WithRequestValidation(Suite[struct{}, struct{}]{}, func(context.Context, ckconfig.Selection, struct{}, struct{}) (*promptkit.Engine, error) {
return nil, nil
@@ -232,8 +232,8 @@ func TestWithRequestValidationRetainsNilEngineAndLoadDiagnostics(t *testing.T) {
}, nil)
acquired, err = loadFailure.ValidateRequest(context.Background(), request, struct{}{}, struct{}{})
var retained *configkit.LoadError
if !errors.Is(err, loadErr) || !errors.As(err, &retained) || retained != loadErr || len(acquired.Diagnostics) == 0 {
t.Fatalf("load failure = (%#v, %v), want original ConfigKit error and diagnostics", acquired, err)
if !errors.Is(err, loadErr) || !errors.As(err, &retained) || retained != loadErr || len(acquired.Explanation.Details) == 0 {
t.Fatalf("load failure = (%#v, %v), want original ConfigKit error and terminal context", acquired, err)
}
}
@@ -289,7 +289,7 @@ func TestWithRequestValidationCancellationBoundaries(t *testing.T) {
}, nil)
acquired, err := suite.ValidateRequest(ctx, request, 0, 0)
var retained *configkit.LoadError
if !errors.Is(err, loadErr) || !errors.As(err, &retained) || retained != loadErr || len(acquired.Diagnostics) == 0 || loaderCalls != 1 {
if !errors.Is(err, loadErr) || !errors.As(err, &retained) || retained != loadErr || len(acquired.Explanation.Details) == 0 || loaderCalls != 1 {
t.Fatalf("cancelled load = (%#v, %v), loader calls=%d", acquired, err, loaderCalls)
}
})