v0.6.0 b250c5e

history_payload_test.go RawHistoryFolder

1package vt
2
3import (
4	"bytes"
5	"encoding/binary"
6	"testing"
7
8	renderer "github.com/bnema/vev-vt/core"
9	"github.com/stretchr/testify/require"
10)
11
12func TestPayloadSurvivesSnapshotResizeAndRecovery(t *testing.T) {
13	payload, err := renderer.NewCellPayload("e\u0301", "https://example.com")
14	require.NoError(t, err)
15	s := NewScreen(4, 2)
16	s.Write([]byte("ex"))
17	cell := s.Cell(0, 0)
18	cell.Payload = payload
19	s.frame.Set(0, 0, cell)
20	snapshot := s.Snapshot()
21	recovery := s.RecoveryTranscriptSnapshot()
22	s.Resize(2, 3)
23	require.Equal(t, payload, s.Cell(0, 0).Payload)
24	s.Write([]byte("\x1b[2J"))
25	require.Equal(t, payload, snapshot.Cell(0, 0).Payload)
26	blob, err := recovery.Marshal()
27	require.NoError(t, err)
28	view, err := UnmarshalHistory(blob)
29	require.NoError(t, err)
30	require.Equal(t, payload, view.Cell(0, 0).Payload)
31}
32
33func TestHistoryPayloadRoundTripEvictionAndRestore(t *testing.T) {
34	payload, err := renderer.NewCellPayload("e\u0301", "https://example.com")
35	require.NoError(t, err)
36	h := NewHistory(HistoryConfig{MaxRows: 2, MaxBytes: 1 << 20, ChunkRows: 2})
37	row := []renderer.Cell{{Rune: 'e', Style: renderer.DefaultStyle(), Payload: payload}}
38	require.NoError(t, h.Append(row, LineBound{End: 1}))
39	require.NoError(t, h.Append(historyRow("x"), LineBound{End: 1}))
40	before := h.View()
41	encoded, err := MarshalHistory(before)
42	require.NoError(t, err)
43	decoded, err := UnmarshalHistory(encoded)
44	require.NoError(t, err)
45	require.Equal(t, payload, decoded.Cell(0, 0).Payload)
46	require.Equal(t, before.LogicalBytes(), decoded.LogicalBytes())
47	require.NoError(t, decoded.Chunk(0).CheckInvariants())
48	require.NoError(t, h.Append(historyRow("y"), LineBound{End: 1}))
49	require.Equal(t, uint64(0), h.View().Chunk(0).payloadBytes)
50	require.Equal(t, payload, before.Cell(0, 0).Payload, "eviction must not mutate a sealed borrowed view")
51	sealed, tail, err := MarshalSealedHistory(before)
52	require.NoError(t, err)
53	restored, err := HistoryFromBlobs(HistoryConfig{MaxRows: 1, MaxBytes: 1 << 20}, sealed, tail)
54	require.NoError(t, err)
55	require.Equal(t, 'x', restored.View().Cell(0, 0).Rune)
56	require.True(t, restored.View().Cell(0, 0).Payload.Empty())
57	require.NoError(t, restored.View().Chunk(0).CheckInvariants())
58}
59
60func TestHistoryPayloadBudgetRejectsBeforeMutation(t *testing.T) {
61	payload, err := renderer.NewCellPayload("e\u0301", "https://example.com")
62	require.NoError(t, err)
63	rowBytes := renderer.StoredCellLogicalBytes + renderer.RowDescriptorLogicalBytes + renderer.StyleRecordLogicalBytes
64	h := NewHistory(HistoryConfig{MaxRows: 2, MaxBytes: rowBytes + payload.LogicalBytes() - 1})
65	err = h.Append([]renderer.Cell{{Rune: 'e', Style: renderer.DefaultStyle(), Payload: payload}}, LineBound{End: 1})
66	require.ErrorIs(t, err, ErrHistoryRowTooLarge)
67	require.Zero(t, h.Len())
68	require.Zero(t, h.LogicalBytes())
69}
70
71func TestPayloadCodecRejectsInvalidDictionariesAndReferences(t *testing.T) {
72	a, err := renderer.NewCellPayload("", "https://a")
73	require.NoError(t, err)
74	b, err := renderer.NewCellPayload("", "https://b")
75	require.NoError(t, err)
76	h := NewHistory(HistoryConfig{MaxRows: 1, MaxBytes: 1 << 20})
77	require.NoError(t, h.Append([]renderer.Cell{
78		{Rune: 'a', Style: renderer.DefaultStyle(), Payload: a},
79		{Rune: 'b', Style: renderer.DefaultStyle(), Payload: b},
80	}, LineBound{End: 2}))
81	valid, err := MarshalHistory(h.View())
82	require.NoError(t, err)
83	const dict = historyHeaderBytes + historyChunkHeaderBytes
84	firstLen := 8 + len(a.Hyperlink())
85	for _, tt := range []struct {
86		name   string
87		mutate func([]byte)
88	}{
89		{"missing payload reference", func(v []byte) { binary.BigEndian.PutUint32(v[len(v)-4:], 3) }},
90		{"zero explicit payload reference", func(v []byte) { clear(v[len(v)-4:]) }},
91		{"unused payload", func(v []byte) { binary.BigEndian.PutUint32(v[len(v)-4:], 1) }},
92		{"duplicate payload", func(v []byte) { copy(v[dict+firstLen:dict+2*firstLen], v[dict:dict+firstLen]) }},
93		{"control injection", func(v []byte) { v[dict+8] = 0x1b }},
94		{"invalid UTF8", func(v []byte) { v[dict+8] = 0xff }},
95		{"oversized string", func(v []byte) { binary.BigEndian.PutUint32(v[dict+4:], renderer.MaxHyperlinkBytes+1) }},
96		{"excessive dictionary", func(v []byte) { binary.BigEndian.PutUint32(v[28:], ^uint32(0)) }},
97	} {
98		t.Run(tt.name, func(t *testing.T) {
99			v := bytes.Clone(valid)
100			tt.mutate(v)
101			_, err := PreflightHistoryBlob(v)
102			require.Error(t, err)
103			_, err = UnmarshalHistory(v)
104			require.Error(t, err)
105		})
106	}
107	for end := range len(valid) {
108		_, err := UnmarshalHistory(valid[:end])
109		require.Error(t, err)
110	}
111}

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 2

Tags 13

↑↓ moveenter openesc close