From 61aba0f7fbf70f7a3f2b27797d5b6f62c80b06c8 Mon Sep 17 00:00:00 2001 From: Andrew Lee Date: Sun, 14 Jun 2026 16:06:46 -0700 Subject: [PATCH] test: inject conflicts into the random invariant model runModel's cases never call conflictOn, so every random restack took the clean path -- ErrConflict/PendingReparent/Continue were unreachable in the random walk (only the fixed 2-branch fixtures exercised them), so the model never explored op-after-op interactions around a just-resolved rebase. Add an 11th carve-out (like the repair/prune ones, outside the undo oracle): with low probability pick a non-trunk-parented branch, conflictOn it, amend its parent to trigger the upstack conflict, assert ErrConflict + rebase-in-progress, Continue, then assert the restack is idempotent and invariants hold. The resolved state flows into the later walk for free. Stable across seeds (-count=6). --- internal/stack/model_test.go | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/internal/stack/model_test.go b/internal/stack/model_test.go index c1c0e48..a2c02ab 100644 --- a/internal/stack/model_test.go +++ b/internal/stack/model_test.go @@ -83,6 +83,42 @@ func runModel(t *testing.T, seed int64, steps int) { checkInvariants(t, f, s, step) continue } + // Occasionally inject a conflict and resolve it with Continue. The random + // walk otherwise only takes the clean restack path, so ErrConflict / + // PendingReparent / Continue are never reached; this makes "a conflict + // resolved at any random point still reconciles invariant-clean" a + // property. Runs outside the undo oracle (the mutation is half-applied). + if len(tracked) > 0 && rng.Intn(12) == 0 { + var cands []string + for _, n := range tracked { + if s.Branches[n].Parent != s.Trunk { // amend the parent -> upstack restack hits n + cands = append(cands, n) + } + } + if len(cands) > 0 { + victim := pick(rng, cands) + parent := s.Branches[victim].Parent + f.conflictOn(victim) + mustCheckout(t, f, parent) + if _, err := Modify(env, s, "", true, false); !errors.Is(err, ErrConflict) { + t.Fatalf("step %d: want ErrConflict injecting on %s, got %v", step, victim, err) + } + if inProgress, _ := f.RebaseInProgress(); !inProgress { + t.Fatalf("step %d: expected a rebase in progress after the injected conflict", step) + } + if _, err := Continue(env, s); err != nil { + t.Fatalf("step %d: continue after injected conflict: %v", step, err) + } + f.head = "main" + if res, err := Restack(env, s); err != nil { + t.Fatalf("step %d: restack after continue: %v", step, err) + } else if len(res.Restacked) != 0 { + t.Fatalf("step %d: restack after continue not idempotent, rebased %v", step, res.Restacked) + } + checkInvariants(t, f, s, step) + continue + } + } // Build the step as a re-runnable closure so it can be applied, undone, // and applied again. var label string