detached a7d13d1

fix(vt): handle history append failures

commit
a7d13d1b93e4f487359380886a55c3e96b660325
author
brice · · 2026-07-21 19:05 UTC
parent
4dede25
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
17 files · +119 -53
M internal/usecase/copy/mode.go +22 -2
@@ -2,6 +2,7 @@ package copy
22
33 import (
44 "encoding/base64"
5+ "math"
56 "slices"
67 "strings"
78 "unicode"
@@ -27,12 +28,31 @@ func NewSnapshot(historySource *vt.History, screen renderer.Frame) Snapshot {
2728 return Snapshot{history: history, screen: screen.Clone(), Width: screen.Width, Height: screen.Height}
2829 }
2930 func NewSnapshotFromRows(rows [][]renderer.Cell, width, height int) Snapshot {
30- history := vt.NewHistory(vt.HistoryConfig{MaxRows: len(rows), ChunkRows: 256})
31+ history := vt.NewHistory(vt.HistoryConfig{
32+ MaxRows: len(rows),
33+ MaxCells: snapshotRowsCellBudget(rows),
34+ ChunkRows: 256,
35+ })
3136 for _, row := range rows {
32- history.Append(row)
37+ if err := history.Append(row); err != nil {
38+ panic("copy snapshot: configured history rejected supplied row")
39+ }
3340 }
3441 return Snapshot{history: history.SealAndView(), Width: width, Height: height}
3542 }
43+
44+// snapshotRowsCellBudget returns a capacity that retains every supplied row.
45+// Saturation is safe because a larger representable capacity does not exist.
46+func snapshotRowsCellBudget(rows [][]renderer.Cell) int {
47+ cells := 0
48+ for _, row := range rows {
49+ if len(row) > math.MaxInt-cells {
50+ return math.MaxInt
51+ }
52+ cells += len(row)
53+ }
54+ return cells
55+}
3656 func (s Snapshot) Len() int { return s.history.Len() + s.screen.Height }
3757 func (s Snapshot) Row(i int) []renderer.Cell {
3858 if i < 0 {
@@ -2,6 +2,7 @@ package copy
2 2
3 import (3 import (
4 "encoding/base64"4 "encoding/base64"
5+ "math"
5 "slices"6 "slices"
6 "strings"7 "strings"
7 "unicode"8 "unicode"
@@ -27,12 +28,31 @@ func NewSnapshot(historySource *vt.History, screen renderer.Frame) Snapshot {
27 return Snapshot{history: history, screen: screen.Clone(), Width: screen.Width, Height: screen.Height}28 return Snapshot{history: history, screen: screen.Clone(), Width: screen.Width, Height: screen.Height}
28 }29 }
29 func NewSnapshotFromRows(rows [][]renderer.Cell, width, height int) Snapshot {30 func NewSnapshotFromRows(rows [][]renderer.Cell, width, height int) Snapshot {
30- history := vt.NewHistory(vt.HistoryConfig{MaxRows: len(rows), ChunkRows: 256})31+ history := vt.NewHistory(vt.HistoryConfig{
32+ MaxRows: len(rows),
33+ MaxCells: snapshotRowsCellBudget(rows),
34+ ChunkRows: 256,
35+ })
31 for _, row := range rows {36 for _, row := range rows {
32- history.Append(row)37+ if err := history.Append(row); err != nil {
38+ panic("copy snapshot: configured history rejected supplied row")
39+ }
33 }40 }
34 return Snapshot{history: history.SealAndView(), Width: width, Height: height}41 return Snapshot{history: history.SealAndView(), Width: width, Height: height}
35 }42 }
43+
44+// snapshotRowsCellBudget returns a capacity that retains every supplied row.
45+// Saturation is safe because a larger representable capacity does not exist.
46+func snapshotRowsCellBudget(rows [][]renderer.Cell) int {
47+ cells := 0
48+ for _, row := range rows {
49+ if len(row) > math.MaxInt-cells {
50+ return math.MaxInt
51+ }
52+ cells += len(row)
53+ }
54+ return cells
55+}
36 func (s Snapshot) Len() int { return s.history.Len() + s.screen.Height }56 func (s Snapshot) Len() int { return s.history.Len() + s.screen.Height }
37 func (s Snapshot) Row(i int) []renderer.Cell {57 func (s Snapshot) Row(i int) []renderer.Cell {
38 if i < 0 {58 if i < 0 {
M internal/usecase/copy/mode_test.go +17 -2
@@ -27,6 +27,21 @@ func modeFor(lines []string, height int) *Mode {
2727 return NewMode(NewDocument(NewSnapshotFromRows(rows, 16, height), domain.DefaultWordSeparators))
2828 }
2929
30+func TestNewSnapshotFromRowsPreservesWideRows(t *testing.T) {
31+ wide := make([]renderer.Cell, 321)
32+ for i := range wide {
33+ wide[i] = renderer.Cell{Rune: rune('a' + i%26)}
34+ }
35+ rows := [][]renderer.Cell{wide, row("tail")}
36+
37+ snapshot := NewSnapshotFromRows(rows, len(wide), len(rows))
38+
39+ require.Equal(t, len(rows), snapshot.history.Len())
40+ for i, want := range rows {
41+ require.Equal(t, want, snapshot.history.Row(i))
42+ }
43+}
44+
3045 func TestCopyModeKeyboardSelection(t *testing.T) {
3146 m := modeFor([]string{"alpha", "xy", "bravo", "charlie"}, 2)
3247 require.Equal(t, Pos{Row: 3}, m.Cursor())
@@ -175,9 +190,9 @@ func TestFindMatchesUsesSealedScrollbackWithoutGlobalCopy(t *testing.T) {
175190 const rows = 10_000
176191 history := vt.NewHistory(vt.HistoryConfig{MaxRows: rows + 1, ChunkRows: 256})
177192 for range rows {
178- history.Append(row("unmatched"))
193+ require.NoError(t, history.Append(row("unmatched")))
179194 }
180- history.Append(row("target"))
195+ require.NoError(t, history.Append(row("target")))
181196 snapshot := NewSnapshot(history, renderer.NewFrame(16, 1))
182197 view := history.SealAndView()
183198 require.Same(t, view.Chunk(0), snapshot.history.Chunk(0))
@@ -27,6 +27,21 @@ func modeFor(lines []string, height int) *Mode {
27 return NewMode(NewDocument(NewSnapshotFromRows(rows, 16, height), domain.DefaultWordSeparators))27 return NewMode(NewDocument(NewSnapshotFromRows(rows, 16, height), domain.DefaultWordSeparators))
28 }28 }
29 29
30+func TestNewSnapshotFromRowsPreservesWideRows(t *testing.T) {
31+ wide := make([]renderer.Cell, 321)
32+ for i := range wide {
33+ wide[i] = renderer.Cell{Rune: rune('a' + i%26)}
34+ }
35+ rows := [][]renderer.Cell{wide, row("tail")}
36+
37+ snapshot := NewSnapshotFromRows(rows, len(wide), len(rows))
38+
39+ require.Equal(t, len(rows), snapshot.history.Len())
40+ for i, want := range rows {
41+ require.Equal(t, want, snapshot.history.Row(i))
42+ }
43+}
44+
30 func TestCopyModeKeyboardSelection(t *testing.T) {45 func TestCopyModeKeyboardSelection(t *testing.T) {
31 m := modeFor([]string{"alpha", "xy", "bravo", "charlie"}, 2)46 m := modeFor([]string{"alpha", "xy", "bravo", "charlie"}, 2)
32 require.Equal(t, Pos{Row: 3}, m.Cursor())47 require.Equal(t, Pos{Row: 3}, m.Cursor())
@@ -175,9 +190,9 @@ func TestFindMatchesUsesSealedScrollbackWithoutGlobalCopy(t *testing.T) {
175 const rows = 10_000190 const rows = 10_000
176 history := vt.NewHistory(vt.HistoryConfig{MaxRows: rows + 1, ChunkRows: 256})191 history := vt.NewHistory(vt.HistoryConfig{MaxRows: rows + 1, ChunkRows: 256})
177 for range rows {192 for range rows {
178- history.Append(row("unmatched"))193+ require.NoError(t, history.Append(row("unmatched")))
179 }194 }
180- history.Append(row("target"))195+ require.NoError(t, history.Append(row("target")))
181 snapshot := NewSnapshot(history, renderer.NewFrame(16, 1))196 snapshot := NewSnapshot(history, renderer.NewFrame(16, 1))
182 view := history.SealAndView()197 view := history.SealAndView()
183 require.Same(t, view.Chunk(0), snapshot.history.Chunk(0))198 require.Same(t, view.Chunk(0), snapshot.history.Chunk(0))
M internal/usecase/daemon/copymode_test.go +9 -9
@@ -301,7 +301,7 @@ func TestCopyModeSearchModalCapturesMouseAndClearsOnExit(t *testing.T) {
301301 d, sess, ac, sends := newManualSessionWithPTYs(t, p)
302302 pane := sess.tabs[0].focusedPane()
303303 installTestHistory(pane, vt.HistoryConfig{MaxRows: 4})
304- pane.history.Append(testRow("old alpha"))
304+ require.NoError(t, pane.history.Append(testRow("old alpha")))
305305 copy(pane.screen.Frame.Row(0), testRow("live alpha"))
306306
307307 d.enterCopyMode(sess, ac)
@@ -330,8 +330,8 @@ func TestCopyModeInputNotForwardedAndOSC52Copy(t *testing.T) {
330330 p, _ := newBlockingPTYWithWrites(t, writes)
331331 d, sess, ac, sends := newManualSessionWithPTYs(t, p)
332332 installTestHistory(sess.tabs[0].focusedPane(), vt.HistoryConfig{MaxRows: 4})
333- sess.tabs[0].focusedPane().history.Append(testRow("old1 "))
334- sess.tabs[0].focusedPane().history.Append(testRow("old2 "))
333+ require.NoError(t, sess.tabs[0].focusedPane().history.Append(testRow("old1 ")))
334+ require.NoError(t, sess.tabs[0].focusedPane().history.Append(testRow("old2 ")))
335335 sess.tabs[0].focusedPane().screen.Write([]byte("live"))
336336
337337 d.enterCopyMode(sess, ac)
@@ -505,8 +505,8 @@ func TestCopyModeSplitArrowDoesNotExit(t *testing.T) {
505505 p, _ := newBlockingPTY(t)
506506 d, sess, ac, sends := newManualSessionWithPTYs(t, p)
507507 installTestHistory(sess.tabs[0].focusedPane(), vt.HistoryConfig{MaxRows: 4})
508- sess.tabs[0].focusedPane().history.Append(testRow("old1 "))
509- sess.tabs[0].focusedPane().history.Append(testRow("old2 "))
508+ require.NoError(t, sess.tabs[0].focusedPane().history.Append(testRow("old1 ")))
509+ require.NoError(t, sess.tabs[0].focusedPane().history.Append(testRow("old2 ")))
510510 sess.tabs[0].focusedPane().screen.Write([]byte("live"))
511511
512512 d.enterCopyMode(sess, ac)
@@ -739,11 +739,11 @@ func TestCopyModeCapturesSourceAndRetainsItAcrossFocusMove(t *testing.T) {
739739 var want *pane
740740 if tc.useFloating {
741741 want = installFloatingCopyFixture(t, sess, domain.Size{Cols: 20, Rows: 3})
742- want.history.Append(testRow("flt-old"))
742+ require.NoError(t, want.history.Append(testRow("flt-old")))
743743 want.screen.Write([]byte("flt-live"))
744744 } else {
745745 want = main
746- want.history.Append(testRow("one-old"))
746+ require.NoError(t, want.history.Append(testRow("one-old")))
747747 want.screen.Write([]byte("one-live"))
748748 }
749749
@@ -780,7 +780,7 @@ func TestFloatingCopyModeWheelUsesCapturedSnapshot(t *testing.T) {
780780 d, sess, ac, sends := newManualSessionWithPTYs(t, normal)
781781 fp := installFloatingCopyFixture(t, sess, domain.Size{Cols: 20, Rows: 3})
782782 for i := range 30 {
783- fp.history.Append(testRow(fmt.Sprintf("old-%02d", i)))
783+ require.NoError(t, fp.history.Append(testRow(fmt.Sprintf("old-%02d", i))))
784784 }
785785 fp.screen.Write([]byte("live"))
786786 fp.mu.Lock()
@@ -806,7 +806,7 @@ func TestFloatingCopyModeMouseSelectsFloatingRows(t *testing.T) {
806806 d, sess, ac, sends := newManualSessionWithPTYs(t, normal)
807807 fp := installFloatingCopyFixture(t, sess, domain.Size{Cols: 20, Rows: 3})
808808 for i := range 5 {
809- fp.history.Append(testRow(fmt.Sprintf("old-%d", i)))
809+ require.NoError(t, fp.history.Append(testRow(fmt.Sprintf("old-%d", i))))
810810 }
811811 fp.screen.Write([]byte("live"))
812812
@@ -301,7 +301,7 @@ func TestCopyModeSearchModalCapturesMouseAndClearsOnExit(t *testing.T) {
301 d, sess, ac, sends := newManualSessionWithPTYs(t, p)301 d, sess, ac, sends := newManualSessionWithPTYs(t, p)
302 pane := sess.tabs[0].focusedPane()302 pane := sess.tabs[0].focusedPane()
303 installTestHistory(pane, vt.HistoryConfig{MaxRows: 4})303 installTestHistory(pane, vt.HistoryConfig{MaxRows: 4})
304- pane.history.Append(testRow("old alpha"))304+ require.NoError(t, pane.history.Append(testRow("old alpha")))
305 copy(pane.screen.Frame.Row(0), testRow("live alpha"))305 copy(pane.screen.Frame.Row(0), testRow("live alpha"))
306 306
307 d.enterCopyMode(sess, ac)307 d.enterCopyMode(sess, ac)
@@ -330,8 +330,8 @@ func TestCopyModeInputNotForwardedAndOSC52Copy(t *testing.T) {
330 p, _ := newBlockingPTYWithWrites(t, writes)330 p, _ := newBlockingPTYWithWrites(t, writes)
331 d, sess, ac, sends := newManualSessionWithPTYs(t, p)331 d, sess, ac, sends := newManualSessionWithPTYs(t, p)
332 installTestHistory(sess.tabs[0].focusedPane(), vt.HistoryConfig{MaxRows: 4})332 installTestHistory(sess.tabs[0].focusedPane(), vt.HistoryConfig{MaxRows: 4})
333- sess.tabs[0].focusedPane().history.Append(testRow("old1 "))333+ require.NoError(t, sess.tabs[0].focusedPane().history.Append(testRow("old1 ")))
334- sess.tabs[0].focusedPane().history.Append(testRow("old2 "))334+ require.NoError(t, sess.tabs[0].focusedPane().history.Append(testRow("old2 ")))
335 sess.tabs[0].focusedPane().screen.Write([]byte("live"))335 sess.tabs[0].focusedPane().screen.Write([]byte("live"))
336 336
337 d.enterCopyMode(sess, ac)337 d.enterCopyMode(sess, ac)
@@ -505,8 +505,8 @@ func TestCopyModeSplitArrowDoesNotExit(t *testing.T) {
505 p, _ := newBlockingPTY(t)505 p, _ := newBlockingPTY(t)
506 d, sess, ac, sends := newManualSessionWithPTYs(t, p)506 d, sess, ac, sends := newManualSessionWithPTYs(t, p)
507 installTestHistory(sess.tabs[0].focusedPane(), vt.HistoryConfig{MaxRows: 4})507 installTestHistory(sess.tabs[0].focusedPane(), vt.HistoryConfig{MaxRows: 4})
508- sess.tabs[0].focusedPane().history.Append(testRow("old1 "))508+ require.NoError(t, sess.tabs[0].focusedPane().history.Append(testRow("old1 ")))
509- sess.tabs[0].focusedPane().history.Append(testRow("old2 "))509+ require.NoError(t, sess.tabs[0].focusedPane().history.Append(testRow("old2 ")))
510 sess.tabs[0].focusedPane().screen.Write([]byte("live"))510 sess.tabs[0].focusedPane().screen.Write([]byte("live"))
511 511
512 d.enterCopyMode(sess, ac)512 d.enterCopyMode(sess, ac)
@@ -739,11 +739,11 @@ func TestCopyModeCapturesSourceAndRetainsItAcrossFocusMove(t *testing.T) {
739 var want *pane739 var want *pane
740 if tc.useFloating {740 if tc.useFloating {
741 want = installFloatingCopyFixture(t, sess, domain.Size{Cols: 20, Rows: 3})741 want = installFloatingCopyFixture(t, sess, domain.Size{Cols: 20, Rows: 3})
742- want.history.Append(testRow("flt-old"))742+ require.NoError(t, want.history.Append(testRow("flt-old")))
743 want.screen.Write([]byte("flt-live"))743 want.screen.Write([]byte("flt-live"))
744 } else {744 } else {
745 want = main745 want = main
746- want.history.Append(testRow("one-old"))746+ require.NoError(t, want.history.Append(testRow("one-old")))
747 want.screen.Write([]byte("one-live"))747 want.screen.Write([]byte("one-live"))
748 }748 }
749 749
@@ -780,7 +780,7 @@ func TestFloatingCopyModeWheelUsesCapturedSnapshot(t *testing.T) {
780 d, sess, ac, sends := newManualSessionWithPTYs(t, normal)780 d, sess, ac, sends := newManualSessionWithPTYs(t, normal)
781 fp := installFloatingCopyFixture(t, sess, domain.Size{Cols: 20, Rows: 3})781 fp := installFloatingCopyFixture(t, sess, domain.Size{Cols: 20, Rows: 3})
782 for i := range 30 {782 for i := range 30 {
783- fp.history.Append(testRow(fmt.Sprintf("old-%02d", i)))783+ require.NoError(t, fp.history.Append(testRow(fmt.Sprintf("old-%02d", i))))
784 }784 }
785 fp.screen.Write([]byte("live"))785 fp.screen.Write([]byte("live"))
786 fp.mu.Lock()786 fp.mu.Lock()
@@ -806,7 +806,7 @@ func TestFloatingCopyModeMouseSelectsFloatingRows(t *testing.T) {
806 d, sess, ac, sends := newManualSessionWithPTYs(t, normal)806 d, sess, ac, sends := newManualSessionWithPTYs(t, normal)
807 fp := installFloatingCopyFixture(t, sess, domain.Size{Cols: 20, Rows: 3})807 fp := installFloatingCopyFixture(t, sess, domain.Size{Cols: 20, Rows: 3})
808 for i := range 5 {808 for i := range 5 {
809- fp.history.Append(testRow(fmt.Sprintf("old-%d", i)))809+ require.NoError(t, fp.history.Append(testRow(fmt.Sprintf("old-%d", i))))
810 }810 }
811 fp.screen.Write([]byte("live"))811 fp.screen.Write([]byte("live"))
812 812
M internal/usecase/daemon/input_test.go +2 -2
@@ -1237,7 +1237,7 @@ func TestMouseNormalScreenDragUsesPressOwnedDocumentAfterOutputEviction(t *testi
12371237
12381238 // Output evicts five lines after Press; the Document remains immutable.
12391239 for range 5 {
1240- sess.tabs[0].focusedPane().history.Append(testRow("evicted"))
1240+ require.NoError(t, sess.tabs[0].focusedPane().history.Append(testRow("evicted")))
12411241 }
12421242
12431243 // Motion maps the next endpoint through the press-owned Document.
@@ -1764,7 +1764,7 @@ func TestActiveCopyMouseRejectsViewportChangeAfterMappingSnapshot(t *testing.T)
17641764 d, sess, ac, sends := newManualSessionWithPTYs(t, p)
17651765 pane := sess.activeTab().focusedPane()
17661766 for range 8 {
1767- pane.history.Append(testRow("history"))
1767+ require.NoError(t, pane.history.Append(testRow("history")))
17681768 }
17691769 d.enterCopyMode(sess, ac)
17701770 mustOutputData(t, sends)
@@ -1237,7 +1237,7 @@ func TestMouseNormalScreenDragUsesPressOwnedDocumentAfterOutputEviction(t *testi
1237 1237
1238 // Output evicts five lines after Press; the Document remains immutable.1238 // Output evicts five lines after Press; the Document remains immutable.
1239 for range 5 {1239 for range 5 {
1240- sess.tabs[0].focusedPane().history.Append(testRow("evicted"))1240+ require.NoError(t, sess.tabs[0].focusedPane().history.Append(testRow("evicted")))
1241 }1241 }
1242 1242
1243 // Motion maps the next endpoint through the press-owned Document.1243 // Motion maps the next endpoint through the press-owned Document.
@@ -1764,7 +1764,7 @@ func TestActiveCopyMouseRejectsViewportChangeAfterMappingSnapshot(t *testing.T)
1764 d, sess, ac, sends := newManualSessionWithPTYs(t, p)1764 d, sess, ac, sends := newManualSessionWithPTYs(t, p)
1765 pane := sess.activeTab().focusedPane()1765 pane := sess.activeTab().focusedPane()
1766 for range 8 {1766 for range 8 {
1767- pane.history.Append(testRow("history"))1767+ require.NoError(t, pane.history.Append(testRow("history")))
1768 }1768 }
1769 d.enterCopyMode(sess, ac)1769 d.enterCopyMode(sess, ac)
1770 mustOutputData(t, sends)1770 mustOutputData(t, sends)
M internal/usecase/daemon/overlay_runtime_test.go +2 -2
@@ -199,7 +199,7 @@ func TestOverlayRuntimeCopyModeDocumentSurvivesRingOverwrite(t *testing.T) {
199199 ac := &attachedClient{}
200200 ac.initOverlays()
201201 history := newTestHistory(1)
202- history.Append(testRow("before"))
202+ require.NoError(t, history.Append(testRow("before")))
203203 document := scopy.NewDocument(scopy.NewSnapshot(history, renderer.NewFrame(6, 1)), domain.DefaultWordSeparators)
204204
205205 ac.overlays.copyMu.Lock()
@@ -207,7 +207,7 @@ func TestOverlayRuntimeCopyModeDocumentSurvivesRingOverwrite(t *testing.T) {
207207 ac.overlays.copyDocument = document
208208 ac.overlays.copyMu.Unlock()
209209
210- history.Append(testRow("after"))
210+ require.NoError(t, history.Append(testRow("after")))
211211 renderSnap := ac.overlays.SnapshotForRender()
212212 defer renderSnap.Unlock()
213213 require.NotNil(t, renderSnap.copyMode)
@@ -199,7 +199,7 @@ func TestOverlayRuntimeCopyModeDocumentSurvivesRingOverwrite(t *testing.T) {
199 ac := &attachedClient{}199 ac := &attachedClient{}
200 ac.initOverlays()200 ac.initOverlays()
201 history := newTestHistory(1)201 history := newTestHistory(1)
202- history.Append(testRow("before"))202+ require.NoError(t, history.Append(testRow("before")))
203 document := scopy.NewDocument(scopy.NewSnapshot(history, renderer.NewFrame(6, 1)), domain.DefaultWordSeparators)203 document := scopy.NewDocument(scopy.NewSnapshot(history, renderer.NewFrame(6, 1)), domain.DefaultWordSeparators)
204 204
205 ac.overlays.copyMu.Lock()205 ac.overlays.copyMu.Lock()
@@ -207,7 +207,7 @@ func TestOverlayRuntimeCopyModeDocumentSurvivesRingOverwrite(t *testing.T) {
207 ac.overlays.copyDocument = document207 ac.overlays.copyDocument = document
208 ac.overlays.copyMu.Unlock()208 ac.overlays.copyMu.Unlock()
209 209
210- history.Append(testRow("after"))210+ require.NoError(t, history.Append(testRow("after")))
211 renderSnap := ac.overlays.SnapshotForRender()211 renderSnap := ac.overlays.SnapshotForRender()
212 defer renderSnap.Unlock()212 defer renderSnap.Unlock()
213 require.NotNil(t, renderSnap.copyMode)213 require.NotNil(t, renderSnap.copyMode)
M internal/usecase/daemon/picker_test.go +4 -3
@@ -796,7 +796,7 @@ func TestCaptureOverlayLayersResizeRecomposesPickerWithoutStalePreview(t *testin
796796 }
797797
798798 func TestPickerPreviewContainsOnlyVisibleFrameRowsWithLargeScrollback(t *testing.T) {
799- withScrollback := newPickerPreviewTabWithHistoryRows(10_000)
799+ withScrollback := newPickerPreviewTabWithHistoryRows(t, 10_000)
800800 require.Equal(t, 10_000, withScrollback.focusedPane().screen.History().Len())
801801
802802 preview := snapshotPickerPreview(withScrollback)
@@ -820,11 +820,12 @@ func TestPickerPreviewContainsOnlyVisibleFrameRowsWithLargeScrollback(t *testing
820820 require.NotContains(t, got, "history-only-marker")
821821 }
822822
823-func newPickerPreviewTabWithHistoryRows(historyRows int) *tab {
823+func newPickerPreviewTabWithHistoryRows(t testing.TB, historyRows int) *tab {
824+ t.Helper()
824825 tb := newTab(nil, domain.Size{Cols: 10, Rows: 3})
825826 p := tb.focusedPane()
826827 for range historyRows {
827- p.screen.History().Append(testRow("history-only-marker"))
828+ require.NoError(t, p.screen.History().Append(testRow("history-only-marker")))
828829 }
829830 p.screen.Write([]byte("NOW"))
830831 return tb
@@ -796,7 +796,7 @@ func TestCaptureOverlayLayersResizeRecomposesPickerWithoutStalePreview(t *testin
796 }796 }
797 797
798 func TestPickerPreviewContainsOnlyVisibleFrameRowsWithLargeScrollback(t *testing.T) {798 func TestPickerPreviewContainsOnlyVisibleFrameRowsWithLargeScrollback(t *testing.T) {
799- withScrollback := newPickerPreviewTabWithHistoryRows(10_000)799+ withScrollback := newPickerPreviewTabWithHistoryRows(t, 10_000)
800 require.Equal(t, 10_000, withScrollback.focusedPane().screen.History().Len())800 require.Equal(t, 10_000, withScrollback.focusedPane().screen.History().Len())
801 801
802 preview := snapshotPickerPreview(withScrollback)802 preview := snapshotPickerPreview(withScrollback)
@@ -820,11 +820,12 @@ func TestPickerPreviewContainsOnlyVisibleFrameRowsWithLargeScrollback(t *testing
820 require.NotContains(t, got, "history-only-marker")820 require.NotContains(t, got, "history-only-marker")
821 }821 }
822 822
823-func newPickerPreviewTabWithHistoryRows(historyRows int) *tab {823+func newPickerPreviewTabWithHistoryRows(t testing.TB, historyRows int) *tab {
824+ t.Helper()
824 tb := newTab(nil, domain.Size{Cols: 10, Rows: 3})825 tb := newTab(nil, domain.Size{Cols: 10, Rows: 3})
825 p := tb.focusedPane()826 p := tb.focusedPane()
826 for range historyRows {827 for range historyRows {
827- p.screen.History().Append(testRow("history-only-marker"))828+ require.NoError(t, p.screen.History().Append(testRow("history-only-marker")))
828 }829 }
829 p.screen.Write([]byte("NOW"))830 p.screen.Write([]byte("NOW"))
830 return tb831 return tb
M internal/usecase/daemon/render_test.go +1 -1
@@ -1289,7 +1289,7 @@ func TestPaintComposesCopyBodyAboveFloating(t *testing.T) {
12891289 defer releaseFloating()
12901290 d, sess, ac, sends := newManualSessionWithPTYs(t, normal)
12911291 fp := newPane("floating", floatingPTY, domain.Size{Cols: 20, Rows: 3})
1292- fp.history.Append(testRow("flt-old"))
1292+ require.NoError(t, fp.history.Append(testRow("flt-old")))
12931293 fp.screen.Write([]byte("flt-live"))
12941294 installTestFloating(sess.activeTab(), fp, true)
12951295
@@ -1289,7 +1289,7 @@ func TestPaintComposesCopyBodyAboveFloating(t *testing.T) {
1289 defer releaseFloating()1289 defer releaseFloating()
1290 d, sess, ac, sends := newManualSessionWithPTYs(t, normal)1290 d, sess, ac, sends := newManualSessionWithPTYs(t, normal)
1291 fp := newPane("floating", floatingPTY, domain.Size{Cols: 20, Rows: 3})1291 fp := newPane("floating", floatingPTY, domain.Size{Cols: 20, Rows: 3})
1292- fp.history.Append(testRow("flt-old"))1292+ require.NoError(t, fp.history.Append(testRow("flt-old")))
1293 fp.screen.Write([]byte("flt-live"))1293 fp.screen.Write([]byte("flt-live"))
1294 installTestFloating(sess.activeTab(), fp, true)1294 installTestFloating(sess.activeTab(), fp, true)
1295 1295
M internal/usecase/daemon/snapshot_acceptance_test.go +7 -7
@@ -28,7 +28,7 @@ func TestSnapshotCaptureMarshalUnmarshalRestorePreservesTopologyAndTerminalState
2828 first.nextPaneID = 3
2929 first.panes["pane-1"].stableID = "pane-one"
3030 secondPane := newPaneWithStableID("pane-2", "pane-two", snapshotAcceptancePTY(t), domain.Size{Cols: 40, Rows: 24})
31- secondPane.history.Append([]renderer.Cell{{Rune: 't'}, {Rune: 'w'}, {Rune: 'o'}})
31+ require.NoError(t, secondPane.history.Append([]renderer.Cell{{Rune: 't'}, {Rune: 'w'}, {Rune: 'o'}}))
3232 secondPane.screen.Write([]byte("two"))
3333 first.panes["pane-2"] = secondPane
3434 first.tree = &layout.Tree{Focus: "pane-2", Root: &layout.Node{Kind: layout.Split, Dir: layout.Horizontal, Children: []*layout.Node{
@@ -40,7 +40,7 @@ func TestSnapshotCaptureMarshalUnmarshalRestorePreservesTopologyAndTerminalState
4040 second.stableID = "tab-two"
4141 second.nextPaneID = 2
4242 second.panes["pane-1"].stableID = "pane-three"
43- second.panes["pane-1"].history.Append([]renderer.Cell{{Rune: 't'}, {Rune: 'h'}, {Rune: 'r'}, {Rune: 'e'}, {Rune: 'e'}})
43+ require.NoError(t, second.panes["pane-1"].history.Append([]renderer.Cell{{Rune: 't'}, {Rune: 'h'}, {Rune: 'r'}, {Rune: 'e'}, {Rune: 'e'}}))
4444 second.panes["pane-1"].screen.Write([]byte("three"))
4545 sess.tabs = []*tab{first, second}
4646 sess.active = 1
@@ -102,14 +102,14 @@ func TestSnapshotCaptureRetainsImmutableValuesAfterTailMutationAndEviction(t *te
102102 pane := sess.tabs[0].panes["pane-1"]
103103 pane.screen = vt.NewScreenWithHistory(8, 3, vt.HistoryConfig{MaxRows: 2, ChunkRows: 1})
104104 pane.history = pane.screen.History()
105- pane.history.Append([]renderer.Cell{{Rune: 'o'}, {Rune: 'l'}, {Rune: 'd'}})
105+ require.NoError(t, pane.history.Append([]renderer.Cell{{Rune: 'o'}, {Rune: 'l'}, {Rune: 'd'}}))
106106 pane.screen.Write([]byte("before"))
107107
108108 require.True(t, d.captureSession(sess))
109109 <-entered
110110 pane.mu.Lock()
111- pane.history.Append([]renderer.Cell{{Rune: 'n'}, {Rune: 'e'}, {Rune: 'w'}})
112- pane.history.Append([]renderer.Cell{{Rune: 'e'}, {Rune: 'v'}, {Rune: 'i'}, {Rune: 'c'}, {Rune: 't'}})
111+ require.NoError(t, pane.history.Append([]renderer.Cell{{Rune: 'n'}, {Rune: 'e'}, {Rune: 'w'}}))
112+ require.NoError(t, pane.history.Append([]renderer.Cell{{Rune: 'e'}, {Rune: 'v'}, {Rune: 'i'}, {Rune: 'c'}, {Rune: 't'}}))
113113 pane.screen.Write([]byte("\rafter"))
114114 pane.mu.Unlock()
115115 close(release)
@@ -130,13 +130,13 @@ func TestDaemonHistoryEvictionLeavesCopyAndSearchSnapshotsUsable(t *testing.T) {
130130 pane.screen = vt.NewScreenWithHistory(8, 1, vt.HistoryConfig{MaxRows: 2, ChunkRows: 1})
131131 pane.history = pane.screen.History()
132132 for _, text := range []string{"first", "second"} {
133- pane.history.Append(cells(text))
133+ require.NoError(t, pane.history.Append(cells(text)))
134134 }
135135 pane.screen.Write([]byte("screen"))
136136
137137 document := scopy.NewSnapshot(pane.history, pane.screen.Frame)
138138 for _, text := range []string{"third", "fourth"} {
139- pane.history.Append(cells(text))
139+ require.NoError(t, pane.history.Append(cells(text)))
140140 }
141141
142142 for _, tt := range []struct {
@@ -28,7 +28,7 @@ func TestSnapshotCaptureMarshalUnmarshalRestorePreservesTopologyAndTerminalState
28 first.nextPaneID = 328 first.nextPaneID = 3
29 first.panes["pane-1"].stableID = "pane-one"29 first.panes["pane-1"].stableID = "pane-one"
30 secondPane := newPaneWithStableID("pane-2", "pane-two", snapshotAcceptancePTY(t), domain.Size{Cols: 40, Rows: 24})30 secondPane := newPaneWithStableID("pane-2", "pane-two", snapshotAcceptancePTY(t), domain.Size{Cols: 40, Rows: 24})
31- secondPane.history.Append([]renderer.Cell{{Rune: 't'}, {Rune: 'w'}, {Rune: 'o'}})31+ require.NoError(t, secondPane.history.Append([]renderer.Cell{{Rune: 't'}, {Rune: 'w'}, {Rune: 'o'}}))
32 secondPane.screen.Write([]byte("two"))32 secondPane.screen.Write([]byte("two"))
33 first.panes["pane-2"] = secondPane33 first.panes["pane-2"] = secondPane
34 first.tree = &layout.Tree{Focus: "pane-2", Root: &layout.Node{Kind: layout.Split, Dir: layout.Horizontal, Children: []*layout.Node{34 first.tree = &layout.Tree{Focus: "pane-2", Root: &layout.Node{Kind: layout.Split, Dir: layout.Horizontal, Children: []*layout.Node{
@@ -40,7 +40,7 @@ func TestSnapshotCaptureMarshalUnmarshalRestorePreservesTopologyAndTerminalState
40 second.stableID = "tab-two"40 second.stableID = "tab-two"
41 second.nextPaneID = 241 second.nextPaneID = 2
42 second.panes["pane-1"].stableID = "pane-three"42 second.panes["pane-1"].stableID = "pane-three"
43- second.panes["pane-1"].history.Append([]renderer.Cell{{Rune: 't'}, {Rune: 'h'}, {Rune: 'r'}, {Rune: 'e'}, {Rune: 'e'}})43+ require.NoError(t, second.panes["pane-1"].history.Append([]renderer.Cell{{Rune: 't'}, {Rune: 'h'}, {Rune: 'r'}, {Rune: 'e'}, {Rune: 'e'}}))
44 second.panes["pane-1"].screen.Write([]byte("three"))44 second.panes["pane-1"].screen.Write([]byte("three"))
45 sess.tabs = []*tab{first, second}45 sess.tabs = []*tab{first, second}
46 sess.active = 146 sess.active = 1
@@ -102,14 +102,14 @@ func TestSnapshotCaptureRetainsImmutableValuesAfterTailMutationAndEviction(t *te
102 pane := sess.tabs[0].panes["pane-1"]102 pane := sess.tabs[0].panes["pane-1"]
103 pane.screen = vt.NewScreenWithHistory(8, 3, vt.HistoryConfig{MaxRows: 2, ChunkRows: 1})103 pane.screen = vt.NewScreenWithHistory(8, 3, vt.HistoryConfig{MaxRows: 2, ChunkRows: 1})
104 pane.history = pane.screen.History()104 pane.history = pane.screen.History()
105- pane.history.Append([]renderer.Cell{{Rune: 'o'}, {Rune: 'l'}, {Rune: 'd'}})105+ require.NoError(t, pane.history.Append([]renderer.Cell{{Rune: 'o'}, {Rune: 'l'}, {Rune: 'd'}}))
106 pane.screen.Write([]byte("before"))106 pane.screen.Write([]byte("before"))
107 107
108 require.True(t, d.captureSession(sess))108 require.True(t, d.captureSession(sess))
109 <-entered109 <-entered
110 pane.mu.Lock()110 pane.mu.Lock()
111- pane.history.Append([]renderer.Cell{{Rune: 'n'}, {Rune: 'e'}, {Rune: 'w'}})111+ require.NoError(t, pane.history.Append([]renderer.Cell{{Rune: 'n'}, {Rune: 'e'}, {Rune: 'w'}}))
112- pane.history.Append([]renderer.Cell{{Rune: 'e'}, {Rune: 'v'}, {Rune: 'i'}, {Rune: 'c'}, {Rune: 't'}})112+ require.NoError(t, pane.history.Append([]renderer.Cell{{Rune: 'e'}, {Rune: 'v'}, {Rune: 'i'}, {Rune: 'c'}, {Rune: 't'}}))
113 pane.screen.Write([]byte("\rafter"))113 pane.screen.Write([]byte("\rafter"))
114 pane.mu.Unlock()114 pane.mu.Unlock()
115 close(release)115 close(release)
@@ -130,13 +130,13 @@ func TestDaemonHistoryEvictionLeavesCopyAndSearchSnapshotsUsable(t *testing.T) {
130 pane.screen = vt.NewScreenWithHistory(8, 1, vt.HistoryConfig{MaxRows: 2, ChunkRows: 1})130 pane.screen = vt.NewScreenWithHistory(8, 1, vt.HistoryConfig{MaxRows: 2, ChunkRows: 1})
131 pane.history = pane.screen.History()131 pane.history = pane.screen.History()
132 for _, text := range []string{"first", "second"} {132 for _, text := range []string{"first", "second"} {
133- pane.history.Append(cells(text))133+ require.NoError(t, pane.history.Append(cells(text)))
134 }134 }
135 pane.screen.Write([]byte("screen"))135 pane.screen.Write([]byte("screen"))
136 136
137 document := scopy.NewSnapshot(pane.history, pane.screen.Frame)137 document := scopy.NewSnapshot(pane.history, pane.screen.Frame)
138 for _, text := range []string{"third", "fourth"} {138 for _, text := range []string{"third", "fourth"} {
139- pane.history.Append(cells(text))139+ require.NoError(t, pane.history.Append(cells(text)))
140 }140 }
141 141
142 for _, tt := range []struct {142 for _, tt := range []struct {
M internal/usecase/daemon/snapshot_restore_test.go +1 -1
@@ -25,7 +25,7 @@ func terminalPane(t *testing.T, id, stableID, cwd string, historyRows, visibleRo
2525 t.Helper()
2626 h := vt.NewHistory(vt.HistoryConfig{MaxRows: defaultScrollbackRows})
2727 for _, row := range historyRows {
28- h.Append(row)
28+ require.NoError(t, h.Append(row))
2929 }
3030 sealed, tail, err := vt.MarshalSealedHistory(h.SealAndView())
3131 require.NoError(t, err)
@@ -25,7 +25,7 @@ func terminalPane(t *testing.T, id, stableID, cwd string, historyRows, visibleRo
25 t.Helper()25 t.Helper()
26 h := vt.NewHistory(vt.HistoryConfig{MaxRows: defaultScrollbackRows})26 h := vt.NewHistory(vt.HistoryConfig{MaxRows: defaultScrollbackRows})
27 for _, row := range historyRows {27 for _, row := range historyRows {
28- h.Append(row)28+ require.NoError(t, h.Append(row))
29 }29 }
30 sealed, tail, err := vt.MarshalSealedHistory(h.SealAndView())30 sealed, tail, err := vt.MarshalSealedHistory(h.SealAndView())
31 require.NoError(t, err)31 require.NoError(t, err)
M internal/usecase/daemon/snapshot_test.go +1 -1
@@ -1034,7 +1034,7 @@ func newSnapshotTestSession(t *testing.T, name string, ephemeral bool, cwd strin
10341034 tb := newTab(pty, domain.Size{Cols: 8, Rows: 3})
10351035 p := tb.panes["pane-1"]
10361036 p.screen.Write([]byte("hello"))
1037- p.history.Append([]renderer.Cell{{Rune: 'h'}, {Rune: 'i'}})
1037+ require.NoError(t, p.history.Append([]renderer.Cell{{Rune: 'h'}, {Rune: 'i'}}))
10381038 sess := &session{
10391039 id: domain.SessionID("sess-" + name),
10401040 name: name,
@@ -1034,7 +1034,7 @@ func newSnapshotTestSession(t *testing.T, name string, ephemeral bool, cwd strin
1034 tb := newTab(pty, domain.Size{Cols: 8, Rows: 3})1034 tb := newTab(pty, domain.Size{Cols: 8, Rows: 3})
1035 p := tb.panes["pane-1"]1035 p := tb.panes["pane-1"]
1036 p.screen.Write([]byte("hello"))1036 p.screen.Write([]byte("hello"))
1037- p.history.Append([]renderer.Cell{{Rune: 'h'}, {Rune: 'i'}})1037+ require.NoError(t, p.history.Append([]renderer.Cell{{Rune: 'h'}, {Rune: 'i'}}))
1038 sess := &session{1038 sess := &session{
1039 id: domain.SessionID("sess-" + name),1039 id: domain.SessionID("sess-" + name),
1040 name: name,1040 name: name,
M internal/usecase/snapshot/benchmark_test.go +3 -1
@@ -63,7 +63,9 @@ func benchmarkV3Session(t testing.TB) (Session, []byte) {
6363 for col := range cells {
6464 cells[col] = renderer.Cell{Rune: rune('a' + (row+col)%26)}
6565 }
66- history.Append(cells)
66+ if err := history.Append(cells); err != nil {
67+ t.Fatal(err)
68+ }
6769 }
6870 sealed, tail, err := vt.MarshalSealedHistory(history.SealAndView())
6971 if err != nil {
@@ -63,7 +63,9 @@ func benchmarkV3Session(t testing.TB) (Session, []byte) {
63 for col := range cells {63 for col := range cells {
64 cells[col] = renderer.Cell{Rune: rune('a' + (row+col)%26)}64 cells[col] = renderer.Cell{Rune: rune('a' + (row+col)%26)}
65 }65 }
66- history.Append(cells)66+ if err := history.Append(cells); err != nil {
67+ t.Fatal(err)
68+ }
67 }69 }
68 sealed, tail, err := vt.MarshalSealedHistory(history.SealAndView())70 sealed, tail, err := vt.MarshalSealedHistory(history.SealAndView())
69 if err != nil {71 if err != nil {
M internal/usecase/snapshot/codec_test.go +2 -2
@@ -322,7 +322,7 @@ func TestUnmarshalRejectsProcessWithoutArgv(t *testing.T) {
322322 func TestUnmarshalGlobalBudgetRejectionDoesNotAllocatePerPane(t *testing.T) {
323323 history := vt.NewHistory(vt.HistoryConfig{MaxRows: 256, ChunkRows: 256})
324324 for range 256 {
325- history.Append([]renderer.Cell{{Rune: 'x'}})
325+ requireNoError(t, history.Append([]renderer.Cell{{Rune: 'x'}}))
326326 }
327327 sealed, tail, err := vt.MarshalSealedHistory(history.SealAndView())
328328 requireNoError(t, err)
@@ -385,7 +385,7 @@ func historyBlobs(t *testing.T, rows [][]renderer.Cell) ([][]byte, []byte) {
385385 t.Helper()
386386 h := vt.NewHistory(vt.HistoryConfig{MaxRows: 128, ChunkRows: 2})
387387 for _, row := range rows {
388- h.Append(row)
388+ requireNoError(t, h.Append(row))
389389 }
390390 sealed, tail, err := vt.MarshalSealedHistory(h.SealAndView())
391391 if err != nil {
@@ -322,7 +322,7 @@ func TestUnmarshalRejectsProcessWithoutArgv(t *testing.T) {
322 func TestUnmarshalGlobalBudgetRejectionDoesNotAllocatePerPane(t *testing.T) {322 func TestUnmarshalGlobalBudgetRejectionDoesNotAllocatePerPane(t *testing.T) {
323 history := vt.NewHistory(vt.HistoryConfig{MaxRows: 256, ChunkRows: 256})323 history := vt.NewHistory(vt.HistoryConfig{MaxRows: 256, ChunkRows: 256})
324 for range 256 {324 for range 256 {
325- history.Append([]renderer.Cell{{Rune: 'x'}})325+ requireNoError(t, history.Append([]renderer.Cell{{Rune: 'x'}}))
326 }326 }
327 sealed, tail, err := vt.MarshalSealedHistory(history.SealAndView())327 sealed, tail, err := vt.MarshalSealedHistory(history.SealAndView())
328 requireNoError(t, err)328 requireNoError(t, err)
@@ -385,7 +385,7 @@ func historyBlobs(t *testing.T, rows [][]renderer.Cell) ([][]byte, []byte) {
385 t.Helper()385 t.Helper()
386 h := vt.NewHistory(vt.HistoryConfig{MaxRows: 128, ChunkRows: 2})386 h := vt.NewHistory(vt.HistoryConfig{MaxRows: 128, ChunkRows: 2})
387 for _, row := range rows {387 for _, row := range rows {
388- h.Append(row)388+ requireNoError(t, h.Append(row))
389 }389 }
390 sealed, tail, err := vt.MarshalSealedHistory(h.SealAndView())390 sealed, tail, err := vt.MarshalSealedHistory(h.SealAndView())
391 if err != nil {391 if err != nil {
M pkg/vt/benchmark_test.go +2 -2
@@ -175,7 +175,7 @@ func benchmarkHistory(t testing.TB, rows, width, chunkRows int) *History {
175175 for col := range cells {
176176 cells[col] = renderer.Cell{Rune: rune('a' + (row+col)%26)}
177177 }
178- history.Append(cells)
178+ requireHistoryAppend(t, history, cells)
179179 }
180180 return history
181181 }
@@ -193,7 +193,7 @@ func benchmarkScreenResizeReflow(b *testing.B, historyRows int) {
193193 s := NewScreenWithHistory(120, 40, HistoryConfig{MaxRows: historyRows + 500})
194194 if historyRows > 0 {
195195 for range historyRows {
196- s.history.Append(make([]renderer.Cell, 120))
196+ requireHistoryAppend(b, s.history, make([]renderer.Cell, 120))
197197 }
198198 }
199199 s.Write(bytes.Repeat([]byte("x"), 120*39))
@@ -175,7 +175,7 @@ func benchmarkHistory(t testing.TB, rows, width, chunkRows int) *History {
175 for col := range cells {175 for col := range cells {
176 cells[col] = renderer.Cell{Rune: rune('a' + (row+col)%26)}176 cells[col] = renderer.Cell{Rune: rune('a' + (row+col)%26)}
177 }177 }
178- history.Append(cells)178+ requireHistoryAppend(t, history, cells)
179 }179 }
180 return history180 return history
181 }181 }
@@ -193,7 +193,7 @@ func benchmarkScreenResizeReflow(b *testing.B, historyRows int) {
193 s := NewScreenWithHistory(120, 40, HistoryConfig{MaxRows: historyRows + 500})193 s := NewScreenWithHistory(120, 40, HistoryConfig{MaxRows: historyRows + 500})
194 if historyRows > 0 {194 if historyRows > 0 {
195 for range historyRows {195 for range historyRows {
196- s.history.Append(make([]renderer.Cell, 120))196+ requireHistoryAppend(b, s.history, make([]renderer.Cell, 120))
197 }197 }
198 }198 }
199 s.Write(bytes.Repeat([]byte("x"), 120*39))199 s.Write(bytes.Repeat([]byte("x"), 120*39))
M pkg/vt/history_acceptance_test.go +1 -1
@@ -62,7 +62,7 @@ func TestSnapshotCodecsPreserveEveryTerminalCellAcrossSealedTailAndVisible(t *te
6262 makeRow('D', rgb), makeRow('E', indexed), makeRow('F', rgb), makeRow('G', indexed),
6363 }
6464 for _, row := range want {
65- history.Append(row)
65+ requireHistoryAppend(t, history, row)
6666 }
6767 if got, wantChunks := history.View().ChunkCount(), 4; got != wantChunks {
6868 t.Fatalf("chunk count = %d, want three sealed chunks plus copied tail %d", got, wantChunks)
@@ -62,7 +62,7 @@ func TestSnapshotCodecsPreserveEveryTerminalCellAcrossSealedTailAndVisible(t *te
62 makeRow('D', rgb), makeRow('E', indexed), makeRow('F', rgb), makeRow('G', indexed),62 makeRow('D', rgb), makeRow('E', indexed), makeRow('F', rgb), makeRow('G', indexed),
63 }63 }
64 for _, row := range want {64 for _, row := range want {
65- history.Append(row)65+ requireHistoryAppend(t, history, row)
66 }66 }
67 if got, wantChunks := history.View().ChunkCount(), 4; got != wantChunks {67 if got, wantChunks := history.View().ChunkCount(), 4; got != wantChunks {
68 t.Fatalf("chunk count = %d, want three sealed chunks plus copied tail %d", got, wantChunks)68 t.Fatalf("chunk count = %d, want three sealed chunks plus copied tail %d", got, wantChunks)
M pkg/vt/history_codec.go +2 -2
@@ -194,10 +194,10 @@ func parseHistory(data []byte, populate bool) (HistoryView, historyDecodeStats,
194194 cellCount, ok := p.uint32()
195195 cellBytes, validCellBytes := historyCellByteCount(uint64(cellCount))
196196 if !ok || !validCellBytes ||
197- uint64(cellCount) > uint64(len(p.data))/historyCellBytes ||
198197 !addHistoryDecodeBudget(&stats.cells, uint64(cellCount), maxHistoryCells) ||
199198 !addHistoryDecodeBudget(&stats.styles, uint64(cellCount), maxHistoryDecodeStyles) ||
200- !addHistoryDecodeBudget(&stats.bytes, cellBytes, maxHistoryDecodedBytes) {
199+ !addHistoryDecodeBudget(&stats.bytes, cellBytes, maxHistoryDecodedBytes) ||
200+ uint64(cellCount) > uint64(len(p.data))/historyCellBytes {
201201 return HistoryView{}, historyDecodeStats{}, false
202202 }
203203
@@ -194,10 +194,10 @@ func parseHistory(data []byte, populate bool) (HistoryView, historyDecodeStats,
194 cellCount, ok := p.uint32()194 cellCount, ok := p.uint32()
195 cellBytes, validCellBytes := historyCellByteCount(uint64(cellCount))195 cellBytes, validCellBytes := historyCellByteCount(uint64(cellCount))
196 if !ok || !validCellBytes ||196 if !ok || !validCellBytes ||
197- uint64(cellCount) > uint64(len(p.data))/historyCellBytes ||
198 !addHistoryDecodeBudget(&stats.cells, uint64(cellCount), maxHistoryCells) ||197 !addHistoryDecodeBudget(&stats.cells, uint64(cellCount), maxHistoryCells) ||
199 !addHistoryDecodeBudget(&stats.styles, uint64(cellCount), maxHistoryDecodeStyles) ||198 !addHistoryDecodeBudget(&stats.styles, uint64(cellCount), maxHistoryDecodeStyles) ||
200- !addHistoryDecodeBudget(&stats.bytes, cellBytes, maxHistoryDecodedBytes) {199+ !addHistoryDecodeBudget(&stats.bytes, cellBytes, maxHistoryDecodedBytes) ||
200+ uint64(cellCount) > uint64(len(p.data))/historyCellBytes {
201 return HistoryView{}, historyDecodeStats{}, false201 return HistoryView{}, historyDecodeStats{}, false
202 }202 }
203 203
M pkg/vt/history_codec_test.go +29 -8
@@ -57,7 +57,7 @@ func TestChunkCodecRoundTripsLosslessCellsAndStyles(t *testing.T) {
5757 t.Run(tt.name, func(t *testing.T) {
5858 history := NewHistory(HistoryConfig{MaxRows: 8, ChunkRows: 2})
5959 for _, row := range tt.rows {
60- history.Append(row)
60+ requireHistoryAppend(t, history, row)
6161 }
6262
6363 encoded, err := MarshalHistory(history.View())
@@ -75,8 +75,8 @@ func TestChunkCodecRoundTripsLosslessCellsAndStyles(t *testing.T) {
7575
7676 func TestChunkCodecRejectsTruncatedAndTrailingPayloads(t *testing.T) {
7777 history := NewHistory(HistoryConfig{MaxRows: 4, ChunkRows: 2})
78- history.Append(historyRow("AAAA"))
79- history.Append(historyRow("BBBB"))
78+ requireHistoryAppend(t, history, historyRow("AAAA"))
79+ requireHistoryAppend(t, history, historyRow("BBBB"))
8080 encoded, err := MarshalHistory(history.View())
8181 if err != nil {
8282 t.Fatalf("marshal history: %v", err)
@@ -136,11 +136,6 @@ func TestChunkCodecRejectsDimensionsBeyondSupportedLimits(t *testing.T) {
136136 view: historyViewWithDimensions(supportedRows+1, 0),
137137 data: historyPayloadWithDimensions(supportedRows+1, 0),
138138 },
139- {
140- name: "too many aggregate cells within row budget",
141- view: historyViewWithDimensions(maxHistoryRows, maxHistoryRowCells+1),
142- data: historyPayloadWithDimensions(maxHistoryRows, maxHistoryRowCells+1),
143- },
144139 }
145140
146141 for _, tt := range tests {
@@ -155,6 +150,19 @@ func TestChunkCodecRejectsDimensionsBeyondSupportedLimits(t *testing.T) {
155150 }
156151 }
157152
153+func TestChunkCodecRejectsAggregateCellDeclarationBeforePayloadAllocation(t *testing.T) {
154+ data := aggregateCellLimitDeclaration()
155+ if len(data) >= 1024 {
156+ t.Fatalf("aggregate declaration length = %d, want compact fixture", len(data))
157+ }
158+ if _, err := PreflightHistoryBlob(data); err == nil {
159+ t.Fatal("preflight accepted aggregate cell declaration")
160+ }
161+ if _, err := UnmarshalHistory(data); err == nil {
162+ t.Fatal("unmarshal accepted aggregate cell declaration")
163+ }
164+}
165+
158166 func TestChunkCodecAcceptsWideRowsWithinAggregateBudget(t *testing.T) {
159167 for _, width := range []int{161, 293, math.MaxUint16} {
160168 t.Run(fmt.Sprintf("width_%d", width), func(t *testing.T) {
@@ -234,6 +242,18 @@ func historyPayloadWithDimensions(rowCount, width int) []byte {
234242 return data
235243 }
236244
245+// aggregateCellLimitDeclaration declares one row, which is within the row
246+// budget, but its cells exceed the aggregate budget. It deliberately omits
247+// cell payload bytes: the aggregate limit must reject it before allocation.
248+func aggregateCellLimitDeclaration() []byte {
249+ data := make([]byte, 9, 21)
250+ copy(data, historyMagic)
251+ data[4] = historyVersion
252+ binary.BigEndian.PutUint32(data[5:], 1)
253+ data = binary.BigEndian.AppendUint32(data, 1)
254+ return binary.BigEndian.AppendUint32(data, uint32(maxHistoryCells+1))
255+}
256+
237257 func hostileHistoryDeclarations(chunkCount, rowCount int) []byte {
238258 data := make([]byte, 9, 9+chunkCount*(4+rowCount*4))
239259 copy(data, historyMagic)
@@ -269,6 +289,7 @@ func TestHistoryPreflightMatchesUnmarshalMalformedInput(t *testing.T) {
269289 {name: "invalid rune", data: invalidRune},
270290 {name: "invalid underline style", data: invalidUnderlineStyle},
271291 {name: "aggregate row budget", data: hostileHistoryDeclarations(47, maxHistoryChunkRows)},
292+ {name: "aggregate cell budget", data: aggregateCellLimitDeclaration()},
272293 {name: "zero row count", data: []byte{'V', 'T', 'H', '1', historyVersion, 0, 0, 0, 1, 0, 0, 0, 0}},
273294 }
274295 for _, tt := range tests {
@@ -57,7 +57,7 @@ func TestChunkCodecRoundTripsLosslessCellsAndStyles(t *testing.T) {
57 t.Run(tt.name, func(t *testing.T) {57 t.Run(tt.name, func(t *testing.T) {
58 history := NewHistory(HistoryConfig{MaxRows: 8, ChunkRows: 2})58 history := NewHistory(HistoryConfig{MaxRows: 8, ChunkRows: 2})
59 for _, row := range tt.rows {59 for _, row := range tt.rows {
60- history.Append(row)60+ requireHistoryAppend(t, history, row)
61 }61 }
62 62
63 encoded, err := MarshalHistory(history.View())63 encoded, err := MarshalHistory(history.View())
@@ -75,8 +75,8 @@ func TestChunkCodecRoundTripsLosslessCellsAndStyles(t *testing.T) {
75 75
76 func TestChunkCodecRejectsTruncatedAndTrailingPayloads(t *testing.T) {76 func TestChunkCodecRejectsTruncatedAndTrailingPayloads(t *testing.T) {
77 history := NewHistory(HistoryConfig{MaxRows: 4, ChunkRows: 2})77 history := NewHistory(HistoryConfig{MaxRows: 4, ChunkRows: 2})
78- history.Append(historyRow("AAAA"))78+ requireHistoryAppend(t, history, historyRow("AAAA"))
79- history.Append(historyRow("BBBB"))79+ requireHistoryAppend(t, history, historyRow("BBBB"))
80 encoded, err := MarshalHistory(history.View())80 encoded, err := MarshalHistory(history.View())
81 if err != nil {81 if err != nil {
82 t.Fatalf("marshal history: %v", err)82 t.Fatalf("marshal history: %v", err)
@@ -136,11 +136,6 @@ func TestChunkCodecRejectsDimensionsBeyondSupportedLimits(t *testing.T) {
136 view: historyViewWithDimensions(supportedRows+1, 0),136 view: historyViewWithDimensions(supportedRows+1, 0),
137 data: historyPayloadWithDimensions(supportedRows+1, 0),137 data: historyPayloadWithDimensions(supportedRows+1, 0),
138 },138 },
139- {
140- name: "too many aggregate cells within row budget",
141- view: historyViewWithDimensions(maxHistoryRows, maxHistoryRowCells+1),
142- data: historyPayloadWithDimensions(maxHistoryRows, maxHistoryRowCells+1),
143- },
144 }139 }
145 140
146 for _, tt := range tests {141 for _, tt := range tests {
@@ -155,6 +150,19 @@ func TestChunkCodecRejectsDimensionsBeyondSupportedLimits(t *testing.T) {
155 }150 }
156 }151 }
157 152
153+func TestChunkCodecRejectsAggregateCellDeclarationBeforePayloadAllocation(t *testing.T) {
154+ data := aggregateCellLimitDeclaration()
155+ if len(data) >= 1024 {
156+ t.Fatalf("aggregate declaration length = %d, want compact fixture", len(data))
157+ }
158+ if _, err := PreflightHistoryBlob(data); err == nil {
159+ t.Fatal("preflight accepted aggregate cell declaration")
160+ }
161+ if _, err := UnmarshalHistory(data); err == nil {
162+ t.Fatal("unmarshal accepted aggregate cell declaration")
163+ }
164+}
165+
158 func TestChunkCodecAcceptsWideRowsWithinAggregateBudget(t *testing.T) {166 func TestChunkCodecAcceptsWideRowsWithinAggregateBudget(t *testing.T) {
159 for _, width := range []int{161, 293, math.MaxUint16} {167 for _, width := range []int{161, 293, math.MaxUint16} {
160 t.Run(fmt.Sprintf("width_%d", width), func(t *testing.T) {168 t.Run(fmt.Sprintf("width_%d", width), func(t *testing.T) {
@@ -234,6 +242,18 @@ func historyPayloadWithDimensions(rowCount, width int) []byte {
234 return data242 return data
235 }243 }
236 244
245+// aggregateCellLimitDeclaration declares one row, which is within the row
246+// budget, but its cells exceed the aggregate budget. It deliberately omits
247+// cell payload bytes: the aggregate limit must reject it before allocation.
248+func aggregateCellLimitDeclaration() []byte {
249+ data := make([]byte, 9, 21)
250+ copy(data, historyMagic)
251+ data[4] = historyVersion
252+ binary.BigEndian.PutUint32(data[5:], 1)
253+ data = binary.BigEndian.AppendUint32(data, 1)
254+ return binary.BigEndian.AppendUint32(data, uint32(maxHistoryCells+1))
255+}
256+
237 func hostileHistoryDeclarations(chunkCount, rowCount int) []byte {257 func hostileHistoryDeclarations(chunkCount, rowCount int) []byte {
238 data := make([]byte, 9, 9+chunkCount*(4+rowCount*4))258 data := make([]byte, 9, 9+chunkCount*(4+rowCount*4))
239 copy(data, historyMagic)259 copy(data, historyMagic)
@@ -269,6 +289,7 @@ func TestHistoryPreflightMatchesUnmarshalMalformedInput(t *testing.T) {
269 {name: "invalid rune", data: invalidRune},289 {name: "invalid rune", data: invalidRune},
270 {name: "invalid underline style", data: invalidUnderlineStyle},290 {name: "invalid underline style", data: invalidUnderlineStyle},
271 {name: "aggregate row budget", data: hostileHistoryDeclarations(47, maxHistoryChunkRows)},291 {name: "aggregate row budget", data: hostileHistoryDeclarations(47, maxHistoryChunkRows)},
292+ {name: "aggregate cell budget", data: aggregateCellLimitDeclaration()},
272 {name: "zero row count", data: []byte{'V', 'T', 'H', '1', historyVersion, 0, 0, 0, 1, 0, 0, 0, 0}},293 {name: "zero row count", data: []byte{'V', 'T', 'H', '1', historyVersion, 0, 0, 0, 1, 0, 0, 0, 0}},
273 }294 }
274 for _, tt := range tests {295 for _, tt := range tests {
M pkg/vt/history_test.go +14 -7
@@ -8,6 +8,13 @@ import (
88 "github.com/bnema/vev/pkg/renderer"
99 )
1010
11+func requireHistoryAppend(t testing.TB, history *History, row []renderer.Cell) {
12+ t.Helper()
13+ if err := history.Append(row); err != nil {
14+ t.Fatalf("append history row: %v", err)
15+ }
16+}
17+
1118 func TestHistoryOrdersSealedChunksAndEvictsAtCapacity(t *testing.T) {
1219 tests := []struct {
1320 name string
@@ -43,7 +50,7 @@ func TestHistoryOrdersSealedChunksAndEvictsAtCapacity(t *testing.T) {
4350 t.Run(tt.name, func(t *testing.T) {
4451 history := NewHistory(HistoryConfig{MaxRows: tt.maxRows, ChunkRows: tt.chunkRows})
4552 for _, text := range tt.rows {
46- history.Append(historyRow(text))
53+ requireHistoryAppend(t, history, historyRow(text))
4754 }
4855
4956 view := history.View()
@@ -72,15 +79,15 @@ func TestHistoryTailRotationAndStableViews(t *testing.T) {
7279 t.Run(tt.name, func(t *testing.T) {
7380 history := NewHistory(HistoryConfig{MaxRows: 6, ChunkRows: 2})
7481 first := historyRow("AAAA")
75- history.Append(first)
76- history.Append(historyRow("BBBB"))
82+ requireHistoryAppend(t, history, first)
83+ requireHistoryAppend(t, history, historyRow("BBBB"))
7784 view := history.View()
7885 if got, want := view.ChunkCount(), 1; got != want {
7986 t.Fatalf("sealed chunk count = %d, want %d", got, want)
8087 }
8188
8289 first[0].Rune = 'X'
83- history.Append(historyRow("CCCC"))
90+ requireHistoryAppend(t, history, historyRow("CCCC"))
8491
8592 if got, want := historyViewTexts(view), []string{"AAAA", "BBBB"}; !equalStrings(got, want) {
8693 t.Fatalf("stable view rows = %#v, want %#v", got, want)
@@ -102,11 +109,11 @@ func TestHistoryReusesSealedChunkIdentityAcrossViews(t *testing.T) {
102109 for _, tt := range tests {
103110 t.Run(tt.name, func(t *testing.T) {
104111 history := NewHistory(HistoryConfig{MaxRows: 8, ChunkRows: 2})
105- history.Append(historyRow("AAAA"))
106- history.Append(historyRow("BBBB"))
112+ requireHistoryAppend(t, history, historyRow("AAAA"))
113+ requireHistoryAppend(t, history, historyRow("BBBB"))
107114 before := history.View()
108115
109- history.Append(historyRow("CCCC"))
116+ requireHistoryAppend(t, history, historyRow("CCCC"))
110117 after := history.View()
111118
112119 if before.ChunkCount() == 0 || after.ChunkCount() == 0 {
@@ -8,6 +8,13 @@ import (
8 "github.com/bnema/vev/pkg/renderer"8 "github.com/bnema/vev/pkg/renderer"
9 )9 )
10 10
11+func requireHistoryAppend(t testing.TB, history *History, row []renderer.Cell) {
12+ t.Helper()
13+ if err := history.Append(row); err != nil {
14+ t.Fatalf("append history row: %v", err)
15+ }
16+}
17+
11 func TestHistoryOrdersSealedChunksAndEvictsAtCapacity(t *testing.T) {18 func TestHistoryOrdersSealedChunksAndEvictsAtCapacity(t *testing.T) {
12 tests := []struct {19 tests := []struct {
13 name string20 name string
@@ -43,7 +50,7 @@ func TestHistoryOrdersSealedChunksAndEvictsAtCapacity(t *testing.T) {
43 t.Run(tt.name, func(t *testing.T) {50 t.Run(tt.name, func(t *testing.T) {
44 history := NewHistory(HistoryConfig{MaxRows: tt.maxRows, ChunkRows: tt.chunkRows})51 history := NewHistory(HistoryConfig{MaxRows: tt.maxRows, ChunkRows: tt.chunkRows})
45 for _, text := range tt.rows {52 for _, text := range tt.rows {
46- history.Append(historyRow(text))53+ requireHistoryAppend(t, history, historyRow(text))
47 }54 }
48 55
49 view := history.View()56 view := history.View()
@@ -72,15 +79,15 @@ func TestHistoryTailRotationAndStableViews(t *testing.T) {
72 t.Run(tt.name, func(t *testing.T) {79 t.Run(tt.name, func(t *testing.T) {
73 history := NewHistory(HistoryConfig{MaxRows: 6, ChunkRows: 2})80 history := NewHistory(HistoryConfig{MaxRows: 6, ChunkRows: 2})
74 first := historyRow("AAAA")81 first := historyRow("AAAA")
75- history.Append(first)82+ requireHistoryAppend(t, history, first)
76- history.Append(historyRow("BBBB"))83+ requireHistoryAppend(t, history, historyRow("BBBB"))
77 view := history.View()84 view := history.View()
78 if got, want := view.ChunkCount(), 1; got != want {85 if got, want := view.ChunkCount(), 1; got != want {
79 t.Fatalf("sealed chunk count = %d, want %d", got, want)86 t.Fatalf("sealed chunk count = %d, want %d", got, want)
80 }87 }
81 88
82 first[0].Rune = 'X'89 first[0].Rune = 'X'
83- history.Append(historyRow("CCCC"))90+ requireHistoryAppend(t, history, historyRow("CCCC"))
84 91
85 if got, want := historyViewTexts(view), []string{"AAAA", "BBBB"}; !equalStrings(got, want) {92 if got, want := historyViewTexts(view), []string{"AAAA", "BBBB"}; !equalStrings(got, want) {
86 t.Fatalf("stable view rows = %#v, want %#v", got, want)93 t.Fatalf("stable view rows = %#v, want %#v", got, want)
@@ -102,11 +109,11 @@ func TestHistoryReusesSealedChunkIdentityAcrossViews(t *testing.T) {
102 for _, tt := range tests {109 for _, tt := range tests {
103 t.Run(tt.name, func(t *testing.T) {110 t.Run(tt.name, func(t *testing.T) {
104 history := NewHistory(HistoryConfig{MaxRows: 8, ChunkRows: 2})111 history := NewHistory(HistoryConfig{MaxRows: 8, ChunkRows: 2})
105- history.Append(historyRow("AAAA"))112+ requireHistoryAppend(t, history, historyRow("AAAA"))
106- history.Append(historyRow("BBBB"))113+ requireHistoryAppend(t, history, historyRow("BBBB"))
107 before := history.View()114 before := history.View()
108 115
109- history.Append(historyRow("CCCC"))116+ requireHistoryAppend(t, history, historyRow("CCCC"))
110 after := history.View()117 after := history.View()
111 118
112 if before.ChunkCount() == 0 || after.ChunkCount() == 0 {119 if before.ChunkCount() == 0 || after.ChunkCount() == 0 {

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 12

Tags 11

↑↓ moveenter openesc close