221 lines
9.9 KiB
Go
221 lines
9.9 KiB
Go
|
|
package commandkit_test
|
||
|
|
|
||
|
|
import (
|
||
|
|
"bytes"
|
||
|
|
"context"
|
||
|
|
"errors"
|
||
|
|
"io"
|
||
|
|
"strings"
|
||
|
|
"testing"
|
||
|
|
|
||
|
|
commandkit "gitea.maximumdirect.net/eric/CommandKit"
|
||
|
|
configkit "gitea.maximumdirect.net/eric/ConfigKit"
|
||
|
|
"gitea.maximumdirect.net/eric/promptkit"
|
||
|
|
)
|
||
|
|
|
||
|
|
func TestPromptKitScopeUsesCapturedInspectionAndSelectedInputs(t *testing.T) {
|
||
|
|
type appContext struct{ marker string }
|
||
|
|
type contextKey struct{}
|
||
|
|
|
||
|
|
engine, err := promptkit.NewEngine(promptkit.Config{Sources: promptkit.ConfigurationSources{
|
||
|
|
Main: &configkit.Input{Source: configkit.Mapping("local fixture", map[string]any{
|
||
|
|
"backends": map[string]any{"local": map[string]any{"endpoint": "https://example.invalid/v1"}},
|
||
|
|
"profiles": map[string]any{"base.v1": map[string]any{"backend": "local", "model": "fixture-model"}},
|
||
|
|
})},
|
||
|
|
}})
|
||
|
|
if err != nil {
|
||
|
|
t.Fatalf("construct PromptKit engine: %v", err)
|
||
|
|
}
|
||
|
|
inspection, err := engine.InspectConfiguration()
|
||
|
|
if err != nil {
|
||
|
|
t.Fatalf("inspect PromptKit engine: %v", err)
|
||
|
|
}
|
||
|
|
|
||
|
|
ctx := context.WithValue(context.Background(), contextKey{}, "caller-context")
|
||
|
|
selection := commandkit.Selection{Config: commandkit.Supplied[string]{Value: "main.yml", Set: true}, Overlay: commandkit.Supplied[string]{Value: "dev", Set: true}}
|
||
|
|
loaderCalls := 0
|
||
|
|
var receivedSelections []commandkit.Selection
|
||
|
|
scope, err := commandkit.NewPromptKitScope(func(gotContext context.Context, gotSelection commandkit.Selection, gotAppContext appContext) (*promptkit.Engine, error) {
|
||
|
|
loaderCalls++
|
||
|
|
receivedSelections = append(receivedSelections, gotSelection)
|
||
|
|
if gotContext != ctx || gotContext.Value(contextKey{}) != "caller-context" {
|
||
|
|
t.Fatal("PromptKit loader did not receive the caller context")
|
||
|
|
}
|
||
|
|
if gotAppContext.marker != "app-state" {
|
||
|
|
t.Fatalf("loader app context = %+v", gotAppContext)
|
||
|
|
}
|
||
|
|
return engine, nil
|
||
|
|
})
|
||
|
|
if err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
if scope.Name != "promptkit" || len(scope.Operations) != 4 {
|
||
|
|
t.Fatalf("scope declaration = %+v", scope)
|
||
|
|
}
|
||
|
|
|
||
|
|
acquired, err := scope.Acquire(ctx, commandkit.ConfigRequest{Operation: commandkit.ConfigShow, Selection: selection}, appContext{marker: "app-state"})
|
||
|
|
if err != nil {
|
||
|
|
t.Fatalf("acquire PromptKit scope: %v", err)
|
||
|
|
}
|
||
|
|
var gotEffective, wantEffective bytes.Buffer
|
||
|
|
if err := acquired.Value.Effective.WriteEffective(&gotEffective); err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
if err := inspection.WriteEffective(&wantEffective); err != nil || gotEffective.String() != wantEffective.String() {
|
||
|
|
t.Fatalf("native effective output = (%q, %v), want %q", gotEffective.String(), err, wantEffective.String())
|
||
|
|
}
|
||
|
|
if len(acquired.Value.Sources.Sources()) != len(inspection.Sources()) {
|
||
|
|
t.Fatalf("source inventory = %#v, want %#v", acquired.Value.Sources.Sources(), inspection.Sources())
|
||
|
|
}
|
||
|
|
path := configkit.Keys("profiles", "base.v1", "model")
|
||
|
|
var gotExplanation, wantExplanation bytes.Buffer
|
||
|
|
if err := acquired.Value.Explanation.WriteExplanation(&gotExplanation, path); err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
if err := inspection.WriteExplanation(&wantExplanation, path); err != nil || gotExplanation.String() != wantExplanation.String() {
|
||
|
|
t.Fatalf("native explanation = (%q, %v), want %q", gotExplanation.String(), err, wantExplanation.String())
|
||
|
|
}
|
||
|
|
if loaderCalls != 1 {
|
||
|
|
t.Fatalf("loader called %d times, want once", loaderCalls)
|
||
|
|
}
|
||
|
|
if len(receivedSelections) != 1 || receivedSelections[0] != selection {
|
||
|
|
t.Fatalf("loader selection = %#v, want %#v", receivedSelections, selection)
|
||
|
|
}
|
||
|
|
|
||
|
|
app, err := commandkit.New(commandkit.Identity{Name: "example"})
|
||
|
|
if err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
if err := commandkit.RegisterConfig(app, commandkit.ConfigSuite[appContext]{
|
||
|
|
NewContext: func() appContext { return appContext{marker: "app-state"} },
|
||
|
|
Scopes: []commandkit.ConfigScope[appContext]{scope},
|
||
|
|
}); err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
var show bytes.Buffer
|
||
|
|
outcome := app.Execute(ctx, []string{"config", "show", "--scope", "promptkit", "--config", "main.yml", "--overlay", "dev"}, commandkit.Streams{Out: &show, Err: io.Discard})
|
||
|
|
if outcome.Kind != commandkit.OutcomeSuccess || show.String() != wantEffective.String() {
|
||
|
|
t.Fatalf("config show = (%#v, %q), want native output %q", outcome, show.String(), wantEffective.String())
|
||
|
|
}
|
||
|
|
var sources bytes.Buffer
|
||
|
|
outcome = app.Execute(ctx, []string{"config", "sources", "--scope", "promptkit", "--format", "yaml"}, commandkit.Streams{Out: &sources, Err: io.Discard})
|
||
|
|
if outcome.Kind != commandkit.OutcomeSuccess || !strings.Contains(sources.String(), "label: local fixture") {
|
||
|
|
t.Fatalf("config sources = (%#v, %q), want captured PromptKit source", outcome, sources.String())
|
||
|
|
}
|
||
|
|
var explanation bytes.Buffer
|
||
|
|
outcome = app.Execute(ctx, []string{"config", "explain", "--scope", "promptkit", "--path", path.String()}, commandkit.Streams{Out: &explanation, Err: io.Discard})
|
||
|
|
if outcome.Kind != commandkit.OutcomeSuccess || explanation.String() != wantExplanation.String() {
|
||
|
|
t.Fatalf("config explain = (%#v, %q), want native output %q", outcome, explanation.String(), wantExplanation.String())
|
||
|
|
}
|
||
|
|
|
||
|
|
callsBeforeDiscovery := loaderCalls
|
||
|
|
outcome = app.Execute(ctx, []string{"config", "scopes"}, commandkit.Streams{Out: io.Discard, Err: io.Discard})
|
||
|
|
if outcome.Kind != commandkit.OutcomeSuccess || loaderCalls != callsBeforeDiscovery {
|
||
|
|
t.Fatalf("metadata discovery outcome=%#v loader calls=%d", outcome, loaderCalls)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestPromptKitScopeAggregateOrderAndConfigurationOnlyRegistration(t *testing.T) {
|
||
|
|
engine, err := promptkit.NewEngine(promptkit.Config{})
|
||
|
|
if err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
scope, err := commandkit.NewPromptKitScope(func(context.Context, commandkit.Selection, struct{}) (*promptkit.Engine, error) {
|
||
|
|
return engine, nil
|
||
|
|
})
|
||
|
|
if err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
app, err := commandkit.New(commandkit.Identity{Name: "example"})
|
||
|
|
if err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
if err := commandkit.RegisterConfig(app, commandkit.ConfigSuite[struct{}]{Scopes: []commandkit.ConfigScope[struct{}]{
|
||
|
|
{Name: "app", Operations: []commandkit.ConfigOperation{commandkit.ConfigShow}, Acquire: func(context.Context, commandkit.ConfigRequest, struct{}) (commandkit.Acquired[commandkit.ConfigView], error) {
|
||
|
|
return commandkit.Acquired[commandkit.ConfigView]{Value: commandkit.ConfigView{Effective: effectiveYAML("app: true\n")}}, nil
|
||
|
|
}}, scope,
|
||
|
|
}}); err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
var output bytes.Buffer
|
||
|
|
outcome := app.Execute(context.Background(), []string{"config", "show", "--scope", "all"}, commandkit.Streams{Out: &output, Err: io.Discard})
|
||
|
|
if outcome.Kind != commandkit.OutcomeSuccess {
|
||
|
|
t.Fatalf("aggregate show outcome = %#v", outcome)
|
||
|
|
}
|
||
|
|
appIndex, promptkitIndex := strings.Index(output.String(), "# scope: app"), strings.Index(output.String(), "# scope: promptkit")
|
||
|
|
if appIndex < 0 || promptkitIndex <= appIndex {
|
||
|
|
t.Fatalf("aggregate order = %q", output.String())
|
||
|
|
}
|
||
|
|
var scopes bytes.Buffer
|
||
|
|
outcome = app.Execute(context.Background(), []string{"config", "scopes"}, commandkit.Streams{Out: &scopes, Err: io.Discard})
|
||
|
|
if outcome.Kind != commandkit.OutcomeSuccess || !strings.Contains(scopes.String(), "promptkit: validate, show, sources, explain") {
|
||
|
|
t.Fatalf("configuration scope discovery = (%#v, %q)", outcome, scopes.String())
|
||
|
|
}
|
||
|
|
outcome = app.Execute(context.Background(), []string{"help", "promptkit"}, commandkit.Streams{Out: io.Discard, Err: io.Discard})
|
||
|
|
if outcome.Kind != commandkit.OutcomeUsage {
|
||
|
|
t.Fatalf("configuration-only install exposed promptkit definition commands: %#v", outcome)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestPromptKitScopeErrorsPreserveIdentityAndRejectNilInputs(t *testing.T) {
|
||
|
|
if _, err := commandkit.NewPromptKitScope[struct{}](nil); err == nil {
|
||
|
|
t.Fatal("nil loader was accepted")
|
||
|
|
}
|
||
|
|
var nilEngine *promptkit.Engine
|
||
|
|
if _, err := nilEngine.InspectConfiguration(); !errors.Is(err, promptkit.ErrInvalidConfig) {
|
||
|
|
t.Fatalf("PromptKit nil engine identity = %v", err)
|
||
|
|
}
|
||
|
|
|
||
|
|
loadErr := errors.Join(promptkit.ErrInvalidConfig, errors.New("loader context"))
|
||
|
|
scope, err := commandkit.NewPromptKitScope(func(context.Context, commandkit.Selection, struct{}) (*promptkit.Engine, error) {
|
||
|
|
return nil, loadErr
|
||
|
|
})
|
||
|
|
if err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
_, err = scope.Acquire(context.Background(), commandkit.ConfigRequest{}, struct{}{})
|
||
|
|
if err != loadErr || !errors.Is(err, promptkit.ErrInvalidConfig) {
|
||
|
|
t.Fatalf("loader error = %v, want original error identity %v", err, loadErr)
|
||
|
|
}
|
||
|
|
|
||
|
|
nilScope, err := commandkit.NewPromptKitScope(func(context.Context, commandkit.Selection, struct{}) (*promptkit.Engine, error) {
|
||
|
|
return nil, nil
|
||
|
|
})
|
||
|
|
if err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
if _, err := nilScope.Acquire(context.Background(), commandkit.ConfigRequest{}, struct{}{}); err == nil || !strings.Contains(err.Error(), "nil engine") {
|
||
|
|
t.Fatalf("nil engine acquisition error = %v", err)
|
||
|
|
}
|
||
|
|
|
||
|
|
negativeRetries := -1
|
||
|
|
invalidEngine, err := promptkit.NewEngine(promptkit.Config{TransientRetries: &negativeRetries})
|
||
|
|
if invalidEngine != nil || !errors.Is(err, promptkit.ErrInvalidConfig) {
|
||
|
|
t.Fatalf("invalid PromptKit construction = (%v, %v)", invalidEngine, err)
|
||
|
|
}
|
||
|
|
|
||
|
|
invalidEngine, err = promptkit.NewEngine(promptkit.Config{Sources: promptkit.ConfigurationSources{Main: &configkit.Input{
|
||
|
|
Source: configkit.Mapping("invalid inheritance", map[string]any{
|
||
|
|
"profiles": map[string]any{"child": map[string]any{"extends": "missing"}},
|
||
|
|
}),
|
||
|
|
}}})
|
||
|
|
if invalidEngine != nil || !errors.Is(err, promptkit.ErrInvalidConfig) {
|
||
|
|
t.Fatalf("invalid PromptKit profile construction = (%v, %v)", invalidEngine, err)
|
||
|
|
}
|
||
|
|
var configLoadErr *configkit.LoadError
|
||
|
|
if !errors.As(err, &configLoadErr) {
|
||
|
|
t.Fatalf("invalid PromptKit construction did not preserve ConfigKit LoadError: %v", err)
|
||
|
|
}
|
||
|
|
failedScope, scopeErr := commandkit.NewPromptKitScope(func(context.Context, commandkit.Selection, struct{}) (*promptkit.Engine, error) {
|
||
|
|
return nil, err
|
||
|
|
})
|
||
|
|
if scopeErr != nil {
|
||
|
|
t.Fatal(scopeErr)
|
||
|
|
}
|
||
|
|
failed, scopeErr := failedScope.Acquire(context.Background(), commandkit.ConfigRequest{}, struct{}{})
|
||
|
|
var acquiredLoadErr *configkit.LoadError
|
||
|
|
if scopeErr != err || !errors.As(scopeErr, &acquiredLoadErr) || acquiredLoadErr != configLoadErr || len(failed.Diagnostics) == 0 {
|
||
|
|
t.Fatalf("scope load error = (%v, %+v), want original LoadError and diagnostics", scopeErr, failed)
|
||
|
|
}
|
||
|
|
}
|