Undo reported "nothing left to undo" after a successful undo (#72)
Build image / build-and-push (push) Successful in 19s
Build image / build-and-push (push) Successful in 19s
Co-authored-by: Steve Dudenhoeffer <[email protected]>
This commit was merged in pull request #72.
This commit is contained in:
@@ -704,3 +704,104 @@ func TestClearObjectOnlyClearsWhatItSnapshotted(t *testing.T) {
|
||||
t.Errorf("cleared %d of %d with %d revisions — all three should match", n, len(before), len(revs))
|
||||
}
|
||||
}
|
||||
|
||||
// TestRevertResultCarriesItsCounts — found by using the thing.
|
||||
//
|
||||
// commitScope returns the freshly inserted row, which carries no tally. A caller
|
||||
// receiving a change set with empty Counts cannot tell "nothing was reverted"
|
||||
// from "the tally wasn't loaded", and the chat panel read that ambiguity the
|
||||
// wrong way: a successful undo reported "nothing left to undo", which is the
|
||||
// worst possible thing to say about an action that just worked.
|
||||
//
|
||||
// It also pins the cross-layer contract that fix created. countRevisions tallies
|
||||
// in Go what ListChangeSets tallies in SQL, and nothing but this test stops the
|
||||
// two drifting — so it compares the FULL per-(entity, op) breakdown, not just
|
||||
// the totals, which would agree even if the groupings had diverged.
|
||||
func TestRevertResultCarriesItsCounts(t *testing.T) {
|
||||
s := newTestService(t, openConfig())
|
||||
owner := seedUser(t, s, "[email protected]")
|
||||
g := seedGarden(t, s, owner)
|
||||
bed := seedBed(t, s, owner, g.ID)
|
||||
plant := seedOwnPlant(t, s, owner, 15)
|
||||
ctx := context.Background()
|
||||
|
||||
if _, err := s.FillNamedRegion(ctx, owner, bed.ID, "all", plant.ID, nil); err != nil {
|
||||
t.Fatalf("fill: %v", err)
|
||||
}
|
||||
// A second, different kind of change, so the breakdown has more than one row
|
||||
// to get wrong.
|
||||
if _, err := s.UpdateObject(ctx, owner, bed.ID, ObjectPatch{Name: strPtr("North Bed")}, bed.Version); err != nil {
|
||||
t.Fatalf("rename: %v", err)
|
||||
}
|
||||
sets := history(t, s, owner, g.ID)
|
||||
rename, fill := sets[0], sets[1]
|
||||
|
||||
undoRename, conflicts, err := s.RevertChangeSet(ctx, owner, rename.ID, domain.SourceUI)
|
||||
if err != nil || len(conflicts) != 0 {
|
||||
t.Fatalf("revert rename: err=%v conflicts=%+v", err, conflicts)
|
||||
}
|
||||
undoFill, conflicts, err := s.RevertChangeSet(ctx, owner, fill.ID, domain.SourceUI)
|
||||
if err != nil || len(conflicts) != 0 {
|
||||
t.Fatalf("revert fill: err=%v conflicts=%+v", err, conflicts)
|
||||
}
|
||||
|
||||
for _, undo := range []*domain.ChangeSet{undoRename, undoFill} {
|
||||
if undo == nil {
|
||||
t.Fatal("a revert that did work returned no change set")
|
||||
}
|
||||
if len(undo.Counts) == 0 {
|
||||
t.Fatalf("change set %d came back with no tally — an empty tally reads as 'nothing happened'", undo.ID)
|
||||
}
|
||||
}
|
||||
// Undoing a fill deletes every plop it created, so the tallies must match.
|
||||
if got, want := countsTotal(undoFill.Counts), countsTotal(fill.Counts); got != want {
|
||||
t.Errorf("undo of the fill reports %d changes, want %d", got, want)
|
||||
}
|
||||
|
||||
// The SQL tally and the Go tally must agree, row for row.
|
||||
listed := history(t, s, owner, g.ID)
|
||||
byID := map[int64][]domain.ChangeCount{}
|
||||
for _, cs := range listed {
|
||||
byID[cs.ID] = cs.Counts
|
||||
}
|
||||
for _, undo := range []*domain.ChangeSet{undoRename, undoFill} {
|
||||
fromSQL, ok := byID[undo.ID]
|
||||
if !ok {
|
||||
t.Fatalf("revert %d is missing from the history list", undo.ID)
|
||||
}
|
||||
if !sameCounts(undo.Counts, fromSQL) {
|
||||
t.Errorf("change set %d: revert returned %+v, the list read says %+v — countRevisions and the SQL grouping have drifted",
|
||||
undo.ID, undo.Counts, fromSQL)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// sameCounts compares two tallies regardless of order.
|
||||
func sameCounts(a, b []domain.ChangeCount) bool {
|
||||
if len(a) != len(b) {
|
||||
return false
|
||||
}
|
||||
index := func(cs []domain.ChangeCount) map[string]int {
|
||||
m := map[string]int{}
|
||||
for _, c := range cs {
|
||||
m[c.EntityType+"/"+c.Op] = c.N
|
||||
}
|
||||
return m
|
||||
}
|
||||
ia, ib := index(a), index(b)
|
||||
for k, v := range ia {
|
||||
if ib[k] != v {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// countsTotal sums a tally — the server-side twin of the client's totalChanges.
|
||||
func countsTotal(counts []domain.ChangeCount) int {
|
||||
n := 0
|
||||
for _, c := range counts {
|
||||
n += c.N
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user