diff --git a/internal/provider/model_test.go b/internal/provider/model_test.go new file mode 100644 index 0000000..d8f59f7 --- /dev/null +++ b/internal/provider/model_test.go @@ -0,0 +1,331 @@ +package provider + +import ( + "testing" + + "github.com/leonardotrapani/hyprvoice/internal/language" +) + +func TestModel_NeedsDownload(t *testing.T) { + tests := []struct { + name string + model Model + expected bool + }{ + { + name: "local model with LocalInfo", + model: Model{ + ID: "base.en", + Local: true, + LocalInfo: &LocalModelInfo{ + Filename: "ggml-base.en.bin", + Size: "142MB", + DownloadURL: "https://example.com/model.bin", + }, + }, + expected: true, + }, + { + name: "cloud model without LocalInfo", + model: Model{ + ID: "whisper-1", + Local: false, + Endpoint: &EndpointConfig{BaseURL: "https://api.openai.com", Path: "/v1/audio/transcriptions"}, + }, + expected: false, + }, + { + name: "model with nil LocalInfo", + model: Model{ + ID: "gpt-4o", + LocalInfo: nil, + }, + expected: false, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + if got := tc.model.NeedsDownload(); got != tc.expected { + t.Errorf("NeedsDownload() = %v, want %v", got, tc.expected) + } + }) + } +} + +func TestModel_IsStreaming(t *testing.T) { + tests := []struct { + name string + model Model + expected bool + }{ + { + name: "streaming model", + model: Model{ID: "scribe_v1-streaming", Streaming: true}, + expected: true, + }, + { + name: "batch model", + model: Model{ID: "whisper-1", Streaming: false}, + expected: false, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + if got := tc.model.IsStreaming(); got != tc.expected { + t.Errorf("IsStreaming() = %v, want %v", got, tc.expected) + } + }) + } +} + +func TestModel_SupportsLanguage(t *testing.T) { + allCodes := language.AllLanguageCodes() + + multilingualModel := Model{ + ID: "whisper-large-v3", + SupportedLanguages: allCodes, + } + + englishOnlyModel := Model{ + ID: "base.en", + SupportedLanguages: []string{"en"}, + } + + tests := []struct { + name string + model Model + code string + expected bool + }{ + { + name: "multilingual supports en", + model: multilingualModel, + code: "en", + expected: true, + }, + { + name: "multilingual supports es", + model: multilingualModel, + code: "es", + expected: true, + }, + { + name: "multilingual supports zh", + model: multilingualModel, + code: "zh", + expected: true, + }, + { + name: "english-only supports en", + model: englishOnlyModel, + code: "en", + expected: true, + }, + { + name: "english-only does not support es", + model: englishOnlyModel, + code: "es", + expected: false, + }, + { + name: "english-only does not support zh", + model: englishOnlyModel, + code: "zh", + expected: false, + }, + { + name: "auto always supported on multilingual", + model: multilingualModel, + code: "", + expected: true, + }, + { + name: "auto always supported on english-only", + model: englishOnlyModel, + code: "", + expected: true, + }, + { + name: "empty SupportedLanguages still supports auto", + model: Model{ID: "empty", SupportedLanguages: []string{}}, + code: "", + expected: true, + }, + { + name: "empty SupportedLanguages does not support en", + model: Model{ID: "empty", SupportedLanguages: []string{}}, + code: "en", + expected: false, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + if got := tc.model.SupportsLanguage(tc.code); got != tc.expected { + t.Errorf("SupportsLanguage(%q) = %v, want %v", tc.code, got, tc.expected) + } + }) + } +} + +func TestModel_SupportsAllLanguages(t *testing.T) { + allCodes := language.AllLanguageCodes() + + tests := []struct { + name string + model Model + expected bool + }{ + { + name: "model with all 57 languages", + model: Model{ + ID: "whisper-large-v3", + SupportedLanguages: allCodes, + }, + expected: true, + }, + { + name: "english-only model", + model: Model{ + ID: "base.en", + SupportedLanguages: []string{"en"}, + }, + expected: false, + }, + { + name: "model with some languages", + model: Model{ + ID: "partial", + SupportedLanguages: []string{"en", "es", "fr", "de"}, + }, + expected: false, + }, + { + name: "model with empty languages", + model: Model{ + ID: "empty", + SupportedLanguages: []string{}, + }, + expected: false, + }, + { + name: "model with nil languages", + model: Model{ + ID: "nil", + SupportedLanguages: nil, + }, + expected: false, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + if got := tc.model.SupportsAllLanguages(); got != tc.expected { + t.Errorf("SupportsAllLanguages() = %v, want %v", got, tc.expected) + } + }) + } +} + +func TestModelType_Constants(t *testing.T) { + // verify ModelType constants exist and are distinct + if Transcription == LLM { + t.Error("Transcription and LLM should be different") + } + + // verify they're the expected values + if Transcription != 0 { + t.Errorf("Transcription = %d, want 0", Transcription) + } + if LLM != 1 { + t.Errorf("LLM = %d, want 1", LLM) + } +} + +func TestEndpointConfig_Fields(t *testing.T) { + endpoint := EndpointConfig{ + BaseURL: "https://api.openai.com", + Path: "/v1/audio/transcriptions", + } + + if endpoint.BaseURL != "https://api.openai.com" { + t.Errorf("BaseURL = %q, want 'https://api.openai.com'", endpoint.BaseURL) + } + if endpoint.Path != "/v1/audio/transcriptions" { + t.Errorf("Path = %q, want '/v1/audio/transcriptions'", endpoint.Path) + } +} + +func TestLocalModelInfo_Fields(t *testing.T) { + info := LocalModelInfo{ + Filename: "ggml-base.en.bin", + Size: "142MB", + DownloadURL: "https://huggingface.co/ggerganov/whisper.cpp/resolve/main/ggml-base.en.bin", + } + + if info.Filename != "ggml-base.en.bin" { + t.Errorf("Filename = %q, want 'ggml-base.en.bin'", info.Filename) + } + if info.Size != "142MB" { + t.Errorf("Size = %q, want '142MB'", info.Size) + } + if info.DownloadURL != "https://huggingface.co/ggerganov/whisper.cpp/resolve/main/ggml-base.en.bin" { + t.Errorf("DownloadURL = %q", info.DownloadURL) + } +} + +func TestModel_AllFields(t *testing.T) { + // verify all Model struct fields can be set and read correctly + model := Model{ + ID: "test-model", + Name: "Test Model", + Description: "A test model for verification", + Type: Transcription, + Streaming: true, + Local: true, + AdapterType: "test-adapter", + SupportedLanguages: []string{"en", "es"}, + Endpoint: &EndpointConfig{ + BaseURL: "https://api.test.com", + Path: "/v1/test", + }, + LocalInfo: &LocalModelInfo{ + Filename: "test.bin", + Size: "100MB", + DownloadURL: "https://example.com/test.bin", + }, + } + + if model.ID != "test-model" { + t.Errorf("ID = %q, want 'test-model'", model.ID) + } + if model.Name != "Test Model" { + t.Errorf("Name = %q, want 'Test Model'", model.Name) + } + if model.Description != "A test model for verification" { + t.Errorf("Description = %q", model.Description) + } + if model.Type != Transcription { + t.Errorf("Type = %v, want Transcription", model.Type) + } + if !model.Streaming { + t.Error("Streaming should be true") + } + if !model.Local { + t.Error("Local should be true") + } + if model.AdapterType != "test-adapter" { + t.Errorf("AdapterType = %q, want 'test-adapter'", model.AdapterType) + } + if len(model.SupportedLanguages) != 2 { + t.Errorf("SupportedLanguages length = %d, want 2", len(model.SupportedLanguages)) + } + if model.Endpoint == nil { + t.Error("Endpoint should not be nil") + } + if model.LocalInfo == nil { + t.Error("LocalInfo should not be nil") + } +} diff --git a/progress.txt b/progress.txt index 4e38613..b6a2c53 100644 --- a/progress.txt +++ b/progress.txt @@ -62,3 +62,15 @@ Started: Sun Feb 1 12:22:47 AM CET 2026 - Added MockStreamingAdapter and comprehensive tests - Tests verify: start/stop, result accumulation, partial result filtering, error handling, concurrent access - All tests passing with -race flag, typecheck passes + +### Task 7: Write tests for Model, Provider, and interfaces +- Created `internal/provider/model_test.go` +- TestModel_NeedsDownload: local with LocalInfo = true, cloud = false, nil = false +- TestModel_IsStreaming: returns Streaming field value +- TestModel_SupportsLanguage: multilingual supports all, english-only supports en, auto always true +- TestModel_SupportsAllLanguages: true when 57 languages, false otherwise +- TestModelType_Constants: Transcription=0, LLM=1 +- TestEndpointConfig_Fields, TestLocalModelInfo_Fields: struct fields accessible +- TestModel_AllFields: comprehensive struct field test +- provider_test.go already had GetModel, ModelsOfType, FindModelByID, ModelsForLanguage, ValidateModelLanguage tests +- All tests passing, typecheck passes diff --git a/tasks/prd.jsonc b/tasks/prd.jsonc index 30f2866..d9597d9 100644 --- a/tasks/prd.jsonc +++ b/tasks/prd.jsonc @@ -185,7 +185,7 @@ "Language validation helpers tested with proper error messages", "Typecheck passes" ], - "passes": false + "passes": true }, // ============================================================================ // PHASE 2: MIGRATE PROVIDERS TO NEW MODEL STRUCTURE