Retain PromptKit diagnostics after cancellation
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
# Implement PromptKit comparison and catalogs
|
# Implement PromptKit comparison and catalogs
|
||||||
|
|
||||||
Status: Stage 1 complete; remaining stages are planned.
|
Status: Stages 1–2 complete; remaining stages are planned.
|
||||||
|
|
||||||
## Scope and authority
|
## Scope and authority
|
||||||
|
|
||||||
@@ -154,6 +154,25 @@ warnings. Check ordinary headings and deferred-writer error propagation. Create
|
|||||||
native diagnostic fixtures through public APIs; do not import upstream internals
|
native diagnostic fixtures through public APIs; do not import upstream internals
|
||||||
or assert exact cancellation polling counts.
|
or assert exact cancellation polling counts.
|
||||||
|
|
||||||
|
**Completion evidence (2026-10-12)**
|
||||||
|
|
||||||
|
- Refactored the private engine loader to collect inspection diagnostics before
|
||||||
|
checking cancellation observed after a successful load. Diagnostic headings
|
||||||
|
retain the ordinary scope form and can identify a comparison side. ConfigKit
|
||||||
|
load diagnostics and the original load error remain available.
|
||||||
|
- Added adapter coverage for post-load cancellation, ConfigKit load-error identity
|
||||||
|
and diagnostics, ordinary and comparison-side headings, and deferred-writer
|
||||||
|
error propagation. `GOTOOLCHAIN=local go test ./...` passed in
|
||||||
|
`integrations/ckprompt`.
|
||||||
|
- PromptKit v0.14.0 provides no public warning callback in engine configuration,
|
||||||
|
and its native configuration path emits no warnings. Therefore a successful
|
||||||
|
engine with inspection warnings cannot be constructed through the public API;
|
||||||
|
native warning retention across cancellation remains unexercised. The ordering
|
||||||
|
change directly retains any diagnostics if a later PromptKit version supplies
|
||||||
|
them. The selected integration evidence is otherwise complete.
|
||||||
|
- Full repository and final assembled-state validation remain assigned to
|
||||||
|
Stage 12.
|
||||||
|
|
||||||
## Stage 3: Add the bundled PromptKit configuration pair provider
|
## Stage 3: Add the bundled PromptKit configuration pair provider
|
||||||
|
|
||||||
**Outcome:** `ConfigScope` advertises Diff and supplies a native provider usable
|
**Outcome:** `ConfigScope` advertises Diff and supplies a native provider usable
|
||||||
|
|||||||
@@ -76,7 +76,7 @@ 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) {
|
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, diagnostics, err := loadPromptKitEngine(ctx, request.Selection, input, state, "", load)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return commandkit.Acquired[ckconfig.View]{Diagnostics: diagnostics}, err
|
return commandkit.Acquired[ckconfig.View]{Diagnostics: diagnostics}, err
|
||||||
}
|
}
|
||||||
@@ -104,7 +104,7 @@ func FromEngine[I, S any](load func(context.Context, ckconfig.Selection, I, S) (
|
|||||||
|
|
||||||
return Suite[I, S]{
|
return Suite[I, S]{
|
||||||
InspectPrompt: func(ctx context.Context, request PromptRequest, input I, state S) (commandkit.Acquired[*promptkit.PromptInspection], error) {
|
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, _, diagnostics, err := loadPromptKitEngine(ctx, request.Selection, input, state, "", load)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return commandkit.Acquired[*promptkit.PromptInspection]{Diagnostics: diagnostics}, err
|
return commandkit.Acquired[*promptkit.PromptInspection]{Diagnostics: diagnostics}, err
|
||||||
}
|
}
|
||||||
@@ -112,7 +112,7 @@ func FromEngine[I, S any](load func(context.Context, ckconfig.Selection, I, S) (
|
|||||||
return commandkit.Acquired[*promptkit.PromptInspection]{Value: inspection, Diagnostics: diagnostics}, err
|
return commandkit.Acquired[*promptkit.PromptInspection]{Value: inspection, Diagnostics: diagnostics}, err
|
||||||
},
|
},
|
||||||
InspectProfile: func(ctx context.Context, request ProfileRequest, input I, state S) (commandkit.Acquired[*promptkit.ProfileInspection], error) {
|
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, _, diagnostics, err := loadPromptKitEngine(ctx, request.Selection, input, state, "", load)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return commandkit.Acquired[*promptkit.ProfileInspection]{Diagnostics: diagnostics}, err
|
return commandkit.Acquired[*promptkit.ProfileInspection]{Diagnostics: diagnostics}, err
|
||||||
}
|
}
|
||||||
@@ -127,33 +127,34 @@ func loadPromptKitEngine[I, S any](
|
|||||||
selection ckconfig.Selection,
|
selection ckconfig.Selection,
|
||||||
input I,
|
input I,
|
||||||
state S,
|
state S,
|
||||||
|
side string,
|
||||||
load engineLoader[I, S],
|
load engineLoader[I, S],
|
||||||
) (*promptkit.Engine, promptkit.ConfigurationInspection, []commandkit.Diagnostic, error) {
|
) (*promptkit.Engine, promptkit.ConfigurationInspection, []commandkit.Diagnostic, error) {
|
||||||
engine, err := load(ctx, selection, input, state)
|
engine, err := load(ctx, selection, input, state)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
var loadErr *configkit.LoadError
|
var loadErr *configkit.LoadError
|
||||||
if errors.As(err, &loadErr) {
|
if errors.As(err, &loadErr) {
|
||||||
return nil, promptkit.ConfigurationInspection{}, promptKitDiagnostics("promptkit", loadErr.Sources(), loadErr.Diagnostics()), err
|
return nil, promptkit.ConfigurationInspection{}, promptKitDiagnostics("promptkit", side, loadErr.Sources(), loadErr.Diagnostics()), err
|
||||||
}
|
}
|
||||||
return nil, promptkit.ConfigurationInspection{}, nil, err
|
return nil, promptkit.ConfigurationInspection{}, nil, err
|
||||||
}
|
}
|
||||||
if engine == nil {
|
if engine == nil {
|
||||||
return nil, promptkit.ConfigurationInspection{}, nil, errors.New("PromptKit configuration loader returned a nil engine")
|
return nil, promptkit.ConfigurationInspection{}, nil, errors.New("PromptKit configuration loader returned a nil engine")
|
||||||
}
|
}
|
||||||
if ctx != nil {
|
|
||||||
if err := ctx.Err(); err != nil {
|
|
||||||
return nil, promptkit.ConfigurationInspection{}, nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
inspection, err := engine.InspectConfiguration()
|
inspection, err := engine.InspectConfiguration()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, promptkit.ConfigurationInspection{}, nil, err
|
return nil, promptkit.ConfigurationInspection{}, nil, err
|
||||||
}
|
}
|
||||||
diagnostics := promptKitDiagnostics("promptkit", inspection.Sources(), inspection.Diagnostics())
|
diagnostics := promptKitDiagnostics("promptkit", side, inspection.Sources(), inspection.Diagnostics())
|
||||||
|
if ctx != nil {
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return nil, promptkit.ConfigurationInspection{}, diagnostics, err
|
||||||
|
}
|
||||||
|
}
|
||||||
return engine, inspection, diagnostics, nil
|
return engine, inspection, diagnostics, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func promptKitDiagnostics(scope string, sources []configkit.SourceInfo, records []configkit.Diagnostic) []commandkit.Diagnostic {
|
func promptKitDiagnostics(scope, side string, sources []configkit.SourceInfo, records []configkit.Diagnostic) []commandkit.Diagnostic {
|
||||||
if len(records) == 0 {
|
if len(records) == 0 {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -177,7 +178,11 @@ func promptKitDiagnostics(scope string, sources []configkit.SourceInfo, records
|
|||||||
return []commandkit.Diagnostic{{
|
return []commandkit.Diagnostic{{
|
||||||
Severity: severity,
|
Severity: severity,
|
||||||
Write: func(writer io.Writer) error {
|
Write: func(writer io.Writer) error {
|
||||||
if _, err := fmt.Fprintf(writer, "Configuration scope %s:\n", strconv.Quote(scope)); err != nil {
|
heading := "Configuration scope " + strconv.Quote(scope)
|
||||||
|
if side != "" {
|
||||||
|
heading += " (" + side + ")"
|
||||||
|
}
|
||||||
|
if _, err := fmt.Fprintf(writer, "%s:\n", heading); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if len(sources) > 0 {
|
if len(sources) > 0 {
|
||||||
|
|||||||
@@ -203,12 +203,48 @@ func TestAdapterAcquisitionPreservesLoadAndContextErrors(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestFromEngineRetainsConfigKitLoadDiagnostics(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")
|
||||||
|
}
|
||||||
|
suite := FromEngine[int, int](func(context.Context, ckconfig.Selection, int, int) (*promptkit.Engine, error) {
|
||||||
|
return nil, loadErr
|
||||||
|
})
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConfigScopeHonorsCancellationAfterLoad(t *testing.T) {
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
defer cancel()
|
||||||
|
engine := adapterEngine(t)
|
||||||
|
scope := ConfigScope(func(context.Context, ckconfig.Selection, int, int) (*promptkit.Engine, error) {
|
||||||
|
cancel()
|
||||||
|
return engine, nil
|
||||||
|
})
|
||||||
|
|
||||||
|
acquired, err := scope.Acquire(ctx, ckconfig.Request{Operation: ckconfig.Show}, 0, 0)
|
||||||
|
if !errors.Is(err, context.Canceled) || acquired.Value.Effective != nil {
|
||||||
|
t.Fatalf("cancelled scope acquisition = (%#v, %v), want cancellation and no value", acquired, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestPromptKitDiagnosticsRemainDeferredAndPropagateWriterErrors(t *testing.T) {
|
func TestPromptKitDiagnosticsRemainDeferredAndPropagateWriterErrors(t *testing.T) {
|
||||||
writeErr := errors.New("diagnostic writer failed")
|
writeErr := errors.New("diagnostic writer failed")
|
||||||
diagnostics := promptKitDiagnostics("promptkit", []configkit.SourceInfo{{ID: "source"}}, []configkit.Diagnostic{{
|
sources := []configkit.SourceInfo{{ID: "source"}}
|
||||||
|
records := []configkit.Diagnostic{{
|
||||||
Severity: configkit.SeverityWarning,
|
Severity: configkit.SeverityWarning,
|
||||||
Message: "safe warning",
|
Message: "safe warning",
|
||||||
}})
|
}}
|
||||||
|
diagnostics := promptKitDiagnostics("promptkit", "", sources, records)
|
||||||
if len(diagnostics) != 1 || diagnostics[0].Severity != commandkit.SeverityWarning {
|
if len(diagnostics) != 1 || diagnostics[0].Severity != commandkit.SeverityWarning {
|
||||||
t.Fatalf("diagnostics = %#v", diagnostics)
|
t.Fatalf("diagnostics = %#v", diagnostics)
|
||||||
}
|
}
|
||||||
@@ -216,9 +252,15 @@ func TestPromptKitDiagnosticsRemainDeferredAndPropagateWriterErrors(t *testing.T
|
|||||||
t.Fatalf("diagnostic write error = %v", err)
|
t.Fatalf("diagnostic write error = %v", err)
|
||||||
}
|
}
|
||||||
var output bytes.Buffer
|
var output bytes.Buffer
|
||||||
if err := diagnostics[0].Write(&output); err != nil || !strings.Contains(output.String(), "safe warning") || !strings.Contains(output.String(), `"source"`) {
|
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"`) {
|
||||||
t.Fatalf("diagnostic text = %q, %v", output.String(), err)
|
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):`) {
|
||||||
|
t.Fatalf("comparison-side diagnostic text = %q, %v", output.String(), err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type failingDiagnosticWriter struct{ err error }
|
type failingDiagnosticWriter struct{ err error }
|
||||||
|
|||||||
Reference in New Issue
Block a user