summaryrefslogtreecommitdiff
path: root/internal
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-04-06 11:34:20 +0300
committerPaul Buetow <paul@buetow.org>2026-04-06 11:34:20 +0300
commit7193ec765f0b51093a203f8f3d00e69008be1cce (patch)
tree8d7df4403b55602c12f3bc85276b55ff705849c5 /internal
parent2d905b109cd2ea3763b3c39f54dbb0f86b4b19ba (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>
Diffstat (limited to 'internal')
-rw-r--r--internal/cli/prompts_test.go45
-rw-r--r--internal/cli/video_runner_test.go44
-rw-r--r--internal/video/veo_test.go115
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)
+ }
+}