package processor
import (
"context"
"errors"
"fmt"
"io"
"os"
"path/filepath"
"strings"
"testing"
"codeberg.org/snonux/totalrecall/internal/audio"
"codeberg.org/snonux/totalrecall/internal/cli"
"codeberg.org/snonux/totalrecall/internal/gui"
"codeberg.org/snonux/totalrecall/internal/image"
"codeberg.org/snonux/totalrecall/internal/phonetic"
"github.com/spf13/viper"
)
type stubImageSearcher struct {
lastPrompt string
searchErr error
downloadErr error
promptCallback func(string)
}
func (s *stubImageSearcher) Search(ctx context.Context, opts *image.SearchOptions) ([]image.SearchResult, error) {
if s.searchErr != nil {
return nil, s.searchErr
}
s.lastPrompt = "stub nanobanana prompt"
if s.promptCallback != nil {
s.promptCallback(s.lastPrompt)
}
return []image.SearchResult{
{
ID: "stub-image",
URL: "data:image/png;base64,AAAA",
ThumbnailURL: "data:image/png;base64,AAAA",
Width: 1,
Height: 1,
Description: "stub image",
Attribution: "stub attribution",
Source: "nanobanana",
},
}, nil
}
func (s *stubImageSearcher) Download(ctx context.Context, url string) (io.ReadCloser, error) {
if s.downloadErr != nil {
return nil, s.downloadErr
}
return io.NopCloser(strings.NewReader("mock image data")), nil
}
func (s *stubImageSearcher) GetAttribution(result *image.SearchResult) string {
return result.Attribution
}
func (s *stubImageSearcher) Name() string {
return "nanobanana"
}
func (s *stubImageSearcher) GetLastPrompt() string {
return s.lastPrompt
}
func (s *stubImageSearcher) SetPromptCallback(callback func(string)) {
s.promptCallback = callback
}
type fakeAudioProvider struct {
generateCalls int
texts []string
outputFiles []string
lastText string
lastOutputFile string
generateFunc func(text, outputFile string) error
}
func (f *fakeAudioProvider) GenerateAudio(_ context.Context, text, outputFile string) error {
f.generateCalls++
f.texts = append(f.texts, text)
f.outputFiles = append(f.outputFiles, outputFile)
f.lastText = text
f.lastOutputFile = outputFile
if f.generateFunc != nil {
return f.generateFunc(text, outputFile)
}
return nil
}
func (f *fakeAudioProvider) Name() string {
return "fake-audio"
}
func (f *fakeAudioProvider) IsAvailable() error {
return nil
}
func TestNewProcessor(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()
flags := cli.NewFlags()
p := NewProcessor(flags)
if p == nil {
t.Fatal("NewProcessor returned nil")
}
if p.flags != flags {
t.Error("Processor flags not set correctly")
}
if p.translator == nil {
t.Error("Translator not initialized")
}
if p.translationCache == nil {
t.Error("Translation cache not initialized")
}
if p.phoneticFetcher == nil {
t.Error("Phonetic fetcher not initialized")
}
}
func TestNewProcessor_DefaultPhoneticProviderUsesGemini(t *testing.T) {
t.Setenv("OPENAI_API_KEY", "")
t.Setenv("GOOGLE_API_KEY", "")
originalConfig := viper.New()
*originalConfig = *viper.GetViper()
defer func() {
*viper.GetViper() = *originalConfig
}()
viper.Reset()
flags := cli.NewFlags()
p := NewProcessor(flags)
if got := p.phoneticFetcher.Provider(); got != phonetic.ProviderGemini {
t.Fatalf("expected default phonetic provider %q, got %q", phonetic.ProviderGemini, got)
}
}
func TestNewProcessor_ExplicitGeminiPhoneticProvider(t *testing.T) {
t.Setenv("OPENAI_API_KEY", "")
t.Setenv("GOOGLE_API_KEY", "")
originalConfig := viper.New()
*originalConfig = *viper.GetViper()
defer func() {
*viper.GetViper() = *originalConfig
}()
viper.Reset()
viper.Set("phonetic.provider", "gemini")
flags := cli.NewFlags()
p := NewProcessor(flags)
if got := p.phoneticFetcher.Provider(); got != phonetic.ProviderGemini {
t.Fatalf("expected gemini phonetic provider %q, got %q", phonetic.ProviderGemini, got)
}
}
func TestNewProcessor_DefaultTranslationProviderUsesGemini(t *testing.T) {
t.Setenv("OPENAI_API_KEY", "")
t.Setenv("GOOGLE_API_KEY", "")
originalConfig := viper.New()
*originalConfig = *viper.GetViper()
defer func() {
*viper.GetViper() = *originalConfig
}()
viper.Reset()
flags := cli.NewFlags()
p := NewProcessor(flags)
_, err := p.translator.TranslateWord("ябълка")
if err == nil {
t.Fatal("Expected error for missing Google API key")
}
if err.Error() != "google API key not found" {
t.Fatalf("Expected Gemini default provider error, got: %v", err)
}
}
func TestNewProcessor_ExplicitGeminiTranslationProvider(t *testing.T) {
t.Setenv("OPENAI_API_KEY", "")
t.Setenv("GOOGLE_API_KEY", "")
originalConfig := viper.New()
*originalConfig = *viper.GetViper()
defer func() {
*viper.GetViper() = *originalConfig
}()
viper.Reset()
viper.Set("translation.provider", "gemini")
flags := cli.NewFlags()
p := NewProcessor(flags)
_, err := p.translator.TranslateWord("ябълка")
if err == nil {
t.Fatal("Expected error for missing Google API key")
}
if err.Error() != "google API key not found" {
t.Fatalf("Expected Gemini provider error, got: %v", err)
}
}
func TestGUIConfigForRunModeUsesNanoBananaDefaultWhenImageAPIIsNotSpecified(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")
flags := cli.NewFlags()
flags.AudioFormat = "mp3"
flags.ImageAPI = "openai"
flags.ImageAPISpecified = false
p := NewProcessor(flags)
guiConfig := p.guiConfigForRunMode()
if guiConfig.ImageProvider != gui.DefaultConfig().ImageProvider {
t.Fatalf(
|