From 63bd86d8046949e80e5b1122a1fcf717f51915c6 Mon Sep 17 00:00:00 2001 From: Paul Buetow Date: Fri, 3 Apr 2026 08:03:05 +0300 Subject: tasks 00d, 006, 007: move test seams to struct fields, centralize Viper config, extract shared card-dir logic MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit task 00d: remove package-level var test seams from processor and gui packages; factory functions (newAudioProvider, newOpenAIImageClient, newNanoBananaImageClient) are now struct fields on Processor and Application, initialized with production defaults in constructors and replaced in tests without global mutation. task 006: add viperConfig struct captured once in NewProcessor; no method body calls viper.GetString/IsSet/GetFloat64 directly any more — all config-file values are accessed via p.viperCfg fields. task 007: extract FindCardDirectory and FindOrCreateCardDirectory into internal/utils.go; both Processor.findCardDirectory and Application.findCardDirectory now delegate to the shared implementation, which also handles the legacy _word.txt backward-compat fallback. Co-Authored-By: Claude Sonnet 4.6 --- internal/processor/processor_test.go | 222 ++++++++++++----------------------- 1 file changed, 76 insertions(+), 146 deletions(-) (limited to 'internal/processor/processor_test.go') diff --git a/internal/processor/processor_test.go b/internal/processor/processor_test.go index 34cc844..0d0e9e3 100644 --- a/internal/processor/processor_test.go +++ b/internal/processor/processor_test.go @@ -369,11 +369,6 @@ func TestGUIConfigForRunModeHonorsExplicitNanoBananaModelFlags(t *testing.T) { } func TestGenerateAudioUsesSharedOpenAIVoices(t *testing.T) { - originalFactory := newAudioProvider - t.Cleanup(func() { - newAudioProvider = originalFactory - }) - originalVoices := append([]string(nil), audio.OpenAIVoices...) t.Cleanup(func() { audio.OpenAIVoices = originalVoices @@ -383,11 +378,6 @@ func TestGenerateAudioUsesSharedOpenAIVoices(t *testing.T) { fakeProvider := &fakeAudioProvider{} var capturedConfig *audio.Config - newAudioProvider = func(config *audio.Config) (audio.Provider, error) { - copyConfig := *config - capturedConfig = ©Config - return fakeProvider, nil - } tempDir := t.TempDir() flags := cli.NewFlags() @@ -397,6 +387,11 @@ func TestGenerateAudioUsesSharedOpenAIVoices(t *testing.T) { flags.AudioProvider = "openai" p := NewProcessor(flags) + p.newAudioProvider = func(config *audio.Config) (audio.Provider, error) { + copyConfig := *config + capturedConfig = ©Config + return fakeProvider, nil + } if err := p.generateAudio(context.Background(), "ябълка"); err != nil { t.Fatalf("generateAudio() unexpected error: %v", err) @@ -420,11 +415,6 @@ func TestGenerateAudioUsesSharedOpenAIVoices(t *testing.T) { } func TestGenerateAudioBgBgUsesSharedOpenAIVoices(t *testing.T) { - originalFactory := newAudioProvider - t.Cleanup(func() { - newAudioProvider = originalFactory - }) - originalVoices := append([]string(nil), audio.OpenAIVoices...) t.Cleanup(func() { audio.OpenAIVoices = originalVoices @@ -434,11 +424,6 @@ func TestGenerateAudioBgBgUsesSharedOpenAIVoices(t *testing.T) { fakeProvider := &fakeAudioProvider{} var capturedConfig *audio.Config - newAudioProvider = func(config *audio.Config) (audio.Provider, error) { - copyConfig := *config - capturedConfig = ©Config - return fakeProvider, nil - } tempDir := t.TempDir() flags := cli.NewFlags() @@ -447,6 +432,11 @@ func TestGenerateAudioBgBgUsesSharedOpenAIVoices(t *testing.T) { flags.AudioProvider = "openai" p := NewProcessor(flags) + p.newAudioProvider = func(config *audio.Config) (audio.Provider, error) { + copyConfig := *config + capturedConfig = ©Config + return fakeProvider, nil + } if err := p.generateAudioBgBg(context.Background(), "ябълка", "круша"); err != nil { t.Fatalf("generateAudioBgBg() unexpected error: %v", err) } @@ -506,20 +496,12 @@ func TestGenerateAudioBgBgUsesSharedOpenAIVoices(t *testing.T) { } func TestGenerateAudioProviderFactoryError(t *testing.T) { - originalFactory := newAudioProvider - t.Cleanup(func() { - newAudioProvider = originalFactory - }) - originalVoices := append([]string(nil), audio.OpenAIVoices...) t.Cleanup(func() { audio.OpenAIVoices = originalVoices }) audio.OpenAIVoices = []string{"sentinel-failure-voice"} - newAudioProvider = func(*audio.Config) (audio.Provider, error) { - return nil, errors.New("provider factory failed") - } tempDir := t.TempDir() flags := cli.NewFlags() @@ -528,6 +510,9 @@ func TestGenerateAudioProviderFactoryError(t *testing.T) { flags.AudioProvider = "openai" p := NewProcessor(flags) + p.newAudioProvider = func(*audio.Config) (audio.Provider, error) { + return nil, errors.New("provider factory failed") + } err := p.generateAudio(context.Background(), "ябълка") if err == nil { t.Fatal("generateAudio() expected error from provider factory") @@ -538,18 +523,8 @@ func TestGenerateAudioProviderFactoryError(t *testing.T) { } func TestGenerateAudioUsesConfiguredGeminiVoiceAndModel(t *testing.T) { - originalFactory := newAudioProvider - t.Cleanup(func() { - newAudioProvider = originalFactory - }) - fakeProvider := &fakeAudioProvider{} var capturedConfig *audio.Config - newAudioProvider = func(config *audio.Config) (audio.Provider, error) { - copyConfig := *config - capturedConfig = ©Config - return fakeProvider, nil - } originalConfig := viper.New() *originalConfig = *viper.GetViper() @@ -566,6 +541,11 @@ func TestGenerateAudioUsesConfiguredGeminiVoiceAndModel(t *testing.T) { flags.AudioFormat = "mp3" p := NewProcessor(flags) + p.newAudioProvider = func(config *audio.Config) (audio.Provider, error) { + copyConfig := *config + capturedConfig = ©Config + return fakeProvider, nil + } if err := p.generateAudio(context.Background(), "ябълка!?"); err != nil { t.Fatalf("generateAudio() unexpected error: %v", err) } @@ -633,10 +613,6 @@ func TestGenerateAudioUsesConfiguredGeminiVoiceAndModel(t *testing.T) { } func TestGenerateAudioUsesGeminiModelDefaultWhenVoiceNotSet(t *testing.T) { - originalFactory := newAudioProvider - t.Cleanup(func() { - newAudioProvider = originalFactory - }) originalVoices := append([]string(nil), audio.GeminiVoices...) t.Cleanup(func() { audio.GeminiVoices = originalVoices @@ -645,11 +621,6 @@ func TestGenerateAudioUsesGeminiModelDefaultWhenVoiceNotSet(t *testing.T) { fakeProvider := &fakeAudioProvider{} var capturedConfig *audio.Config - newAudioProvider = func(config *audio.Config) (audio.Provider, error) { - copyConfig := *config - capturedConfig = ©Config - return fakeProvider, nil - } originalConfig := viper.New() *originalConfig = *viper.GetViper() @@ -665,6 +636,11 @@ func TestGenerateAudioUsesGeminiModelDefaultWhenVoiceNotSet(t *testing.T) { flags.AudioProvider = "gemini" p := NewProcessor(flags) + p.newAudioProvider = func(config *audio.Config) (audio.Provider, error) { + copyConfig := *config + capturedConfig = ©Config + return fakeProvider, nil + } if err := p.generateAudio(context.Background(), "ябълка!?"); err != nil { t.Fatalf("generateAudio() unexpected error: %v", err) } @@ -717,11 +693,6 @@ func TestGenerateAudioUsesGeminiModelDefaultWhenVoiceNotSet(t *testing.T) { } func TestGenerateGeminiAudioWithFallbacksRetriesAlternateVoice(t *testing.T) { - originalFactory := newAudioProvider - t.Cleanup(func() { - newAudioProvider = originalFactory - }) - originalVoices := append([]string(nil), audio.GeminiVoices...) t.Cleanup(func() { audio.GeminiVoices = originalVoices @@ -729,17 +700,6 @@ func TestGenerateGeminiAudioWithFallbacksRetriesAlternateVoice(t *testing.T) { audio.GeminiVoices = []string{"Charon", "Kore", "Leda"} var attemptedVoices []string - newAudioProvider = func(config *audio.Config) (audio.Provider, error) { - attemptedVoices = append(attemptedVoices, config.GeminiVoice) - return &fakeAudioProvider{ - generateFunc: func(_ string, _ string) error { - if config.GeminiVoice == "Charon" { - return audio.ErrGeminiNoAudioData - } - return nil - }, - }, nil - } originalConfig := viper.New() *originalConfig = *viper.GetViper() @@ -754,6 +714,17 @@ func TestGenerateGeminiAudioWithFallbacksRetriesAlternateVoice(t *testing.T) { flags.AudioProvider = "gemini" p := NewProcessor(flags) + p.newAudioProvider = func(config *audio.Config) (audio.Provider, error) { + attemptedVoices = append(attemptedVoices, config.GeminiVoice) + return &fakeAudioProvider{ + generateFunc: func(_ string, _ string) error { + if config.GeminiVoice == "Charon" { + return audio.ErrGeminiNoAudioData + } + return nil + }, + }, nil + } p.randomIntn = func(int) int { return 0 } output := captureStdout(t, func() { if err := p.generateAudio(context.Background(), "ябълка"); err != nil { @@ -788,25 +759,12 @@ func TestGenerateGeminiAudioWithFallbacksRetriesAlternateVoice(t *testing.T) { } func TestGenerateAudioReturnsExhaustedGeminiFallbackError(t *testing.T) { - originalFactory := newAudioProvider - t.Cleanup(func() { - newAudioProvider = originalFactory - }) - originalVoices := append([]string(nil), audio.GeminiVoices...) t.Cleanup(func() { audio.GeminiVoices = originalVoices }) audio.GeminiVoices = []string{"Charon"} - newAudioProvider = func(*audio.Config) (audio.Provider, error) { - return &fakeAudioProvider{ - generateFunc: func(_ string, _ string) error { - return audio.ErrGeminiNoAudioData - }, - }, nil - } - originalConfig := viper.New() *originalConfig = *viper.GetViper() defer func() { @@ -820,6 +778,13 @@ func TestGenerateAudioReturnsExhaustedGeminiFallbackError(t *testing.T) { flags.AudioProvider = "gemini" p := NewProcessor(flags) + p.newAudioProvider = func(*audio.Config) (audio.Provider, error) { + return &fakeAudioProvider{ + generateFunc: func(_ string, _ string) error { + return audio.ErrGeminiNoAudioData + }, + }, nil + } err := p.generateAudio(context.Background(), "ябълка") if !errors.Is(err, audio.ErrGeminiNoAudioData) { t.Fatalf("generateAudio() error = %v, want wrapped ErrGeminiNoAudioData", err) @@ -830,10 +795,6 @@ func TestGenerateAudioReturnsExhaustedGeminiFallbackError(t *testing.T) { } func TestGenerateAudioBgBgUsesGeminiModelDefaultWhenVoiceNotSet(t *testing.T) { - originalFactory := newAudioProvider - t.Cleanup(func() { - newAudioProvider = originalFactory - }) originalVoices := append([]string(nil), audio.GeminiVoices...) t.Cleanup(func() { audio.GeminiVoices = originalVoices @@ -842,11 +803,6 @@ func TestGenerateAudioBgBgUsesGeminiModelDefaultWhenVoiceNotSet(t *testing.T) { fakeProvider := &fakeAudioProvider{} var capturedConfigs []*audio.Config - newAudioProvider = func(config *audio.Config) (audio.Provider, error) { - copyConfig := *config - capturedConfigs = append(capturedConfigs, ©Config) - return fakeProvider, nil - } originalConfig := viper.New() *originalConfig = *viper.GetViper() @@ -862,6 +818,11 @@ func TestGenerateAudioBgBgUsesGeminiModelDefaultWhenVoiceNotSet(t *testing.T) { flags.AudioProvider = "gemini" p := NewProcessor(flags) + p.newAudioProvider = func(config *audio.Config) (audio.Provider, error) { + copyConfig := *config + capturedConfigs = append(capturedConfigs, ©Config) + return fakeProvider, nil + } if err := p.generateAudioBgBg(context.Background(), "ябълка!?", "круша."); err != nil { t.Fatalf("generateAudioBgBg() unexpected error: %v", err) } @@ -895,18 +856,8 @@ func TestGenerateAudioBgBgUsesGeminiModelDefaultWhenVoiceNotSet(t *testing.T) { } func TestGenerateAudioUsesConfiguredAudioFormatWhenOpenAIConfigIsSetOnly(t *testing.T) { - originalFactory := newAudioProvider - t.Cleanup(func() { - newAudioProvider = originalFactory - }) - fakeProvider := &fakeAudioProvider{} var capturedConfig *audio.Config - newAudioProvider = func(config *audio.Config) (audio.Provider, error) { - copyConfig := *config - capturedConfig = ©Config - return fakeProvider, nil - } originalConfig := viper.New() *originalConfig = *viper.GetViper() @@ -923,6 +874,11 @@ func TestGenerateAudioUsesConfiguredAudioFormatWhenOpenAIConfigIsSetOnly(t *test flags.AudioFormat = "wav" p := NewProcessor(flags) + p.newAudioProvider = func(config *audio.Config) (audio.Provider, error) { + copyConfig := *config + capturedConfig = ©Config + return fakeProvider, nil + } wordDir := p.findOrCreateWordDirectory("ябълка!?") if err := p.generateAudioWithVoiceAndFilenameInDir(context.Background(), "ябълка!?", "alloy", "audio", wordDir); err != nil { t.Fatalf("generateAudioWithVoiceAndFilenameInDir() unexpected error: %v", err) @@ -973,18 +929,8 @@ func TestGenerateAudioUsesConfiguredAudioFormatWhenOpenAIConfigIsSetOnly(t *test } func TestGenerateAudioUsesConfiguredOpenAIVoiceFromConfig(t *testing.T) { - originalFactory := newAudioProvider - t.Cleanup(func() { - newAudioProvider = originalFactory - }) - fakeProvider := &fakeAudioProvider{} var capturedConfig *audio.Config - newAudioProvider = func(config *audio.Config) (audio.Provider, error) { - copyConfig := *config - capturedConfig = ©Config - return fakeProvider, nil - } originalConfig := viper.New() *originalConfig = *viper.GetViper() @@ -1000,6 +946,11 @@ func TestGenerateAudioUsesConfiguredOpenAIVoiceFromConfig(t *testing.T) { flags.AudioFormat = "mp3" p := NewProcessor(flags) + p.newAudioProvider = func(config *audio.Config) (audio.Provider, error) { + copyConfig := *config + capturedConfig = ©Config + return fakeProvider, nil + } if err := p.generateAudio(context.Background(), "ябълка"); err != nil { t.Fatalf("generateAudio() unexpected error: %v", err) } @@ -1048,18 +999,8 @@ func TestGenerateAudioUsesConfiguredOpenAIVoiceFromConfig(t *testing.T) { } func TestGenerateAudioOmitsOpenAIInstructionsForUnsupportedModel(t *testing.T) { - originalFactory := newAudioProvider - t.Cleanup(func() { - newAudioProvider = originalFactory - }) - fakeProvider := &fakeAudioProvider{} var capturedConfig *audio.Config - newAudioProvider = func(config *audio.Config) (audio.Provider, error) { - copyConfig := *config - capturedConfig = ©Config - return fakeProvider, nil - } originalConfig := viper.New() *originalConfig = *viper.GetViper() @@ -1077,6 +1018,11 @@ func TestGenerateAudioOmitsOpenAIInstructionsForUnsupportedModel(t *testing.T) { flags.OpenAIModel = "tts-1" p := NewProcessor(flags) + p.newAudioProvider = func(config *audio.Config) (audio.Provider, error) { + copyConfig := *config + capturedConfig = ©Config + return fakeProvider, nil + } if err := p.generateAudio(context.Background(), "ябълка!?"); err != nil { t.Fatalf("generateAudio() unexpected error: %v", err) } @@ -1223,16 +1169,8 @@ func TestDownloadImagesWithTranslationUsesNanoBananaConfigAndSavesPrompt(t *test viper.Set("image.nanobanana_model", "custom-image-model") viper.Set("image.nanobanana_text_model", "custom-text-model") - originalConstructor := newNanoBananaImageClient stubSearcher := &stubImageSearcher{} capturedConfig := new(image.NanoBananaConfig) - newNanoBananaImageClient = func(config *image.NanoBananaConfig) image.ImageClient { - *capturedConfig = *config - return stubSearcher - } - t.Cleanup(func() { - newNanoBananaImageClient = originalConstructor - }) flags := cli.NewFlags() flags.OutputDir = t.TempDir() @@ -1240,6 +1178,10 @@ func TestDownloadImagesWithTranslationUsesNanoBananaConfigAndSavesPrompt(t *test flags.ImageAPISpecified = true p := NewProcessor(flags) + p.newNanoBananaImageClient = func(config *image.NanoBananaConfig) image.ImageClient { + *capturedConfig = *config + return stubSearcher + } if err := p.downloadImagesWithTranslation(context.Background(), "ябълка", "apple"); err != nil { t.Fatalf("downloadImagesWithTranslation() unexpected error: %v", err) } @@ -1280,14 +1222,7 @@ func TestDownloadImagesWithTranslationPersistsPromptWhenDownloadFails(t *testing viper.Reset() viper.Set("image.provider", "nanobanana") - originalConstructor := newNanoBananaImageClient stubSearcher := &stubImageSearcher{downloadErr: errors.New("download failed")} - newNanoBananaImageClient = func(config *image.NanoBananaConfig) image.ImageClient { - return stubSearcher - } - t.Cleanup(func() { - newNanoBananaImageClient = originalConstructor - }) flags := cli.NewFlags() flags.OutputDir = t.TempDir() @@ -1295,6 +1230,9 @@ func TestDownloadImagesWithTranslationPersistsPromptWhenDownloadFails(t *testing flags.ImageAPISpecified = true p := NewProcessor(flags) + p.newNanoBananaImageClient = func(config *image.NanoBananaConfig) image.ImageClient { + return stubSearcher + } err := p.downloadImagesWithTranslation(context.Background(), "ябълка", "apple") if err == nil { t.Fatal("downloadImagesWithTranslation() expected error from failed download") @@ -1328,16 +1266,8 @@ func TestDownloadImagesWithTranslationUsesConfiguredNanoBananaWhenImageAPINotSpe viper.Set("image.nanobanana_model", "config-image-model") viper.Set("image.nanobanana_text_model", "config-text-model") - originalConstructor := newNanoBananaImageClient stubSearcher := &stubImageSearcher{} capturedConfig := new(image.NanoBananaConfig) - newNanoBananaImageClient = func(config *image.NanoBananaConfig) image.ImageClient { - *capturedConfig = *config - return stubSearcher - } - t.Cleanup(func() { - newNanoBananaImageClient = originalConstructor - }) flags := cli.NewFlags() flags.OutputDir = t.TempDir() @@ -1345,6 +1275,10 @@ func TestDownloadImagesWithTranslationUsesConfiguredNanoBananaWhenImageAPINotSpe flags.ImageAPISpecified = false p := NewProcessor(flags) + p.newNanoBananaImageClient = func(config *image.NanoBananaConfig) image.ImageClient { + *capturedConfig = *config + return stubSearcher + } if err := p.downloadImagesWithTranslation(context.Background(), "ябълка", "apple"); err != nil { t.Fatalf("downloadImagesWithTranslation() unexpected error: %v", err) } @@ -1435,15 +1369,7 @@ func TestNewNanoBananaImageSearcherExplicitDefaultWinsOverConfig(t *testing.T) { 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.ImageClient { - *capturedConfig = *config - return &stubImageSearcher{} - } - t.Cleanup(func() { - newNanoBananaImageClient = originalConstructor - }) flags := cli.NewFlags() flags.OutputDir = t.TempDir() @@ -1455,6 +1381,10 @@ func TestNewNanoBananaImageSearcherExplicitDefaultWinsOverConfig(t *testing.T) { flags.NanoBananaTextModelSpecified = true p := NewProcessor(flags) + p.newNanoBananaImageClient = func(config *image.NanoBananaConfig) image.ImageClient { + *capturedConfig = *config + return &stubImageSearcher{} + } searcher, err := p.newNanoBananaImageSearcher() if err != nil { t.Fatalf("newNanoBananaImageSearcher() unexpected error: %v", err) -- cgit v1.2.3