diff options
| author | Paul Buetow <paul@buetow.org> | 2026-04-01 15:10:48 +0300 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2026-04-01 15:10:48 +0300 |
| commit | 716e212af67e13a65630920b3afe97f1b3aa8899 (patch) | |
| tree | b313be0878342c1d7e16f36cda9e8a6966ecdd59 | |
| parent | 9eed82a18a7701faa82adaca5c4ec8dba45ade45 (diff) | |
z6: strengthen Nano Banana provider tests
| -rw-r--r-- | internal/image/nanobanana.go | 9 | ||||
| -rw-r--r-- | internal/image/nanobanana_test.go | 210 |
2 files changed, 218 insertions, 1 deletions
diff --git a/internal/image/nanobanana.go b/internal/image/nanobanana.go index c65d8d4..0a49ed7 100644 --- a/internal/image/nanobanana.go +++ b/internal/image/nanobanana.go @@ -110,7 +110,14 @@ func (c *NanoBananaClient) Search(ctx context.Context, opts *SearchOptions) ([]S imageBytes, mimeType, err := nanoBananaGenerateImage(ctx, c, prompt) if err != nil { - return nil, err + if searchErr, ok := err.(*SearchError); ok { + return nil, searchErr + } + return nil, &SearchError{ + Provider: nanoBananaSource, + Code: "API_ERROR", + Message: fmt.Sprintf("failed to generate image: %v", err), + } } dataURL, err := encodeDataURL(imageBytes, mimeType) diff --git a/internal/image/nanobanana_test.go b/internal/image/nanobanana_test.go index 925fcab..39ce208 100644 --- a/internal/image/nanobanana_test.go +++ b/internal/image/nanobanana_test.go @@ -4,6 +4,7 @@ import ( "bytes" "context" "encoding/base64" + "fmt" "image" "image/color" "image/jpeg" @@ -109,6 +110,215 @@ func TestNanoBananaClient_Search_CustomPromptSkipsTextGeneration(t *testing.T) { } } +func TestNanoBananaClient_Search_GeneratedPromptFlow(t *testing.T) { + originalText := nanoBananaGenerateText + originalImage := nanoBananaGenerateImage + originalStyles := append([]string(nil), ArtisticStyles...) + t.Cleanup(func() { + nanoBananaGenerateText = originalText + nanoBananaGenerateImage = originalImage + ArtisticStyles = originalStyles + }) + + ArtisticStyles = []string{"Photorealism"} + + var translationCalls int + var sceneCalls int + var gotPrompt string + var callbackPrompt string + + nanoBananaGenerateText = func(_ context.Context, _ *NanoBananaClient, _, systemPrompt, userPrompt string, temperature float32, maxOutputTokens int32) (string, error) { + switch { + case strings.Contains(systemPrompt, "Bulgarian language expert"): + translationCalls++ + if temperature != 0.3 || maxOutputTokens != 50 { + t.Fatalf("translation params = %v/%d, want 0.3/50", temperature, maxOutputTokens) + } + if !strings.Contains(userPrompt, "ябълка") { + t.Fatalf("translation prompt = %q, want Bulgarian query", userPrompt) + } + return "apple", nil + case strings.Contains(systemPrompt, "educational flashcards for language learning"): + sceneCalls++ + if temperature != 0.7 || maxOutputTokens != 100 { + t.Fatalf("scene params = %v/%d, want 0.7/100", temperature, maxOutputTokens) + } + if !strings.Contains(userPrompt, "apple") { + t.Fatalf("scene prompt = %q, want English translation", userPrompt) + } + return "A bright apple sits centered on a wooden table.", nil + default: + t.Fatalf("unexpected system prompt: %q", systemPrompt) + return "", nil + } + } + + nanoBananaGenerateImage = func(_ context.Context, _ *NanoBananaClient, prompt string) ([]byte, string, error) { + gotPrompt = prompt + return mustJPEGBytes(t), "image/jpeg", nil + } + + client := NewNanoBananaClient(&NanoBananaConfig{APIKey: "test-key"}) + callbackCalled := false + client.SetPromptCallback(func(prompt string) { + callbackCalled = true + callbackPrompt = prompt + }) + + results, err := client.Search(context.Background(), DefaultSearchOptions("ябълка")) + if err != nil { + t.Fatalf("Search() unexpected error: %v", err) + } + if translationCalls != 1 { + t.Fatalf("translationCalls = %d, want 1", translationCalls) + } + if sceneCalls != 1 { + t.Fatalf("sceneCalls = %d, want 1", sceneCalls) + } + if !callbackCalled { + t.Fatal("expected prompt callback to be called") + } + if callbackPrompt != gotPrompt { + t.Fatalf("prompt callback = %q, want %q", callbackPrompt, gotPrompt) + } + if client.GetLastPrompt() != gotPrompt { + t.Fatalf("GetLastPrompt() = %q, want %q", client.GetLastPrompt(), gotPrompt) + } + if len(results) != 1 { + t.Fatalf("expected 1 result, got %d", len(results)) + } + + result := results[0] + if result.Source != nanoBananaSource { + t.Fatalf("Source = %q, want %q", result.Source, nanoBananaSource) + } + if result.Width != nanoBananaImageWidth || result.Height != nanoBananaImageHeight { + t.Fatalf("Size = %dx%d, want %dx%d", result.Width, result.Height, nanoBananaImageWidth, nanoBananaImageHeight) + } + if !strings.Contains(result.Description, "apple") { + t.Fatalf("Description = %q, want translated word", result.Description) + } + if !strings.Contains(gotPrompt, "Generate a Photorealism depicting: A bright apple sits centered on a wooden table.") { + t.Fatalf("Prompt = %q, want generated scene and selected style", gotPrompt) + } + + reader, err := client.Download(context.Background(), result.URL) + if err != nil { + t.Fatalf("Download() unexpected error: %v", err) + } + t.Cleanup(func() { + _ = reader.Close() + }) + + data, err := io.ReadAll(reader) + if err != nil { + t.Fatalf("ReadAll() unexpected error: %v", err) + } + if !bytes.HasPrefix(data, []byte("\x89PNG\r\n\x1a\n")) { + t.Fatalf("Search output was not normalized to PNG") + } +} + +func TestNanoBananaClient_Search_TranslationFailureFallsBackToQuery(t *testing.T) { + originalText := nanoBananaGenerateText + originalImage := nanoBananaGenerateImage + originalStyles := append([]string(nil), ArtisticStyles...) + t.Cleanup(func() { + nanoBananaGenerateText = originalText + nanoBananaGenerateImage = originalImage + ArtisticStyles = originalStyles + }) + + ArtisticStyles = []string{"Photorealism"} + + var sceneSawOriginalQuery bool + nanoBananaGenerateText = func(_ context.Context, _ *NanoBananaClient, _, systemPrompt, userPrompt string, _ float32, _ int32) (string, error) { + if strings.Contains(systemPrompt, "Bulgarian language expert") { + if !strings.Contains(userPrompt, "ябълка") { + t.Fatalf("translation prompt = %q, want Bulgarian query", userPrompt) + } + return "", fmt.Errorf("translation unavailable") + } + if strings.Contains(systemPrompt, "educational flashcards for language learning") { + if !strings.Contains(userPrompt, "ябълка") { + t.Fatalf("scene prompt = %q, want original query fallback", userPrompt) + } + sceneSawOriginalQuery = true + return "Fallback scene", nil + } + t.Fatalf("unexpected system prompt: %q", systemPrompt) + return "", nil + } + + nanoBananaGenerateImage = func(_ context.Context, _ *NanoBananaClient, prompt string) ([]byte, string, error) { + return mustPNGBytes(t), "image/png", nil + } + + client := NewNanoBananaClient(&NanoBananaConfig{APIKey: "test-key"}) + results, err := client.Search(context.Background(), DefaultSearchOptions("ябълка")) + if err != nil { + t.Fatalf("Search() unexpected error: %v", err) + } + if len(results) != 1 { + t.Fatalf("expected 1 result, got %d", len(results)) + } + if !sceneSawOriginalQuery { + t.Fatal("expected scene generation to use the original query after translation failure") + } + if client.GetLastPrompt() == "" { + t.Fatal("expected last prompt to be recorded") + } +} + +func TestNanoBananaClient_Search_ImageGenerationError(t *testing.T) { + originalText := nanoBananaGenerateText + originalImage := nanoBananaGenerateImage + originalStyles := append([]string(nil), ArtisticStyles...) + t.Cleanup(func() { + nanoBananaGenerateText = originalText + nanoBananaGenerateImage = originalImage + ArtisticStyles = originalStyles + }) + + ArtisticStyles = []string{"Photorealism"} + + nanoBananaGenerateText = func(_ context.Context, _ *NanoBananaClient, _, systemPrompt, userPrompt string, _ float32, _ int32) (string, error) { + switch { + case strings.Contains(systemPrompt, "Bulgarian language expert"): + if !strings.Contains(userPrompt, "ябълка") { + t.Fatalf("translation prompt = %q, want Bulgarian query", userPrompt) + } + return "apple", nil + case strings.Contains(systemPrompt, "educational flashcards for language learning"): + if !strings.Contains(userPrompt, "apple") { + t.Fatalf("scene prompt = %q, want translated word", userPrompt) + } + return "A bright apple sits centered on a wooden table.", nil + default: + t.Fatalf("unexpected system prompt: %q", systemPrompt) + return "", nil + } + } + + nanoBananaGenerateImage = func(_ context.Context, _ *NanoBananaClient, _ string) ([]byte, string, error) { + return nil, "", fmt.Errorf("image generation failed") + } + + client := NewNanoBananaClient(&NanoBananaConfig{APIKey: "test-key"}) + _, err := client.Search(context.Background(), DefaultSearchOptions("ябълка")) + if err == nil { + t.Fatal("expected image generation error") + } + + searchErr, ok := err.(*SearchError) + if !ok { + t.Fatalf("expected SearchError, got %T", err) + } + if searchErr.Code != "API_ERROR" { + t.Fatalf("expected API_ERROR, got %s", searchErr.Code) + } +} + func TestNanoBananaClient_Search_InvalidOptions(t *testing.T) { t.Parallel() |
