package processor
import (
"bytes"
"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 (f *fakeAudioProvider) Voices() []string {
return nil
}
func (f *fakeAudioProvider) BuildAttribution(params audio.AttributionParams) string {
return ""
}
func captureStdout(t *testing.T, fn func()) (output string) {
t.Helper()
originalStdout := os.Stdout
reader, writer, err := os.Pipe()
if err != nil {
t.Fatalf("failed to create stdout pipe: %v", err)
}
os.Stdout = writer
outputCh := make(chan string, 1)
defer func() {
os.Stdout = originalStdout
if err := writer.Close(); err != nil {
t.Fatalf("failed to close stdout pipe: %v", err)
}
output = <-outputCh
if err := reader.Close(); err != nil {
t.Fatalf("failed to close stdout reader: %v", err)
}
}()
go func() {
var buf bytes.Buffer
_, _ = io.Copy(&buf, reader)
outputCh <- buf.String()
}()
fn()
return output
}
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(
|