Move integration code into optional modules
This commit is contained in:
53
integrations/ckprompt/cancellation_test.go
Normal file
53
integrations/ckprompt/cancellation_test.go
Normal file
@@ -0,0 +1,53 @@
|
||||
package ckprompt_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
commandkit "gitea.maximumdirect.net/eric/CommandKit"
|
||||
configkit "gitea.maximumdirect.net/eric/ConfigKit"
|
||||
"gitea.maximumdirect.net/eric/promptkit"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestDefinitionInspectionCancellationBoundaries(t *testing.T) {
|
||||
for _, resource := range []string{"prompt", "profile"} {
|
||||
for _, before := range []bool{true, false} {
|
||||
t.Run(fmt.Sprintf("%s/before=%t", resource, before), func(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
if before {
|
||||
cancel()
|
||||
}
|
||||
calls, contexts := 0, 0
|
||||
diagnostics := []commandkit.Diagnostic{commandkit.MessageDiagnostic(configkit.SeverityWarning, "acquired warning")}
|
||||
suite := commandkit.PromptKitSuite[struct{}]{NewContext: func() struct{} { contexts++; return struct{}{} },
|
||||
InspectPrompt: func(context.Context, commandkit.Selection, struct{}, string, string) (commandkit.Acquired[*promptkit.PromptInspection], error) {
|
||||
calls++
|
||||
cancel()
|
||||
return commandkit.Acquired[*promptkit.PromptInspection]{Value: &promptkit.PromptInspection{}, Diagnostics: diagnostics}, nil
|
||||
},
|
||||
InspectProfile: func(context.Context, commandkit.Selection, struct{}, string) (commandkit.Acquired[*promptkit.ProfileInspection], error) {
|
||||
calls++
|
||||
cancel()
|
||||
return commandkit.Acquired[*promptkit.ProfileInspection]{Value: &promptkit.ProfileInspection{}, Diagnostics: diagnostics}, nil
|
||||
},
|
||||
}
|
||||
app, _ := commandkit.New(commandkit.Identity{Name: "example"})
|
||||
if err := commandkit.RegisterPromptKit(app, suite); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var out, errs bytes.Buffer
|
||||
result := app.Execute(ctx, []string{"promptkit", "show", resource, "example"}, commandkit.Streams{Out: &out, Err: &errs})
|
||||
want := 1
|
||||
if before {
|
||||
want = 0
|
||||
}
|
||||
if result.Kind != commandkit.OutcomeOperational || !errors.Is(result.Cause, context.Canceled) || calls != want || contexts != want || out.Len() != 0 || (!before && !strings.Contains(errs.String(), "acquired warning")) {
|
||||
t.Fatalf("result=%#v calls=%d contexts=%d out=%q diagnostics=%q", result, calls, contexts, out.String(), errs.String())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
2
integrations/ckprompt/doc.go
Normal file
2
integrations/ckprompt/doc.go
Normal file
@@ -0,0 +1,2 @@
|
||||
// Package ckprompt integrates PromptKit inspection with CommandKit.
|
||||
package ckprompt
|
||||
18
integrations/ckprompt/examples/composed/README.md
Normal file
18
integrations/ckprompt/examples/composed/README.md
Normal file
@@ -0,0 +1,18 @@
|
||||
# Composed consumer
|
||||
|
||||
This example composes application configuration, PromptKit configuration,
|
||||
definition inspection, and one application-owned command:
|
||||
|
||||
```sh
|
||||
go run . config scopes
|
||||
go run . config validate --scope all --campaign spring
|
||||
go run . promptkit show prompt briefing --campaign spring --format yaml
|
||||
go run . promptkit show profile spring --campaign spring
|
||||
go run . session restore session-42 --campaign spring
|
||||
```
|
||||
|
||||
The sources and session store are small in-memory fixtures. `--campaign` is a
|
||||
typed selector shared by the app and PromptKit configuration scopes. Their
|
||||
scope contexts share a per-invocation cache created by `ConfigSuite.NewContext`.
|
||||
Prompt/profile inspection has its own invocation context and composes the same
|
||||
application and engine loader functions.
|
||||
223
integrations/ckprompt/examples/composed/main.go
Normal file
223
integrations/ckprompt/examples/composed/main.go
Normal file
@@ -0,0 +1,223 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
commandkit "gitea.maximumdirect.net/eric/CommandKit"
|
||||
configkit "gitea.maximumdirect.net/eric/ConfigKit"
|
||||
"gitea.maximumdirect.net/eric/promptkit"
|
||||
)
|
||||
|
||||
type campaign struct {
|
||||
PromptID string `yaml:"prompt_id"`
|
||||
PromptVersion string `yaml:"prompt_version"`
|
||||
ProfileID string `yaml:"profile_id"`
|
||||
Model string `yaml:"model"`
|
||||
}
|
||||
|
||||
type appConfiguration struct {
|
||||
DefaultCampaign string `yaml:"default_campaign"`
|
||||
Campaigns map[string]campaign `yaml:"campaigns"`
|
||||
}
|
||||
|
||||
type invocationCache struct {
|
||||
application *configkit.Result[appConfiguration]
|
||||
}
|
||||
|
||||
type configContext struct {
|
||||
Cache *invocationCache
|
||||
Campaign commandkit.Supplied[string]
|
||||
}
|
||||
|
||||
type definitionContext struct {
|
||||
Cache *invocationCache
|
||||
Campaign commandkit.Supplied[string]
|
||||
}
|
||||
|
||||
type restoreInput struct {
|
||||
Campaign commandkit.Supplied[string]
|
||||
Session string
|
||||
}
|
||||
|
||||
type session struct {
|
||||
ID string
|
||||
Campaign string
|
||||
Summary string
|
||||
}
|
||||
|
||||
type sessionStore struct {
|
||||
active string
|
||||
records map[string]session
|
||||
}
|
||||
|
||||
func loadApplicationConfig(_ context.Context, _ commandkit.Selection, cache *invocationCache) (*configkit.Result[appConfiguration], error) {
|
||||
if cache != nil && cache.application != nil {
|
||||
return cache.application, nil
|
||||
}
|
||||
result, err := configkit.Load[appConfiguration](configkit.Mapping("composed app config", map[string]any{
|
||||
"default_campaign": "spring",
|
||||
"campaigns": map[string]any{
|
||||
"spring": map[string]any{"prompt_id": "briefing", "prompt_version": "1", "profile_id": "spring", "model": "spring-model"},
|
||||
"autumn": map[string]any{"prompt_id": "briefing", "prompt_version": "1", "profile_id": "autumn", "model": "autumn-model"},
|
||||
},
|
||||
}))
|
||||
if err == nil && cache != nil {
|
||||
cache.application = result
|
||||
}
|
||||
return result, err
|
||||
}
|
||||
|
||||
func selectedCampaign(configuration appConfiguration, supplied commandkit.Supplied[string]) (string, campaign, error) {
|
||||
name := configuration.DefaultCampaign
|
||||
if supplied.Set {
|
||||
name = supplied.Value
|
||||
}
|
||||
value, ok := configuration.Campaigns[name]
|
||||
if !ok {
|
||||
return "", campaign{}, fmt.Errorf("unknown campaign %q", name)
|
||||
}
|
||||
return name, value, nil
|
||||
}
|
||||
|
||||
func loadPromptEngine(ctx context.Context, selection commandkit.Selection, cache *invocationCache, selected commandkit.Supplied[string]) (*promptkit.Engine, error) {
|
||||
application, err := loadApplicationConfig(ctx, selection, cache)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
name, chosen, err := selectedCampaign(application.Config, selected)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
engineConfig := configkit.Mapping("campaign "+name, map[string]any{
|
||||
"profiles": map[string]any{chosen.ProfileID: map[string]any{
|
||||
"endpoint": "https://example.invalid/v1",
|
||||
"model": chosen.Model,
|
||||
"extra_params": map[string]any{
|
||||
"campaign": name,
|
||||
},
|
||||
}},
|
||||
"prompts": map[string]any{chosen.PromptID: map[string]any{
|
||||
chosen.PromptVersion: map[string]any{
|
||||
"default_profile": chosen.ProfileID,
|
||||
"inputs": []any{map[string]any{"name": "topic", "required": true, "content_type": "text/plain", "description": "Topic to brief"}},
|
||||
"messages": []any{map[string]any{"role": "user", "content": "Write a concise briefing about {{.topic}}."}},
|
||||
"output": map[string]any{"format": "text", "validation_mode": "none"},
|
||||
},
|
||||
}},
|
||||
})
|
||||
return promptkit.NewEngine(promptkit.Config{Sources: promptkit.ConfigurationSources{Main: &configkit.Input{Source: engineConfig}}})
|
||||
}
|
||||
|
||||
func campaignSelector[C any](set func(*C, commandkit.Supplied[string])) commandkit.Selector[C] {
|
||||
return commandkit.Selector[C]{
|
||||
Binding: commandkit.ValueFlag[C](commandkit.FlagMeta{Name: "campaign", Usage: "Select a configured campaign."}, nil, func(value string) (string, error) { return value, nil }, set),
|
||||
Scopes: []string{"app", "promptkit"},
|
||||
}
|
||||
}
|
||||
|
||||
func newApp() (*commandkit.App, error) {
|
||||
app, err := commandkit.New(commandkit.Identity{Name: "composed-example", Version: "1.0"})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
appScope, err := commandkit.NewConfigKitScope[configContext, appConfiguration]("app", "Application campaigns.", func(ctx context.Context, selection commandkit.Selection, input configContext) (*configkit.Result[appConfiguration], error) {
|
||||
return loadApplicationConfig(ctx, selection, input.Cache)
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
promptScope, err := commandkit.NewPromptKitScope(func(ctx context.Context, selection commandkit.Selection, input configContext) (*promptkit.Engine, error) {
|
||||
return loadPromptEngine(ctx, selection, input.Cache, input.Campaign)
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
configSuite := commandkit.ConfigSuite[configContext]{
|
||||
NewContext: func() configContext { return configContext{Cache: &invocationCache{}} },
|
||||
Selectors: []commandkit.Selector[configContext]{campaignSelector(func(input *configContext, value commandkit.Supplied[string]) {
|
||||
input.Campaign = value
|
||||
})},
|
||||
Scopes: []commandkit.ConfigScope[configContext]{appScope, promptScope},
|
||||
}
|
||||
if err := commandkit.RegisterConfig(app, configSuite); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
definitionSuite, err := commandkit.NewPromptKitSuite(func(ctx context.Context, selection commandkit.Selection, input definitionContext) (*promptkit.Engine, error) {
|
||||
return loadPromptEngine(ctx, selection, input.Cache, input.Campaign)
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
definitionSuite.NewContext = func() definitionContext { return definitionContext{Cache: &invocationCache{}} }
|
||||
definitionSuite.Selectors = []commandkit.Selector[definitionContext]{campaignSelector(func(input *definitionContext, value commandkit.Supplied[string]) {
|
||||
input.Campaign = value
|
||||
})}
|
||||
if err := commandkit.RegisterPromptKit(app, definitionSuite); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
store := &sessionStore{records: map[string]session{"session-42": {ID: "session-42", Campaign: "spring", Summary: "A synthetic planning session."}}}
|
||||
restore, err := commandkit.NewCommand(commandkit.CommandSpec[restoreInput]{
|
||||
Name: "restore",
|
||||
Summary: "Restore one saved session.",
|
||||
Flags: []commandkit.Flag[restoreInput]{commandkit.ValueFlag[restoreInput](commandkit.FlagMeta{Name: "campaign", Usage: "Campaign owning the session."}, nil, func(value string) (string, error) { return value, nil }, func(input *restoreInput, value commandkit.Supplied[string]) {
|
||||
input.Campaign = value
|
||||
})},
|
||||
Arguments: []commandkit.Argument[restoreInput]{commandkit.ValueArgument[restoreInput](commandkit.ArgumentMeta{Name: "ID"}, true, func(value string) (string, error) { return value, nil }, func(input *restoreInput, value commandkit.Supplied[string]) {
|
||||
input.Session = value.Value
|
||||
})},
|
||||
Run: func(invocation commandkit.Invocation, input restoreInput) commandkit.Outcome {
|
||||
return restoreSession(invocation, store, input)
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sessionGroup, err := commandkit.Group("session", "Manage saved sessions.", restore)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := app.Register(sessionGroup); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return app, nil
|
||||
}
|
||||
|
||||
func restoreSession(invocation commandkit.Invocation, store *sessionStore, input restoreInput) commandkit.Outcome {
|
||||
application, err := loadApplicationConfig(invocation.Context, commandkit.Selection{}, nil)
|
||||
if err != nil {
|
||||
return commandkit.Failure(err)
|
||||
}
|
||||
name, _, err := selectedCampaign(application.Config, input.Campaign)
|
||||
if err != nil {
|
||||
return commandkit.Failure(err)
|
||||
}
|
||||
record, ok := store.records[input.Session]
|
||||
if !ok {
|
||||
return commandkit.Outcome{Kind: commandkit.OutcomeDomain, Status: 4, Domain: "session_not_found", Cause: errors.New("saved session was not found")}
|
||||
}
|
||||
if record.Campaign != name {
|
||||
return commandkit.Outcome{Kind: commandkit.OutcomeDomain, Status: 3, Domain: "campaign_mismatch", Cause: fmt.Errorf("session belongs to campaign %q", record.Campaign)}
|
||||
}
|
||||
store.active = record.ID
|
||||
if _, err := fmt.Fprintf(invocation.Streams.Out, "restored %s: %s\n", record.ID, record.Summary); err != nil {
|
||||
return commandkit.Failure(err)
|
||||
}
|
||||
return commandkit.Success()
|
||||
}
|
||||
|
||||
func main() {
|
||||
app, err := newApp()
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
outcome := app.Execute(context.Background(), os.Args[1:], commandkit.Streams{Out: os.Stdout, Err: os.Stderr})
|
||||
if outcome.Status != 0 {
|
||||
os.Exit(outcome.Status)
|
||||
}
|
||||
}
|
||||
10
integrations/ckprompt/go.mod
Normal file
10
integrations/ckprompt/go.mod
Normal file
@@ -0,0 +1,10 @@
|
||||
module gitea.maximumdirect.net/eric/CommandKit/integrations/ckprompt
|
||||
|
||||
go 1.26.0
|
||||
|
||||
require (
|
||||
gitea.maximumdirect.net/eric/CommandKit v0.2.0
|
||||
gitea.maximumdirect.net/eric/CommandKit/integrations/ckconfig v0.1.0
|
||||
gitea.maximumdirect.net/eric/promptkit v0.12.0
|
||||
go.yaml.in/yaml/v4 v4.0.0-rc.6
|
||||
)
|
||||
161
integrations/ckprompt/performance_test.go
Normal file
161
integrations/ckprompt/performance_test.go
Normal file
@@ -0,0 +1,161 @@
|
||||
package ckprompt
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
promptkit "gitea.maximumdirect.net/eric/promptkit"
|
||||
)
|
||||
|
||||
func BenchmarkPerformancePromptReports(b *testing.B) {
|
||||
for _, inputCount := range []int{4, 128} {
|
||||
inspection := performancePromptInspection(inputCount)
|
||||
app := performancePromptApp(b, inspection, performanceProfileInspection(nil))
|
||||
for _, format := range []string{"text", "yaml"} {
|
||||
name := fmt.Sprintf("prompt/inputs_%d/%s", inputCount, format)
|
||||
args := []string{"promptkit", "show", "prompt", "briefing", "--format", format}
|
||||
b.Run(name, func(b *testing.B) {
|
||||
verifyPromptReport(b, app, args, format, "prompt_id")
|
||||
streams := Streams{Out: io.Discard, Err: io.Discard}
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
if outcome := app.Execute(context.Background(), args, streams); outcome.Kind != OutcomeSuccess {
|
||||
b.Fatalf("prompt report outcome = %#v", outcome)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
for _, fixture := range []struct {
|
||||
name string
|
||||
extra map[string]any
|
||||
}{
|
||||
{name: "small", extra: performanceExtraParams(2, 3)},
|
||||
{name: "large", extra: performanceExtraParams(24, 8)},
|
||||
} {
|
||||
profile := performanceProfileInspection(fixture.extra)
|
||||
app := performancePromptApp(b, performancePromptInspection(4), profile)
|
||||
for _, format := range []string{"text", "yaml"} {
|
||||
name := fmt.Sprintf("profile/extra_%s/%s", fixture.name, format)
|
||||
args := []string{"promptkit", "show", "profile", "baseline", "--format", format}
|
||||
b.Run(name, func(b *testing.B) {
|
||||
verifyPromptReport(b, app, args, format, "profile_id")
|
||||
streams := Streams{Out: io.Discard, Err: io.Discard}
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
if outcome := app.Execute(context.Background(), args, streams); outcome.Kind != OutcomeSuccess {
|
||||
b.Fatalf("profile report outcome = %#v", outcome)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func performancePromptInspection(inputCount int) *promptkit.PromptInspection {
|
||||
inputs := make([]promptkit.PromptInputDefinition, inputCount)
|
||||
for index := range inputs {
|
||||
inputs[index] = promptkit.PromptInputDefinition{
|
||||
Name: fmt.Sprintf("input%03d", index),
|
||||
Required: index%2 == 0,
|
||||
ContentType: "text/plain",
|
||||
Description: "A representative prompt input.",
|
||||
}
|
||||
}
|
||||
return &promptkit.PromptInspection{
|
||||
PromptID: "briefing",
|
||||
PromptVersion: "1",
|
||||
PromptHash: "fixed-hash",
|
||||
DefaultProfileID: "baseline",
|
||||
Inputs: inputs,
|
||||
OutputContract: promptkit.OutputContract{
|
||||
Format: promptkit.FormatText,
|
||||
ValidationMode: promptkit.ValidationBasic,
|
||||
RepairAttempts: 1,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func performanceProfileInspection(extra map[string]any) *promptkit.ProfileInspection {
|
||||
return &promptkit.ProfileInspection{
|
||||
ProfileID: "baseline",
|
||||
EffectiveModelParams: promptkit.ExecutionTarget{
|
||||
BackendID: "benchmark-backend",
|
||||
Endpoint: "https://example.invalid/v1",
|
||||
Model: "benchmark-model",
|
||||
Temperature: 0.5,
|
||||
MaxTokens: 512,
|
||||
ExtraParams: extra,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func performanceExtraParams(keys, width int) map[string]any {
|
||||
values := make(map[string]any, keys)
|
||||
for keyIndex := 0; keyIndex < keys; keyIndex++ {
|
||||
nested := make(map[string]any, width)
|
||||
for valueIndex := 0; valueIndex < width; valueIndex++ {
|
||||
nested[fmt.Sprintf("value%02d", valueIndex)] = fmt.Sprintf("sample-%02d-%02d", keyIndex, valueIndex)
|
||||
}
|
||||
values[fmt.Sprintf("group%02d", keyIndex)] = map[string]any{
|
||||
"enabled": keyIndex%2 == 0,
|
||||
"nested": nested,
|
||||
"sequence": []any{
|
||||
"first",
|
||||
keyIndex,
|
||||
map[string]any{"leaf": true},
|
||||
},
|
||||
}
|
||||
}
|
||||
return values
|
||||
}
|
||||
|
||||
func performancePromptApp(tb testing.TB, prompt *promptkit.PromptInspection, profile *promptkit.ProfileInspection) *App {
|
||||
tb.Helper()
|
||||
app, err := New(Identity{Name: "bench"})
|
||||
if err != nil {
|
||||
tb.Fatal(err)
|
||||
}
|
||||
suite := PromptKitSuite[struct{}]{
|
||||
InspectPrompt: func(context.Context, Selection, struct{}, string, string) (Acquired[*promptkit.PromptInspection], error) {
|
||||
return Acquired[*promptkit.PromptInspection]{Value: prompt}, nil
|
||||
},
|
||||
InspectProfile: func(context.Context, Selection, struct{}, string) (Acquired[*promptkit.ProfileInspection], error) {
|
||||
return Acquired[*promptkit.ProfileInspection]{Value: profile}, nil
|
||||
},
|
||||
}
|
||||
if err := RegisterPromptKit(app, suite); err != nil {
|
||||
tb.Fatal(err)
|
||||
}
|
||||
return app
|
||||
}
|
||||
|
||||
func verifyPromptReport(tb testing.TB, app *App, args []string, format, marker string) {
|
||||
tb.Helper()
|
||||
var output bytes.Buffer
|
||||
outcome := app.Execute(context.Background(), args, Streams{Out: &output, Err: io.Discard})
|
||||
wantMarker := marker
|
||||
if format == "text" {
|
||||
if marker == "prompt_id" {
|
||||
wantMarker = "Prompt ID:"
|
||||
} else {
|
||||
wantMarker = "Profile ID:"
|
||||
}
|
||||
}
|
||||
if outcome.Kind != OutcomeSuccess || !strings.Contains(output.String(), wantMarker) {
|
||||
tb.Fatalf("%s report outcome = %#v, output length %d", format, outcome, output.Len())
|
||||
}
|
||||
if format == "yaml" && !strings.Contains(output.String(), ":") {
|
||||
tb.Fatalf("YAML report lacks mapping entries: %q", output.String())
|
||||
}
|
||||
if format == "text" && !strings.Contains(output.String(), " ID:") {
|
||||
tb.Fatalf("text report has unexpected shape: %q", output.String()[:min(output.Len(), 80)])
|
||||
}
|
||||
}
|
||||
478
integrations/ckprompt/promptkit.go
Normal file
478
integrations/ckprompt/promptkit.go
Normal file
@@ -0,0 +1,478 @@
|
||||
package ckprompt
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
configkit "gitea.maximumdirect.net/eric/ConfigKit"
|
||||
"gitea.maximumdirect.net/eric/promptkit"
|
||||
)
|
||||
|
||||
// PromptKitSuite declares optional PromptKit prompt and profile inspection
|
||||
// commands for an App.
|
||||
type PromptKitSuite[C any] struct {
|
||||
// NewContext creates command-local application context when an inspection runs.
|
||||
NewContext func() C
|
||||
// Selectors declares context flags accepted by the installed commands. Each
|
||||
// explicit scope list must include "promptkit"; an empty list applies here.
|
||||
Selectors []Selector[C]
|
||||
// CheckInput validates standard configuration selections and selector values.
|
||||
CheckInput func(Selection, C) error
|
||||
// InspectPrompt inspects an ID and exact or omitted version.
|
||||
InspectPrompt func(context.Context, Selection, C, string, string) (Acquired[*promptkit.PromptInspection], error)
|
||||
// InspectProfile inspects one profile ID.
|
||||
InspectProfile func(context.Context, Selection, C, string) (Acquired[*promptkit.ProfileInspection], error)
|
||||
}
|
||||
|
||||
// NewPromptKitSuite creates prompt and profile inspection callbacks backed by
|
||||
// a PromptKit Engine loader. The loader runs once per selected command. Each
|
||||
// callback captures configuration diagnostics before calling the corresponding
|
||||
// PromptKit inspection method, and returns those diagnostics with lookup errors.
|
||||
func NewPromptKitSuite[C any](
|
||||
load func(context.Context, Selection, C) (*promptkit.Engine, error),
|
||||
) (PromptKitSuite[C], error) {
|
||||
if load == nil {
|
||||
return PromptKitSuite[C]{}, fmt.Errorf("PromptKit engine loader is nil")
|
||||
}
|
||||
return PromptKitSuite[C]{
|
||||
InspectPrompt: func(ctx context.Context, selection Selection, appContext C, id, version string) (Acquired[*promptkit.PromptInspection], error) {
|
||||
engine, _, diagnostics, err := loadPromptKitInspection(ctx, selection, appContext, load)
|
||||
if err != nil {
|
||||
return Acquired[*promptkit.PromptInspection]{Diagnostics: diagnostics}, err
|
||||
}
|
||||
inspection, err := engine.InspectPrompt(ctx, id, version)
|
||||
return Acquired[*promptkit.PromptInspection]{Value: inspection, Diagnostics: diagnostics}, err
|
||||
},
|
||||
InspectProfile: func(ctx context.Context, selection Selection, appContext C, id string) (Acquired[*promptkit.ProfileInspection], error) {
|
||||
engine, _, diagnostics, err := loadPromptKitInspection(ctx, selection, appContext, load)
|
||||
if err != nil {
|
||||
return Acquired[*promptkit.ProfileInspection]{Diagnostics: diagnostics}, err
|
||||
}
|
||||
inspection, err := engine.InspectProfile(ctx, id)
|
||||
return Acquired[*promptkit.ProfileInspection]{Value: inspection, Diagnostics: diagnostics}, err
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func loadPromptKitInspection[C any](ctx context.Context, selection Selection, appContext C, load func(context.Context, Selection, C) (*promptkit.Engine, error)) (*promptkit.Engine, promptkit.ConfigurationInspection, []Diagnostic, error) {
|
||||
engine, err := load(ctx, selection, appContext)
|
||||
if err != nil {
|
||||
var loadErr *configkit.LoadError
|
||||
if errors.As(err, &loadErr) {
|
||||
return nil, promptkit.ConfigurationInspection{}, ConfigKitDiagnostics("promptkit", loadErr.Sources(), loadErr.Diagnostics()), err
|
||||
}
|
||||
return nil, promptkit.ConfigurationInspection{}, nil, err
|
||||
}
|
||||
if engine == nil {
|
||||
return nil, promptkit.ConfigurationInspection{}, nil, fmt.Errorf("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()
|
||||
if err != nil {
|
||||
return nil, promptkit.ConfigurationInspection{}, nil, err
|
||||
}
|
||||
sources := inspection.Sources()
|
||||
diagnostics := ConfigKitDiagnostics("promptkit", sources, inspection.Diagnostics())
|
||||
return engine, inspection, diagnostics, nil
|
||||
}
|
||||
|
||||
// RegisterPromptKit installs the supplied PromptKit inspection commands under
|
||||
// promptkit show. Each nonnil callback installs only its matching command. A
|
||||
// suite with no inspection callbacks installs no PromptKit namespace.
|
||||
func RegisterPromptKit[C any](app *App, suite PromptKitSuite[C]) error {
|
||||
if suite.InspectPrompt == nil && suite.InspectProfile == nil {
|
||||
return nil
|
||||
}
|
||||
if app == nil {
|
||||
return fmt.Errorf("cannot register PromptKit commands on a nil app")
|
||||
}
|
||||
normalized, err := normalizePromptKitSuite(suite)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
showCommands := make([]Command, 0, 2)
|
||||
if normalized.InspectPrompt != nil {
|
||||
command, err := newPromptKitPromptCommand(normalized)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
showCommands = append(showCommands, command)
|
||||
}
|
||||
if normalized.InspectProfile != nil {
|
||||
command, err := newPromptKitProfileCommand(normalized)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
showCommands = append(showCommands, command)
|
||||
}
|
||||
show, err := Group("show", "Inspect PromptKit definitions.", showCommands...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
root, err := Group("promptkit", "Inspect PromptKit definitions.", show)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return app.Register(root)
|
||||
}
|
||||
|
||||
func normalizePromptKitSuite[C any](suite PromptKitSuite[C]) (PromptKitSuite[C], error) {
|
||||
normalized := suite
|
||||
normalized.Selectors = append([]Selector[C](nil), suite.Selectors...)
|
||||
standardNames := map[string]bool{"help": true}
|
||||
for _, flag := range promptKitInputFlags(PromptKitSuite[C]{}, suite.InspectPrompt != nil) {
|
||||
standardNames[flag.binding.meta.Name] = true
|
||||
}
|
||||
seenNames := make(map[string]struct{}, len(normalized.Selectors))
|
||||
seenShorts := make(map[string]struct{}, len(normalized.Selectors))
|
||||
for i, selector := range normalized.Selectors {
|
||||
binding := selector.Binding.binding
|
||||
meta := binding.meta
|
||||
if !binding.valid || binding.clear == nil || meta.Name == "" {
|
||||
return PromptKitSuite[C]{}, fmt.Errorf("PromptKit selector %d has an invalid flag binding", i)
|
||||
}
|
||||
if !validFlagName(meta.Name) {
|
||||
return PromptKitSuite[C]{}, fmt.Errorf("PromptKit selector flag name %q is invalid", meta.Name)
|
||||
}
|
||||
if standardNames[meta.Name] {
|
||||
return PromptKitSuite[C]{}, fmt.Errorf("PromptKit selector flag --%s conflicts with a standard flag", meta.Name)
|
||||
}
|
||||
if meta.Short == "h" {
|
||||
return PromptKitSuite[C]{}, fmt.Errorf("PromptKit selector short flag -%s conflicts with help", meta.Short)
|
||||
}
|
||||
if meta.Short != "" && (len(meta.Short) != 1 || meta.Short == "-") {
|
||||
return PromptKitSuite[C]{}, fmt.Errorf("PromptKit selector short spelling %q must be one byte", meta.Short)
|
||||
}
|
||||
if _, exists := seenNames[meta.Name]; exists {
|
||||
return PromptKitSuite[C]{}, fmt.Errorf("duplicate PromptKit selector flag --%s", meta.Name)
|
||||
}
|
||||
seenNames[meta.Name] = struct{}{}
|
||||
if meta.Short != "" {
|
||||
if _, exists := seenShorts[meta.Short]; exists {
|
||||
return PromptKitSuite[C]{}, fmt.Errorf("duplicate PromptKit selector short flag -%s", meta.Short)
|
||||
}
|
||||
seenShorts[meta.Short] = struct{}{}
|
||||
}
|
||||
applies := len(selector.Scopes) == 0
|
||||
for _, scope := range selector.Scopes {
|
||||
if scope == "promptkit" {
|
||||
applies = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !applies {
|
||||
return PromptKitSuite[C]{}, fmt.Errorf("PromptKit selector --%s must apply to scope %q", meta.Name, "promptkit")
|
||||
}
|
||||
normalized.Selectors[i].Scopes = append([]string(nil), selector.Scopes...)
|
||||
}
|
||||
return normalized, nil
|
||||
}
|
||||
|
||||
type promptKitInput[C any] struct {
|
||||
Selection Selection
|
||||
Format string
|
||||
ID string
|
||||
Version string
|
||||
SelectorContext C
|
||||
selectors []selectorState[C]
|
||||
}
|
||||
|
||||
func promptKitInputFlags[C any](suite PromptKitSuite[C], prompt bool) []Flag[promptKitInput[C]] {
|
||||
formatDefault := "text"
|
||||
flags := []Flag[promptKitInput[C]]{
|
||||
ValueFlag[promptKitInput[C]](FlagMeta{Name: "format", Usage: "Output format."}, &formatDefault, parseString, func(input *promptKitInput[C], value Supplied[string]) {
|
||||
input.Format = value.Value
|
||||
}),
|
||||
}
|
||||
flags = append(flags, configurationSelectionFlags(
|
||||
func(input *promptKitInput[C], value Supplied[string]) { input.Selection.Config = value },
|
||||
func(input *promptKitInput[C], value Supplied[string]) { input.Selection.Overlay = value },
|
||||
)...)
|
||||
if prompt {
|
||||
versionDefault := ""
|
||||
flags = append(flags, ValueFlag[promptKitInput[C]](FlagMeta{Name: "version", Usage: "Exact prompt version; empty selects the owning default."}, &versionDefault, parseString, func(input *promptKitInput[C], value Supplied[string]) {
|
||||
input.Version = value.Value
|
||||
}))
|
||||
}
|
||||
for _, selector := range suite.Selectors {
|
||||
flags = append(flags, adaptContextSelector(selector,
|
||||
func(input *promptKitInput[C]) *C { return &input.SelectorContext },
|
||||
func(input *promptKitInput[C], state selectorState[C]) {
|
||||
input.selectors = append(input.selectors, state)
|
||||
},
|
||||
))
|
||||
}
|
||||
return flags
|
||||
}
|
||||
|
||||
func checkPromptKitInput[C any](suite PromptKitSuite[C], input promptKitInput[C]) error {
|
||||
if _, err := parseConfigFormat(input.Format); err != nil {
|
||||
return err
|
||||
}
|
||||
if suite.CheckInput != nil {
|
||||
return suite.CheckInput(input.Selection, input.SelectorContext)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func promptKitContext[C any](suite PromptKitSuite[C], input promptKitInput[C]) C {
|
||||
var result C
|
||||
if suite.NewContext != nil {
|
||||
result = suite.NewContext()
|
||||
}
|
||||
for _, selector := range input.selectors {
|
||||
if selectorApplies(selector.scopes, "promptkit") {
|
||||
selector.apply(&result)
|
||||
} else {
|
||||
selector.clear(&result)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func newPromptKitPromptCommand[C any](suite PromptKitSuite[C]) (Command, error) {
|
||||
inputSpec := CommandSpec[promptKitInput[C]]{
|
||||
Name: "prompt",
|
||||
Summary: "Inspect one PromptKit prompt definition.",
|
||||
Flags: promptKitInputFlags(suite, true),
|
||||
Arguments: []Argument[promptKitInput[C]]{ValueArgument[promptKitInput[C]](ArgumentMeta{Name: "ID"}, true, parseString, func(input *promptKitInput[C], value Supplied[string]) {
|
||||
input.ID = value.Value
|
||||
})},
|
||||
CheckInput: func(input promptKitInput[C]) error { return checkPromptKitInput(suite, input) },
|
||||
Run: func(invocation Invocation, input promptKitInput[C]) Outcome {
|
||||
return runPromptKitPrompt(invocation, suite, input)
|
||||
},
|
||||
}
|
||||
return NewCommand(inputSpec)
|
||||
}
|
||||
|
||||
func newPromptKitProfileCommand[C any](suite PromptKitSuite[C]) (Command, error) {
|
||||
inputSpec := CommandSpec[promptKitInput[C]]{
|
||||
Name: "profile",
|
||||
Summary: "Inspect one PromptKit profile definition.",
|
||||
Flags: promptKitInputFlags(suite, false),
|
||||
Arguments: []Argument[promptKitInput[C]]{ValueArgument[promptKitInput[C]](ArgumentMeta{Name: "ID"}, true, parseString, func(input *promptKitInput[C], value Supplied[string]) {
|
||||
input.ID = value.Value
|
||||
})},
|
||||
CheckInput: func(input promptKitInput[C]) error { return checkPromptKitInput(suite, input) },
|
||||
Run: func(invocation Invocation, input promptKitInput[C]) Outcome {
|
||||
return runPromptKitProfile(invocation, suite, input)
|
||||
},
|
||||
}
|
||||
return NewCommand(inputSpec)
|
||||
}
|
||||
|
||||
func runPromptKitPrompt[C any](invocation Invocation, suite PromptKitSuite[C], input promptKitInput[C]) Outcome {
|
||||
if err := invocation.Context.Err(); err != nil {
|
||||
return Failure(err)
|
||||
}
|
||||
contextValue := promptKitContext(suite, input)
|
||||
if err := invocation.Context.Err(); err != nil {
|
||||
return Failure(err)
|
||||
}
|
||||
acquired, err := suite.InspectPrompt(invocation.Context, input.Selection, contextValue, input.ID, input.Version)
|
||||
if err != nil {
|
||||
return promptKitFailure(err, acquired.Diagnostics)
|
||||
}
|
||||
if err := invocation.Context.Err(); err != nil {
|
||||
return promptKitFailure(err, acquired.Diagnostics)
|
||||
}
|
||||
if acquired.Value == nil {
|
||||
return promptKitFailure(fmt.Errorf("PromptKit prompt inspection returned no result"), acquired.Diagnostics)
|
||||
}
|
||||
report := promptReportFromInspection(acquired.Value)
|
||||
var output []byte
|
||||
if input.Format == "yaml" {
|
||||
var buffer bytes.Buffer
|
||||
if err := encodeYAML(&buffer, report); err != nil {
|
||||
return promptKitFailure(err, acquired.Diagnostics)
|
||||
}
|
||||
output = buffer.Bytes()
|
||||
} else {
|
||||
output = []byte(formatPromptText(report))
|
||||
}
|
||||
return writePromptKitReport(invocation, output, acquired.Diagnostics)
|
||||
}
|
||||
|
||||
func runPromptKitProfile[C any](invocation Invocation, suite PromptKitSuite[C], input promptKitInput[C]) Outcome {
|
||||
if err := invocation.Context.Err(); err != nil {
|
||||
return Failure(err)
|
||||
}
|
||||
contextValue := promptKitContext(suite, input)
|
||||
if err := invocation.Context.Err(); err != nil {
|
||||
return Failure(err)
|
||||
}
|
||||
acquired, err := suite.InspectProfile(invocation.Context, input.Selection, contextValue, input.ID)
|
||||
if err != nil {
|
||||
return promptKitFailure(err, acquired.Diagnostics)
|
||||
}
|
||||
if err := invocation.Context.Err(); err != nil {
|
||||
return promptKitFailure(err, acquired.Diagnostics)
|
||||
}
|
||||
if acquired.Value == nil {
|
||||
return promptKitFailure(fmt.Errorf("PromptKit profile inspection returned no result"), acquired.Diagnostics)
|
||||
}
|
||||
report := profileReportFromInspection(acquired.Value)
|
||||
var output []byte
|
||||
if input.Format == "yaml" {
|
||||
var buffer bytes.Buffer
|
||||
if err := encodeYAML(&buffer, report); err != nil {
|
||||
return promptKitFailure(err, acquired.Diagnostics)
|
||||
}
|
||||
output = buffer.Bytes()
|
||||
} else {
|
||||
textReport, err := formatProfileText(report)
|
||||
if err != nil {
|
||||
return promptKitFailure(err, acquired.Diagnostics)
|
||||
}
|
||||
output = []byte(textReport)
|
||||
}
|
||||
return writePromptKitReport(invocation, output, acquired.Diagnostics)
|
||||
}
|
||||
|
||||
func promptKitFailure(err error, diagnostics []Diagnostic) Outcome {
|
||||
outcome := Failure(err)
|
||||
outcome.Diagnostics = diagnostics
|
||||
return outcome
|
||||
}
|
||||
|
||||
func writePromptKitReport(invocation Invocation, output []byte, diagnostics []Diagnostic) Outcome {
|
||||
if err := invocation.Context.Err(); err != nil {
|
||||
return promptKitFailure(err, diagnostics)
|
||||
}
|
||||
if err := writeReport(invocation.Streams.Out, output); err != nil {
|
||||
outcome := incompleteReport(err)
|
||||
outcome.Diagnostics = diagnostics
|
||||
return outcome
|
||||
}
|
||||
outcome := Success()
|
||||
outcome.Diagnostics = diagnostics
|
||||
return outcome
|
||||
}
|
||||
|
||||
type promptInputReport struct {
|
||||
Name string `yaml:"name"`
|
||||
Required bool `yaml:"required"`
|
||||
ContentType string `yaml:"content_type"`
|
||||
Description string `yaml:"description"`
|
||||
}
|
||||
|
||||
type promptOutputContractReport struct {
|
||||
Format string `yaml:"format"`
|
||||
ValidationMode string `yaml:"validation_mode"`
|
||||
SchemaPath string `yaml:"schema_path"`
|
||||
RepairAttempts int `yaml:"repair_attempts"`
|
||||
}
|
||||
|
||||
type promptInspectionReport struct {
|
||||
PromptID string `yaml:"prompt_id"`
|
||||
PromptVersion string `yaml:"prompt_version"`
|
||||
PromptHash string `yaml:"prompt_hash"`
|
||||
DefaultProfileID string `yaml:"default_profile_id"`
|
||||
Inputs []promptInputReport `yaml:"inputs"`
|
||||
OutputContract promptOutputContractReport `yaml:"output_contract"`
|
||||
}
|
||||
|
||||
func promptReportFromInspection(inspection *promptkit.PromptInspection) promptInspectionReport {
|
||||
report := promptInspectionReport{
|
||||
PromptID: inspection.PromptID,
|
||||
PromptVersion: inspection.PromptVersion,
|
||||
PromptHash: inspection.PromptHash,
|
||||
DefaultProfileID: inspection.DefaultProfileID,
|
||||
Inputs: make([]promptInputReport, 0, len(inspection.Inputs)),
|
||||
OutputContract: promptOutputContractReport{
|
||||
Format: string(inspection.OutputContract.Format),
|
||||
ValidationMode: string(inspection.OutputContract.ValidationMode),
|
||||
SchemaPath: inspection.OutputContract.SchemaPath,
|
||||
RepairAttempts: inspection.OutputContract.RepairAttempts,
|
||||
},
|
||||
}
|
||||
for _, input := range inspection.Inputs {
|
||||
report.Inputs = append(report.Inputs, promptInputReport{Name: input.Name, Required: input.Required, ContentType: input.ContentType, Description: input.Description})
|
||||
}
|
||||
return report
|
||||
}
|
||||
|
||||
type modelParamsReport struct {
|
||||
BackendID string `yaml:"backend_id"`
|
||||
Endpoint string `yaml:"endpoint"`
|
||||
Model string `yaml:"model"`
|
||||
Temperature float64 `yaml:"temperature"`
|
||||
MaxTokens int `yaml:"max_tokens"`
|
||||
TopP float64 `yaml:"top_p"`
|
||||
TimeoutSeconds int `yaml:"timeout_seconds"`
|
||||
ServiceTier string `yaml:"service_tier"`
|
||||
ReasoningEffort string `yaml:"reasoning_effort"`
|
||||
APIKeyEnv string `yaml:"api_key_env"`
|
||||
ExtraParams map[string]any `yaml:"extra_params"`
|
||||
}
|
||||
|
||||
type profileInspectionReport struct {
|
||||
ProfileID string `yaml:"profile_id"`
|
||||
EffectiveModelParams modelParamsReport `yaml:"effective_model_params"`
|
||||
APIKeyRequired bool `yaml:"api_key_required"`
|
||||
}
|
||||
|
||||
func profileReportFromInspection(inspection *promptkit.ProfileInspection) profileInspectionReport {
|
||||
params := inspection.EffectiveModelParams
|
||||
extra := params.ExtraParams
|
||||
if extra == nil {
|
||||
extra = map[string]any{}
|
||||
}
|
||||
return profileInspectionReport{
|
||||
ProfileID: inspection.ProfileID,
|
||||
EffectiveModelParams: modelParamsReport{
|
||||
BackendID: params.BackendID, Endpoint: params.Endpoint, Model: params.Model,
|
||||
Temperature: params.Temperature, MaxTokens: params.MaxTokens, TopP: params.TopP,
|
||||
TimeoutSeconds: params.TimeoutSeconds, ServiceTier: params.ServiceTier,
|
||||
ReasoningEffort: params.ReasoningEffort, APIKeyEnv: params.APIKeyEnv, ExtraParams: extra,
|
||||
},
|
||||
APIKeyRequired: inspection.APIKeyRequired,
|
||||
}
|
||||
}
|
||||
|
||||
func formatPromptText(report promptInspectionReport) string {
|
||||
var output strings.Builder
|
||||
fmt.Fprintf(&output, "Prompt ID: %s\n", strconv.Quote(report.PromptID))
|
||||
fmt.Fprintf(&output, "Prompt version: %s\n", strconv.Quote(report.PromptVersion))
|
||||
fmt.Fprintf(&output, "Prompt hash: %s\n", strconv.Quote(report.PromptHash))
|
||||
fmt.Fprintf(&output, "Default profile ID: %s\n", strconv.Quote(report.DefaultProfileID))
|
||||
output.WriteString("Inputs:\n")
|
||||
for _, input := range report.Inputs {
|
||||
fmt.Fprintf(&output, " - name: %s\n required: %t\n content_type: %s\n description: %s\n", strconv.Quote(input.Name), input.Required, strconv.Quote(input.ContentType), strconv.Quote(input.Description))
|
||||
}
|
||||
if len(report.Inputs) == 0 {
|
||||
output.WriteString(" []\n")
|
||||
}
|
||||
fmt.Fprintf(&output, "Output contract:\n format: %s\n validation_mode: %s\n schema_path: %s\n repair_attempts: %d\n", strconv.Quote(report.OutputContract.Format), strconv.Quote(report.OutputContract.ValidationMode), strconv.Quote(report.OutputContract.SchemaPath), report.OutputContract.RepairAttempts)
|
||||
return output.String()
|
||||
}
|
||||
|
||||
func formatProfileText(report profileInspectionReport) (string, error) {
|
||||
params := report.EffectiveModelParams
|
||||
var output strings.Builder
|
||||
fmt.Fprintf(&output, "Profile ID: %s\n", strconv.Quote(report.ProfileID))
|
||||
output.WriteString("Effective model params:\n")
|
||||
fmt.Fprintf(&output, " backend_id: %s\n endpoint: %s\n model: %s\n temperature: %s\n max_tokens: %d\n top_p: %s\n timeout_seconds: %d\n service_tier: %s\n reasoning_effort: %s\n api_key_env: %s\n", strconv.Quote(params.BackendID), strconv.Quote(params.Endpoint), strconv.Quote(params.Model), strconv.FormatFloat(params.Temperature, 'g', -1, 64), params.MaxTokens, strconv.FormatFloat(params.TopP, 'g', -1, 64), params.TimeoutSeconds, strconv.Quote(params.ServiceTier), strconv.Quote(params.ReasoningEffort), strconv.Quote(params.APIKeyEnv))
|
||||
output.WriteString(" extra_params:\n")
|
||||
var nested strings.Builder
|
||||
if err := encodeYAML(&nested, params.ExtraParams); err != nil {
|
||||
return "", err
|
||||
}
|
||||
for _, line := range strings.Split(strings.TrimSuffix(nested.String(), "\n"), "\n") {
|
||||
output.WriteString(" ")
|
||||
output.WriteString(line)
|
||||
output.WriteByte('\n')
|
||||
}
|
||||
fmt.Fprintf(&output, "API key required: %t\n", report.APIKeyRequired)
|
||||
return output.String(), nil
|
||||
}
|
||||
41
integrations/ckprompt/promptkit_scope.go
Normal file
41
integrations/ckprompt/promptkit_scope.go
Normal file
@@ -0,0 +1,41 @@
|
||||
package ckprompt
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"gitea.maximumdirect.net/eric/promptkit"
|
||||
)
|
||||
|
||||
// NewPromptKitScope adapts PromptKit's captured configuration inspection to
|
||||
// CommandKit's configuration commands. The loader runs only when a selected
|
||||
// configuration operation acquires this scope. Successful construction and
|
||||
// inspection supply validation; the adapter does not execute prompts, resolve
|
||||
// credentials, or probe model availability.
|
||||
func NewPromptKitScope[C any](
|
||||
load func(context.Context, Selection, C) (*promptkit.Engine, error),
|
||||
) (ConfigScope[C], error) {
|
||||
if load == nil {
|
||||
return ConfigScope[C]{}, fmt.Errorf("PromptKit configuration loader is nil")
|
||||
}
|
||||
|
||||
return ConfigScope[C]{
|
||||
Name: "promptkit",
|
||||
Summary: "PromptKit engine configuration.",
|
||||
Operations: []ConfigOperation{ConfigValidate, ConfigShow, ConfigSources, ConfigExplain},
|
||||
Acquire: func(ctx context.Context, request ConfigRequest, appContext C) (Acquired[ConfigView], error) {
|
||||
_, inspection, diagnostics, err := loadPromptKitInspection(ctx, request.Selection, appContext, load)
|
||||
if err != nil {
|
||||
return Acquired[ConfigView]{Diagnostics: diagnostics}, err
|
||||
}
|
||||
return Acquired[ConfigView]{
|
||||
Value: ConfigView{
|
||||
Effective: inspection,
|
||||
Sources: inspection,
|
||||
Explanation: inspection,
|
||||
},
|
||||
Diagnostics: diagnostics,
|
||||
}, nil
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
220
integrations/ckprompt/promptkit_scope_test.go
Normal file
220
integrations/ckprompt/promptkit_scope_test.go
Normal file
@@ -0,0 +1,220 @@
|
||||
package ckprompt_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)
|
||||
}
|
||||
}
|
||||
450
integrations/ckprompt/promptkit_test.go
Normal file
450
integrations/ckprompt/promptkit_test.go
Normal file
@@ -0,0 +1,450 @@
|
||||
package ckprompt_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"io"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
"testing/fstest"
|
||||
|
||||
commandkit "gitea.maximumdirect.net/eric/CommandKit"
|
||||
configkit "gitea.maximumdirect.net/eric/ConfigKit"
|
||||
"gitea.maximumdirect.net/eric/promptkit"
|
||||
"go.yaml.in/yaml/v4"
|
||||
)
|
||||
|
||||
const promptKitInspectionFixture = `prompts:
|
||||
"team/summary":
|
||||
"2":
|
||||
default_profile: child
|
||||
inputs:
|
||||
- name: topic
|
||||
required: true
|
||||
content_type: text/plain
|
||||
description: "Topic to summarize"
|
||||
- name: audience
|
||||
required: false
|
||||
content_type: text/plain
|
||||
description: ""
|
||||
messages:
|
||||
- role: user
|
||||
content: "Summarize {{.topic}}"
|
||||
output:
|
||||
format: json
|
||||
validation_mode: basic
|
||||
repair_attempts: 2
|
||||
"3":
|
||||
messages:
|
||||
- role: user
|
||||
content: "Version three"
|
||||
output:
|
||||
format: text
|
||||
validation_mode: none
|
||||
empty:
|
||||
"1":
|
||||
messages:
|
||||
- role: user
|
||||
content: "No inputs"
|
||||
output:
|
||||
format: text
|
||||
validation_mode: none
|
||||
`
|
||||
|
||||
func promptKitInspectionEngine(t *testing.T) *promptkit.Engine {
|
||||
t.Helper()
|
||||
inputs, err := promptkit.FilesystemInputs("prompt fixture", fstest.MapFS{
|
||||
"config/prompts.yml": &fstest.MapFile{Data: []byte(promptKitInspectionFixture)},
|
||||
}, "config")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
engine, err := promptkit.NewEngine(promptkit.Config{Sources: promptkit.ConfigurationSources{
|
||||
Underlays: []configkit.Underlay{
|
||||
{Inputs: inputs},
|
||||
{Inputs: []configkit.Input{{Source: configkit.Mapping("profile fixture", map[string]any{
|
||||
"backends": map[string]any{"local": map[string]any{"endpoint": "https://example.invalid/v1"}},
|
||||
"profiles": map[string]any{
|
||||
"parent": map[string]any{
|
||||
"backend": "local", "model": "base-model", "temperature": 0.7,
|
||||
"max_tokens": 100, "top_p": 0.8,
|
||||
"extra_params": map[string]any{
|
||||
"token": "provider-private-sentinel",
|
||||
"nested": map[string]any{"null_value": nil, "visible": "kept"},
|
||||
"sequence": []any{"first", nil, 3},
|
||||
},
|
||||
},
|
||||
"child": map[string]any{"extends": "parent", "temperature": 0, "max_tokens": 0, "api_key_env": "PROMPTKIT_TEST_KEY"},
|
||||
"requires-key": map[string]any{"backend": "local", "model": "key-model", "api_key_required": true},
|
||||
"no-extra": map[string]any{"backend": "local", "model": "empty-params"},
|
||||
},
|
||||
})}}},
|
||||
},
|
||||
}})
|
||||
if err != nil {
|
||||
t.Fatalf("construct PromptKit fixture engine: %v", err)
|
||||
}
|
||||
return engine
|
||||
}
|
||||
|
||||
func TestPromptKitInspectionCommandsReportRealCapturedDefinitions(t *testing.T) {
|
||||
t.Setenv("PROMPTKIT_TEST_KEY", "credential-private-sentinel")
|
||||
engine := promptKitInspectionEngine(t)
|
||||
loadCalls := 0
|
||||
suite, err := commandkit.NewPromptKitSuite(func(context.Context, commandkit.Selection, struct{}) (*promptkit.Engine, error) {
|
||||
loadCalls++
|
||||
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.RegisterPromptKit(app, suite); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
var promptYAML bytes.Buffer
|
||||
outcome := app.Execute(context.Background(), []string{"promptkit", "show", "prompt", "team/summary", "--version", "2", "--format", "yaml"}, commandkit.Streams{Out: &promptYAML, Err: io.Discard})
|
||||
if outcome.Kind != commandkit.OutcomeSuccess || loadCalls != 1 {
|
||||
t.Fatalf("prompt inspection outcome=%#v calls=%d report=%q", outcome, loadCalls, promptYAML.String())
|
||||
}
|
||||
var promptReport map[string]any
|
||||
if err := yaml.Unmarshal(promptYAML.Bytes(), &promptReport); err != nil {
|
||||
t.Fatalf("decode prompt YAML: %v", err)
|
||||
}
|
||||
if !reflect.DeepEqual(sortedKeys(promptReport), []string{"default_profile_id", "inputs", "output_contract", "prompt_hash", "prompt_id", "prompt_version"}) {
|
||||
t.Fatalf("prompt report keys = %v", sortedKeys(promptReport))
|
||||
}
|
||||
if promptReport["prompt_id"] != "team/summary" || promptReport["prompt_version"] != "2" || promptReport["default_profile_id"] != "child" || promptReport["prompt_hash"] == "" {
|
||||
t.Fatalf("prompt report identity = %#v", promptReport)
|
||||
}
|
||||
inputs := promptReport["inputs"].([]any)
|
||||
if len(inputs) != 2 || inputs[0].(map[string]any)["name"] != "topic" || inputs[1].(map[string]any)["name"] != "audience" {
|
||||
t.Fatalf("prompt inputs/order = %#v", inputs)
|
||||
}
|
||||
outputContract := promptReport["output_contract"].(map[string]any)
|
||||
if outputContract["format"] != "json" || outputContract["validation_mode"] != "basic" || outputContract["repair_attempts"] != 2 {
|
||||
t.Fatalf("output contract = %#v", outputContract)
|
||||
}
|
||||
|
||||
var promptText bytes.Buffer
|
||||
outcome = app.Execute(context.Background(), []string{"promptkit", "show", "prompt", "team/summary", "--version", "2"}, commandkit.Streams{Out: &promptText, Err: io.Discard})
|
||||
if outcome.Kind != commandkit.OutcomeSuccess || !strings.Contains(promptText.String(), `Prompt hash: "`) || !strings.Contains(promptText.String(), `- name: "topic"`) || !strings.Contains(promptText.String(), `validation_mode: "basic"`) {
|
||||
t.Fatalf("prompt text = (%#v, %q)", outcome, promptText.String())
|
||||
}
|
||||
var omittedVersion bytes.Buffer
|
||||
outcome = app.Execute(context.Background(), []string{"promptkit", "show", "prompt", "empty", "--format", "yaml"}, commandkit.Streams{Out: &omittedVersion, Err: io.Discard})
|
||||
if outcome.Kind != commandkit.OutcomeSuccess || !strings.Contains(omittedVersion.String(), "prompt_version: \"1\"") {
|
||||
t.Fatalf("omitted singleton prompt version = (%#v, %q)", outcome, omittedVersion.String())
|
||||
}
|
||||
|
||||
var profileYAML bytes.Buffer
|
||||
outcome = app.Execute(context.Background(), []string{"promptkit", "show", "profile", "child", "--format", "yaml"}, commandkit.Streams{Out: &profileYAML, Err: io.Discard})
|
||||
if outcome.Kind != commandkit.OutcomeSuccess {
|
||||
t.Fatalf("profile inspection outcome=%#v report=%q", outcome, profileYAML.String())
|
||||
}
|
||||
var profileReport map[string]any
|
||||
if err := yaml.Unmarshal(profileYAML.Bytes(), &profileReport); err != nil {
|
||||
t.Fatalf("decode profile YAML: %v", err)
|
||||
}
|
||||
if !reflect.DeepEqual(sortedKeys(profileReport), []string{"api_key_required", "effective_model_params", "profile_id"}) || profileReport["profile_id"] != "child" || profileReport["api_key_required"] != false {
|
||||
t.Fatalf("profile report = %#v", profileReport)
|
||||
}
|
||||
params := profileReport["effective_model_params"].(map[string]any)
|
||||
if !reflect.DeepEqual(sortedKeys(params), []string{"api_key_env", "backend_id", "endpoint", "extra_params", "max_tokens", "model", "reasoning_effort", "service_tier", "temperature", "timeout_seconds", "top_p"}) {
|
||||
t.Fatalf("model parameter keys = %v", sortedKeys(params))
|
||||
}
|
||||
if params["temperature"] != 0 || params["max_tokens"] != 0 || params["api_key_env"] != "PROMPTKIT_TEST_KEY" {
|
||||
t.Fatalf("effective owning values = %#v", params)
|
||||
}
|
||||
extra := params["extra_params"].(map[string]any)
|
||||
nested := extra["nested"].(map[string]any)
|
||||
sequence := extra["sequence"].([]any)
|
||||
if nested["null_value"] != nil || nested["visible"] != "kept" || len(sequence) != 3 || sequence[1] != nil || sequence[2] != 3 {
|
||||
t.Fatalf("nested provider parameters changed: %#v", extra)
|
||||
}
|
||||
if extra["token"] != "provider-private-sentinel" || strings.Contains(profileYAML.String(), "credential-private-sentinel") {
|
||||
t.Fatalf("provider parameters or credential reference changed: %q", profileYAML.String())
|
||||
}
|
||||
|
||||
var profileText bytes.Buffer
|
||||
outcome = app.Execute(context.Background(), []string{"promptkit", "show", "profile", "child"}, commandkit.Streams{Out: &profileText, Err: io.Discard})
|
||||
if outcome.Kind != commandkit.OutcomeSuccess || !strings.Contains(profileText.String(), "temperature: 0\n") || !strings.Contains(profileText.String(), "api_key_env: \"PROMPTKIT_TEST_KEY\"") || !strings.Contains(profileText.String(), "null_value: null") {
|
||||
t.Fatalf("profile text = (%#v, %q)", outcome, profileText.String())
|
||||
}
|
||||
|
||||
var requiredKey bytes.Buffer
|
||||
outcome = app.Execute(context.Background(), []string{"promptkit", "show", "profile", "requires-key", "--format", "yaml"}, commandkit.Streams{Out: &requiredKey, Err: io.Discard})
|
||||
var requiredReport map[string]any
|
||||
if outcome.Kind != commandkit.OutcomeSuccess || yaml.Unmarshal(requiredKey.Bytes(), &requiredReport) != nil || requiredReport["api_key_required"] != true {
|
||||
t.Fatalf("credential requirement report = (%#v, %q)", outcome, requiredKey.String())
|
||||
}
|
||||
|
||||
var emptyPrompt bytes.Buffer
|
||||
outcome = app.Execute(context.Background(), []string{"promptkit", "show", "prompt", "empty", "--version", "1", "--format", "yaml"}, commandkit.Streams{Out: &emptyPrompt, Err: io.Discard})
|
||||
if outcome.Kind != commandkit.OutcomeSuccess || !strings.Contains(emptyPrompt.String(), "inputs: []") {
|
||||
t.Fatalf("empty inputs report = (%#v, %q)", outcome, emptyPrompt.String())
|
||||
}
|
||||
var zeroExtra bytes.Buffer
|
||||
outcome = app.Execute(context.Background(), []string{"promptkit", "show", "profile", "no-extra", "--format", "yaml"}, commandkit.Streams{Out: &zeroExtra, Err: io.Discard})
|
||||
if outcome.Kind != commandkit.OutcomeSuccess || !strings.Contains(zeroExtra.String(), "extra_params: {}") {
|
||||
t.Fatalf("nil extra params report = (%#v, %q)", outcome, zeroExtra.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestPromptKitInspectionSelectionErrorsHelpAndOptionalCapabilities(t *testing.T) {
|
||||
engine := promptKitInspectionEngine(t)
|
||||
loaderCalls := 0
|
||||
suite, err := commandkit.NewPromptKitSuite(func(context.Context, commandkit.Selection, struct{}) (*promptkit.Engine, error) {
|
||||
loaderCalls++
|
||||
return engine, nil
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
app, _ := commandkit.New(commandkit.Identity{Name: "example"})
|
||||
if err := commandkit.RegisterPromptKit(app, suite); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
badTarget := suite
|
||||
badTarget.Selectors = []commandkit.Selector[struct{}]{{
|
||||
Binding: commandkit.ValueFlag[struct{}](commandkit.FlagMeta{Name: "campaign"}, nil, func(value string) (string, error) { return value, nil }, func(*struct{}, commandkit.Supplied[string]) {}),
|
||||
Scopes: []string{"app"},
|
||||
}}
|
||||
invalidApp, _ := commandkit.New(commandkit.Identity{Name: "invalid"})
|
||||
if err := commandkit.RegisterPromptKit(invalidApp, badTarget); err == nil {
|
||||
t.Fatal("selector for another scope was accepted")
|
||||
}
|
||||
for _, tc := range []struct {
|
||||
args []string
|
||||
want error
|
||||
}{
|
||||
{args: []string{"promptkit", "show", "prompt", "team/summary"}, want: promptkit.ErrPromptLoad},
|
||||
{args: []string{"promptkit", "show", "prompt", "missing", "--version", "1"}, want: promptkit.ErrPromptNotFound},
|
||||
{args: []string{"promptkit", "show", "profile", "missing"}, want: promptkit.ErrProfileNotFound},
|
||||
{args: []string{"promptkit", "show", "prompt", "", "--version", "2"}, want: promptkit.ErrInvalidRequest},
|
||||
} {
|
||||
outcome := app.Execute(context.Background(), tc.args, commandkit.Streams{Out: io.Discard, Err: io.Discard})
|
||||
if outcome.Kind != commandkit.OutcomeOperational || !errors.Is(outcome.Cause, tc.want) {
|
||||
t.Errorf("execute %q = (%#v), want operational error matching %v", tc.args, outcome, tc.want)
|
||||
}
|
||||
}
|
||||
cancelled, cancel := context.WithCancel(context.Background())
|
||||
cancel()
|
||||
outcome := app.Execute(cancelled, []string{"promptkit", "show", "prompt", "empty", "--version", "1"}, commandkit.Streams{Out: io.Discard, Err: io.Discard})
|
||||
if outcome.Kind != commandkit.OutcomeOperational || !errors.Is(outcome.Cause, context.Canceled) {
|
||||
t.Fatalf("cancelled prompt inspection = %#v", outcome)
|
||||
}
|
||||
beforeHelp := loaderCalls
|
||||
outcome = app.Execute(context.Background(), []string{"promptkit", "show", "prompt", "--help"}, commandkit.Streams{Out: io.Discard, Err: io.Discard})
|
||||
if outcome.Kind != commandkit.OutcomeSuccess || loaderCalls != beforeHelp {
|
||||
t.Fatalf("help acquired engine: outcome=%#v calls=%d", outcome, loaderCalls)
|
||||
}
|
||||
beforeMissing := loaderCalls
|
||||
outcome = app.Execute(context.Background(), []string{"promptkit", "show", "prompt"}, commandkit.Streams{Out: io.Discard, Err: io.Discard})
|
||||
if outcome.Kind != commandkit.OutcomeUsage || loaderCalls != beforeMissing {
|
||||
t.Fatalf("missing ID = (%#v, calls=%d), want usage before acquisition", outcome, loaderCalls)
|
||||
}
|
||||
beforeScopeFlag := loaderCalls
|
||||
outcome = app.Execute(context.Background(), []string{"promptkit", "show", "profile", "child", "--scope", "promptkit"}, commandkit.Streams{Out: io.Discard, Err: io.Discard})
|
||||
if outcome.Kind != commandkit.OutcomeUsage || loaderCalls != beforeScopeFlag {
|
||||
t.Fatalf("definition command accepted --scope: outcome=%#v calls=%d", outcome, loaderCalls)
|
||||
}
|
||||
|
||||
profileOnlyApp, _ := commandkit.New(commandkit.Identity{Name: "profile-only"})
|
||||
if err := commandkit.RegisterPromptKit(profileOnlyApp, commandkit.PromptKitSuite[struct{}]{InspectProfile: func(ctx context.Context, _ commandkit.Selection, _ struct{}, id string) (commandkit.Acquired[*promptkit.ProfileInspection], error) {
|
||||
value, err := engine.InspectProfile(ctx, id)
|
||||
return commandkit.Acquired[*promptkit.ProfileInspection]{Value: value}, err
|
||||
}}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
outcome = profileOnlyApp.Execute(context.Background(), []string{"promptkit", "show", "profile", "child"}, commandkit.Streams{Out: io.Discard, Err: io.Discard})
|
||||
if outcome.Kind != commandkit.OutcomeSuccess {
|
||||
t.Fatalf("profile-only capability was not installed: %#v", outcome)
|
||||
}
|
||||
outcome = profileOnlyApp.Execute(context.Background(), []string{"promptkit", "show", "prompt", "empty"}, commandkit.Streams{Out: io.Discard, Err: io.Discard})
|
||||
if outcome.Kind != commandkit.OutcomeUsage {
|
||||
t.Fatalf("profile-only suite installed prompt inspection: %#v", outcome)
|
||||
}
|
||||
promptOnlyApp, _ := commandkit.New(commandkit.Identity{Name: "prompt-only"})
|
||||
if err := commandkit.RegisterPromptKit(promptOnlyApp, commandkit.PromptKitSuite[struct{}]{InspectPrompt: func(ctx context.Context, _ commandkit.Selection, _ struct{}, id, version string) (commandkit.Acquired[*promptkit.PromptInspection], error) {
|
||||
value, err := engine.InspectPrompt(ctx, id, version)
|
||||
return commandkit.Acquired[*promptkit.PromptInspection]{Value: value}, err
|
||||
}}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
outcome = promptOnlyApp.Execute(context.Background(), []string{"promptkit", "show", "prompt", "empty"}, commandkit.Streams{Out: io.Discard, Err: io.Discard})
|
||||
if outcome.Kind != commandkit.OutcomeSuccess {
|
||||
t.Fatalf("prompt-only capability was not installed: %#v", outcome)
|
||||
}
|
||||
outcome = promptOnlyApp.Execute(context.Background(), []string{"promptkit", "show", "profile", "child"}, commandkit.Streams{Out: io.Discard, Err: io.Discard})
|
||||
if outcome.Kind != commandkit.OutcomeUsage {
|
||||
t.Fatalf("prompt-only suite installed profile inspection: %#v", outcome)
|
||||
}
|
||||
|
||||
emptyApp, _ := commandkit.New(commandkit.Identity{Name: "empty"})
|
||||
if err := commandkit.RegisterPromptKit(emptyApp, commandkit.PromptKitSuite[struct{}]{}); err != nil {
|
||||
t.Fatalf("empty PromptKit suite = %v", err)
|
||||
}
|
||||
outcome = emptyApp.Execute(context.Background(), []string{"help"}, commandkit.Streams{Out: io.Discard, Err: io.Discard})
|
||||
if outcome.Kind != commandkit.OutcomeSuccess {
|
||||
t.Fatalf("empty PromptKit suite changed command family: %#v", outcome)
|
||||
}
|
||||
outcome = emptyApp.Execute(context.Background(), []string{"help", "promptkit"}, commandkit.Streams{Out: io.Discard, Err: io.Discard})
|
||||
if outcome.Kind != commandkit.OutcomeUsage {
|
||||
t.Fatalf("empty PromptKit suite installed its namespace: %#v", outcome)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPromptKitSuitePassesSelectionAndProjectedContext(t *testing.T) {
|
||||
type appContext struct {
|
||||
Campaign commandkit.Supplied[string]
|
||||
Runtime string
|
||||
}
|
||||
type contextKey struct{}
|
||||
engine := promptKitInspectionEngine(t)
|
||||
ctx := context.WithValue(context.Background(), contextKey{}, "caller")
|
||||
wantSelection := commandkit.Selection{
|
||||
Config: commandkit.Supplied[string]{Value: "main.yml", Set: true},
|
||||
Overlay: commandkit.Supplied[string]{Value: "dev", Set: true},
|
||||
}
|
||||
loaderCalls, contexts, checks := 0, 0, 0
|
||||
var gotSelection commandkit.Selection
|
||||
var gotContext appContext
|
||||
suite, err := commandkit.NewPromptKitSuite(func(gotCtx context.Context, selection commandkit.Selection, input appContext) (*promptkit.Engine, error) {
|
||||
loaderCalls++
|
||||
gotSelection, gotContext = selection, input
|
||||
if gotCtx != ctx || gotCtx.Value(contextKey{}) != "caller" {
|
||||
t.Fatal("engine loader did not receive invocation context")
|
||||
}
|
||||
return engine, nil
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
suite.NewContext = func() appContext { contexts++; return appContext{Runtime: "invocation cache"} }
|
||||
suite.Selectors = []commandkit.Selector[appContext]{{
|
||||
Binding: commandkit.ValueFlag[appContext](commandkit.FlagMeta{Name: "campaign"}, nil, func(value string) (string, error) { return value, nil }, func(input *appContext, value commandkit.Supplied[string]) { input.Campaign = value }),
|
||||
Scopes: []string{"promptkit"},
|
||||
}}
|
||||
suite.CheckInput = func(selection commandkit.Selection, input appContext) error {
|
||||
checks++
|
||||
if selection != wantSelection || input.Campaign != (commandkit.Supplied[string]{Value: "spring", Set: true}) {
|
||||
t.Errorf("preflight inputs = (%+v, %+v)", selection, input)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
app, _ := commandkit.New(commandkit.Identity{Name: "example"})
|
||||
if err := commandkit.RegisterPromptKit(app, suite); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
outcome := app.Execute(ctx, []string{"promptkit", "show", "profile", "child", "--config", "main.yml", "--overlay", "dev", "--campaign", "spring"}, commandkit.Streams{Out: io.Discard, Err: io.Discard})
|
||||
if outcome.Kind != commandkit.OutcomeSuccess || loaderCalls != 1 || contexts != 1 || checks != 1 {
|
||||
t.Fatalf("inspection = (%#v), loader/context/check calls = %d/%d/%d", outcome, loaderCalls, contexts, checks)
|
||||
}
|
||||
if gotSelection != wantSelection || gotContext.Campaign != (commandkit.Supplied[string]{Value: "spring", Set: true}) || gotContext.Runtime != "invocation cache" {
|
||||
t.Fatalf("loader selection/context = (%+v, %+v)", gotSelection, gotContext)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPromptKitDirectSuiteRetainsDiagnosticsOnNamedLookupFailure(t *testing.T) {
|
||||
warning := commandkit.Diagnostic{Severity: configkit.SeverityWarning, Write: func(w io.Writer) error { _, err := io.WriteString(w, "configuration warning\n"); return err }}
|
||||
app, _ := commandkit.New(commandkit.Identity{Name: "example"})
|
||||
err := commandkit.RegisterPromptKit(app, commandkit.PromptKitSuite[struct{}]{InspectPrompt: func(context.Context, commandkit.Selection, struct{}, string, string) (commandkit.Acquired[*promptkit.PromptInspection], error) {
|
||||
return commandkit.Acquired[*promptkit.PromptInspection]{Diagnostics: []commandkit.Diagnostic{warning}}, promptkit.ErrPromptNotFound
|
||||
}})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
outcome := app.Execute(context.Background(), []string{"promptkit", "show", "prompt", "missing"}, commandkit.Streams{Out: io.Discard, Err: io.Discard})
|
||||
if outcome.Kind != commandkit.OutcomeOperational || !errors.Is(outcome.Cause, promptkit.ErrPromptNotFound) || len(outcome.Diagnostics) < 1 || outcome.Diagnostics[0].Severity != configkit.SeverityWarning {
|
||||
t.Fatalf("named lookup outcome did not retain acquisition diagnostic: %#v", outcome)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPromptKitInspectionReportsOutputDeliveryFailure(t *testing.T) {
|
||||
engine := promptKitInspectionEngine(t)
|
||||
suite, err := commandkit.NewPromptKitSuite(func(context.Context, commandkit.Selection, struct{}) (*promptkit.Engine, error) {
|
||||
return engine, nil
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
app, _ := commandkit.New(commandkit.Identity{Name: "example"})
|
||||
if err := commandkit.RegisterPromptKit(app, suite); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
writeErr := errors.New("report output failed")
|
||||
outcome := app.Execute(context.Background(), []string{"promptkit", "show", "profile", "child", "--format", "yaml"}, commandkit.Streams{Out: promptKitFailingWriter{err: writeErr}, Err: io.Discard})
|
||||
if outcome.Kind != commandkit.OutcomeOperational || !outcome.ReportIncomplete || !errors.Is(outcome.DeliveryError, writeErr) {
|
||||
t.Fatalf("promptkit output failure = %#v", outcome)
|
||||
}
|
||||
}
|
||||
|
||||
type promptKitFailingWriter struct{ err error }
|
||||
|
||||
func (writer promptKitFailingWriter) Write([]byte) (int, error) { return 0, writer.err }
|
||||
|
||||
func sortedKeys(values map[string]any) []string {
|
||||
keys := make([]string, 0, len(values))
|
||||
for key := range values {
|
||||
keys = append(keys, key)
|
||||
}
|
||||
for i := 0; i < len(keys); i++ {
|
||||
for j := i + 1; j < len(keys); j++ {
|
||||
if keys[j] < keys[i] {
|
||||
keys[i], keys[j] = keys[j], keys[i]
|
||||
}
|
||||
}
|
||||
}
|
||||
return keys
|
||||
}
|
||||
|
||||
func TestPromptKitSelectorNamesCollideOnlyWithInstalledFlags(t *testing.T) {
|
||||
for _, name := range []string{"version", "scope"} {
|
||||
var captured commandkit.Supplied[string]
|
||||
suite := commandkit.PromptKitSuite[commandkit.Supplied[string]]{
|
||||
Selectors: []commandkit.Selector[commandkit.Supplied[string]]{{Binding: commandkit.ValueFlag[commandkit.Supplied[string]](commandkit.FlagMeta{Name: name}, nil, func(s string) (string, error) { return s, nil }, func(c *commandkit.Supplied[string], v commandkit.Supplied[string]) { *c = v })}},
|
||||
InspectProfile: func(_ context.Context, _ commandkit.Selection, c commandkit.Supplied[string], _ string) (commandkit.Acquired[*promptkit.ProfileInspection], error) {
|
||||
captured = c
|
||||
return commandkit.Acquired[*promptkit.ProfileInspection]{Value: &promptkit.ProfileInspection{}}, nil
|
||||
},
|
||||
}
|
||||
app, _ := commandkit.New(commandkit.Identity{Name: "example"})
|
||||
if err := commandkit.RegisterPromptKit(app, suite); err != nil {
|
||||
t.Fatalf("%s: %v", name, err)
|
||||
}
|
||||
result := app.Execute(context.Background(), []string{"promptkit", "show", "profile", "example", "--" + name, "selected"}, commandkit.Streams{})
|
||||
if result.Kind != commandkit.OutcomeSuccess || captured != (commandkit.Supplied[string]{Value: "selected", Set: true}) {
|
||||
t.Fatalf("%s: result=%#v captured=%#v", name, result, captured)
|
||||
}
|
||||
suite.InspectPrompt = func(context.Context, commandkit.Selection, commandkit.Supplied[string], string, string) (commandkit.Acquired[*promptkit.PromptInspection], error) {
|
||||
return commandkit.Acquired[*promptkit.PromptInspection]{Value: &promptkit.PromptInspection{}}, nil
|
||||
}
|
||||
other, _ := commandkit.New(commandkit.Identity{Name: "example"})
|
||||
err := commandkit.RegisterPromptKit(other, suite)
|
||||
if name == "version" && err == nil {
|
||||
t.Fatal("accepted collision with installed prompt version flag")
|
||||
}
|
||||
if name == "scope" && err != nil {
|
||||
t.Fatalf("scope selector conflicts with no installed flag: %v", err)
|
||||
}
|
||||
}
|
||||
for _, name := range []string{"config", "overlay", "format", "help"} {
|
||||
app, _ := commandkit.New(commandkit.Identity{Name: "example"})
|
||||
suite := commandkit.PromptKitSuite[struct{}]{
|
||||
InspectProfile: func(context.Context, commandkit.Selection, struct{}, string) (commandkit.Acquired[*promptkit.ProfileInspection], error) {
|
||||
return commandkit.Acquired[*promptkit.ProfileInspection]{}, nil
|
||||
},
|
||||
Selectors: []commandkit.Selector[struct{}]{{Binding: commandkit.ValueFlag[struct{}](commandkit.FlagMeta{Name: name}, nil, func(s string) (string, error) { return s, nil }, func(*struct{}, commandkit.Supplied[string]) {})}},
|
||||
}
|
||||
if err := commandkit.RegisterPromptKit(app, suite); err == nil {
|
||||
t.Fatalf("accepted standard flag collision --%s", name)
|
||||
}
|
||||
}
|
||||
}
|
||||
85
integrations/ckprompt/registration_test.go
Normal file
85
integrations/ckprompt/registration_test.go
Normal file
@@ -0,0 +1,85 @@
|
||||
package ckprompt_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
commandkit "gitea.maximumdirect.net/eric/CommandKit"
|
||||
"gitea.maximumdirect.net/eric/promptkit"
|
||||
"io"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestBridgeTreeRefreshesAfterSuccessfulRegistration(t *testing.T) {
|
||||
app, err := commandkit.New(commandkit.Identity{Name: "example"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var output bytes.Buffer
|
||||
streams := commandkit.Streams{Out: &output, Err: io.Discard}
|
||||
renderRootHelp := func() string {
|
||||
t.Helper()
|
||||
output.Reset()
|
||||
outcome := app.Execute(context.Background(), []string{"--help"}, streams)
|
||||
if outcome.Kind != commandkit.OutcomeSuccess {
|
||||
t.Fatalf("root help outcome = %#v", outcome)
|
||||
}
|
||||
return output.String()
|
||||
}
|
||||
initial := renderRootHelp()
|
||||
if strings.Contains(initial, "custom") || strings.Contains(initial, "config") || strings.Contains(initial, "promptkit") {
|
||||
t.Fatalf("initial help unexpectedly lists future registrations: %q", initial)
|
||||
}
|
||||
|
||||
custom, err := NewCommand(CommandSpec[struct{}]{Name: "custom", Run: func(Invocation, struct{}) Outcome { return commandkit.Success() }})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := app.Register(custom); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
providerCalls := 0
|
||||
if err := commandkit.RegisterConfig(app, commandkit.ConfigSuite[struct{}]{Scopes: []commandkit.ConfigScope[struct{}]{{
|
||||
Name: "app",
|
||||
Operations: []ConfigOperation{commandkit.ConfigShow},
|
||||
Acquire: func(context.Context, commandkit.ConfigRequest, struct{}) (commandkit.Acquired[commandkit.ConfigView], error) {
|
||||
providerCalls++
|
||||
return commandkit.Acquired[commandkit.ConfigView]{}, nil
|
||||
},
|
||||
}}}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := commandkit.RegisterPromptKit(app, commandkit.PromptKitSuite[struct{}]{InspectPrompt: func(context.Context, commandkit.Selection, struct{}, string, string) (commandkit.Acquired[*promptkit.PromptInspection], error) {
|
||||
providerCalls++
|
||||
return commandkit.Acquired[*promptkit.PromptInspection]{}, nil
|
||||
}}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
updated := renderRootHelp()
|
||||
for _, name := range []string{"custom", "config", "promptkit"} {
|
||||
if !strings.Contains(updated, name) {
|
||||
t.Errorf("updated help omits %q: %q", name, updated)
|
||||
}
|
||||
}
|
||||
if providerCalls != 0 {
|
||||
t.Fatalf("help invoked inspection or configuration callbacks %d times", providerCalls)
|
||||
}
|
||||
|
||||
late, err := NewCommand(CommandSpec[struct{}]{Name: "late"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := app.Register(late, custom); err == nil {
|
||||
t.Fatal("Register accepted a batch with an existing-name collision")
|
||||
}
|
||||
if err := app.Register(); err != nil {
|
||||
t.Fatalf("empty registration failed: %v", err)
|
||||
}
|
||||
final := renderRootHelp()
|
||||
if !strings.Contains(final, "custom") || !strings.Contains(final, "config") || !strings.Contains(final, "promptkit") || strings.Contains(final, "late") {
|
||||
t.Fatalf("help after failed and empty registration = %q", final)
|
||||
}
|
||||
if outcome := app.Execute(context.Background(), []string{"custom"}, streams); outcome.Kind != commandkit.OutcomeSuccess {
|
||||
t.Fatalf("new command did not parse after registration: %#v", outcome)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user