fix(vt): align row ID exhaustion boundary
- commit
- 3c2478bf710314e71111c7d4f5c14a811863a5bc
- author
- brice · · 2026-08-02 21:11 UTC
- committer
- brice · · 2026-08-03 17:54 UTC
- parent
- cc001e4
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
2 files · +43 -1
M pkg/vt/row_id.go +1 -1
| @@ -5,7 +5,7 @@ package vt | |||
| 5 | 5 | type RowID uint64 | |
| 6 | 6 | ||
| 7 | 7 | func (s *Screen) nextRowIDValue() RowID { | |
| 8 | - | if s.nextRowID >= ^RowID(0)-1 { | |
| 8 | + | if s.nextRowID >= ^RowID(0)-2 { | |
| 9 | 9 | panic("vt: row ID space exhausted") | |
| 10 | 10 | } | |
| 11 | 11 | s.nextRowID++ | |
| @@ -5,7 +5,7 @@ package vt | |||||
| 5 | type RowID uint64 | 5 | type RowID uint64 | ||
| 6 | 6 | ||||
| 7 | func (s *Screen) nextRowIDValue() RowID { | 7 | func (s *Screen) nextRowIDValue() RowID { | ||
| 8 | - | if s.nextRowID >= ^RowID(0)-1 { | 8 | + | if s.nextRowID >= ^RowID(0)-2 { |
| 9 | panic("vt: row ID space exhausted") | 9 | panic("vt: row ID space exhausted") | ||
| 10 | } | 10 | } | ||
| 11 | s.nextRowID++ | 11 | s.nextRowID++ | ||
M pkg/vt/row_id_persistence_test.go +42 -0
| @@ -43,6 +43,48 @@ func TestHistoryAppendRejectsExhaustedRowIDCounter(t *testing.T) { | |||
| 43 | 43 | require.Error(t, history.Append(historyRow("row"), LineBound{End: 3})) | |
| 44 | 44 | } | |
| 45 | 45 | ||
| 46 | + | func TestScreenRowIDBoundaryMatchesHistoryPersistence(t *testing.T) { | |
| 47 | + | const maxPersistedRowID = ^RowID(0) - 2 | |
| 48 | + | for _, test := range []struct { | |
| 49 | + | name string | |
| 50 | + | current RowID | |
| 51 | + | want RowID | |
| 52 | + | persist bool | |
| 53 | + | }{ | |
| 54 | + | {name: "last persistable ID", current: maxPersistedRowID - 1, want: maxPersistedRowID, persist: true}, | |
| 55 | + | {name: "first non-persistable ID", current: maxPersistedRowID, want: maxPersistedRowID + 1}, | |
| 56 | + | } { | |
| 57 | + | t.Run(test.name, func(t *testing.T) { | |
| 58 | + | screen := NewScreen(1, 1) | |
| 59 | + | screen.nextRowID = test.current | |
| 60 | + | allocate := func() { | |
| 61 | + | require.Equal(t, test.want, screen.nextRowIDValue()) | |
| 62 | + | } | |
| 63 | + | if !test.persist { | |
| 64 | + | require.Panics(t, allocate) | |
| 65 | + | return | |
| 66 | + | } | |
| 67 | + | ||
| 68 | + | allocate() | |
| 69 | + | row := historyRow("x") | |
| 70 | + | screen.buffer.rowIDs[0] = test.want | |
| 71 | + | screen.buffer.frame.Row(0)[0] = row[0] | |
| 72 | + | screen.buffer.boundaries[0] = LineBound{End: 1} | |
| 73 | + | transcript, err := screen.RecoveryTranscriptSnapshot().Marshal() | |
| 74 | + | require.NoError(t, err) | |
| 75 | + | view, err := UnmarshalHistory(transcript) | |
| 76 | + | require.NoError(t, err) | |
| 77 | + | require.Equal(t, test.want, view.RowID(0)) | |
| 78 | + | require.Equal(t, test.want+1, view.NextRowID()) | |
| 79 | + | ||
| 80 | + | history := NewHistory(HistoryConfig{MaxRows: 1, ChunkRows: 1}) | |
| 81 | + | require.NoError(t, history.Append(row, LineBound{End: 1}, screen.RowID(0))) | |
| 82 | + | _, err = MarshalHistory(history.SealAndView()) | |
| 83 | + | require.NoError(t, err) | |
| 84 | + | }) | |
| 85 | + | } | |
| 86 | + | } | |
| 87 | + | ||
| 46 | 88 | func TestHistoryCodecRejectsMissingRowIDsOnMarshal(t *testing.T) { | |
| 47 | 89 | _, err := MarshalHistory(HistoryView{ | |
| 48 | 90 | chunks: []*HistoryChunk{{rows: [][]renderer.Cell{historyRow("row")}, bounds: []LineBound{{End: 3}}}}, | |
| @@ -43,6 +43,48 @@ func TestHistoryAppendRejectsExhaustedRowIDCounter(t *testing.T) { | |||||
| 43 | require.Error(t, history.Append(historyRow("row"), LineBound{End: 3})) | 43 | require.Error(t, history.Append(historyRow("row"), LineBound{End: 3})) | ||
| 44 | } | 44 | } | ||
| 45 | 45 | ||||
| 46 | + | func TestScreenRowIDBoundaryMatchesHistoryPersistence(t *testing.T) { | |||
| 47 | + | const maxPersistedRowID = ^RowID(0) - 2 | |||
| 48 | + | for _, test := range []struct { | |||
| 49 | + | name string | |||
| 50 | + | current RowID | |||
| 51 | + | want RowID | |||
| 52 | + | persist bool | |||
| 53 | + | }{ | |||
| 54 | + | {name: "last persistable ID", current: maxPersistedRowID - 1, want: maxPersistedRowID, persist: true}, | |||
| 55 | + | {name: "first non-persistable ID", current: maxPersistedRowID, want: maxPersistedRowID + 1}, | |||
| 56 | + | } { | |||
| 57 | + | t.Run(test.name, func(t *testing.T) { | |||
| 58 | + | screen := NewScreen(1, 1) | |||
| 59 | + | screen.nextRowID = test.current | |||
| 60 | + | allocate := func() { | |||
| 61 | + | require.Equal(t, test.want, screen.nextRowIDValue()) | |||
| 62 | + | } | |||
| 63 | + | if !test.persist { | |||
| 64 | + | require.Panics(t, allocate) | |||
| 65 | + | return | |||
| 66 | + | } | |||
| 67 | + | ||||
| 68 | + | allocate() | |||
| 69 | + | row := historyRow("x") | |||
| 70 | + | screen.buffer.rowIDs[0] = test.want | |||
| 71 | + | screen.buffer.frame.Row(0)[0] = row[0] | |||
| 72 | + | screen.buffer.boundaries[0] = LineBound{End: 1} | |||
| 73 | + | transcript, err := screen.RecoveryTranscriptSnapshot().Marshal() | |||
| 74 | + | require.NoError(t, err) | |||
| 75 | + | view, err := UnmarshalHistory(transcript) | |||
| 76 | + | require.NoError(t, err) | |||
| 77 | + | require.Equal(t, test.want, view.RowID(0)) | |||
| 78 | + | require.Equal(t, test.want+1, view.NextRowID()) | |||
| 79 | + | ||||
| 80 | + | history := NewHistory(HistoryConfig{MaxRows: 1, ChunkRows: 1}) | |||
| 81 | + | require.NoError(t, history.Append(row, LineBound{End: 1}, screen.RowID(0))) | |||
| 82 | + | _, err = MarshalHistory(history.SealAndView()) | |||
| 83 | + | require.NoError(t, err) | |||
| 84 | + | }) | |||
| 85 | + | } | |||
| 86 | + | } | |||
| 87 | + | ||||
| 46 | func TestHistoryCodecRejectsMissingRowIDsOnMarshal(t *testing.T) { | 88 | func TestHistoryCodecRejectsMissingRowIDsOnMarshal(t *testing.T) { | ||
| 47 | _, err := MarshalHistory(HistoryView{ | 89 | _, err := MarshalHistory(HistoryView{ | ||
| 48 | chunks: []*HistoryChunk{{rows: [][]renderer.Cell{historyRow("row")}, bounds: []LineBound{{End: 3}}}}, | 90 | chunks: []*HistoryChunk{{rows: [][]renderer.Cell{historyRow("row")}, bounds: []LineBound{{End: 3}}}}, | ||