add tests
This commit is contained in:
@@ -262,6 +262,7 @@ timeout = "5m" # Maximum recording duration (prevents runaway record
|
|||||||
```
|
```
|
||||||
|
|
||||||
**Recording Timeout:**
|
**Recording Timeout:**
|
||||||
|
|
||||||
- Prevents accidental long recordings that could consume resources
|
- Prevents accidental long recordings that could consume resources
|
||||||
- Default: 5 minutes (`"5m"`)
|
- Default: 5 minutes (`"5m"`)
|
||||||
- Format: Go duration strings like `"30s"`, `"2m"`, `"10m"`
|
- Format: Go duration strings like `"30s"`, `"2m"`, `"10m"`
|
||||||
|
|||||||
@@ -0,0 +1,443 @@
|
|||||||
|
package bus
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestPidManager_CheckExisting(t *testing.T) {
|
||||||
|
// Test with no existing PID file
|
||||||
|
t.Run("no existing PID file", func(t *testing.T) {
|
||||||
|
tempDir := t.TempDir()
|
||||||
|
originalCacheDir := os.Getenv("XDG_CACHE_HOME")
|
||||||
|
os.Setenv("XDG_CACHE_HOME", tempDir)
|
||||||
|
defer func() {
|
||||||
|
if originalCacheDir == "" {
|
||||||
|
os.Unsetenv("XDG_CACHE_HOME")
|
||||||
|
} else {
|
||||||
|
os.Setenv("XDG_CACHE_HOME", originalCacheDir)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
pm, err := newPidManager()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create PID manager: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = pm.checkExisting()
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("checkExisting() error = %v, want no error", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Test with invalid PID in file
|
||||||
|
t.Run("invalid PID in file", func(t *testing.T) {
|
||||||
|
tempDir := t.TempDir()
|
||||||
|
originalCacheDir := os.Getenv("XDG_CACHE_HOME")
|
||||||
|
os.Setenv("XDG_CACHE_HOME", tempDir)
|
||||||
|
defer func() {
|
||||||
|
if originalCacheDir == "" {
|
||||||
|
os.Unsetenv("XDG_CACHE_HOME")
|
||||||
|
} else {
|
||||||
|
os.Setenv("XDG_CACHE_HOME", originalCacheDir)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Create PID file with invalid content
|
||||||
|
pidPath := filepath.Join(tempDir, "hyprvoice", PidName)
|
||||||
|
os.MkdirAll(filepath.Dir(pidPath), 0755)
|
||||||
|
err := os.WriteFile(pidPath, []byte("invalid"), 0644)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create PID file: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
pm, err := newPidManager()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create PID manager: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = pm.checkExisting()
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("checkExisting() error = %v, want no error", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPidManager_Create(t *testing.T) {
|
||||||
|
tempDir := t.TempDir()
|
||||||
|
originalCacheDir := os.Getenv("XDG_CACHE_HOME")
|
||||||
|
os.Setenv("XDG_CACHE_HOME", tempDir)
|
||||||
|
defer func() {
|
||||||
|
if originalCacheDir == "" {
|
||||||
|
os.Unsetenv("XDG_CACHE_HOME")
|
||||||
|
} else {
|
||||||
|
os.Setenv("XDG_CACHE_HOME", originalCacheDir)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
pm, err := newPidManager()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create PID manager: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = pm.create()
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("create() error = %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify PID file was created
|
||||||
|
pidPath := filepath.Join(tempDir, "hyprvoice", PidName)
|
||||||
|
if _, err := os.Stat(pidPath); os.IsNotExist(err) {
|
||||||
|
t.Errorf("create() did not create PID file")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPidManager_Remove(t *testing.T) {
|
||||||
|
tempDir := t.TempDir()
|
||||||
|
originalCacheDir := os.Getenv("XDG_CACHE_HOME")
|
||||||
|
os.Setenv("XDG_CACHE_HOME", tempDir)
|
||||||
|
defer func() {
|
||||||
|
if originalCacheDir == "" {
|
||||||
|
os.Unsetenv("XDG_CACHE_HOME")
|
||||||
|
} else {
|
||||||
|
os.Setenv("XDG_CACHE_HOME", originalCacheDir)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Create a PID file first
|
||||||
|
pidPath := filepath.Join(tempDir, "hyprvoice", PidName)
|
||||||
|
os.MkdirAll(filepath.Dir(pidPath), 0755)
|
||||||
|
err := os.WriteFile(pidPath, []byte("1234"), 0644)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create PID file: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
pm, err := newPidManager()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create PID manager: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = pm.remove()
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("remove() error = %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify PID file was removed
|
||||||
|
if _, err := os.Stat(pidPath); !os.IsNotExist(err) {
|
||||||
|
t.Errorf("remove() did not remove PID file")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSocketManager_Listen(t *testing.T) {
|
||||||
|
tempDir := t.TempDir()
|
||||||
|
originalCacheDir := os.Getenv("XDG_CACHE_HOME")
|
||||||
|
os.Setenv("XDG_CACHE_HOME", tempDir)
|
||||||
|
defer func() {
|
||||||
|
if originalCacheDir == "" {
|
||||||
|
os.Unsetenv("XDG_CACHE_HOME")
|
||||||
|
} else {
|
||||||
|
os.Setenv("XDG_CACHE_HOME", originalCacheDir)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
sm, err := newSocketManager()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create socket manager: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
listener, err := sm.listen()
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("listen() error = %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer listener.Close()
|
||||||
|
|
||||||
|
// Verify socket file was created
|
||||||
|
sockPath := filepath.Join(tempDir, "hyprvoice", SockName)
|
||||||
|
if _, err := os.Stat(sockPath); os.IsNotExist(err) {
|
||||||
|
t.Errorf("listen() did not create socket file")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSocketManager_Dial(t *testing.T) {
|
||||||
|
tempDir := t.TempDir()
|
||||||
|
originalCacheDir := os.Getenv("XDG_CACHE_HOME")
|
||||||
|
os.Setenv("XDG_CACHE_HOME", tempDir)
|
||||||
|
defer func() {
|
||||||
|
if originalCacheDir == "" {
|
||||||
|
os.Unsetenv("XDG_CACHE_HOME")
|
||||||
|
} else {
|
||||||
|
os.Setenv("XDG_CACHE_HOME", originalCacheDir)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Start a test server
|
||||||
|
sm, err := newSocketManager()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create socket manager: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
listener, err := sm.listen()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to start listener: %v", err)
|
||||||
|
}
|
||||||
|
defer listener.Close()
|
||||||
|
|
||||||
|
// Accept connections in background
|
||||||
|
go func() {
|
||||||
|
for {
|
||||||
|
conn, err := listener.Accept()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
conn.Close()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Give the server a moment to start
|
||||||
|
time.Sleep(10 * time.Millisecond)
|
||||||
|
|
||||||
|
// Test dialing
|
||||||
|
conn, err := sm.dial()
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("dial() error = %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer conn.Close()
|
||||||
|
|
||||||
|
// Verify connection is working
|
||||||
|
if conn == nil {
|
||||||
|
t.Errorf("dial() returned nil connection")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSendCommand(t *testing.T) {
|
||||||
|
tempDir := t.TempDir()
|
||||||
|
originalCacheDir := os.Getenv("XDG_CACHE_HOME")
|
||||||
|
os.Setenv("XDG_CACHE_HOME", tempDir)
|
||||||
|
defer func() {
|
||||||
|
if originalCacheDir == "" {
|
||||||
|
os.Unsetenv("XDG_CACHE_HOME")
|
||||||
|
} else {
|
||||||
|
os.Setenv("XDG_CACHE_HOME", originalCacheDir)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Start a test server
|
||||||
|
sm, err := newSocketManager()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create socket manager: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
listener, err := sm.listen()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to start listener: %v", err)
|
||||||
|
}
|
||||||
|
defer listener.Close()
|
||||||
|
|
||||||
|
// Handle test commands
|
||||||
|
go func() {
|
||||||
|
for {
|
||||||
|
conn, err := listener.Accept()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read command
|
||||||
|
buf := make([]byte, 1)
|
||||||
|
n, err := conn.Read(buf)
|
||||||
|
if err != nil || n != 1 {
|
||||||
|
conn.Close()
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Respond based on command
|
||||||
|
var response string
|
||||||
|
switch buf[0] {
|
||||||
|
case 't':
|
||||||
|
response = "OK toggled\n"
|
||||||
|
case 's':
|
||||||
|
response = "STATUS status=idle\n"
|
||||||
|
case 'v':
|
||||||
|
response = "STATUS proto=0.1\n"
|
||||||
|
case 'q':
|
||||||
|
response = "OK quitting\n"
|
||||||
|
default:
|
||||||
|
response = "ERR unknown\n"
|
||||||
|
}
|
||||||
|
|
||||||
|
conn.Write([]byte(response))
|
||||||
|
conn.Close()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Give the server a moment to start
|
||||||
|
time.Sleep(10 * time.Millisecond)
|
||||||
|
|
||||||
|
// Test toggle command
|
||||||
|
response, err := SendCommand('t')
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("SendCommand() error = %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if response != "OK toggled\n" {
|
||||||
|
t.Errorf("SendCommand() = %q, want %q", response, "OK toggled\n")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCheckExistingDaemon(t *testing.T) {
|
||||||
|
// Test with no existing daemon
|
||||||
|
t.Run("no existing daemon", func(t *testing.T) {
|
||||||
|
tempDir := t.TempDir()
|
||||||
|
originalCacheDir := os.Getenv("XDG_CACHE_HOME")
|
||||||
|
os.Setenv("XDG_CACHE_HOME", tempDir)
|
||||||
|
defer func() {
|
||||||
|
if originalCacheDir == "" {
|
||||||
|
os.Unsetenv("XDG_CACHE_HOME")
|
||||||
|
} else {
|
||||||
|
os.Setenv("XDG_CACHE_HOME", originalCacheDir)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
err := CheckExistingDaemon()
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("CheckExistingDaemon() error = %v, want no error", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCreatePidFile(t *testing.T) {
|
||||||
|
tempDir := t.TempDir()
|
||||||
|
originalCacheDir := os.Getenv("XDG_CACHE_HOME")
|
||||||
|
os.Setenv("XDG_CACHE_HOME", tempDir)
|
||||||
|
defer func() {
|
||||||
|
if originalCacheDir == "" {
|
||||||
|
os.Unsetenv("XDG_CACHE_HOME")
|
||||||
|
} else {
|
||||||
|
os.Setenv("XDG_CACHE_HOME", originalCacheDir)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
err := CreatePidFile()
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("CreatePidFile() error = %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify PID file was created
|
||||||
|
pidPath := filepath.Join(tempDir, "hyprvoice", PidName)
|
||||||
|
if _, err := os.Stat(pidPath); os.IsNotExist(err) {
|
||||||
|
t.Errorf("CreatePidFile() did not create PID file")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRemovePidFile(t *testing.T) {
|
||||||
|
tempDir := t.TempDir()
|
||||||
|
originalCacheDir := os.Getenv("XDG_CACHE_HOME")
|
||||||
|
os.Setenv("XDG_CACHE_HOME", tempDir)
|
||||||
|
defer func() {
|
||||||
|
if originalCacheDir == "" {
|
||||||
|
os.Unsetenv("XDG_CACHE_HOME")
|
||||||
|
} else {
|
||||||
|
os.Setenv("XDG_CACHE_HOME", originalCacheDir)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Create a PID file first
|
||||||
|
pidPath := filepath.Join(tempDir, "hyprvoice", PidName)
|
||||||
|
os.MkdirAll(filepath.Dir(pidPath), 0755)
|
||||||
|
err := os.WriteFile(pidPath, []byte("1234"), 0644)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create PID file: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = RemovePidFile()
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("RemovePidFile() error = %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify PID file was removed
|
||||||
|
if _, err := os.Stat(pidPath); !os.IsNotExist(err) {
|
||||||
|
t.Errorf("RemovePidFile() did not remove PID file")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestListen(t *testing.T) {
|
||||||
|
tempDir := t.TempDir()
|
||||||
|
originalCacheDir := os.Getenv("XDG_CACHE_HOME")
|
||||||
|
os.Setenv("XDG_CACHE_HOME", tempDir)
|
||||||
|
defer func() {
|
||||||
|
if originalCacheDir == "" {
|
||||||
|
os.Unsetenv("XDG_CACHE_HOME")
|
||||||
|
} else {
|
||||||
|
os.Setenv("XDG_CACHE_HOME", originalCacheDir)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
listener, err := Listen()
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Listen() error = %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer listener.Close()
|
||||||
|
|
||||||
|
// Verify socket file was created
|
||||||
|
sockPath := filepath.Join(tempDir, "hyprvoice", SockName)
|
||||||
|
if _, err := os.Stat(sockPath); os.IsNotExist(err) {
|
||||||
|
t.Errorf("Listen() did not create socket file")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDial(t *testing.T) {
|
||||||
|
tempDir := t.TempDir()
|
||||||
|
originalCacheDir := os.Getenv("XDG_CACHE_HOME")
|
||||||
|
os.Setenv("XDG_CACHE_HOME", tempDir)
|
||||||
|
defer func() {
|
||||||
|
if originalCacheDir == "" {
|
||||||
|
os.Unsetenv("XDG_CACHE_HOME")
|
||||||
|
} else {
|
||||||
|
os.Setenv("XDG_CACHE_HOME", originalCacheDir)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Start a test server
|
||||||
|
listener, err := Listen()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to start listener: %v", err)
|
||||||
|
}
|
||||||
|
defer listener.Close()
|
||||||
|
|
||||||
|
// Accept connections in background
|
||||||
|
go func() {
|
||||||
|
for {
|
||||||
|
conn, err := listener.Accept()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
conn.Close()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Give the server a moment to start
|
||||||
|
time.Sleep(10 * time.Millisecond)
|
||||||
|
|
||||||
|
// Test dialing
|
||||||
|
conn, err := Dial()
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Dial() error = %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer conn.Close()
|
||||||
|
|
||||||
|
// Verify connection is working
|
||||||
|
if conn == nil {
|
||||||
|
t.Errorf("Dial() returned nil connection")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,776 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// createTestConfig returns a valid configuration for testing
|
||||||
|
func createTestConfig() *Config {
|
||||||
|
return &Config{
|
||||||
|
Recording: RecordingConfig{
|
||||||
|
SampleRate: 16000,
|
||||||
|
Channels: 1,
|
||||||
|
Format: "s16",
|
||||||
|
BufferSize: 8192,
|
||||||
|
Device: "",
|
||||||
|
ChannelBufferSize: 30,
|
||||||
|
Timeout: 5 * time.Minute,
|
||||||
|
},
|
||||||
|
Transcription: TranscriptionConfig{
|
||||||
|
Provider: "openai",
|
||||||
|
APIKey: "test-api-key",
|
||||||
|
Language: "",
|
||||||
|
Model: "whisper-1",
|
||||||
|
},
|
||||||
|
Injection: InjectionConfig{
|
||||||
|
Mode: "fallback",
|
||||||
|
RestoreClipboard: true,
|
||||||
|
WtypeTimeout: 5 * time.Second,
|
||||||
|
ClipboardTimeout: 3 * time.Second,
|
||||||
|
},
|
||||||
|
Notifications: NotificationsConfig{
|
||||||
|
Enabled: true,
|
||||||
|
Type: "log",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// createTestConfigWithInvalidValues returns a config with invalid values for testing validation
|
||||||
|
func createTestConfigWithInvalidValues() *Config {
|
||||||
|
return &Config{
|
||||||
|
Recording: RecordingConfig{
|
||||||
|
SampleRate: 0, // Invalid
|
||||||
|
Channels: 0, // Invalid
|
||||||
|
Format: "", // Invalid
|
||||||
|
BufferSize: 0, // Invalid
|
||||||
|
ChannelBufferSize: 0, // Invalid
|
||||||
|
Timeout: 0, // Invalid
|
||||||
|
},
|
||||||
|
Transcription: TranscriptionConfig{
|
||||||
|
Provider: "", // Invalid
|
||||||
|
APIKey: "", // Invalid
|
||||||
|
Model: "", // Invalid
|
||||||
|
},
|
||||||
|
Injection: InjectionConfig{
|
||||||
|
Mode: "invalid", // Invalid
|
||||||
|
WtypeTimeout: 0, // Invalid
|
||||||
|
ClipboardTimeout: 0, // Invalid
|
||||||
|
},
|
||||||
|
Notifications: NotificationsConfig{
|
||||||
|
Type: "invalid", // Invalid
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConfig_Validate(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
config *Config
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "valid config",
|
||||||
|
config: createTestConfig(),
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid config",
|
||||||
|
config: createTestConfigWithInvalidValues(),
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid recording sample rate",
|
||||||
|
config: &Config{
|
||||||
|
Recording: RecordingConfig{
|
||||||
|
SampleRate: 0,
|
||||||
|
Channels: 1,
|
||||||
|
Format: "s16",
|
||||||
|
BufferSize: 8192,
|
||||||
|
ChannelBufferSize: 30,
|
||||||
|
Timeout: time.Minute,
|
||||||
|
},
|
||||||
|
Transcription: TranscriptionConfig{
|
||||||
|
Provider: "openai",
|
||||||
|
APIKey: "test-key",
|
||||||
|
Model: "whisper-1",
|
||||||
|
},
|
||||||
|
Injection: InjectionConfig{
|
||||||
|
Mode: "fallback",
|
||||||
|
WtypeTimeout: time.Second,
|
||||||
|
ClipboardTimeout: time.Second,
|
||||||
|
},
|
||||||
|
Notifications: NotificationsConfig{
|
||||||
|
Type: "log",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid transcription provider",
|
||||||
|
config: &Config{
|
||||||
|
Recording: RecordingConfig{
|
||||||
|
SampleRate: 16000,
|
||||||
|
Channels: 1,
|
||||||
|
Format: "s16",
|
||||||
|
BufferSize: 8192,
|
||||||
|
ChannelBufferSize: 30,
|
||||||
|
Timeout: time.Minute,
|
||||||
|
},
|
||||||
|
Transcription: TranscriptionConfig{
|
||||||
|
Provider: "",
|
||||||
|
APIKey: "test-key",
|
||||||
|
Model: "whisper-1",
|
||||||
|
},
|
||||||
|
Injection: InjectionConfig{
|
||||||
|
Mode: "fallback",
|
||||||
|
WtypeTimeout: time.Second,
|
||||||
|
ClipboardTimeout: time.Second,
|
||||||
|
},
|
||||||
|
Notifications: NotificationsConfig{
|
||||||
|
Type: "log",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid injection mode",
|
||||||
|
config: &Config{
|
||||||
|
Recording: RecordingConfig{
|
||||||
|
SampleRate: 16000,
|
||||||
|
Channels: 1,
|
||||||
|
Format: "s16",
|
||||||
|
BufferSize: 8192,
|
||||||
|
ChannelBufferSize: 30,
|
||||||
|
Timeout: time.Minute,
|
||||||
|
},
|
||||||
|
Transcription: TranscriptionConfig{
|
||||||
|
Provider: "openai",
|
||||||
|
APIKey: "test-key",
|
||||||
|
Model: "whisper-1",
|
||||||
|
},
|
||||||
|
Injection: InjectionConfig{
|
||||||
|
Mode: "invalid",
|
||||||
|
WtypeTimeout: time.Second,
|
||||||
|
ClipboardTimeout: time.Second,
|
||||||
|
},
|
||||||
|
Notifications: NotificationsConfig{
|
||||||
|
Type: "log",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid notification type",
|
||||||
|
config: &Config{
|
||||||
|
Recording: RecordingConfig{
|
||||||
|
SampleRate: 16000,
|
||||||
|
Channels: 1,
|
||||||
|
Format: "s16",
|
||||||
|
BufferSize: 8192,
|
||||||
|
ChannelBufferSize: 30,
|
||||||
|
Timeout: time.Minute,
|
||||||
|
},
|
||||||
|
Transcription: TranscriptionConfig{
|
||||||
|
Provider: "openai",
|
||||||
|
APIKey: "test-key",
|
||||||
|
Model: "whisper-1",
|
||||||
|
},
|
||||||
|
Injection: InjectionConfig{
|
||||||
|
Mode: "fallback",
|
||||||
|
WtypeTimeout: time.Second,
|
||||||
|
ClipboardTimeout: time.Second,
|
||||||
|
},
|
||||||
|
Notifications: NotificationsConfig{
|
||||||
|
Type: "invalid",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "valid language codes",
|
||||||
|
config: &Config{
|
||||||
|
Recording: RecordingConfig{
|
||||||
|
SampleRate: 16000,
|
||||||
|
Channels: 1,
|
||||||
|
Format: "s16",
|
||||||
|
BufferSize: 8192,
|
||||||
|
ChannelBufferSize: 30,
|
||||||
|
Timeout: time.Minute,
|
||||||
|
},
|
||||||
|
Transcription: TranscriptionConfig{
|
||||||
|
Provider: "openai",
|
||||||
|
APIKey: "test-key",
|
||||||
|
Language: "en",
|
||||||
|
Model: "whisper-1",
|
||||||
|
},
|
||||||
|
Injection: InjectionConfig{
|
||||||
|
Mode: "fallback",
|
||||||
|
WtypeTimeout: time.Second,
|
||||||
|
ClipboardTimeout: time.Second,
|
||||||
|
},
|
||||||
|
Notifications: NotificationsConfig{
|
||||||
|
Type: "log",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid language code",
|
||||||
|
config: &Config{
|
||||||
|
Recording: RecordingConfig{
|
||||||
|
SampleRate: 16000,
|
||||||
|
Channels: 1,
|
||||||
|
Format: "s16",
|
||||||
|
BufferSize: 8192,
|
||||||
|
ChannelBufferSize: 30,
|
||||||
|
Timeout: time.Minute,
|
||||||
|
},
|
||||||
|
Transcription: TranscriptionConfig{
|
||||||
|
Provider: "openai",
|
||||||
|
APIKey: "test-key",
|
||||||
|
Language: "invalid",
|
||||||
|
Model: "whisper-1",
|
||||||
|
},
|
||||||
|
Injection: InjectionConfig{
|
||||||
|
Mode: "fallback",
|
||||||
|
WtypeTimeout: time.Second,
|
||||||
|
ClipboardTimeout: time.Second,
|
||||||
|
},
|
||||||
|
Notifications: NotificationsConfig{
|
||||||
|
Type: "log",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
err := tt.config.Validate()
|
||||||
|
if (err != nil) != tt.wantErr {
|
||||||
|
t.Errorf("Config.Validate() error = %v, wantErr %v", err, tt.wantErr)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConfig_Load(t *testing.T) {
|
||||||
|
// Test that Load creates default config when none exists
|
||||||
|
t.Run("creates default config when none exists", func(t *testing.T) {
|
||||||
|
tempDir := t.TempDir()
|
||||||
|
originalConfigDir := os.Getenv("XDG_CONFIG_HOME")
|
||||||
|
os.Setenv("XDG_CONFIG_HOME", tempDir)
|
||||||
|
defer func() {
|
||||||
|
if originalConfigDir == "" {
|
||||||
|
os.Unsetenv("XDG_CONFIG_HOME")
|
||||||
|
} else {
|
||||||
|
os.Setenv("XDG_CONFIG_HOME", originalConfigDir)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
config, err := Load()
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Load() error = %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify the loaded config is valid
|
||||||
|
if err := config.Validate(); err != nil {
|
||||||
|
t.Errorf("Loaded config is invalid: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify config file was created
|
||||||
|
configPath := filepath.Join(tempDir, "hyprvoice", "config.toml")
|
||||||
|
if _, err := os.Stat(configPath); os.IsNotExist(err) {
|
||||||
|
t.Errorf("Load() did not create config file")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Test that Load works with existing valid config
|
||||||
|
t.Run("loads existing valid config", func(t *testing.T) {
|
||||||
|
tempDir := t.TempDir()
|
||||||
|
configPath := filepath.Join(tempDir, "hyprvoice", "config.toml")
|
||||||
|
|
||||||
|
// Create directory and config file
|
||||||
|
err := os.MkdirAll(filepath.Dir(configPath), 0755)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create config directory: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
validConfig := `[recording]
|
||||||
|
sample_rate = 16000
|
||||||
|
channels = 1
|
||||||
|
format = "s16"
|
||||||
|
buffer_size = 8192
|
||||||
|
channel_buffer_size = 30
|
||||||
|
timeout = "5m"
|
||||||
|
|
||||||
|
[transcription]
|
||||||
|
provider = "openai"
|
||||||
|
api_key = "test-key"
|
||||||
|
model = "whisper-1"
|
||||||
|
|
||||||
|
[injection]
|
||||||
|
mode = "fallback"
|
||||||
|
wtype_timeout = "5s"
|
||||||
|
clipboard_timeout = "3s"
|
||||||
|
|
||||||
|
[notifications]
|
||||||
|
enabled = true
|
||||||
|
type = "log"`
|
||||||
|
|
||||||
|
err = os.WriteFile(configPath, []byte(validConfig), 0644)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create config file: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
originalConfigDir := os.Getenv("XDG_CONFIG_HOME")
|
||||||
|
os.Setenv("XDG_CONFIG_HOME", tempDir)
|
||||||
|
defer func() {
|
||||||
|
if originalConfigDir == "" {
|
||||||
|
os.Unsetenv("XDG_CONFIG_HOME")
|
||||||
|
} else {
|
||||||
|
os.Setenv("XDG_CONFIG_HOME", originalConfigDir)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
config, err := Load()
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Load() error = %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify the loaded config is valid
|
||||||
|
if err := config.Validate(); err != nil {
|
||||||
|
t.Errorf("Loaded config is invalid: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify specific values were loaded
|
||||||
|
if config.Recording.SampleRate != 16000 {
|
||||||
|
t.Errorf("Expected SampleRate 16000, got %d", config.Recording.SampleRate)
|
||||||
|
}
|
||||||
|
if config.Transcription.Provider != "openai" {
|
||||||
|
t.Errorf("Expected Provider 'openai', got %s", config.Transcription.Provider)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConfig_SaveDefaultConfig(t *testing.T) {
|
||||||
|
// Override the config path by setting environment variable
|
||||||
|
tempDir := t.TempDir()
|
||||||
|
originalConfigDir := os.Getenv("XDG_CONFIG_HOME")
|
||||||
|
os.Setenv("XDG_CONFIG_HOME", tempDir)
|
||||||
|
defer func() {
|
||||||
|
if originalConfigDir == "" {
|
||||||
|
os.Unsetenv("XDG_CONFIG_HOME")
|
||||||
|
} else {
|
||||||
|
os.Setenv("XDG_CONFIG_HOME", originalConfigDir)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
err := SaveDefaultConfig()
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("SaveDefaultConfig() error = %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify file was created
|
||||||
|
configPath := filepath.Join(tempDir, "hyprvoice", "config.toml")
|
||||||
|
if _, err := os.Stat(configPath); os.IsNotExist(err) {
|
||||||
|
t.Errorf("SaveDefaultConfig() did not create config file")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify file content
|
||||||
|
content, err := os.ReadFile(configPath)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Failed to read created config file: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(content) == 0 {
|
||||||
|
t.Errorf("SaveDefaultConfig() created empty config file")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify it's valid TOML
|
||||||
|
config, err := Load()
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("SaveDefaultConfig() created invalid config: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify validation passes
|
||||||
|
if err := config.Validate(); err != nil {
|
||||||
|
t.Errorf("SaveDefaultConfig() created invalid config: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConfig_ConversionMethods(t *testing.T) {
|
||||||
|
config := createTestConfig()
|
||||||
|
|
||||||
|
t.Run("ToRecordingConfig", func(t *testing.T) {
|
||||||
|
recordingConfig := config.ToRecordingConfig()
|
||||||
|
|
||||||
|
if recordingConfig.SampleRate != config.Recording.SampleRate {
|
||||||
|
t.Errorf("SampleRate mismatch: got %d, want %d", recordingConfig.SampleRate, config.Recording.SampleRate)
|
||||||
|
}
|
||||||
|
if recordingConfig.Channels != config.Recording.Channels {
|
||||||
|
t.Errorf("Channels mismatch: got %d, want %d", recordingConfig.Channels, config.Recording.Channels)
|
||||||
|
}
|
||||||
|
if recordingConfig.Format != config.Recording.Format {
|
||||||
|
t.Errorf("Format mismatch: got %s, want %s", recordingConfig.Format, config.Recording.Format)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("ToTranscriberConfig", func(t *testing.T) {
|
||||||
|
transcriberConfig := config.ToTranscriberConfig()
|
||||||
|
|
||||||
|
if transcriberConfig.Provider != config.Transcription.Provider {
|
||||||
|
t.Errorf("Provider mismatch: got %s, want %s", transcriberConfig.Provider, config.Transcription.Provider)
|
||||||
|
}
|
||||||
|
if transcriberConfig.APIKey != config.Transcription.APIKey {
|
||||||
|
t.Errorf("APIKey mismatch: got %s, want %s", transcriberConfig.APIKey, config.Transcription.APIKey)
|
||||||
|
}
|
||||||
|
if transcriberConfig.Language != config.Transcription.Language {
|
||||||
|
t.Errorf("Language mismatch: got %s, want %s", transcriberConfig.Language, config.Transcription.Language)
|
||||||
|
}
|
||||||
|
if transcriberConfig.Model != config.Transcription.Model {
|
||||||
|
t.Errorf("Model mismatch: got %s, want %s", transcriberConfig.Model, config.Transcription.Model)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("ToInjectionConfig", func(t *testing.T) {
|
||||||
|
injectionConfig := config.ToInjectionConfig()
|
||||||
|
|
||||||
|
if injectionConfig.Mode != config.Injection.Mode {
|
||||||
|
t.Errorf("Mode mismatch: got %s, want %s", injectionConfig.Mode, config.Injection.Mode)
|
||||||
|
}
|
||||||
|
if injectionConfig.RestoreClipboard != config.Injection.RestoreClipboard {
|
||||||
|
t.Errorf("RestoreClipboard mismatch: got %t, want %t", injectionConfig.RestoreClipboard, config.Injection.RestoreClipboard)
|
||||||
|
}
|
||||||
|
if injectionConfig.WtypeTimeout != config.Injection.WtypeTimeout {
|
||||||
|
t.Errorf("WtypeTimeout mismatch: got %v, want %v", injectionConfig.WtypeTimeout, config.Injection.WtypeTimeout)
|
||||||
|
}
|
||||||
|
if injectionConfig.ClipboardTimeout != config.Injection.ClipboardTimeout {
|
||||||
|
t.Errorf("ClipboardTimeout mismatch: got %v, want %v", injectionConfig.ClipboardTimeout, config.Injection.ClipboardTimeout)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIsValidLanguageCode(t *testing.T) {
|
||||||
|
validCodes := []string{"en", "es", "fr", "de", "it", "pt", "ru", "ja", "ko", "zh", "ar", "hi"}
|
||||||
|
invalidCodes := []string{"", "invalid", "xx", "123", "EN", "en-us"}
|
||||||
|
|
||||||
|
for _, code := range validCodes {
|
||||||
|
t.Run("valid_"+code, func(t *testing.T) {
|
||||||
|
if !isValidLanguageCode(code) {
|
||||||
|
t.Errorf("isValidLanguageCode(%s) = false, want true", code)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, code := range invalidCodes {
|
||||||
|
t.Run("invalid_"+code, func(t *testing.T) {
|
||||||
|
if isValidLanguageCode(code) {
|
||||||
|
t.Errorf("isValidLanguageCode(%s) = true, want false", code)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetConfigPath(t *testing.T) {
|
||||||
|
// Override user config dir for testing using environment variable
|
||||||
|
tempDir := t.TempDir()
|
||||||
|
originalConfigDir := os.Getenv("XDG_CONFIG_HOME")
|
||||||
|
os.Setenv("XDG_CONFIG_HOME", tempDir)
|
||||||
|
defer func() {
|
||||||
|
if originalConfigDir == "" {
|
||||||
|
os.Unsetenv("XDG_CONFIG_HOME")
|
||||||
|
} else {
|
||||||
|
os.Setenv("XDG_CONFIG_HOME", originalConfigDir)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
path, err := GetConfigPath()
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("GetConfigPath() error = %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
expectedPath := filepath.Join(tempDir, "hyprvoice", "config.toml")
|
||||||
|
if path != expectedPath {
|
||||||
|
t.Errorf("GetConfigPath() = %s, want %s", path, expectedPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify directory was created
|
||||||
|
if _, err := os.Stat(filepath.Dir(path)); os.IsNotExist(err) {
|
||||||
|
t.Errorf("GetConfigPath() did not create config directory")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConfig_ToTranscriberConfig_WithEnvVar(t *testing.T) {
|
||||||
|
config := &Config{
|
||||||
|
Transcription: TranscriptionConfig{
|
||||||
|
Provider: "openai",
|
||||||
|
APIKey: "", // Empty API key to test env var fallback
|
||||||
|
Language: "en",
|
||||||
|
Model: "whisper-1",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set environment variable
|
||||||
|
originalAPIKey := os.Getenv("OPENAI_API_KEY")
|
||||||
|
os.Setenv("OPENAI_API_KEY", "env-api-key")
|
||||||
|
defer func() {
|
||||||
|
if originalAPIKey == "" {
|
||||||
|
os.Unsetenv("OPENAI_API_KEY")
|
||||||
|
} else {
|
||||||
|
os.Setenv("OPENAI_API_KEY", originalAPIKey)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
transcriberConfig := config.ToTranscriberConfig()
|
||||||
|
|
||||||
|
if transcriberConfig.APIKey != "env-api-key" {
|
||||||
|
t.Errorf("Expected APIKey from env var 'env-api-key', got %s", transcriberConfig.APIKey)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConfig_ToTranscriberConfig_WithoutEnvVar(t *testing.T) {
|
||||||
|
config := &Config{
|
||||||
|
Transcription: TranscriptionConfig{
|
||||||
|
Provider: "openai",
|
||||||
|
APIKey: "config-api-key", // Config has API key
|
||||||
|
Language: "en",
|
||||||
|
Model: "whisper-1",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure environment variable is not set
|
||||||
|
originalAPIKey := os.Getenv("OPENAI_API_KEY")
|
||||||
|
os.Unsetenv("OPENAI_API_KEY")
|
||||||
|
defer func() {
|
||||||
|
if originalAPIKey != "" {
|
||||||
|
os.Setenv("OPENAI_API_KEY", originalAPIKey)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
transcriberConfig := config.ToTranscriberConfig()
|
||||||
|
|
||||||
|
if transcriberConfig.APIKey != "config-api-key" {
|
||||||
|
t.Errorf("Expected APIKey from config 'config-api-key', got %s", transcriberConfig.APIKey)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConfig_Load_InvalidTOML(t *testing.T) {
|
||||||
|
tempDir := t.TempDir()
|
||||||
|
configPath := filepath.Join(tempDir, "hyprvoice", "config.toml")
|
||||||
|
|
||||||
|
// Create directory and invalid config file
|
||||||
|
err := os.MkdirAll(filepath.Dir(configPath), 0755)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create config directory: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
invalidConfig := `[recording]
|
||||||
|
sample_rate = "invalid_number"`
|
||||||
|
|
||||||
|
err = os.WriteFile(configPath, []byte(invalidConfig), 0644)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create config file: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
originalConfigDir := os.Getenv("XDG_CONFIG_HOME")
|
||||||
|
os.Setenv("XDG_CONFIG_HOME", tempDir)
|
||||||
|
defer func() {
|
||||||
|
if originalConfigDir == "" {
|
||||||
|
os.Unsetenv("XDG_CONFIG_HOME")
|
||||||
|
} else {
|
||||||
|
os.Setenv("XDG_CONFIG_HOME", originalConfigDir)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
_, err = Load()
|
||||||
|
if err == nil {
|
||||||
|
t.Errorf("Load() should have failed with invalid TOML")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConfig_Validate_OpenAI_WithoutAPIKey(t *testing.T) {
|
||||||
|
config := &Config{
|
||||||
|
Recording: RecordingConfig{
|
||||||
|
SampleRate: 16000,
|
||||||
|
Channels: 1,
|
||||||
|
Format: "s16",
|
||||||
|
BufferSize: 8192,
|
||||||
|
ChannelBufferSize: 30,
|
||||||
|
Timeout: time.Minute,
|
||||||
|
},
|
||||||
|
Transcription: TranscriptionConfig{
|
||||||
|
Provider: "openai",
|
||||||
|
APIKey: "", // No API key
|
||||||
|
Model: "whisper-1",
|
||||||
|
},
|
||||||
|
Injection: InjectionConfig{
|
||||||
|
Mode: "fallback",
|
||||||
|
WtypeTimeout: time.Second,
|
||||||
|
ClipboardTimeout: time.Second,
|
||||||
|
},
|
||||||
|
Notifications: NotificationsConfig{
|
||||||
|
Type: "log",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure environment variable is not set
|
||||||
|
originalAPIKey := os.Getenv("OPENAI_API_KEY")
|
||||||
|
os.Unsetenv("OPENAI_API_KEY")
|
||||||
|
defer func() {
|
||||||
|
if originalAPIKey != "" {
|
||||||
|
os.Setenv("OPENAI_API_KEY", originalAPIKey)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
err := config.Validate()
|
||||||
|
if err == nil {
|
||||||
|
t.Errorf("Validate() should have failed without OpenAI API key")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConfig_Validate_OpenAI_WithEnvVarAPIKey(t *testing.T) {
|
||||||
|
config := &Config{
|
||||||
|
Recording: RecordingConfig{
|
||||||
|
SampleRate: 16000,
|
||||||
|
Channels: 1,
|
||||||
|
Format: "s16",
|
||||||
|
BufferSize: 8192,
|
||||||
|
ChannelBufferSize: 30,
|
||||||
|
Timeout: time.Minute,
|
||||||
|
},
|
||||||
|
Transcription: TranscriptionConfig{
|
||||||
|
Provider: "openai",
|
||||||
|
APIKey: "", // No API key in config
|
||||||
|
Model: "whisper-1",
|
||||||
|
},
|
||||||
|
Injection: InjectionConfig{
|
||||||
|
Mode: "fallback",
|
||||||
|
WtypeTimeout: time.Second,
|
||||||
|
ClipboardTimeout: time.Second,
|
||||||
|
},
|
||||||
|
Notifications: NotificationsConfig{
|
||||||
|
Type: "log",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set environment variable
|
||||||
|
originalAPIKey := os.Getenv("OPENAI_API_KEY")
|
||||||
|
os.Setenv("OPENAI_API_KEY", "env-api-key")
|
||||||
|
defer func() {
|
||||||
|
if originalAPIKey == "" {
|
||||||
|
os.Unsetenv("OPENAI_API_KEY")
|
||||||
|
} else {
|
||||||
|
os.Setenv("OPENAI_API_KEY", originalAPIKey)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
err := config.Validate()
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Validate() should have passed with OpenAI API key from environment: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConfig_Validate_RecordingTimeout(t *testing.T) {
|
||||||
|
config := &Config{
|
||||||
|
Recording: RecordingConfig{
|
||||||
|
SampleRate: 16000,
|
||||||
|
Channels: 1,
|
||||||
|
Format: "s16",
|
||||||
|
BufferSize: 8192,
|
||||||
|
ChannelBufferSize: 30,
|
||||||
|
Timeout: 0, // Invalid timeout
|
||||||
|
},
|
||||||
|
Transcription: TranscriptionConfig{
|
||||||
|
Provider: "openai",
|
||||||
|
APIKey: "test-key",
|
||||||
|
Model: "whisper-1",
|
||||||
|
},
|
||||||
|
Injection: InjectionConfig{
|
||||||
|
Mode: "fallback",
|
||||||
|
WtypeTimeout: time.Second,
|
||||||
|
ClipboardTimeout: time.Second,
|
||||||
|
},
|
||||||
|
Notifications: NotificationsConfig{
|
||||||
|
Type: "log",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
err := config.Validate()
|
||||||
|
if err == nil {
|
||||||
|
t.Errorf("Validate() should have failed with invalid recording timeout")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConfig_Validate_InjectionTimeouts(t *testing.T) {
|
||||||
|
config := &Config{
|
||||||
|
Recording: RecordingConfig{
|
||||||
|
SampleRate: 16000,
|
||||||
|
Channels: 1,
|
||||||
|
Format: "s16",
|
||||||
|
BufferSize: 8192,
|
||||||
|
ChannelBufferSize: 30,
|
||||||
|
Timeout: time.Minute,
|
||||||
|
},
|
||||||
|
Transcription: TranscriptionConfig{
|
||||||
|
Provider: "openai",
|
||||||
|
APIKey: "test-key",
|
||||||
|
Model: "whisper-1",
|
||||||
|
},
|
||||||
|
Injection: InjectionConfig{
|
||||||
|
Mode: "fallback",
|
||||||
|
WtypeTimeout: 0, // Invalid timeout
|
||||||
|
ClipboardTimeout: 0, // Invalid timeout
|
||||||
|
},
|
||||||
|
Notifications: NotificationsConfig{
|
||||||
|
Type: "log",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
err := config.Validate()
|
||||||
|
if err == nil {
|
||||||
|
t.Errorf("Validate() should have failed with invalid injection timeouts")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConfig_Validate_RecordingBufferSizes(t *testing.T) {
|
||||||
|
config := &Config{
|
||||||
|
Recording: RecordingConfig{
|
||||||
|
SampleRate: 16000,
|
||||||
|
Channels: 1,
|
||||||
|
Format: "s16",
|
||||||
|
BufferSize: 0, // Invalid buffer size
|
||||||
|
ChannelBufferSize: 0, // Invalid buffer size
|
||||||
|
Timeout: time.Minute,
|
||||||
|
},
|
||||||
|
Transcription: TranscriptionConfig{
|
||||||
|
Provider: "openai",
|
||||||
|
APIKey: "test-key",
|
||||||
|
Model: "whisper-1",
|
||||||
|
},
|
||||||
|
Injection: InjectionConfig{
|
||||||
|
Mode: "fallback",
|
||||||
|
WtypeTimeout: time.Second,
|
||||||
|
ClipboardTimeout: time.Second,
|
||||||
|
},
|
||||||
|
Notifications: NotificationsConfig{
|
||||||
|
Type: "log",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
err := config.Validate()
|
||||||
|
if err == nil {
|
||||||
|
t.Errorf("Validate() should have failed with invalid recording buffer sizes")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,482 @@
|
|||||||
|
package daemon
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"io"
|
||||||
|
"net"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/leonardotrapani/hyprvoice/internal/pipeline"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestNew(t *testing.T) {
|
||||||
|
// Set up a temporary config directory
|
||||||
|
tempDir := t.TempDir()
|
||||||
|
originalConfigDir := os.Getenv("XDG_CONFIG_HOME")
|
||||||
|
os.Setenv("XDG_CONFIG_HOME", tempDir)
|
||||||
|
defer func() {
|
||||||
|
if originalConfigDir == "" {
|
||||||
|
os.Unsetenv("XDG_CONFIG_HOME")
|
||||||
|
} else {
|
||||||
|
os.Setenv("XDG_CONFIG_HOME", originalConfigDir)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Create a basic config file
|
||||||
|
configPath := filepath.Join(tempDir, "hyprvoice", "config.toml")
|
||||||
|
os.MkdirAll(filepath.Dir(configPath), 0755)
|
||||||
|
configContent := `[recording]
|
||||||
|
sample_rate = 16000
|
||||||
|
channels = 1
|
||||||
|
format = "s16"
|
||||||
|
buffer_size = 8192
|
||||||
|
channel_buffer_size = 30
|
||||||
|
timeout = "5m"
|
||||||
|
|
||||||
|
[transcription]
|
||||||
|
provider = "openai"
|
||||||
|
api_key = "test-key"
|
||||||
|
model = "whisper-1"
|
||||||
|
|
||||||
|
[injection]
|
||||||
|
mode = "fallback"
|
||||||
|
wtype_timeout = "5s"
|
||||||
|
clipboard_timeout = "3s"
|
||||||
|
|
||||||
|
[notifications]
|
||||||
|
enabled = true
|
||||||
|
type = "log"`
|
||||||
|
os.WriteFile(configPath, []byte(configContent), 0644)
|
||||||
|
|
||||||
|
daemon, err := New()
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("New() error = %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if daemon == nil {
|
||||||
|
t.Errorf("New() returned nil")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test that daemon has required components
|
||||||
|
if daemon.notifier == nil {
|
||||||
|
t.Errorf("Daemon notifier is nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
if daemon.configMgr == nil {
|
||||||
|
t.Errorf("Daemon config manager is nil")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDaemon_Status(t *testing.T) {
|
||||||
|
// Set up a temporary config directory
|
||||||
|
tempDir := t.TempDir()
|
||||||
|
originalConfigDir := os.Getenv("XDG_CONFIG_HOME")
|
||||||
|
os.Setenv("XDG_CONFIG_HOME", tempDir)
|
||||||
|
defer func() {
|
||||||
|
if originalConfigDir == "" {
|
||||||
|
os.Unsetenv("XDG_CONFIG_HOME")
|
||||||
|
} else {
|
||||||
|
os.Setenv("XDG_CONFIG_HOME", originalConfigDir)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Create a basic config file
|
||||||
|
configPath := filepath.Join(tempDir, "hyprvoice", "config.toml")
|
||||||
|
os.MkdirAll(filepath.Dir(configPath), 0755)
|
||||||
|
configContent := `[recording]
|
||||||
|
sample_rate = 16000
|
||||||
|
channels = 1
|
||||||
|
format = "s16"
|
||||||
|
buffer_size = 8192
|
||||||
|
channel_buffer_size = 30
|
||||||
|
timeout = "5m"
|
||||||
|
|
||||||
|
[transcription]
|
||||||
|
provider = "openai"
|
||||||
|
api_key = "test-key"
|
||||||
|
model = "whisper-1"
|
||||||
|
|
||||||
|
[injection]
|
||||||
|
mode = "fallback"
|
||||||
|
wtype_timeout = "5s"
|
||||||
|
clipboard_timeout = "3s"
|
||||||
|
|
||||||
|
[notifications]
|
||||||
|
enabled = true
|
||||||
|
type = "log"`
|
||||||
|
os.WriteFile(configPath, []byte(configContent), 0644)
|
||||||
|
|
||||||
|
daemon, err := New()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create daemon: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test initial status (should be idle with no pipeline)
|
||||||
|
status := daemon.status()
|
||||||
|
if status != "idle" {
|
||||||
|
t.Errorf("Initial status = %s, want idle", status)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDaemon_Toggle(t *testing.T) {
|
||||||
|
// Set up a temporary config directory
|
||||||
|
tempDir := t.TempDir()
|
||||||
|
originalConfigDir := os.Getenv("XDG_CONFIG_HOME")
|
||||||
|
os.Setenv("XDG_CONFIG_HOME", tempDir)
|
||||||
|
defer func() {
|
||||||
|
if originalConfigDir == "" {
|
||||||
|
os.Unsetenv("XDG_CONFIG_HOME")
|
||||||
|
} else {
|
||||||
|
os.Setenv("XDG_CONFIG_HOME", originalConfigDir)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Create a basic config file
|
||||||
|
configPath := filepath.Join(tempDir, "hyprvoice", "config.toml")
|
||||||
|
os.MkdirAll(filepath.Dir(configPath), 0755)
|
||||||
|
configContent := `[recording]
|
||||||
|
sample_rate = 16000
|
||||||
|
channels = 1
|
||||||
|
format = "s16"
|
||||||
|
buffer_size = 8192
|
||||||
|
channel_buffer_size = 30
|
||||||
|
timeout = "5m"
|
||||||
|
|
||||||
|
[transcription]
|
||||||
|
provider = "openai"
|
||||||
|
api_key = "test-key"
|
||||||
|
model = "whisper-1"
|
||||||
|
|
||||||
|
[injection]
|
||||||
|
mode = "fallback"
|
||||||
|
wtype_timeout = "5s"
|
||||||
|
clipboard_timeout = "3s"
|
||||||
|
|
||||||
|
[notifications]
|
||||||
|
enabled = true
|
||||||
|
type = "log"`
|
||||||
|
os.WriteFile(configPath, []byte(configContent), 0644)
|
||||||
|
|
||||||
|
daemon, err := New()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create daemon: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test toggle from idle to recording
|
||||||
|
daemon.toggle()
|
||||||
|
status := daemon.status()
|
||||||
|
t.Logf("Status after first toggle = %s", status)
|
||||||
|
|
||||||
|
// Test toggle from recording to idle (abort)
|
||||||
|
daemon.toggle()
|
||||||
|
status = daemon.status()
|
||||||
|
t.Logf("Status after second toggle = %s", status)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDaemon_Handle(t *testing.T) {
|
||||||
|
// Set up a temporary config directory
|
||||||
|
tempDir := t.TempDir()
|
||||||
|
originalConfigDir := os.Getenv("XDG_CONFIG_HOME")
|
||||||
|
os.Setenv("XDG_CONFIG_HOME", tempDir)
|
||||||
|
defer func() {
|
||||||
|
if originalConfigDir == "" {
|
||||||
|
os.Unsetenv("XDG_CONFIG_HOME")
|
||||||
|
} else {
|
||||||
|
os.Setenv("XDG_CONFIG_HOME", originalConfigDir)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Create a basic config file
|
||||||
|
configPath := filepath.Join(tempDir, "hyprvoice", "config.toml")
|
||||||
|
os.MkdirAll(filepath.Dir(configPath), 0755)
|
||||||
|
configContent := `[recording]
|
||||||
|
sample_rate = 16000
|
||||||
|
channels = 1
|
||||||
|
format = "s16"
|
||||||
|
buffer_size = 8192
|
||||||
|
channel_buffer_size = 30
|
||||||
|
timeout = "5m"
|
||||||
|
|
||||||
|
[transcription]
|
||||||
|
provider = "openai"
|
||||||
|
api_key = "test-key"
|
||||||
|
model = "whisper-1"
|
||||||
|
|
||||||
|
[injection]
|
||||||
|
mode = "fallback"
|
||||||
|
wtype_timeout = "5s"
|
||||||
|
clipboard_timeout = "3s"
|
||||||
|
|
||||||
|
[notifications]
|
||||||
|
enabled = true
|
||||||
|
type = "log"`
|
||||||
|
os.WriteFile(configPath, []byte(configContent), 0644)
|
||||||
|
|
||||||
|
daemon, err := New()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create daemon: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test status command (simpler test without goroutines)
|
||||||
|
t.Run("status_command", func(t *testing.T) {
|
||||||
|
mockConn := &MockConn{
|
||||||
|
readData: []byte("s\n"),
|
||||||
|
writeData: []byte{},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize WaitGroup to avoid panic
|
||||||
|
daemon.wg.Add(1)
|
||||||
|
|
||||||
|
// Handle the command
|
||||||
|
daemon.handle(mockConn)
|
||||||
|
|
||||||
|
// Check response
|
||||||
|
response := string(mockConn.writeData)
|
||||||
|
if response != "STATUS status=idle\n" {
|
||||||
|
t.Errorf("handle() response = %q, want %q", response, "STATUS status=idle\n")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// MockConn implements net.Conn for testing
|
||||||
|
type MockConn struct {
|
||||||
|
readData []byte
|
||||||
|
writeData []byte
|
||||||
|
readPos int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MockConn) Read(b []byte) (n int, err error) {
|
||||||
|
if m.readPos >= len(m.readData) {
|
||||||
|
return 0, io.EOF
|
||||||
|
}
|
||||||
|
n = copy(b, m.readData[m.readPos:])
|
||||||
|
m.readPos += n
|
||||||
|
return n, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MockConn) Write(b []byte) (n int, err error) {
|
||||||
|
m.writeData = append(m.writeData, b...)
|
||||||
|
return len(b), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MockConn) Close() error { return nil }
|
||||||
|
func (m *MockConn) LocalAddr() net.Addr { return nil }
|
||||||
|
func (m *MockConn) RemoteAddr() net.Addr { return nil }
|
||||||
|
func (m *MockConn) SetDeadline(t time.Time) error { return nil }
|
||||||
|
func (m *MockConn) SetReadDeadline(t time.Time) error { return nil }
|
||||||
|
func (m *MockConn) SetWriteDeadline(t time.Time) error { return nil }
|
||||||
|
|
||||||
|
func TestDaemon_OnConfigReload(t *testing.T) {
|
||||||
|
// Set up a temporary config directory
|
||||||
|
tempDir := t.TempDir()
|
||||||
|
originalConfigDir := os.Getenv("XDG_CONFIG_HOME")
|
||||||
|
os.Setenv("XDG_CONFIG_HOME", tempDir)
|
||||||
|
defer func() {
|
||||||
|
if originalConfigDir == "" {
|
||||||
|
os.Unsetenv("XDG_CONFIG_HOME")
|
||||||
|
} else {
|
||||||
|
os.Setenv("XDG_CONFIG_HOME", originalConfigDir)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Create a basic config file
|
||||||
|
configPath := filepath.Join(tempDir, "hyprvoice", "config.toml")
|
||||||
|
os.MkdirAll(filepath.Dir(configPath), 0755)
|
||||||
|
configContent := `[recording]
|
||||||
|
sample_rate = 16000
|
||||||
|
channels = 1
|
||||||
|
format = "s16"
|
||||||
|
buffer_size = 8192
|
||||||
|
channel_buffer_size = 30
|
||||||
|
timeout = "5m"
|
||||||
|
|
||||||
|
[transcription]
|
||||||
|
provider = "openai"
|
||||||
|
api_key = "test-key"
|
||||||
|
model = "whisper-1"
|
||||||
|
|
||||||
|
[injection]
|
||||||
|
mode = "fallback"
|
||||||
|
wtype_timeout = "5s"
|
||||||
|
clipboard_timeout = "3s"
|
||||||
|
|
||||||
|
[notifications]
|
||||||
|
enabled = true
|
||||||
|
type = "log"`
|
||||||
|
os.WriteFile(configPath, []byte(configContent), 0644)
|
||||||
|
|
||||||
|
daemon, err := New()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create daemon: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test onConfigReload method
|
||||||
|
daemon.onConfigReload()
|
||||||
|
|
||||||
|
// Verify that the method completes without panicking
|
||||||
|
// (We can't easily test the internal state changes without more complex mocking)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDaemon_StopPipeline(t *testing.T) {
|
||||||
|
// Set up a temporary config directory
|
||||||
|
tempDir := t.TempDir()
|
||||||
|
originalConfigDir := os.Getenv("XDG_CONFIG_HOME")
|
||||||
|
os.Setenv("XDG_CONFIG_HOME", tempDir)
|
||||||
|
defer func() {
|
||||||
|
if originalConfigDir == "" {
|
||||||
|
os.Unsetenv("XDG_CONFIG_HOME")
|
||||||
|
} else {
|
||||||
|
os.Setenv("XDG_CONFIG_HOME", originalConfigDir)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Create a basic config file
|
||||||
|
configPath := filepath.Join(tempDir, "hyprvoice", "config.toml")
|
||||||
|
os.MkdirAll(filepath.Dir(configPath), 0755)
|
||||||
|
configContent := `[recording]
|
||||||
|
sample_rate = 16000
|
||||||
|
channels = 1
|
||||||
|
format = "s16"
|
||||||
|
buffer_size = 8192
|
||||||
|
channel_buffer_size = 30
|
||||||
|
timeout = "5m"
|
||||||
|
|
||||||
|
[transcription]
|
||||||
|
provider = "openai"
|
||||||
|
api_key = "test-key"
|
||||||
|
model = "whisper-1"
|
||||||
|
|
||||||
|
[injection]
|
||||||
|
mode = "fallback"
|
||||||
|
wtype_timeout = "5s"
|
||||||
|
clipboard_timeout = "3s"
|
||||||
|
|
||||||
|
[notifications]
|
||||||
|
enabled = true
|
||||||
|
type = "log"`
|
||||||
|
os.WriteFile(configPath, []byte(configContent), 0644)
|
||||||
|
|
||||||
|
daemon, err := New()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create daemon: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test stopPipeline with nil pipeline
|
||||||
|
daemon.stopPipeline()
|
||||||
|
|
||||||
|
// Test stopPipeline with a mock pipeline
|
||||||
|
// (This is simplified since we can't easily mock the pipeline interface)
|
||||||
|
daemon.mu.Lock()
|
||||||
|
daemon.pipeline = &MockPipeline{}
|
||||||
|
daemon.mu.Unlock()
|
||||||
|
|
||||||
|
daemon.stopPipeline()
|
||||||
|
|
||||||
|
// Verify pipeline is set to nil
|
||||||
|
daemon.mu.RLock()
|
||||||
|
if daemon.pipeline != nil {
|
||||||
|
t.Errorf("Pipeline should be nil after stopPipeline")
|
||||||
|
}
|
||||||
|
daemon.mu.RUnlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDaemon_Handle_Commands(t *testing.T) {
|
||||||
|
// Set up a temporary config directory
|
||||||
|
tempDir := t.TempDir()
|
||||||
|
originalConfigDir := os.Getenv("XDG_CONFIG_HOME")
|
||||||
|
os.Setenv("XDG_CONFIG_HOME", tempDir)
|
||||||
|
defer func() {
|
||||||
|
if originalConfigDir == "" {
|
||||||
|
os.Unsetenv("XDG_CONFIG_HOME")
|
||||||
|
} else {
|
||||||
|
os.Setenv("XDG_CONFIG_HOME", originalConfigDir)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Create a basic config file
|
||||||
|
configPath := filepath.Join(tempDir, "hyprvoice", "config.toml")
|
||||||
|
os.MkdirAll(filepath.Dir(configPath), 0755)
|
||||||
|
configContent := `[recording]
|
||||||
|
sample_rate = 16000
|
||||||
|
channels = 1
|
||||||
|
format = "s16"
|
||||||
|
buffer_size = 8192
|
||||||
|
channel_buffer_size = 30
|
||||||
|
timeout = "5m"
|
||||||
|
|
||||||
|
[transcription]
|
||||||
|
provider = "openai"
|
||||||
|
api_key = "test-key"
|
||||||
|
model = "whisper-1"
|
||||||
|
|
||||||
|
[injection]
|
||||||
|
mode = "fallback"
|
||||||
|
wtype_timeout = "5s"
|
||||||
|
clipboard_timeout = "3s"
|
||||||
|
|
||||||
|
[notifications]
|
||||||
|
enabled = true
|
||||||
|
type = "log"`
|
||||||
|
os.WriteFile(configPath, []byte(configContent), 0644)
|
||||||
|
|
||||||
|
daemon, err := New()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create daemon: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
command string
|
||||||
|
expected string
|
||||||
|
}{
|
||||||
|
{"toggle_command", "t\n", "OK toggled\n"},
|
||||||
|
{"version_command", "v\n", "STATUS proto="},
|
||||||
|
{"quit_command", "q\n", "OK quitting\n"},
|
||||||
|
{"unknown_command", "x\n", "ERR unknown="},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
mockConn := &MockConn{
|
||||||
|
readData: []byte(tt.command),
|
||||||
|
writeData: []byte{},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize WaitGroup to avoid panic
|
||||||
|
daemon.wg.Add(1)
|
||||||
|
|
||||||
|
// Handle the command
|
||||||
|
daemon.handle(mockConn)
|
||||||
|
|
||||||
|
// Check response
|
||||||
|
response := string(mockConn.writeData)
|
||||||
|
if tt.name == "version_command" {
|
||||||
|
if len(response) == 0 || !((len(response) >= 12 && response[:12] == "STATUS proto=") || (len(response) >= 13 && response[:13] == "STATUS proto=")) {
|
||||||
|
t.Errorf("handle() response = %q, want prefix %q", response, "STATUS proto=")
|
||||||
|
}
|
||||||
|
} else if tt.name == "unknown_command" {
|
||||||
|
if len(response) == 0 || !((len(response) >= 12 && response[:12] == "ERR unknown=") || (len(response) >= 13 && response[:13] == "ERR unknown=")) {
|
||||||
|
t.Errorf("handle() response = %q, want prefix %q", response, "ERR unknown=")
|
||||||
|
}
|
||||||
|
} else if response != tt.expected {
|
||||||
|
t.Errorf("handle() response = %q, want %q", response, tt.expected)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MockPipeline implements pipeline.Pipeline for testing
|
||||||
|
type MockPipeline struct{}
|
||||||
|
|
||||||
|
func (m *MockPipeline) Run(ctx context.Context) {}
|
||||||
|
func (m *MockPipeline) Stop() {}
|
||||||
|
func (m *MockPipeline) Status() pipeline.Status { return pipeline.Idle }
|
||||||
|
func (m *MockPipeline) GetErrorCh() <-chan pipeline.PipelineError {
|
||||||
|
return make(chan pipeline.PipelineError)
|
||||||
|
}
|
||||||
|
func (m *MockPipeline) GetActionCh() chan<- pipeline.Action { return make(chan pipeline.Action) }
|
||||||
@@ -0,0 +1,354 @@
|
|||||||
|
package injection
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestNewInjector(t *testing.T) {
|
||||||
|
config := Config{
|
||||||
|
Mode: "fallback",
|
||||||
|
RestoreClipboard: true,
|
||||||
|
WtypeTimeout: 5 * time.Second,
|
||||||
|
ClipboardTimeout: 3 * time.Second,
|
||||||
|
}
|
||||||
|
|
||||||
|
injector := NewInjector(config)
|
||||||
|
if injector == nil {
|
||||||
|
t.Errorf("NewInjector() returned nil")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test that the injector works with the expected config
|
||||||
|
ctx := context.Background()
|
||||||
|
err := injector.Inject(ctx, "test")
|
||||||
|
// We expect this to fail due to missing external tools, but it should be the right type of error
|
||||||
|
if err != nil {
|
||||||
|
t.Logf("Injector created successfully (failed as expected due to missing tools): %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInjector_Inject(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
config Config
|
||||||
|
text string
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "inject with clipboard mode",
|
||||||
|
config: Config{
|
||||||
|
Mode: "clipboard",
|
||||||
|
RestoreClipboard: false,
|
||||||
|
WtypeTimeout: 5 * time.Second,
|
||||||
|
ClipboardTimeout: 3 * time.Second,
|
||||||
|
},
|
||||||
|
text: "test text",
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "inject with type mode",
|
||||||
|
config: Config{
|
||||||
|
Mode: "type",
|
||||||
|
RestoreClipboard: false,
|
||||||
|
WtypeTimeout: 5 * time.Second,
|
||||||
|
ClipboardTimeout: 3 * time.Second,
|
||||||
|
},
|
||||||
|
text: "test text",
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "inject with fallback mode",
|
||||||
|
config: Config{
|
||||||
|
Mode: "fallback",
|
||||||
|
RestoreClipboard: false,
|
||||||
|
WtypeTimeout: 5 * time.Second,
|
||||||
|
ClipboardTimeout: 3 * time.Second,
|
||||||
|
},
|
||||||
|
text: "test text",
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "inject empty text",
|
||||||
|
config: Config{
|
||||||
|
Mode: "clipboard",
|
||||||
|
RestoreClipboard: false,
|
||||||
|
WtypeTimeout: 5 * time.Second,
|
||||||
|
ClipboardTimeout: 3 * time.Second,
|
||||||
|
},
|
||||||
|
text: "",
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "inject with invalid mode",
|
||||||
|
config: Config{
|
||||||
|
Mode: "invalid",
|
||||||
|
RestoreClipboard: false,
|
||||||
|
WtypeTimeout: 5 * time.Second,
|
||||||
|
ClipboardTimeout: 3 * time.Second,
|
||||||
|
},
|
||||||
|
text: "test text",
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
injector := NewInjector(tt.config)
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
err := injector.Inject(ctx, tt.text)
|
||||||
|
if (err != nil) != tt.wantErr {
|
||||||
|
t.Errorf("Inject() error = %v, wantErr %v", err, tt.wantErr)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConfig(t *testing.T) {
|
||||||
|
config := Config{
|
||||||
|
Mode: "fallback",
|
||||||
|
RestoreClipboard: true,
|
||||||
|
WtypeTimeout: 5 * time.Second,
|
||||||
|
ClipboardTimeout: 3 * time.Second,
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.Mode != "fallback" {
|
||||||
|
t.Errorf("Mode mismatch: got %s, want %s", config.Mode, "fallback")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !config.RestoreClipboard {
|
||||||
|
t.Errorf("RestoreClipboard should be true")
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.WtypeTimeout != 5*time.Second {
|
||||||
|
t.Errorf("WtypeTimeout mismatch: got %v, want %v", config.WtypeTimeout, 5*time.Second)
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.ClipboardTimeout != 3*time.Second {
|
||||||
|
t.Errorf("ClipboardTimeout mismatch: got %v, want %v", config.ClipboardTimeout, 3*time.Second)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestTypeText tests the typeText function
|
||||||
|
func TestTypeText(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
text string
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "type normal text",
|
||||||
|
text: "hello world",
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "type empty text",
|
||||||
|
text: "",
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "type text with special characters",
|
||||||
|
text: "hello\nworld\t!",
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
err := typeText(ctx, tt.text, 1*time.Second)
|
||||||
|
if (err != nil) != tt.wantErr {
|
||||||
|
t.Errorf("typeText() error = %v, wantErr %v", err, tt.wantErr)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestCheckWtypeAvailable tests the wtype availability check
|
||||||
|
func TestCheckWtypeAvailable(t *testing.T) {
|
||||||
|
err := checkWtypeAvailable()
|
||||||
|
if err != nil {
|
||||||
|
t.Logf("checkWtypeAvailable() failed (expected if wtype not installed): %v", err)
|
||||||
|
// Don't fail the test if wtype is not available
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Logf("checkWtypeAvailable() succeeded - wtype is available")
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestGetClipboard tests the clipboard get functionality
|
||||||
|
func TestGetClipboard(t *testing.T) {
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
// Test getting clipboard content
|
||||||
|
content, err := getClipboard(ctx, 1*time.Second)
|
||||||
|
if err != nil {
|
||||||
|
t.Logf("getClipboard() failed (expected if wl-paste not available): %v", err)
|
||||||
|
// Don't fail the test if clipboard tools are not available
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Logf("getClipboard() succeeded, content length: %d", len(content))
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestSetClipboard tests the clipboard set functionality
|
||||||
|
func TestSetClipboard(t *testing.T) {
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
testText := "test clipboard content"
|
||||||
|
|
||||||
|
err := setClipboard(ctx, testText, 1*time.Second)
|
||||||
|
if err != nil {
|
||||||
|
t.Logf("setClipboard() failed (expected if wl-copy not available): %v", err)
|
||||||
|
// Don't fail the test if clipboard tools are not available
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Logf("setClipboard() succeeded")
|
||||||
|
|
||||||
|
// Try to read it back
|
||||||
|
content, err := getClipboard(ctx, 1*time.Second)
|
||||||
|
if err != nil {
|
||||||
|
t.Logf("Failed to read back clipboard content: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if content != testText {
|
||||||
|
t.Logf("Clipboard content mismatch: got %q, want %q", content, testText)
|
||||||
|
// Don't fail - clipboard might have been modified by other processes
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestCheckClipboardAvailable tests the clipboard tools availability check
|
||||||
|
func TestCheckClipboardAvailable(t *testing.T) {
|
||||||
|
err := checkClipboardAvailable()
|
||||||
|
if err != nil {
|
||||||
|
t.Logf("checkClipboardAvailable() failed (expected if clipboard tools not installed): %v", err)
|
||||||
|
// Don't fail the test if clipboard tools are not available
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Logf("checkClipboardAvailable() succeeded - clipboard tools are available")
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestInjector_ClipboardMode tests clipboard-only injection
|
||||||
|
func TestInjector_ClipboardMode(t *testing.T) {
|
||||||
|
config := Config{
|
||||||
|
Mode: "clipboard",
|
||||||
|
RestoreClipboard: false,
|
||||||
|
WtypeTimeout: 5 * time.Second,
|
||||||
|
ClipboardTimeout: 3 * time.Second,
|
||||||
|
}
|
||||||
|
|
||||||
|
injector := NewInjector(config)
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
err := injector.Inject(ctx, "test clipboard text")
|
||||||
|
if err != nil {
|
||||||
|
t.Logf("Clipboard injection failed (expected if clipboard tools not available): %v", err)
|
||||||
|
// Don't fail the test if clipboard tools are not available
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Logf("Clipboard injection succeeded")
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestInjector_TypeMode tests typing-only injection
|
||||||
|
func TestInjector_TypeMode(t *testing.T) {
|
||||||
|
config := Config{
|
||||||
|
Mode: "type",
|
||||||
|
RestoreClipboard: false,
|
||||||
|
WtypeTimeout: 5 * time.Second,
|
||||||
|
ClipboardTimeout: 3 * time.Second,
|
||||||
|
}
|
||||||
|
|
||||||
|
injector := NewInjector(config)
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
err := injector.Inject(ctx, "test typing text")
|
||||||
|
if err != nil {
|
||||||
|
t.Logf("Typing injection failed (expected if wtype not available): %v", err)
|
||||||
|
// Don't fail the test if wtype is not available
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Logf("Typing injection succeeded")
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestInjector_FallbackMode tests fallback injection behavior
|
||||||
|
func TestInjector_FallbackMode(t *testing.T) {
|
||||||
|
config := Config{
|
||||||
|
Mode: "fallback",
|
||||||
|
RestoreClipboard: false,
|
||||||
|
WtypeTimeout: 5 * time.Second,
|
||||||
|
ClipboardTimeout: 3 * time.Second,
|
||||||
|
}
|
||||||
|
|
||||||
|
injector := NewInjector(config)
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
err := injector.Inject(ctx, "test fallback text")
|
||||||
|
if err != nil {
|
||||||
|
t.Logf("Fallback injection failed (expected if both wtype and clipboard tools not available): %v", err)
|
||||||
|
// Don't fail the test if tools are not available
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Logf("Fallback injection succeeded")
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestInjector_EmptyText tests injection of empty text
|
||||||
|
func TestInjector_EmptyText(t *testing.T) {
|
||||||
|
config := Config{
|
||||||
|
Mode: "clipboard",
|
||||||
|
RestoreClipboard: false,
|
||||||
|
WtypeTimeout: 5 * time.Second,
|
||||||
|
ClipboardTimeout: 3 * time.Second,
|
||||||
|
}
|
||||||
|
|
||||||
|
injector := NewInjector(config)
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
err := injector.Inject(ctx, "")
|
||||||
|
if err == nil {
|
||||||
|
t.Errorf("Inject() should fail with empty text")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err.Error() != "cannot inject empty text" {
|
||||||
|
t.Errorf("Inject() error message = %q, want %q", err.Error(), "cannot inject empty text")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestInjector_InvalidMode tests injection with invalid mode
|
||||||
|
func TestInjector_InvalidMode(t *testing.T) {
|
||||||
|
config := Config{
|
||||||
|
Mode: "invalid",
|
||||||
|
RestoreClipboard: false,
|
||||||
|
WtypeTimeout: 5 * time.Second,
|
||||||
|
ClipboardTimeout: 3 * time.Second,
|
||||||
|
}
|
||||||
|
|
||||||
|
injector := NewInjector(config)
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
err := injector.Inject(ctx, "test text")
|
||||||
|
if err == nil {
|
||||||
|
t.Errorf("Inject() should fail with invalid mode")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
expectedError := "unsupported injection mode: invalid"
|
||||||
|
if err.Error() != expectedError {
|
||||||
|
t.Errorf("Inject() error message = %q, want %q", err.Error(), expectedError)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,203 @@
|
|||||||
|
package notify
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/leonardotrapani/hyprvoice/internal/config"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestDesktop_Notify(t *testing.T) {
|
||||||
|
desktop := Desktop{}
|
||||||
|
|
||||||
|
// Test normal notification
|
||||||
|
desktop.Notify("Test Title", "Test Message")
|
||||||
|
|
||||||
|
// Test error notification
|
||||||
|
desktop.Error("Test Error Message")
|
||||||
|
|
||||||
|
// Test specific methods
|
||||||
|
desktop.RecordingStarted()
|
||||||
|
desktop.Transcribing()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLog_Notify(t *testing.T) {
|
||||||
|
logNotifier := Log{}
|
||||||
|
|
||||||
|
// Test normal notification
|
||||||
|
logNotifier.Notify("Test Title", "Test Message")
|
||||||
|
|
||||||
|
// Test error notification
|
||||||
|
logNotifier.Error("Test Error Message")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNop_Notify(t *testing.T) {
|
||||||
|
nop := Nop{}
|
||||||
|
|
||||||
|
// Test that these methods don't panic
|
||||||
|
nop.Notify("Test Title", "Test Message")
|
||||||
|
nop.Error("Test Error Message")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetNotifierBasedOnConfig(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
config *config.Config
|
||||||
|
expected string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "desktop notification type",
|
||||||
|
config: &config.Config{
|
||||||
|
Notifications: config.NotificationsConfig{
|
||||||
|
Type: "desktop",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expected: "desktop",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "log notification type",
|
||||||
|
config: &config.Config{
|
||||||
|
Notifications: config.NotificationsConfig{
|
||||||
|
Type: "log",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expected: "log",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "none notification type",
|
||||||
|
config: &config.Config{
|
||||||
|
Notifications: config.NotificationsConfig{
|
||||||
|
Type: "none",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expected: "nop",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "unknown notification type",
|
||||||
|
config: &config.Config{
|
||||||
|
Notifications: config.NotificationsConfig{
|
||||||
|
Type: "unknown",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expected: "nop",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
notifier := GetNotifierBasedOnConfig(tt.config)
|
||||||
|
|
||||||
|
// Test the notifier by calling its methods
|
||||||
|
notifier.Notify("Test", "Message")
|
||||||
|
notifier.Error("Error")
|
||||||
|
|
||||||
|
// Check the type by testing behavior
|
||||||
|
switch tt.expected {
|
||||||
|
case "desktop":
|
||||||
|
// Desktop notifier should not panic
|
||||||
|
if desktop, ok := notifier.(Desktop); ok {
|
||||||
|
desktop.RecordingStarted()
|
||||||
|
desktop.Transcribing()
|
||||||
|
}
|
||||||
|
case "log":
|
||||||
|
// Log notifier should not panic
|
||||||
|
if logNotifier, ok := notifier.(Log); ok {
|
||||||
|
logNotifier.Notify("Test", "Message")
|
||||||
|
logNotifier.Error("Error")
|
||||||
|
}
|
||||||
|
case "nop":
|
||||||
|
// Nop notifier should not panic
|
||||||
|
if nop, ok := notifier.(Nop); ok {
|
||||||
|
nop.Notify("Test", "Message")
|
||||||
|
nop.Error("Error")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDesktop_Methods(t *testing.T) {
|
||||||
|
desktop := Desktop{}
|
||||||
|
|
||||||
|
// Test RecordingStarted method
|
||||||
|
desktop.RecordingStarted()
|
||||||
|
|
||||||
|
// Test Transcribing method
|
||||||
|
desktop.Transcribing()
|
||||||
|
|
||||||
|
// Test Error method
|
||||||
|
desktop.Error("Test error")
|
||||||
|
|
||||||
|
// Test Notify method
|
||||||
|
desktop.Notify("Test Title", "Test Message")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLog_Methods(t *testing.T) {
|
||||||
|
logNotifier := Log{}
|
||||||
|
|
||||||
|
// Test Error method
|
||||||
|
logNotifier.Error("Test error")
|
||||||
|
|
||||||
|
// Test Notify method
|
||||||
|
logNotifier.Notify("Test Title", "Test Message")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNop_Methods(t *testing.T) {
|
||||||
|
nop := Nop{}
|
||||||
|
|
||||||
|
// Test Error method
|
||||||
|
nop.Error("Test error")
|
||||||
|
|
||||||
|
// Test Notify method
|
||||||
|
nop.Notify("Test Title", "Test Message")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNotifierInterface(t *testing.T) {
|
||||||
|
// Test that all notifiers implement the Notifier interface
|
||||||
|
var notifier Notifier
|
||||||
|
|
||||||
|
// Test Desktop
|
||||||
|
notifier = Desktop{}
|
||||||
|
notifier.Notify("Test", "Message")
|
||||||
|
notifier.Error("Error")
|
||||||
|
|
||||||
|
// Test Log
|
||||||
|
notifier = Log{}
|
||||||
|
notifier.Notify("Test", "Message")
|
||||||
|
notifier.Error("Error")
|
||||||
|
|
||||||
|
// Test Nop
|
||||||
|
notifier = Nop{}
|
||||||
|
notifier.Notify("Test", "Message")
|
||||||
|
notifier.Error("Error")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNotificationTypes(t *testing.T) {
|
||||||
|
// Test different notification configurations
|
||||||
|
configs := []*config.Config{
|
||||||
|
{
|
||||||
|
Notifications: config.NotificationsConfig{
|
||||||
|
Type: "desktop",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Notifications: config.NotificationsConfig{
|
||||||
|
Type: "log",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Notifications: config.NotificationsConfig{
|
||||||
|
Type: "none",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, cfg := range configs {
|
||||||
|
t.Run("type_"+cfg.Notifications.Type, func(t *testing.T) {
|
||||||
|
notifier := GetNotifierBasedOnConfig(cfg)
|
||||||
|
|
||||||
|
// Test that the notifier works
|
||||||
|
notifier.Notify("Test Title", "Test Message")
|
||||||
|
notifier.Error("Test Error")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,386 @@
|
|||||||
|
package pipeline
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/leonardotrapani/hyprvoice/internal/config"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestNew(t *testing.T) {
|
||||||
|
cfg := &config.Config{
|
||||||
|
Recording: config.RecordingConfig{
|
||||||
|
SampleRate: 16000,
|
||||||
|
Channels: 1,
|
||||||
|
Format: "s16",
|
||||||
|
BufferSize: 8192,
|
||||||
|
ChannelBufferSize: 30,
|
||||||
|
Timeout: 5 * time.Minute,
|
||||||
|
},
|
||||||
|
Transcription: config.TranscriptionConfig{
|
||||||
|
Provider: "openai",
|
||||||
|
APIKey: "test-key",
|
||||||
|
Language: "en",
|
||||||
|
Model: "whisper-1",
|
||||||
|
},
|
||||||
|
Injection: config.InjectionConfig{
|
||||||
|
Mode: "fallback",
|
||||||
|
RestoreClipboard: true,
|
||||||
|
WtypeTimeout: 5 * time.Second,
|
||||||
|
ClipboardTimeout: 3 * time.Second,
|
||||||
|
},
|
||||||
|
Notifications: config.NotificationsConfig{
|
||||||
|
Enabled: true,
|
||||||
|
Type: "log",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
pipeline := New(cfg)
|
||||||
|
if pipeline == nil {
|
||||||
|
t.Errorf("New() returned nil")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test that pipeline is created successfully
|
||||||
|
// Note: Status may be empty initially due to implementation
|
||||||
|
t.Logf("Initial status = %s", pipeline.Status())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPipeline_Status(t *testing.T) {
|
||||||
|
cfg := &config.Config{
|
||||||
|
Recording: config.RecordingConfig{
|
||||||
|
SampleRate: 16000,
|
||||||
|
Channels: 1,
|
||||||
|
Format: "s16",
|
||||||
|
BufferSize: 8192,
|
||||||
|
ChannelBufferSize: 30,
|
||||||
|
Timeout: 5 * time.Minute,
|
||||||
|
},
|
||||||
|
Transcription: config.TranscriptionConfig{
|
||||||
|
Provider: "openai",
|
||||||
|
APIKey: "test-key",
|
||||||
|
Language: "en",
|
||||||
|
Model: "whisper-1",
|
||||||
|
},
|
||||||
|
Injection: config.InjectionConfig{
|
||||||
|
Mode: "fallback",
|
||||||
|
RestoreClipboard: true,
|
||||||
|
WtypeTimeout: 5 * time.Second,
|
||||||
|
ClipboardTimeout: 3 * time.Second,
|
||||||
|
},
|
||||||
|
Notifications: config.NotificationsConfig{
|
||||||
|
Enabled: true,
|
||||||
|
Type: "log",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
pipeline := New(cfg)
|
||||||
|
|
||||||
|
// Test that we can get status (may be empty initially)
|
||||||
|
status := pipeline.Status()
|
||||||
|
t.Logf("Status() = %s", status)
|
||||||
|
|
||||||
|
// Test that we can get action channel
|
||||||
|
actionCh := pipeline.GetActionCh()
|
||||||
|
if actionCh == nil {
|
||||||
|
t.Errorf("GetActionCh() returned nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test that we can get error channel
|
||||||
|
errorCh := pipeline.GetErrorCh()
|
||||||
|
if errorCh == nil {
|
||||||
|
t.Errorf("GetErrorCh() returned nil")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPipeline_GetActionCh(t *testing.T) {
|
||||||
|
cfg := &config.Config{
|
||||||
|
Recording: config.RecordingConfig{
|
||||||
|
SampleRate: 16000,
|
||||||
|
Channels: 1,
|
||||||
|
Format: "s16",
|
||||||
|
BufferSize: 8192,
|
||||||
|
ChannelBufferSize: 30,
|
||||||
|
Timeout: 5 * time.Minute,
|
||||||
|
},
|
||||||
|
Transcription: config.TranscriptionConfig{
|
||||||
|
Provider: "openai",
|
||||||
|
APIKey: "test-key",
|
||||||
|
Language: "en",
|
||||||
|
Model: "whisper-1",
|
||||||
|
},
|
||||||
|
Injection: config.InjectionConfig{
|
||||||
|
Mode: "fallback",
|
||||||
|
RestoreClipboard: true,
|
||||||
|
WtypeTimeout: 5 * time.Second,
|
||||||
|
ClipboardTimeout: 3 * time.Second,
|
||||||
|
},
|
||||||
|
Notifications: config.NotificationsConfig{
|
||||||
|
Enabled: true,
|
||||||
|
Type: "log",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
pipeline := New(cfg)
|
||||||
|
actionCh := pipeline.GetActionCh()
|
||||||
|
|
||||||
|
if actionCh == nil {
|
||||||
|
t.Errorf("GetActionCh() returned nil")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test sending an action
|
||||||
|
select {
|
||||||
|
case actionCh <- Inject:
|
||||||
|
// Action sent successfully
|
||||||
|
default:
|
||||||
|
t.Errorf("Could not send action to channel")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPipeline_GetErrorCh(t *testing.T) {
|
||||||
|
cfg := &config.Config{
|
||||||
|
Recording: config.RecordingConfig{
|
||||||
|
SampleRate: 16000,
|
||||||
|
Channels: 1,
|
||||||
|
Format: "s16",
|
||||||
|
BufferSize: 8192,
|
||||||
|
ChannelBufferSize: 30,
|
||||||
|
Timeout: 5 * time.Minute,
|
||||||
|
},
|
||||||
|
Transcription: config.TranscriptionConfig{
|
||||||
|
Provider: "openai",
|
||||||
|
APIKey: "test-key",
|
||||||
|
Language: "en",
|
||||||
|
Model: "whisper-1",
|
||||||
|
},
|
||||||
|
Injection: config.InjectionConfig{
|
||||||
|
Mode: "fallback",
|
||||||
|
RestoreClipboard: true,
|
||||||
|
WtypeTimeout: 5 * time.Second,
|
||||||
|
ClipboardTimeout: 3 * time.Second,
|
||||||
|
},
|
||||||
|
Notifications: config.NotificationsConfig{
|
||||||
|
Enabled: true,
|
||||||
|
Type: "log",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
pipeline := New(cfg)
|
||||||
|
errorCh := pipeline.GetErrorCh()
|
||||||
|
|
||||||
|
if errorCh == nil {
|
||||||
|
t.Errorf("GetErrorCh() returned nil")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test that we can receive from the error channel
|
||||||
|
select {
|
||||||
|
case <-errorCh:
|
||||||
|
// Error received
|
||||||
|
default:
|
||||||
|
// No error available, which is expected
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPipeline_Stop(t *testing.T) {
|
||||||
|
cfg := &config.Config{
|
||||||
|
Recording: config.RecordingConfig{
|
||||||
|
SampleRate: 16000,
|
||||||
|
Channels: 1,
|
||||||
|
Format: "s16",
|
||||||
|
BufferSize: 8192,
|
||||||
|
ChannelBufferSize: 30,
|
||||||
|
Timeout: 5 * time.Minute,
|
||||||
|
},
|
||||||
|
Transcription: config.TranscriptionConfig{
|
||||||
|
Provider: "openai",
|
||||||
|
APIKey: "test-key",
|
||||||
|
Language: "en",
|
||||||
|
Model: "whisper-1",
|
||||||
|
},
|
||||||
|
Injection: config.InjectionConfig{
|
||||||
|
Mode: "fallback",
|
||||||
|
RestoreClipboard: true,
|
||||||
|
WtypeTimeout: 5 * time.Second,
|
||||||
|
ClipboardTimeout: 3 * time.Second,
|
||||||
|
},
|
||||||
|
Notifications: config.NotificationsConfig{
|
||||||
|
Enabled: true,
|
||||||
|
Type: "log",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
pipeline := New(cfg)
|
||||||
|
|
||||||
|
// Stop should be safe to call even when not running
|
||||||
|
pipeline.Stop()
|
||||||
|
|
||||||
|
// Status should be consistent after stop
|
||||||
|
status := pipeline.Status()
|
||||||
|
t.Logf("Status after stop = %s", status)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPipeline_Run(t *testing.T) {
|
||||||
|
cfg := &config.Config{
|
||||||
|
Recording: config.RecordingConfig{
|
||||||
|
SampleRate: 16000,
|
||||||
|
Channels: 1,
|
||||||
|
Format: "s16",
|
||||||
|
BufferSize: 8192,
|
||||||
|
ChannelBufferSize: 30,
|
||||||
|
Timeout: 5 * time.Minute,
|
||||||
|
},
|
||||||
|
Transcription: config.TranscriptionConfig{
|
||||||
|
Provider: "openai",
|
||||||
|
APIKey: "test-key",
|
||||||
|
Language: "en",
|
||||||
|
Model: "whisper-1",
|
||||||
|
},
|
||||||
|
Injection: config.InjectionConfig{
|
||||||
|
Mode: "fallback",
|
||||||
|
RestoreClipboard: true,
|
||||||
|
WtypeTimeout: 5 * time.Second,
|
||||||
|
ClipboardTimeout: 3 * time.Second,
|
||||||
|
},
|
||||||
|
Notifications: config.NotificationsConfig{
|
||||||
|
Enabled: true,
|
||||||
|
Type: "log",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
pipeline := New(cfg)
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
// Test running the pipeline
|
||||||
|
pipeline.Run(ctx)
|
||||||
|
|
||||||
|
// Give it a moment to start
|
||||||
|
time.Sleep(100 * time.Millisecond)
|
||||||
|
|
||||||
|
// Check status after starting (may transition quickly due to test environment)
|
||||||
|
status := pipeline.Status()
|
||||||
|
t.Logf("Status after Run = %s", status)
|
||||||
|
|
||||||
|
// Stop the pipeline
|
||||||
|
pipeline.Stop()
|
||||||
|
|
||||||
|
// Give it a moment to stop
|
||||||
|
time.Sleep(100 * time.Millisecond)
|
||||||
|
|
||||||
|
// Check final status
|
||||||
|
finalStatus := pipeline.Status()
|
||||||
|
t.Logf("Status after Stop = %s", finalStatus)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStatus_String(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
status Status
|
||||||
|
expected string
|
||||||
|
}{
|
||||||
|
{Idle, "idle"},
|
||||||
|
{Recording, "recording"},
|
||||||
|
{Transcribing, "transcribing"},
|
||||||
|
{Injecting, "injecting"},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(string(tt.status), func(t *testing.T) {
|
||||||
|
if string(tt.status) != tt.expected {
|
||||||
|
t.Errorf("Status string = %s, want %s", string(tt.status), tt.expected)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAction_String(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
action Action
|
||||||
|
expected string
|
||||||
|
}{
|
||||||
|
{Inject, "inject"},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(string(tt.action), func(t *testing.T) {
|
||||||
|
if string(tt.action) != tt.expected {
|
||||||
|
t.Errorf("Action string = %s, want %s", string(tt.action), tt.expected)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPipelineError_Struct(t *testing.T) {
|
||||||
|
err := PipelineError{
|
||||||
|
Title: "Test Title",
|
||||||
|
Message: "Test Message",
|
||||||
|
Err: nil,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err.Title != "Test Title" {
|
||||||
|
t.Errorf("Title = %s, want %s", err.Title, "Test Title")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err.Message != "Test Message" {
|
||||||
|
t.Errorf("Message = %s, want %s", err.Message, "Test Message")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err.Err != nil {
|
||||||
|
t.Errorf("Err = %v, want nil", err.Err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestPipeline_ConcurrentAccess tests concurrent access to pipeline methods
|
||||||
|
func TestPipeline_ConcurrentAccess(t *testing.T) {
|
||||||
|
cfg := &config.Config{
|
||||||
|
Recording: config.RecordingConfig{
|
||||||
|
SampleRate: 16000,
|
||||||
|
Channels: 1,
|
||||||
|
Format: "s16",
|
||||||
|
BufferSize: 8192,
|
||||||
|
ChannelBufferSize: 30,
|
||||||
|
Timeout: 5 * time.Minute,
|
||||||
|
},
|
||||||
|
Transcription: config.TranscriptionConfig{
|
||||||
|
Provider: "openai",
|
||||||
|
APIKey: "test-key",
|
||||||
|
Language: "en",
|
||||||
|
Model: "whisper-1",
|
||||||
|
},
|
||||||
|
Injection: config.InjectionConfig{
|
||||||
|
Mode: "fallback",
|
||||||
|
RestoreClipboard: true,
|
||||||
|
WtypeTimeout: 5 * time.Second,
|
||||||
|
ClipboardTimeout: 3 * time.Second,
|
||||||
|
},
|
||||||
|
Notifications: config.NotificationsConfig{
|
||||||
|
Enabled: true,
|
||||||
|
Type: "log",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
pipeline := New(cfg)
|
||||||
|
|
||||||
|
// Test concurrent access to Status()
|
||||||
|
done := make(chan bool)
|
||||||
|
go func() {
|
||||||
|
for i := 0; i < 100; i++ {
|
||||||
|
pipeline.Status()
|
||||||
|
}
|
||||||
|
done <- true
|
||||||
|
}()
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
for i := 0; i < 100; i++ {
|
||||||
|
pipeline.GetActionCh()
|
||||||
|
pipeline.GetErrorCh()
|
||||||
|
}
|
||||||
|
done <- true
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Wait for both goroutines to complete
|
||||||
|
<-done
|
||||||
|
<-done
|
||||||
|
}
|
||||||
@@ -0,0 +1,368 @@
|
|||||||
|
package recording
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestNewRecorder(t *testing.T) {
|
||||||
|
config := Config{
|
||||||
|
SampleRate: 16000,
|
||||||
|
Channels: 1,
|
||||||
|
Format: "s16",
|
||||||
|
BufferSize: 8192,
|
||||||
|
Device: "",
|
||||||
|
ChannelBufferSize: 30,
|
||||||
|
Timeout: 5 * time.Minute,
|
||||||
|
}
|
||||||
|
|
||||||
|
recorder := NewRecorder(config)
|
||||||
|
if recorder == nil {
|
||||||
|
t.Errorf("NewRecorder() returned nil")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if recorder.config.SampleRate != config.SampleRate {
|
||||||
|
t.Errorf("SampleRate not set correctly: got %d, want %d", recorder.config.SampleRate, config.SampleRate)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRecorder_IsRecording(t *testing.T) {
|
||||||
|
config := Config{
|
||||||
|
SampleRate: 16000,
|
||||||
|
Channels: 1,
|
||||||
|
Format: "s16",
|
||||||
|
BufferSize: 8192,
|
||||||
|
Device: "",
|
||||||
|
ChannelBufferSize: 30,
|
||||||
|
Timeout: 5 * time.Minute,
|
||||||
|
}
|
||||||
|
|
||||||
|
recorder := NewRecorder(config)
|
||||||
|
|
||||||
|
// Initially should not be recording
|
||||||
|
if recorder.IsRecording() {
|
||||||
|
t.Errorf("IsRecording() = true, want false initially")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRecorder_ValidateConfig(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
config Config
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "valid config",
|
||||||
|
config: Config{
|
||||||
|
SampleRate: 16000,
|
||||||
|
Channels: 1,
|
||||||
|
Format: "s16",
|
||||||
|
BufferSize: 8192,
|
||||||
|
Device: "",
|
||||||
|
ChannelBufferSize: 30,
|
||||||
|
Timeout: 5 * time.Minute,
|
||||||
|
},
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid sample rate",
|
||||||
|
config: Config{
|
||||||
|
SampleRate: 0,
|
||||||
|
Channels: 1,
|
||||||
|
Format: "s16",
|
||||||
|
BufferSize: 8192,
|
||||||
|
ChannelBufferSize: 30,
|
||||||
|
Timeout: 5 * time.Minute,
|
||||||
|
},
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid channels",
|
||||||
|
config: Config{
|
||||||
|
SampleRate: 16000,
|
||||||
|
Channels: 0,
|
||||||
|
Format: "s16",
|
||||||
|
BufferSize: 8192,
|
||||||
|
ChannelBufferSize: 30,
|
||||||
|
Timeout: 5 * time.Minute,
|
||||||
|
},
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid buffer size",
|
||||||
|
config: Config{
|
||||||
|
SampleRate: 16000,
|
||||||
|
Channels: 1,
|
||||||
|
Format: "s16",
|
||||||
|
BufferSize: 0,
|
||||||
|
ChannelBufferSize: 30,
|
||||||
|
Timeout: 5 * time.Minute,
|
||||||
|
},
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid channel buffer size",
|
||||||
|
config: Config{
|
||||||
|
SampleRate: 16000,
|
||||||
|
Channels: 1,
|
||||||
|
Format: "s16",
|
||||||
|
BufferSize: 8192,
|
||||||
|
ChannelBufferSize: 0,
|
||||||
|
Timeout: 5 * time.Minute,
|
||||||
|
},
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid format",
|
||||||
|
config: Config{
|
||||||
|
SampleRate: 16000,
|
||||||
|
Channels: 1,
|
||||||
|
Format: "",
|
||||||
|
BufferSize: 8192,
|
||||||
|
ChannelBufferSize: 30,
|
||||||
|
Timeout: 5 * time.Minute,
|
||||||
|
},
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid timeout",
|
||||||
|
config: Config{
|
||||||
|
SampleRate: 16000,
|
||||||
|
Channels: 1,
|
||||||
|
Format: "s16",
|
||||||
|
BufferSize: 8192,
|
||||||
|
ChannelBufferSize: 30,
|
||||||
|
Timeout: 0,
|
||||||
|
},
|
||||||
|
wantErr: false, // Timeout validation is not implemented in validateConfig
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
recorder := NewRecorder(tt.config)
|
||||||
|
err := recorder.validateConfig()
|
||||||
|
if (err != nil) != tt.wantErr {
|
||||||
|
t.Errorf("validateConfig() error = %v, wantErr %v", err, tt.wantErr)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRecorder_BuildPwRecordArgs(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
config Config
|
||||||
|
expected []string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "default config",
|
||||||
|
config: Config{
|
||||||
|
SampleRate: 16000,
|
||||||
|
Channels: 1,
|
||||||
|
Format: "s16",
|
||||||
|
Device: "",
|
||||||
|
},
|
||||||
|
expected: []string{
|
||||||
|
"--format", "s16",
|
||||||
|
"--rate", "16000",
|
||||||
|
"--channels", "1",
|
||||||
|
"-",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "with device",
|
||||||
|
config: Config{
|
||||||
|
SampleRate: 44100,
|
||||||
|
Channels: 2,
|
||||||
|
Format: "s32",
|
||||||
|
Device: "hw:0",
|
||||||
|
},
|
||||||
|
expected: []string{
|
||||||
|
"--format", "s32",
|
||||||
|
"--rate", "44100",
|
||||||
|
"--channels", "2",
|
||||||
|
"-",
|
||||||
|
"--target", "hw:0",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
recorder := NewRecorder(tt.config)
|
||||||
|
args := recorder.buildPwRecordArgs()
|
||||||
|
|
||||||
|
if len(args) != len(tt.expected) {
|
||||||
|
t.Errorf("buildPwRecordArgs() returned %d args, want %d", len(args), len(tt.expected))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, arg := range args {
|
||||||
|
if arg != tt.expected[i] {
|
||||||
|
t.Errorf("buildPwRecordArgs()[%d] = %q, want %q", i, arg, tt.expected[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCheckPipeWireAvailable(t *testing.T) {
|
||||||
|
// Test with context timeout
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
// This test will fail if pw-record is not available in the system
|
||||||
|
// In a real CI environment, we would mock this
|
||||||
|
err := CheckPipeWireAvailable(ctx)
|
||||||
|
if err != nil {
|
||||||
|
t.Logf("CheckPipeWireAvailable() failed (expected if pw-record not installed): %v", err)
|
||||||
|
// Don't fail the test if pw-record is not available
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// If pw-record is available, the function should succeed
|
||||||
|
t.Logf("CheckPipeWireAvailable() succeeded - pw-record is available")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAudioFrame(t *testing.T) {
|
||||||
|
data := []byte{1, 2, 3, 4}
|
||||||
|
timestamp := time.Now()
|
||||||
|
|
||||||
|
frame := AudioFrame{
|
||||||
|
Data: data,
|
||||||
|
Timestamp: timestamp,
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(frame.Data) != len(data) {
|
||||||
|
t.Errorf("Data length mismatch: got %d, want %d", len(frame.Data), len(data))
|
||||||
|
}
|
||||||
|
|
||||||
|
if frame.Timestamp != timestamp {
|
||||||
|
t.Errorf("Timestamp mismatch: got %v, want %v", frame.Timestamp, timestamp)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test with nil data
|
||||||
|
emptyFrame := AudioFrame{}
|
||||||
|
if emptyFrame.Data != nil {
|
||||||
|
t.Errorf("Empty frame should have nil data")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestRecorder_Start tests the Start method with mocked external dependencies
|
||||||
|
// This is a simplified test that focuses on the logic rather than actual audio capture
|
||||||
|
func TestRecorder_Start(t *testing.T) {
|
||||||
|
config := Config{
|
||||||
|
SampleRate: 16000,
|
||||||
|
Channels: 1,
|
||||||
|
Format: "s16",
|
||||||
|
BufferSize: 8192,
|
||||||
|
Device: "",
|
||||||
|
ChannelBufferSize: 30,
|
||||||
|
Timeout: 5 * time.Minute,
|
||||||
|
}
|
||||||
|
|
||||||
|
recorder := NewRecorder(config)
|
||||||
|
|
||||||
|
// Test that we can't start recording if already recording
|
||||||
|
ctx := context.Background()
|
||||||
|
frameCh1, errCh1, err := recorder.Start(ctx)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Start() error = %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if frameCh1 == nil {
|
||||||
|
t.Errorf("Start() returned nil frame channel")
|
||||||
|
}
|
||||||
|
|
||||||
|
if errCh1 == nil {
|
||||||
|
t.Errorf("Start() returned nil error channel")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Should not be able to start again
|
||||||
|
_, _, err = recorder.Start(ctx)
|
||||||
|
if err == nil {
|
||||||
|
t.Errorf("Start() should fail when already recording")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stop the recorder
|
||||||
|
recorder.Stop()
|
||||||
|
|
||||||
|
// Give it a moment to stop
|
||||||
|
time.Sleep(10 * time.Millisecond)
|
||||||
|
|
||||||
|
// Should be able to start again after stopping
|
||||||
|
frameCh2, errCh2, err := recorder.Start(ctx)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Start() error after stop = %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if frameCh2 == nil {
|
||||||
|
t.Errorf("Start() returned nil frame channel after restart")
|
||||||
|
}
|
||||||
|
|
||||||
|
if errCh2 == nil {
|
||||||
|
t.Errorf("Start() returned nil error channel after restart")
|
||||||
|
}
|
||||||
|
|
||||||
|
recorder.Stop()
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestRecorder_Stop tests the Stop method
|
||||||
|
func TestRecorder_Stop(t *testing.T) {
|
||||||
|
config := Config{
|
||||||
|
SampleRate: 16000,
|
||||||
|
Channels: 1,
|
||||||
|
Format: "s16",
|
||||||
|
BufferSize: 8192,
|
||||||
|
Device: "",
|
||||||
|
ChannelBufferSize: 30,
|
||||||
|
Timeout: 5 * time.Minute,
|
||||||
|
}
|
||||||
|
|
||||||
|
recorder := NewRecorder(config)
|
||||||
|
|
||||||
|
// Stop should be safe to call even when not recording
|
||||||
|
recorder.Stop()
|
||||||
|
|
||||||
|
// Start recording
|
||||||
|
ctx := context.Background()
|
||||||
|
_, _, err := recorder.Start(ctx)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Start() error = %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stop should work when recording
|
||||||
|
recorder.Stop()
|
||||||
|
|
||||||
|
// Give it a moment to stop
|
||||||
|
time.Sleep(10 * time.Millisecond)
|
||||||
|
|
||||||
|
// Should not be recording anymore
|
||||||
|
if recorder.IsRecording() {
|
||||||
|
t.Errorf("IsRecording() = true after stop, want false")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestRecorder_Start_InvalidConfig tests starting with invalid config
|
||||||
|
func TestRecorder_Start_InvalidConfig(t *testing.T) {
|
||||||
|
invalidConfig := Config{
|
||||||
|
SampleRate: 0, // Invalid
|
||||||
|
Channels: 1,
|
||||||
|
Format: "s16",
|
||||||
|
BufferSize: 8192,
|
||||||
|
}
|
||||||
|
|
||||||
|
recorder := NewRecorder(invalidConfig)
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
_, _, err := recorder.Start(ctx)
|
||||||
|
if err == nil {
|
||||||
|
t.Errorf("Start() should fail with invalid config")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,181 @@
|
|||||||
|
package testutil
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/leonardotrapani/hyprvoice/internal/config"
|
||||||
|
"github.com/leonardotrapani/hyprvoice/internal/recording"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TestConfig returns a valid configuration for testing
|
||||||
|
func TestConfig() *config.Config {
|
||||||
|
return &config.Config{
|
||||||
|
Recording: config.RecordingConfig{
|
||||||
|
SampleRate: 16000,
|
||||||
|
Channels: 1,
|
||||||
|
Format: "s16",
|
||||||
|
BufferSize: 8192,
|
||||||
|
Device: "",
|
||||||
|
ChannelBufferSize: 30,
|
||||||
|
Timeout: 5 * time.Minute,
|
||||||
|
},
|
||||||
|
Transcription: config.TranscriptionConfig{
|
||||||
|
Provider: "openai",
|
||||||
|
APIKey: "test-api-key",
|
||||||
|
Language: "",
|
||||||
|
Model: "whisper-1",
|
||||||
|
},
|
||||||
|
Injection: config.InjectionConfig{
|
||||||
|
Mode: "fallback",
|
||||||
|
RestoreClipboard: true,
|
||||||
|
WtypeTimeout: 5 * time.Second,
|
||||||
|
ClipboardTimeout: 3 * time.Second,
|
||||||
|
},
|
||||||
|
Notifications: config.NotificationsConfig{
|
||||||
|
Enabled: true,
|
||||||
|
Type: "log",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestConfigWithInvalidValues returns a config with invalid values for testing validation
|
||||||
|
func TestConfigWithInvalidValues() *config.Config {
|
||||||
|
return &config.Config{
|
||||||
|
Recording: config.RecordingConfig{
|
||||||
|
SampleRate: 0, // Invalid
|
||||||
|
Channels: 0, // Invalid
|
||||||
|
Format: "", // Invalid
|
||||||
|
BufferSize: 0, // Invalid
|
||||||
|
ChannelBufferSize: 0, // Invalid
|
||||||
|
Timeout: 0, // Invalid
|
||||||
|
},
|
||||||
|
Transcription: config.TranscriptionConfig{
|
||||||
|
Provider: "", // Invalid
|
||||||
|
APIKey: "", // Invalid
|
||||||
|
Model: "", // Invalid
|
||||||
|
},
|
||||||
|
Injection: config.InjectionConfig{
|
||||||
|
Mode: "invalid", // Invalid
|
||||||
|
WtypeTimeout: 0, // Invalid
|
||||||
|
ClipboardTimeout: 0, // Invalid
|
||||||
|
},
|
||||||
|
Notifications: config.NotificationsConfig{
|
||||||
|
Type: "invalid", // Invalid
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateTempConfigFile creates a temporary config file for testing
|
||||||
|
func CreateTempConfigFile(t *testing.T, configContent string) string {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
tempDir := t.TempDir()
|
||||||
|
configPath := filepath.Join(tempDir, "config.toml")
|
||||||
|
|
||||||
|
err := os.WriteFile(configPath, []byte(configContent), 0644)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create temp config file: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return configPath
|
||||||
|
}
|
||||||
|
|
||||||
|
// MockCommandExecutor provides a way to mock exec.Command calls
|
||||||
|
type MockCommandExecutor struct {
|
||||||
|
Commands []MockCommand
|
||||||
|
}
|
||||||
|
|
||||||
|
type MockCommand struct {
|
||||||
|
Command string
|
||||||
|
Args []string
|
||||||
|
Output string
|
||||||
|
Error error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MockCommandExecutor) AddCommand(cmd string, args []string, output string, err error) {
|
||||||
|
m.Commands = append(m.Commands, MockCommand{
|
||||||
|
Command: cmd,
|
||||||
|
Args: args,
|
||||||
|
Output: output,
|
||||||
|
Error: err,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// MockAudioFrame creates a test audio frame
|
||||||
|
func MockAudioFrame(data []byte) recording.AudioFrame {
|
||||||
|
if data == nil {
|
||||||
|
data = make([]byte, 1024)
|
||||||
|
for i := range data {
|
||||||
|
data[i] = byte(i % 256)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return recording.AudioFrame{
|
||||||
|
Data: data,
|
||||||
|
Timestamp: time.Now(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MockTranscriberAdapter implements transcriber.TranscriptionAdapter for testing
|
||||||
|
type MockTranscriberAdapter struct {
|
||||||
|
TranscribeFunc func(ctx context.Context, audioData []byte) (string, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MockTranscriberAdapter) Transcribe(ctx context.Context, audioData []byte) (string, error) {
|
||||||
|
if m.TranscribeFunc != nil {
|
||||||
|
return m.TranscribeFunc(ctx, audioData)
|
||||||
|
}
|
||||||
|
return "mock transcription", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewMockTranscriberAdapter creates a mock transcriber adapter
|
||||||
|
func NewMockTranscriberAdapter() *MockTranscriberAdapter {
|
||||||
|
return &MockTranscriberAdapter{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestContext returns a context with timeout for testing
|
||||||
|
func TestContext() (context.Context, context.CancelFunc) {
|
||||||
|
return context.WithTimeout(context.Background(), 5*time.Second)
|
||||||
|
}
|
||||||
|
|
||||||
|
// WaitForCondition waits for a condition to be true or times out
|
||||||
|
func WaitForCondition(t *testing.T, condition func() bool, timeout time.Duration) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
t.Fatalf("Condition not met within %v", timeout)
|
||||||
|
default:
|
||||||
|
if condition() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
time.Sleep(10 * time.Millisecond)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// CaptureOutput captures stdout/stderr for testing
|
||||||
|
func CaptureOutput(t *testing.T, fn func()) string {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
old := os.Stdout
|
||||||
|
r, w, _ := os.Pipe()
|
||||||
|
os.Stdout = w
|
||||||
|
|
||||||
|
fn()
|
||||||
|
|
||||||
|
w.Close()
|
||||||
|
os.Stdout = old
|
||||||
|
|
||||||
|
out, _ := io.ReadAll(r)
|
||||||
|
return string(out)
|
||||||
|
}
|
||||||
@@ -0,0 +1,409 @@
|
|||||||
|
package transcriber
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/leonardotrapani/hyprvoice/internal/recording"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestNewTranscriber(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
config Config
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "valid openai config",
|
||||||
|
config: Config{
|
||||||
|
Provider: "openai",
|
||||||
|
APIKey: "test-key",
|
||||||
|
Language: "en",
|
||||||
|
Model: "whisper-1",
|
||||||
|
},
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "openai config without api key",
|
||||||
|
config: Config{
|
||||||
|
Provider: "openai",
|
||||||
|
APIKey: "",
|
||||||
|
Language: "en",
|
||||||
|
Model: "whisper-1",
|
||||||
|
},
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "unsupported provider",
|
||||||
|
config: Config{
|
||||||
|
Provider: "unsupported",
|
||||||
|
APIKey: "test-key",
|
||||||
|
Model: "whisper-1",
|
||||||
|
},
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "empty provider",
|
||||||
|
config: Config{
|
||||||
|
Provider: "",
|
||||||
|
APIKey: "test-key",
|
||||||
|
Model: "whisper-1",
|
||||||
|
},
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "empty model",
|
||||||
|
config: Config{
|
||||||
|
Provider: "openai",
|
||||||
|
APIKey: "test-key",
|
||||||
|
Language: "en",
|
||||||
|
Model: "",
|
||||||
|
},
|
||||||
|
wantErr: false, // Model validation is not implemented in NewTranscriber
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
transcriber, err := NewTranscriber(tt.config)
|
||||||
|
if (err != nil) != tt.wantErr {
|
||||||
|
t.Errorf("NewTranscriber() error = %v, wantErr %v", err, tt.wantErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if !tt.wantErr && transcriber == nil {
|
||||||
|
t.Errorf("NewTranscriber() returned nil transcriber")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConfig(t *testing.T) {
|
||||||
|
config := Config{
|
||||||
|
Provider: "openai",
|
||||||
|
APIKey: "test-key",
|
||||||
|
Language: "en",
|
||||||
|
Model: "whisper-1",
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.Provider != "openai" {
|
||||||
|
t.Errorf("Provider mismatch: got %s, want %s", config.Provider, "openai")
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.APIKey != "test-key" {
|
||||||
|
t.Errorf("APIKey mismatch: got %s, want %s", config.APIKey, "test-key")
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.Language != "en" {
|
||||||
|
t.Errorf("Language mismatch: got %s, want %s", config.Language, "en")
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.Model != "whisper-1" {
|
||||||
|
t.Errorf("Model mismatch: got %s, want %s", config.Model, "whisper-1")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MockTranscriptionAdapter implements TranscriptionAdapter for testing
|
||||||
|
type MockTranscriptionAdapter struct {
|
||||||
|
TranscribeFunc func(ctx context.Context, audioData []byte) (string, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MockTranscriptionAdapter) Transcribe(ctx context.Context, audioData []byte) (string, error) {
|
||||||
|
if m.TranscribeFunc != nil {
|
||||||
|
return m.TranscribeFunc(ctx, audioData)
|
||||||
|
}
|
||||||
|
return "mock transcription", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSimpleTranscriber_Start(t *testing.T) {
|
||||||
|
config := Config{
|
||||||
|
Provider: "openai",
|
||||||
|
APIKey: "test-key",
|
||||||
|
Language: "en",
|
||||||
|
Model: "whisper-1",
|
||||||
|
}
|
||||||
|
|
||||||
|
adapter := &MockTranscriptionAdapter{}
|
||||||
|
transcriber := NewSimpleTranscriber(config, adapter)
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
frameCh := make(chan recording.AudioFrame, 10)
|
||||||
|
|
||||||
|
// Test starting transcriber
|
||||||
|
errCh, err := transcriber.Start(ctx, frameCh)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Start() error = %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if errCh == nil {
|
||||||
|
t.Errorf("Start() returned nil error channel")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test starting again should fail
|
||||||
|
_, err = transcriber.Start(ctx, frameCh)
|
||||||
|
if err == nil {
|
||||||
|
t.Errorf("Start() should fail when already running")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stop the transcriber
|
||||||
|
err = transcriber.Stop(ctx)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Stop() error = %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSimpleTranscriber_Stop(t *testing.T) {
|
||||||
|
config := Config{
|
||||||
|
Provider: "openai",
|
||||||
|
APIKey: "test-key",
|
||||||
|
Language: "en",
|
||||||
|
Model: "whisper-1",
|
||||||
|
}
|
||||||
|
|
||||||
|
adapter := &MockTranscriptionAdapter{}
|
||||||
|
transcriber := NewSimpleTranscriber(config, adapter)
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
frameCh := make(chan recording.AudioFrame, 10)
|
||||||
|
|
||||||
|
// Stop should be safe when not running
|
||||||
|
err := transcriber.Stop(ctx)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Stop() error when not running = %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start and then stop
|
||||||
|
_, err = transcriber.Start(ctx, frameCh)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Start() error = %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close the frame channel to signal completion
|
||||||
|
close(frameCh)
|
||||||
|
|
||||||
|
err = transcriber.Stop(ctx)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Stop() error = %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stop again should be safe
|
||||||
|
err = transcriber.Stop(ctx)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Stop() error after already stopped = %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSimpleTranscriber_GetFinalTranscription(t *testing.T) {
|
||||||
|
config := Config{
|
||||||
|
Provider: "openai",
|
||||||
|
APIKey: "test-key",
|
||||||
|
Language: "en",
|
||||||
|
Model: "whisper-1",
|
||||||
|
}
|
||||||
|
|
||||||
|
adapter := &MockTranscriptionAdapter{
|
||||||
|
TranscribeFunc: func(ctx context.Context, audioData []byte) (string, error) {
|
||||||
|
return "test transcription", nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
transcriber := NewSimpleTranscriber(config, adapter)
|
||||||
|
|
||||||
|
// Test getting transcription before any processing
|
||||||
|
transcription, err := transcriber.GetFinalTranscription()
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("GetFinalTranscription() error = %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Should return empty string initially
|
||||||
|
if transcription != "" {
|
||||||
|
t.Errorf("GetFinalTranscription() = %q, want empty string", transcription)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSimpleTranscriber_CollectAudio(t *testing.T) {
|
||||||
|
config := Config{
|
||||||
|
Provider: "openai",
|
||||||
|
APIKey: "test-key",
|
||||||
|
Language: "en",
|
||||||
|
Model: "whisper-1",
|
||||||
|
}
|
||||||
|
|
||||||
|
adapter := &MockTranscriptionAdapter{}
|
||||||
|
transcriber := NewSimpleTranscriber(config, adapter)
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
frameCh := make(chan recording.AudioFrame, 10)
|
||||||
|
errCh := make(chan error, 1)
|
||||||
|
|
||||||
|
// Start collecting audio in background
|
||||||
|
transcriber.wg.Add(1)
|
||||||
|
go transcriber.collectAudio(ctx, frameCh, errCh)
|
||||||
|
|
||||||
|
// Send some test audio frames
|
||||||
|
testData1 := []byte{1, 2, 3, 4}
|
||||||
|
testData2 := []byte{5, 6, 7, 8}
|
||||||
|
|
||||||
|
frame1 := recording.AudioFrame{
|
||||||
|
Data: testData1,
|
||||||
|
Timestamp: time.Now(),
|
||||||
|
}
|
||||||
|
|
||||||
|
frame2 := recording.AudioFrame{
|
||||||
|
Data: testData2,
|
||||||
|
Timestamp: time.Now(),
|
||||||
|
}
|
||||||
|
|
||||||
|
frameCh <- frame1
|
||||||
|
frameCh <- frame2
|
||||||
|
close(frameCh)
|
||||||
|
|
||||||
|
// Wait for processing to complete
|
||||||
|
transcriber.wg.Wait()
|
||||||
|
|
||||||
|
// Check that audio was collected
|
||||||
|
if len(transcriber.audioBuffer) != len(testData1)+len(testData2) {
|
||||||
|
t.Errorf("Audio buffer length = %d, want %d", len(transcriber.audioBuffer), len(testData1)+len(testData2))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSimpleTranscriber_TranscribeAll(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
audioData []byte
|
||||||
|
mockResult string
|
||||||
|
mockError error
|
||||||
|
expectError bool
|
||||||
|
expectedResult string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "successful transcription",
|
||||||
|
audioData: []byte{1, 2, 3, 4},
|
||||||
|
mockResult: "hello world",
|
||||||
|
mockError: nil,
|
||||||
|
expectError: false,
|
||||||
|
expectedResult: "hello world",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "empty audio data",
|
||||||
|
audioData: []byte{},
|
||||||
|
mockResult: "",
|
||||||
|
mockError: nil,
|
||||||
|
expectError: false,
|
||||||
|
expectedResult: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "transcription error",
|
||||||
|
audioData: []byte{1, 2, 3, 4},
|
||||||
|
mockResult: "",
|
||||||
|
mockError: fmt.Errorf("api error"),
|
||||||
|
expectError: true,
|
||||||
|
expectedResult: "",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
config := Config{
|
||||||
|
Provider: "openai",
|
||||||
|
APIKey: "test-key",
|
||||||
|
Language: "en",
|
||||||
|
Model: "whisper-1",
|
||||||
|
}
|
||||||
|
|
||||||
|
adapter := &MockTranscriptionAdapter{
|
||||||
|
TranscribeFunc: func(ctx context.Context, audioData []byte) (string, error) {
|
||||||
|
return tt.mockResult, tt.mockError
|
||||||
|
},
|
||||||
|
}
|
||||||
|
transcriber := NewSimpleTranscriber(config, adapter)
|
||||||
|
|
||||||
|
// Set up audio buffer
|
||||||
|
transcriber.audioBuffer = tt.audioData
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
err := transcriber.transcribeAll(ctx)
|
||||||
|
|
||||||
|
if (err != nil) != tt.expectError {
|
||||||
|
t.Errorf("transcribeAll() error = %v, expectError %v", err, tt.expectError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if !tt.expectError {
|
||||||
|
result, err := transcriber.GetFinalTranscription()
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("GetFinalTranscription() error = %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if result != tt.expectedResult {
|
||||||
|
t.Errorf("GetFinalTranscription() = %q, want %q", result, tt.expectedResult)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewSimpleTranscriber(t *testing.T) {
|
||||||
|
config := Config{
|
||||||
|
Provider: "openai",
|
||||||
|
APIKey: "test-key",
|
||||||
|
Language: "en",
|
||||||
|
Model: "whisper-1",
|
||||||
|
}
|
||||||
|
|
||||||
|
adapter := &MockTranscriptionAdapter{}
|
||||||
|
transcriber := NewSimpleTranscriber(config, adapter)
|
||||||
|
|
||||||
|
if transcriber == nil {
|
||||||
|
t.Errorf("NewSimpleTranscriber() returned nil")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if transcriber.adapter != adapter {
|
||||||
|
t.Errorf("Adapter not set correctly")
|
||||||
|
}
|
||||||
|
|
||||||
|
if transcriber.config.Provider != config.Provider {
|
||||||
|
t.Errorf("Config not set correctly")
|
||||||
|
}
|
||||||
|
|
||||||
|
if transcriber.running {
|
||||||
|
t.Errorf("Transcriber should not be running initially")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(transcriber.audioBuffer) != 0 {
|
||||||
|
t.Errorf("Audio buffer should be empty initially")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTranscriptionAdapter(t *testing.T) {
|
||||||
|
adapter := &MockTranscriptionAdapter{
|
||||||
|
TranscribeFunc: func(ctx context.Context, audioData []byte) (string, error) {
|
||||||
|
return "test result", nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
audioData := []byte{1, 2, 3, 4}
|
||||||
|
|
||||||
|
result, err := adapter.Transcribe(ctx, audioData)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Transcribe() error = %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if result != "test result" {
|
||||||
|
t.Errorf("Transcribe() = %q, want %q", result, "test result")
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user