Files
ai-media-hub/backend/services/gemini_test.go
T
2026-03-18 13:00:41 +09:00

231 lines
8.2 KiB
Go

package services
import (
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"
)
func TestTranslateQueryFallsBackToGoogleWithoutGeminiKey(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`[[["rainy city street","비 오는 도시 거리",null,null,1]],null,"ko"]`))
}))
defer server.Close()
service := NewGeminiService("")
service.Client = &http.Client{Timeout: 2 * time.Second}
service.TranslateEndpoint = server.URL
translated := service.TranslateQuery("비 오는 도시 거리")
if translated != "rainy city street" {
t.Fatalf("expected google fallback translation, got %q", translated)
}
}
func TestTranslateQueryFallsBackToDictionaryWhenTranslateFails(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
http.Error(w, "boom", http.StatusBadGateway)
}))
defer server.Close()
service := NewGeminiService("")
service.Client = &http.Client{Timeout: 2 * time.Second}
service.TranslateEndpoint = server.URL
translated := service.TranslateQuery("숲속 커플")
if translated != "forest couple" {
t.Fatalf("expected dictionary fallback translation, got %q", translated)
}
}
func TestTranslateSummaryToKoreanUsesGoogleAndCaches(t *testing.T) {
requests := 0
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
requests++
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`[[["도시에서 웃는 커플","smiling couple in city",null,null,1]],null,"en"]`))
}))
defer server.Close()
service := NewGeminiService("")
service.Client = &http.Client{Timeout: 2 * time.Second}
service.TranslateEndpoint = server.URL
first, err := service.TranslateSummaryToKorean("smiling couple in city")
if err != nil {
t.Fatalf("expected translation to succeed, got error: %v", err)
}
second, err := service.TranslateSummaryToKorean("smiling couple in city")
if err != nil {
t.Fatalf("expected cached translation to succeed, got error: %v", err)
}
if first != "도시에서 웃는 커플" || second != first {
t.Fatalf("unexpected translated summary values: %q %q", first, second)
}
if requests != 1 {
t.Fatalf("expected one upstream translation request due to cache, got %d", requests)
}
}
func TestNormalizeKnownMediaPhrases(t *testing.T) {
translated := translateKoreanMediaTerms("사이버 펑크 도시")
if translated != "cyberpunk city" {
t.Fatalf("expected cyberpunk city, got %q", translated)
}
}
func TestBuildSupplementalQueriesReturnsGeneratedLines(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`{"candidates":[{"content":{"parts":[{"text":"authentic couple city walk\ncandid couple park footage\nnatural lifestyle b-roll"}]}}]}`))
}))
defer server.Close()
service := NewGeminiService("dummy-key")
service.Client = &http.Client{Timeout: 2 * time.Second}
service.GenerateEndpoint = server.URL
queries, err := service.BuildSupplementalQueries("다정한 커플", []string{"friendly couple"}, []AIRecommendation{
{Assessment: "irrelevant", SearchHint: "authentic lifestyle couple"},
})
if err != nil {
t.Fatalf("expected supplemental query generation to succeed, got %v", err)
}
if len(queries) < 3 || queries[0] != "authentic couple city walk" {
t.Fatalf("unexpected supplemental queries: %#v", queries)
}
}
func TestSelectUnevaluatedCandidatesSkipsReviewedLinks(t *testing.T) {
ranked := []SearchResult{
{Link: "https://a.example"},
{Link: "https://b.example"},
{Link: "https://c.example"},
}
reviewed := map[string]bool{
"https://a.example": true,
}
selected := SelectUnevaluatedCandidates(ranked, reviewed, 2)
if len(selected) != 2 {
t.Fatalf("expected 2 selected candidates, got %d", len(selected))
}
if selected[0].Link != "https://b.example" || selected[1].Link != "https://c.example" {
t.Fatalf("unexpected selection order: %#v", selected)
}
}
func TestRemainingGeminiCapacityShrinksWithReviewedItems(t *testing.T) {
reviewed := []AIRecommendation{
{Link: "https://a.example"},
{Link: "https://b.example"},
}
if got := RemainingGeminiCapacity(reviewed); got != 22 {
t.Fatalf("expected 22 remaining slots, got %d", got)
}
}
func TestGeminiVisualCacheRoundTrip(t *testing.T) {
service := NewGeminiService("")
service.setCachedVisual("image\nhttps://example.com/thumb.jpg", "abc", "image/jpeg", time.Minute)
data, mimeType, ok := service.getCachedVisual("image\nhttps://example.com/thumb.jpg")
if !ok {
t.Fatal("expected visual cache hit")
}
if data != "abc" || mimeType != "image/jpeg" {
t.Fatalf("unexpected cached visual data: %q %q", data, mimeType)
}
}
func TestGeminiTranslationCacheRoundTrip(t *testing.T) {
service := NewGeminiService("")
service.setCachedTranslation("비 오는 도시", "rainy city", time.Minute)
value, ok := service.getCachedTranslation("비 오는 도시")
if !ok {
t.Fatal("expected translation cache hit")
}
if value != "rainy city" {
t.Fatalf("unexpected translation cache value: %q", value)
}
}
func TestGeminiExpansionCacheRoundTrip(t *testing.T) {
service := NewGeminiService("")
service.setCachedExpansion("city rain", []string{"city rain", "city rain stock footage"}, time.Minute)
value, ok := service.getCachedExpansion("city rain")
if !ok {
t.Fatal("expected expansion cache hit")
}
if len(value) != 2 || value[1] != "city rain stock footage" {
t.Fatalf("unexpected expansion cache value: %#v", value)
}
}
func TestDecorateRecommendationMediaUsesEmbedForGoogleVideo(t *testing.T) {
item := DecorateRecommendationMedia(AIRecommendation{
Source: "Google Video",
Link: "https://www.youtube.com/watch?v=dQw4w9WgXcQ",
ThumbnailURL: "https://i.ytimg.com/vi/dQw4w9WgXcQ/hqdefault.jpg",
})
if item.MediaMode != "thumbnail" {
t.Fatalf("expected thumbnail media mode, got %q", item.MediaMode)
}
if item.EmbedURL == "" || !strings.Contains(item.EmbedURL, "youtube-nocookie.com/embed/") {
t.Fatalf("unexpected embed url: %q", item.EmbedURL)
}
if item.ActionType != "download" || item.ActionLabel != "Direct Download" {
t.Fatalf("unexpected Google Video actions: %#v", item)
}
}
func TestRankSearchResultsPrefersUsableVisuals(t *testing.T) {
results := []SearchResult{
{Title: "cyberpunk city", Link: "https://example.com/a", ThumbnailURL: "https://example.com/favicon.ico"},
{Title: "cyberpunk city", Link: "https://example.com/b", ThumbnailURL: "https://example.com/frame.jpg"},
}
ranked := RankSearchResults("cyberpunk city", results)
if ranked[0].Link != "https://example.com/b" {
t.Fatalf("expected usable thumbnail result first, got %#v", ranked)
}
}
func TestMergeRecommendationsExcludesIrrelevantAndPendingFiller(t *testing.T) {
recommended := []AIRecommendation{
{Title: "keep", Link: "https://a.example", Recommended: true, Assessment: "positive", ThumbnailURL: "https://example.com/a.jpg"},
{Title: "drop", Link: "https://b.example", Recommended: false, Assessment: "irrelevant", ThumbnailURL: "https://example.com/b.jpg", Reason: "관련이 없습니다."},
}
ranked := []SearchResult{
{Title: "keep", Link: "https://a.example", ThumbnailURL: "https://example.com/a.jpg"},
{Title: "extra", Link: "https://c.example", ThumbnailURL: "https://example.com/c.jpg"},
}
merged := MergeRecommendations(recommended, ranked, 16)
if len(merged) != 1 {
t.Fatalf("expected only the positive recommendation without pending filler, got %#v", merged)
}
if merged[0].Link != "https://a.example" {
t.Fatalf("unexpected merged result: %#v", merged)
}
}
func TestFilterHardGeminiErrorsIgnoresLowValueVisualFailures(t *testing.T) {
errs := []string{
"candidate thumbnail is low value",
"no candidate thumbnails or preview frames could be fetched for gemini vision",
"gemini vision JSON extraction failed: no complete JSON object found",
}
filtered := filterHardGeminiErrors(errs)
if len(filtered) != 1 {
t.Fatalf("expected only hard errors to remain, got %#v", filtered)
}
if !strings.Contains(filtered[0], "JSON extraction failed") {
t.Fatalf("unexpected filtered errors: %#v", filtered)
}
}