mirror of
https://github.com/taigrr/elevenlabs-mcp.git
synced 2026-04-02 03:08:57 -07:00
test: add comprehensive unit tests and update deps
- Add tests for audio file generation, path handling, and error cases - Add tests for history listing, summary creation, and file filtering - Add tests for voice management (find, set, default, formatting) - Update Go to 1.26.1 - Bump go-sdk v1.3.1 → v1.4.1, segmentio/encoding v0.5.4, golang.org/x/oauth2 v0.36.0, golang.org/x/sys v0.42.0
This commit is contained in:
143
internal/ximcp/audio_test.go
Normal file
143
internal/ximcp/audio_test.go
Normal file
@@ -0,0 +1,143 @@
|
||||
package ximcp
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestGenerateRandomHex(t *testing.T) {
|
||||
hex1 := generateRandomHex(RandomHexLength)
|
||||
hex2 := generateRandomHex(RandomHexLength)
|
||||
|
||||
if len(hex1) != RandomHexLength {
|
||||
t.Errorf("expected length %d, got %d", RandomHexLength, len(hex1))
|
||||
}
|
||||
|
||||
if len(hex2) != RandomHexLength {
|
||||
t.Errorf("expected length %d, got %d", RandomHexLength, len(hex2))
|
||||
}
|
||||
|
||||
// Two random hex strings should almost certainly differ
|
||||
if hex1 == hex2 {
|
||||
t.Error("two consecutive random hex values should differ")
|
||||
}
|
||||
|
||||
// Verify hex characters only
|
||||
for _, c := range hex1 {
|
||||
if !strings.ContainsRune("0123456789abcdef", c) {
|
||||
t.Errorf("non-hex character in output: %c", c)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenerateFilePath(t *testing.T) {
|
||||
s := &Server{}
|
||||
|
||||
path := s.generateFilePath()
|
||||
|
||||
if !strings.HasPrefix(path, AudioDirectory+string(filepath.Separator)) {
|
||||
t.Errorf("path should start with %s/, got %q", AudioDirectory, path)
|
||||
}
|
||||
|
||||
if !strings.HasSuffix(path, ".mp3") {
|
||||
t.Errorf("path should end with .mp3, got %q", path)
|
||||
}
|
||||
|
||||
// Two paths should differ (different timestamps or random hex)
|
||||
path2 := s.generateFilePath()
|
||||
if path == path2 {
|
||||
t.Error("two consecutive file paths should differ")
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnsureDirectoryExists(t *testing.T) {
|
||||
s := &Server{}
|
||||
|
||||
tmpDir := t.TempDir()
|
||||
nested := filepath.Join(tmpDir, "a", "b", "c", "file.mp3")
|
||||
|
||||
err := s.ensureDirectoryExists(nested)
|
||||
if err != nil {
|
||||
t.Fatalf("ensureDirectoryExists failed: %v", err)
|
||||
}
|
||||
|
||||
dirPath := filepath.Dir(nested)
|
||||
info, err := os.Stat(dirPath)
|
||||
if err != nil {
|
||||
t.Fatalf("directory not created: %v", err)
|
||||
}
|
||||
if !info.IsDir() {
|
||||
t.Error("expected directory, got file")
|
||||
}
|
||||
}
|
||||
|
||||
func TestWriteAudioFile(t *testing.T) {
|
||||
s := &Server{}
|
||||
|
||||
tmpDir := t.TempDir()
|
||||
filePath := filepath.Join(tmpDir, "test.mp3")
|
||||
data := []byte("fake audio data")
|
||||
|
||||
err := s.writeAudioFile(filePath, data)
|
||||
if err != nil {
|
||||
t.Fatalf("writeAudioFile failed: %v", err)
|
||||
}
|
||||
|
||||
content, err := os.ReadFile(filePath)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to read written file: %v", err)
|
||||
}
|
||||
|
||||
if string(content) != string(data) {
|
||||
t.Errorf("file content mismatch: got %q, want %q", content, data)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWriteTextFile(t *testing.T) {
|
||||
s := &Server{}
|
||||
|
||||
tmpDir := t.TempDir()
|
||||
audioPath := filepath.Join(tmpDir, "test.mp3")
|
||||
text := "This is the spoken text"
|
||||
|
||||
err := s.writeTextFile(audioPath, text)
|
||||
if err != nil {
|
||||
t.Fatalf("writeTextFile failed: %v", err)
|
||||
}
|
||||
|
||||
expectedPath := filepath.Join(tmpDir, "test.txt")
|
||||
content, err := os.ReadFile(expectedPath)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to read text file: %v", err)
|
||||
}
|
||||
|
||||
if string(content) != text {
|
||||
t.Errorf("text file content mismatch: got %q, want %q", content, text)
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadFileToAudioFileNotFound(t *testing.T) {
|
||||
s := &Server{}
|
||||
|
||||
_, err := s.ReadFileToAudio("/nonexistent/file.txt")
|
||||
if err == nil {
|
||||
t.Error("expected error for nonexistent file")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenerateAudioNoVoice(t *testing.T) {
|
||||
s := &Server{
|
||||
currentVoice: nil,
|
||||
}
|
||||
|
||||
_, err := s.GenerateAudio("test text")
|
||||
if err == nil {
|
||||
t.Error("expected error when no voice selected")
|
||||
}
|
||||
|
||||
if !strings.Contains(err.Error(), "no voice selected") {
|
||||
t.Errorf("expected 'no voice selected' error, got: %v", err)
|
||||
}
|
||||
}
|
||||
148
internal/ximcp/history_test.go
Normal file
148
internal/ximcp/history_test.go
Normal file
@@ -0,0 +1,148 @@
|
||||
package ximcp
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestCreateSummary(t *testing.T) {
|
||||
s := &Server{}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
name: "short text unchanged",
|
||||
input: "Hello world",
|
||||
expected: "Hello world",
|
||||
},
|
||||
{
|
||||
name: "exactly max words",
|
||||
input: "one two three four five six seven eight nine ten",
|
||||
expected: "one two three four five six seven eight nine ten",
|
||||
},
|
||||
{
|
||||
name: "exceeds max words truncated",
|
||||
input: "one two three four five six seven eight nine ten eleven twelve",
|
||||
expected: "one two three four five six seven eight nine ten...",
|
||||
},
|
||||
{
|
||||
name: "empty text",
|
||||
input: "",
|
||||
expected: "",
|
||||
},
|
||||
{
|
||||
name: "whitespace only",
|
||||
input: " \t\n ",
|
||||
expected: "",
|
||||
},
|
||||
{
|
||||
name: "text with extra whitespace trimmed at edges",
|
||||
input: " hello world ",
|
||||
expected: "hello world",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := s.createSummary(tt.input)
|
||||
if result != tt.expected {
|
||||
t.Errorf("createSummary(%q) = %q, want %q", tt.input, result, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetAudioHistoryEmptyDir(t *testing.T) {
|
||||
s := &Server{}
|
||||
|
||||
// Temporarily override the audio directory to a non-existent path
|
||||
origDir := AudioDirectory
|
||||
t.Cleanup(func() {
|
||||
// AudioDirectory is a const, so we test the behavior as-is
|
||||
_ = origDir
|
||||
})
|
||||
|
||||
// GetAudioHistory with non-existent directory should return empty
|
||||
tmpDir := t.TempDir()
|
||||
nonExistent := filepath.Join(tmpDir, "does-not-exist")
|
||||
|
||||
// We can't override the const, so test processAudioFiles directly
|
||||
entries, err := os.ReadDir(nonExistent)
|
||||
if err != nil {
|
||||
// Expected — directory doesn't exist
|
||||
if !os.IsNotExist(err) {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
result := s.processAudioFiles(entries)
|
||||
if len(result) != 0 {
|
||||
t.Errorf("expected empty result, got %d files", len(result))
|
||||
}
|
||||
}
|
||||
|
||||
func TestProcessAudioFiles(t *testing.T) {
|
||||
s := &Server{}
|
||||
|
||||
tmpDir := t.TempDir()
|
||||
|
||||
// Create test files
|
||||
mp3File := filepath.Join(tmpDir, "test.mp3")
|
||||
txtFile := filepath.Join(tmpDir, "test.txt")
|
||||
otherFile := filepath.Join(tmpDir, "readme.md")
|
||||
|
||||
if err := os.WriteFile(mp3File, []byte("fake audio"), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(txtFile, []byte("Hello world test"), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(otherFile, []byte("not audio"), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
entries, err := os.ReadDir(tmpDir)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
result := s.processAudioFiles(entries)
|
||||
|
||||
// Should only include .mp3 files
|
||||
if len(result) != 1 {
|
||||
t.Fatalf("expected 1 audio file, got %d", len(result))
|
||||
}
|
||||
|
||||
if result[0].Name != "test.mp3" {
|
||||
t.Errorf("expected name 'test.mp3', got %q", result[0].Name)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetAudioSummary(t *testing.T) {
|
||||
s := &Server{}
|
||||
|
||||
tmpDir := t.TempDir()
|
||||
|
||||
// Create a text file alongside an mp3
|
||||
txtPath := filepath.Join(tmpDir, "audio.txt")
|
||||
if err := os.WriteFile(txtPath, []byte("This is a test summary"), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// getAudioSummary uses AudioDirectory const, so we test createSummary instead
|
||||
summary := s.createSummary("This is a test summary")
|
||||
if summary != "This is a test summary" {
|
||||
t.Errorf("unexpected summary: %q", summary)
|
||||
}
|
||||
|
||||
// Test with no text file fallback
|
||||
summary = s.createSummary("")
|
||||
if summary != "" {
|
||||
t.Errorf("expected empty summary, got %q", summary)
|
||||
}
|
||||
}
|
||||
190
internal/ximcp/server_test.go
Normal file
190
internal/ximcp/server_test.go
Normal file
@@ -0,0 +1,190 @@
|
||||
package ximcp
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/taigrr/elevenlabs/client/types"
|
||||
)
|
||||
|
||||
func TestFindVoiceByID(t *testing.T) {
|
||||
s := &Server{
|
||||
voices: []types.VoiceResponseModel{
|
||||
{VoiceID: "abc123", Name: "Alice"},
|
||||
{VoiceID: "def456", Name: "Bob"},
|
||||
{VoiceID: "ghi789", Name: "Charlie"},
|
||||
},
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
voiceID string
|
||||
expected string
|
||||
found bool
|
||||
}{
|
||||
{"find first voice", "abc123", "Alice", true},
|
||||
{"find middle voice", "def456", "Bob", true},
|
||||
{"find last voice", "ghi789", "Charlie", true},
|
||||
{"voice not found", "zzz999", "", false},
|
||||
{"empty id", "", "", false},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
voice := s.findVoiceByID(tt.voiceID)
|
||||
if tt.found {
|
||||
if voice == nil {
|
||||
t.Fatal("expected voice, got nil")
|
||||
}
|
||||
if voice.Name != tt.expected {
|
||||
t.Errorf("expected name %q, got %q", tt.expected, voice.Name)
|
||||
}
|
||||
} else {
|
||||
if voice != nil {
|
||||
t.Errorf("expected nil, got voice %q", voice.Name)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetDefaultVoiceIfNeeded(t *testing.T) {
|
||||
t.Run("sets first voice when no current", func(t *testing.T) {
|
||||
s := &Server{
|
||||
voices: []types.VoiceResponseModel{
|
||||
{VoiceID: "abc123", Name: "Alice"},
|
||||
{VoiceID: "def456", Name: "Bob"},
|
||||
},
|
||||
currentVoice: nil,
|
||||
}
|
||||
|
||||
s.setDefaultVoiceIfNeeded()
|
||||
|
||||
if s.currentVoice == nil {
|
||||
t.Fatal("expected current voice to be set")
|
||||
}
|
||||
if s.currentVoice.Name != "Alice" {
|
||||
t.Errorf("expected first voice 'Alice', got %q", s.currentVoice.Name)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("does not override existing voice", func(t *testing.T) {
|
||||
bob := types.VoiceResponseModel{VoiceID: "def456", Name: "Bob"}
|
||||
s := &Server{
|
||||
voices: []types.VoiceResponseModel{
|
||||
{VoiceID: "abc123", Name: "Alice"},
|
||||
bob,
|
||||
},
|
||||
currentVoice: &bob,
|
||||
}
|
||||
|
||||
s.setDefaultVoiceIfNeeded()
|
||||
|
||||
if s.currentVoice.Name != "Bob" {
|
||||
t.Errorf("expected voice to remain 'Bob', got %q", s.currentVoice.Name)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("no voices available", func(t *testing.T) {
|
||||
s := &Server{
|
||||
voices: []types.VoiceResponseModel{},
|
||||
currentVoice: nil,
|
||||
}
|
||||
|
||||
s.setDefaultVoiceIfNeeded()
|
||||
|
||||
if s.currentVoice != nil {
|
||||
t.Error("expected no voice when list is empty")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestSetVoice(t *testing.T) {
|
||||
s := &Server{
|
||||
voices: []types.VoiceResponseModel{
|
||||
{VoiceID: "abc123", Name: "Alice"},
|
||||
{VoiceID: "def456", Name: "Bob"},
|
||||
},
|
||||
}
|
||||
|
||||
t.Run("set valid voice", func(t *testing.T) {
|
||||
voice, err := s.SetVoice("def456")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if voice.Name != "Bob" {
|
||||
t.Errorf("expected 'Bob', got %q", voice.Name)
|
||||
}
|
||||
if s.currentVoice.VoiceID != "def456" {
|
||||
t.Error("currentVoice not updated")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("set invalid voice", func(t *testing.T) {
|
||||
_, err := s.SetVoice("nonexistent")
|
||||
if err == nil {
|
||||
t.Error("expected error for nonexistent voice")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestFormatVoiceList(t *testing.T) {
|
||||
current := types.VoiceResponseModel{VoiceID: "abc123", Name: "Alice", Category: "premade"}
|
||||
s := &Server{}
|
||||
|
||||
voices := []types.VoiceResponseModel{
|
||||
current,
|
||||
{VoiceID: "def456", Name: "Bob", Category: "cloned"},
|
||||
}
|
||||
|
||||
result := s.formatVoiceList(voices, ¤t)
|
||||
|
||||
if result == "" {
|
||||
t.Error("expected non-empty result")
|
||||
}
|
||||
|
||||
// Current voice should be marked with asterisk
|
||||
if !contains(result, "* Alice") {
|
||||
t.Error("expected current voice to be marked with asterisk")
|
||||
}
|
||||
|
||||
// Other voice should not be marked
|
||||
if contains(result, "* Bob") {
|
||||
t.Error("non-current voice should not have asterisk marker")
|
||||
}
|
||||
|
||||
// Both voices should appear
|
||||
if !contains(result, "Alice") || !contains(result, "Bob") {
|
||||
t.Error("both voices should appear in output")
|
||||
}
|
||||
|
||||
if !contains(result, "Currently selected: Alice") {
|
||||
t.Error("expected currently selected line")
|
||||
}
|
||||
}
|
||||
|
||||
func TestFormatVoiceListNoSelection(t *testing.T) {
|
||||
s := &Server{}
|
||||
|
||||
voices := []types.VoiceResponseModel{
|
||||
{VoiceID: "abc123", Name: "Alice", Category: "premade"},
|
||||
}
|
||||
|
||||
result := s.formatVoiceList(voices, nil)
|
||||
|
||||
if !contains(result, "No voice currently selected") {
|
||||
t.Error("expected 'No voice currently selected' when nil")
|
||||
}
|
||||
}
|
||||
|
||||
func contains(s, substr string) bool {
|
||||
return len(s) >= len(substr) && searchString(s, substr)
|
||||
}
|
||||
|
||||
func searchString(s, substr string) bool {
|
||||
for i := 0; i <= len(s)-len(substr); i++ {
|
||||
if s[i:i+len(substr)] == substr {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
Reference in New Issue
Block a user