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") }