diff --git a/internal/web/uploads_handler_test.go b/internal/web/uploads_handler_test.go
index a94400a..158eafd 100644
--- a/internal/web/uploads_handler_test.go
+++ b/internal/web/uploads_handler_test.go
@@ -3,6 +3,7 @@ package web_test
import (
"bytes"
"context"
+ "fmt"
"log/slog"
"mime/multipart"
"net/http"
@@ -355,3 +356,107 @@ func TestUploadPost_ARejectionIsLoggedWithItsReasonAndNeverTheFilenameOrTitle(t
assert.NotContains(t, logged, "Secret project notes", "the title is private content")
assert.NotContains(t, logged, "client-acme-roadmap", "the filename is private content")
}
+
+func setReconciliation(t *testing.T, id, json string) {
+ t.Helper()
+ _, err := rawPool(t).Exec(context.Background(), `UPDATE uploaded_transcripts SET reconciliation = $2::jsonb WHERE id = $1`, id, json)
+ require.NoError(t, err)
+}
+
+const reconciledJSON = `{"status":"reconciled","base":"iguana/pianissimo","alt":"iguana/kb-whisper","reconciler":"iguana/gemma4-26b",
+ "disputes":380,"auto_resolved":90,"adjudicated":285,"changed":120,"unsure":5,"errors":0,
+ "contested":[
+ {"seconds":2210.5,"base":"SEB","alt":"SCB","used":"alt","unsure":false},
+ {"seconds":3400,"base":"T+1","alt":"en T","used":"base","unsure":true}]}`
+
+func TestUploadStatus_ReconciledSaysWhatWasDoneInPlainWords(t *testing.T) {
+ app, _ := newUploadApp(t, audioOK, 200<<20)
+ id := seedUpload(t, userID, "m", "done", "Synthetic transcript.")
+ setReconciliation(t, id, reconciledJSON)
+
+ b := body(t, do(t, app, get("/uploads/"+id+"/status")))
+
+ assert.Contains(t, b, "Reconciled from two transcripts")
+ assert.Contains(t, b, "iguana/pianissimo")
+ assert.Contains(t, b, "iguana/kb-whisper")
+ assert.Contains(t, b, "iguana/gemma4-26b", "who decided is part of the account")
+ assert.Contains(t, b, "380 differences")
+ assert.Contains(t, b, "90 settled automatically")
+ assert.Contains(t, b, "285 decided by the model")
+ assert.Contains(t, b, "5 uncertain")
+}
+
+func TestUploadStatus_ListsTheContestedSpotsWithAMinuteMarkBothReadingsAndWhichWasUsed(t *testing.T) {
+ app, _ := newUploadApp(t, audioOK, 200<<20)
+ id := seedUpload(t, userID, "m", "done", "Synthetic transcript.")
+ setReconciliation(t, id, reconciledJSON)
+
+ b := body(t, do(t, app, get("/uploads/"+id+"/status")))
+
+ assert.Contains(t, b, "~37 min", "2210.5 s")
+ assert.Contains(t, b, "SEB")
+ assert.Contains(t, b, "SCB")
+ assert.Contains(t, b, "~57 min", "3400 s")
+ assert.Contains(t, b, "T+1")
+ assert.Contains(t, b, "en T")
+ assert.Contains(t, b, "uncertain", "an unsure spot is marked so the reader knows to check it")
+}
+
+func TestUploadStatus_ASingleEngineTranscriptDoesNotClaimASecondOpinion(t *testing.T) {
+ app, _ := newUploadApp(t, audioOK, 200<<20)
+ id := seedUpload(t, userID, "m", "done", "Synthetic transcript.")
+
+ assert.NotContains(t, body(t, do(t, app, get("/uploads/"+id+"/status"))), "Reconciled", "no reconciliation, no claim")
+}
+
+func TestUploadStatus_WhenOnlyOneEngineWorkedItSaysWhyTheSecondOpinionIsMissing(t *testing.T) {
+ app, _ := newUploadApp(t, audioOK, 200<<20)
+ id := seedUpload(t, userID, "m", "done", "Synthetic transcript.")
+ setReconciliation(t, id, `{"status":"single_engine","reason":"iguana/pianissimo failed: wedged","base":"iguana/pianissimo","alt":"iguana/kb-whisper","reconciler":"iguana/gemma4-26b"}`)
+
+ b := body(t, do(t, app, get("/uploads/"+id+"/status")))
+
+ assert.Contains(t, b, "Only one engine")
+ assert.Contains(t, b, "iguana/pianissimo failed")
+ assert.NotContains(t, b, "Reconciled from two")
+}
+
+func TestUploadStatus_WhenTheModelWasUnavailableItSaysTheTextIsUnreconciled(t *testing.T) {
+ app, _ := newUploadApp(t, audioOK, 200<<20)
+ id := seedUpload(t, userID, "m", "done", "Synthetic transcript.")
+ setReconciliation(t, id, `{"status":"skipped","reason":"the model failed 3 times in a row: gateway down","base":"iguana/pianissimo","alt":"iguana/kb-whisper","reconciler":"iguana/gemma4-26b","disputes":380,"errors":3}`)
+
+ b := body(t, do(t, app, get("/uploads/"+id+"/status")))
+
+ assert.Contains(t, b, "could not be reconciled")
+ assert.Contains(t, b, "iguana/pianissimo", "which engine's text is being shown")
+ assert.Contains(t, b, "gateway down")
+}
+
+func TestUploadStatus_ContestedReadingsAreEscaped(t *testing.T) {
+ app, _ := newUploadApp(t, audioOK, 200<<20)
+ id := seedUpload(t, userID, "m", "done", "Synthetic transcript.")
+ setReconciliation(t, id, `{"status":"reconciled","base":"a","alt":"b","reconciler":"c","disputes":1,"unsure":1,
+ "contested":[{"seconds":60,"base":"","alt":"ok","used":"base","unsure":true}]}`)
+
+ b := body(t, do(t, app, get("/uploads/"+id+"/status")))
+
+ assert.NotContains(t, b, "", "a reading is words from a recording, never markup")
+ assert.Contains(t, b, "<script>")
+}
+
+func TestUploadStatus_AVeryLongListOfContestedSpotsIsShortenedWithItsTrueCount(t *testing.T) {
+ app, _ := newUploadApp(t, audioOK, 200<<20)
+ id := seedUpload(t, userID, "m", "done", "Synthetic transcript.")
+ var spots []string
+ for i := 0; i < 120; i++ {
+ spots = append(spots, fmt.Sprintf(`{"seconds":%d,"base":"b%d","alt":"a%d","used":"base","unsure":true}`, i*10, i, i))
+ }
+ setReconciliation(t, id, `{"status":"reconciled","base":"a","alt":"b","reconciler":"c","disputes":400,"unsure":120,"contested":[`+strings.Join(spots, ",")+`]}`)
+
+ b := body(t, do(t, app, get("/uploads/"+id+"/status")))
+
+ assert.Contains(t, b, "b0", "the first spots are shown")
+ assert.NotContains(t, b, ">b119<", "the page does not render every one")
+ assert.Contains(t, b, "70 more", "120 stored, 50 shown")
+}