diff --git a/docs/roadmap/implementation.md b/docs/roadmap/implementation.md index 8333c42..ab3f5e5 100644 --- a/docs/roadmap/implementation.md +++ b/docs/roadmap/implementation.md @@ -1,6 +1,6 @@ # Implement the optional integration modules and captured registration API -Status: implementation in progress. Stages 1 through 12 are complete; subsequent work follows the ordered roadmap. +Status: implementation in progress. Stages 1 through 13 are complete; subsequent work follows the ordered roadmap. Execute the stages in order. Each stage is sized as a separate Luna coding-agent assignment. This document authorizes no publication or changes to sibling repositories. @@ -631,10 +631,15 @@ dependency ownership. including test variants. Prepare the independent module graph checks in Stage 16; do not mistake workspace-combined graphs for isolation evidence. -**Completion evidence:** examples use only selected public contracts; core-only -and ConfigKit-only package imports exclude their forbidden libraries. The -composed consumer demonstrates both standard families and an application-owned -command. No historical dependency-version matrix is required. +**Completion evidence — complete:** all three maintained examples use the +selected public contracts, and the composed consumer covers both standard +families plus an application-owned command. Full tests pass for each module; +maintained consumer invocations pass, including help/version and the inspection, +presence, and restore paths. Dependency checks including tests show no optional +library or nested integration imports in core and no PromptKit dependency in +ckconfig. These package checks use the local workspace; independent module-graph +validation remains for Stage 16. No historical dependency-version matrix is +required. ## Stage 14: Adopt ReleaseKit v0.4.0 and selected-module publication diff --git a/examples/core/main.go b/examples/core/main.go new file mode 100644 index 0000000..7cc4a56 --- /dev/null +++ b/examples/core/main.go @@ -0,0 +1,60 @@ +package main + +import ( + "context" + "fmt" + "os" + "strconv" + + commandkit "gitea.maximumdirect.net/eric/CommandKit" +) + +type greetInput struct { + Count commandkit.Supplied[int] + Tags commandkit.Supplied[[]string] +} + +func newApp() (*commandkit.App, error) { + countDefault := 1 + tagDefaults := []string{"demo"} + greet := commandkit.NewCommand(commandkit.CommandSpec[greetInput]{ + Name: "greet", + Summary: "Show presence-aware values and repeated labels.", + Flags: []commandkit.Flag[greetInput]{ + commandkit.ValueFlag(commandkit.FlagMeta{Name: "count", Usage: "Greeting count."}, &countDefault, strconv.Atoi, func(input *greetInput, value commandkit.Supplied[int]) { + input.Count = value + }), + commandkit.RepeatFlag(commandkit.FlagMeta{Name: "tag", Usage: "A repeatable label."}, tagDefaults, func(value string) (string, error) { return value, nil }, func(input *greetInput, value commandkit.Supplied[[]string]) { + input.Tags = value + }), + }, + Run: func(invocation commandkit.Invocation, input greetInput) commandkit.Outcome { + count := fmt.Sprintf("default %d (not supplied)", input.Count.Value) + if input.Count.Set { + count = fmt.Sprintf("explicit %d", input.Count.Value) + } + tags := fmt.Sprintf("default %v (not supplied)", input.Tags.Value) + if input.Tags.Set { + tags = fmt.Sprintf("explicit %v", input.Tags.Value) + } + if _, err := fmt.Fprintf(invocation.Streams.Out, "Hello; count=%s; tags=%s\n", count, tags); err != nil { + return commandkit.Failure(err) + } + return commandkit.Success() + }, + }) + workspace := commandkit.Group("workspace", "Workspace commands.", greet) + return commandkit.New(commandkit.Identity{Name: "core-example", Version: "1.0"}, workspace) +} + +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) + } +} diff --git a/integrations/ckconfig/cancellation_test.go b/integrations/ckconfig/cancellation_test.go deleted file mode 100644 index d5ab1f8..0000000 --- a/integrations/ckconfig/cancellation_test.go +++ /dev/null @@ -1,78 +0,0 @@ -package ckconfig_test - -import ( - "bytes" - "context" - "errors" - "fmt" - commandkit "gitea.maximumdirect.net/eric/CommandKit" - configkit "gitea.maximumdirect.net/eric/ConfigKit" - "strings" - "testing" -) - -func TestStandardConfigurationCancellationBoundaries(t *testing.T) { - for _, operation := range []string{"validate", "show", "sources", "explain"} { - for _, before := range []bool{true, false} { - t.Run(fmt.Sprintf("%s/before=%t", operation, before), func(t *testing.T) { - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - if before { - cancel() - } - loads, contexts := 0, 0 - scope, err := commandkit.NewConfigKitScope[struct{}, struct{}]("app", "", func(context.Context, commandkit.Selection, struct{}) (*configkit.Result[struct{}], error) { - loads++ - cancel() - return configkit.Load(configkit.NoSource(), configkit.Options[struct{}]{}) - }) - if err != nil { - t.Fatal(err) - } - app, _ := commandkit.New(commandkit.Identity{Name: "example"}) - if err := commandkit.RegisterConfig(app, commandkit.ConfigSuite[struct{}]{Scopes: []commandkit.ConfigScope[struct{}]{scope}, NewContext: func() struct{} { contexts++; return struct{}{} }}); err != nil { - t.Fatal(err) - } - args := []string{"config", operation} - if operation == "explain" { - args = append(args, "--path", "") - } - var out bytes.Buffer - result := app.Execute(ctx, args, commandkit.Streams{Out: &out}) - want := 1 - if before { - want = 0 - } - if result.Kind != commandkit.OutcomeOperational || result.Status != 1 || !errors.Is(result.Cause, context.Canceled) || loads != want || contexts != want || out.Len() != 0 { - t.Fatalf("result=%#v loads=%d contexts=%d out=%q", result, loads, contexts, out.String()) - } - }) - } - } -} - -func TestAggregateCancellationRetainsCompletedValidationAndDiagnostics(t *testing.T) { - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - app, _ := commandkit.New(commandkit.Identity{Name: "example"}) - calls := 0 - acquire := func(context.Context, commandkit.ConfigRequest, struct{}) (commandkit.Acquired[commandkit.ConfigView], error) { - calls++ - if calls == 2 { - cancel() - } - return commandkit.Acquired[commandkit.ConfigView]{Diagnostics: []commandkit.Diagnostic{commandkit.MessageDiagnostic(configkit.SeverityWarning, "acquired warning")}}, nil - } - if err := commandkit.RegisterConfig(app, commandkit.ConfigSuite[struct{}]{Scopes: []commandkit.ConfigScope[struct{}]{ - {Name: "app", Operations: []commandkit.ConfigOperation{commandkit.ConfigValidate}, Acquire: acquire}, - {Name: "promptkit", Operations: []commandkit.ConfigOperation{commandkit.ConfigValidate}, Acquire: acquire}, - {Name: "zeta", Operations: []commandkit.ConfigOperation{commandkit.ConfigValidate}, Acquire: acquire}, - }}); err != nil { - t.Fatal(err) - } - var out, diagnostics bytes.Buffer - result := app.Execute(ctx, []string{"config", "validate", "--scope", "all"}, commandkit.Streams{Out: &out, Err: &diagnostics}) - if !errors.Is(result.Cause, context.Canceled) || !result.ReportIncomplete || calls != 2 || out.String() != "app: valid\n" || strings.Count(diagnostics.String(), "acquired warning") != 2 { - t.Fatalf("result=%#v calls=%d out=%q diagnostics=%q", result, calls, out.String(), diagnostics.String()) - } -} diff --git a/integrations/ckconfig/config_test.go b/integrations/ckconfig/config_test.go deleted file mode 100644 index 46ea78c..0000000 --- a/integrations/ckconfig/config_test.go +++ /dev/null @@ -1,359 +0,0 @@ -package ckconfig - -import ( - "bytes" - "context" - "reflect" - "strconv" - "strings" - "testing" -) - -func TestConfigScopeDiscoveryOrdersAndDeduplicatesCapabilities(t *testing.T) { - app, err := New(Identity{Name: "example"}) - if err != nil { - t.Fatal(err) - } - newContextCalls := 0 - acquireCalls := 0 - suite := ConfigSuite[struct{}]{ - NewContext: func() struct{} { newContextCalls++; return struct{}{} }, - Scopes: []ConfigScope[struct{}]{ - {Name: "zeta", Operations: []ConfigOperation{ConfigExplain, ConfigShow, ConfigExplain}, Acquire: func(context.Context, ConfigRequest, struct{}) (Acquired[ConfigView], error) { - acquireCalls++ - return Acquired[ConfigView]{}, nil - }}, - {Name: "promptkit", Operations: []ConfigOperation{ConfigSources}, Acquire: noopConfigAcquire[struct{}]}, - {Name: "app", Operations: []ConfigOperation{ConfigShow, ConfigValidate, ConfigShow}, Acquire: func(context.Context, ConfigRequest, struct{}) (Acquired[ConfigView], error) { - acquireCalls++ - return Acquired[ConfigView]{}, nil - }}, - {Name: "metadata", Summary: "No provider is required."}, - }, - } - normalized, err := normalizeConfigSuite(suite) - if err != nil { - t.Fatal(err) - } - if !reflect.DeepEqual(normalized.Scopes[0].Operations, []ConfigOperation{ConfigShow, ConfigExplain}) || !reflect.DeepEqual(normalized.Scopes[2].Operations, []ConfigOperation{ConfigValidate, ConfigShow}) { - t.Fatalf("normalized capabilities = %#v", normalized.Scopes) - } - if err := RegisterConfig(app, suite); err != nil { - t.Fatal(err) - } - - var textOutput bytes.Buffer - outcome := app.Execute(context.Background(), []string{"config", "scopes"}, Streams{Out: &textOutput}) - if outcome.Kind != OutcomeSuccess { - t.Fatalf("text inventory outcome = %#v", outcome) - } - wantText := "app: validate, show\npromptkit: sources\nmetadata:\nzeta: show, explain\n" - if textOutput.String() != wantText { - t.Fatalf("text inventory = %q, want %q", textOutput.String(), wantText) - } - - var yamlOutput bytes.Buffer - outcome = app.Execute(context.Background(), []string{"config", "scopes", "--format", "yaml"}, Streams{Out: &yamlOutput}) - if outcome.Kind != OutcomeSuccess { - t.Fatalf("YAML inventory outcome = %#v", outcome) - } - if got := yamlOutput.String(); !strings.Contains(got, "scopes:") || strings.Index(got, "name: app") > strings.Index(got, "name: promptkit") || !strings.Contains(got, "operations: []") { - t.Fatalf("YAML inventory = %q", got) - } - outcome = app.Execute(context.Background(), []string{"config", "scopes", "--help"}, Streams{}) - if outcome.Kind != OutcomeSuccess { - t.Fatalf("scope help outcome = %#v", outcome) - } - outcome = app.Execute(context.Background(), []string{"config", "scopes", "--format", "json"}, Streams{}) - if outcome.Kind != OutcomeUsage { - t.Fatalf("invalid format outcome = %#v", outcome) - } - if newContextCalls != 0 || acquireCalls != 0 { - t.Fatalf("discovery called runtime callbacks: NewContext=%d Acquire=%d", newContextCalls, acquireCalls) - } -} - -func TestConfigInputDefaultsToAppAndRetainsAbsentSelections(t *testing.T) { - suite := ConfigSuite[struct{}]{Scopes: []ConfigScope[struct{}]{{Name: "app"}}} - prepared, err := prepareInput(CommandSpec[configInput[struct{}]]{Flags: configInputFlags(suite, "yaml")}, make([][]string, 4), nil, true) - if err != nil { - t.Fatal(err) - } - input := prepared.build() - if input.Scope != "app" || input.Format != "yaml" || input.Selection.Config.Set || input.Selection.Overlay.Set { - t.Fatalf("configuration defaults = %#v", input) - } -} - -func TestRegisterConfigOwnsPreparedScopeAndSelectorMetadata(t *testing.T) { - type selectorInput struct{ Region Supplied[string] } - var acquired selectorInput - suite := ConfigSuite[selectorInput]{ - Selectors: []Selector[selectorInput]{{ - Binding: ValueFlag[selectorInput](FlagMeta{Name: "region"}, nil, parseString, func(input *selectorInput, value Supplied[string]) { input.Region = value }), - Scopes: []string{"app"}, - }}, - Scopes: []ConfigScope[selectorInput]{{ - Name: "app", - Operations: []ConfigOperation{ConfigValidate}, - Acquire: func(_ context.Context, _ ConfigRequest, input selectorInput) (Acquired[ConfigView], error) { - acquired = input - return Acquired[ConfigView]{}, nil - }, - }}, - } - app, err := New(Identity{Name: "example"}) - if err != nil { - t.Fatal(err) - } - if err := RegisterConfig(app, suite); err != nil { - t.Fatal(err) - } - - suite.Scopes[0].Name = "renamed" - suite.Scopes[0].Operations[0] = ConfigShow - suite.Selectors[0].Scopes[0] = "renamed" - - var inventory bytes.Buffer - if result := app.Execute(context.Background(), []string{"config", "scopes"}, Streams{Out: &inventory}); result.Kind != OutcomeSuccess { - t.Fatalf("scope inventory outcome = %#v", result) - } - if got := inventory.String(); got != "app: validate\n" { - t.Fatalf("scope inventory after caller mutation = %q", got) - } - if result := app.Execute(context.Background(), []string{"config", "validate", "--scope=app", "--region=west"}, Streams{}); result.Kind != OutcomeSuccess { - t.Fatalf("registered scope execution after caller mutation = %#v", result) - } - if acquired.Region != (Supplied[string]{Value: "west", Set: true}) { - t.Fatalf("acquired selector after caller mutation = %#v", acquired) - } -} - -func TestRegisterConfigRejectsDuplicateSelectorNamesAndShorts(t *testing.T) { - type selectorInput struct{ First, Second string } - tests := []struct { - name string - firstMeta FlagMeta - secondMeta FlagMeta - }{ - {name: "name", firstMeta: FlagMeta{Name: "region", Short: "r"}, secondMeta: FlagMeta{Name: "region", Short: "g"}}, - {name: "short", firstMeta: FlagMeta{Name: "region", Short: "r"}, secondMeta: FlagMeta{Name: "group", Short: "r"}}, - } - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - app, err := New(Identity{Name: "example"}) - if err != nil { - t.Fatal(err) - } - suite := ConfigSuite[selectorInput]{Selectors: []Selector[selectorInput]{ - {Binding: ValueFlag[selectorInput](test.firstMeta, nil, parseString, func(input *selectorInput, value Supplied[string]) { input.First = value.Value })}, - {Binding: ValueFlag[selectorInput](test.secondMeta, nil, parseString, func(input *selectorInput, value Supplied[string]) { input.Second = value.Value })}, - }} - if err := RegisterConfig(app, suite); err == nil || !strings.Contains(err.Error(), "duplicate configuration selector flag") { - t.Fatalf("RegisterConfig error = %v", err) - } - }) - } -} - -func TestRegisterConfigRejectsInvalidSuitesAndCollisions(t *testing.T) { - tests := []struct { - name string - scope ConfigScope[struct{}] - want string - }{ - {name: "empty name", scope: ConfigScope[struct{}]{}, want: "name must not be empty"}, - {name: "all name", scope: ConfigScope[struct{}]{Name: "all"}, want: "reserved"}, - {name: "unknown operation", scope: ConfigScope[struct{}]{Name: "app", Operations: []ConfigOperation{"diff"}}, want: "unknown operation"}, - {name: "missing acquisition", scope: ConfigScope[struct{}]{Name: "app", Operations: []ConfigOperation{ConfigValidate}}, want: "without an acquisition"}, - } - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - app, _ := New(Identity{Name: "example"}) - err := RegisterConfig(app, ConfigSuite[struct{}]{Scopes: []ConfigScope[struct{}]{test.scope}}) - if err == nil || !strings.Contains(err.Error(), test.want) { - t.Fatalf("RegisterConfig error = %v, want %q", err, test.want) - } - }) - } - - app, _ := New(Identity{Name: "example"}) - custom, err := NewCommand(CommandSpec[struct{}]{Name: "config", Run: func(Invocation, struct{}) Outcome { return Success() }}) - if err != nil { - t.Fatal(err) - } - if err := app.Register(custom); err != nil { - t.Fatal(err) - } - if err := RegisterConfig(app, ConfigSuite[struct{}]{}); err == nil { - t.Fatal("replaced a pre-existing config command") - } -} - -func TestConfigSelectorPreflightAndScopedContextProjection(t *testing.T) { - type selectorInput struct { - Count Supplied[int] - Enabled Supplied[bool] - Other Supplied[int] - Runtime string - } - baseContextCalls := 0 - checkCalls := 0 - var checked selectorInput - countDefault := 9 - suite := ConfigSuite[selectorInput]{ - NewContext: func() selectorInput { baseContextCalls++; return selectorInput{Runtime: "invocation cache"} }, - Selectors: []Selector[selectorInput]{ - {Binding: ValueFlag[selectorInput](FlagMeta{Name: "count"}, &countDefault, strconv.Atoi, func(input *selectorInput, value Supplied[int]) { input.Count = value }), Scopes: []string{"app"}}, - {Binding: BoolFlag[selectorInput](FlagMeta{Name: "enabled"}, true, func(input *selectorInput, value Supplied[bool]) { input.Enabled = value }), Scopes: []string{"promptkit"}}, - {Binding: ValueFlag[selectorInput](FlagMeta{Name: "other"}, nil, strconv.Atoi, func(input *selectorInput, value Supplied[int]) { input.Other = value })}, - }, - CheckInput: func(_ Selection, input selectorInput) error { checkCalls++; checked = input; return nil }, - Scopes: []ConfigScope[selectorInput]{ - {Name: "app", Operations: []ConfigOperation{ConfigValidate}, Acquire: noopConfigAcquire[selectorInput]}, - {Name: "promptkit", Operations: []ConfigOperation{ConfigValidate}, Acquire: noopConfigAcquire[selectorInput]}, - }, - } - normalized, err := normalizeConfigSuite(suite) - if err != nil { - t.Fatal(err) - } - preparedSuite := prepareConfigSuite(normalized) - spec := CommandSpec[configInput[selectorInput]]{Flags: configInputFlags(normalized, "text")} - prepared, err := prepareInput(spec, [][]string{{"all"}, {"text"}, {""}, nil, {"0"}, {"false"}, nil}, nil, true) - if err != nil { - t.Fatal(err) - } - input := prepared.build() - scopeNames, err := preflightConfig(preparedSuite, input, ConfigValidate) - if err != nil { - t.Fatal(err) - } - if !reflect.DeepEqual(scopeNames, []string{"app", "promptkit"}) || checkCalls != 1 || checked.Count != (Supplied[int]{Value: 0, Set: true}) || checked.Enabled != (Supplied[bool]{Value: false, Set: true}) { - t.Fatalf("preflight result=%v checked=%#v calls=%d", scopeNames, checked, checkCalls) - } - if !input.Selection.Config.Set || input.Selection.Config.Value != "" || input.Selection.Overlay.Set { - t.Fatalf("configuration selection presence = %#v", input.Selection) - } - if baseContextCalls != 0 { - t.Fatalf("NewContext called during preflight %d times", baseContextCalls) - } - - contexts := contextsForScopes(preparedSuite, input, scopeNames) - if baseContextCalls != 1 { - t.Fatalf("NewContext calls = %d, want one", baseContextCalls) - } - if contexts["app"].Count != (Supplied[int]{Value: 0, Set: true}) || contexts["app"].Enabled != (Supplied[bool]{}) || contexts["app"].Other != (Supplied[int]{}) { - t.Fatalf("app selectors = %#v", contexts["app"]) - } - if contexts["promptkit"].Count != (Supplied[int]{}) || contexts["promptkit"].Enabled != (Supplied[bool]{Value: false, Set: true}) || contexts["promptkit"].Runtime != "invocation cache" { - t.Fatalf("promptkit selectors = %#v", contexts["promptkit"]) - } -} - -func TestConfigPreflightRejectsUnsupportedAggregateAndInapplicableSelector(t *testing.T) { - type selectorInput struct{ Region Supplied[string] } - newContextCalls := 0 - suite := ConfigSuite[selectorInput]{ - NewContext: func() selectorInput { newContextCalls++; return selectorInput{} }, - Selectors: []Selector[selectorInput]{{Binding: ValueFlag[selectorInput](FlagMeta{Name: "region"}, nil, parseString, func(input *selectorInput, value Supplied[string]) { input.Region = value }), Scopes: []string{"promptkit"}}}, - Scopes: []ConfigScope[selectorInput]{ - {Name: "app", Operations: []ConfigOperation{ConfigValidate}, Acquire: noopConfigAcquire[selectorInput]}, - {Name: "promptkit", Operations: []ConfigOperation{ConfigShow}, Acquire: noopConfigAcquire[selectorInput]}, - }, - } - normalized, err := normalizeConfigSuite(suite) - if err != nil { - t.Fatal(err) - } - preparedSuite := prepareConfigSuite(normalized) - prepared, err := prepareInput(CommandSpec[configInput[selectorInput]]{Flags: configInputFlags(normalized, "text")}, [][]string{{"all"}, {"text"}, nil, nil, nil}, nil, true) - if err != nil { - t.Fatal(err) - } - if _, err := preflightConfig(preparedSuite, prepared.build(), ConfigValidate); err == nil || !strings.Contains(err.Error(), "does not support validate") { - t.Fatalf("unsupported aggregate error = %v", err) - } - if newContextCalls != 0 { - t.Fatalf("unsupported aggregate created context %d times", newContextCalls) - } - - prepared, err = prepareInput(CommandSpec[configInput[selectorInput]]{Flags: configInputFlags(normalized, "text")}, [][]string{{"app"}, {"text"}, nil, nil, {"west"}}, nil, true) - if err != nil { - t.Fatal(err) - } - if _, err := preflightConfig(preparedSuite, prepared.build(), ConfigValidate); err == nil || !strings.Contains(err.Error(), "not applicable") { - t.Fatalf("inapplicable selector error = %v", err) - } -} - -func noopConfigAcquire[C any](context.Context, ConfigRequest, C) (Acquired[ConfigView], error) { - return Acquired[ConfigView]{}, nil -} - -func TestRequiredConfigSelectorOnlyAppliesToSelectedScopes(t *testing.T) { - app, _ := New(Identity{Name: "example"}) - calls := 0 - acquire := func(context.Context, ConfigRequest, string) (Acquired[ConfigView], error) { - calls++ - return Acquired[ConfigView]{}, nil - } - err := RegisterConfig(app, ConfigSuite[string]{ - Selectors: []Selector[string]{{Scopes: []string{"app"}, Binding: ValueFlag[string](FlagMeta{Name: "campaign", Required: true}, nil, parseString, func(c *string, v Supplied[string]) { *c = v.Value })}}, - Scopes: []ConfigScope[string]{{Name: "app", Operations: []ConfigOperation{ConfigValidate}, Acquire: acquire}, {Name: "promptkit", Operations: []ConfigOperation{ConfigValidate}, Acquire: acquire}}, - }) - if err != nil { - t.Fatal(err) - } - for _, tc := range []struct { - args []string - kind OutcomeKind - calls int - }{ - {[]string{"--scope", "promptkit"}, OutcomeSuccess, 1}, - {nil, OutcomeUsage, 0}, - {[]string{"--scope", "all"}, OutcomeUsage, 0}, - {[]string{"--scope", "all", "--campaign", "spring"}, OutcomeSuccess, 2}, - {[]string{"--scope", "promptkit", "--campaign", "spring"}, OutcomeUsage, 0}, - {[]string{"--help"}, OutcomeSuccess, 0}, - } { - calls = 0 - var out bytes.Buffer - result := app.Execute(context.Background(), append([]string{"config", "validate"}, tc.args...), Streams{Out: &out}) - if result.Kind != tc.kind || calls != tc.calls { - t.Fatalf("%v: result=%#v calls=%d", tc.args, result, calls) - } - if len(tc.args) > 0 && tc.args[0] == "--help" && !strings.Contains(out.String(), "required where applicable") { - t.Fatalf("help=%q", out.String()) - } - } -} - -func TestUnknownConfigScopeReportsChoicesWithoutRuntimeWork(t *testing.T) { - for _, tc := range []struct { - names []string - scope []string - choices string - }{ - {[]string{"promptkit"}, nil, "available: promptkit"}, - {[]string{"zeta", "alpha", "promptkit", "app"}, []string{"--scope", "unknown"}, "available: app, promptkit, alpha, zeta"}, - } { - runtimeCalls := 0 - suite := ConfigSuite[struct{}]{NewContext: func() struct{} { runtimeCalls++; return struct{}{} }} - for _, name := range tc.names { - suite.Scopes = append(suite.Scopes, ConfigScope[struct{}]{Name: name, Operations: []ConfigOperation{ConfigValidate}, Acquire: func(context.Context, ConfigRequest, struct{}) (Acquired[ConfigView], error) { - runtimeCalls++ - return Acquired[ConfigView]{}, nil - }}) - } - app, _ := New(Identity{Name: "example"}) - if err := RegisterConfig(app, suite); err != nil { - t.Fatal(err) - } - var out, diagnostics bytes.Buffer - result := app.Execute(context.Background(), append([]string{"config", "validate"}, tc.scope...), Streams{Out: &out, Err: &diagnostics}) - if result.Kind != OutcomeUsage || runtimeCalls != 0 || out.Len() != 0 || !strings.Contains(diagnostics.String(), tc.choices) { - t.Fatalf("result=%#v calls=%d out=%q diagnostics=%q", result, runtimeCalls, out.String(), diagnostics.String()) - } - } -} diff --git a/integrations/ckconfig/configkit_test.go b/integrations/ckconfig/configkit_test.go deleted file mode 100644 index daf3e1f..0000000 --- a/integrations/ckconfig/configkit_test.go +++ /dev/null @@ -1,720 +0,0 @@ -package ckconfig_test - -import ( - "bytes" - "context" - "errors" - "io" - "reflect" - "strings" - "testing" - "testing/fstest" - - commandkit "gitea.maximumdirect.net/eric/CommandKit" - configkit "gitea.maximumdirect.net/eric/ConfigKit" - "go.yaml.in/yaml/v4" -) - -type effectiveYAML string - -func (document effectiveYAML) WriteEffective(writer io.Writer) error { - _, err := io.WriteString(writer, string(document)) - return err -} - -type capturedSourceReader struct { - entries []configkit.SourceInfo - calls int -} - -func TestConfigExplainPreservesConfigKitProvenanceAndRedaction(t *testing.T) { - type entry struct { - Enabled bool `yaml:"enabled"` - Credential configkit.Secret `yaml:"credential"` - } - type configuration struct { - Entries map[string]entry `yaml:"entries"` - Odd map[string]string `yaml:"odd"` - Value string `yaml:"value"` - Dependency string `yaml:"dependency"` - Secret configkit.Secret `yaml:"secret"` - } - const secretValue = "EXPLAIN_TEST_SECRET_VALUE" - t.Setenv("EXPLAIN_TEST_SECRET", secretValue) - input := []byte("entries:\n parent:\n enabled: false\n credential: EXPLAIN_TEST_SECRET\n child:\n extends: parent\nodd:\n \"key/name~\": escaped\nvalue: original\ndependency: source\nsecret: EXPLAIN_TEST_SECRET\n") - result, err := configkit.Load[configuration](configkit.Bytes("explanation.yml", input), configkit.Options[configuration]{Definition: configkit.Definition[configuration]{ - Secret: func(path configkit.Path) bool { return path.String() == "/entries/parent/credential" }, - Resolve: func(value *configuration, resolution *configkit.Resolution) error { - resolution.Record(configkit.Keys("value"), configkit.ChangeNormalization, "upper case", configkit.Keys("dependency")) - value.Value = strings.ToUpper(value.Value) - return nil - }, - }}) - if err != nil { - t.Fatal(err) - } - loads, contexts := 0, 0 - scope, err := commandkit.NewConfigKitScope[struct{}, configuration]("app", "", func(context.Context, commandkit.Selection, struct{}) (*configkit.Result[configuration], error) { - loads++ - return result, 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{}]{NewContext: func() struct{} { contexts++; return struct{}{} }, Scopes: []commandkit.ConfigScope[struct{}]{scope}}); err != nil { - t.Fatal(err) - } - runYAML := func(pointer string) (map[string]any, commandkit.Outcome, string) { - var output bytes.Buffer - argv := []string{"config", "explain", "--path", pointer, "--format", "yaml"} - if pointer == "" { - argv = []string{"config", "explain", "--path=", "--format", "yaml"} - } - outcome := app.Execute(context.Background(), argv, commandkit.Streams{Out: &output, Err: io.Discard}) - var report map[string]any - if outcome.Kind == commandkit.OutcomeSuccess { - if err := yaml.Unmarshal(output.Bytes(), &report); err != nil { - t.Fatalf("decode explanation YAML: %v; output=%q", err, output.String()) - } - } - return report, outcome, output.String() - } - - path := "/entries/child/enabled" - var native bytes.Buffer - if err := result.WriteExplanation(&native, configkit.Keys("entries", "child", "enabled")); err != nil { - t.Fatal(err) - } - var textOutput bytes.Buffer - outcome := app.Execute(context.Background(), []string{"config", "explain", "--path", path}, commandkit.Streams{Out: &textOutput, Err: io.Discard}) - if outcome.Kind != commandkit.OutcomeSuccess || textOutput.String() != native.String() { - t.Fatalf("native text outcome=%#v output=%q want=%q", outcome, textOutput.String(), native.String()) - } - var inherited map[string]any - inherited, outcome, _ = runYAML(path) - if outcome.Kind != commandkit.OutcomeSuccess || inherited["path"] != path { - t.Fatalf("inherited explanation outcome=%#v report=%#v", outcome, inherited) - } - effective := inherited["effective"].(map[string]any) - if effective["present"] != true || effective["yaml"] != "false\n" { - t.Fatalf("inherited effective snapshot = %#v", effective) - } - origin := inherited["origin"].(map[string]any) - steps := origin["inheritance"].([]any) - step := steps[0].(map[string]any) - if step["from"] != "/entries/parent/enabled" || step["to"] != path || step["reference"] != "/entries/child/extends" { - t.Fatalf("inheritance paths = %#v", step) - } - - escaped, outcome, _ := runYAML("/odd/key~1name~0") - if outcome.Kind != commandkit.OutcomeSuccess || escaped["path"] != "/odd/key~1name~0" || escaped["effective"].(map[string]any)["yaml"] != "escaped\n" { - t.Fatalf("escaped path outcome=%#v report=%#v", outcome, escaped) - } - historical, outcome, _ := runYAML("/entries/child/extends") - if outcome.Kind != commandkit.OutcomeSuccess || historical["effective"].(map[string]any)["present"] != false || len(historical["history"].([]any)) == 0 { - t.Fatalf("historical path outcome=%#v report=%#v", outcome, historical) - } - resolved, outcome, _ := runYAML("/value") - if outcome.Kind != commandkit.OutcomeSuccess || resolved["effective"].(map[string]any)["yaml"] != "ORIGINAL\n" { - t.Fatalf("resolved snapshot outcome=%#v report=%#v", outcome, resolved) - } - changes := resolved["history"].([]any) - last := changes[len(changes)-1].(map[string]any) - if last["reasons"].([]any)[0] != "upper case" || last["dependencies"].([]any)[0] != "/dependency" { - t.Fatalf("resolution change = %#v", last) - } - secretReport, outcome, secretText := runYAML("/entries/child/credential") - secretEffective := secretReport["effective"].(map[string]any) - if outcome.Kind != commandkit.OutcomeSuccess || secretEffective["redacted"] != true || strings.Contains(secretText, secretValue) { - t.Fatalf("secret report outcome=%#v report=%#v", outcome, secretReport) - } - for _, change := range secretReport["history"].([]any) { - entry := change.(map[string]any) - for _, key := range []string{"before", "after"} { - snapshot := entry[key].(map[string]any) - if snapshot["present"] == true && snapshot["redacted"] != true { - t.Fatalf("secret history was not redacted: %#v", entry) - } - } - } - directSecret, outcome, directSecretText := runYAML("/secret") - secretRef, refPresent := directSecret["secret_reference"].(map[string]any) - if !refPresent { - t.Fatalf("secret reference was absent: %#v", directSecret) - } - if outcome.Kind != commandkit.OutcomeSuccess || secretRef["name"] != "EXPLAIN_TEST_SECRET" || strings.Contains(directSecretText, secretValue) { - t.Fatalf("direct secret reference outcome=%#v report=%#v", outcome, directSecret) - } - root, outcome, _ := runYAML("") - if outcome.Kind != commandkit.OutcomeSuccess || root["path"] != "" { - t.Fatalf("root path outcome=%#v report=%#v", outcome, root) - } - - loadsBeforeInvalid, contextsBeforeInvalid := loads, contexts - outcome = app.Execute(context.Background(), []string{"config", "explain"}, commandkit.Streams{Out: io.Discard, Err: io.Discard}) - if outcome.Kind != commandkit.OutcomeUsage || loads != loadsBeforeInvalid || contexts != contextsBeforeInvalid { - t.Fatalf("missing path outcome=%#v loads=%d contexts=%d", outcome, loads, contexts) - } - outcome = app.Execute(context.Background(), []string{"config", "explain", "--path", "/bad~2path"}, commandkit.Streams{Out: io.Discard, Err: io.Discard}) - if outcome.Kind != commandkit.OutcomeUsage || outcome.Status != 2 || loads != loadsBeforeInvalid || contexts != contextsBeforeInvalid { - t.Fatalf("malformed path outcome=%#v loads=%d contexts=%d", outcome, loads, contexts) - } - outcome = app.Execute(context.Background(), []string{"config", "explain", "--path", "/does-not-exist"}, commandkit.Streams{Out: io.Discard, Err: io.Discard}) - if outcome.Kind != commandkit.OutcomeOperational || outcome.Status != 1 { - t.Fatalf("unknown path outcome = %#v", outcome) - } - partial := &configKitShowPartialWriter{failure: io.ErrClosedPipe} - outcome = app.Execute(context.Background(), []string{"config", "explain", "--path", path}, commandkit.Streams{Out: partial, Err: io.Discard}) - if outcome.Kind != commandkit.OutcomeOperational || !outcome.ReportIncomplete || !errors.Is(outcome.DeliveryError, io.ErrClosedPipe) { - t.Fatalf("explanation writer failure = %#v", outcome) - } -} - -func (reader *capturedSourceReader) Sources() []configkit.SourceInfo { - reader.calls++ - return reader.entries -} - -func TestConfigSourcesReportsCapturedConfigKitSourceInventory(t *testing.T) { - type configuration struct { - Value string `yaml:"value"` - Shared string `yaml:"shared"` - Synthetic string `yaml:"synthetic"` - } - filesystem := configkit.NewFilesystem("fixture", fstest.MapFS{ - "root/config/main.yml": &fstest.MapFile{Data: []byte("configkit: {imports: [{path: ../shared.yml}]}\nvalue: base\n")}, - "root/shared.yml": &fstest.MapFile{Data: []byte("shared: imported\n")}, - "root/overlays/dev.yml": &fstest.MapFile{Data: []byte("value: overlay\n")}, - }) - filesystemContext := &configkit.FilesystemContext{Root: "root", FS: &filesystem} - overlayFilesystemContext := &configkit.FilesystemContext{Root: "root", BaseDir: "root/config", FS: &filesystem} - loaderCalls := 0 - reader := &capturedSourceReader{} - scope, err := commandkit.NewConfigKitScope[struct{}, configuration]("app", "", func(context.Context, commandkit.Selection, struct{}) (*configkit.Result[configuration], error) { - loaderCalls++ - result, err := configkit.LoadLayered(configkit.Input{ - Source: filesystem.File("root/config/main.yml"), - Imports: true, - Filesystem: filesystemContext, - }, configkit.LayeredOptions[configuration]{ - Definition: configkit.Definition[configuration]{}, - Underlays: []configkit.Underlay{{Inputs: []configkit.Input{{Source: configkit.Mapping("synthetic source", map[string]any{"synthetic": "underlay"})}}}}, - Overlay: "dev", - OverlayFilesystem: overlayFilesystemContext, - }) - if err != nil { - return nil, err - } - reader.entries = result.Sources() - return result, nil - }) - if err != nil { - t.Fatal(err) - } - // Replace the result's reader after the owning loader has captured its source inventory. - originalAcquire := scope.Acquire - scope.Acquire = func(ctx context.Context, request commandkit.ConfigRequest, input struct{}) (commandkit.Acquired[commandkit.ConfigView], error) { - acquired, err := originalAcquire(ctx, request, input) - if err == nil { - acquired.Value.Sources = reader - } - return acquired, err - } - // Register the wrapped scope so the command reads the captured public facet once. - app, _ := commandkit.New(commandkit.Identity{Name: "example"}) - if err := commandkit.RegisterConfig(app, commandkit.ConfigSuite[struct{}]{Scopes: []commandkit.ConfigScope[struct{}]{scope}}); err != nil { - t.Fatal(err) - } - var output bytes.Buffer - outcome := app.Execute(context.Background(), []string{"config", "sources", "--format", "yaml"}, commandkit.Streams{Out: &output, Err: io.Discard}) - if outcome.Kind != commandkit.OutcomeSuccess || loaderCalls != 1 || reader.calls != 1 { - var details bytes.Buffer - for _, diagnostic := range outcome.Diagnostics { - _ = diagnostic.Write(&details) - } - t.Fatalf("sources outcome=%#v cause=%v details=%q loader calls=%d reader calls=%d", outcome, outcome.Cause, details.String(), loaderCalls, reader.calls) - } - var report struct { - Scope string `yaml:"scope"` - Sources []struct { - ID string `yaml:"id"` - Label string `yaml:"label"` - Path string `yaml:"path"` - Layer string `yaml:"layer"` - Kind string `yaml:"kind"` - UnderlayIndex int `yaml:"underlay_index"` - ImportedFrom *struct { - SourceID string `yaml:"source_id"` - Line int `yaml:"line"` - Column int `yaml:"column"` - } `yaml:"imported_from"` - } `yaml:"sources"` - } - if err := yaml.Unmarshal(output.Bytes(), &report); err != nil { - t.Fatal(err) - } - if report.Scope != "app" || len(report.Sources) != 4 { - t.Fatalf("source report = %#v", report) - } - if report.Sources[0].Path != "root/config/main.yml" || report.Sources[0].ID != "source-1" { - t.Fatalf("main source = %#v", report.Sources[0]) - } - if report.Sources[0].ImportedFrom != nil { - t.Fatalf("root source imported_from = %#v, want null", report.Sources[0].ImportedFrom) - } - imported := report.Sources[1] - if imported.Path != "root/shared.yml" || imported.ImportedFrom == nil || imported.ImportedFrom.SourceID != report.Sources[0].ID || imported.ImportedFrom.Line == 0 { - t.Fatalf("import source correlation = %#v", imported) - } - synthetic := report.Sources[2] - if synthetic.Kind != string(configkit.SourceKindMapping) || synthetic.Path != "" || synthetic.Layer != string(configkit.LayerUnderlay) || synthetic.UnderlayIndex != 1 { - t.Fatalf("synthetic underlay source = %#v", synthetic) - } - if report.Sources[3].Layer != string(configkit.LayerOverlay) || report.Sources[3].Path != "root/overlays/dev.yml" { - t.Fatalf("overlay source = %#v", report.Sources[3]) - } -} - -func TestConfigSourcesHandlesEmptyInventoryAllPreflightAndWriterFailure(t *testing.T) { - reader := &capturedSourceReader{} - newContextCalls, acquireCalls := 0, 0 - app, err := commandkit.New(commandkit.Identity{Name: "example"}) - if err != nil { - t.Fatal(err) - } - suite := commandkit.ConfigSuite[string]{ - NewContext: func() string { newContextCalls++; return "context" }, - Scopes: []commandkit.ConfigScope[string]{{Name: "app", Operations: []commandkit.ConfigOperation{commandkit.ConfigSources}, Acquire: func(context.Context, commandkit.ConfigRequest, string) (commandkit.Acquired[commandkit.ConfigView], error) { - acquireCalls++ - return commandkit.Acquired[commandkit.ConfigView]{Value: commandkit.ConfigView{Sources: reader}}, nil - }}}, - } - if err := commandkit.RegisterConfig(app, suite); err != nil { - t.Fatal(err) - } - var output bytes.Buffer - outcome := app.Execute(context.Background(), []string{"config", "sources", "--scope", "all"}, commandkit.Streams{Out: &output, Err: io.Discard}) - if outcome.Kind != commandkit.OutcomeUsage || newContextCalls != 0 || acquireCalls != 0 || output.Len() != 0 { - t.Fatalf("all preflight outcome=%#v contexts=%d acquires=%d output=%q", outcome, newContextCalls, acquireCalls, output.String()) - } - outcome = app.Execute(context.Background(), []string{"config", "sources", "--format", "yaml"}, commandkit.Streams{Out: &output, Err: io.Discard}) - if outcome.Kind != commandkit.OutcomeSuccess || output.String() != "scope: app\nsources: []\n" || reader.calls != 1 { - t.Fatalf("empty source report outcome=%#v output=%q reader calls=%d", outcome, output.String(), reader.calls) - } - reader.entries = []configkit.SourceInfo{{ID: "source-1", Label: "label with \"quotes\"", Path: "path with spaces.yml", Layer: configkit.LayerBase, Kind: configkit.SourceKindFile}} - var textOutput bytes.Buffer - outcome = app.Execute(context.Background(), []string{"config", "sources"}, commandkit.Streams{Out: &textOutput, Err: io.Discard}) - if outcome.Kind != commandkit.OutcomeSuccess || !strings.Contains(textOutput.String(), `label: "label with \"quotes\""`) || !strings.Contains(textOutput.String(), `path: "path with spaces.yml"`) { - t.Fatalf("quoted text source output outcome=%#v output=%q", outcome, textOutput.String()) - } - partial := &configKitShowPartialWriter{failure: io.ErrClosedPipe} - outcome = app.Execute(context.Background(), []string{"config", "sources"}, commandkit.Streams{Out: partial, Err: io.Discard}) - if outcome.Kind != commandkit.OutcomeOperational || !outcome.ReportIncomplete || !errors.Is(outcome.DeliveryError, io.ErrClosedPipe) { - t.Fatalf("source writer failure = %#v", outcome) - } -} - -func TestConfigShowWritesNativeSingleScopeAndFramesAggregateYAML(t *testing.T) { - const native = "%YAML 1.1\n# authored comment\n---\nvalue: null\nitems: []\nmissing: {}\n" - app, err := commandkit.New(commandkit.Identity{Name: "example"}) - if err != nil { - t.Fatal(err) - } - var calls []string - suite := commandkit.ConfigSuite[struct{}]{Scopes: []commandkit.ConfigScope[struct{}]{ - {Name: "zeta", Operations: []commandkit.ConfigOperation{commandkit.ConfigShow}, Acquire: func(context.Context, commandkit.ConfigRequest, struct{}) (commandkit.Acquired[commandkit.ConfigView], error) { - calls = append(calls, "zeta") - return commandkit.Acquired[commandkit.ConfigView]{Value: commandkit.ConfigView{Effective: effectiveYAML("%YAML 1.1\n# zeta comment\nvalue: zeta\n")}}, nil - }}, - {Name: "app", Operations: []commandkit.ConfigOperation{commandkit.ConfigShow}, Acquire: func(context.Context, commandkit.ConfigRequest, struct{}) (commandkit.Acquired[commandkit.ConfigView], error) { - calls = append(calls, "app") - return commandkit.Acquired[commandkit.ConfigView]{Value: commandkit.ConfigView{Effective: effectiveYAML(native)}}, nil - }}, - }} - if err := commandkit.RegisterConfig(app, suite); err != nil { - t.Fatal(err) - } - var single bytes.Buffer - outcome := app.Execute(context.Background(), []string{"config", "show"}, commandkit.Streams{Out: &single, Err: io.Discard}) - if outcome.Kind != commandkit.OutcomeSuccess || single.String() != native { - t.Fatalf("single show outcome=%#v output=%q", outcome, single.String()) - } - calls = nil - outcome = app.Execute(context.Background(), []string{"config", "show", "--format", "text"}, commandkit.Streams{Out: &single, Err: io.Discard}) - if outcome.Kind != commandkit.OutcomeUsage || len(calls) != 0 { - t.Fatalf("text format outcome=%#v acquisitions=%v", outcome, calls) - } - - calls = nil - var aggregate bytes.Buffer - outcome = app.Execute(context.Background(), []string{"config", "show", "--scope", "all"}, commandkit.Streams{Out: &aggregate, Err: io.Discard}) - if outcome.Kind != commandkit.OutcomeSuccess || !reflect.DeepEqual(calls, []string{"app", "zeta"}) { - t.Fatalf("aggregate outcome=%#v acquisitions=%v", outcome, calls) - } - if !strings.Contains(aggregate.String(), "%YAML 1.1\n# authored comment\n---\n# scope: app\n") || !strings.Contains(aggregate.String(), "...\n%YAML 1.1\n# zeta comment\n---\n# scope: zeta\nvalue: zeta\n") { - t.Fatalf("aggregate framing = %q", aggregate.String()) - } - decoder := yaml.NewDecoder(bytes.NewReader(aggregate.Bytes())) - var first, second map[string]any - if err := decoder.Decode(&first); err != nil { - t.Fatal(err) - } - if err := decoder.Decode(&second); err != nil { - t.Fatal(err) - } - var extra any - if err := decoder.Decode(&extra); err != io.EOF { - t.Fatalf("third YAML document decode error = %v, value=%#v", err, extra) - } - if first["value"] != nil || first["items"] == nil || first["missing"] == nil || second["value"] != "zeta" { - t.Fatalf("decoded documents = %#v and %#v", first, second) - } - if _, exists := first["absent"]; exists { - t.Fatalf("absent field was added to native document: %#v", first) - } -} - -func TestConfigShowAcquiresAllBeforeOutputAndTracksWriterFailures(t *testing.T) { - failure := errors.New("second scope failed") - app, err := commandkit.New(commandkit.Identity{Name: "example"}) - if err != nil { - t.Fatal(err) - } - calls := 0 - suite := 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) { - calls++ - return commandkit.Acquired[commandkit.ConfigView]{Value: commandkit.ConfigView{Effective: effectiveYAML("value: app\n")}}, nil - }}, - {Name: "promptkit", Operations: []commandkit.ConfigOperation{commandkit.ConfigShow}, Acquire: func(context.Context, commandkit.ConfigRequest, struct{}) (commandkit.Acquired[commandkit.ConfigView], error) { - calls++ - return commandkit.Acquired[commandkit.ConfigView]{Diagnostics: []commandkit.Diagnostic{commandkit.MessageDiagnostic(configkit.SeverityWarning, "provider warning")}}, failure - }}, - }} - if err := commandkit.RegisterConfig(app, suite); 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 !errors.Is(outcome.Cause, failure) || outcome.ReportIncomplete || output.Len() != 0 || calls != 2 || len(outcome.Diagnostics) < 1 || outcome.Diagnostics[0].Severity != configkit.SeverityWarning { - t.Fatalf("acquisition failure outcome=%#v output=%q calls=%d", outcome, output.String(), calls) - } - - app, _ = commandkit.New(commandkit.Identity{Name: "example"}) - 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("value: app\n")}}, nil - }}}}); err != nil { - t.Fatal(err) - } - partial := &configKitShowPartialWriter{failure: io.ErrClosedPipe} - outcome = app.Execute(context.Background(), []string{"config", "show", "--scope", "all"}, commandkit.Streams{Out: partial, Err: io.Discard}) - if outcome.Kind != commandkit.OutcomeOperational || !outcome.ReportIncomplete || !errors.Is(outcome.Cause, io.ErrClosedPipe) || !errors.Is(outcome.DeliveryError, io.ErrClosedPipe) || len(partial.written) == 0 { - t.Fatalf("writer failure outcome = %#v", outcome) - } -} - -type configKitShowPartialWriter struct { - written []byte - failure error -} - -func (writer *configKitShowPartialWriter) Write(data []byte) (int, error) { - n := len(data) - if n > 4 { - n = 4 - } - writer.written = append(writer.written, data[:n]...) - return n, writer.failure -} - -func TestConfigKitScopeUsesDefaultsOverlayAndRecordedResolution(t *testing.T) { - type configuration struct { - Mode string `yaml:"mode"` - Defaulted string `yaml:"defaulted"` - } - filesystem := configkit.NewFilesystem("fixture", fstest.MapFS{ - "config/main.yml": &fstest.MapFile{Data: []byte("mode: base\n")}, - "config/overlays/dev.yml": &fstest.MapFile{Data: []byte("mode: overlay\n")}, - }) - definition := configkit.Definition[configuration]{ - Defaults: func() configuration { return configuration{Defaulted: "from default"} }, - Resolve: func(input *configuration, resolution *configkit.Resolution) error { - resolution.Record(configkit.Keys("mode"), configkit.ChangeNormalization, "normalized for use") - input.Mode = strings.ToUpper(input.Mode) - return nil - }, - } - var received commandkit.Selection - scope, err := commandkit.NewConfigKitScope[struct{}, configuration]("app", "Application settings.", func(_ context.Context, selection commandkit.Selection, _ struct{}) (*configkit.Result[configuration], error) { - received = selection - return configkit.LoadLayered(configkit.Input{Source: filesystem.File(selection.Config.Value)}, configkit.LayeredOptions[configuration]{ - Definition: definition, - Overlay: selection.Overlay.Value, - OverlayFilesystem: &configkit.FilesystemContext{Root: "config", BaseDir: "config", FS: &filesystem}, - }) - }) - if err != nil { - t.Fatal(err) - } - selection := commandkit.Selection{ - Config: commandkit.Supplied[string]{Value: "config/main.yml", Set: true}, - Overlay: commandkit.Supplied[string]{Value: "dev", Set: true}, - } - acquired, err := scope.Acquire(context.Background(), commandkit.ConfigRequest{Operation: commandkit.ConfigValidate, Selection: selection}, struct{}{}) - if err != nil { - t.Fatal(err) - } - if !reflect.DeepEqual(received, selection) { - t.Fatalf("loader selection = %#v, want %#v", received, selection) - } - if acquired.Value.Effective == nil || acquired.Value.Sources == nil || acquired.Value.Explanation == nil { - t.Fatalf("ConfigKit result facets = %#v", acquired.Value) - } - if got := acquired.Value.Sources.Sources(); len(got) < 2 { - t.Fatalf("sources = %#v, want base and overlay", got) - } - var effective bytes.Buffer - if err := acquired.Value.Effective.WriteEffective(&effective); err != nil { - t.Fatal(err) - } - if !strings.Contains(effective.String(), "mode: OVERLAY") || !strings.Contains(effective.String(), "defaulted: from default") { - t.Fatalf("effective configuration = %q", effective.String()) - } - if explanation, ok := acquired.Value.Explanation.Explain(configkit.Keys("mode")); !ok || len(explanation.History) == 0 { - t.Fatalf("resolution explanation = %#v, present=%t", explanation, ok) - } -} - -func TestConfigKitScopePreservesAbsentAndEmptySelections(t *testing.T) { - type configuration struct { - Value string `yaml:"value"` - } - var seen []commandkit.Selection - scope, err := commandkit.NewConfigKitScope[struct{}, configuration]("app", "", func(_ context.Context, selection commandkit.Selection, _ struct{}) (*configkit.Result[configuration], error) { - seen = append(seen, selection) - return configkit.Load(configkit.NoSource(), configkit.Options[configuration]{Definition: configkit.Definition[configuration]{Defaults: func() configuration { return configuration{Value: "default"} }}}) - }) - if err != nil { - t.Fatal(err) - } - _, err = scope.Acquire(context.Background(), commandkit.ConfigRequest{}, struct{}{}) - if err != nil { - t.Fatal(err) - } - empty := commandkit.Selection{Config: commandkit.Supplied[string]{Set: true}, Overlay: commandkit.Supplied[string]{Set: true}} - _, err = scope.Acquire(context.Background(), commandkit.ConfigRequest{Selection: empty}, struct{}{}) - if err != nil { - t.Fatal(err) - } - want := []commandkit.Selection{{}, empty} - if !reflect.DeepEqual(seen, want) { - t.Fatalf("loader selections = %#v, want %#v", seen, want) - } -} - -func TestConfigKitScopeRequiresLoaderAndNonNilResult(t *testing.T) { - if _, err := commandkit.NewConfigKitScope[struct{}, struct{}]("app", "", nil); err == nil { - t.Fatal("accepted a nil ConfigKit loader") - } - scope, err := commandkit.NewConfigKitScope[struct{}, struct{}]("app", "", func(context.Context, commandkit.Selection, struct{}) (*configkit.Result[struct{}], error) { - return nil, nil - }) - if err != nil { - t.Fatal(err) - } - if _, err := scope.Acquire(context.Background(), commandkit.ConfigRequest{}, struct{}{}); err == nil || !strings.Contains(err.Error(), "nil result") { - t.Fatalf("nil result error = %v", err) - } -} - -func TestConfigKitLoadErrorRetainsIdentityAndStructuredDiagnostics(t *testing.T) { - type configuration struct { - Name string `yaml:"name"` - } - var loadError error - scope, err := commandkit.NewConfigKitScope[struct{}, configuration]("app", "", func(context.Context, commandkit.Selection, struct{}) (*configkit.Result[configuration], error) { - _, loadError = configkit.Load(configkit.Mapping("bad source", map[string]any{"unknown": true}), configkit.Options[configuration]{Definition: configkit.Definition[configuration]{}}) - return nil, loadError - }) - if err != nil { - t.Fatal(err) - } - acquired, acquireError := scope.Acquire(context.Background(), commandkit.ConfigRequest{}, struct{}{}) - var publicLoadError *configkit.LoadError - if acquireError != loadError || !errors.As(acquireError, &publicLoadError) { - t.Fatalf("load error identity = %v (original %v), public type=%T", acquireError, loadError, acquireError) - } - if len(acquired.Diagnostics) != 1 || acquired.Diagnostics[0].Severity != configkit.SeverityError { - t.Fatalf("structured diagnostics = %#v", acquired.Diagnostics) - } - var rendered bytes.Buffer - if err := acquired.Diagnostics[0].Write(&rendered); err != nil { - t.Fatal(err) - } - if !strings.Contains(rendered.String(), `Configuration scope "app":`) || !strings.Contains(rendered.String(), `"bad source"`) { - t.Fatalf("grouped diagnostics = %q", rendered.String()) - } - - app, _ := commandkit.New(commandkit.Identity{Name: "example"}) - if err := commandkit.RegisterConfig(app, commandkit.ConfigSuite[struct{}]{Scopes: []commandkit.ConfigScope[struct{}]{scope}}); err != nil { - t.Fatal(err) - } - var output, diagnostic bytes.Buffer - outcome := app.Execute(context.Background(), []string{"config", "validate"}, commandkit.Streams{Out: &output, Err: &diagnostic}) - if outcome.Cause != loadError || len(outcome.Diagnostics) != 1 || strings.Count(diagnostic.String(), `Configuration scope "app":`) != 1 || output.Len() != 0 { - t.Fatalf("presented load failure outcome=%#v output=%q diagnostics=%q", outcome, output.String(), diagnostic.String()) - } -} - -func TestConfigValidationEmitsSuccessAsScopesCompleteAndStopsAtFailure(t *testing.T) { - app, err := commandkit.New(commandkit.Identity{Name: "example"}) - if err != nil { - t.Fatal(err) - } - var calls []string - newContextCalls := 0 - failure := errors.New("promptkit could not load") - suite := commandkit.ConfigSuite[string]{ - NewContext: func() string { newContextCalls++; return "runtime" }, - Scopes: []commandkit.ConfigScope[string]{ - {Name: "zeta", Operations: []commandkit.ConfigOperation{commandkit.ConfigValidate}, Acquire: func(context.Context, commandkit.ConfigRequest, string) (commandkit.Acquired[commandkit.ConfigView], error) { - calls = append(calls, "zeta") - return commandkit.Acquired[commandkit.ConfigView]{}, nil - }}, - {Name: "promptkit", Operations: []commandkit.ConfigOperation{commandkit.ConfigValidate}, Acquire: func(context.Context, commandkit.ConfigRequest, string) (commandkit.Acquired[commandkit.ConfigView], error) { - calls = append(calls, "promptkit") - return commandkit.Acquired[commandkit.ConfigView]{}, failure - }}, - {Name: "app", Operations: []commandkit.ConfigOperation{commandkit.ConfigValidate}, Acquire: func(context.Context, commandkit.ConfigRequest, string) (commandkit.Acquired[commandkit.ConfigView], error) { - calls = append(calls, "app") - return commandkit.Acquired[commandkit.ConfigView]{}, nil - }}, - }, - } - if err := commandkit.RegisterConfig(app, suite); err != nil { - t.Fatal(err) - } - var output, diagnostic bytes.Buffer - outcome := app.Execute(context.Background(), []string{"config", "validate", "--scope", "all"}, commandkit.Streams{Out: &output, Err: &diagnostic}) - if outcome.Kind != commandkit.OutcomeOperational || !errors.Is(outcome.Cause, failure) || !outcome.ReportIncomplete { - t.Fatalf("validation outcome = %#v", outcome) - } - if !reflect.DeepEqual(calls, []string{"app", "promptkit"}) || newContextCalls != 1 { - t.Fatalf("acquisitions = %v, NewContext calls=%d", calls, newContextCalls) - } - if output.String() != "app: valid\n" || !strings.Contains(diagnostic.String(), failure.Error()) { - t.Fatalf("output=%q diagnostics=%q", output.String(), diagnostic.String()) - } -} - -func TestConfigValidationYAMLWarningsAndShortWrites(t *testing.T) { - type configuration struct { - Name string `yaml:"name"` - } - warningScope, err := commandkit.NewConfigKitScope[struct{}, configuration]("app", "", func(context.Context, commandkit.Selection, struct{}) (*configkit.Result[configuration], error) { - return configkit.Load(configkit.NoSource(), configkit.Options[configuration]{Definition: configkit.Definition[configuration]{ - Validate: func(_ *configuration, validation *configkit.Validation) { - validation.Warning(configkit.Keys("name"), "uses a demonstration name") - }, - }}) - }) - if err != nil { - t.Fatal(err) - } - app, _ := commandkit.New(commandkit.Identity{Name: "example"}) - if err := commandkit.RegisterConfig(app, commandkit.ConfigSuite[struct{}]{Scopes: []commandkit.ConfigScope[struct{}]{warningScope}}); err != nil { - t.Fatal(err) - } - var output, diagnostic bytes.Buffer - outcome := app.Execute(context.Background(), []string{"config", "validate", "--format", "yaml"}, commandkit.Streams{Out: &output, Err: &diagnostic}) - if outcome.Kind != commandkit.OutcomeSuccess || outcome.Status != 0 || outcome.DiagnosticDelivery != commandkit.DeliveryComplete { - t.Fatalf("warning validation outcome = %#v", outcome) - } - if output.String() != "scopes:\n - name: app\n valid: true\n" || !strings.Contains(diagnostic.String(), "uses a demonstration name") { - t.Fatalf("output=%q diagnostics=%q", output.String(), diagnostic.String()) - } - - outcome = app.Execute(context.Background(), []string{"config", "validate"}, commandkit.Streams{Out: configKitPartialWriter{}, Err: io.Discard}) - if outcome.Kind != commandkit.OutcomeOperational || !outcome.ReportIncomplete || !errors.Is(outcome.Cause, io.ErrShortWrite) || !errors.Is(outcome.DeliveryError, io.ErrShortWrite) { - t.Fatalf("report short write outcome = %#v", outcome) - } - var delivered bytes.Buffer - outcome = app.Execute(context.Background(), []string{"config", "validate"}, commandkit.Streams{Out: &delivered, Err: configKitPartialWriter{}}) - if outcome.DiagnosticDelivery != commandkit.DeliveryIncomplete || !errors.Is(outcome.DeliveryError, io.ErrShortWrite) { - t.Fatalf("diagnostic short write outcome = %#v", outcome) - } -} - -type configKitPartialWriter struct{} - -func (configKitPartialWriter) Write(data []byte) (int, error) { - if len(data) == 0 { - return 0, nil - } - return len(data) - 1, nil -} - -func TestValidationReportDeliveryErrorsAtEachWriteBoundary(t *testing.T) { - reportErr := errors.New("report failed") - diagnosticErr := errors.New("diagnostic failed") - app, _ := commandkit.New(commandkit.Identity{Name: "example"}) - acquire := func(context.Context, commandkit.ConfigRequest, struct{}) (commandkit.Acquired[commandkit.ConfigView], error) { - return commandkit.Acquired[commandkit.ConfigView]{}, nil - } - if err := commandkit.RegisterConfig(app, commandkit.ConfigSuite[struct{}]{Scopes: []commandkit.ConfigScope[struct{}]{ - {Name: "app", Operations: []commandkit.ConfigOperation{commandkit.ConfigValidate}, Acquire: acquire}, - {Name: "promptkit", Operations: []commandkit.ConfigOperation{commandkit.ConfigValidate}, Acquire: acquire}, - }}); err != nil { - t.Fatal(err) - } - for _, format := range []string{"text", "yaml"} { - writes := 2 - if format == "yaml" { - writes = 3 - } - for at := 1; at <= writes; at++ { - for _, brokenErr := range []bool{false, true} { - out := &validationBoundaryWriter{failAt: at, err: reportErr} - errStream := io.Writer(io.Discard) - if brokenErr { - errStream = failingWriter{diagnosticErr} - } - result := app.Execute(context.Background(), []string{"config", "validate", "--scope", "all", "--format", format}, commandkit.Streams{Out: out, Err: errStream}) - if result.Kind != commandkit.OutcomeOperational || !result.ReportIncomplete || !errors.Is(result.Cause, reportErr) || !errors.Is(result.DeliveryError, reportErr) || (brokenErr && !errors.Is(result.DeliveryError, diagnosticErr)) { - t.Fatalf("format=%s at=%d brokenErr=%t result=%#v", format, at, brokenErr, result) - } - if at > 1 && out.written.Len() == 0 { - t.Fatal("lost completed prefix") - } - } - } - } - failedApp, _ := commandkit.New(commandkit.Identity{Name: "example"}) - if err := commandkit.RegisterConfig(failedApp, commandkit.ConfigSuite[struct{}]{Scopes: []commandkit.ConfigScope[struct{}]{{Name: "app", Operations: []commandkit.ConfigOperation{commandkit.ConfigValidate}, Acquire: func(context.Context, commandkit.ConfigRequest, struct{}) (commandkit.Acquired[commandkit.ConfigView], error) { - return commandkit.Acquired[commandkit.ConfigView]{}, reportErr - }}}}); err != nil { - t.Fatal(err) - } - result := failedApp.Execute(context.Background(), []string{"config", "validate"}, commandkit.Streams{}) - if !errors.Is(result.Cause, reportErr) || result.DeliveryError != nil { - t.Fatalf("acquisition failure=%#v", result) - } -} - -type validationBoundaryWriter struct { - failAt, calls int - err error - written bytes.Buffer -} - -func (w *validationBoundaryWriter) Write(p []byte) (int, error) { - w.calls++ - if w.calls == w.failAt { - return 0, w.err - } - return w.written.Write(p) -} diff --git a/integrations/ckconfig/examples/simple/main.go b/integrations/ckconfig/examples/simple/main.go index 38c5b5e..df92c10 100644 --- a/integrations/ckconfig/examples/simple/main.go +++ b/integrations/ckconfig/examples/simple/main.go @@ -7,6 +7,7 @@ import ( "strconv" commandkit "gitea.maximumdirect.net/eric/CommandKit" + "gitea.maximumdirect.net/eric/CommandKit/integrations/ckconfig" configkit "gitea.maximumdirect.net/eric/ConfigKit" ) @@ -19,38 +20,28 @@ type greetInput struct { Tags commandkit.Supplied[[]string] } -func loadAppConfiguration(context.Context, commandkit.Selection, struct{}) (*configkit.Result[appConfiguration], error) { - return configkit.Load[appConfiguration](configkit.Bytes("simple example", []byte("greeting: hello\n"))) +func loadAppConfiguration(_ context.Context, _ ckconfig.Selection) (*configkit.Result[appConfiguration], error) { + return configkit.Load[appConfiguration](configkit.Mapping("simple application settings", map[string]any{ + "greeting": "Hello", + })) } func newApp() (*commandkit.App, error) { - app, err := commandkit.New(commandkit.Identity{Name: "simple-example", Version: "1.0"}) - if err != nil { - return nil, err - } - scope, err := commandkit.NewConfigKitScope[struct{}, appConfiguration]("app", "Application settings.", loadAppConfiguration) - if err != nil { - return nil, err - } - if err := commandkit.RegisterConfig(app, commandkit.ConfigSuite[struct{}]{Scopes: []commandkit.ConfigScope[struct{}]{scope}}); err != nil { - return nil, err - } - countDefault := 1 tagDefaults := []string{"example"} - greet, err := commandkit.NewCommand(commandkit.CommandSpec[greetInput]{ + greet := commandkit.NewCommand(commandkit.CommandSpec[greetInput]{ Name: "greet", Summary: "Show flag presence and repeated values.", Flags: []commandkit.Flag[greetInput]{ - commandkit.ValueFlag[greetInput](commandkit.FlagMeta{Name: "count", Usage: "Greeting count."}, &countDefault, strconv.Atoi, func(input *greetInput, value commandkit.Supplied[int]) { + commandkit.ValueFlag(commandkit.FlagMeta{Name: "count", Usage: "Greeting count."}, &countDefault, strconv.Atoi, func(input *greetInput, value commandkit.Supplied[int]) { input.Count = value }), - commandkit.RepeatFlag[greetInput](commandkit.FlagMeta{Name: "tag", Usage: "A repeatable label."}, tagDefaults, func(value string) (string, error) { return value, nil }, func(input *greetInput, value commandkit.Supplied[[]string]) { + commandkit.RepeatFlag(commandkit.FlagMeta{Name: "tag", Usage: "A repeatable label."}, tagDefaults, func(value string) (string, error) { return value, nil }, func(input *greetInput, value commandkit.Supplied[[]string]) { input.Tags = value }), }, Run: func(invocation commandkit.Invocation, input greetInput) commandkit.Outcome { - configuration, err := loadAppConfiguration(invocation.Context, commandkit.Selection{}, struct{}{}) + configuration, err := loadAppConfiguration(invocation.Context, ckconfig.Selection{}) if err != nil { return commandkit.Failure(err) } @@ -68,13 +59,11 @@ func newApp() (*commandkit.App, error) { return commandkit.Success() }, }) - if err != nil { - return nil, err - } - if err := app.Register(greet); err != nil { - return nil, err - } - return app, nil + return commandkit.New( + commandkit.Identity{Name: "simple-example", Version: "1.0"}, + ckconfig.AppCommand(loadAppConfiguration), + greet, + ) } func main() { diff --git a/integrations/ckconfig/parent_help_test.go b/integrations/ckconfig/parent_help_test.go deleted file mode 100644 index 67500ce..0000000 --- a/integrations/ckconfig/parent_help_test.go +++ /dev/null @@ -1,52 +0,0 @@ -package ckconfig_test - -import ( - "bytes" - "context" - "strings" - "testing" - - commandkit "gitea.maximumdirect.net/eric/CommandKit" - "gitea.maximumdirect.net/eric/CommandKit/integrations/ckconfig" -) - -func TestParentHelpDiscoversMetadataOnlyGroups(t *testing.T) { - leaf := commandkit.Group("notes", "Read notes metadata.") - parent := commandkit.Group("topics", "Browse topic metadata.", leaf) - app, err := commandkit.New(commandkit.Identity{Name: "example"}, parent) - if err != nil { - t.Fatal(err) - } - loads := 0 - if err := ckconfig.RegisterConfig(app, ckconfig.ConfigSuite[struct{}]{Scopes: []ckconfig.ConfigScope[struct{}]{{Name: "app", Operations: []ckconfig.ConfigOperation{ckconfig.ConfigValidate}, Acquire: func(context.Context, ckconfig.ConfigRequest, struct{}) (commandkit.Acquired[ckconfig.ConfigView], error) { - loads++ - return commandkit.Acquired[ckconfig.ConfigView]{}, nil - }}}}); err != nil { - t.Fatal(err) - } - for _, tc := range []struct { - args []string - content string - }{ - {nil, "Browse topic metadata."}, - {[]string{"topics"}, "Read notes metadata."}, - {[]string{"topics", "--help"}, "Read notes metadata."}, - {[]string{"help", "topics"}, "Read notes metadata."}, - {[]string{"topics", "notes"}, "Read notes metadata."}, - {[]string{"help", "topics", "notes"}, "Read notes metadata."}, - } { - var out bytes.Buffer - result := app.Execute(context.Background(), tc.args, commandkit.Streams{Out: &out}) - if result.Kind != commandkit.OutcomeSuccess || !strings.Contains(out.String(), tc.content) || loads != 0 { - t.Fatalf("%v: result=%#v output=%q loads=%d", tc.args, result, out.String(), loads) - } - if len(tc.args) == 1 && (!strings.Contains(out.String(), "Available Commands:") || !strings.Contains(out.String(), "[command]")) { - t.Fatalf("missing child inventory/usage: %q", out.String()) - } - } - var diagnostic bytes.Buffer - result := app.Execute(context.Background(), []string{"topics", "unknown"}, commandkit.Streams{Err: &diagnostic}) - if result.Kind != commandkit.OutcomeUsage || !strings.Contains(diagnostic.String(), "[command]") { - t.Fatalf("result=%#v diagnostic=%q", result, diagnostic.String()) - } -} diff --git a/integrations/ckconfig/performance_test.go b/integrations/ckconfig/performance_test.go deleted file mode 100644 index 6586902..0000000 --- a/integrations/ckconfig/performance_test.go +++ /dev/null @@ -1,200 +0,0 @@ -package ckconfig - -import ( - "context" - "fmt" - "io" - "strings" - "testing" -) - -var performanceFramedBytes []byte - -func BenchmarkPerformanceYAMLFraming(b *testing.B) { - fixtures := []struct { - name string - scope string - native []byte - }{ - {name: "4k_ordinary", scope: "app", native: performanceNativeDocument(4<<10, false)}, - {name: "1m_directive", scope: "promptkit", native: performanceNativeDocument(1<<20, true)}, - } - for _, fixture := range fixtures { - b.Run("framing/"+fixture.name, func(b *testing.B) { - framed, hasDirective := frameEffectiveYAML(fixture.scope, fixture.native) - if len(framed) <= len(fixture.native) || hasDirective != strings.Contains(fixture.name, "directive") { - b.Fatalf("framing fixture result has unexpected size or directive classification: native=%d framed=%d", len(fixture.native), len(framed)) - } - b.ReportAllocs() - b.ResetTimer() - for i := 0; i < b.N; i++ { - performanceFramedBytes, _ = frameEffectiveYAML(fixture.scope, fixture.native) - } - }) - b.Run("aggregate/"+fixture.name, func(b *testing.B) { - app := performanceShowApp(b, fixture.native, fixture.native) - args := []string{"config", "show", "--scope", "all"} - verifyConfigReport(b, app, args, len(fixture.native)*2) - 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("aggregate config show outcome = %#v", outcome) - } - } - }) - } - - single := performanceNativeDocument(4<<10, false) - app := performanceShowApp(b, single) - args := []string{"config", "show"} - b.Run("single_scope/4k_ordinary", func(b *testing.B) { - verifyConfigReport(b, app, args, len(single)) - 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("single-scope config show outcome = %#v", outcome) - } - } - }) -} - -func BenchmarkPerformanceConfigScopes(b *testing.B) { - for _, scopeCount := range []int{2, 32} { - suite := performanceConfigSuite(scopeCount, []ConfigOperation{ConfigValidate, ConfigShow}) - app := performanceConfigApp(b, suite) - selected := fmt.Sprintf("scope%02d", scopeCount-1) - for _, operation := range []struct { - name string - args []string - }{ - {name: "discovery", args: []string{"config", "scopes"}}, - {name: "named_selection", args: []string{"config", "validate", "--scope", selected}}, - {name: "aggregate_validation", args: []string{"config", "validate", "--scope", "all"}}, - } { - b.Run(fmt.Sprintf("%s/scopes_%d", operation.name, scopeCount), func(b *testing.B) { - verifyConfigReport(b, app, operation.args, 1) - 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(), operation.args, streams); outcome.Kind != OutcomeSuccess { - b.Fatalf("configuration operation outcome = %#v", outcome) - } - } - }) - } - } -} - -func BenchmarkPerformanceConfigRegistration(b *testing.B) { - for _, selectorCount := range []int{4, 64} { - suite := performanceSelectorSuite(selectorCount) - performanceConfigApp(b, suite) - b.Run(fmt.Sprintf("selectors_%d", selectorCount), func(b *testing.B) { - b.ReportAllocs() - for i := 0; i < b.N; i++ { - app, err := New(Identity{Name: "bench"}) - if err != nil { - b.Fatal(err) - } - if err := RegisterConfig(app, suite); err != nil { - b.Fatal(err) - } - } - }) - } -} - -func performanceNativeDocument(targetBytes int, directive bool) []byte { - preamble := "# generated native configuration\n" - if directive { - preamble = "%YAML 1.2\n---\n" - } - var body strings.Builder - body.Grow(targetBytes) - for index := 0; len(preamble)+body.Len() < targetBytes; index++ { - fmt.Fprintf(&body, "key%06d: value\n", index) - } - return []byte(preamble + body.String()) -} - -type performanceEffectiveWriter []byte - -func (document performanceEffectiveWriter) WriteEffective(writer io.Writer) error { - _, err := writer.Write(document) - return err -} - -func performanceShowApp(tb testing.TB, documents ...[]byte) *App { - tb.Helper() - scopes := make([]ConfigScope[struct{}], len(documents)) - for index, document := range documents { - name := "app" - if index > 0 { - name = fmt.Sprintf("scope%02d", index) - } - writer := performanceEffectiveWriter(document) - scopes[index] = ConfigScope[struct{}]{ - Name: name, - Operations: []ConfigOperation{ConfigShow}, - Acquire: func(context.Context, ConfigRequest, struct{}) (Acquired[ConfigView], error) { - return Acquired[ConfigView]{Value: ConfigView{Effective: writer}}, nil - }, - } - } - suite := ConfigSuite[struct{}]{Scopes: scopes} - return performanceConfigApp(tb, suite) -} - -func performanceConfigSuite(scopeCount int, operations []ConfigOperation) ConfigSuite[struct{}] { - scopes := make([]ConfigScope[struct{}], scopeCount) - for index := range scopes { - name := "app" - if index > 0 { - name = fmt.Sprintf("scope%02d", index) - } - scopes[index] = ConfigScope[struct{}]{ - Name: name, - Operations: append([]ConfigOperation(nil), operations...), - Acquire: func(context.Context, ConfigRequest, struct{}) (Acquired[ConfigView], error) { - return Acquired[ConfigView]{}, nil - }, - } - } - return ConfigSuite[struct{}]{Scopes: scopes} -} - -func performanceSelectorSuite(selectorCount int) ConfigSuite[struct{}] { - suite := performanceConfigSuite(1, []ConfigOperation{ConfigValidate}) - suite.Selectors = make([]Selector[struct{}], selectorCount) - for index := range suite.Selectors { - flag := ValueFlag(FlagMeta{Name: fmt.Sprintf("selector%02d", index)}, (*string)(nil), parseString, func(*struct{}, Supplied[string]) {}) - suite.Selectors[index] = Selector[struct{}]{Binding: flag, Scopes: []string{"app"}} - } - return suite -} - -func performanceConfigApp(tb testing.TB, suite ConfigSuite[struct{}]) *App { - tb.Helper() - app, err := New(Identity{Name: "bench"}) - if err != nil { - tb.Fatal(err) - } - if err := RegisterConfig(app, suite); err != nil { - tb.Fatal(err) - } - return app -} - -func verifyConfigReport(tb testing.TB, app *App, args []string, minimumBytes int) { - tb.Helper() - var output bytes.Buffer - outcome := app.Execute(context.Background(), args, Streams{Out: &output, Err: io.Discard}) - if outcome.Kind != OutcomeSuccess || output.Len() < minimumBytes { - tb.Fatalf("configuration report outcome = %#v, output bytes %d, want at least %d", outcome, output.Len(), minimumBytes) - } -} diff --git a/integrations/ckconfig/show_test.go b/integrations/ckconfig/show_test.go deleted file mode 100644 index 64c6db4..0000000 --- a/integrations/ckconfig/show_test.go +++ /dev/null @@ -1,94 +0,0 @@ -package commandkit - -import ( - "bytes" - "go.yaml.in/yaml/v4" - "io" - "reflect" - "strings" - "testing" -) - -func TestEffectiveYAMLFramingPreservesOneNativeDocument(t *testing.T) { - for _, native := range []string{ - "--- {setting: value}\n", "---\t{setting: value}\n", "--- value\n", - "--- |\n first\n second\n", "--- # authored\nsetting: value\n", - "setting: value\n", "%YAML 1.1\n--- {setting: value}\n", - } { - t.Run(native, func(t *testing.T) { - var want any - if err := yaml.Unmarshal([]byte(native), &want); err != nil { - t.Fatal(err) - } - framed, _ := frameEffectiveYAML("app", []byte(native)) - withoutLabel := bytes.Replace(framed, []byte("# scope: app\n"), nil, 1) - if !strings.HasPrefix(native, "---") && !strings.HasPrefix(native, "%") { - withoutLabel = bytes.TrimPrefix(withoutLabel, []byte("---\n")) - } - if !bytes.Equal(withoutLabel, []byte(native)) { - t.Fatalf("native content changed: %q", framed) - } - if !bytes.Contains(framed, []byte("# scope: app\n")) { - t.Fatalf("missing label: %q", framed) - } - decoder := yaml.NewDecoder(bytes.NewReader(framed)) - var got any - if err := decoder.Decode(&got); err != nil { - t.Fatal(err) - } - if !reflect.DeepEqual(got, want) { - t.Fatalf("got=%#v want=%#v", got, want) - } - if err := decoder.Decode(new(any)); err != io.EOF { - t.Fatalf("extra document: %v", err) - } - }) - } -} - -func TestEffectiveYAMLFramingPreservesBoundariesAndDirectives(t *testing.T) { - for _, test := range []struct { - name string - scope string - native string - directive bool - label string - }{ - {name: "no trailing newline", scope: "app", native: "value: plain", label: "# scope: app\n"}, - {name: "CRLF", scope: "app", native: "# authored\r\n---\r\nvalue: yes\r\n", label: "# scope: app\n"}, - {name: "directive preamble", scope: "app", native: "%YAML 1.1\n# authored\n---\nvalue: yes", directive: true, label: "# scope: app\n"}, - {name: "control character label", scope: "line\nbreak", native: "value: yes", label: "# scope: \"line\\nbreak\"\n"}, - } { - t.Run(test.name, func(t *testing.T) { - var want any - if err := yaml.Unmarshal([]byte(test.native), &want); err != nil { - t.Fatal(err) - } - framed, hasDirective := frameEffectiveYAML(test.scope, []byte(test.native)) - if hasDirective != test.directive { - t.Fatalf("hasDirective = %t, want %t", hasDirective, test.directive) - } - withoutLabel := bytes.Replace(framed, []byte(test.label), nil, 1) - if !strings.HasPrefix(test.native, "---") && !strings.HasPrefix(test.native, "%") { - withoutLabel = bytes.TrimPrefix(withoutLabel, []byte("---\n")) - } - if !bytes.Equal(withoutLabel, []byte(test.native)) { - t.Fatalf("native content changed: got %q want %q", withoutLabel, test.native) - } - if !bytes.Contains(framed, []byte(test.label)) { - t.Fatalf("missing scope label %q in %q", test.label, framed) - } - decoder := yaml.NewDecoder(bytes.NewReader(framed)) - var first any - if err := decoder.Decode(&first); err != nil { - t.Fatal(err) - } - if !reflect.DeepEqual(first, want) { - t.Fatalf("decoded value = %#v, want %#v", first, want) - } - if err := decoder.Decode(new(any)); err != io.EOF { - t.Fatalf("unexpected extra YAML document: %v", err) - } - }) - } -} diff --git a/integrations/ckprompt/cancellation_test.go b/integrations/ckprompt/cancellation_test.go deleted file mode 100644 index 85ed3db..0000000 --- a/integrations/ckprompt/cancellation_test.go +++ /dev/null @@ -1,53 +0,0 @@ -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()) - } - }) - } - } -} diff --git a/integrations/ckprompt/composition_test.go b/integrations/ckprompt/composition_test.go new file mode 100644 index 0000000..94a4f6f --- /dev/null +++ b/integrations/ckprompt/composition_test.go @@ -0,0 +1,110 @@ +package ckprompt_test + +import ( + "bytes" + "context" + "io" + "testing" + "testing/fstest" + + commandkit "gitea.maximumdirect.net/eric/CommandKit" + "gitea.maximumdirect.net/eric/CommandKit/integrations/ckconfig" + "gitea.maximumdirect.net/eric/CommandKit/integrations/ckprompt" + configkit "gitea.maximumdirect.net/eric/ConfigKit" + promptkit "gitea.maximumdirect.net/eric/promptkit" +) + +type compositionInput struct{ Campaign commandkit.Supplied[string] } +type compositionState struct{ token int } + +func TestPublicConfigurationAndDefinitionFamiliesRemainIndependent(t *testing.T) { + inputs, err := promptkit.FilesystemInputs("composition fixture", fstest.MapFS{ + "config/prompts.yml": &fstest.MapFile{Data: []byte(`prompts: + briefing: + "1": + messages: + - role: user + content: "Hello" + output: + format: text + validation_mode: none +backends: + local: + endpoint: "https://example.invalid/v1" +profiles: + default: + backend: local + model: example +`)}}, "config") + if err != nil { + t.Fatal(err) + } + engine, err := promptkit.NewEngine(promptkit.Config{Sources: promptkit.ConfigurationSources{Underlays: []configkit.Underlay{{Inputs: inputs}}}}) + if err != nil { + t.Fatalf("construct engine: %v", err) + } + + flag := commandkit.ValueFlag(commandkit.FlagMeta{Name: "campaign"}, (*string)(nil), func(value string) (string, error) { return value, nil }, + func(input **compositionInput, value commandkit.Supplied[string]) { (*input).Campaign = value }) + newInputCalls, newStateCalls := 0, 0 + newInput := func() *compositionInput { newInputCalls++; return &compositionInput{} } + newState := func() *compositionState { newStateCalls++; return &compositionState{token: newStateCalls} } + var appInput, engineInput *compositionInput + var appState, configEngineState *compositionState + appAcquisitions, engineScopeAcquisitions, loaderCalls := 0, 0, 0 + loadEngine := func(_ context.Context, _ ckconfig.Selection, input *compositionInput, state *compositionState) (*promptkit.Engine, error) { + loaderCalls++ + if !input.Campaign.Set || input.Campaign.Value != "spring" { + t.Errorf("engine loader input = %#v", input) + } + if loaderCalls == 1 { + engineInput, configEngineState = input, state + } + return engine, nil + } + appScope := ckconfig.Scope[*compositionInput, *compositionState]{ + Name: "app", Operations: []ckconfig.Operation{ckconfig.Validate}, + Acquire: func(_ context.Context, _ ckconfig.Request, input *compositionInput, state *compositionState) (commandkit.Acquired[ckconfig.View], error) { + appAcquisitions++ + appInput, appState = input, state + return commandkit.Acquired[ckconfig.View]{}, nil + }, + } + engineScope := ckprompt.ConfigScope(loadEngine) + acquireEngineScope := engineScope.Acquire + engineScope.Acquire = func(ctx context.Context, request ckconfig.Request, input *compositionInput, state *compositionState) (commandkit.Acquired[ckconfig.View], error) { + engineScopeAcquisitions++ + return acquireEngineScope(ctx, request, input, state) + } + configCommand := ckconfig.Command(ckconfig.Suite[*compositionInput, *compositionState]{ + NewInput: newInput, NewState: newState, + Selectors: []ckconfig.Selector[*compositionInput]{{Binding: flag, Scopes: []string{"app", "promptkit"}}}, + Scopes: []ckconfig.Scope[*compositionInput, *compositionState]{appScope, engineScope}, + }) + definitions := ckprompt.FromEngine(loadEngine) + definitions.NewInput = newInput + definitions.NewState = newState + definitions.Selectors = []commandkit.Flag[*compositionInput]{flag} + command, err := commandkit.New(commandkit.Identity{Name: "composition"}, configCommand, ckprompt.Command(definitions)) + if err != nil { + t.Fatal(err) + } + + var output bytes.Buffer + outcome := command.Execute(context.Background(), []string{"config", "validate", "--scope", "all", "--campaign", "spring"}, commandkit.Streams{Out: &output, Err: io.Discard}) + if outcome.Kind != commandkit.OutcomeSuccess || appAcquisitions != 1 || engineScopeAcquisitions != 1 || loaderCalls != 1 { + t.Fatalf("configuration run = (%#v, output=%q, counts=%d/%d/%d)", outcome, output.String(), appAcquisitions, engineScopeAcquisitions, loaderCalls) + } + if appInput == nil || engineInput == nil || appInput == engineInput || appState == nil || appState != configEngineState || appState.token != 1 { + t.Fatalf("scope values were not isolated/shared as selected: inputs=%p/%p states=%p/%p", appInput, engineInput, appState, configEngineState) + } + + output.Reset() + outcome = command.Execute(context.Background(), []string{"promptkit", "show", "profile", "default", "--campaign", "spring", "--format", "yaml"}, commandkit.Streams{Out: &output, Err: io.Discard}) + if outcome.Kind != commandkit.OutcomeSuccess || appAcquisitions != 1 || engineScopeAcquisitions != 1 || loaderCalls != 2 || !bytes.Contains(output.Bytes(), []byte("profile_id: default")) { + t.Fatalf("independent profile run = (%#v, output=%q, counts=%d/%d/%d)", outcome, output.String(), appAcquisitions, engineScopeAcquisitions, loaderCalls) + } + if newInputCalls != 3 || newStateCalls != 2 { + t.Fatalf("invocation factory calls input/state=%d/%d", newInputCalls, newStateCalls) + } +} diff --git a/integrations/ckprompt/examples/composed/main.go b/integrations/ckprompt/examples/composed/main.go index deb53de..0483579 100644 --- a/integrations/ckprompt/examples/composed/main.go +++ b/integrations/ckprompt/examples/composed/main.go @@ -5,10 +5,13 @@ import ( "errors" "fmt" "os" + "strconv" commandkit "gitea.maximumdirect.net/eric/CommandKit" + "gitea.maximumdirect.net/eric/CommandKit/integrations/ckconfig" + "gitea.maximumdirect.net/eric/CommandKit/integrations/ckprompt" configkit "gitea.maximumdirect.net/eric/ConfigKit" - "gitea.maximumdirect.net/eric/promptkit" + promptkit "gitea.maximumdirect.net/eric/promptkit" ) type campaign struct { @@ -23,20 +26,17 @@ type appConfiguration struct { Campaigns map[string]campaign `yaml:"campaigns"` } -type invocationCache struct { +type inspectionInput struct { + Campaign commandkit.Supplied[string] + Limit commandkit.Supplied[int] +} + +// runCache belongs to one invocation and is shared only where the application +// chooses to pass it to its loaders. +type runCache 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 @@ -48,16 +48,19 @@ type session struct { Summary string } -type sessionStore struct { - active string - records map[string]session +func campaignSelector() commandkit.Flag[*inspectionInput] { + return commandkit.ValueFlag(commandkit.FlagMeta{Name: "campaign", Usage: "Select campaign inputs."}, (*string)(nil), parseString, + func(input **inspectionInput, value commandkit.Supplied[string]) { (*input).Campaign = value }) } -func loadApplicationConfig(_ context.Context, _ commandkit.Selection, cache *invocationCache) (*configkit.Result[appConfiguration], error) { +func loadApplicationConfig(ctx context.Context, _ ckconfig.Selection, _ *inspectionInput, cache *runCache) (*configkit.Result[appConfiguration], error) { + if err := ctx.Err(); err != nil { + return nil, err + } if cache != nil && cache.application != nil { return cache.application, nil } - result, err := configkit.Load[appConfiguration](configkit.Mapping("composed app config", map[string]any{ + result, err := configkit.Load[appConfiguration](configkit.Mapping("composed application settings", 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"}, @@ -70,24 +73,12 @@ func loadApplicationConfig(_ context.Context, _ commandkit.Selection, cache *inv 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) +func loadPromptEngine(ctx context.Context, selection ckconfig.Selection, input *inspectionInput, cache *runCache) (*promptkit.Engine, error) { + application, err := loadApplicationConfig(ctx, selection, input, cache) if err != nil { return nil, err } - name, chosen, err := selectedCampaign(application.Config, selected) + name, chosen, err := selectedCampaign(application.Config, input.Campaign) if err != nil { return nil, err } @@ -111,104 +102,93 @@ func loadPromptEngine(ctx context.Context, selection commandkit.Selection, cache 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 selectedCampaign(configuration appConfiguration, supplied commandkit.Supplied[string]) (string, campaign, error) { + name := configuration.DefaultCampaign + if supplied.Set { + name = supplied.Value } + chosen, ok := configuration.Campaigns[name] + if !ok { + return "", campaign{}, fmt.Errorf("unknown campaign %q", name) + } + return name, chosen, nil } 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 - } + limitDefault := 0 + limitFlag := commandkit.ValueFlag(commandkit.FlagMeta{Name: "limit", Usage: "Override the application item limit."}, &limitDefault, strconv.Atoi, + func(input **inspectionInput, value commandkit.Supplied[int]) { (*input).Limit = value }) + inputFactory := func() *inspectionInput { return &inspectionInput{} } + stateFactory := func() *runCache { return &runCache{} } - definitionSuite, err := commandkit.NewPromptKitSuite(func(ctx context.Context, selection commandkit.Selection, input definitionContext) (*promptkit.Engine, error) { - return loadPromptEngine(ctx, selection, input.Cache, input.Campaign) + appScope := ckconfig.FromConfigKit("app", "Application settings.", loadApplicationConfig) + engineScope := ckprompt.ConfigScope(loadPromptEngine) + configCommand := ckconfig.Command(ckconfig.Suite[*inspectionInput, *runCache]{ + NewInput: inputFactory, + NewState: stateFactory, + CheckInput: func(scope string, _ ckconfig.Selection, input *inspectionInput) error { + if scope == "app" && input.Campaign.Set && !input.Limit.Set { + return errors.New("application campaign inspection requires --limit") + } + return nil + }, + Selectors: []ckconfig.Selector[*inspectionInput]{ + {Binding: campaignSelector(), Scopes: []string{"app", "promptkit"}}, + {Binding: limitFlag, Scopes: []string{"app"}}, + }, + Scopes: []ckconfig.Scope[*inspectionInput, *runCache]{appScope, engineScope}, }) - 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]{ + definitions := ckprompt.FromEngine(loadPromptEngine) + definitions.NewInput = inputFactory + definitions.NewState = stateFactory + definitions.Selectors = []commandkit.Flag[*inspectionInput]{campaignSelector()} + definitionCommand := ckprompt.Command(definitions) + + store := map[string]session{ + "session-42": {ID: "session-42", Campaign: "spring", Summary: "A synthetic planning session."}, + } + restore := 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 - })}, + Summary: "Restore a saved session.", + Flags: []commandkit.Flag[restoreInput]{commandkit.ValueFlag(commandkit.FlagMeta{Name: "campaign", Usage: "Campaign owning the session."}, (*string)(nil), parseString, + func(input *restoreInput, value commandkit.Supplied[string]) { input.Campaign = value })}, + Arguments: []commandkit.Argument[restoreInput]{commandkit.ValueArgument(commandkit.ArgumentMeta{Name: "ID", Usage: "Session identifier."}, true, parseString, + 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 := invocation.Context.Err(); err != nil { + return commandkit.Failure(err) + } + application, err := loadApplicationConfig(invocation.Context, ckconfig.Selection{}, nil, 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[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)} + } + if _, err := fmt.Fprintf(invocation.Streams.Out, "restored %s: %s\n", record.ID, record.Summary); err != nil { + return commandkit.Failure(err) + } + return commandkit.Success() }, }) - 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 + return commandkit.New( + commandkit.Identity{Name: "composed-example", Version: "1.0"}, + configCommand, + definitionCommand, + commandkit.Group("session", "Manage saved sessions.", restore), + ) } -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 parseString(raw string) (string, error) { return raw, nil } func main() { app, err := newApp() diff --git a/integrations/ckprompt/performance_test.go b/integrations/ckprompt/performance_test.go deleted file mode 100644 index e1e96d8..0000000 --- a/integrations/ckprompt/performance_test.go +++ /dev/null @@ -1,161 +0,0 @@ -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)]) - } -} diff --git a/integrations/ckprompt/promptkit.go b/integrations/ckprompt/promptkit.go deleted file mode 100644 index 7232a8b..0000000 --- a/integrations/ckprompt/promptkit.go +++ /dev/null @@ -1,478 +0,0 @@ -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 -} diff --git a/integrations/ckprompt/promptkit_scope_test.go b/integrations/ckprompt/promptkit_scope_test.go deleted file mode 100644 index 1f3d955..0000000 --- a/integrations/ckprompt/promptkit_scope_test.go +++ /dev/null @@ -1,220 +0,0 @@ -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) - } -} diff --git a/integrations/ckprompt/promptkit_test.go b/integrations/ckprompt/promptkit_test.go deleted file mode 100644 index becf526..0000000 --- a/integrations/ckprompt/promptkit_test.go +++ /dev/null @@ -1,450 +0,0 @@ -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) - } - } -} diff --git a/integrations/ckprompt/registration_test.go b/integrations/ckprompt/registration_test.go deleted file mode 100644 index a99609d..0000000 --- a/integrations/ckprompt/registration_test.go +++ /dev/null @@ -1,85 +0,0 @@ -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) - } -}