codec_test.go RawHistoryFolder

1package snapshotcodec
2
3import (
4	"bytes"
5	"encoding/binary"
6	"errors"
7	"hash/crc32"
8	"math"
9	"strconv"
10	"testing"
11
12	vt "github.com/bnema/vev-vt"
13	renderer "github.com/bnema/vev-vt/ansi"
14	"github.com/bnema/vev/internal/domain/layout"
15)
16
17func TestPayloadReaderGetBytes(t *testing.T) {
18	for _, tc := range []struct {
19		name      string
20		input     []byte
21		n         int
22		want      []byte
23		remaining []byte
24		wantErr   error
25	}{
26		{name: "reads and advances", input: []byte{1, 2, 3}, n: 2, want: []byte{1, 2}, remaining: []byte{3}},
27		{name: "reads empty", input: []byte{1}, n: 0, want: []byte{}, remaining: []byte{1}},
28		{name: "short payload does not advance", input: []byte{1}, n: 2, remaining: []byte{1}, wantErr: ErrShortPayload},
29	} {
30		t.Run(tc.name, func(t *testing.T) {
31			r := payloadReader{b: append([]byte(nil), tc.input...)}
32			got, err := r.getBytes(tc.n)
33			if !errors.Is(err, tc.wantErr) {
34				t.Fatalf("getBytes(%d) error = %v, want %v", tc.n, err, tc.wantErr)
35			}
36			if !bytes.Equal(got, tc.want) {
37				t.Fatalf("getBytes(%d) = %v, want %v", tc.n, got, tc.want)
38			}
39			if !bytes.Equal(r.b, tc.remaining) {
40				t.Fatalf("remaining bytes = %v, want %v", r.b, tc.remaining)
41			}
42		})
43	}
44}
45
46func TestV4SnapshotRoundTripPreservesCanonicalTerminalData(t *testing.T) {
47	indexed := renderer.DefaultStyle()
48	indexed.Bold, indexed.Inverse = true, true
49	indexed.Attrs = renderer.AttrDim | renderer.AttrUnderline | renderer.AttrBlink | renderer.AttrStrikethrough
50	indexed.Foreground, indexed.Background = 196, 17
51	indexed.UnderlineStyle, indexed.HasUnderlineColor, indexed.UnderlineColor = renderer.UnderlineCurly, true, 203
52	rgb := renderer.DefaultStyle()
53	rgb.Italic, rgb.HasForegroundRGB, rgb.ForegroundRGB = true, true, renderer.RGB{R: 1, G: 2, B: 3}
54	rgb.HasBackgroundRGB, rgb.BackgroundRGB = true, renderer.RGB{R: 4, G: 5, B: 6}
55	rgb.UnderlineStyle, rgb.HasUnderlineColorRGB, rgb.UnderlineColorRGB = renderer.UnderlineDashed, true, renderer.RGB{R: 7, G: 8, B: 9}
56	sealed, tail := historyBlobs(t, [][]renderer.Cell{{{Rune: 'I', Style: indexed}, {Rune: '好', Style: rgb}, {Continuation: true, Style: rgb}}})
57	transcript := transcriptBlob(t, [][]renderer.Cell{{{Rune: 'V', Style: rgb}, {Rune: '界', Style: indexed}, {Continuation: true, Style: indexed}}})
58	sealed2, tail2 := historyBlobs(t, [][]renderer.Cell{{{Rune: 'S', Style: rgb}}, {{Rune: '2', Style: indexed}}})
59	transcript2 := transcriptBlob(t, [][]renderer.Cell{{{Rune: 'P', Style: indexed}, {Rune: '2', Style: rgb}}})
60	want := Session{Name: "v4 exact", Tabs: []Tab{{Focus: "p", Tree: &layout.Tree{Focus: "p", Root: &layout.Node{Kind: layout.Split, Dir: layout.Horizontal, Children: []*layout.Node{layout.NewLeaf("p"), layout.NewLeaf("p2")}}}, Panes: []Pane{
61		{ID: "p", SealedChunks: sealed, Tail: tail, Transcript: transcript},
62		{ID: "p2", SealedChunks: sealed2, Tail: tail2, Transcript: transcript2},
63	}}}}
64	encoded, err := marshalTest(want)
65	if err != nil {
66		t.Fatal(err)
67	}
68	requireV4Envelope(t, encoded)
69	got, err := Unmarshal(encoded)
70	if err != nil {
71		t.Fatal(err)
72	}
73	if len(got.Tabs) != 1 || len(got.Tabs[0].Panes) != 2 || got.Tabs[0].Panes[0].ID != "p" || got.Tabs[0].Panes[1].ID != "p2" {
74		t.Fatalf("manifest round trip = %#v", got)
75	}
76	for i, wantPane := range want.Tabs[0].Panes {
77		gotPane := got.Tabs[0].Panes[i]
78		if len(gotPane.SealedChunks) != len(wantPane.SealedChunks) {
79			t.Fatalf("pane %q sealed chunks = %d, want %d", wantPane.ID, len(gotPane.SealedChunks), len(wantPane.SealedChunks))
80		}
81		for j := range wantPane.SealedChunks {
82			if !bytes.Equal(gotPane.SealedChunks[j], wantPane.SealedChunks[j]) {
83				t.Fatalf("pane %q sealed chunk %d changed during round trip", wantPane.ID, j)
84			}
85		}
86		if !bytes.Equal(gotPane.Tail, wantPane.Tail) {
87			t.Fatalf("pane %q tail changed during round trip", wantPane.ID)
88		}
89		if !bytes.Equal(gotPane.Transcript, wantPane.Transcript) {
90			t.Fatalf("pane %q transcript blob changed during round trip", wantPane.ID)
91		}
92	}
93	if _, err := vt.HistoryFromBlobs(vt.HistoryConfig{MaxRows: 8, ChunkRows: 2}, got.Tabs[0].Panes[0].SealedChunks, got.Tabs[0].Panes[0].Tail); err != nil {
94		t.Fatalf("history decode: %v", err)
95	}
96	view, err := vt.UnmarshalHistory(got.Tabs[0].Panes[0].Transcript)
97	if err != nil {
98		t.Fatal(err)
99	}
100	if !view.Row(0)[0].Equal(renderer.Cell{Rune: 'V', Style: rgb}) || !view.Row(0)[2].Continuation {
101		t.Fatalf("transcript terminal data lost: %#v", view.Row(0))
102	}
103}
104
105func TestActiveTabReferenceMustBeExact(t *testing.T) {
106	validTab := Tab{}
107	for _, tc := range []struct {
108		name  string
109		s     Session
110		valid bool
111	}{
112		{name: "zero tabs active zero", s: Session{Active: 0}, valid: true},
113		{name: "zero tabs active nonzero", s: Session{Active: 1}},
114		{name: "nonempty tabs active in range", s: Session{Active: 0, Tabs: []Tab{validTab}}, valid: true},
115		{name: "nonempty tabs active past end", s: Session{Active: 1, Tabs: []Tab{validTab}}},
116	} {
117		t.Run("marshal "+tc.name, func(t *testing.T) {
118			_, err := marshalTest(tc.s)
119			if tc.valid {
120				requireNoError(t, err)
121				return
122			}
123			if !errors.Is(err, ErrInvalidData) {
124				t.Fatalf("marshalTest() error = %v, want %v", err, ErrInvalidData)
125			}
126		})
127	}
128
129	for _, tc := range []struct {
130		name string
131		data []byte
132	}{
133		{name: "zero tabs active nonzero", data: v4ActiveManifest(1, 0)},
134		{name: "nonempty tabs active past end", data: v4ActiveManifest(1, 1)},
135	} {
136		t.Run("unmarshal "+tc.name, func(t *testing.T) {
137			reject(t, v4Envelope(version, 0, tc.data), ErrInvalidData)
138		})
139	}
140}
141
142func TestUnmarshalRejectsV4LegacyVersions(t *testing.T) {
143	for _, version := range []uint16{1, 2, 3, 4} {
144		if _, err := Unmarshal(v4Envelope(version, 0, nil)); !errors.Is(err, ErrBadVersion) {
145			t.Fatalf("version %d: %v", version, err)
146		}
147	}
148}
149
150func TestUnmarshalRejectsMalformedV4EnvelopeBeforeAllocation(t *testing.T) {
151	body := v4Manifest(0, nil, nil, 0, nil, 0, nil)
152	valid := v4Envelope(version, 0, body)
153	badCRC := append([]byte(nil), valid...)
154	badCRC[15] ^= 1
155	for _, tc := range []struct {
156		name string
157		data []byte
158		want error
159	}{
160		{"short", valid[:15], ErrShortPayload}, {"magic", append([]byte("NOPE"), valid[4:]...), ErrBadMagic},
161		{"future", v4Envelope(version+1, 0, body), ErrBadVersion}, {"flags", v4Envelope(version, 1, body), ErrInvalidData},
162		{"crc", badCRC, ErrBadCRC}, {"body", v4EnvelopeWithLength(version, 0, nil, math.MaxUint32), ErrInvalidData},
163	} {
164		t.Run(tc.name, func(t *testing.T) { reject(t, tc.data, tc.want) })
165	}
166}
167
168func TestUnmarshalRejectsHostileV4ManifestDeclarationsBeforeAllocation(t *testing.T) {
169	for _, tc := range []struct {
170		name string
171		body []byte
172	}{
173		{"count", v4Manifest(math.MaxUint32, nil, nil, 0, nil, 0, nil)},
174		{"sealed", v4Manifest(1, []uint32{math.MaxUint32}, nil, 0, nil, 0, nil)},
175		{"tail", v4Manifest(0, nil, nil, math.MaxUint32, nil, 0, nil)},
176		{"transcript", v4Manifest(0, nil, nil, 0, nil, math.MaxUint32, nil)},
177	} {
178		t.Run(tc.name, func(t *testing.T) { reject(t, v4Envelope(version, 0, tc.body), ErrInvalidData) })
179	}
180}
181
182func TestUnmarshalRejectsEveryV4PrefixAndTrailingGarbage(t *testing.T) {
183	sealed, tail := historyBlobs(t, [][]renderer.Cell{{{Rune: 'h'}}})
184	transcript := transcriptBlob(t, [][]renderer.Cell{{{Rune: 'v'}}})
185	encoded, err := marshalTest(Session{Name: "p", Tabs: []Tab{{Focus: "p", Tree: layout.NewTree("p"), Panes: []Pane{{ID: "p", SealedChunks: sealed, Tail: tail, Transcript: transcript}}}}})
186	if err != nil {
187		t.Fatal(err)
188	}
189	for n := range len(encoded) {
190		reject(t, encoded[:n], nil)
191	}
192	reject(t, append(encoded, 0), ErrTrailingBytes)
193}
194
195func TestUnmarshalRejectsEmptyAndDuplicatePaneIDsDuringPreflight(t *testing.T) {
196	sealed, tail := historyBlobs(t, nil)
197	transcript := transcriptBlob(t, [][]renderer.Cell{{{Rune: 'v'}}})
198	for _, tc := range []struct {
199		name  string
200		panes []Pane
201	}{
202		{name: "empty", panes: []Pane{{ID: "", Tail: tail, Transcript: transcript}}},
203		{name: "duplicate", panes: []Pane{{ID: "p", SealedChunks: sealed, Tail: tail, Transcript: transcript}, {ID: "p", Tail: tail, Transcript: transcript}}},
204	} {
205		t.Run(tc.name, func(t *testing.T) {
206			data, err := marshalTest(Session{Name: "s", Tabs: []Tab{{Panes: tc.panes}}})
207			requireNoError(t, err)
208			reject(t, data, ErrInvalidData)
209		})
210	}
211}
212
213func TestUnmarshalRejectsInvalidTreeReference(t *testing.T) {
214	sealed, tail := historyBlobs(t, nil)
215	transcript := transcriptBlob(t, [][]renderer.Cell{{{Rune: 'v'}}})
216	encoded, err := marshalTest(Session{Name: "p", Tabs: []Tab{{Focus: "missing", Tree: layout.NewTree("missing"), Panes: []Pane{{ID: "p", SealedChunks: sealed, Tail: tail, Transcript: transcript}}}}})
217	if err != nil {
218		t.Fatal(err)
219	}
220	if _, err := Unmarshal(encoded); !errors.Is(err, ErrUnknownPane) {
221		t.Fatalf("error = %v", err)
222	}
223}
224
225func TestUnmarshalEnforcesCanonicalHistoryBlobRoles(t *testing.T) {
226	sealed, emptyTail := historyBlobs(t, [][]renderer.Cell{{{Rune: 'a'}}, {{Rune: 'b'}}, {{Rune: 'c'}}})
227	if len(sealed) < 2 {
228		t.Fatalf("expected at least two sealed chunks, got %d", len(sealed))
229	}
230	multiChunk := append([]byte(nil), sealed[0][:16]...)
231	binary.BigEndian.PutUint32(multiChunk[4:8], 2)
232	binary.BigEndian.PutUint64(multiChunk[8:16], 4)
233	multiChunk = append(multiChunk, sealed[0][16:]...)
234	multiChunk = append(multiChunk, sealed[1][16:]...)
235	transcript := transcriptBlob(t, [][]renderer.Cell{{{Rune: 'v'}}})
236	emptyTranscript := transcriptBlob(t, nil)
237
238	// Incremental captures retain one copied mutable tail chunk instead of
239	// rotating it into the sealed history set. That one chunk is canonical and
240	// must remain a valid v4 import payload.
241	mutableHistory := vt.NewHistory(vt.HistoryConfig{MaxRows: 8, ChunkRows: 2})
242	for _, row := range [][]renderer.Cell{{{Rune: 'a'}}, {{Rune: 'b'}}, {{Rune: 'c'}}} {
243		requireNoError(t, mutableHistory.Append(row, vt.LineBound{End: len(row)}))
244	}
245	mutableTail, err := vt.MarshalHistoryTail(mutableHistory.SnapshotView())
246	requireNoError(t, err)
247	fullHistory, err := vt.MarshalHistory(mutableHistory.SealAndView())
248	requireNoError(t, err)
249	for _, tc := range []struct {
250		name       string
251		transcript []byte
252	}{
253		{name: "single chunk", transcript: transcript},
254		{name: "empty", transcript: emptyTranscript},
255		{name: "multiple chunks", transcript: multiChunk},
256	} {
257		t.Run("valid "+tc.name, func(t *testing.T) {
258			data, err := marshalTest(Session{Name: "s", Tabs: []Tab{{Cols: 1, Rows: 1, Panes: []Pane{{ID: "p", SealedChunks: sealed[:1], Tail: mutableTail, Transcript: tc.transcript}}}}})
259			requireNoError(t, err)
260			roundTrip, err := Unmarshal(data)
261			requireNoError(t, err)
262			if got := roundTrip.Tabs[0].Panes[0].Tail; !bytes.Equal(got, mutableTail) {
263				t.Fatal("mutable tail did not round trip")
264			}
265		})
266	}
267
268	for _, tc := range []struct {
269		name string
270		tab  Tab
271	}{
272		{name: "sealed multi-chunk", tab: Tab{Cols: 1, Rows: 1, Panes: []Pane{{ID: "p", SealedChunks: [][]byte{multiChunk}, Tail: emptyTail, Transcript: transcript}}}},
273		{name: "tail has multiple chunks", tab: Tab{Cols: 1, Rows: 1, Panes: []Pane{{ID: "p", Tail: multiChunk, Transcript: transcript}}}},
274		{name: "noncanonical full history tail", tab: Tab{Cols: 1, Rows: 1, Panes: []Pane{{ID: "p", Tail: fullHistory, Transcript: transcript}}}},
275	} {
276		t.Run(tc.name, func(t *testing.T) {
277			data, err := marshalTest(Session{Name: "s", Tabs: []Tab{tc.tab}})
278			requireNoError(t, err)
279			reject(t, data, ErrInvalidData)
280		})
281	}
282}
283
284func TestUnmarshalRejectsMalformedCanonicalVTBlobsWithoutPanic(t *testing.T) {
285	tail := mustHistoryTail(t)
286	transcript := transcriptBlob(t, [][]renderer.Cell{{{Rune: 'v'}}})
287	for _, tc := range []struct {
288		name string
289		blob []byte
290	}{
291		{name: "malformed", blob: append([]byte("NOPE"), transcript[4:]...)},
292		{name: "truncated", blob: transcript[:len(transcript)-1]},
293		{name: "trailing", blob: append(append([]byte(nil), transcript...), 0)},
294	} {
295		t.Run(tc.name, func(t *testing.T) {
296			body := v4Manifest(0, nil, nil, uint32(len(tail)), tail, uint32(len(tc.blob)), tc.blob)
297			reject(t, v4Envelope(version, 0, body), ErrInvalidData)
298		})
299	}
300}
301
302func TestRoundTripNilTreeAndRootDoNotMaterializeLeaf(t *testing.T) {
303	for _, tc := range []struct {
304		name string
305		tab  Tab
306	}{
307		{name: "nil tree", tab: Tab{Tree: nil}},
308		{name: "nil root", tab: Tab{Tree: &layout.Tree{Root: nil}}},
309	} {
310		t.Run(tc.name, func(t *testing.T) {
311			data, err := marshalTest(Session{Name: "s", Tabs: []Tab{tc.tab}})
312			requireNoError(t, err)
313			got, err := Unmarshal(data)
314			requireNoError(t, err)
315			if got.Tabs[0].Tree == nil || got.Tabs[0].Tree.Root != nil {
316				t.Fatalf("tree = %#v, want nonnil tree with nil root", got.Tabs[0].Tree)
317			}
318		})
319	}
320}
321
322func TestRoundTripSessionMetadataAndProcess(t *testing.T) {
323	sealed, tail := historyBlobs(t, [][]renderer.Cell{{{Rune: 'h'}}, {{Rune: 'i'}}})
324	transcript := transcriptBlob(t, [][]renderer.Cell{{{Rune: 'v'}}})
325	want := Session{Name: "named", CreatedAt: 42, Active: 1, Tabs: []Tab{
326		{
327			StableID: "t1", Cols: 100, Rows: 40, NextPaneID: 9, Focus: "2",
328			Tree: &layout.Tree{Focus: "2", Root: &layout.Node{Kind: layout.Split, Dir: layout.Horizontal, Children: []*layout.Node{
329				layout.NewLeaf("1"), layout.NewLeaf("2"),
330			}}},
331			Panes: []Pane{
332				{ID: "1", StableID: "p1", Cwd: "/one", SealedChunks: sealed, Tail: tail, Transcript: transcript, Process: &Process{Argv: []string{"pi", "--resume"}, Strategy: "pi", Opts: ProcessOpts{AgentSessionID: "agent-123"}}},
333				{ID: "2", StableID: "p2", Cwd: "/two", SealedChunks: sealed, Tail: tail, Transcript: transcript},
334			},
335		},
336		{StableID: "t2", Cols: 80, Rows: 24, NextPaneID: 2, Focus: "a", Tree: layout.NewTree("a"), Panes: []Pane{{ID: "a", Cwd: "/tmp", SealedChunks: sealed, Tail: tail, Transcript: transcript}}},
337	}}
338	data, err := marshalTest(want)
339	requireNoError(t, err)
340	got, err := Unmarshal(data)
341	requireNoError(t, err)
342	if got.Name != want.Name || got.CreatedAt != want.CreatedAt || got.Active != want.Active || len(got.Tabs) != 2 {
343		t.Fatalf("session metadata round trip = %#v", got)
344	}
345	pane := got.Tabs[0].Panes[0]
346	if pane.ID != "1" || pane.StableID != "p1" || pane.Cwd != "/one" || pane.Process == nil || pane.Process.Opts.AgentSessionID != "agent-123" || got.Tabs[0].Tree.Root.Kind != layout.Split {
347		t.Fatalf("pane metadata round trip = %#v", pane)
348	}
349}
350
351func TestMarshalRejectsProcessWithoutArgv(t *testing.T) {
352	_, err := marshalTest(Session{Name: "s", Tabs: []Tab{
353		{Tree: layout.NewTree("p"), Focus: "p", Panes: []Pane{{ID: "p", Process: &Process{Strategy: "generic"}}}},
354	}})
355	if !errors.Is(err, ErrInvalidData) {
356		t.Fatalf("marshalTest() error = %v, want %v", err, ErrInvalidData)
357	}
358}
359
360func TestUnmarshalRejectsProcessWithoutArgv(t *testing.T) {
361	tail := mustHistoryTail(t)
362	transcript := transcriptBlob(t, [][]renderer.Cell{{{Rune: 'v'}}})
363	var w payloadWriter
364	_ = w.putString("s")
365	w.putUint64(0)
366	w.putUint16(0)
367	w.putUint16(1)
368	_ = w.putString("t")
369	w.putUint16(80)
370	w.putUint16(24)
371	w.putUint64(1)
372	_ = w.putString("p")
373	_ = writeNode(&w, layout.NewLeaf("p"))
374	w.putUint16(1)
375	_ = w.putString("p")
376	_ = w.putString("")
377	_ = w.putString("")
378	w.putUint32(0)
379	w.putUint32(uint32(len(tail)))
380	w.b = append(w.b, tail...)
381	w.putUint32(uint32(len(transcript)))
382	w.b = append(w.b, transcript...)
383	w.putUint8(1)
384	w.putUint16(0) // argv count
385	_ = w.putString("generic")
386	_ = w.putString("")
387	if _, err := Unmarshal(v4Envelope(version, 0, w.b)); !errors.Is(err, ErrInvalidData) {
388		t.Fatalf("Unmarshal() error = %v, want %v", err, ErrInvalidData)
389	}
390}
391
392func TestUnmarshalGlobalBudgetRejectionDoesNotAllocatePerPane(t *testing.T) {
393	history := vt.NewHistory(vt.HistoryConfig{MaxRows: 256, ChunkRows: 256})
394	for range 256 {
395		requireNoError(t, history.Append([]renderer.Cell{{Rune: 'x'}}, vt.LineBound{End: 1}))
396	}
397	sealed, tail, err := vt.MarshalSealedHistory(history.SealAndView())
398	requireNoError(t, err)
399	transcript := transcriptBlob(t, [][]renderer.Cell{{{Rune: 'v'}}})
400
401	const paneCount = maxSnapshotRows/256 + 1
402	tabs := make([]Tab, paneCount)
403	for i := range tabs {
404		tabs[i] = Tab{Panes: []Pane{{ID: layout.PaneID(strconv.Itoa(i)), SealedChunks: sealed, Tail: tail, Transcript: transcript}}}
405	}
406	data, err := marshalTest(Session{Name: "over-budget", Tabs: tabs})
407	requireNoError(t, err)
408
409	allocs := testing.AllocsPerRun(3, func() {
410		_, err := Unmarshal(data)
411		if !errors.Is(err, ErrInvalidData) {
412			panic(err)
413		}
414	})
415	// The error and testing harness may allocate a small fixed amount. Rejecting
416	// before semantic maps/slices are built must not scale with paneCount.
417	if allocs > 8 {
418		t.Fatalf("Unmarshal() allocations = %f, want fixed overhead only", allocs)
419	}
420}
421
422func TestPreflightBudgetRejectsAggregateDecodedAllocation(t *testing.T) {
423	budget := preflightBudget{alloc: maxSnapshotDecodedAllocation - 1}
424	if budget.addProduct(1, 2) {
425		t.Fatal("allocation budget accepted an over-limit aggregate")
426	}
427}
428
429func TestPreflightRejectsBroadTreeBeforeSecondPassAllocation(t *testing.T) {
430	body := v4WideTreeManifest(32_769)
431	if err := preflightSession(body); !errors.Is(err, ErrInvalidData) {
432		t.Fatalf("preflightSession() error = %v, want %v", err, ErrInvalidData)
433	}
434}
435
436func TestUnmarshalRejectsAggregateOpaqueBlobDeclarations(t *testing.T) {
437	body := v4Manifest(math.MaxUint32, nil, nil, math.MaxUint32, nil, math.MaxUint32, nil)
438	reject(t, v4Envelope(version, 0, body), ErrInvalidData)
439}
440
441func mustHistoryTail(t *testing.T) []byte {
442	t.Helper()
443	_, tail := historyBlobs(t, nil)
444	return tail
445}
446
447func requireNoError(t *testing.T, err error) {
448	t.Helper()
449	if err != nil {
450		t.Fatal(err)
451	}
452}
453
454func historyBlobs(t *testing.T, rows [][]renderer.Cell) ([][]byte, []byte) {
455	t.Helper()
456	h := vt.NewHistory(vt.HistoryConfig{MaxRows: 128, ChunkRows: 2})
457	for _, row := range rows {
458		requireNoError(t, h.Append(row, vt.LineBound{End: len(row)}))
459	}
460	sealed, tail, err := vt.MarshalSealedHistory(h.SealAndView())
461	if err != nil {
462		t.Fatal(err)
463	}
464	return sealed, tail
465}
466func transcriptBlob(t *testing.T, rows [][]renderer.Cell) []byte {
467	t.Helper()
468	history := vt.NewHistory(vt.HistoryConfig{MaxRows: max(1, len(rows)), ChunkRows: 2})
469	for _, row := range rows {
470		requireNoError(t, history.Append(row, vt.LineBound{End: len(row)}))
471	}
472	blob, err := vt.MarshalHistory(history.SealAndView())
473	if err != nil {
474		t.Fatal(err)
475	}
476	return blob
477}
478func requireV4Envelope(t *testing.T, data []byte) {
479	t.Helper()
480	if version != 5 || len(data) < 16 || binary.BigEndian.Uint16(data[4:6]) != 5 || binary.BigEndian.Uint16(data[6:8]) != 0 {
481		t.Fatalf("not v4 envelope: % x", data)
482	}
483}
484func reject(t *testing.T, data []byte, want error) {
485	t.Helper()
486	defer func() {
487		if r := recover(); r != nil {
488			t.Fatalf("panic: %v", r)
489		}
490	}()
491	_, err := Unmarshal(data)
492	if err == nil {
493		t.Fatal("accepted malformed input")
494	}
495	if want != nil && !errors.Is(err, want) {
496		t.Fatalf("error = %v, want %v", err, want)
497	}
498}
499func v4Envelope(v, flags uint16, body []byte) []byte {
500	return v4EnvelopeWithLength(v, flags, body, uint32(len(body)))
501}
502func v4EnvelopeWithLength(v, flags uint16, body []byte, length uint32) []byte {
503	out := make([]byte, 16+len(body))
504	copy(out, magic)
505	binary.BigEndian.PutUint16(out[4:6], v)
506	binary.BigEndian.PutUint16(out[6:8], flags)
507	binary.BigEndian.PutUint32(out[8:12], length)
508	binary.BigEndian.PutUint32(out[12:16], crc32.ChecksumIEEE(body))
509	copy(out[16:], body)
510	return out
511}
512
513func v4ActiveManifest(active, tabs uint16) []byte {
514	var b []byte
515	putS := func(s string) { b = binary.BigEndian.AppendUint16(b, uint16(len(s))); b = append(b, s...) }
516	putS("s")
517	b = append(b, make([]byte, 8)...)
518	b = binary.BigEndian.AppendUint16(b, active)
519	b = binary.BigEndian.AppendUint16(b, tabs)
520	for range tabs {
521		putS("")
522		b = append(b, make([]byte, 2+2+8)...)
523		putS("")
524		b = append(b, 3) // nil root
525		b = binary.BigEndian.AppendUint16(b, 0)
526	}
527	return b
528}
529
530// v4Manifest creates just enough session/tab/pane framing for hostile declarations.
531// v4WideTreeManifest declares a broad but compact tree without allocating a
532// matching layout.Node graph. It exercises preflight's manifest budget.
533func v4WideTreeManifest(children uint16) []byte {
534	var b []byte
535	putS := func(s string) { b = binary.BigEndian.AppendUint16(b, uint16(len(s))); b = append(b, s...) }
536	putS("s")
537	b = append(b, make([]byte, 8+2)...)
538	b = binary.BigEndian.AppendUint16(b, 1)
539	putS("")
540	b = append(b, make([]byte, 2+2+8)...)
541	putS("")
542	b = append(b, 1, byte(layout.Horizontal)) // split node
543	b = binary.BigEndian.AppendUint16(b, children)
544	for range children {
545		b = append(b, 0) // leaf
546		putS("")
547	}
548	b = binary.BigEndian.AppendUint16(b, 0) // panes
549	return b
550}
551
552func v4Manifest(sealed uint32, sealedLens []uint32, sealedData []byte, tailLen uint32, tailData []byte, transcriptLen uint32, transcriptData []byte) []byte {
553	var b []byte
554	putS := func(s string) { b = binary.BigEndian.AppendUint16(b, uint16(len(s))); b = append(b, s...) }
555	putS("s")
556	b = append(b, make([]byte, 8+2)...)
557	b = binary.BigEndian.AppendUint16(b, 1)
558	putS("")
559	b = append(b, make([]byte, 2+2+8)...)
560	putS("p")
561	b = append(b, 0)
562	putS("p")
563	b = binary.BigEndian.AppendUint16(b, 1)
564	putS("p")
565	putS("")
566	putS("")
567	b = binary.BigEndian.AppendUint32(b, sealed)
568	for _, l := range sealedLens {
569		b = binary.BigEndian.AppendUint32(b, l)
570	}
571	b = append(b, sealedData...)
572	b = binary.BigEndian.AppendUint32(b, tailLen)
573	b = append(b, tailData...)
574	b = binary.BigEndian.AppendUint32(b, transcriptLen)
575	b = append(b, transcriptData...)
576	return b
577}

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