From 42af1da1422d85655ed663148f6f4830feb82955 Mon Sep 17 00:00:00 2001 From: Eric Rakestraw Date: Sat, 7 Nov 2026 04:54:44 +0000 Subject: [PATCH] Update the simple configuration consumer --- docs/roadmap/implementation.md | 7 ++- .../ckconfig/examples/simple/README.md | 10 ++++ .../examples/simple/fixtures/invalid.yml | 3 ++ integrations/ckconfig/examples/simple/main.go | 13 ++++- .../ckconfig/examples/simple/main_test.go | 54 +++++++++++++++++++ 5 files changed, 84 insertions(+), 3 deletions(-) create mode 100644 integrations/ckconfig/examples/simple/fixtures/invalid.yml diff --git a/docs/roadmap/implementation.md b/docs/roadmap/implementation.md index 366e42b..29df0b7 100644 --- a/docs/roadmap/implementation.md +++ b/docs/roadmap/implementation.md @@ -1,6 +1,6 @@ # Shared reporting and PromptKit v0.20.0 implementation plan -Status: implementation in progress; Stages 1–20 complete. +Status: implementation in progress; Stages 1–21 complete. ## Scope, authority, and execution @@ -756,6 +756,11 @@ snapshots and upstream validation-rule tests. ## Stage 21: Simple consumer and core/configuration API exercise +**Complete.** The simple consumer opts into native overrides and demonstrates +the new inspection and failure workflows. The core consumer's structured report +and template remain exercised. Core example and ckconfig workspace tests passed +with Go 1.27.0. + Update the maintained [simple ckconfig consumer](../../integrations/ckconfig/examples/simple) to install an explicitly configured FromConfigKit scope with Overrides enabled, replacing AppCommand convenience wiring there. Append the selected native CLI diff --git a/integrations/ckconfig/examples/simple/README.md b/integrations/ckconfig/examples/simple/README.md index a90b745..ce92c89 100644 --- a/integrations/ckconfig/examples/simple/README.md +++ b/integrations/ckconfig/examples/simple/README.md @@ -11,7 +11,12 @@ Run the maintained example with: go run . config show go run . config show --config fixtures/before.yml --verbose go run . config show --config fixtures/before.yml --quiet +go run . config explain --list --under /schema +go run . config show --path /greeting --set '/greeting="Hi"' go run . config diff --config fixtures/before.yml --against-config fixtures/after.yml +go run . config diff --set '/greeting="Hi"' --details --check +go run . config validate --config fixtures/invalid.yml --details --severity error +go run . config sources --config fixtures/invalid.yml --source-id source-1 go run . greet go run . greet --count=0 --tag=first --tag second ``` @@ -25,3 +30,8 @@ command loads the two local fixtures and reports `changes.greeting: Goodbye`. The before fixture uses schema version 1, which ConfigKit reports as a provider warning in verbose mode. Ordinary mode hides the warning while preserving the effective YAML report; quiet mode suppresses that report. +The invalid fixture demonstrates a failed-load report and captured source +inventory; `source-1` is the ID from that fixture's acquisition. The scope opts +in to transient overrides and passes them into its normal ConfigKit load. The +diff example compares an overridden left side with ordinary configuration on +the right and returns status 3 because the values differ. diff --git a/integrations/ckconfig/examples/simple/fixtures/invalid.yml b/integrations/ckconfig/examples/simple/fixtures/invalid.yml new file mode 100644 index 0000000..455a1af --- /dev/null +++ b/integrations/ckconfig/examples/simple/fixtures/invalid.yml @@ -0,0 +1,3 @@ +schema: + version: 2 +greeting: [invalid] diff --git a/integrations/ckconfig/examples/simple/main.go b/integrations/ckconfig/examples/simple/main.go index b899018..7cf06d0 100644 --- a/integrations/ckconfig/examples/simple/main.go +++ b/integrations/ckconfig/examples/simple/main.go @@ -49,10 +49,15 @@ func loadAppConfiguration(_ context.Context, selection ckconfig.Selection) (*con options.Overlay = selection.Overlay.Value options.Filesystem = &configkit.FilesystemContext{Root: ".", BaseDir: "."} } + options.Overrides = append(options.Overrides, selection.Overrides...) return configkit.Load(source, options) } func newApp() (*commandkit.App, error) { + return newAppWithLoader(loadAppConfiguration) +} + +func newAppWithLoader(load func(context.Context, ckconfig.Selection) (*configkit.Result[appConfiguration], error)) (*commandkit.App, error) { countDefault := 1 tagDefaults := []string{"example"} greet := commandkit.NewCommand(commandkit.CommandSpec[greetInput]{ @@ -68,7 +73,7 @@ func newApp() (*commandkit.App, error) { }, Run: func(invocation commandkit.Invocation, input greetInput) commandkit.Outcome { reporter := invocation.Reporter() - configuration, err := loadAppConfiguration(invocation.Context, ckconfig.Selection{}) + configuration, err := load(invocation.Context, ckconfig.Selection{}) if err != nil { return commandkit.Failure("Could not load application configuration.").WithCause(err) } @@ -87,9 +92,13 @@ func newApp() (*commandkit.App, error) { return commandkit.Success() }, }) + scope := ckconfig.FromConfigKit("app", "Application configuration.", func(ctx context.Context, selection ckconfig.Selection, _, _ struct{}) (*configkit.Result[appConfiguration], error) { + return load(ctx, selection) + }) + scope.Features.Overrides = true return commandkit.New( commandkit.Identity{Name: "simple-example", Version: "1.0"}, - ckconfig.AppCommand(loadAppConfiguration), + ckconfig.Command(ckconfig.Suite[struct{}, struct{}]{Scopes: []ckconfig.Scope[struct{}, struct{}]{scope}}), greet, ) } diff --git a/integrations/ckconfig/examples/simple/main_test.go b/integrations/ckconfig/examples/simple/main_test.go index 0549bc6..e994dc1 100644 --- a/integrations/ckconfig/examples/simple/main_test.go +++ b/integrations/ckconfig/examples/simple/main_test.go @@ -3,12 +3,14 @@ package main import ( "bytes" "context" + "errors" "io" "strings" "testing" "go.maximumdirect.net/ckconfig" commandkit "go.maximumdirect.net/commandkit" + configkit "go.maximumdirect.net/configkit" "go.yaml.in/yaml/v4" ) @@ -147,3 +149,55 @@ func decodeReport(data []byte) (*yaml.Node, error) { } return document.Content[0], nil } + +func TestSimpleInspectionAndIndependentOverrides(t *testing.T) { + outcome, output, _ := executeSimple(t, "config", "explain", "--list", "--under=/schema", "--format=json") + if outcome.Kind != commandkit.OutcomeSuccess || !strings.Contains(output, `"path": "/schema/version"`) { + t.Fatalf("discovery = %#v %q", outcome, output) + } + outcome, output, _ = executeSimple(t, "config", "show", "--path=/greeting", "--set=/greeting=\"Hi\"") + if outcome.Kind != commandkit.OutcomeSuccess || output != "Hi\n" { + t.Fatalf("focused override = %#v %q", outcome, output) + } + outcome, output, _ = executeSimple(t, "config", "diff", "--set=/greeting=\"Hi\"", "--details", "--check", "--format=json") + if outcome.Kind != commandkit.OutcomeDomain || outcome.Status != 3 || !strings.Contains(output, `"before"`) || !strings.Contains(output, `"after"`) { + t.Fatalf("independent diff = %#v %q", outcome, output) + } + outcome, output, _ = executeSimple(t, "config", "diff", "--set=/greeting=\"Hi\"", "--against-set=/greeting=\"Hi\"", "--details", "--check", "--format=json") + if outcome.Kind != commandkit.OutcomeSuccess || !strings.Contains(output, `"differences": []`) { + t.Fatalf("equal overrides = %#v %q", outcome, output) + } +} + +func TestSimpleFailedLoadReportsEvidence(t *testing.T) { + _, failure := loadAppConfiguration(context.Background(), ckconfig.Selection{Config: commandkit.Supplied[string]{Value: "fixtures/invalid.yml", Set: true}}) + var native *configkit.LoadError + if !errors.As(failure, &native) || len(native.Sources()) == 0 { + t.Fatalf("fixture failure = %v", failure) + } + outcome, output, _ := executeSimple(t, "config", "validate", "--config=fixtures/invalid.yml", "--details", "--severity=error", "--format=json") + if outcome.Kind != commandkit.OutcomeOperational || !strings.Contains(output, `"state": "present"`) || !strings.Contains(output, `"severity": "error"`) { + t.Fatalf("detailed failure = %#v %q", outcome, output) + } + outcome, output, _ = executeSimple(t, "config", "sources", "--config=fixtures/invalid.yml", "--source-id="+native.Sources()[0].ID, "--format=json") + if outcome.Kind != commandkit.OutcomeOperational || !strings.Contains(output, `"id":`) || !strings.Contains(output, "invalid.yml") { + t.Fatalf("source failure = %#v %q", outcome, output) + } +} + +func TestSimpleMetadataCommandsDoNotLoadConfiguration(t *testing.T) { + loads := 0 + app, err := newAppWithLoader(func(context.Context, ckconfig.Selection) (*configkit.Result[appConfiguration], error) { + loads++ + return nil, errors.New("unexpected load") + }) + if err != nil { + t.Fatal(err) + } + for _, args := range [][]string{{"version"}, {"config", "scopes"}, {"config", "explain"}, {"config", "show", "--help"}} { + result := app.Execute(context.Background(), args, commandkit.Streams{Out: io.Discard, Err: io.Discard}) + if result.Kind != commandkit.OutcomeSuccess || loads != 0 { + t.Fatalf("%v: %#v loads=%d", args, result, loads) + } + } +}