660 lines
32 KiB
Go
660 lines
32 KiB
Go
package commandkit_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) {
|
|
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
|
|
}
|