| index 8547c58..be2164e 100644 |
| --- a/internal/docs/snapshots_test.go |
| +++ b/internal/docs/snapshots_test.go |
| @@ -667,12 +667,24 @@ func seedLongLog(t *testing.T, app *App, doc *Document, total int, distinct map[ |
| const pastTheCapMarker = "BBB" |
| |
| // longLogDistinctRows is the shape every test below seeds: a log past |
| -// core.MaxRetainedUpdates whose one distinct update sits PAST the cap, |
| -// among rows only a replay that carries on beyond core.ErrUpdateLogFull |
| -// ever applies. That placement is what makes "still reads" mean "reads |
| -// the whole document" rather than "reads its first 4096 rows" — a |
| -// replay that stopped at the cap and returned what it had would render |
| -// without pastTheCapMarker and fail. |
| +// core.MaxRetainedUpdates whose one distinct update sits at seq |
| +// core.MaxRetainedUpdates+1 — the row a replay's first handle REFUSES |
| +// with core.ErrUpdateLogFull, having already retained the 4096 before |
| +// it. That seq, and not a later one, is the whole point of the |
| +// placement, because it pins two properties with one assertion: |
| +// |
| +// - the replay carried on past the cap at all, rather than stopping |
| +// there and returning what it had; and |
| +// - the refused row was re-applied into the re-seeded handle rather |
| +// than dropped. |
| +// |
| +// The second needs this exact seq. Every other row here is validUpdate's |
| +// bytes, and Yjs applies a duplicate update idempotently, so dropping |
| +// any ordinary row changes nothing anyone can observe: a re-seed that |
| +// discarded the refused row and continued would render identically, and |
| +// a fixture parked at, say, +40 would let that variant pass. With the |
| +// fixture ON the refused row, dropping it loses content nothing else in |
| +// the log holds, and every assertion below notices. |
| // |
| // One distinct fixture, not several. The fragment fixtures are each |
| // generated from their own yjs document, and their struct ids overlap, |
| @@ -682,14 +694,14 @@ const pastTheCapMarker = "BBB" |
| func longLogDistinctRows(t *testing.T) (total int, distinct map[int][]byte) { |
| t.Helper() |
| return core.MaxRetainedUpdates + 50, map[int][]byte{ |
| - core.MaxRetainedUpdates + 40: ReadFragmentFixture(t, "concurrent"), |
| + core.MaxRetainedUpdates + 1: ReadFragmentFixture(t, "concurrent"), |
| } |
| } |
| |
| // assertHoldsTheWholeLog fails unless html holds both ends of a log |
| // seeded by seedLongLog with longLogDistinctRows' placement: the |
| -// ordinary rows' own paragraphs, and pastTheCapMarker from the row |
| -// beyond core.MaxRetainedUpdates. |
| +// ordinary rows' own paragraphs, and pastTheCapMarker from the row the |
| +// first handle refused with core.ErrUpdateLogFull. |
| // |
| // It first renders the ordinary update on its own and requires that |
| // NOT to contain the marker. Without that, a fixture whose content the |
| @@ -715,7 +727,7 @@ func assertHoldsTheWholeLog(t *testing.T, app *App, doc *Document, html string) |
| // its only snapshot goes away — a corrupt row, or the first ABI bump, |
| // which newestSnapshot's exact-match filter invalidates every snapshot |
| // with at once — replay runs the whole log into one handle, meets |
| -// ErrUpdateLogFull at row 4096 and returns an error. Text, HTML and |
| +// ErrUpdateLogFull at row 4097 and returns an error. Text, HTML and |
| // JSON all go through replay, so such a document is not slow, it is |
| // unreadable. |
| func TestALongLogWithNoUsableSnapshotStillReads(t *testing.T) { |
| @@ -776,6 +788,17 @@ func TestALongLogWithNoUsableSnapshotStillAcceptsWrites(t *testing.T) { |
| if want := int64(total + 1); seq != want { |
| t.Errorf("ApplyUpdate wrote seq %d, want %d", seq, want) |
| } |
| + |
| + // Accepting a write is not enough on its own: a write that went |
| + // through on top of a replay that had quietly lost the row the cap |
| + // refused would persist that loss into the snapshot ApplyUpdate |
| + // takes above. So read the document back and require the whole log |
| + // to still be in it. |
| + html, err := app.Store().HTML(ctx, doc) |
| + if err != nil { |
| + t.Fatalf("HTML after the write: %v", err) |
| + } |
| + assertHoldsTheWholeLog(t, app, doc, string(html)) |
| } |
| |
| // The ABI-bump case, spelled out because it is the one that will |