2026-10-11 01:43:02 +00:00
|
|
|
package ckprompt
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"bytes"
|
|
|
|
|
"context"
|
|
|
|
|
"errors"
|
|
|
|
|
"io"
|
2026-10-12 02:01:10 +00:00
|
|
|
"reflect"
|
2026-10-12 15:11:13 +00:00
|
|
|
"slices"
|
2026-10-11 01:43:02 +00:00
|
|
|
"strings"
|
|
|
|
|
"testing"
|
|
|
|
|
"testing/fstest"
|
|
|
|
|
|
|
|
|
|
commandkit "gitea.maximumdirect.net/eric/CommandKit"
|
|
|
|
|
"gitea.maximumdirect.net/eric/CommandKit/integrations/ckconfig"
|
|
|
|
|
configkit "gitea.maximumdirect.net/eric/ConfigKit"
|
|
|
|
|
promptkit "gitea.maximumdirect.net/eric/promptkit"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
const adapterPrompt = `prompts:
|
|
|
|
|
briefing:
|
2026-10-17 18:39:38 +00:00
|
|
|
version: "1"
|
|
|
|
|
default_profile: default
|
|
|
|
|
inputs:
|
|
|
|
|
- name: topic
|
|
|
|
|
required: true
|
|
|
|
|
content_type: text/plain
|
|
|
|
|
description: "Topic"
|
|
|
|
|
- name: audience
|
|
|
|
|
required: false
|
|
|
|
|
content_type: text/plain
|
|
|
|
|
description: ""
|
|
|
|
|
messages:
|
|
|
|
|
- role: user
|
|
|
|
|
content: "Hello"
|
|
|
|
|
output:
|
|
|
|
|
format: text
|
|
|
|
|
validation_mode: none
|
2026-10-11 01:46:54 +00:00
|
|
|
empty:
|
2026-10-17 18:39:38 +00:00
|
|
|
version: "1"
|
|
|
|
|
default_profile: default
|
|
|
|
|
messages:
|
|
|
|
|
- role: user
|
|
|
|
|
content: "Empty inputs"
|
|
|
|
|
output:
|
|
|
|
|
format: text
|
|
|
|
|
validation_mode: none
|
2026-10-11 01:43:02 +00:00
|
|
|
backends:
|
|
|
|
|
local:
|
|
|
|
|
endpoint: "https://example.invalid/v1"
|
|
|
|
|
profiles:
|
|
|
|
|
default:
|
|
|
|
|
backend: local
|
|
|
|
|
model: example
|
2026-10-11 01:46:54 +00:00
|
|
|
temperature: 0
|
|
|
|
|
max_tokens: 0
|
|
|
|
|
api_key_env: PROMPTKIT_ADAPTER_TEST_KEY
|
|
|
|
|
extra_params:
|
|
|
|
|
token: "caller-owned-value"
|
|
|
|
|
nested:
|
|
|
|
|
visible: true
|
|
|
|
|
null_value: null
|
|
|
|
|
sequence: [first, null, 3]
|
|
|
|
|
requires-key:
|
|
|
|
|
backend: local
|
|
|
|
|
model: example
|
|
|
|
|
api_key_required: true
|
2026-10-11 01:43:02 +00:00
|
|
|
`
|
|
|
|
|
|
2026-10-12 16:54:00 +00:00
|
|
|
const adapterRequirementsPrompt = `prompts:
|
|
|
|
|
briefing:
|
2026-10-17 18:39:38 +00:00
|
|
|
version: "1"
|
|
|
|
|
default_profile: default
|
|
|
|
|
session_id: '{{input "session-only"}} {{.session_variable}}'
|
|
|
|
|
messages:
|
|
|
|
|
- role: user
|
|
|
|
|
content: '{{if eq .audience "internal"}}{{input "internal-notes"}}{{else}}{{input "public-notes"}}{{end}} {{.audience}}'
|
|
|
|
|
output:
|
|
|
|
|
format: text
|
|
|
|
|
validation_mode: none
|
2026-10-12 16:54:00 +00:00
|
|
|
backends:
|
|
|
|
|
local:
|
|
|
|
|
endpoint: "https://example.invalid/v1"
|
|
|
|
|
profiles:
|
|
|
|
|
default:
|
|
|
|
|
backend: local
|
|
|
|
|
model: example
|
|
|
|
|
`
|
|
|
|
|
|
2026-10-11 01:43:02 +00:00
|
|
|
func adapterEngine(t *testing.T) *promptkit.Engine {
|
2026-10-12 02:01:10 +00:00
|
|
|
return adapterEngineFrom(t, "adapter fixture", adapterPrompt)
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-17 18:49:18 +00:00
|
|
|
func adapterEngineFrom(t *testing.T, label, configuration string, options ...promptkit.Option) *promptkit.Engine {
|
2026-10-11 01:43:02 +00:00
|
|
|
t.Helper()
|
2026-10-12 02:01:10 +00:00
|
|
|
inputs, err := promptkit.FilesystemInputs(label, fstest.MapFS{
|
|
|
|
|
"config/prompt.yml": &fstest.MapFile{Data: []byte(configuration)},
|
2026-10-11 01:43:02 +00:00
|
|
|
}, "config")
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
engine, err := promptkit.NewEngine(promptkit.Config{Sources: promptkit.ConfigurationSources{
|
|
|
|
|
Underlays: []configkit.Underlay{{Inputs: inputs}},
|
2026-10-17 18:49:18 +00:00
|
|
|
}}, options...)
|
2026-10-11 01:43:02 +00:00
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("construct PromptKit fixture: %v", err)
|
|
|
|
|
}
|
|
|
|
|
return engine
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-17 18:49:18 +00:00
|
|
|
type recordingSmokeClient struct {
|
|
|
|
|
request promptkit.GenerateRequest
|
|
|
|
|
response *promptkit.GenerateResponse
|
|
|
|
|
calls int
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (client *recordingSmokeClient) Generate(_ context.Context, request promptkit.GenerateRequest) (*promptkit.GenerateResponse, error) {
|
|
|
|
|
client.calls++
|
|
|
|
|
client.request = request
|
|
|
|
|
return client.response, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestWithSmokeTestingForwardsAuthoritativeNativeRequestAndRetainsReport(t *testing.T) {
|
|
|
|
|
client := &recordingSmokeClient{
|
|
|
|
|
response: &promptkit.GenerateResponse{Content: "fixture response"},
|
|
|
|
|
}
|
|
|
|
|
engine := adapterEngineFrom(t, "smoke adapter fixture", adapterPrompt, promptkit.WithLLMClient(client))
|
|
|
|
|
type input struct{ label string }
|
|
|
|
|
type state struct{ label string }
|
|
|
|
|
type contextKey struct{}
|
|
|
|
|
wantContext := context.WithValue(context.Background(), contextKey{}, "smoke invocation")
|
|
|
|
|
wantInput := input{label: "typed input"}
|
|
|
|
|
wantState := state{label: "shared state"}
|
|
|
|
|
wantSelection := ckconfig.Selection{Config: commandkit.Supplied[string]{Value: "selected.yml", Set: true}}
|
|
|
|
|
loaderCalls, builderCalls := 0, 0
|
|
|
|
|
seed := promptkit.SmokeTestRequest{Kind: promptkit.SmokeTestAuthentication, BackendID: "unused", APIKey: "seed credential"}
|
|
|
|
|
built := promptkit.SmokeTestRequest{
|
|
|
|
|
Kind: promptkit.SmokeTestTextInference,
|
|
|
|
|
PromptID: "briefing",
|
|
|
|
|
APIKey: "builder credential",
|
|
|
|
|
Execution: &promptkit.ExecutionTargetOverride{Model: "builder-model"},
|
|
|
|
|
}
|
|
|
|
|
suite := WithSmokeTesting(Suite[input, state]{},
|
|
|
|
|
func(ctx context.Context, selection ckconfig.Selection, gotInput input, gotState state) (*promptkit.Engine, error) {
|
|
|
|
|
loaderCalls++
|
|
|
|
|
if ctx.Value(contextKey{}) != "smoke invocation" || selection != wantSelection || gotInput != wantInput || gotState != wantState {
|
|
|
|
|
t.Errorf("loader args = ctx=%v selection=%#v input=%#v state=%#v", ctx.Value(contextKey{}), selection, gotInput, gotState)
|
|
|
|
|
}
|
|
|
|
|
return engine, nil
|
|
|
|
|
},
|
|
|
|
|
func(ctx context.Context, request SmokeRequest, gotInput input, gotState state) (promptkit.SmokeTestRequest, error) {
|
|
|
|
|
builderCalls++
|
|
|
|
|
if ctx.Value(contextKey{}) != "smoke invocation" || request.Selection != wantSelection || request.Request.Kind != promptkit.SmokeTestAuthentication || gotInput != wantInput || gotState != wantState {
|
|
|
|
|
t.Errorf("builder args = ctx=%v request=%#v input=%#v state=%#v", ctx.Value(contextKey{}), request, gotInput, gotState)
|
|
|
|
|
}
|
|
|
|
|
return built, nil
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
if suite.SmokeTest == nil {
|
|
|
|
|
t.Fatal("WithSmokeTesting did not install a callback")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
acquired, err := suite.SmokeTest(wantContext, SmokeRequest{Selection: wantSelection, Request: seed}, wantInput, wantState)
|
|
|
|
|
if err != nil || acquired.Value == nil || len(acquired.Diagnostics) != 0 || loaderCalls != 1 || builderCalls != 1 {
|
|
|
|
|
t.Fatalf("smoke acquisition = (%#v, %v), loaders/builders=%d/%d", acquired, err, loaderCalls, builderCalls)
|
|
|
|
|
}
|
2026-10-18 13:29:13 +00:00
|
|
|
if acquired.Value.Kind != promptkit.SmokeTestTextInference || client.calls == 0 || client.request.Target.Model != "builder-model" || client.request.APIKey != "builder credential" {
|
2026-10-17 18:49:18 +00:00
|
|
|
t.Fatalf("native smoke request/report = request %#v report %#v calls=%d", client.request, acquired.Value, client.calls)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestWithSmokeTestingNilBuilderForwardsSeedAndBuilderFailureSkipsLoader(t *testing.T) {
|
|
|
|
|
client := &recordingSmokeClient{response: &promptkit.GenerateResponse{Content: "fixture response"}}
|
|
|
|
|
engine := adapterEngineFrom(t, "smoke adapter fixture", adapterPrompt, promptkit.WithLLMClient(client))
|
|
|
|
|
seed := promptkit.SmokeTestRequest{Kind: promptkit.SmokeTestTextInference, PromptID: "briefing", APIKey: "seed credential"}
|
|
|
|
|
loaderCalls := 0
|
|
|
|
|
suite := WithSmokeTesting(Suite[struct{}, struct{}]{}, func(context.Context, ckconfig.Selection, struct{}, struct{}) (*promptkit.Engine, error) {
|
|
|
|
|
loaderCalls++
|
|
|
|
|
return engine, nil
|
|
|
|
|
}, nil)
|
|
|
|
|
acquired, err := suite.SmokeTest(context.Background(), SmokeRequest{Request: seed}, struct{}{}, struct{}{})
|
2026-10-18 13:29:13 +00:00
|
|
|
if err != nil || acquired.Value == nil || client.calls == 0 || client.request.APIKey != "seed credential" {
|
2026-10-17 18:49:18 +00:00
|
|
|
t.Fatalf("nil-builder smoke request = (%#v, %v), client=%#v", acquired, err, client)
|
|
|
|
|
}
|
|
|
|
|
acquired, err = suite.SmokeTest(context.Background(), SmokeRequest{Request: promptkit.SmokeTestRequest{Kind: promptkit.SmokeTestKind("unknown")}}, struct{}{}, struct{}{})
|
|
|
|
|
if !errors.Is(err, promptkit.ErrInvalidRequest) || acquired.Value != nil {
|
|
|
|
|
t.Fatalf("native smoke error = (%#v, %v), want invalid request", acquired, err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
buildErr := errors.New("request builder failed")
|
|
|
|
|
loaderCalls = 0
|
|
|
|
|
suite = WithSmokeTesting(Suite[struct{}, struct{}]{}, func(context.Context, ckconfig.Selection, struct{}, struct{}) (*promptkit.Engine, error) {
|
|
|
|
|
loaderCalls++
|
|
|
|
|
return engine, nil
|
|
|
|
|
}, func(context.Context, SmokeRequest, struct{}, struct{}) (promptkit.SmokeTestRequest, error) {
|
|
|
|
|
return promptkit.SmokeTestRequest{}, buildErr
|
|
|
|
|
})
|
|
|
|
|
acquired, err = suite.SmokeTest(context.Background(), SmokeRequest{}, struct{}{}, struct{}{})
|
|
|
|
|
if err != buildErr || acquired.Value != nil || loaderCalls != 0 {
|
|
|
|
|
t.Fatalf("builder failure acquisition = (%#v, %v), loader calls=%d", acquired, err, loaderCalls)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestWithSmokeTestingRetainsNilLoaderIssueAndChecksCancellationBeforeBuild(t *testing.T) {
|
|
|
|
|
sentinel := errors.New("existing suite issue")
|
|
|
|
|
suite := WithSmokeTesting(Suite[int, int]{issues: []error{sentinel}}, nil, nil)
|
|
|
|
|
if suite.SmokeTest != nil || len(suite.issues) != 2 || suite.issues[0] != sentinel || !strings.Contains(suite.issues[1].Error(), "engine loader is nil") {
|
|
|
|
|
t.Fatalf("nil-loader smoke wiring = %#v", suite)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
|
|
|
cancel()
|
|
|
|
|
buildCalls, loadCalls := 0, 0
|
|
|
|
|
suite = WithSmokeTesting(Suite[int, int]{}, func(context.Context, ckconfig.Selection, int, int) (*promptkit.Engine, error) {
|
|
|
|
|
loadCalls++
|
|
|
|
|
return nil, nil
|
|
|
|
|
}, func(context.Context, SmokeRequest, int, int) (promptkit.SmokeTestRequest, error) {
|
|
|
|
|
buildCalls++
|
|
|
|
|
return promptkit.SmokeTestRequest{}, nil
|
|
|
|
|
})
|
|
|
|
|
acquired, err := suite.SmokeTest(ctx, SmokeRequest{}, 0, 0)
|
|
|
|
|
if !errors.Is(err, context.Canceled) || acquired.Value != nil || buildCalls != 0 || loadCalls != 0 {
|
|
|
|
|
t.Fatalf("pre-canceled smoke = (%#v, %v), builders/loaders=%d/%d", acquired, err, buildCalls, loadCalls)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ctx, cancel = context.WithCancel(context.Background())
|
|
|
|
|
buildCalls, loadCalls = 0, 0
|
|
|
|
|
suite = WithSmokeTesting(Suite[int, int]{}, func(context.Context, ckconfig.Selection, int, int) (*promptkit.Engine, error) {
|
|
|
|
|
loadCalls++
|
|
|
|
|
return nil, nil
|
|
|
|
|
}, func(context.Context, SmokeRequest, int, int) (promptkit.SmokeTestRequest, error) {
|
|
|
|
|
buildCalls++
|
|
|
|
|
cancel()
|
|
|
|
|
return promptkit.SmokeTestRequest{}, nil
|
|
|
|
|
})
|
|
|
|
|
acquired, err = suite.SmokeTest(ctx, SmokeRequest{}, 0, 0)
|
|
|
|
|
if !errors.Is(err, context.Canceled) || acquired.Value != nil || buildCalls != 1 || loadCalls != 0 {
|
|
|
|
|
t.Fatalf("canceled-after-build smoke = (%#v, %v), builders/loaders=%d/%d", acquired, err, buildCalls, loadCalls)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-26 03:26:14 +00:00
|
|
|
func TestWithSmokeTestingRetainsLoaderExplanationAndErrorIdentity(t *testing.T) {
|
2026-10-17 18:49:18 +00:00
|
|
|
_, loadErr := configkit.Load[map[string]any](configkit.Bytes("invalid smoke configuration", []byte("broken: [")))
|
|
|
|
|
if loadErr == nil {
|
|
|
|
|
t.Fatal("invalid configuration unexpectedly loaded")
|
|
|
|
|
}
|
|
|
|
|
suite := WithSmokeTesting(Suite[int, int]{}, func(context.Context, ckconfig.Selection, int, int) (*promptkit.Engine, error) {
|
|
|
|
|
return nil, loadErr
|
|
|
|
|
}, nil)
|
|
|
|
|
acquired, err := suite.SmokeTest(context.Background(), SmokeRequest{}, 0, 0)
|
|
|
|
|
var retained *configkit.LoadError
|
2026-10-26 03:26:14 +00:00
|
|
|
if !errors.Is(err, loadErr) || !errors.As(err, &retained) || retained != loadErr || acquired.Value != nil || len(acquired.Explanation.Details) == 0 {
|
2026-10-17 18:49:18 +00:00
|
|
|
t.Fatalf("smoke load failure = (%#v, %v)", acquired, err)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-11 01:43:02 +00:00
|
|
|
func TestConfigScopeUsesNativeInspectionFacetsAndInvocationValues(t *testing.T) {
|
|
|
|
|
type input struct{ token string }
|
|
|
|
|
type state struct{ token string }
|
|
|
|
|
type contextKey struct{}
|
|
|
|
|
|
|
|
|
|
engine := adapterEngine(t)
|
|
|
|
|
wantContext := context.WithValue(context.Background(), contextKey{}, "invocation")
|
|
|
|
|
wantInput := input{token: "input"}
|
|
|
|
|
wantState := state{token: "state"}
|
|
|
|
|
loaderCalls := 0
|
|
|
|
|
scope := ConfigScope(func(ctx context.Context, selection ckconfig.Selection, gotInput input, gotState state) (*promptkit.Engine, error) {
|
|
|
|
|
loaderCalls++
|
|
|
|
|
if ctx.Value(contextKey{}) != "invocation" || !selection.Config.Set || selection.Config.Value != "prompt-config.yml" || gotInput != wantInput || gotState != wantState {
|
|
|
|
|
t.Errorf("loader args = ctx=%v selection=%#v input=%#v state=%#v", ctx.Value(contextKey{}), selection, gotInput, gotState)
|
|
|
|
|
}
|
|
|
|
|
return engine, nil
|
|
|
|
|
})
|
2026-10-12 02:01:10 +00:00
|
|
|
if scope.Name != "promptkit" || len(scope.Operations) != 5 || scope.Acquire == nil {
|
2026-10-11 01:43:02 +00:00
|
|
|
t.Fatalf("configuration scope = %#v", scope)
|
|
|
|
|
}
|
|
|
|
|
acquired, err := scope.Acquire(wantContext, ckconfig.Request{
|
|
|
|
|
Operation: ckconfig.Show,
|
|
|
|
|
Selection: ckconfig.Selection{Config: commandkit.Supplied[string]{Value: "prompt-config.yml", Set: true}},
|
|
|
|
|
}, wantInput, wantState)
|
|
|
|
|
if err != nil || loaderCalls != 1 {
|
|
|
|
|
t.Fatalf("scope acquisition = (%#v, %v), calls=%d", acquired, err, loaderCalls)
|
|
|
|
|
}
|
|
|
|
|
var effective bytes.Buffer
|
|
|
|
|
if err := acquired.Value.Effective.WriteEffective(&effective); err != nil || !strings.Contains(effective.String(), "briefing:") {
|
|
|
|
|
t.Fatalf("native effective report = %q, %v", effective.String(), err)
|
|
|
|
|
}
|
|
|
|
|
if sources := acquired.Value.Sources.Sources(); len(sources) == 0 || sources[0].ID == "" {
|
|
|
|
|
t.Fatalf("native sources = %#v", sources)
|
|
|
|
|
}
|
|
|
|
|
if explanation, ok := acquired.Value.Explanation.Explain(configkit.Path{}); !ok || !explanation.Effective.Present {
|
|
|
|
|
t.Fatalf("native root explanation = %#v, found=%t", explanation, ok)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestConfigScopeRetainsNilLoaderIssueAfterCapabilitiesAreCleared(t *testing.T) {
|
|
|
|
|
scope := ConfigScope[int, int](nil)
|
|
|
|
|
if scope.Acquire != nil {
|
|
|
|
|
t.Fatal("nil loader installed an acquisition callback")
|
|
|
|
|
}
|
|
|
|
|
scope.Operations = nil
|
|
|
|
|
app, err := commandkit.New(commandkit.Identity{Name: "example"})
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
declaration := ckconfig.Command(ckconfig.Suite[int, int]{Scopes: []ckconfig.Scope[int, int]{scope}})
|
|
|
|
|
if err := app.Register(declaration); err == nil || !strings.Contains(err.Error(), "PromptKit configuration loader is nil") {
|
|
|
|
|
t.Fatalf("narrowed nil-loader scope registered: %v", err)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-12 02:01:10 +00:00
|
|
|
func TestConfigScopeCompareUsesNativeDiffAndLoadsBothEqualSelections(t *testing.T) {
|
|
|
|
|
leftEngine := adapterEngine(t)
|
|
|
|
|
rightConfig := strings.Replace(adapterPrompt, `content: "Hello"`, `content: "Goodbye"`, 1)
|
|
|
|
|
if rightConfig == adapterPrompt {
|
|
|
|
|
t.Fatal("comparison fixture did not change the prompt content")
|
|
|
|
|
}
|
|
|
|
|
rightEngine := adapterEngineFrom(t, "right comparison fixture", rightConfig)
|
|
|
|
|
type input struct{ label string }
|
|
|
|
|
type call struct {
|
|
|
|
|
selection ckconfig.Selection
|
|
|
|
|
input input
|
|
|
|
|
state *int
|
|
|
|
|
}
|
|
|
|
|
var calls []call
|
|
|
|
|
scope := ConfigScope(func(_ context.Context, selection ckconfig.Selection, gotInput input, state *int) (*promptkit.Engine, error) {
|
|
|
|
|
calls = append(calls, call{selection: selection, input: gotInput, state: state})
|
|
|
|
|
if gotInput.label == "before" {
|
|
|
|
|
return leftEngine, nil
|
|
|
|
|
}
|
|
|
|
|
return rightEngine, nil
|
|
|
|
|
})
|
2026-10-12 15:11:13 +00:00
|
|
|
if scope.Compare == nil {
|
2026-10-12 02:01:10 +00:00
|
|
|
t.Fatalf("configuration scope does not declare comparison: %#v", scope)
|
|
|
|
|
}
|
2026-10-12 15:11:13 +00:00
|
|
|
for _, operation := range []ckconfig.Operation{ckconfig.Validate, ckconfig.Show, ckconfig.Sources, ckconfig.Explain, ckconfig.Diff} {
|
|
|
|
|
if !slices.Contains(scope.Operations, operation) {
|
|
|
|
|
t.Fatalf("configuration scope is missing operation %q: %#v", operation, scope.Operations)
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-10-12 02:01:10 +00:00
|
|
|
|
|
|
|
|
selection := ckconfig.Selection{Config: commandkit.Supplied[string]{Value: "same-selection.yml", Set: true}}
|
|
|
|
|
state := new(int)
|
|
|
|
|
request := ckconfig.ComparisonRequest[input]{
|
|
|
|
|
Left: ckconfig.ComparisonSide[input]{Selection: selection, Input: input{label: "before"}},
|
|
|
|
|
Right: ckconfig.ComparisonSide[input]{Selection: selection, Input: input{label: "after"}},
|
|
|
|
|
}
|
|
|
|
|
leftInspection, err := leftEngine.InspectConfiguration()
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
rightInspection, err := rightEngine.InspectConfiguration()
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
want, err := leftInspection.Diff(rightInspection)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
if len(want) == 0 {
|
|
|
|
|
t.Fatal("native PromptKit fixture comparison produced no differences")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
got, err := scope.Compare(context.Background(), request, state)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
if !reflect.DeepEqual(got.Value, want) {
|
|
|
|
|
t.Fatalf("adapter differences = %#v, want native PromptKit result %#v", got.Value, want)
|
|
|
|
|
}
|
|
|
|
|
if len(calls) != 2 || calls[0].selection != selection || calls[1].selection != selection || calls[0].input.label != "before" || calls[1].input.label != "after" || calls[0].state != state || calls[1].state != state {
|
|
|
|
|
t.Fatalf("loader calls = %#v", calls)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestConfigScopeCompareStopsAfterLeftLoadFailure(t *testing.T) {
|
|
|
|
|
loadErr := errors.New("left engine load failed")
|
|
|
|
|
rightCalled := false
|
|
|
|
|
scope := ConfigScope(func(_ context.Context, selection ckconfig.Selection, _ struct{}, _ struct{}) (*promptkit.Engine, error) {
|
|
|
|
|
if selection.Config.Value == "left" {
|
|
|
|
|
return nil, loadErr
|
|
|
|
|
}
|
|
|
|
|
rightCalled = true
|
|
|
|
|
return adapterEngine(t), nil
|
|
|
|
|
})
|
|
|
|
|
request := ckconfig.ComparisonRequest[struct{}]{
|
|
|
|
|
Left: ckconfig.ComparisonSide[struct{}]{Selection: ckconfig.Selection{Config: commandkit.Supplied[string]{Value: "left", Set: true}}},
|
|
|
|
|
Right: ckconfig.ComparisonSide[struct{}]{Selection: ckconfig.Selection{Config: commandkit.Supplied[string]{Value: "right", Set: true}}},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
acquired, err := scope.Compare(context.Background(), request, struct{}{})
|
|
|
|
|
if !errors.Is(err, loadErr) || rightCalled || acquired.Value != nil {
|
|
|
|
|
t.Fatalf("left failure = (%#v, %v), right called=%t", acquired, err, rightCalled)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-26 03:26:14 +00:00
|
|
|
func TestConfigScopeCompareRetainsRightLoadExplanationAndErrorIdentity(t *testing.T) {
|
2026-10-12 02:01:10 +00:00
|
|
|
_, loadErr := configkit.Load[map[string]any](configkit.Bytes("invalid right prompt config", []byte("broken: [")))
|
|
|
|
|
if loadErr == nil {
|
|
|
|
|
t.Fatal("invalid right configuration unexpectedly loaded")
|
|
|
|
|
}
|
|
|
|
|
leftEngine := adapterEngine(t)
|
|
|
|
|
var calls []string
|
|
|
|
|
scope := ConfigScope(func(_ context.Context, selection ckconfig.Selection, _ struct{}, _ struct{}) (*promptkit.Engine, error) {
|
|
|
|
|
calls = append(calls, selection.Config.Value)
|
|
|
|
|
if selection.Config.Value == "left" {
|
|
|
|
|
return leftEngine, nil
|
|
|
|
|
}
|
|
|
|
|
return nil, loadErr
|
|
|
|
|
})
|
|
|
|
|
request := ckconfig.ComparisonRequest[struct{}]{
|
|
|
|
|
Left: ckconfig.ComparisonSide[struct{}]{Selection: ckconfig.Selection{Config: commandkit.Supplied[string]{Value: "left", Set: true}}},
|
|
|
|
|
Right: ckconfig.ComparisonSide[struct{}]{Selection: ckconfig.Selection{Config: commandkit.Supplied[string]{Value: "right", Set: true}}},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
acquired, err := scope.Compare(context.Background(), request, struct{}{})
|
|
|
|
|
var retained *configkit.LoadError
|
2026-10-26 03:26:14 +00:00
|
|
|
if !errors.Is(err, loadErr) || !errors.As(err, &retained) || retained != loadErr || len(calls) != 2 || len(acquired.Explanation.Details) == 0 {
|
2026-10-12 02:01:10 +00:00
|
|
|
t.Fatalf("right failure = (%#v, %v), calls=%#v", acquired, err, calls)
|
|
|
|
|
}
|
|
|
|
|
var output bytes.Buffer
|
2026-10-26 03:26:14 +00:00
|
|
|
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)
|
2026-10-12 02:01:10 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestConfigScopeCompareCancellationAfterLeftLoadStopsRight(t *testing.T) {
|
|
|
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
|
|
|
defer cancel()
|
|
|
|
|
leftEngine := adapterEngine(t)
|
|
|
|
|
rightCalled := false
|
|
|
|
|
scope := ConfigScope(func(_ context.Context, selection ckconfig.Selection, _ struct{}, _ struct{}) (*promptkit.Engine, error) {
|
|
|
|
|
if selection.Config.Value == "left" {
|
|
|
|
|
cancel()
|
|
|
|
|
return leftEngine, nil
|
|
|
|
|
}
|
|
|
|
|
rightCalled = true
|
|
|
|
|
return adapterEngine(t), nil
|
|
|
|
|
})
|
|
|
|
|
request := ckconfig.ComparisonRequest[struct{}]{
|
|
|
|
|
Left: ckconfig.ComparisonSide[struct{}]{Selection: ckconfig.Selection{Config: commandkit.Supplied[string]{Value: "left", Set: true}}},
|
|
|
|
|
Right: ckconfig.ComparisonSide[struct{}]{Selection: ckconfig.Selection{Config: commandkit.Supplied[string]{Value: "right", Set: true}}},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
acquired, err := scope.Compare(ctx, request, struct{}{})
|
|
|
|
|
if !errors.Is(err, context.Canceled) || rightCalled || acquired.Value != nil {
|
|
|
|
|
t.Fatalf("cancellation = (%#v, %v), right called=%t", acquired, err, rightCalled)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-11 01:43:02 +00:00
|
|
|
func TestFromEnginePassesRequestsAndRetainsNilLoaderIssue(t *testing.T) {
|
|
|
|
|
type input struct{ value string }
|
|
|
|
|
type state struct{ value string }
|
|
|
|
|
type contextKey struct{}
|
|
|
|
|
|
|
|
|
|
engine := adapterEngine(t)
|
|
|
|
|
wantContext := context.WithValue(context.Background(), contextKey{}, "prompt invocation")
|
|
|
|
|
wantInput := input{value: "typed input"}
|
|
|
|
|
wantState := state{value: "shared state"}
|
|
|
|
|
loaderCalls := 0
|
|
|
|
|
suite := FromEngine(func(ctx context.Context, selection ckconfig.Selection, gotInput input, gotState state) (*promptkit.Engine, error) {
|
|
|
|
|
loaderCalls++
|
|
|
|
|
if ctx.Value(contextKey{}) != "prompt invocation" || !selection.Overlay.Set || selection.Overlay.Value != "overlay.yml" || gotInput != wantInput || gotState != wantState {
|
|
|
|
|
t.Errorf("loader args = ctx=%v selection=%#v input=%#v state=%#v", ctx.Value(contextKey{}), selection, gotInput, gotState)
|
|
|
|
|
}
|
|
|
|
|
return engine, nil
|
|
|
|
|
})
|
|
|
|
|
selection := ckconfig.Selection{Overlay: commandkit.Supplied[string]{Value: "overlay.yml", Set: true}}
|
2026-10-17 18:39:38 +00:00
|
|
|
prompt, err := suite.InspectPrompt(wantContext, PromptRequest{Selection: selection, ID: "briefing"}, wantInput, wantState)
|
|
|
|
|
if err != nil || prompt.Value == nil || prompt.Value.PromptID != "briefing" || prompt.Value.PromptVersion == nil || *prompt.Value.PromptVersion != "1" || loaderCalls != 1 {
|
2026-10-11 01:43:02 +00:00
|
|
|
t.Fatalf("prompt inspection = (%#v, %v), calls=%d", prompt, err, loaderCalls)
|
|
|
|
|
}
|
2026-10-17 18:39:38 +00:00
|
|
|
secondPrompt, err := suite.InspectPrompt(wantContext, PromptRequest{Selection: selection, ID: "briefing"}, wantInput, wantState)
|
|
|
|
|
if err != nil || secondPrompt.Value == nil || secondPrompt.Value.PromptID != "briefing" || loaderCalls != 2 {
|
|
|
|
|
t.Fatalf("ID-only prompt inspection = (%#v, %v), calls=%d", secondPrompt, err, loaderCalls)
|
2026-10-11 01:43:02 +00:00
|
|
|
}
|
|
|
|
|
profile, err := suite.InspectProfile(wantContext, ProfileRequest{Selection: selection, ID: "default"}, wantInput, wantState)
|
|
|
|
|
if err != nil || profile.Value == nil || profile.Value.ProfileID != "default" || loaderCalls != 3 {
|
|
|
|
|
t.Fatalf("profile inspection = (%#v, %v), calls=%d", profile, err, loaderCalls)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
empty := FromEngine[int, int](nil)
|
2026-10-12 02:10:45 +00:00
|
|
|
empty.ListPrompts = nil
|
|
|
|
|
empty.ListProfiles = nil
|
2026-10-11 01:43:02 +00:00
|
|
|
empty.InspectPrompt = nil
|
|
|
|
|
empty.InspectProfile = nil
|
2026-10-12 16:54:00 +00:00
|
|
|
empty.InspectRequirements = nil
|
2026-10-11 01:43:02 +00:00
|
|
|
if len(empty.issues) != 1 || !strings.Contains(empty.issues[0].Error(), "PromptKit engine loader is nil") {
|
|
|
|
|
t.Fatalf("nil-loader issue was lost when callbacks were cleared: %#v", empty.issues)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-12 16:54:00 +00:00
|
|
|
func TestFromEngineInspectsRequirementsWithNativeRequest(t *testing.T) {
|
|
|
|
|
type input struct{ value string }
|
|
|
|
|
type state struct{ value string }
|
|
|
|
|
type contextKey struct{}
|
|
|
|
|
|
|
|
|
|
engine := adapterEngineFrom(t, "requirements adapter fixture", adapterRequirementsPrompt)
|
|
|
|
|
wantContext := context.WithValue(context.Background(), contextKey{}, "requirements invocation")
|
|
|
|
|
wantInput := input{value: "typed input"}
|
|
|
|
|
wantState := state{value: "shared state"}
|
|
|
|
|
selection := ckconfig.Selection{
|
|
|
|
|
Config: commandkit.Supplied[string]{Value: "prompt.yml", Set: true},
|
|
|
|
|
Overlay: commandkit.Supplied[string]{Value: "overlay.yml", Set: true},
|
|
|
|
|
}
|
|
|
|
|
request := RequirementsRequest{
|
|
|
|
|
Selection: selection,
|
|
|
|
|
Inspection: promptkit.RequirementInspectionRequest{
|
2026-10-17 18:39:38 +00:00
|
|
|
PromptID: "briefing",
|
|
|
|
|
ProfileID: "default",
|
|
|
|
|
Vars: map[string]string{"audience": "internal"},
|
|
|
|
|
SessionID: "session-42",
|
2026-10-12 16:54:00 +00:00
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
want, err := engine.InspectRequirements(wantContext, request.Inspection)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("direct native requirements inspection: %v", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
loaderCalls := 0
|
|
|
|
|
suite := FromEngine(func(ctx context.Context, gotSelection ckconfig.Selection, gotInput input, gotState state) (*promptkit.Engine, error) {
|
|
|
|
|
loaderCalls++
|
|
|
|
|
if ctx.Value(contextKey{}) != "requirements invocation" || !reflect.DeepEqual(gotSelection, selection) || gotInput != wantInput || gotState != wantState {
|
|
|
|
|
t.Errorf("loader args = ctx=%v selection=%#v input=%#v state=%#v", ctx.Value(contextKey{}), gotSelection, gotInput, gotState)
|
|
|
|
|
}
|
|
|
|
|
return engine, nil
|
|
|
|
|
})
|
|
|
|
|
acquired, err := suite.InspectRequirements(wantContext, request, wantInput, wantState)
|
|
|
|
|
if err != nil || acquired.Value == nil || !reflect.DeepEqual(acquired.Value, want) || loaderCalls != 1 {
|
|
|
|
|
t.Fatalf("requirements inspection = (%#v, %v), calls=%d; want native result %#v", acquired, err, loaderCalls, want)
|
|
|
|
|
}
|
2026-10-17 18:39:38 +00:00
|
|
|
if acquired.Value.PromptID != "briefing" || acquired.Value.PromptVersion == nil || *acquired.Value.PromptVersion != "1" || acquired.Value.ProfileID != "default" {
|
2026-10-12 16:54:00 +00:00
|
|
|
t.Fatalf("requirements selection = %#v", acquired.Value)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-26 03:26:14 +00:00
|
|
|
func TestFromEngineRequirementsRetainLoadExplanationAndSelectionErrors(t *testing.T) {
|
2026-10-12 16:54:00 +00:00
|
|
|
_, loadErr := configkit.Load[map[string]any](configkit.Bytes("invalid requirements configuration", []byte("broken: [")))
|
|
|
|
|
if loadErr == nil {
|
|
|
|
|
t.Fatal("invalid configuration unexpectedly loaded")
|
|
|
|
|
}
|
|
|
|
|
loadFailure := FromEngine[int, int](func(context.Context, ckconfig.Selection, int, int) (*promptkit.Engine, error) {
|
|
|
|
|
return nil, loadErr
|
|
|
|
|
})
|
|
|
|
|
acquired, err := loadFailure.InspectRequirements(context.Background(), RequirementsRequest{}, 0, 0)
|
|
|
|
|
var retained *configkit.LoadError
|
2026-10-26 03:26:14 +00:00
|
|
|
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)
|
2026-10-12 16:54:00 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
nilEngine := FromEngine[int, int](func(context.Context, ckconfig.Selection, int, int) (*promptkit.Engine, error) {
|
|
|
|
|
return nil, nil
|
|
|
|
|
})
|
|
|
|
|
acquired, err = nilEngine.InspectRequirements(context.Background(), RequirementsRequest{}, 0, 0)
|
|
|
|
|
if err == nil || acquired.Value != nil || !strings.Contains(err.Error(), "nil engine") {
|
|
|
|
|
t.Fatalf("nil engine result = (%#v, %v)", acquired, err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
engine := adapterEngine(t)
|
|
|
|
|
selectionFailure := FromEngine[int, int](func(context.Context, ckconfig.Selection, int, int) (*promptkit.Engine, error) {
|
|
|
|
|
return engine, nil
|
|
|
|
|
})
|
|
|
|
|
acquired, err = selectionFailure.InspectRequirements(context.Background(), RequirementsRequest{
|
|
|
|
|
Inspection: promptkit.RequirementInspectionRequest{PromptID: "missing", ProfileID: "default"},
|
|
|
|
|
}, 0, 0)
|
|
|
|
|
if !errors.Is(err, promptkit.ErrPromptNotFound) || acquired.Value != nil {
|
|
|
|
|
t.Fatalf("native selection failure = (%#v, %v), want ErrPromptNotFound", acquired, err)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestFromEngineRequirementsHonorCancellationAfterLoad(t *testing.T) {
|
|
|
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
|
|
|
engine := adapterEngine(t)
|
|
|
|
|
suite := FromEngine[int, int](func(context.Context, ckconfig.Selection, int, int) (*promptkit.Engine, error) {
|
|
|
|
|
cancel()
|
|
|
|
|
return engine, nil
|
|
|
|
|
})
|
|
|
|
|
acquired, err := suite.InspectRequirements(ctx, RequirementsRequest{
|
|
|
|
|
Inspection: promptkit.RequirementInspectionRequest{PromptID: "briefing", ProfileID: "default"},
|
|
|
|
|
}, 0, 0)
|
|
|
|
|
if !errors.Is(err, context.Canceled) || acquired.Value != nil {
|
|
|
|
|
t.Fatalf("cancelled requirements inspection = (%#v, %v)", acquired, err)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-12 02:10:45 +00:00
|
|
|
func TestFromEngineListsNativeCatalogsWithInvocationValues(t *testing.T) {
|
|
|
|
|
type input struct{ label string }
|
|
|
|
|
type state struct{ label string }
|
|
|
|
|
type contextKey struct{}
|
|
|
|
|
|
|
|
|
|
engine := adapterEngine(t)
|
|
|
|
|
wantContext := context.WithValue(context.Background(), contextKey{}, "catalog invocation")
|
|
|
|
|
wantSelection := ckconfig.Selection{Config: commandkit.Supplied[string]{Value: "selected.yml", Set: true}}
|
|
|
|
|
wantInput := input{label: "typed input"}
|
|
|
|
|
wantState := state{label: "shared state"}
|
|
|
|
|
loaderCalls := 0
|
|
|
|
|
suite := FromEngine(func(ctx context.Context, selection ckconfig.Selection, gotInput input, gotState state) (*promptkit.Engine, error) {
|
|
|
|
|
loaderCalls++
|
|
|
|
|
if ctx.Value(contextKey{}) != "catalog invocation" || selection != wantSelection || gotInput != wantInput || gotState != wantState {
|
|
|
|
|
t.Errorf("loader args = ctx=%v selection=%#v input=%#v state=%#v", ctx.Value(contextKey{}), selection, gotInput, gotState)
|
|
|
|
|
}
|
|
|
|
|
return engine, nil
|
|
|
|
|
})
|
2026-10-17 18:44:10 +00:00
|
|
|
if suite.ListPrompts == nil || suite.ListProfiles == nil || suite.ListBackends == nil || suite.ListNamespaces == nil || suite.ListCollections == nil {
|
|
|
|
|
t.Fatalf("FromEngine did not supply all five catalog callbacks: %#v", suite)
|
2026-10-12 02:10:45 +00:00
|
|
|
}
|
2026-10-17 18:44:10 +00:00
|
|
|
catalogRequest := CatalogRequest{Selection: wantSelection, Filter: promptkit.NamespaceFilter{Mode: promptkit.NamespaceNamed, Name: "missing"}}
|
|
|
|
|
namespaceRequest := NamespaceRequest{Selection: wantSelection, Kinds: []promptkit.ResourceKind{promptkit.ResourcePrompt}}
|
|
|
|
|
wantPrompts, err := engine.ListPrompts(wantContext, catalogRequest.Filter)
|
2026-10-12 02:10:45 +00:00
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
2026-10-17 18:44:10 +00:00
|
|
|
wantProfiles, err := engine.ListProfiles(wantContext, catalogRequest.Filter)
|
2026-10-12 02:10:45 +00:00
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
2026-10-17 18:44:10 +00:00
|
|
|
wantBackends, err := engine.ListBackends(wantContext, catalogRequest.Filter)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
wantNamespaces, err := engine.ListNamespaces(wantContext, namespaceRequest.Kinds...)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
wantCollections, err := engine.ListCollections(wantContext)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
prompts, err := suite.ListPrompts(wantContext, catalogRequest, wantInput, wantState)
|
2026-10-12 02:10:45 +00:00
|
|
|
if err != nil || !reflect.DeepEqual(prompts.Value, wantPrompts) || len(prompts.Diagnostics) != 0 || loaderCalls != 1 {
|
|
|
|
|
t.Fatalf("prompt catalog = (%#v, %v), calls=%d, want %#v", prompts, err, loaderCalls, wantPrompts)
|
|
|
|
|
}
|
2026-10-17 18:44:10 +00:00
|
|
|
profiles, err := suite.ListProfiles(wantContext, catalogRequest, wantInput, wantState)
|
2026-10-12 02:10:45 +00:00
|
|
|
if err != nil || !reflect.DeepEqual(profiles.Value, wantProfiles) || len(profiles.Diagnostics) != 0 || loaderCalls != 2 {
|
|
|
|
|
t.Fatalf("profile catalog = (%#v, %v), calls=%d, want %#v", profiles, err, loaderCalls, wantProfiles)
|
|
|
|
|
}
|
2026-10-17 18:44:10 +00:00
|
|
|
backends, err := suite.ListBackends(wantContext, catalogRequest, wantInput, wantState)
|
|
|
|
|
if err != nil || !reflect.DeepEqual(backends.Value, wantBackends) || len(backends.Diagnostics) != 0 || loaderCalls != 3 {
|
|
|
|
|
t.Fatalf("backend catalog = (%#v, %v), calls=%d, want %#v", backends, err, loaderCalls, wantBackends)
|
|
|
|
|
}
|
|
|
|
|
namespaces, err := suite.ListNamespaces(wantContext, namespaceRequest, wantInput, wantState)
|
|
|
|
|
if err != nil || !reflect.DeepEqual(namespaces.Value, wantNamespaces) || len(namespaces.Diagnostics) != 0 || loaderCalls != 4 {
|
|
|
|
|
t.Fatalf("namespace catalog = (%#v, %v), calls=%d, want %#v", namespaces, err, loaderCalls, wantNamespaces)
|
|
|
|
|
}
|
|
|
|
|
collections, err := suite.ListCollections(wantContext, wantSelection, wantInput, wantState)
|
|
|
|
|
if err != nil || !reflect.DeepEqual(collections.Value, wantCollections) || len(collections.Diagnostics) != 0 || loaderCalls != 5 {
|
|
|
|
|
t.Fatalf("collection catalog = (%#v, %v), calls=%d, want %#v", collections, err, loaderCalls, wantCollections)
|
|
|
|
|
}
|
2026-10-12 02:10:45 +00:00
|
|
|
}
|
|
|
|
|
|
2026-10-26 03:26:14 +00:00
|
|
|
func TestFromEngineCatalogsRetainLoadExplanationAndErrorIdentity(t *testing.T) {
|
2026-10-12 02:10:45 +00:00
|
|
|
_, loadErr := configkit.Load[map[string]any](configkit.Bytes("invalid catalog configuration", []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
|
|
|
|
|
})
|
|
|
|
|
for _, test := range []struct {
|
|
|
|
|
name string
|
|
|
|
|
call func() (int, error)
|
|
|
|
|
}{
|
|
|
|
|
{name: "prompts", call: func() (int, error) {
|
2026-10-17 18:44:10 +00:00
|
|
|
acquired, err := suite.ListPrompts(context.Background(), CatalogRequest{}, 0, 0)
|
2026-10-12 02:10:45 +00:00
|
|
|
var retained *configkit.LoadError
|
2026-10-26 03:26:14 +00:00
|
|
|
if !errors.As(err, &retained) || retained != loadErr || len(acquired.Explanation.Details) == 0 {
|
2026-10-12 02:10:45 +00:00
|
|
|
t.Errorf("prompt load failure = (%#v, %v)", acquired, err)
|
|
|
|
|
}
|
2026-10-26 03:26:14 +00:00
|
|
|
return len(acquired.Diagnostics) + len(acquired.Explanation.Details), err
|
2026-10-12 02:10:45 +00:00
|
|
|
}},
|
|
|
|
|
{name: "profiles", call: func() (int, error) {
|
2026-10-17 18:44:10 +00:00
|
|
|
acquired, err := suite.ListProfiles(context.Background(), CatalogRequest{}, 0, 0)
|
2026-10-12 02:10:45 +00:00
|
|
|
var retained *configkit.LoadError
|
2026-10-26 03:26:14 +00:00
|
|
|
if !errors.As(err, &retained) || retained != loadErr || len(acquired.Explanation.Details) == 0 {
|
2026-10-12 02:10:45 +00:00
|
|
|
t.Errorf("profile load failure = (%#v, %v)", acquired, err)
|
|
|
|
|
}
|
2026-10-26 03:26:14 +00:00
|
|
|
return len(acquired.Diagnostics) + len(acquired.Explanation.Details), err
|
2026-10-12 02:10:45 +00:00
|
|
|
}},
|
2026-10-17 18:44:10 +00:00
|
|
|
{name: "backends", call: func() (int, error) {
|
|
|
|
|
acquired, err := suite.ListBackends(context.Background(), CatalogRequest{}, 0, 0)
|
|
|
|
|
var retained *configkit.LoadError
|
2026-10-26 03:26:14 +00:00
|
|
|
if !errors.As(err, &retained) || retained != loadErr || len(acquired.Explanation.Details) == 0 {
|
2026-10-17 18:44:10 +00:00
|
|
|
t.Errorf("backend load failure = (%#v, %v)", acquired, err)
|
|
|
|
|
}
|
2026-10-26 03:26:14 +00:00
|
|
|
return len(acquired.Diagnostics) + len(acquired.Explanation.Details), err
|
2026-10-17 18:44:10 +00:00
|
|
|
}},
|
|
|
|
|
{name: "namespaces", call: func() (int, error) {
|
|
|
|
|
acquired, err := suite.ListNamespaces(context.Background(), NamespaceRequest{}, 0, 0)
|
|
|
|
|
var retained *configkit.LoadError
|
2026-10-26 03:26:14 +00:00
|
|
|
if !errors.As(err, &retained) || retained != loadErr || len(acquired.Explanation.Details) == 0 {
|
2026-10-17 18:44:10 +00:00
|
|
|
t.Errorf("namespace load failure = (%#v, %v)", acquired, err)
|
|
|
|
|
}
|
2026-10-26 03:26:14 +00:00
|
|
|
return len(acquired.Diagnostics) + len(acquired.Explanation.Details), err
|
2026-10-17 18:44:10 +00:00
|
|
|
}},
|
|
|
|
|
{name: "collections", call: func() (int, error) {
|
|
|
|
|
acquired, err := suite.ListCollections(context.Background(), ckconfig.Selection{}, 0, 0)
|
|
|
|
|
var retained *configkit.LoadError
|
2026-10-26 03:26:14 +00:00
|
|
|
if !errors.As(err, &retained) || retained != loadErr || len(acquired.Explanation.Details) == 0 {
|
2026-10-17 18:44:10 +00:00
|
|
|
t.Errorf("collection load failure = (%#v, %v)", acquired, err)
|
|
|
|
|
}
|
2026-10-26 03:26:14 +00:00
|
|
|
return len(acquired.Diagnostics) + len(acquired.Explanation.Details), err
|
2026-10-17 18:44:10 +00:00
|
|
|
}},
|
2026-10-12 02:10:45 +00:00
|
|
|
} {
|
|
|
|
|
t.Run(test.name, func(t *testing.T) {
|
|
|
|
|
diagnosticCount, err := test.call()
|
|
|
|
|
if !errors.Is(err, loadErr) || diagnosticCount == 0 {
|
|
|
|
|
t.Fatalf("catalog failure = (%d diagnostics, %v)", diagnosticCount, err)
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestFromEngineCatalogsHonorCancellationAfterLoad(t *testing.T) {
|
|
|
|
|
engine := adapterEngine(t)
|
|
|
|
|
loaderCalls := 0
|
|
|
|
|
for _, test := range []struct {
|
|
|
|
|
name string
|
|
|
|
|
call func(Suite[int, int], context.Context) error
|
|
|
|
|
}{
|
|
|
|
|
{name: "prompts", call: func(suite Suite[int, int], ctx context.Context) error {
|
2026-10-17 18:44:10 +00:00
|
|
|
_, err := suite.ListPrompts(ctx, CatalogRequest{}, 0, 0)
|
2026-10-12 02:10:45 +00:00
|
|
|
return err
|
|
|
|
|
}},
|
|
|
|
|
{name: "profiles", call: func(suite Suite[int, int], ctx context.Context) error {
|
2026-10-17 18:44:10 +00:00
|
|
|
_, err := suite.ListProfiles(ctx, CatalogRequest{}, 0, 0)
|
|
|
|
|
return err
|
|
|
|
|
}},
|
|
|
|
|
{name: "backends", call: func(suite Suite[int, int], ctx context.Context) error {
|
|
|
|
|
_, err := suite.ListBackends(ctx, CatalogRequest{}, 0, 0)
|
|
|
|
|
return err
|
|
|
|
|
}},
|
|
|
|
|
{name: "namespaces", call: func(suite Suite[int, int], ctx context.Context) error {
|
|
|
|
|
_, err := suite.ListNamespaces(ctx, NamespaceRequest{}, 0, 0)
|
|
|
|
|
return err
|
|
|
|
|
}},
|
|
|
|
|
{name: "collections", call: func(suite Suite[int, int], ctx context.Context) error {
|
|
|
|
|
_, err := suite.ListCollections(ctx, ckconfig.Selection{}, 0, 0)
|
2026-10-12 02:10:45 +00:00
|
|
|
return err
|
|
|
|
|
}},
|
|
|
|
|
} {
|
|
|
|
|
t.Run(test.name, func(t *testing.T) {
|
|
|
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
|
|
|
cancelSuite := FromEngine(func(_ context.Context, _ ckconfig.Selection, _, _ int) (*promptkit.Engine, error) {
|
|
|
|
|
loaderCalls++
|
|
|
|
|
cancel()
|
|
|
|
|
return engine, nil
|
|
|
|
|
})
|
|
|
|
|
if err := test.call(cancelSuite, ctx); !errors.Is(err, context.Canceled) {
|
|
|
|
|
t.Fatalf("catalog cancellation = %v", err)
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
}
|
2026-10-17 18:44:10 +00:00
|
|
|
if loaderCalls != 5 {
|
2026-10-12 02:10:45 +00:00
|
|
|
t.Fatalf("catalog loaders called = %d, want one per provider", loaderCalls)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-11 01:43:02 +00:00
|
|
|
func TestAdapterAcquisitionPreservesLoadAndContextErrors(t *testing.T) {
|
|
|
|
|
loadErr := errors.New("loader failure")
|
|
|
|
|
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)
|
|
|
|
|
if err != loadErr || len(acquired.Diagnostics) != 0 {
|
|
|
|
|
t.Fatalf("loader failure = (%#v, %v), want original error", acquired, err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
|
|
|
cancel()
|
|
|
|
|
engine := adapterEngine(t)
|
|
|
|
|
called := false
|
|
|
|
|
suite = FromEngine[int, int](func(context.Context, ckconfig.Selection, int, int) (*promptkit.Engine, error) {
|
|
|
|
|
called = true
|
|
|
|
|
return engine, nil
|
|
|
|
|
})
|
|
|
|
|
acquired, err = suite.InspectProfile(ctx, ProfileRequest{ID: "default"}, 0, 0)
|
|
|
|
|
if !errors.Is(err, context.Canceled) || acquired.Value != nil || !called {
|
|
|
|
|
t.Fatalf("cancelled profile acquisition = (%#v, %v), loader called=%t", acquired, err, called)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-26 03:26:14 +00:00
|
|
|
func TestFromEngineRetainsConfigKitLoadExplanation(t *testing.T) {
|
2026-10-12 01:55:08 +00:00
|
|
|
_, 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
|
2026-10-26 03:26:14 +00:00
|
|
|
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)
|
2026-10-12 01:55:08 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-11 01:43:02 +00:00
|
|
|
func TestPromptKitDiagnosticsRemainDeferredAndPropagateWriterErrors(t *testing.T) {
|
|
|
|
|
writeErr := errors.New("diagnostic writer failed")
|
2026-10-12 01:55:08 +00:00
|
|
|
sources := []configkit.SourceInfo{{ID: "source"}}
|
|
|
|
|
records := []configkit.Diagnostic{{
|
2026-10-11 01:43:02 +00:00
|
|
|
Severity: configkit.SeverityWarning,
|
|
|
|
|
Message: "safe warning",
|
2026-10-12 01:55:08 +00:00
|
|
|
}}
|
2026-10-26 03:26:14 +00:00
|
|
|
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)
|
2026-10-11 01:43:02 +00:00
|
|
|
}
|
2026-10-26 03:26:14 +00:00
|
|
|
if err := messages.diagnostics[0].Write(failingDiagnosticWriter{err: writeErr}); !errors.Is(err, writeErr) {
|
2026-10-11 01:43:02 +00:00
|
|
|
t.Fatalf("diagnostic write error = %v", err)
|
|
|
|
|
}
|
|
|
|
|
var output bytes.Buffer
|
2026-10-26 03:26:14 +00:00
|
|
|
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"`) {
|
2026-10-11 01:43:02 +00:00
|
|
|
t.Fatalf("diagnostic text = %q, %v", output.String(), err)
|
|
|
|
|
}
|
2026-10-12 01:55:08 +00:00
|
|
|
|
|
|
|
|
sideDiagnostics := promptKitDiagnostics("promptkit", "left", sources, records)
|
|
|
|
|
output.Reset()
|
2026-10-26 03:26:14 +00:00
|
|
|
if err := sideDiagnostics.diagnostics[0].Write(&output); err != nil || !strings.Contains(output.String(), `Configuration scope "promptkit" (left):`) {
|
2026-10-12 01:55:08 +00:00
|
|
|
t.Fatalf("comparison-side diagnostic text = %q, %v", output.String(), err)
|
|
|
|
|
}
|
2026-10-11 01:43:02 +00:00
|
|
|
}
|
|
|
|
|
|
2026-10-26 03:26:14 +00:00
|
|
|
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)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-11 01:43:02 +00:00
|
|
|
type failingDiagnosticWriter struct{ err error }
|
|
|
|
|
|
|
|
|
|
func (writer failingDiagnosticWriter) Write([]byte) (int, error) { return 0, writer.err }
|
|
|
|
|
|
|
|
|
|
var _ io.Writer = failingDiagnosticWriter{}
|