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>
94 lines
2.6 KiB
Go
94 lines
2.6 KiB
Go
package uploads_test
|
|
|
|
import (
|
|
"errors"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"git.d-ma.be/mathias/tapir/internal/uploads"
|
|
)
|
|
|
|
var testLimits = uploads.Limits{MaxBytes: 200 << 20, MaxDuration: 90 * time.Minute}
|
|
|
|
func TestValidate(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
probe uploads.Probe
|
|
wantReason uploads.Reason // "" = accepted
|
|
wantInMsg []string
|
|
}{
|
|
{
|
|
name: "audio-only file within the duration limit is accepted",
|
|
probe: uploads.Probe{Duration: 45 * time.Minute, HasAudio: true},
|
|
},
|
|
{
|
|
name: "a recording exactly at the limit is accepted",
|
|
probe: uploads.Probe{Duration: 90 * time.Minute, HasAudio: true},
|
|
},
|
|
{
|
|
name: "a recording that also has a video stream is accepted: the audio is what is used",
|
|
probe: uploads.Probe{Duration: 4 * time.Minute, HasAudio: true, HasVideo: true},
|
|
},
|
|
{
|
|
name: "video with no audio is rejected as having no audio, and says so",
|
|
probe: uploads.Probe{Duration: 4 * time.Minute, HasVideo: true},
|
|
wantReason: uploads.ReasonNoAudio,
|
|
wantInMsg: []string{"no audio"},
|
|
},
|
|
{
|
|
name: "a file with no audio stream is rejected",
|
|
probe: uploads.Probe{Duration: 4 * time.Minute},
|
|
wantReason: uploads.ReasonNoAudio,
|
|
wantInMsg: []string{"no audio"},
|
|
},
|
|
{
|
|
name: "a recording over the limit is rejected, stating both lengths",
|
|
probe: uploads.Probe{Duration: 102 * time.Minute, HasAudio: true},
|
|
wantReason: uploads.ReasonTooLong,
|
|
wantInMsg: []string{"1 h 42 min", "1 h 30 min"},
|
|
},
|
|
{
|
|
name: "zero duration is unreadable, not accepted as an empty recording",
|
|
probe: uploads.Probe{HasAudio: true},
|
|
wantReason: uploads.ReasonUnreadable,
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
err := uploads.Validate(tt.probe, testLimits)
|
|
|
|
if tt.wantReason == "" {
|
|
require.NoError(t, err)
|
|
return
|
|
}
|
|
var rej *uploads.RejectError
|
|
require.True(t, errors.As(err, &rej), "want a *RejectError, got %v", err)
|
|
assert.Equal(t, tt.wantReason, rej.Reason)
|
|
for _, s := range tt.wantInMsg {
|
|
assert.Contains(t, rej.Message, s)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestFormatDuration(t *testing.T) {
|
|
tests := []struct {
|
|
d time.Duration
|
|
want string
|
|
}{
|
|
{90 * time.Minute, "1 h 30 min"},
|
|
{102 * time.Minute, "1 h 42 min"},
|
|
{45 * time.Minute, "45 min"},
|
|
{2 * time.Hour, "2 h"},
|
|
{30 * time.Second, "30 s"},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.want, func(t *testing.T) {
|
|
assert.Equal(t, tt.want, uploads.FormatDuration(tt.d))
|
|
})
|
|
}
|
|
}
|