summaryrefslogtreecommitdiff
path: root/internal/cli
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2025-07-20 23:10:50 +0300
committerPaul Buetow <paul@buetow.org>2025-07-20 23:10:50 +0300
commit9c12e879c5d6833ce50f5b6d646ccce03a78db31 (patch)
tree206906b551d595b35d00586b6cc5bf9e1f3fe7f8 /internal/cli
parente580fb57a29ec3c3f3e180b20cfa6ec28687689b (diff)
test: add comprehensive test coverage for refactored packages
Add test suites for all newly created packages from the main.go refactoring: - batch: 100% coverage - file reading, parsing, edge cases - cli: 96.7% coverage - command setup, flags, configuration - translation: 92% coverage - API integration, caching, errors - phonetic: 87.5% coverage - API fetching, file operations - models: 77.3% coverage - model listing functionality - processor: 18% coverage - basic tests (limited by API dependencies) Total: 1159 lines of test code across 7 new test files 🤖 Generated with [opencode](https://opencode.ai) Co-Authored-By: opencode <noreply@opencode.ai>
Diffstat (limited to 'internal/cli')
-rw-r--r--internal/cli/command_test.go260
-rw-r--r--internal/cli/flags_test.go99
2 files changed, 359 insertions, 0 deletions
diff --git a/internal/cli/command_test.go b/internal/cli/command_test.go
new file mode 100644
index 0000000..55e60e8
--- /dev/null
+++ b/internal/cli/command_test.go
@@ -0,0 +1,260 @@
+package cli
+
+import (
+ "os"
+ "path/filepath"
+ "strings"
+ "testing"
+
+ "github.com/spf13/cobra"
+ "github.com/spf13/pflag"
+ "github.com/spf13/viper"
+)
+
+func TestCreateRootCommand(t *testing.T) {
+ flags := NewFlags()
+ cmd := CreateRootCommand(flags)
+
+ // Test basic command properties
+ if cmd.Use != "totalrecall [word]" {
+ t.Errorf("Expected Use to be 'totalrecall [word]', got %s", cmd.Use)
+ }
+
+ if !strings.Contains(cmd.Short, "Bulgarian Anki Flashcard Generator") {
+ t.Errorf("Expected Short description to contain 'Bulgarian Anki Flashcard Generator'")
+ }
+
+ // Test that flags are set up
+ flagTests := []struct {
+ name string
+ expected bool
+ }{
+ {"config", true},
+ {"output", true},
+ {"format", true},
+ {"image-api", true},
+ {"batch", true},
+ {"skip-audio", true},
+ {"skip-images", true},
+ {"anki", true},
+ {"anki-csv", true},
+ {"deck-name", true},
+ {"list-models", true},
+ {"all-voices", true},
+ {"gui", true},
+ {"openai-model", true},
+ {"openai-voice", true},
+ {"openai-speed", true},
+ {"openai-instruction", true},
+ {"openai-image-model", true},
+ {"openai-image-size", true},
+ {"openai-image-quality", true},
+ {"openai-image-style", true},
+ }
+
+ for _, tt := range flagTests {
+ t.Run("flag_"+tt.name, func(t *testing.T) {
+ var flag *pflag.Flag
+ if tt.name == "config" {
+ flag = cmd.PersistentFlags().Lookup(tt.name)
+ } else {
+ flag = cmd.Flags().Lookup(tt.name)
+ }
+ if flag == nil && tt.expected {
+ t.Errorf("Expected flag %s to exist", tt.name)
+ }
+ })
+ }
+}
+
+func TestSetupFlags(t *testing.T) {
+ cmd := &cobra.Command{}
+ flags := NewFlags()
+
+ setupFlags(cmd, flags)
+
+ // Test default values
+ outputFlag := cmd.Flags().Lookup("output")
+ if outputFlag == nil {
+ t.Fatal("output flag not found")
+ }
+
+ home, _ := os.UserHomeDir()
+ expectedDefault := filepath.Join(home, "Downloads")
+ if outputFlag.DefValue != expectedDefault {
+ t.Errorf("Expected default output dir to be %s, got %s", expectedDefault, outputFlag.DefValue)
+ }
+
+ // Test audio format default
+ formatFlag := cmd.Flags().Lookup("format")
+ if formatFlag == nil {
+ t.Fatal("format flag not found")
+ }
+ if formatFlag.DefValue != "mp3" {
+ t.Errorf("Expected default format to be mp3, got %s", formatFlag.DefValue)
+ }
+}
+
+func TestInitConfig(t *testing.T) {
+ // Save original viper state
+ originalConfig := viper.New()
+ *originalConfig = *viper.GetViper()
+ defer func() {
+ *viper.GetViper() = *originalConfig
+ }()
+
+ tests := []struct {
+ name string
+ cfgFile string
+ setupFunc func(t *testing.T) string
+ cleanupFunc func(string)
+ }{
+ {
+ name: "with config file",
+ cfgFile: "test-config.yaml",
+ setupFunc: func(t *testing.T) string {
+ tmpDir := t.TempDir()
+ cfgPath := filepath.Join(tmpDir, "test-config.yaml")
+ content := `audio:
+ provider: openai
+ openai_key: test-key
+output:
+ directory: /test/output`
+ err := os.WriteFile(cfgPath, []byte(content), 0644)
+ if err != nil {
+ t.Fatalf("Failed to create test config: %v", err)
+ }
+ return cfgPath
+ },
+ cleanupFunc: func(path string) {},
+ },
+ {
+ name: "without config file",
+ cfgFile: "",
+ setupFunc: func(t *testing.T) string {
+ return ""
+ },
+ cleanupFunc: func(path string) {},
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ // Reset viper for each test
+ viper.Reset()
+
+ cfgPath := tt.setupFunc(t)
+ if tt.cfgFile != "" && cfgPath != "" {
+ tt.cfgFile = cfgPath
+ }
+
+ InitConfig(tt.cfgFile)
+
+ // Test environment variable prefix
+ os.Setenv("TOTALRECALL_TEST_VAR", "test-value")
+ defer os.Unsetenv("TOTALRECALL_TEST_VAR")
+
+ if viper.GetString("test_var") != "test-value" {
+ t.Error("Environment variable not properly loaded")
+ }
+
+ tt.cleanupFunc(cfgPath)
+ })
+ }
+}
+
+func TestGetOpenAIKey(t *testing.T) {
+ // Save original viper state
+ originalConfig := viper.New()
+ *originalConfig = *viper.GetViper()
+ defer func() {
+ *viper.GetViper() = *originalConfig
+ }()
+
+ tests := []struct {
+ name string
+ envKey string
+ configKey string
+ expected string
+ }{
+ {
+ name: "from environment",
+ envKey: "env-test-key",
+ configKey: "config-test-key",
+ expected: "env-test-key",
+ },
+ {
+ name: "from config when no env",
+ envKey: "",
+ configKey: "config-test-key",
+ expected: "config-test-key",
+ },
+ {
+ name: "empty when neither set",
+ envKey: "",
+ configKey: "",
+ expected: "",
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ // Reset viper
+ viper.Reset()
+
+ // Set up environment
+ if tt.envKey != "" {
+ os.Setenv("OPENAI_API_KEY", tt.envKey)
+ defer os.Unsetenv("OPENAI_API_KEY")
+ } else {
+ os.Unsetenv("OPENAI_API_KEY")
+ }
+
+ // Set up config
+ if tt.configKey != "" {
+ viper.Set("audio.openai_key", tt.configKey)
+ }
+
+ got := GetOpenAIKey()
+ if got != tt.expected {
+ t.Errorf("GetOpenAIKey() = %v, want %v", got, tt.expected)
+ }
+ })
+ }
+}
+
+func TestBindFlagsToViper(t *testing.T) {
+ // Save original viper state
+ originalConfig := viper.New()
+ *originalConfig = *viper.GetViper()
+ defer func() {
+ *viper.GetViper() = *originalConfig
+ }()
+
+ // Reset viper
+ viper.Reset()
+
+ cmd := &cobra.Command{}
+ flags := NewFlags()
+ setupFlags(cmd, flags)
+
+ // Set some flag values
+ cmd.Flags().Set("output", "/test/output")
+ cmd.Flags().Set("format", "wav")
+ cmd.Flags().Set("openai-model", "tts-1-hd")
+
+ bindFlagsToViper(cmd)
+
+ // Test that values are bound
+ if viper.GetString("output.directory") != "/test/output" {
+ t.Errorf("Expected output.directory to be /test/output, got %s", viper.GetString("output.directory"))
+ }
+
+ if viper.GetString("audio.format") != "wav" {
+ t.Errorf("Expected audio.format to be wav, got %s", viper.GetString("audio.format"))
+ }
+
+ if viper.GetString("audio.openai_model") != "tts-1-hd" {
+ t.Errorf("Expected audio.openai_model to be tts-1-hd, got %s", viper.GetString("audio.openai_model"))
+ }
+}
diff --git a/internal/cli/flags_test.go b/internal/cli/flags_test.go
new file mode 100644
index 0000000..d308bb7
--- /dev/null
+++ b/internal/cli/flags_test.go
@@ -0,0 +1,99 @@
+package cli
+
+import (
+ "reflect"
+ "testing"
+)
+
+func TestNewFlags(t *testing.T) {
+ flags := NewFlags()
+
+ // Test default values
+ tests := []struct {
+ name string
+ got interface{}
+ expected interface{}
+ }{
+ {"AudioFormat", flags.AudioFormat, "mp3"},
+ {"ImageAPI", flags.ImageAPI, "openai"},
+ {"DeckName", flags.DeckName, "Bulgarian Vocabulary"},
+ {"OpenAIModel", flags.OpenAIModel, "gpt-4o-mini-tts"},
+ {"OpenAISpeed", flags.OpenAISpeed, 0.9},
+ {"OpenAIImageModel", flags.OpenAIImageModel, "dall-e-3"},
+ {"OpenAIImageSize", flags.OpenAIImageSize, "1024x1024"},
+ {"OpenAIImageQuality", flags.OpenAIImageQuality, "standard"},
+ {"OpenAIImageStyle", flags.OpenAIImageStyle, "natural"},
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ if !reflect.DeepEqual(tt.got, tt.expected) {
+ t.Errorf("%s = %v, want %v", tt.name, tt.got, tt.expected)
+ }
+ })
+ }
+
+ // Test boolean defaults (should be false)
+ boolTests := []struct {
+ name string
+ value bool
+ }{
+ {"SkipAudio", flags.SkipAudio},
+ {"SkipImages", flags.SkipImages},
+ {"GenerateAnki", flags.GenerateAnki},
+ {"AnkiCSV", flags.AnkiCSV},
+ {"ListModels", flags.ListModels},
+ {"AllVoices", flags.AllVoices},
+ {"GUIMode", flags.GUIMode},
+ }
+
+ for _, tt := range boolTests {
+ t.Run(tt.name, func(t *testing.T) {
+ if tt.value != false {
+ t.Errorf("%s = %v, want false", tt.name, tt.value)
+ }
+ })
+ }
+
+ // Test string defaults (should be empty)
+ stringTests := []struct {
+ name string
+ value string
+ }{
+ {"CfgFile", flags.CfgFile},
+ {"OutputDir", flags.OutputDir},
+ {"BatchFile", flags.BatchFile},
+ {"OpenAIVoice", flags.OpenAIVoice},
+ {"OpenAIInstruction", flags.OpenAIInstruction},
+ }
+
+ for _, tt := range stringTests {
+ t.Run(tt.name, func(t *testing.T) {
+ if tt.value != "" {
+ t.Errorf("%s = %v, want empty string", tt.name, tt.value)
+ }
+ })
+ }
+}
+
+func TestFlagsStructure(t *testing.T) {
+ // Test that Flags struct has all expected fields
+ flags := &Flags{}
+ flagsType := reflect.TypeOf(*flags)
+
+ expectedFields := []string{
+ "CfgFile", "OutputDir", "AudioFormat", "ImageAPI", "BatchFile",
+ "SkipAudio", "SkipImages", "GenerateAnki", "AnkiCSV", "DeckName",
+ "ListModels", "AllVoices", "GUIMode",
+ "OpenAIModel", "OpenAIVoice", "OpenAISpeed", "OpenAIInstruction",
+ "OpenAIImageModel", "OpenAIImageSize", "OpenAIImageQuality", "OpenAIImageStyle",
+ }
+
+ for _, fieldName := range expectedFields {
+ t.Run("has_field_"+fieldName, func(t *testing.T) {
+ if _, ok := flagsType.FieldByName(fieldName); !ok {
+ t.Errorf("Flags struct missing field: %s", fieldName)
+ }
+ })
+ }
+}