test(web): the page says what reconciliation did, plainly, and never claims more (red)
Reconciled: who the two engines and the deciding model were, and the counts (differences, settled automatically, decided by the model, uncertain). The contested spots are listed with a minute mark, both readings and which was used, uncertain ones marked, readings escaped, and a very long list shortened with its true count. A single-engine transcript makes no claim of a second opinion; one-engine and model-unavailable cases say why. Red: the page renders none of this yet. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -3,6 +3,7 @@ package web_test
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"mime/multipart"
|
"mime/multipart"
|
||||||
"net/http"
|
"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, "Secret project notes", "the title is private content")
|
||||||
assert.NotContains(t, logged, "client-acme-roadmap", "the filename 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":"<script>alert(1)</script>","alt":"ok","used":"base","unsure":true}]}`)
|
||||||
|
|
||||||
|
b := body(t, do(t, app, get("/uploads/"+id+"/status")))
|
||||||
|
|
||||||
|
assert.NotContains(t, b, "<script>alert(1)</script>", "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")
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user