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