Files
tapir/internal/config/uploads_test.go
T

218 lines
7.3 KiB
Go

package config
import (
"os"
"testing"
"time"
)
// clearUploadEnv unsets every upload knob so a test observes the default, not the
// host environment.
func clearUploadEnv(t *testing.T) {
t.Helper()
for _, k := range []string{
"TAPIR_UPLOADS_ENABLED", "TAPIR_UPLOAD_MAX_MB", "TAPIR_UPLOAD_MAX_MINUTES",
"TAPIR_TRANSCRIBE_MODEL", "TAPIR_TRANSCRIBE_FALLBACK_MODEL", "TAPIR_TRANSCRIBE_TIMEOUT", "TAPIR_TRANSCRIBE_ENGINE_TIMEOUT",
"TAPIR_UPLOAD_TMP_DIR", "TAPIR_FFMPEG_BIN", "TAPIR_FFPROBE_BIN",
"TAPIR_MAX_UPLOADED_TRANSCRIPT_CHARS", "TAPIR_UPLOAD_CHAT_MODEL",
"TAPIR_RECONCILE_BASE_MODEL", "TAPIR_RECONCILE_MODEL", "TAPIR_RECONCILE_CHECK_ORDER",
} {
unset(t, k)
}
}
func TestLoad_UploadDefaults(t *testing.T) {
clearUploadEnv(t)
c, err := Load()
if err != nil {
t.Fatalf("Load: %v", err)
}
if c.UploadsEnabled {
t.Error("uploads must be OFF by default: the runtime image needs ffmpeg first (ADR-033)")
}
if c.UploadMaxBytes != 200<<20 {
t.Errorf("UploadMaxBytes = %d, want 200 MB", c.UploadMaxBytes)
}
if c.UploadMaxDuration != 90*time.Minute {
t.Errorf("UploadMaxDuration = %v, want 90m (the longest length measured, not a found ceiling)", c.UploadMaxDuration)
}
if c.TranscribeModel != "iguana/kb-whisper" {
t.Errorf("TranscribeModel = %q", c.TranscribeModel)
}
if c.TranscribeFallbackModel != "berget/kb-whisper" {
t.Errorf("TranscribeFallbackModel = %q", c.TranscribeFallbackModel)
}
if c.TranscribeTimeout != 60*time.Minute {
t.Errorf("TranscribeTimeout = %v, want 60m (ADR-034: two engines plus ~300 adjudications)", c.TranscribeTimeout)
}
if c.ReconcileBaseModel != "" || c.ReconcileModel != "" || c.ReconcileCheckOrder {
t.Errorf("reconciliation must be OFF by default: %q %q %v", c.ReconcileBaseModel, c.ReconcileModel, c.ReconcileCheckOrder)
}
if c.UploadTmpDir != os.TempDir() {
t.Errorf("UploadTmpDir = %q, want os.TempDir()", c.UploadTmpDir)
}
if c.FFmpegBin != "ffmpeg" || c.FFprobeBin != "ffprobe" {
t.Errorf("bins = %q, %q", c.FFmpegBin, c.FFprobeBin)
}
if c.MaxUploadedTranscriptChars != 90000 {
t.Errorf("MaxUploadedTranscriptChars = %d, want 90000: iguana/gemma4-26b answered correctly at 90,000 chars "+
"but silently dropped the head of a 120,000-char transcript (docs/research/chat-context)", c.MaxUploadedTranscriptChars)
}
if c.UploadChatModel != "iguana/gemma4-26b" {
t.Errorf("UploadChatModel = %q: koala/phi4-mini has an 8,192-token context and answers HTTP 400 on an hour of speech", c.UploadChatModel)
}
}
func TestLoad_UploadChatModelOverride(t *testing.T) {
clearUploadEnv(t)
t.Setenv("TAPIR_UPLOAD_CHAT_MODEL", "koala/qwen36-35b-a3b")
c, err := Load()
if err != nil {
t.Fatalf("Load: %v", err)
}
if c.UploadChatModel != "koala/qwen36-35b-a3b" {
t.Errorf("UploadChatModel = %q", c.UploadChatModel)
}
}
func TestLoad_UploadValuesAreParsed(t *testing.T) {
clearUploadEnv(t)
setEnv(t, map[string]string{
"TAPIR_UPLOADS_ENABLED": "true",
"TAPIR_UPLOAD_MAX_MB": "50",
"TAPIR_UPLOAD_MAX_MINUTES": "30",
"TAPIR_TRANSCRIBE_MODEL": "koala/whisper",
"TAPIR_TRANSCRIBE_TIMEOUT": "10m",
"TAPIR_UPLOAD_TMP_DIR": "/var/spool/tapir",
"TAPIR_FFMPEG_BIN": "/opt/ffmpeg",
"TAPIR_FFPROBE_BIN": "/opt/ffprobe",
"TAPIR_MAX_UPLOADED_TRANSCRIPT_CHARS": "60000",
})
c, err := Load()
if err != nil {
t.Fatalf("Load: %v", err)
}
if !c.UploadsEnabled || c.UploadMaxBytes != 50<<20 || c.UploadMaxDuration != 30*time.Minute ||
c.TranscribeModel != "koala/whisper" || c.TranscribeTimeout != 10*time.Minute ||
c.UploadTmpDir != "/var/spool/tapir" || c.FFmpegBin != "/opt/ffmpeg" ||
c.FFprobeBin != "/opt/ffprobe" || c.MaxUploadedTranscriptChars != 60000 {
t.Errorf("values not applied: %+v", c)
}
}
func TestLoad_EmptyTranscribeFallbackDisablesIt(t *testing.T) {
clearUploadEnv(t)
t.Setenv("TAPIR_TRANSCRIBE_FALLBACK_MODEL", "")
c, err := Load()
if err != nil {
t.Fatalf("Load: %v", err)
}
if c.TranscribeFallbackModel != "" {
t.Errorf("TranscribeFallbackModel = %q: an explicit empty value must disable the cloud fallback, "+
"so private audio can be kept on the homelab", c.TranscribeFallbackModel)
}
}
func TestLoad_UploadRejectsInvalidValues(t *testing.T) {
tests := []struct{ key, val string }{
{"TAPIR_UPLOADS_ENABLED", "maybe"},
{"TAPIR_UPLOAD_MAX_MB", "lots"},
{"TAPIR_UPLOAD_MAX_MB", "0"},
{"TAPIR_UPLOAD_MAX_MB", "-5"},
{"TAPIR_UPLOAD_MAX_MINUTES", "0"},
{"TAPIR_UPLOAD_MAX_MINUTES", "forever"},
{"TAPIR_TRANSCRIBE_TIMEOUT", "soon"},
{"TAPIR_TRANSCRIBE_TIMEOUT", "0s"},
}
for _, tt := range tests {
t.Run(tt.key+"="+tt.val, func(t *testing.T) {
clearUploadEnv(t)
t.Setenv(tt.key, tt.val)
if _, err := Load(); err == nil {
t.Errorf("Load accepted %s=%q; a zero or negative limit would reject every upload", tt.key, tt.val)
}
})
}
}
func TestLoad_ReconcileSettingsAreParsed(t *testing.T) {
clearUploadEnv(t)
setEnv(t, map[string]string{
"TAPIR_RECONCILE_BASE_MODEL": "iguana/pianissimo",
"TAPIR_RECONCILE_MODEL": "iguana/gemma4-31b",
"TAPIR_RECONCILE_CHECK_ORDER": "true",
})
c, err := Load()
if err != nil {
t.Fatalf("Load: %v", err)
}
if c.ReconcileBaseModel != "iguana/pianissimo" || c.ReconcileModel != "iguana/gemma4-31b" || !c.ReconcileCheckOrder {
t.Errorf("not applied: %+v", c.UploadConfig)
}
}
func TestLoad_ReconcileRefusesAnUnsafeOrIncompleteSetup(t *testing.T) {
tests := []struct {
name string
env map[string]string
}{
{"a base engine with no adjudicator", map[string]string{"TAPIR_RECONCILE_BASE_MODEL": "iguana/pianissimo"}},
{"a cloud adjudicator: private meeting content must not reach it", map[string]string{
"TAPIR_RECONCILE_BASE_MODEL": "iguana/pianissimo", "TAPIR_RECONCILE_MODEL": "berget/mistral-small",
}},
{"a cloud adjudicator from another provider", map[string]string{
"TAPIR_RECONCILE_BASE_MODEL": "iguana/pianissimo", "TAPIR_RECONCILE_MODEL": "gemini/gemini-2.5-pro",
}},
{"an adjudicator alias with no host prefix", map[string]string{
"TAPIR_RECONCILE_BASE_MODEL": "iguana/pianissimo", "TAPIR_RECONCILE_MODEL": "gemma4-26b",
}},
{"a cloud alias that merely contains a local host name", map[string]string{
"TAPIR_RECONCILE_BASE_MODEL": "iguana/pianissimo", "TAPIR_RECONCILE_MODEL": "berget/iguana/gemma4-26b",
}},
{"a cloud base engine", map[string]string{
"TAPIR_RECONCILE_BASE_MODEL": "berget/kb-whisper", "TAPIR_RECONCILE_MODEL": "iguana/gemma4-26b",
}},
{"the same engine twice", map[string]string{
"TAPIR_RECONCILE_BASE_MODEL": "iguana/kb-whisper", "TAPIR_RECONCILE_MODEL": "iguana/gemma4-26b",
}},
{"a bad order-check flag", map[string]string{
"TAPIR_RECONCILE_BASE_MODEL": "iguana/pianissimo", "TAPIR_RECONCILE_MODEL": "iguana/gemma4-26b", "TAPIR_RECONCILE_CHECK_ORDER": "perhaps",
}},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
clearUploadEnv(t)
setEnv(t, tt.env)
if _, err := Load(); err == nil {
t.Errorf("Load accepted %v", tt.env)
}
})
}
}
func TestLoad_AnAdjudicatorWithoutABaseEngineIsIgnoredNotAnError(t *testing.T) {
clearUploadEnv(t)
t.Setenv("TAPIR_RECONCILE_MODEL", "iguana/gemma4-26b")
c, err := Load()
if err != nil {
t.Fatalf("Load: %v", err)
}
if c.ReconcileBaseModel != "" {
t.Errorf("reconciliation must stay off without a base engine")
}
}