Demonstrate config diff in simple consumer

This commit is contained in:
2026-10-11 21:52:47 +00:00
parent 18ee987a88
commit 3cbb1f35b0
6 changed files with 109 additions and 8 deletions

View File

@@ -1,6 +1,6 @@
# Implement ConfigKit adoption and configuration comparison
Status: implementation in progress; Stages 1–8 are complete.
Status: implementation in progress; Stages 1–9 are complete.
## Scope and authority
@@ -445,9 +445,15 @@ without custom comparison wiring.
go run . config diff --config fixtures/before.yml --against-config fixtures/after.yml
```
**Completion evidence:** the complete consumer compiles in ckconfig's suite;
fixture comparison and unchanged default commands run without external services.
Composed state/cache and PromptKit changes belong only to Stage 10.
**Completion evidence — complete:** the simple consumer now uses ConfigKit file
sources when `--config` is supplied, preserves the in-memory default when it is
omitted, and forwards supplied overlays through ConfigKit options. The required
before/after fixtures and README invocation are present. Its assembled-app test
checks `changes.greeting: Goodbye`, equal fixture selections with empty
categories, default `config show`, default `greet`, and explicit empty config
selection failing instead of falling back. The documented diff, show, and greet
commands also run locally without external services; the ckconfig suite passes.
Composed state/cache and PromptKit changes remain for the following stage.
## Stage 10: Demonstrate composed selection and typed-input cache identity

View File

@@ -4,6 +4,7 @@ Run the maintained example with:
```sh
go run . config show
go run . config diff --config fixtures/before.yml --against-config fixtures/after.yml
go run . greet
go run . greet --count=0 --tag=first --tag second
```
@@ -11,4 +12,6 @@ go run . greet --count=0 --tag=first --tag second
The `config` family comes from one typed ConfigKit scope. The custom `greet`
command uses CommandKit's typed bindings and shows the difference between an
omitted `--count` and an explicitly supplied zero, plus ordered repeated tags.
The configuration is synthetic and stays in memory.
With no `--config`, application settings use the in-memory default. A supplied
configuration and overlay are passed to ConfigKit unchanged. The comparison
command loads the two local fixtures and reports `changes.greeting: Goodbye`.

View File

@@ -0,0 +1 @@
greeting: Goodbye

View File

@@ -0,0 +1 @@
greeting: Hello

View File

@@ -20,10 +20,19 @@ type greetInput struct {
Tags commandkit.Supplied[[]string]
}
func loadAppConfiguration(_ context.Context, _ ckconfig.Selection) (*configkit.Result[appConfiguration], error) {
return configkit.Load[appConfiguration](configkit.Mapping("simple application settings", map[string]any{
func loadAppConfiguration(_ context.Context, selection ckconfig.Selection) (*configkit.Result[appConfiguration], error) {
source := configkit.Mapping("simple application settings", map[string]any{
"greeting": "Hello",
}))
})
if selection.Config.Set {
source = configkit.File(selection.Config.Value)
}
options := configkit.Options[appConfiguration]{}
if selection.Overlay.Set {
options.Overlay = selection.Overlay.Value
options.Filesystem = &configkit.FilesystemContext{Root: ".", BaseDir: "."}
}
return configkit.Load(source, options)
}
func newApp() (*commandkit.App, error) {

View File

@@ -0,0 +1,81 @@
package main
import (
"bytes"
"context"
"io"
"testing"
commandkit "gitea.maximumdirect.net/eric/CommandKit"
"gitea.maximumdirect.net/eric/CommandKit/integrations/ckconfig"
"go.yaml.in/yaml/v4"
)
func TestSimpleAppComparesFixturesAndKeepsDefaultCommands(t *testing.T) {
app, err := newApp()
if err != nil {
t.Fatal(err)
}
var output bytes.Buffer
outcome := app.Execute(context.Background(), []string{"config", "diff", "--config", "fixtures/before.yml", "--against-config", "fixtures/after.yml"}, commandkit.Streams{Out: &output, Err: io.Discard})
if outcome.Kind != commandkit.OutcomeSuccess {
t.Fatalf("fixture comparison outcome = %#v", outcome)
}
var report yaml.Node
if err := yaml.Unmarshal(output.Bytes(), &report); err != nil {
t.Fatalf("comparison report is not YAML: %q: %v", output.String(), err)
}
changes := field(report.Content[0], "changes")
greeting := field(changes, "greeting")
if greeting == nil || greeting.Value != "Goodbye" {
t.Fatalf("fixture comparison report = %q", output.String())
}
output.Reset()
outcome = app.Execute(context.Background(), []string{"config", "diff", "--config", "fixtures/before.yml", "--against-config", "fixtures/before.yml"}, commandkit.Streams{Out: &output, Err: io.Discard})
if outcome.Kind != commandkit.OutcomeSuccess {
t.Fatalf("equal fixture comparison outcome = %#v", outcome)
}
if reportNode, err := decodeReport(output.Bytes()); err != nil || len(field(reportNode, "additions").Content) != 0 || len(field(reportNode, "changes").Content) != 0 || len(field(reportNode, "deletions").Content) != 0 {
t.Fatalf("equal fixture report = %q, decode error %v", output.String(), err)
}
output.Reset()
outcome = app.Execute(context.Background(), []string{"config", "show"}, commandkit.Streams{Out: &output, Err: io.Discard})
if outcome.Kind != commandkit.OutcomeSuccess || output.String() != "greeting: Hello\n" {
t.Fatalf("default show outcome/output = %#v / %q", outcome, output.String())
}
output.Reset()
outcome = app.Execute(context.Background(), []string{"greet"}, commandkit.Streams{Out: &output, Err: io.Discard})
if outcome.Kind != commandkit.OutcomeSuccess || len(output.String()) == 0 || output.String()[:5] != "Hello" {
t.Fatalf("default greeting outcome/output = %#v / %q", outcome, output.String())
}
}
func TestSimpleLoaderDoesNotReplaceExplicitEmptyConfiguration(t *testing.T) {
_, err := loadAppConfiguration(context.Background(), ckconfig.Selection{Config: commandkit.Supplied[string]{Set: true}})
if err == nil {
t.Fatal("explicit empty configuration fell back to the default source")
}
}
func field(mapping *yaml.Node, name string) *yaml.Node {
if mapping == nil || mapping.Kind != yaml.MappingNode {
return nil
}
for index := 0; index+1 < len(mapping.Content); index += 2 {
if mapping.Content[index].Value == name {
return mapping.Content[index+1]
}
}
return nil
}
func decodeReport(data []byte) (*yaml.Node, error) {
var document yaml.Node
if err := yaml.Unmarshal(data, &document); err != nil {
return nil, err
}
return document.Content[0], nil
}