waveterm/pkg/aiusechat/google/google-summarize_test.go
Copilot 0d04b99b46
Add Google AI file summarization package (#2455)
- [x] Create new directory pkg/aiusechat/google
- [x] Implement SummarizeFile function with:
  - Context parameter for timeout
  - File validation (images, PDFs, text files only)
  - Use gemini-2.5-flash-lite model
  - Configurable API URL and prompt as constants
  - Return (string, usage, error)
- [x] Define Google-specific usage struct
- [x] Test the implementation (all tests pass)
- [x] Verify with existing linting and build
- [x] Run CodeQL security check (no issues found)
- [x] Revert unintended tsunami demo dependency changes

## Summary

Successfully implemented a new Google AI package at
`pkg/aiusechat/google` with:

1. **SummarizeFile function** - A simple request-response API (not
streaming, not SSE)
   - Takes context for timeout
   - Validates file types (images, PDFs, text only) 
   - Enforces file size limits matching wshcmd-ai.go
   - Uses gemini-2.5-flash-lite model
   - Returns (summary string, usage stats, error)

2. **GoogleUsage struct** - Tracks token consumption:
   - PromptTokenCount
   - CachedContentTokenCount  
   - CandidatesTokenCount
   - TotalTokenCount

3. **Configurable constants**:
   - GoogleAPIURL (for reference)
   - SummarizePrompt (customizable prompt)
   - SummarizeModel (gemini-2.5-flash-lite)

4. **Comprehensive tests** - 41.7% coverage with all tests passing
5. **Security verified** - No CodeQL alerts
6. **Package documentation** - doc.go with usage examples

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: sawka <2722291+sawka@users.noreply.github.com>
2025-10-17 17:24:06 -07:00

130 lines
3.4 KiB
Go

// Copyright 2025, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
package google
import (
"context"
"os"
"path/filepath"
"testing"
"time"
)
func TestDetectMimeType(t *testing.T) {
tests := []struct {
name string
data []byte
expected string
}{
{
name: "plain text",
data: []byte("Hello, World!"),
expected: "text/plain",
},
{
name: "empty file",
data: []byte{},
expected: "text/plain",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := detectMimeType(tt.data)
if !containsMimeType(result, tt.expected) {
t.Errorf("detectMimeType() = %v, want to contain %v", result, tt.expected)
}
})
}
}
func containsMimeType(got, want string) bool {
// DetectContentType may return variations like "text/plain; charset=utf-8"
return got == want || (want == "text/plain" && got == "text/plain; charset=utf-8")
}
func TestSummarizeFile_FileNotFound(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
_, _, err := SummarizeFile(ctx, "/nonexistent/file.txt", SummarizeOpts{
APIKey: "fake-api-key",
Mode: ModeQuickSummary,
})
if err == nil {
t.Error("SummarizeFile() expected error for nonexistent file, got nil")
}
}
func TestSummarizeFile_BinaryFile(t *testing.T) {
// Create a temporary binary file
tmpDir := t.TempDir()
binFile := filepath.Join(tmpDir, "test.bin")
// Create binary data (not text, image, or PDF)
binaryData := []byte{0x00, 0x01, 0x02, 0x03, 0x7F, 0x80, 0xFF}
if err := os.WriteFile(binFile, binaryData, 0644); err != nil {
t.Fatalf("Failed to create test file: %v", err)
}
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
_, _, err := SummarizeFile(ctx, binFile, SummarizeOpts{
APIKey: "fake-api-key",
Mode: ModeQuickSummary,
})
if err == nil {
t.Error("SummarizeFile() expected error for binary file, got nil")
}
if err != nil && !containsString(err.Error(), "binary data") {
t.Errorf("SummarizeFile() error = %v, want error containing 'binary data'", err)
}
}
func TestSummarizeFile_FileTooLarge(t *testing.T) {
// Create a temporary text file that exceeds the limit
tmpDir := t.TempDir()
textFile := filepath.Join(tmpDir, "large.txt")
// Create a file larger than 200KB (text file limit)
largeData := make([]byte, 201*1024)
for i := range largeData {
largeData[i] = 'a'
}
if err := os.WriteFile(textFile, largeData, 0644); err != nil {
t.Fatalf("Failed to create test file: %v", err)
}
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
_, _, err := SummarizeFile(ctx, textFile, SummarizeOpts{
APIKey: "fake-api-key",
Mode: ModeQuickSummary,
})
if err == nil {
t.Error("SummarizeFile() expected error for file too large, got nil")
}
if err != nil && !containsString(err.Error(), "exceeds maximum size") {
t.Errorf("SummarizeFile() error = %v, want error containing 'exceeds maximum size'", err)
}
}
func containsString(s, substr string) bool {
return len(s) >= len(substr) && (s == substr || len(substr) == 0 ||
(len(s) > 0 && len(substr) > 0 && stringContains(s, substr)))
}
func stringContains(s, substr string) bool {
for i := 0; i <= len(s)-len(substr); i++ {
if s[i:i+len(substr)] == substr {
return true
}
}
return false
}
// Note: We don't test the actual API call without a real API key
// Integration tests would require setting GOOGLE_API_KEY environment variable