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}