Files
tapir/internal/config/uploads_local_test.go
T
mathias 4a2067063d
CI / Deploy via GitOps (push) Successful in 7s
CI / Lint / Test / Vet (push) Successful in 33s
CI / Build & Import (push) Successful in 23s
feat: upload audio, get a private Swedish transcript, discuss it (#33, ADR-033) (#39)
Upload a recording (audio, or video with the camera off), get a private Swedish transcript, and chat about it.

- ADR-033: RLS-scoped private transcript store; ffprobe/ffmpeg intake; gateway transcription (iguana kb-whisper, berget fallback); in-process single-replica worker; chat over the transcript (local models only).
- ADR-034: optional two-engine mode (pianissimo + kb-whisper) reconciled by a LOCAL model; off unless TAPIR_RECONCILE_BASE_MODEL is set.
- ADR-035: real progress (phases, running/quiet/lost), phase logging, every judged spot stored, merged segments.
- Hardening from four adversarial reviews: errors logged and stored as kinds never text; per-engine deadlines; converted-length check; bounded queue; client hang-ups are not errors; strict local-alias check.
- Dockerfile: static ffmpeg/ffprobe 7.0.2 pinned by sha256 (#34). Built and inspected by hand; CI does not build the image on a PR, so its first CI build is this merge.

Uploads stay OFF until TAPIR_UPLOADS_ENABLED is set in the deployment.

Closes #33, #34, #42, #43, #44.

Co-authored-by: Mathias <mthbqv@gmail.com>
2026-10-03 14:13:37 +00:00

102 lines
3.1 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package config
import (
"testing"
"time"
)
// What counts as a LOCAL model alias, and where that is demanded (ADR-034, ADR-035 review).
// A private transcript may be sent to a local model and to nothing else; the check is a
// guard against a misconfiguration or a crafted alias, so it must be strict about shape,
// not just about a prefix.
func TestIsLocalModel_AcceptsRealLocalAliasesAndNothingShapedLikeAnEscape(t *testing.T) {
tests := []struct {
alias string
want bool
}{
{"koala/phi4-mini", true},
{"iguana/gemma4-26b", true},
{"iguana/gemma4-31b-think", true},
{"koala/qwen36-35b-a3b", true},
{"iguana/kb-whisper", true},
{"iguana/hf.co-model_v1:q4", true},
{"", false},
{"koala/", false},
{"iguana/", false},
{"gemma4-26b", false},
{"berget/mistral-small", false},
{"berget/koala/x", false},
{"xkoala/x", false},
{"Koala/x", false},
{" koala/x", false},
{"koala/x ", false},
{"koala/x y", false},
{"koala/x\n", false},
{"koala/../berget/kb", false},
{"koala//berget/mistral-small", false},
{"koala/x/y", false},
{"koala/x?host=berget", false},
{"koala/x#frag", false},
{"koala/x@evil", false},
{"koalа/x", false}, // Cyrillic а
}
for _, tt := range tests {
t.Run(tt.alias, func(t *testing.T) {
if got := IsLocalModel(tt.alias); got != tt.want {
t.Errorf("IsLocalModel(%q) = %v, want %v", tt.alias, got, tt.want)
}
})
}
}
func TestLoad_UploadChatModelMustBeLocalBecauseTheWholeTranscriptIsSentToIt(t *testing.T) {
for _, alias := range []string{"berget/mistral-small", "gemini/gemini-2.5-pro", "gemma4-26b", "koala/../berget/kb"} {
t.Run(alias, func(t *testing.T) {
clearUploadEnv(t)
setEnv(t, map[string]string{"TAPIR_UPLOAD_CHAT_MODEL": alias})
if _, err := Load(); err == nil {
t.Errorf("Load accepted TAPIR_UPLOAD_CHAT_MODEL=%q: a private transcript of up to 90,000 characters would be sent to it", alias)
}
})
}
}
func TestLoad_ALocalUploadChatModelIsAccepted(t *testing.T) {
clearUploadEnv(t)
setEnv(t, map[string]string{"TAPIR_UPLOAD_CHAT_MODEL": "iguana/gemma4-31b"})
cfg, err := Load()
if err != nil {
t.Fatalf("Load: %v", err)
}
if cfg.UploadChatModel != "iguana/gemma4-31b" {
t.Errorf("UploadChatModel = %q", cfg.UploadChatModel)
}
}
func TestLoad_TheEngineTimeoutDefaultsToTwentyMinutesAndMustBePositive(t *testing.T) {
clearUploadEnv(t)
cfg, err := Load()
if err != nil {
t.Fatalf("Load: %v", err)
}
if cfg.TranscribeEngineTimeout != 20*time.Minute {
t.Errorf("TranscribeEngineTimeout = %v, want 20m: a 90-minute recording takes about 8 minutes on kb-whisper, and two wedged engines in sequence must leave the job time to reach a fallback", cfg.TranscribeEngineTimeout)
}
setEnv(t, map[string]string{"TAPIR_TRANSCRIBE_ENGINE_TIMEOUT": "7m"})
cfg, err = Load()
if err != nil || cfg.TranscribeEngineTimeout != 7*time.Minute {
t.Errorf("override: %v, %v", cfg.TranscribeEngineTimeout, err)
}
for _, bad := range []string{"0", "-1m", "soon"} {
setEnv(t, map[string]string{"TAPIR_TRANSCRIBE_ENGINE_TIMEOUT": bad})
if _, err := Load(); err == nil {
t.Errorf("Load accepted TAPIR_TRANSCRIBE_ENGINE_TIMEOUT=%q", bad)
}
}
}