From 4727a188102d8166e4acabc4fd62863245e6cdce Mon Sep 17 00:00:00 2001 From: Paul Buetow Date: Wed, 1 Apr 2026 20:14:03 +0300 Subject: z8: preserve Nano Banana CLI precedence --- cmd/totalrecall/main.go | 3 +-- internal/cli/command.go | 7 ++++++ internal/cli/command_test.go | 28 +++++++++++++++++++++ internal/cli/flags.go | 4 +++ internal/cli/flags_test.go | 4 ++- internal/processor/processor.go | 4 +-- internal/processor/processor_test.go | 49 ++++++++++++++++++++++++++++++++++++ 7 files changed, 94 insertions(+), 5 deletions(-) diff --git a/cmd/totalrecall/main.go b/cmd/totalrecall/main.go index c0bd142..d719be5 100644 --- a/cmd/totalrecall/main.go +++ b/cmd/totalrecall/main.go @@ -27,6 +27,7 @@ func main() { // Set the run function rootCmd.RunE = func(cmd *cobra.Command, args []string) error { + cli.MarkExplicitFlagValues(cmd, flags) return runCommand(cmd, args, flags) } @@ -60,8 +61,6 @@ func runCommand(cmd *cobra.Command, args []string, flags *cli.Flags) error { fmt.Printf("Note: Using image size 1024x1024 for DALL-E 3 (use --openai-image-size to override)\n") } - flags.ImageAPISpecified = cmd.Flags().Changed("image-api") - // Create processor proc := processor.NewProcessor(flags) diff --git a/internal/cli/command.go b/internal/cli/command.go index 924740e..327c869 100644 --- a/internal/cli/command.go +++ b/internal/cli/command.go @@ -91,6 +91,13 @@ func setupFlags(cmd *cobra.Command, flags *Flags) { } } +// MarkExplicitFlagValues records which CLI flags were explicitly set by the user. +func MarkExplicitFlagValues(cmd *cobra.Command, flags *Flags) { + flags.ImageAPISpecified = cmd.Flags().Changed("image-api") + flags.NanoBananaModelSpecified = cmd.Flags().Changed("nanobanana-model") + flags.NanoBananaTextModelSpecified = cmd.Flags().Changed("nanobanana-text-model") +} + func bindFlagsToViper(cmd *cobra.Command) error { bindings := map[string]string{ "audio.format": "format", diff --git a/internal/cli/command_test.go b/internal/cli/command_test.go index 67ef6c4..37e2533 100644 --- a/internal/cli/command_test.go +++ b/internal/cli/command_test.go @@ -435,3 +435,31 @@ func TestBindFlagsToViper(t *testing.T) { t.Errorf("Expected image.provider to be openai by default, got %s", viper.GetString("image.provider")) } } + +func TestMarkExplicitFlagValues(t *testing.T) { + flags := NewFlags() + cmd := &cobra.Command{} + setupFlags(cmd, flags) + + if err := cmd.Flags().Set("image-api", "nanobanana"); err != nil { + t.Fatalf("Failed to set image-api flag: %v", err) + } + if err := cmd.Flags().Set("nanobanana-model", defaultNanoBananaModel); err != nil { + t.Fatalf("Failed to set nanobanana-model flag: %v", err) + } + if err := cmd.Flags().Set("nanobanana-text-model", defaultNanoBananaTextModel); err != nil { + t.Fatalf("Failed to set nanobanana-text-model flag: %v", err) + } + + MarkExplicitFlagValues(cmd, flags) + + if !flags.ImageAPISpecified { + t.Error("Expected ImageAPISpecified to be true") + } + if !flags.NanoBananaModelSpecified { + t.Error("Expected NanoBananaModelSpecified to be true") + } + if !flags.NanoBananaTextModelSpecified { + t.Error("Expected NanoBananaTextModelSpecified to be true") + } +} diff --git a/internal/cli/flags.go b/internal/cli/flags.go index 1e18766..5fcab40 100644 --- a/internal/cli/flags.go +++ b/internal/cli/flags.go @@ -38,8 +38,12 @@ type Flags struct { // NanoBananaModel is the Gemini image model used for Nano Banana generation. NanoBananaModel string + // NanoBananaModelSpecified records whether the Nano Banana image model was explicitly set on the CLI. + NanoBananaModelSpecified bool // NanoBananaTextModel is the Gemini text model used for Nano Banana prompt generation. NanoBananaTextModel string + // NanoBananaTextModelSpecified records whether the Nano Banana text model was explicitly set on the CLI. + NanoBananaTextModelSpecified bool } // NewFlags creates a new Flags instance with default values diff --git a/internal/cli/flags_test.go b/internal/cli/flags_test.go index 2a63d8d..7ab6425 100644 --- a/internal/cli/flags_test.go +++ b/internal/cli/flags_test.go @@ -17,6 +17,8 @@ func TestNewFlags(t *testing.T) { {"AudioFormat", flags.AudioFormat, "mp3"}, {"ImageAPI", flags.ImageAPI, "openai"}, {"ImageAPISpecified", flags.ImageAPISpecified, false}, + {"NanoBananaModelSpecified", flags.NanoBananaModelSpecified, false}, + {"NanoBananaTextModelSpecified", flags.NanoBananaTextModelSpecified, false}, {"DeckName", flags.DeckName, "Bulgarian Vocabulary"}, {"OpenAIModel", flags.OpenAIModel, "gpt-4o-mini-tts"}, {"OpenAISpeed", flags.OpenAISpeed, 0.9}, @@ -90,7 +92,7 @@ func TestFlagsStructure(t *testing.T) { "ListModels", "AllVoices", "NoAutoPlay", "OpenAIModel", "OpenAIVoice", "OpenAISpeed", "OpenAIInstruction", "OpenAIImageModel", "OpenAIImageSize", "OpenAIImageQuality", "OpenAIImageStyle", - "NanoBananaModel", "NanoBananaTextModel", + "NanoBananaModel", "NanoBananaModelSpecified", "NanoBananaTextModel", "NanoBananaTextModelSpecified", } for _, fieldName := range expectedFields { diff --git a/internal/processor/processor.go b/internal/processor/processor.go index aac9733..d7025a6 100644 --- a/internal/processor/processor.go +++ b/internal/processor/processor.go @@ -606,10 +606,10 @@ func (p *Processor) newNanoBananaImageSearcher() (image.ImageSearcher, error) { TextModel: p.flags.NanoBananaTextModel, } - if p.flags.NanoBananaModel == image.DefaultNanoBananaModel && viper.IsSet("image.nanobanana_model") { + if !p.flags.NanoBananaModelSpecified && viper.IsSet("image.nanobanana_model") { nanoBananaConfig.Model = viper.GetString("image.nanobanana_model") } - if p.flags.NanoBananaTextModel == image.DefaultNanoBananaTextModel && viper.IsSet("image.nanobanana_text_model") { + if !p.flags.NanoBananaTextModelSpecified && viper.IsSet("image.nanobanana_text_model") { nanoBananaConfig.TextModel = viper.GetString("image.nanobanana_text_model") } diff --git a/internal/processor/processor_test.go b/internal/processor/processor_test.go index ef9e655..8ab13e7 100644 --- a/internal/processor/processor_test.go +++ b/internal/processor/processor_test.go @@ -397,6 +397,55 @@ func TestNewImageSearcherConfiguredNanoBananaRequiresGoogleAPIKey(t *testing.T) } } +func TestNewNanoBananaImageSearcherExplicitDefaultWinsOverConfig(t *testing.T) { + t.Setenv("OPENAI_API_KEY", "test-openai-key") + t.Setenv("GOOGLE_API_KEY", "test-google-key") + + originalConfig := viper.New() + *originalConfig = *viper.GetViper() + defer func() { + *viper.GetViper() = *originalConfig + }() + viper.Reset() + viper.Set("image.nanobanana_model", "config-image-model") + viper.Set("image.nanobanana_text_model", "config-text-model") + + originalConstructor := newNanoBananaImageClient + capturedConfig := new(image.NanoBananaConfig) + newNanoBananaImageClient = func(config *image.NanoBananaConfig) image.ImageSearcher { + *capturedConfig = *config + return &stubImageSearcher{} + } + t.Cleanup(func() { + newNanoBananaImageClient = originalConstructor + }) + + flags := cli.NewFlags() + flags.OutputDir = t.TempDir() + flags.ImageAPI = "nanobanana" + flags.ImageAPISpecified = true + flags.NanoBananaModel = image.DefaultNanoBananaModel + flags.NanoBananaModelSpecified = true + flags.NanoBananaTextModel = image.DefaultNanoBananaTextModel + flags.NanoBananaTextModelSpecified = true + + p := NewProcessor(flags) + searcher, err := p.newNanoBananaImageSearcher() + if err != nil { + t.Fatalf("newNanoBananaImageSearcher() unexpected error: %v", err) + } + if searcher == nil { + t.Fatal("expected searcher") + } + + if capturedConfig.Model != image.DefaultNanoBananaModel { + t.Fatalf("NanoBanana Model = %q, want explicit CLI default %q", capturedConfig.Model, image.DefaultNanoBananaModel) + } + if capturedConfig.TextModel != image.DefaultNanoBananaTextModel { + t.Fatalf("NanoBanana TextModel = %q, want explicit CLI default %q", capturedConfig.TextModel, image.DefaultNanoBananaTextModel) + } +} + func TestProcessSingleWord_InvalidWord(t *testing.T) { flags := cli.NewFlags() flags.OutputDir = t.TempDir() -- cgit v1.2.3