Files
tapir/internal/reconcile/diff_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

140 lines
4.6 KiB
Go

package reconcile_test
import (
"math/rand"
"strings"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"git.d-ma.be/mathias/tapir/internal/reconcile"
)
func words(s string) []string { return strings.Fields(s) }
// apply rebuilds b from a using the ops, copying equal runs from a and the
// replacement/insertion runs from b. If Diff is right this must equal b.
func apply(a, b []string, ops []reconcile.Op) []string {
var out []string
for _, o := range ops {
switch o.Kind {
case reconcile.Equal:
out = append(out, a[o.AStart:o.AEnd]...)
default:
out = append(out, b[o.BStart:o.BEnd]...)
}
}
return out
}
func TestDiff_Cases(t *testing.T) {
tests := []struct {
name string
a, b string
want []reconcile.Op
}{
{"identical", "a b c", "a b c", []reconcile.Op{{Kind: reconcile.Equal, AStart: 0, AEnd: 3, BStart: 0, BEnd: 3}}},
{"both empty", "", "", nil},
{"one word replaced", "a b c", "a x c", []reconcile.Op{
{Kind: reconcile.Equal, AStart: 0, AEnd: 1, BStart: 0, BEnd: 1},
{Kind: reconcile.Replace, AStart: 1, AEnd: 2, BStart: 1, BEnd: 2},
{Kind: reconcile.Equal, AStart: 2, AEnd: 3, BStart: 2, BEnd: 3},
}},
{"word only in b", "a c", "a b c", []reconcile.Op{
{Kind: reconcile.Equal, AStart: 0, AEnd: 1, BStart: 0, BEnd: 1},
{Kind: reconcile.Insert, AStart: 1, AEnd: 1, BStart: 1, BEnd: 2},
{Kind: reconcile.Equal, AStart: 1, AEnd: 2, BStart: 2, BEnd: 3},
}},
{"word only in a", "a b c", "a c", []reconcile.Op{
{Kind: reconcile.Equal, AStart: 0, AEnd: 1, BStart: 0, BEnd: 1},
{Kind: reconcile.Delete, AStart: 1, AEnd: 2, BStart: 1, BEnd: 1},
{Kind: reconcile.Equal, AStart: 2, AEnd: 3, BStart: 1, BEnd: 2},
}},
{"nothing in common", "a b", "x y z", []reconcile.Op{
{Kind: reconcile.Replace, AStart: 0, AEnd: 2, BStart: 0, BEnd: 3},
}},
{"a is empty", "", "x y", []reconcile.Op{{Kind: reconcile.Insert, AStart: 0, AEnd: 0, BStart: 0, BEnd: 2}}},
{"b is empty", "x y", "", []reconcile.Op{{Kind: reconcile.Delete, AStart: 0, AEnd: 2, BStart: 0, BEnd: 0}}},
{"adjacent delete and insert read as one replace", "a b c d", "a x y d", []reconcile.Op{
{Kind: reconcile.Equal, AStart: 0, AEnd: 1, BStart: 0, BEnd: 1},
{Kind: reconcile.Replace, AStart: 1, AEnd: 3, BStart: 1, BEnd: 3},
{Kind: reconcile.Equal, AStart: 3, AEnd: 4, BStart: 3, BEnd: 4},
}},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert.Equal(t, tt.want, reconcile.Diff(words(tt.a), words(tt.b)))
})
}
}
// lcsLen is a plain dynamic-programming reference. Myers must find an edit script
// whose equal runs add up to exactly this: a diff that is merely valid but not
// minimal would turn real agreement into disputes the adjudicator is then asked about.
func lcsLen(a, b []string) int {
prev := make([]int, len(b)+1)
for i := 1; i <= len(a); i++ {
cur := make([]int, len(b)+1)
for j := 1; j <= len(b); j++ {
if a[i-1] == b[j-1] {
cur[j] = prev[j-1] + 1
} else {
cur[j] = max(prev[j], cur[j-1])
}
}
prev = cur
}
return prev[len(b)]
}
func TestDiff_RebuildsBAndKeepsEveryCommonWordOnRandomInputs(t *testing.T) {
rng := rand.New(rand.NewSource(7))
alphabet := []string{"a", "b", "c", "d", "e", "f"}
gen := func(n int) []string {
out := make([]string, n)
for i := range out {
out[i] = alphabet[rng.Intn(len(alphabet))]
}
return out
}
for i := 0; i < 400; i++ {
a, b := gen(rng.Intn(25)), gen(rng.Intn(25))
ops := reconcile.Diff(a, b)
require.Equal(t, append([]string{}, b...), append([]string{}, apply(a, b, ops)...), "a=%v b=%v ops=%v", a, b, ops)
common := 0
prevA, prevB := 0, 0
for _, o := range ops {
require.Equal(t, prevA, o.AStart, "ops must tile a without gaps: %v", ops)
require.Equal(t, prevB, o.BStart, "ops must tile b without gaps: %v", ops)
prevA, prevB = o.AEnd, o.BEnd
if o.Kind == reconcile.Equal {
common += o.AEnd - o.AStart
require.Equal(t, a[o.AStart:o.AEnd], b[o.BStart:o.BEnd], "an Equal op must really be equal")
}
}
require.Equal(t, len(a), prevA)
require.Equal(t, len(b), prevB)
require.Equal(t, lcsLen(a, b), common, "the diff must be minimal: a=%v b=%v", a, b)
}
}
func TestDiff_HandlesAnHoursWorthOfWordsFast(t *testing.T) {
rng := rand.New(rand.NewSource(3))
a := make([]string, 6500)
for i := range a {
a[i] = string(rune('a'+rng.Intn(26))) + string(rune('a'+rng.Intn(26)))
}
b := append([]string{}, a...)
for i := 0; i < 380; i++ { // ~380 disputes, as measured on a real hour
b[rng.Intn(len(b))] = "zz" + string(rune('a'+rng.Intn(26)))
}
ops := reconcile.Diff(a, b)
assert.NotEmpty(t, ops)
assert.Equal(t, append([]string{}, b...), append([]string{}, apply(a, b, ops)...))
}