mirror of
https://github.com/sipeed/picoclaw.git
synced 2026-06-12 18:08:54 +00:00
8ad4b9b497
- Add `AudioModelTranscriber` for model-based audio transcription via LLM providers - Support selecting a transcription model with `voice.model_name` in config - Keep Groq transcription as a fallback and move it into dedicated files with focused tests - Serialize `data:audio/...` media as input_audio for OpenAI-compatible providers - Improve transcription logging by rendering error fields as strings - Add coverage for transcriber detection, audio-model behavior, provider audio serialization, and Groq transcription Fixes #1890.
368 lines
8.1 KiB
Go
368 lines
8.1 KiB
Go
package logger
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"errors"
|
|
"testing"
|
|
|
|
"github.com/rs/zerolog"
|
|
)
|
|
|
|
func TestLogLevelFiltering(t *testing.T) {
|
|
initialLevel := GetLevel()
|
|
defer SetLevel(initialLevel)
|
|
|
|
SetLevel(WARN)
|
|
|
|
tests := []struct {
|
|
name string
|
|
level LogLevel
|
|
shouldLog bool
|
|
}{
|
|
{"DEBUG message", DEBUG, false},
|
|
{"INFO message", INFO, false},
|
|
{"WARN message", WARN, true},
|
|
{"ERROR message", ERROR, true},
|
|
{"FATAL message", FATAL, true},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
switch tt.level {
|
|
case DEBUG:
|
|
Debug(tt.name)
|
|
case INFO:
|
|
Info(tt.name)
|
|
case WARN:
|
|
Warn(tt.name)
|
|
case ERROR:
|
|
Error(tt.name)
|
|
case FATAL:
|
|
if tt.shouldLog {
|
|
t.Logf("FATAL test skipped to prevent program exit")
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
SetLevel(INFO)
|
|
}
|
|
|
|
func TestLoggerWithComponent(t *testing.T) {
|
|
initialLevel := GetLevel()
|
|
defer SetLevel(initialLevel)
|
|
|
|
SetLevel(DEBUG)
|
|
|
|
tests := []struct {
|
|
name string
|
|
component string
|
|
message string
|
|
fields map[string]any
|
|
}{
|
|
{"Simple message", "test", "Hello, world!", nil},
|
|
{"Message with component", "discord", "Discord message", nil},
|
|
{"Message with fields", "telegram", "Telegram message", map[string]any{
|
|
"user_id": "12345",
|
|
"count": 42,
|
|
}},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
switch {
|
|
case tt.fields == nil && tt.component != "":
|
|
InfoC(tt.component, tt.message)
|
|
case tt.fields != nil:
|
|
InfoF(tt.message, tt.fields)
|
|
default:
|
|
Info(tt.message)
|
|
}
|
|
})
|
|
}
|
|
|
|
SetLevel(INFO)
|
|
}
|
|
|
|
func TestLogLevels(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
level LogLevel
|
|
want string
|
|
}{
|
|
{"DEBUG level", DEBUG, "DEBUG"},
|
|
{"INFO level", INFO, "INFO"},
|
|
{"WARN level", WARN, "WARN"},
|
|
{"ERROR level", ERROR, "ERROR"},
|
|
{"FATAL level", FATAL, "FATAL"},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
if logLevelNames[tt.level] != tt.want {
|
|
t.Errorf("logLevelNames[%d] = %s, want %s", tt.level, logLevelNames[tt.level], tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestSetGetLevel(t *testing.T) {
|
|
initialLevel := GetLevel()
|
|
defer SetLevel(initialLevel)
|
|
|
|
tests := []LogLevel{DEBUG, INFO, WARN, ERROR, FATAL}
|
|
|
|
for _, level := range tests {
|
|
SetLevel(level)
|
|
if GetLevel() != level {
|
|
t.Errorf("SetLevel(%v) -> GetLevel() = %v, want %v", level, GetLevel(), level)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestLoggerHelperFunctions(t *testing.T) {
|
|
initialLevel := GetLevel()
|
|
defer SetLevel(initialLevel)
|
|
|
|
SetLevel(INFO)
|
|
|
|
Debug("This should not log")
|
|
Debugf("this should not log")
|
|
Info("This should log")
|
|
Warn("This should log")
|
|
Error("This should log")
|
|
|
|
InfoC("test", "Component message")
|
|
InfoF("Fields message", map[string]any{"key": "value"})
|
|
Infof("test from %v", "Infof")
|
|
|
|
WarnC("test", "Warning with component")
|
|
ErrorF("Error with fields", map[string]any{"error": "test"})
|
|
Errorf("test from %v", "Errorf")
|
|
|
|
SetLevel(DEBUG)
|
|
DebugC("test", "Debug with component")
|
|
Debugf("test from %v", "Debugf")
|
|
WarnF("Warning with fields", map[string]any{"key": "value"})
|
|
}
|
|
|
|
func TestFormatFieldValue(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
input any
|
|
expected string
|
|
}{
|
|
// Basic types test (default case of the switch)
|
|
{
|
|
name: "Integer Type",
|
|
input: 42,
|
|
expected: "42",
|
|
},
|
|
{
|
|
name: "Boolean Type",
|
|
input: true,
|
|
expected: "true",
|
|
},
|
|
{
|
|
name: "Unsupported Struct Type",
|
|
input: struct{ A int }{A: 1},
|
|
expected: "{1}",
|
|
},
|
|
|
|
// Simple strings and byte slices test
|
|
{
|
|
name: "Simple string without spaces",
|
|
input: "simple_value",
|
|
expected: "simple_value",
|
|
},
|
|
{
|
|
name: "Simple byte slice",
|
|
input: []byte("byte_value"),
|
|
expected: "byte_value",
|
|
},
|
|
|
|
// Unquoting test (strconv.Unquote)
|
|
{
|
|
name: "Quoted string",
|
|
input: `"quoted_value"`,
|
|
expected: "quoted_value",
|
|
},
|
|
|
|
// Strings with newline (\n) test
|
|
{
|
|
name: "String with newline",
|
|
input: "line1\nline2",
|
|
expected: "\nline1\nline2",
|
|
},
|
|
{
|
|
name: "Quoted string with newline (Unquote -> newline)",
|
|
input: `"line1\nline2"`, // Escaped \n that Unquote will resolve
|
|
expected: "\nline1\nline2",
|
|
},
|
|
|
|
// Strings with spaces test (which should be quoted)
|
|
{
|
|
name: "String with spaces",
|
|
input: "hello world",
|
|
expected: `"hello world"`,
|
|
},
|
|
{
|
|
name: "Quoted string with spaces (Unquote -> has spaces -> Re-quote)",
|
|
input: `"hello world"`,
|
|
expected: `"hello world"`,
|
|
},
|
|
|
|
// JSON formats test (strings with spaces that start/end with brackets)
|
|
{
|
|
name: "Valid JSON object",
|
|
input: `{"key": "value"}`,
|
|
expected: `{"key": "value"}`,
|
|
},
|
|
{
|
|
name: "Valid JSON array",
|
|
input: `[1, 2, "three"]`,
|
|
expected: `[1, 2, "three"]`,
|
|
},
|
|
{
|
|
name: "Fake JSON (starts with { but doesn't end with })",
|
|
input: `{"key": "value"`, // Missing closing bracket, has spaces
|
|
expected: `"{\"key\": \"value\""`,
|
|
},
|
|
{
|
|
name: "Empty JSON (object)",
|
|
input: `{ }`,
|
|
expected: `{ }`,
|
|
},
|
|
|
|
// 7. Edge Cases
|
|
{
|
|
name: "Empty string",
|
|
input: "",
|
|
expected: "",
|
|
},
|
|
{
|
|
name: "Whitespace only string",
|
|
input: " ",
|
|
expected: `" "`,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
actual := formatFieldValue(tt.input)
|
|
if actual != tt.expected {
|
|
t.Errorf("formatFieldValue() = %q, expected %q", actual, tt.expected)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestDefaultLevelIsInfo(t *testing.T) {
|
|
// The package-level default (before any SetLevel call) should be INFO.
|
|
// Because earlier tests may have changed it, we just verify the constant is wired correctly.
|
|
if logLevelNames[INFO] != "INFO" {
|
|
t.Errorf("INFO constant mapped to %q, want \"INFO\"", logLevelNames[INFO])
|
|
}
|
|
}
|
|
|
|
func TestParseLevelValid(t *testing.T) {
|
|
tests := []struct {
|
|
input string
|
|
want LogLevel
|
|
}{
|
|
{"debug", DEBUG},
|
|
{"DEBUG", DEBUG},
|
|
{"Debug", DEBUG},
|
|
{"info", INFO},
|
|
{"INFO", INFO},
|
|
{"warn", WARN},
|
|
{"WARN", WARN},
|
|
{"warning", WARN},
|
|
{"WARNING", WARN},
|
|
{"error", ERROR},
|
|
{"ERROR", ERROR},
|
|
{"fatal", FATAL},
|
|
{"FATAL", FATAL},
|
|
{" info ", INFO},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.input, func(t *testing.T) {
|
|
got, ok := ParseLevel(tt.input)
|
|
if !ok {
|
|
t.Fatalf("ParseLevel(%q) returned ok=false, want true", tt.input)
|
|
}
|
|
if got != tt.want {
|
|
t.Errorf("ParseLevel(%q) = %v, want %v", tt.input, got, tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestParseLevelInvalid(t *testing.T) {
|
|
tests := []string{"", "garbage", "verbose", "trace", "critical"}
|
|
|
|
for _, input := range tests {
|
|
t.Run(input, func(t *testing.T) {
|
|
_, ok := ParseLevel(input)
|
|
if ok {
|
|
t.Errorf("ParseLevel(%q) returned ok=true, want false", input)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestSetLevelFromString(t *testing.T) {
|
|
initialLevel := GetLevel()
|
|
defer SetLevel(initialLevel)
|
|
|
|
// Valid string changes the level
|
|
SetLevel(INFO)
|
|
SetLevelFromString("error")
|
|
if got := GetLevel(); got != ERROR {
|
|
t.Errorf("after SetLevelFromString(\"error\"): GetLevel() = %v, want ERROR", got)
|
|
}
|
|
|
|
// Empty string is a no-op
|
|
SetLevelFromString("")
|
|
if got := GetLevel(); got != ERROR {
|
|
t.Errorf("after SetLevelFromString(\"\"): GetLevel() = %v, want ERROR (unchanged)", got)
|
|
}
|
|
|
|
// Invalid string is a no-op
|
|
SetLevelFromString("garbage")
|
|
if got := GetLevel(); got != ERROR {
|
|
t.Errorf("after SetLevelFromString(\"garbage\"): GetLevel() = %v, want ERROR (unchanged)", got)
|
|
}
|
|
|
|
// Case-insensitive
|
|
SetLevelFromString("FATAL")
|
|
if got := GetLevel(); got != FATAL {
|
|
t.Errorf("after SetLevelFromString(\"FATAL\"): GetLevel() = %v, want FATAL", got)
|
|
}
|
|
}
|
|
|
|
func TestAppendFields_ErrorUsesErrorString(t *testing.T) {
|
|
var buf bytes.Buffer
|
|
l := zerolog.New(&buf)
|
|
|
|
event := l.Info()
|
|
appendFields(event, map[string]any{"error": errors.New("transcription request failed")})
|
|
event.Msg("test")
|
|
|
|
lines := bytes.Split(bytes.TrimSpace(buf.Bytes()), []byte("\n"))
|
|
if len(lines) == 0 {
|
|
t.Fatal("expected log output, got none")
|
|
}
|
|
|
|
var got map[string]any
|
|
if err := json.Unmarshal(lines[0], &got); err != nil {
|
|
t.Fatalf("unmarshal log line: %v", err)
|
|
}
|
|
|
|
if got["error"] != "transcription request failed" {
|
|
t.Fatalf("error field = %#v, want %q", got["error"], "transcription request failed")
|
|
}
|
|
}
|