From 3542422b2fc0faef196411cdb09f57ed995d71a4 Mon Sep 17 00:00:00 2001 From: Eric Rakestraw Date: Sun, 11 Oct 2026 21:47:16 +0000 Subject: [PATCH] Add ConfigKit comparison acquisition --- docs/roadmap/implementation.md | 26 +- .../ckconfig/comparison_adapter_test.go | 327 ++++++++++++++++++ integrations/ckconfig/configkit_adapter.go | 67 +++- .../ckconfig/contracts_adapter_test.go | 2 +- 4 files changed, 408 insertions(+), 14 deletions(-) create mode 100644 integrations/ckconfig/comparison_adapter_test.go diff --git a/docs/roadmap/implementation.md b/docs/roadmap/implementation.md index f02ee9d..e8526d3 100644 --- a/docs/roadmap/implementation.md +++ b/docs/roadmap/implementation.md @@ -1,10 +1,11 @@ # Implement ConfigKit adoption and configuration comparison -Status: implementation plan; no implementation stages are complete. +Status: implementation in progress; Stages 1–5 are complete. ## Scope and authority -Follow the stages in order, starting with Stage 1. Each stage is sized for one +Follow the stages in order, starting with the first stage without completion +evidence. Each stage is sized for one Luna implementation assignment. The [feature roadmap](configkit.md) owns the selected end state, operator behavior, target API, and scope limits. This document owns the steps to reach that state. Read [development guidance](../development.md) @@ -257,14 +258,19 @@ This is the selected internal coordination for the adapter, not a new policy for application-supplied pair callbacks. Tests should protect diagnostic retention and required lifecycle boundaries, not an incidental number of context polls. -**Completion evidence:** direct adapter tests with real ConfigKit loads for -public changes, base-versus-overlay loading, equal effective values, -defaults/resolution, and paired-safe concealed changes. Test right failure -retaining left warnings, wrapped load-error identity, nil results, and -cancellation between loads. Exercise a native -comparison error with a non-nil uninitialized result and retain available -diagnostics. Do not build a global fake comparator just to force an unreachable -branch; handler-level provider errors can carry diagnostics from both sides. +**Completion evidence — complete:** `FromConfigKit` now declares all five +operations and wires its typed comparison callback to sequentially load both +selected sides with the same state, then return `configkit.Diff` records. The +adapter keeps successful and `LoadError` diagnostics in left-to-right order, +labels comparison diagnostics by side, checks cancellation at the selected +boundaries, and wraps load/native comparison failures with their causes intact. +Direct tests use real ConfigKit loads for public changes, base-versus-overlay +loading, equal values after defaults and resolution, and safe concealed-change +records. They also cover both sides' diagnostics when the right load fails, +wrapped `LoadError` identity, nil results, cancellation between loads, and a +native Diff error from a non-nil uninitialized result while retaining available +diagnostics. Ordinary `Acquire` diagnostics and error identity remain covered +and unchanged. `go test -count=1 ./...` passes in ckconfig and ckp. ## Stage 6: Implement safe YAML snapshot and semantic-path placement helpers diff --git a/integrations/ckconfig/comparison_adapter_test.go b/integrations/ckconfig/comparison_adapter_test.go new file mode 100644 index 0000000..12b6a28 --- /dev/null +++ b/integrations/ckconfig/comparison_adapter_test.go @@ -0,0 +1,327 @@ +package ckconfig + +import ( + "bytes" + "context" + "errors" + "fmt" + "os" + "path/filepath" + "strings" + "testing" + + commandkit "gitea.maximumdirect.net/eric/CommandKit" + configkit "gitea.maximumdirect.net/eric/ConfigKit" +) + +type adapterSettings struct { + Greeting string `yaml:"greeting"` + Secret string `yaml:"secret"` +} + +func adapterDefinition() configkit.Definition[adapterSettings] { + return configkit.Definition[adapterSettings]{ + Defaults: func() adapterSettings { return adapterSettings{Greeting: "Hello"} }, + Validate: func(value *adapterSettings, validation *configkit.Validation) { + if value.Greeting == "Hello" { + validation.Warning(configkit.Keys("greeting"), "using the default greeting") + } + }, + Secret: func(path configkit.Path) bool { return path.String() == "/secret" }, + } +} + +func adapterSettingsLoad(source configkit.Source) (*configkit.Result[adapterSettings], error) { + return configkit.Load(source, configkit.Options[adapterSettings]{Definition: adapterDefinition()}) +} + +func comparisonSide(selection Selection, greeting string) ComparisonSide[struct{ Label string }] { + return ComparisonSide[struct{ Label string }]{ + Selection: selection, + Input: struct{ Label string }{Label: greeting}, + } +} + +func TestFromConfigKitCompareReturnsNativeChangesAndLoadsEqualSelections(t *testing.T) { + selection := Selection{Config: commandkit.Supplied[string]{Value: "same.yml", Set: true}} + var calls []struct { + selection Selection + input struct{ Label string } + state *int + } + scope := FromConfigKit[struct{ Label string }, *int, adapterSettings]("app", "Application configuration", func(_ context.Context, selected Selection, input struct{ Label string }, state *int) (*configkit.Result[adapterSettings], error) { + calls = append(calls, struct { + selection Selection + input struct{ Label string } + state *int + }{selected, input, state}) + *state++ + return adapterSettingsLoad(configkit.Mapping(input.Label, map[string]any{"greeting": input.Label})) + }) + state := new(int) + acquired, err := scope.Compare(context.Background(), ComparisonRequest[struct{ Label string }]{ + Left: comparisonSide(selection, "before"), + Right: comparisonSide(selection, "after"), + }, state) + if err != nil { + t.Fatal(err) + } + if len(calls) != 2 || calls[0].selection != selection || calls[1].selection != selection { + t.Fatalf("loader calls = %#v", calls) + } + if calls[0].input.Label != "before" || calls[1].input.Label != "after" || calls[0].state != state || calls[1].state != state || *state != 2 { + t.Fatalf("loader inputs/state = %#v, %#v, state=%d", calls[0], calls[1], *state) + } + if len(acquired.Value) != 1 || acquired.Value[0].Path.String() != "/greeting" || acquired.Value[0].Left.YAML != "before\n" || acquired.Value[0].Right.YAML != "after\n" { + t.Fatalf("native differences = %#v", acquired.Value) + } + + equal, err := scope.Compare(context.Background(), ComparisonRequest[struct{ Label string }]{ + Left: comparisonSide(selection, "same"), + Right: comparisonSide(selection, "same"), + }, state) + if err != nil { + t.Fatal(err) + } + if len(equal.Value) != 0 || len(calls) != 4 { + t.Fatalf("equal comparison records/calls = %#v / %d", equal.Value, len(calls)) + } +} + +func TestFromConfigKitCompareIgnoresDefaultAndResolutionHistoryForEqualValues(t *testing.T) { + type resolvedSettings struct { + Greeting string `yaml:"greeting"` + } + definition := configkit.Definition[resolvedSettings]{ + Defaults: func() resolvedSettings { return resolvedSettings{Greeting: "resolved"} }, + Resolve: func(value *resolvedSettings, resolution *configkit.Resolution) error { + if value.Greeting != "resolved" { + value.Greeting = "resolved" + resolution.Record(configkit.Keys("greeting"), configkit.ChangeNormalization, "canonical greeting") + } + return nil + }, + } + left, err := configkit.Load(configkit.NoSource(), configkit.Options[resolvedSettings]{Definition: definition}) + if err != nil { + t.Fatal(err) + } + right, err := configkit.Load(configkit.Mapping("right-source", map[string]any{"greeting": "source value"}), configkit.Options[resolvedSettings]{Definition: definition}) + if err != nil { + t.Fatal(err) + } + scope := FromConfigKit[struct{}, struct{}, resolvedSettings]("app", "Application configuration", func(_ context.Context, selected Selection, _ struct{}, _ struct{}) (*configkit.Result[resolvedSettings], error) { + if selected.Config.Value == "left" { + return left, nil + } + return right, nil + }) + acquired, err := scope.Compare(context.Background(), ComparisonRequest[struct{}]{ + Left: ComparisonSide[struct{}]{Selection: Selection{Config: commandkit.Supplied[string]{Value: "left", Set: true}}}, + Right: ComparisonSide[struct{}]{Selection: Selection{Config: commandkit.Supplied[string]{Value: "right", Set: true}}}, + }, struct{}{}) + if err != nil { + t.Fatal(err) + } + if left.Config != right.Config || left.Config.Greeting != "resolved" || len(acquired.Value) != 0 { + t.Fatalf("equal resolved values yielded differences: left=%#v right=%#v records=%#v", left.Config, right.Config, acquired.Value) + } +} + +func TestFromConfigKitCompareUsesBaseAndOverlaySelections(t *testing.T) { + dir := t.TempDir() + basePath := filepath.Join(dir, "base.yml") + overlayPath := filepath.Join(dir, "overlays", "testing.yml") + if err := os.WriteFile(basePath, []byte("greeting: Hello\n"), 0o600); err != nil { + t.Fatal(err) + } + if err := os.Mkdir(filepath.Dir(overlayPath), 0o700); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(overlayPath, []byte("greeting: Goodbye\n"), 0o600); err != nil { + t.Fatal(err) + } + loader := func(_ context.Context, selection Selection, _ struct{}, _ struct{}) (*configkit.Result[adapterSettings], error) { + options := configkit.LayeredOptions[adapterSettings]{Definition: adapterDefinition()} + if selection.Overlay.Set { + options.Overlay = selection.Overlay.Value + options.OverlayFilesystem = &configkit.FilesystemContext{Root: dir, BaseDir: dir} + } + return configkit.LoadLayered(configkit.Input{Source: configkit.File(selection.Config.Value)}, options) + } + scope := FromConfigKit[struct{}, struct{}, adapterSettings]("app", "Application configuration", loader) + acquired, err := scope.Compare(context.Background(), ComparisonRequest[struct{}]{ + Left: ComparisonSide[struct{}]{Selection: Selection{Config: commandkit.Supplied[string]{Value: basePath, Set: true}}}, + Right: ComparisonSide[struct{}]{Selection: Selection{ + Config: commandkit.Supplied[string]{Value: basePath, Set: true}, + Overlay: commandkit.Supplied[string]{Value: "testing", Set: true}, + }}, + }, struct{}{}) + if err != nil { + t.Fatal(err) + } + if len(acquired.Value) != 1 || acquired.Value[0].Path.String() != "/greeting" || acquired.Value[0].Left.YAML != "Hello\n" || acquired.Value[0].Right.YAML != "Goodbye\n" { + t.Fatalf("base/overlay differences = %#v", acquired.Value) + } +} + +func TestFromConfigKitCompareRetainsSideDiagnosticsAndLoadError(t *testing.T) { + left, err := adapterSettingsLoad(configkit.Mapping("left-source", map[string]any{"greeting": "Hello"})) + if err != nil { + t.Fatal(err) + } + _, loadErr := configkit.Load(configkit.Mapping("right-source", map[string]any{"unknown": true}), configkit.Options[adapterSettings]{Definition: adapterDefinition()}) + if loadErr == nil { + t.Fatal("invalid right source unexpectedly loaded") + } + wrapper := fmt.Errorf("application loader context: %w", loadErr) + scope := FromConfigKit[struct{}, struct{}, adapterSettings]("app", "Application configuration", func(_ context.Context, selected Selection, _ struct{}, _ struct{}) (*configkit.Result[adapterSettings], error) { + if selected.Config.Value == "left" { + return left, nil + } + return nil, wrapper + }) + acquired, gotErr := scope.Compare(context.Background(), ComparisonRequest[struct{}]{ + Left: ComparisonSide[struct{}]{Selection: Selection{Config: commandkit.Supplied[string]{Value: "left", Set: true}}}, + Right: ComparisonSide[struct{}]{Selection: Selection{Config: commandkit.Supplied[string]{Value: "right", Set: true}}}, + }, struct{}{}) + if !errors.Is(gotErr, loadErr) { + t.Fatalf("load error identity not preserved: %v", gotErr) + } + var typed *configkit.LoadError + if !errors.As(gotErr, &typed) || typed != loadErr { + t.Fatalf("load error type not preserved: %#v", typed) + } + if len(acquired.Diagnostics) != 2 || acquired.Diagnostics[0].Severity != commandkit.SeverityWarning || acquired.Diagnostics[1].Severity != commandkit.SeverityError { + t.Fatalf("retained diagnostics = %#v", acquired.Diagnostics) + } + var leftText, rightText bytes.Buffer + if err := acquired.Diagnostics[0].Write(&leftText); err != nil { + t.Fatal(err) + } + if err := acquired.Diagnostics[1].Write(&rightText); err != nil { + t.Fatal(err) + } + if !strings.Contains(leftText.String(), `Configuration scope "app" left:`) || !strings.Contains(leftText.String(), `"left-source"`) { + t.Fatalf("left diagnostic lost side/source identity: %q", leftText.String()) + } + if !strings.Contains(rightText.String(), `Configuration scope "app" right:`) || !strings.Contains(rightText.String(), `"right-source"`) { + t.Fatalf("right diagnostic lost side/source identity: %q", rightText.String()) + } +} + +func TestFromConfigKitCompareStopsAfterLeftLoadFailure(t *testing.T) { + _, loadErr := configkit.Load(configkit.Mapping("left-source", map[string]any{"unknown": true}), configkit.Options[adapterSettings]{Definition: adapterDefinition()}) + if loadErr == nil { + t.Fatal("invalid left source unexpectedly loaded") + } + rightCalled := false + scope := FromConfigKit[struct{}, struct{}, adapterSettings]("app", "Application configuration", func(_ context.Context, selected Selection, _ struct{}, _ struct{}) (*configkit.Result[adapterSettings], error) { + if selected.Config.Value == "left" { + return nil, loadErr + } + rightCalled = true + return adapterSettingsLoad(configkit.Mapping("right-source", map[string]any{"greeting": "Goodbye"})) + }) + acquired, err := scope.Compare(context.Background(), ComparisonRequest[struct{}]{ + Left: ComparisonSide[struct{}]{Selection: Selection{Config: commandkit.Supplied[string]{Value: "left", Set: true}}}, + Right: ComparisonSide[struct{}]{Selection: Selection{Config: commandkit.Supplied[string]{Value: "right", Set: true}}}, + }, struct{}{}) + if !errors.Is(err, loadErr) || rightCalled { + t.Fatalf("left failure/right load = %v / %v", err, rightCalled) + } + var typed *configkit.LoadError + if !errors.As(err, &typed) || typed != loadErr || len(acquired.Diagnostics) != 1 || acquired.Diagnostics[0].Severity != commandkit.SeverityError { + t.Fatalf("left failure identity/diagnostics = %v / %#v", err, acquired.Diagnostics) + } +} + +func TestFromConfigKitCompareCancellationStopsBeforeRightLoad(t *testing.T) { + left, err := adapterSettingsLoad(configkit.Mapping("left-source", map[string]any{"greeting": "Hello"})) + if err != nil { + t.Fatal(err) + } + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + rightCalled := false + scope := FromConfigKit[struct{}, struct{}, adapterSettings]("app", "Application configuration", func(_ context.Context, selection Selection, _ struct{}, _ struct{}) (*configkit.Result[adapterSettings], error) { + if selection.Config.Value == "left" { + cancel() + return left, nil + } + rightCalled = true + return adapterSettingsLoad(configkit.Mapping("right-source", map[string]any{"greeting": "Goodbye"})) + }) + acquired, err := scope.Compare(ctx, ComparisonRequest[struct{}]{ + Left: ComparisonSide[struct{}]{Selection: Selection{Config: commandkit.Supplied[string]{Value: "left", Set: true}}}, + Right: ComparisonSide[struct{}]{Selection: Selection{Config: commandkit.Supplied[string]{Value: "right", Set: true}}}, + }, struct{}{}) + if !errors.Is(err, context.Canceled) || rightCalled { + t.Fatalf("cancellation/right load = %v / %v", err, rightCalled) + } + if len(acquired.Diagnostics) != 1 || acquired.Diagnostics[0].Severity != commandkit.SeverityWarning { + t.Fatalf("left diagnostics lost on cancellation: %#v", acquired.Diagnostics) + } +} + +func TestFromConfigKitCompareRejectsNilResultAndRetainsNativeDiffFailureDiagnostics(t *testing.T) { + t.Run("nil result", func(t *testing.T) { + scope := FromConfigKit[struct{}, struct{}, adapterSettings]("app", "Application configuration", func(context.Context, Selection, struct{}, struct{}) (*configkit.Result[adapterSettings], error) { + return nil, nil + }) + _, err := scope.Compare(context.Background(), ComparisonRequest[struct{}]{}, struct{}{}) + if err == nil || !strings.Contains(err.Error(), `"app" left`) || !strings.Contains(err.Error(), "nil result") { + t.Fatalf("nil left result error = %v", err) + } + }) + + t.Run("native diff error", func(t *testing.T) { + left, err := adapterSettingsLoad(configkit.Mapping("left-source", map[string]any{"greeting": "Hello"})) + if err != nil { + t.Fatal(err) + } + scope := FromConfigKit[struct{}, struct{}, adapterSettings]("app", "Application configuration", func(_ context.Context, selection Selection, _ struct{}, _ struct{}) (*configkit.Result[adapterSettings], error) { + if selection.Config.Value == "left" { + return left, nil + } + return &configkit.Result[adapterSettings]{}, nil + }) + acquired, err := scope.Compare(context.Background(), ComparisonRequest[struct{}]{ + Left: ComparisonSide[struct{}]{Selection: Selection{Config: commandkit.Supplied[string]{Value: "left", Set: true}}}, + Right: ComparisonSide[struct{}]{Selection: Selection{Config: commandkit.Supplied[string]{Value: "right", Set: true}}}, + }, struct{}{}) + if err == nil || !strings.Contains(err.Error(), "native diff") { + t.Fatalf("native diff error = %v", err) + } + if len(acquired.Diagnostics) != 1 || acquired.Diagnostics[0].Severity != commandkit.SeverityWarning { + t.Fatalf("native diff failure lost diagnostics: %#v", acquired.Diagnostics) + } + }) +} + +func TestFromConfigKitComparePreservesConcealedNativeDifference(t *testing.T) { + left, err := adapterSettingsLoad(configkit.Mapping("left-source", map[string]any{"secret": "old"})) + if err != nil { + t.Fatal(err) + } + right, err := adapterSettingsLoad(configkit.Mapping("right-source", map[string]any{"secret": "new"})) + if err != nil { + t.Fatal(err) + } + scope := FromConfigKit[struct{}, struct{}, adapterSettings]("app", "Application configuration", func(_ context.Context, selected Selection, _ struct{}, _ struct{}) (*configkit.Result[adapterSettings], error) { + if selected.Config.Value == "left" { + return left, nil + } + return right, nil + }) + acquired, err := scope.Compare(context.Background(), ComparisonRequest[struct{}]{ + Left: ComparisonSide[struct{}]{Selection: Selection{Config: commandkit.Supplied[string]{Value: "left", Set: true}}}, + Right: ComparisonSide[struct{}]{Selection: Selection{Config: commandkit.Supplied[string]{Value: "right", Set: true}}}, + }, struct{}{}) + if err != nil { + t.Fatal(err) + } + if len(acquired.Value) != 1 || !acquired.Value[0].HiddenChanged || !acquired.Value[0].Left.Redacted || !acquired.Value[0].Right.Redacted || acquired.Value[0].Left.YAML != acquired.Value[0].Right.YAML { + t.Fatalf("concealed native difference was altered: %#v", acquired.Value) + } +} diff --git a/integrations/ckconfig/configkit_adapter.go b/integrations/ckconfig/configkit_adapter.go index 489be06..0f9decf 100644 --- a/integrations/ckconfig/configkit_adapter.go +++ b/integrations/ckconfig/configkit_adapter.go @@ -11,7 +11,7 @@ import ( configkit "gitea.maximumdirect.net/eric/ConfigKit" ) -// FromConfigKit adapts a typed ConfigKit loader to all four native +// FromConfigKit adapts a typed ConfigKit loader to all five native // configuration operations. The loader receives the operation's explicit // Selection, initialized input, and shared invocation state, and runs only when // an installed operation selects this scope. Construction and help do not call @@ -24,7 +24,7 @@ func FromConfigKit[I, S, T any]( scope := Scope[I, S]{ Name: name, Summary: summary, - Operations: []Operation{Validate, Show, Sources, Explain}, + Operations: []Operation{Validate, Show, Sources, Explain, Diff}, } if load == nil { return WithScopeError(scope, fmt.Errorf("ConfigKit scope %q loader must not be nil", name)) @@ -49,10 +49,71 @@ func FromConfigKit[I, S, T any]( Diagnostics: configKitDiagnostics(name, sources, result.Diagnostics()), }, nil } + scope.Compare = func(ctx context.Context, request ComparisonRequest[I], state S) (commandkit.Acquired[[]configkit.Difference], error) { + if err := ctx.Err(); err != nil { + return commandkit.Acquired[[]configkit.Difference]{}, fmt.Errorf("ConfigKit scope %q comparison before left load: %w", name, err) + } + + left, leftDiagnostics, err := loadComparisonSide(ctx, name, "left", load, request.Left, state) + diagnostics := append([]commandkit.Diagnostic(nil), leftDiagnostics...) + if err != nil { + return commandkit.Acquired[[]configkit.Difference]{Diagnostics: diagnostics}, err + } + + if err := ctx.Err(); err != nil { + return commandkit.Acquired[[]configkit.Difference]{Diagnostics: diagnostics}, fmt.Errorf("ConfigKit scope %q comparison after left load: %w", name, err) + } + + right, rightDiagnostics, err := loadComparisonSide(ctx, name, "right", load, request.Right, state) + diagnostics = append(diagnostics, rightDiagnostics...) + if err != nil { + return commandkit.Acquired[[]configkit.Difference]{Diagnostics: diagnostics}, err + } + + if err := ctx.Err(); err != nil { + return commandkit.Acquired[[]configkit.Difference]{Diagnostics: diagnostics}, fmt.Errorf("ConfigKit scope %q comparison before native diff: %w", name, err) + } + + differences, err := configkit.Diff(left, right) + if err != nil { + return commandkit.Acquired[[]configkit.Difference]{Diagnostics: diagnostics}, fmt.Errorf("ConfigKit scope %q native diff: %w", name, err) + } + return commandkit.Acquired[[]configkit.Difference]{Value: differences, Diagnostics: diagnostics}, nil + } return scope } +func loadComparisonSide[I, S, T any]( + ctx context.Context, + scope, side string, + load func(context.Context, Selection, I, S) (*configkit.Result[T], error), + request ComparisonSide[I], + state S, +) (*configkit.Result[T], []commandkit.Diagnostic, error) { + result, err := load(ctx, request.Selection, request.Input, state) + if err != nil { + var loadError *configkit.LoadError + var diagnostics []commandkit.Diagnostic + if errors.As(err, &loadError) && loadError != nil { + diagnostics = configKitComparisonDiagnostics(scope, side, loadError.Sources(), loadError.Diagnostics()) + } + return nil, diagnostics, fmt.Errorf("ConfigKit scope %q %s load: %w", scope, side, err) + } + if result == nil { + return nil, nil, fmt.Errorf("ConfigKit scope %q %s loader returned a nil result", scope, side) + } + return result, configKitComparisonDiagnostics(scope, side, result.Sources(), result.Diagnostics()), nil +} + func configKitDiagnostics(scope string, sources []configkit.SourceInfo, records []configkit.Diagnostic) []commandkit.Diagnostic { + return configKitDiagnosticsWithHeading("Configuration scope "+strconv.Quote(scope)+":\n", sources, records) +} + +func configKitComparisonDiagnostics(scope, side string, sources []configkit.SourceInfo, records []configkit.Diagnostic) []commandkit.Diagnostic { + return configKitDiagnosticsWithHeading("Configuration scope "+strconv.Quote(scope)+" "+side+":\n", sources, records) +} + +func configKitDiagnosticsWithHeading(heading string, sources []configkit.SourceInfo, records []configkit.Diagnostic) []commandkit.Diagnostic { if len(records) == 0 { return nil } @@ -72,7 +133,7 @@ func configKitDiagnostics(scope string, sources []configkit.SourceInfo, records return []commandkit.Diagnostic{{ Severity: severity, Write: func(writer io.Writer) error { - if err := writeDiagnosticText(writer, "Configuration scope "+strconv.Quote(scope)+":\n"); err != nil { + if err := writeDiagnosticText(writer, heading); err != nil { return err } if len(sources) > 0 { diff --git a/integrations/ckconfig/contracts_adapter_test.go b/integrations/ckconfig/contracts_adapter_test.go index 37aa47d..62287c4 100644 --- a/integrations/ckconfig/contracts_adapter_test.go +++ b/integrations/ckconfig/contracts_adapter_test.go @@ -80,7 +80,7 @@ func TestFromConfigKitPassesInvocationValuesAndNativeFacets(t *testing.T) { st.Calls++ return result, nil }) - if len(scope.Operations) != 4 || scope.Operations[0] != Validate || scope.Operations[1] != Show || scope.Operations[2] != Sources || scope.Operations[3] != Explain { + if len(scope.Operations) != 5 || scope.Operations[0] != Validate || scope.Operations[1] != Show || scope.Operations[2] != Sources || scope.Operations[3] != Explain || scope.Operations[4] != Diff { t.Fatalf("operations = %#v", scope.Operations) } if gotContext != nil {