From a54ff03aeff64848e6bf192a83a58357b662ea00 Mon Sep 17 00:00:00 2001 From: Eric Rakestraw Date: Sun, 8 Nov 2026 15:03:24 +0000 Subject: [PATCH] Cover shared debug binding composition --- docs/roadmap/implementation.md | 8 +++- shared_debug_external_test.go | 81 ++++++++++++++++++++++++++++++++++ 2 files changed, 88 insertions(+), 1 deletion(-) diff --git a/docs/roadmap/implementation.md b/docs/roadmap/implementation.md index 2215396..53b037e 100644 --- a/docs/roadmap/implementation.md +++ b/docs/roadmap/implementation.md @@ -1,6 +1,6 @@ # Shared debug binding implementation plan -Status: in progress; Stages 1–3 complete. +Status: in progress; Stages 1–4 complete. Baseline: `6727d22ce6e1ac668b56a3e6d8fa5f7a06e54d88`. Execution audience: Luna coding agent. @@ -185,6 +185,12 @@ parse, without changing unrelated input indexes, grammar, or failure handling. ## Stage 4 — Preserve composition, defaults, and alias behavior +**Complete.** Existing capture and rename machinery retains the boolean +identity and requiredness without production changes. Focused public tests +cover true defaults, explicit false, direct and captured requiredness, both +application methods, renaming in both directions, and an alias to the local +short spelling; capture and alias tests pass offline with Go 1.27.0. + Primary files: [flag_values.go](../../flag_values.go), [binding.go](../../binding.go), [flag_aliases.go](../../flag_aliases.go). diff --git a/shared_debug_external_test.go b/shared_debug_external_test.go index 0678471..9b79486 100644 --- a/shared_debug_external_test.go +++ b/shared_debug_external_test.go @@ -2,6 +2,7 @@ package commandkit_test import ( "context" + "errors" "io" "sync" "testing" @@ -122,3 +123,83 @@ func TestSharedDebugBindingAndReportingMode(t *testing.T) { t.Fatalf("operand after terminator: %#v %#v mode=%v", got, received, mode) } } + +func TestSharedDebugCapturedDefaultsAndRequiredness(t *testing.T) { + type inner struct{ debug commandkit.Supplied[bool] } + type outer struct{ flags commandkit.FlagValues[inner] } + for _, required := range []bool{false, true} { + flag := commandkit.BoolFlag(commandkit.FlagMeta{Name: "debug", Short: "d", Required: required}, true, func(i *inner, v commandkit.Supplied[bool]) { i.debug = v }) + captured := commandkit.CaptureFlags([]commandkit.Flag[inner]{flag}, func(i *outer) *commandkit.FlagValues[inner] { return &i.flags }) + var applied, supplied inner + var applyErr error + var mode commandkit.ReportingMode + cmd := commandkit.NewCommand(commandkit.CommandSpec[outer]{Name: "run", Flags: captured, Run: func(inv commandkit.Invocation, i outer) commandkit.Outcome { + mode = inv.ReportingMode() + applied = inner{} + supplied = inner{debug: commandkit.Supplied[bool]{Value: true, Set: true}} + applyErr = i.flags.ApplyTo(&applied, flag) + if err := i.flags.ApplySuppliedTo(&supplied, flag); err != nil && applyErr == nil { + applyErr = err + } + return commandkit.Success() + }}) + app, err := commandkit.New(commandkit.Identity{Name: "example"}, cmd, commandkit.FlagAliasFor("run", "-x", "-d")) + if err != nil { + t.Fatal(err) + } + result := app.Execute(context.Background(), []string{"run"}, commandkit.Streams{}) + if result.Kind != commandkit.OutcomeSuccess || mode != commandkit.ReportingModeOrdinary { + t.Fatalf("omission=%#v mode=%v", result, mode) + } + if required { + if !errors.Is(applyErr, commandkit.ErrFlagInput) { + t.Fatalf("missing required: %v", applyErr) + } + } else if applyErr != nil || !applied.debug.Value || applied.debug.Set || !supplied.debug.Value || !supplied.debug.Set { + t.Fatalf("defaults=%#v %#v %v", applied, supplied, applyErr) + } + result = app.Execute(context.Background(), []string{"run", "-x=false"}, commandkit.Streams{}) + if result.Kind != commandkit.OutcomeSuccess || applyErr != nil || applied.debug.Value || !applied.debug.Set || supplied.debug.Value || !supplied.debug.Set || mode != commandkit.ReportingModeOrdinary { + t.Fatalf("false=%#v %#v %#v mode=%v err=%v", result, applied, supplied, mode, applyErr) + } + } +} + +func TestSharedDebugDirectRequiredAndRenamedBinding(t *testing.T) { + type input struct{ debug commandkit.Supplied[bool] } + var got input + var mode commandkit.ReportingMode + original := commandkit.BoolFlag(commandkit.FlagMeta{Name: "debug", Required: true}, true, func(i *input, v commandkit.Supplied[bool]) { i.debug = v }) + required := commandkit.NewCommand(commandkit.CommandSpec[input]{Name: "required", Flags: []commandkit.Flag[input]{original}, Run: func(inv commandkit.Invocation, i input) commandkit.Outcome { + got = i + mode = inv.ReportingMode() + return commandkit.Success() + }}) + renamed := commandkit.NewCommand(commandkit.CommandSpec[input]{Name: "renamed", Flags: []commandkit.Flag[input]{commandkit.RenameFlag(original, "trace")}, Run: func(inv commandkit.Invocation, i input) commandkit.Outcome { + got = i + mode = inv.ReportingMode() + return commandkit.Success() + }}) + ordinary := commandkit.BoolFlag(commandkit.FlagMeta{Name: "local"}, false, func(i *input, v commandkit.Supplied[bool]) { i.debug = v }) + attached := commandkit.NewCommand(commandkit.CommandSpec[input]{Name: "attached", Flags: []commandkit.Flag[input]{commandkit.RenameFlag(ordinary, "debug")}, Run: func(inv commandkit.Invocation, i input) commandkit.Outcome { + got = i + mode = inv.ReportingMode() + return commandkit.Success() + }}) + app, err := commandkit.New(commandkit.Identity{Name: "example"}, required, renamed, attached) + if err != nil { + t.Fatal(err) + } + if r := app.Execute(context.Background(), []string{"required"}, commandkit.Streams{}); r.Kind != commandkit.OutcomeUsage { + t.Fatalf("required omission=%#v", r) + } + if r := app.Execute(context.Background(), []string{"required", "--debug=false"}, commandkit.Streams{}); r.Kind != commandkit.OutcomeSuccess || got.debug.Value || !got.debug.Set || mode != commandkit.ReportingModeOrdinary { + t.Fatalf("explicit false=%#v %#v", r, got) + } + if r := app.Execute(context.Background(), []string{"renamed", "--trace=false", "--debug"}, commandkit.Streams{}); r.Kind != commandkit.OutcomeSuccess || got.debug.Value || !got.debug.Set || mode != commandkit.ReportingModeDebug { + t.Fatalf("renamed=%#v %#v mode=%v", r, got, mode) + } + if r := app.Execute(context.Background(), []string{"attached", "--debug"}, commandkit.Streams{}); r.Kind != commandkit.OutcomeSuccess || !got.debug.Value || !got.debug.Set || mode != commandkit.ReportingModeDebug { + t.Fatalf("renamed to debug=%#v %#v mode=%v", r, got, mode) + } +}