From 520ab666f8ca02cfbf9933ee5e68e1751159c865 Mon Sep 17 00:00:00 2001 From: Mathias Date: Fri, 2 Oct 2026 08:55:43 +0200 Subject: [PATCH] test(reconcile): the comparison and the deciding phase report real counts, and a shutdown is neither done nor failed (red) Co-Authored-By: Claude Sonnet 5.5 --- internal/reconcile/reconcile_progress_test.go | 116 ++++++++++++++++++ 1 file changed, 116 insertions(+) create mode 100644 internal/reconcile/reconcile_progress_test.go diff --git a/internal/reconcile/reconcile_progress_test.go b/internal/reconcile/reconcile_progress_test.go new file mode 100644 index 0000000..fdf8271 --- /dev/null +++ b/internal/reconcile/reconcile_progress_test.go @@ -0,0 +1,116 @@ +package reconcile_test + +import ( + "context" + "errors" + "fmt" + "strings" + "sync" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "git.d-ma.be/mathias/tapir/internal/progress" + "git.d-ma.be/mathias/tapir/internal/reconcile" +) + +// What Reconcile reports while it works (ADR-035): the comparison, then the deciding +// phase with the real number of model decisions as its total. + +type repRec struct { + mu sync.Mutex + events []string +} + +func (r *repRec) add(e string) { + r.mu.Lock() + defer r.mu.Unlock() + r.events = append(r.events, e) +} + +func (r *repRec) Begin(key string, total int) { r.add(fmt.Sprintf("begin:%s:%d", key, total)) } +func (r *repRec) Advance(key string, done int) { r.add(fmt.Sprintf("advance:%s:%d", key, done)) } +func (r *repRec) Done(key string) { r.add("done:" + key) } +func (r *repRec) Fail(key string, _ error) { r.add("fail:" + key) } +func (r *repRec) Note(string, ...any) {} +func (r *repRec) joined() string { + r.mu.Lock() + defer r.mu.Unlock() + return strings.Join(r.events, ",") +} +func withRep(r *repRec) context.Context { return progress.With(context.Background(), r) } +func reconcileWith(t *testing.T, ctx context.Context, base, alt string, adj reconcile.Adjudicator) error { + t.Helper() + _, err := reconcile.Reconcile(ctx, tr(base), tr(alt), adj, opts) + return err +} + +// two compound/filler disputes the model is never asked about, then two it is. +const ( + progBase = "i crossborder delen ska vi ju betala totalt 1015 kronor till SEB idag" + progAlt = "i crossborderdelen ska vi betala totalt 1050 kronor till SCB idag" +) + +func TestReconcileProgress_TheDecidingPhaseTotalIsTheModelDecisionsNotTheDisputes(t *testing.T) { + rep := &repRec{} + + require.NoError(t, reconcileWith(t, withRep(rep), progBase, progAlt, always(reconcile.VerdictAlt))) + + assert.Equal(t, + "begin:compare:0,done:compare,begin:decide:2,advance:decide:1,advance:decide:2,done:decide", + rep.joined(), + "4 disputes, 2 settled without the model: the bar counts the 2 the model decides, so it can reach its end") +} + +func TestReconcileProgress_TextsThatAgreeStillClosePhasesSoNothingIsLeftRunning(t *testing.T) { + rep := &repRec{} + + require.NoError(t, reconcileWith(t, withRep(rep), "samma text hela vägen", "samma text hela vägen", always(reconcile.VerdictAlt))) + + assert.Equal(t, "begin:compare:0,done:compare,begin:decide:0,done:decide", rep.joined()) +} + +func TestReconcileProgress_AModelErrorStillCountsAsADecisionTaken(t *testing.T) { + rep := &repRec{} + adj := &scripted{fn: func(i int, _ reconcile.Request) (reconcile.Verdict, error) { + if i == 0 { + return reconcile.VerdictBase, errors.New("one timeout") + } + return reconcile.VerdictAlt, nil + }} + + require.NoError(t, reconcileWith(t, withRep(rep), progBase, progAlt, adj)) + + assert.Contains(t, rep.joined(), "advance:decide:1,advance:decide:2", "a spot that errored was still handled; the bar must move past it") +} + +func TestReconcileProgress_TheBreakerTrippingShowsTheDecidingPhaseFailedNotDone(t *testing.T) { + rep := &repRec{} + adj := &scripted{fn: func(int, reconcile.Request) (reconcile.Verdict, error) { + return reconcile.VerdictBase, errors.New("model down") + }} + small := opts + small.BreakAfter = 2 + + _, err := reconcile.Reconcile(withRep(rep), tr(progBase), tr(progAlt), adj, small) + + require.NoError(t, err, "a model that is down degrades the result; it does not fail the job") + assert.Contains(t, rep.joined(), "fail:decide") + assert.NotContains(t, rep.joined(), "done:decide") +} + +func TestReconcileProgress_AShutdownIsNeitherDoneNorFailed(t *testing.T) { + rep := &repRec{} + ctx, cancel := context.WithCancel(withRep(rep)) + adj := &scripted{fn: func(int, reconcile.Request) (reconcile.Verdict, error) { + cancel() + return reconcile.VerdictAlt, nil + }} + + err := reconcileWith(t, ctx, progBase, progAlt, adj) + + require.ErrorIs(t, err, context.Canceled) + assert.NotContains(t, rep.joined(), "fail:decide") + assert.NotContains(t, rep.joined(), "done:decide") +}