From bbbc50e8986e699220bb755248e5ba6f0e642bb0 Mon Sep 17 00:00:00 2001 From: wydrox <79707825+wydrox@users.noreply.github.com> Date: Mon, 20 Apr 2026 18:51:43 +0200 Subject: [PATCH] Add round-trip parity test for config CLI/TUI surfaces Enforces every exported config.Config field has both a matching flag on `martmart config set` and a reference in internal/tui/config.go, so a new field cannot silently ship without being editable from both channels. --- internal/commands/config_roundtrip_test.go | 100 +++++++++++++++++++++ 1 file changed, 100 insertions(+) create mode 100644 internal/commands/config_roundtrip_test.go diff --git a/internal/commands/config_roundtrip_test.go b/internal/commands/config_roundtrip_test.go new file mode 100644 index 0000000..f6f2238 --- /dev/null +++ b/internal/commands/config_roundtrip_test.go @@ -0,0 +1,100 @@ +package commands + +import ( + "os" + "reflect" + "sort" + "strings" + "testing" + + "github.com/wydrox/martmart-cli/internal/config" +) + +// configFieldFlagMap lists every exported field on config.Config together with +// the CLI flag that must exist on `martmart config set` to mutate it. Adding a +// new field to config.Config without updating this map (or the CLI/TUI wiring +// it points at) will fail TestConfigFieldsCovered, TestConfigStructHasCLISetterFlag, +// or TestConfigStructIsReferencedByTUI. +var configFieldFlagMap = map[string]string{ + "DefaultProvider": "default-provider", + "RateLimitRPS": "rate-limit-rps", + "RateLimitBurst": "rate-limit-burst", + "OpenAIAPIKey": "openai-api-key", + "OpenAIModel": "openai-model", + "OpenAIVoice": "openai-voice", + "OpenAILanguage": "openai-language", + "OpenAITranscriptionModel": "openai-transcription-model", + "OpenAIVoiceSpeed": "openai-voice-speed", + "OpenAIInputDevice": "openai-input-device", + "OpenAIOutputDevice": "openai-output-device", +} + +// tuiConfigSourcePath is relative to the commands package directory; `go test` +// runs with CWD set to the package under test. +const tuiConfigSourcePath = "../tui/config.go" + +func configStructFieldNames(t *testing.T) []string { + t.Helper() + typ := reflect.TypeOf(config.Config{}) + names := make([]string, 0, typ.NumField()) + for i := 0; i < typ.NumField(); i++ { + f := typ.Field(i) + if !f.IsExported() { + continue + } + names = append(names, f.Name) + } + return names +} + +// TestConfigFieldsCovered enforces set equality between exported config.Config +// fields and configFieldFlagMap. Adding a field to Config without updating the +// map (or leaving stale map entries after a field removal) fails here first. +func TestConfigFieldsCovered(t *testing.T) { + structFields := configStructFieldNames(t) + mapFields := make([]string, 0, len(configFieldFlagMap)) + for k := range configFieldFlagMap { + mapFields = append(mapFields, k) + } + sort.Strings(structFields) + sort.Strings(mapFields) + + if !reflect.DeepEqual(structFields, mapFields) { + t.Fatalf("config.Config exported fields and configFieldFlagMap are out of sync.\n struct fields: %v\n map fields: %v\nUpdate configFieldFlagMap (and the CLI/TUI wiring) to match.", structFields, mapFields) + } +} + +// TestConfigStructHasCLISetterFlag verifies every config.Config field has a +// matching flag registered on `martmart config set`. Catches fields added to +// Config but not to newConfigSetCmd. +func TestConfigStructHasCLISetterFlag(t *testing.T) { + cmd := newConfigSetCmd() + for _, fieldName := range configStructFieldNames(t) { + flagName, ok := configFieldFlagMap[fieldName] + if !ok { + // TestConfigFieldsCovered will flag this; skip here to keep the + // failure focused on CLI wiring. + continue + } + if cmd.Flag(flagName) == nil { + t.Errorf("config.Config field %q has no corresponding --%s flag on `config set`", fieldName, flagName) + } + } +} + +// TestConfigStructIsReferencedByTUI verifies every config.Config field is +// mentioned in internal/tui/config.go source, so fields can be edited through +// the interactive editor. This is a heuristic (substring match) but reliably +// catches fields added to Config but not plumbed into the TUI editor. +func TestConfigStructIsReferencedByTUI(t *testing.T) { + raw, err := os.ReadFile(tuiConfigSourcePath) + if err != nil { + t.Fatalf("reading %s: %v", tuiConfigSourcePath, err) + } + src := string(raw) + for _, fieldName := range configStructFieldNames(t) { + if !strings.Contains(src, fieldName) { + t.Errorf("config.Config field %q is not referenced in %s; TUI editor is missing this field", fieldName, tuiConfigSourcePath) + } + } +}