diff options
| author | Paul Buetow <paul@buetow.org> | 2026-04-06 11:34:20 +0300 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2026-04-06 11:34:20 +0300 |
| commit | 7193ec765f0b51093a203f8f3d00e69008be1cce (patch) | |
| tree | 8d7df4403b55602c12f3bc85276b55ff705849c5 | |
| parent | 2d905b109cd2ea3763b3c39f54dbb0f86b4b19ba (diff) | |
test: add missing unit tests for Veo video feature (task 016)
- internal/video/veo_test.go: add pageNumFromPath table test, loadGalleryImage
multiple-matches edge case, saveMP4 fallback name, and NewVeoGenerator success
path with mock client injection
- internal/cli/prompts_test.go: add parseSelection edge cases — zero max with
"all", page number exceeding max, and whitespace-only input
- internal/cli/video_runner_test.go: new file covering GenerateSelectedVideos
for empty/nil paths (early return) and empty/whitespace API key (error path)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
| -rw-r--r-- | internal/cli/prompts_test.go | 45 | ||||
| -rw-r--r-- | internal/cli/video_runner_test.go | 44 | ||||
| -rw-r--r-- | internal/video/veo_test.go | 115 |
3 files changed, 204 insertions, 0 deletions
diff --git a/internal/cli/prompts_test.go b/internal/cli/prompts_test.go index 053f0b4..7eb6344 100644 --- a/internal/cli/prompts_test.go +++ b/internal/cli/prompts_test.go @@ -266,3 +266,48 @@ func TestFilterPathsByPages_Empty(t *testing.T) { t.Errorf("expected empty result, got %v", got) } } + +// --------------------------------------------------------------------------- +// parseSelection — additional edge cases +// --------------------------------------------------------------------------- + +// TestParseSelection_AllWithZeroMax verifies that "all" with a zero max +// returns an empty (but non-nil) slice, since there are no available pages. +func TestParseSelection_AllWithZeroMax(t *testing.T) { + got, err := parseSelection("all", 0) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if len(got) != 0 { + t.Errorf("parseSelection(\"all\", 0): expected empty slice, got %v", got) + } +} + +// TestParseSelection_PageExceedsMax verifies that a page number larger than +// max is accepted without error — the caller filters against real files. +func TestParseSelection_PageExceedsMax(t *testing.T) { + got, err := parseSelection("99", 5) + if err != nil { + t.Fatalf("unexpected error for page > max: %v", err) + } + want := []int{99} + if len(got) != 1 || got[0] != want[0] { + t.Errorf("parseSelection(\"99\", 5) = %v, want %v", got, want) + } +} + +// TestParseSelection_OnlyWhitespace verifies that a purely whitespace input +// (after trimming) is treated as "all" via the caller, but parseSelection +// itself should not receive it — nonetheless it should not panic. +func TestParseSelection_EmptyAfterTrim(t *testing.T) { + // Empty string after trimming: no tokens, so result is empty, no error. + got, err := parseSelection(" ", 5) + if err != nil { + t.Fatalf("unexpected error for all-whitespace input: %v", err) + } + // "all" is the keyword path — plain whitespace is NOT "all", so the + // comma-split path runs and produces an empty result (all tokens are ""). + if len(got) != 0 { + t.Errorf("parseSelection(\" \", 5): expected empty slice, got %v", got) + } +} diff --git a/internal/cli/video_runner_test.go b/internal/cli/video_runner_test.go new file mode 100644 index 0000000..1fd201d --- /dev/null +++ b/internal/cli/video_runner_test.go @@ -0,0 +1,44 @@ +package cli + +import ( + "testing" +) + +// TestGenerateSelectedVideos_EmptyPaths verifies that GenerateSelectedVideos +// returns nil immediately when no paths are provided, without attempting any +// API calls. +func TestGenerateSelectedVideos_EmptyPaths(t *testing.T) { + err := GenerateSelectedVideos("any-api-key", []string{}) + if err != nil { + t.Fatalf("expected nil for empty paths, got: %v", err) + } +} + +// TestGenerateSelectedVideos_NilPaths verifies that GenerateSelectedVideos +// handles a nil slice the same way as an empty slice. +func TestGenerateSelectedVideos_NilPaths(t *testing.T) { + err := GenerateSelectedVideos("any-api-key", nil) + if err != nil { + t.Fatalf("expected nil for nil paths, got: %v", err) + } +} + +// TestGenerateSelectedVideos_EmptyAPIKey verifies that GenerateSelectedVideos +// returns an error when a non-empty path list is provided but the API key is +// empty. The error originates from video.NewVeoGenerator, so we just check +// that some error is returned without making any real API calls. +func TestGenerateSelectedVideos_EmptyAPIKey(t *testing.T) { + err := GenerateSelectedVideos("", []string{"/some/word_gallery_1.png"}) + if err == nil { + t.Fatal("expected error for empty API key with non-empty paths, got nil") + } +} + +// TestGenerateSelectedVideos_WhitespaceAPIKey verifies that a whitespace-only +// API key is treated equivalently to an empty key when paths are supplied. +func TestGenerateSelectedVideos_WhitespaceAPIKey(t *testing.T) { + err := GenerateSelectedVideos(" ", []string{"/some/word_gallery_1.png"}) + if err == nil { + t.Fatal("expected error for whitespace API key with non-empty paths, got nil") + } +} diff --git a/internal/video/veo_test.go b/internal/video/veo_test.go index 5f7abd9..7facd43 100644 --- a/internal/video/veo_test.go +++ b/internal/video/veo_test.go @@ -156,3 +156,118 @@ func TestSaveMP4_CreatesOutputDir(t *testing.T) { t.Error("expected output directory to be created") } } + +// TestSaveMP4_FallbackName verifies that saveMP4 uses a fallback name when the +// source path lacks a recognisable gallery file name (no .png suffix). +func TestSaveMP4_FallbackName(t *testing.T) { + t.Parallel() + + outDir := t.TempDir() + fakeVideo := []byte("video-data") + + // srcPath with no .png extension triggers the fallback naming path. + got, err := saveMP4(fakeVideo, outDir, "unusual_source", 5) + if err != nil { + t.Fatalf("saveMP4 failed: %v", err) + } + + if !strings.HasSuffix(got, ".mp4") { + t.Errorf("expected .mp4 suffix even for fallback name, got %q", got) + } +} + +// --------------------------------------------------------------------------- +// pageNumFromPath +// --------------------------------------------------------------------------- + +// TestPageNumFromPath verifies that pageNumFromPath correctly extracts the +// gallery page number from various file name patterns. +func TestPageNumFromPath(t *testing.T) { + t.Parallel() + + cases := []struct { + path string + want int + }{ + {"/stories/ябълка/ябълка_gallery_1.png", 1}, + {"/stories/word/word_gallery_10.png", 10}, + // Non-gallery path — should return 0. + {"/stories/word/word_cover.png", 0}, + // Missing trailing number — should return 0. + {"/stories/word/word_gallery_.png", 0}, + // Page number zero — should return 0 (non-positive). + {"/stories/word/word_gallery_0.png", 0}, + // Nested gallery name with multiple "_gallery_" tokens — last one wins. + {"/comics/slug/slug_gallery_3.png", 3}, + } + + for _, tc := range cases { + got := pageNumFromPath(tc.path) + if got != tc.want { + t.Errorf("pageNumFromPath(%q) = %d, want %d", tc.path, got, tc.want) + } + } +} + +// --------------------------------------------------------------------------- +// loadGalleryImage — additional edge cases +// --------------------------------------------------------------------------- + +// TestLoadGalleryImage_MultipleMatchesUsesFirst verifies that when several +// gallery files share the same page number, loadGalleryImage returns the +// lexicographically first match without error. +func TestLoadGalleryImage_MultipleMatchesUsesFirst(t *testing.T) { + t.Parallel() + + dir := t.TempDir() + + // Two files for page 1 — alphabetical order determines which is returned. + files := []string{"aaa_gallery_1.png", "zzz_gallery_1.png"} + for _, name := range files { + if err := os.WriteFile(filepath.Join(dir, name), []byte(name), 0o644); err != nil { + t.Fatalf("setup: %v", err) + } + } + + gotPath, gotBytes, err := loadGalleryImage(dir, 1) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + // filepath.Glob returns results in sorted order, so aaa_gallery_1.png comes first. + expectedName := "aaa_gallery_1.png" + if filepath.Base(gotPath) != expectedName { + t.Errorf("expected first match %q, got %q", expectedName, filepath.Base(gotPath)) + } + if string(gotBytes) != expectedName { + t.Errorf("bytes mismatch: got %q, want %q", gotBytes, expectedName) + } +} + +// --------------------------------------------------------------------------- +// VeoGenerator — constructor with valid mock client +// --------------------------------------------------------------------------- + +// TestNewVeoGenerator_WithMockClient verifies that NewVeoGenerator succeeds +// when the genai client factory does not return an error. +func TestNewVeoGenerator_WithMockClient(t *testing.T) { + t.Parallel() + + orig := newGenaiClient + newGenaiClient = func(_ context.Context, _ *genai.ClientConfig) (*genai.Client, error) { + // Return a zero-value client pointer — sufficient for construction. + return &genai.Client{}, nil + } + t.Cleanup(func() { newGenaiClient = orig }) + + gen, err := NewVeoGenerator("valid-api-key") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if gen == nil { + t.Fatal("expected non-nil VeoGenerator") + } + if gen.model != DefaultVeoModel { + t.Errorf("model: got %q, want %q", gen.model, DefaultVeoModel) + } +} |
