From 9297f728c2c9eb5543868687e5c3a2bb4e52041f Mon Sep 17 00:00:00 2001 From: Mathias Date: Fri, 2 Oct 2026 08:55:43 +0200 Subject: [PATCH] test(reconcile): every judged spot with its left context; segments rewritten per segment, or left alone when they do not line up (red) Co-Authored-By: Claude Sonnet 5.5 --- internal/reconcile/reconcile_spots_test.go | 120 +++++++++++++++++++++ 1 file changed, 120 insertions(+) create mode 100644 internal/reconcile/reconcile_spots_test.go diff --git a/internal/reconcile/reconcile_spots_test.go b/internal/reconcile/reconcile_spots_test.go new file mode 100644 index 0000000..a3eba9b --- /dev/null +++ b/internal/reconcile/reconcile_spots_test.go @@ -0,0 +1,120 @@ +package reconcile_test + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "git.d-ma.be/mathias/tapir/internal/domain" + "git.d-ma.be/mathias/tapir/internal/reconcile" +) + +// Every judged spot is stored with a few words of left context, and the saved segments +// carry the merged words while keeping the base's timing (ADR-035 decisions 6 and 7). + +func TestReconcileSpots_AKeptSpotAndAChangedSpotAreBothStoredWithTheirVerdict(t *testing.T) { + adj := &scripted{fn: func(i int, _ reconcile.Request) (reconcile.Verdict, error) { + if i == 0 { + return reconcile.VerdictBase, nil + } + return reconcile.VerdictAlt, nil + }} + + out := merge(t, tr(progBase), tr(progAlt), adj) + + require.Len(t, out.Reconciliation.Contested, 2) + assert.Equal(t, "base", out.Reconciliation.Contested[0].Used) + assert.Equal(t, "1015", out.Reconciliation.Contested[0].Base) + assert.Equal(t, "alt", out.Reconciliation.Contested[1].Used) + assert.Equal(t, 1, out.Reconciliation.Changed, "the exact counts are unchanged by storing more") +} + +func TestReconcileSpots_AutoResolvedDisputesAreNotStored(t *testing.T) { + out := merge(t, tr("i crossborder delen ska vi ju betala"), tr("i crossborderdelen ska vi betala"), always(reconcile.VerdictAlt)) + + assert.Empty(t, out.Reconciliation.Contested, "compounds and fillers were never judged; they are counted, not listed") +} + +func TestReconcileSpots_ASpotCarriesTheWordsJustBeforeIt(t *testing.T) { + out := merge(t, tr("vi skickar in ett special meddelande till taxsystemet"), tr("vi skickar in ett CAMT meddelande till taxsystemet"), always(reconcile.VerdictAlt)) + + require.Len(t, out.Reconciliation.Contested, 1) + assert.Equal(t, "vi skickar in ett", out.Reconciliation.Contested[0].Before) +} + +func TestReconcileSpots_TheContextIsAFewWordsNotTheWholeMeeting(t *testing.T) { + long := "ett två tre fyra fem sex sju åtta nio tio elva tolv" + out := merge(t, tr(long+" X slut"), tr(long+" Y slut"), always(reconcile.VerdictAlt)) + + require.Len(t, out.Reconciliation.Contested, 1) + assert.Equal(t, "fem sex sju åtta nio tio elva tolv", out.Reconciliation.Contested[0].Before, + "eight words: enough to find the place, small enough to stay out of the way") +} + +func segmented(text string, segs ...domain.Segment) domain.Transcription { + t := tr(text) + t.Segments = segs + return t +} + +func TestReconcileSegments_AChangedWordReplacesTheBaseWordInItsOwnSegmentOnly(t *testing.T) { + base := segmented("ett två tre. fyra fem sex. sju åtta nio.", + domain.Segment{Start: 0, End: 10, Text: "ett två tre."}, + domain.Segment{Start: 10, End: 20, Text: "fyra fem sex."}, + domain.Segment{Start: 20, End: 30, Text: "sju åtta nio."}) + + out := merge(t, base, tr("ett två tre. fyra femm sex. sju åtta nio."), always(reconcile.VerdictAlt)) + + require.Len(t, out.Segments, 3) + assert.Equal(t, "ett två tre.", out.Segments[0].Text, "an untouched segment keeps its exact text") + assert.Equal(t, "fyra femm sex.", out.Segments[1].Text) + assert.Equal(t, "sju åtta nio.", out.Segments[2].Text) + assert.InDelta(t, 10.0, out.Segments[1].Start, 1e-9, "timing stays the base's") + assert.InDelta(t, 20.0, out.Segments[1].End, 1e-9) +} + +func TestReconcileSegments_AWordOnlyTheAlternativeHeardLandsInTheSegmentItBelongsTo(t *testing.T) { + base := segmented("a b c. d e f.", domain.Segment{Start: 0, End: 5, Text: "a b c."}, domain.Segment{Start: 5, End: 10, Text: "d e f."}) + + out := merge(t, base, tr("a b c. d NY e f."), always(reconcile.VerdictAlt)) + + assert.Equal(t, "a b c.", out.Segments[0].Text) + assert.Equal(t, "d NY e f.", out.Segments[1].Text) +} + +func TestReconcileSegments_AWordOnlyTheBaseHeardIsRemovedFromItsSegment(t *testing.T) { + base := segmented("a b c. d X e f.", domain.Segment{Start: 0, End: 5, Text: "a b c."}, domain.Segment{Start: 5, End: 10, Text: "d X e f."}) + + out := merge(t, base, tr("a b c. d e f."), always(reconcile.VerdictAlt)) + + assert.Equal(t, "d e f.", out.Segments[1].Text) +} + +func TestReconcileSegments_AKeptBaseLeavesEverySegmentAsItWas(t *testing.T) { + base := segmented("a x b", domain.Segment{Start: 0, End: 4, Text: "a x b"}) + + out := merge(t, base, tr("a y b"), always(reconcile.VerdictBase)) + + assert.Equal(t, base.Segments, out.Segments) +} + +func TestReconcileSegments_TheCallersSegmentsAreNeverChangedInPlace(t *testing.T) { + segs := []domain.Segment{{Start: 0, End: 4, Text: "a x b"}} + base := segmented("a x b", segs...) + + _ = merge(t, base, tr("a y b"), always(reconcile.VerdictAlt)) + + assert.Equal(t, "a x b", segs[0].Text, "Reconcile builds new segments; it does not edit the base transcription's") +} + +func TestReconcileSegments_SegmentsThatDoNotLineUpWithTheTextAreLeftAsTheBases(t *testing.T) { + // A word wrapped across a segment edge makes the segments' word count differ from + // the joined text's. Guessing would put words in the wrong place, so leave them. + base := segmented("a x b c", domain.Segment{Start: 0, End: 4, Text: "a x"}, domain.Segment{Start: 4, End: 8, Text: "b c d"}) + + out := merge(t, base, tr("a y b c"), always(reconcile.VerdictAlt)) + + assert.Equal(t, base.Segments, out.Segments) + assert.Equal(t, "a y b c", out.Text, "the merged text is unaffected") +}