extraction_contract_external_test.go RawHistoryFolder
1package vt_test
2
3import (
4 "reflect"
5 "testing"
6
7 vt "github.com/bnema/vev-vt"
8 "github.com/bnema/vev-vt/core"
9)
10
11func TestExternalScreenParserPreservesPublicTerminalModel(t *testing.T) {
12 screen := vt.NewScreen(6, 2)
13 screen.Write([]byte("A界"))
14
15 var source vt.CellSource = screen
16 if source.Columns() != 6 || source.Rows() != 2 {
17 t.Fatalf("screen dimensions = %dx%d, want 6x2", source.Columns(), source.Rows())
18 }
19 if got := source.Cell(0, 0); got.Rune != 'A' {
20 t.Fatalf("ASCII cell = %#v, want A", got)
21 }
22 if got := source.Cell(1, 0); got.Rune != '界' || got.Continuation {
23 t.Fatalf("wide-rune lead cell = %#v, want 界 lead", got)
24 }
25 if got := source.Cell(2, 0); !got.Continuation {
26 t.Fatalf("wide-rune continuation = %#v, want continuation", got)
27 }
28 owned := screen.RowCells(0)
29 owned[0] = core.BlankCell()
30 if got := source.Cell(0, 0).Rune; got != 'A' {
31 t.Fatalf("mutating RowCells result changed live screen cell to %q", got)
32 }
33 if got := vt.RuneWidth('界'); got != 2 {
34 t.Fatalf("RuneWidth(界) = %d, want 2", got)
35 }
36
37 // The parser owns incomplete escape sequences across writes, so callers can
38 // provide arbitrary PTY chunk boundaries.
39 screen.Write([]byte("\x1b[2;2H\x1b[3"))
40 screen.Write([]byte("1mX"))
41 cell := screen.Cell(1, 1)
42 if cell.Rune != 'X' || cell.Style.Foreground != 1 {
43 t.Fatalf("split ANSI write = %#v, want red X", cell)
44 }
45
46 screen.Write([]byte("\x1b]2;public title\a"))
47 if got := screen.Snapshot().Title(); got != "public title" {
48 t.Fatalf("terminal title = %q, want public title", got)
49 }
50}
51
52func TestExternalCallbacksAreSynchronousAndOrdered(t *testing.T) {
53 screen := vt.NewScreen(8, 2)
54 inWrite := false
55 var events []string
56 assertInWrite := func(name string) {
57 if !inWrite {
58 t.Errorf("%s callback ran after Write returned", name)
59 }
60 }
61
62 screen.OnBell = func() {
63 assertInWrite("bell")
64 events = append(events, "bell")
65 }
66 screen.OnResponse = func(response []byte) {
67 assertInWrite("response")
68 events = append(events, "response:"+string(response))
69 }
70 screen.OnNotify = func(title, body string) {
71 assertInWrite("notify")
72 events = append(events, "notify:"+title+":"+body)
73 }
74 screen.OnProgress = func(errored bool) {
75 assertInWrite("progress")
76 if errored {
77 events = append(events, "progress:error")
78 } else {
79 events = append(events, "progress:clear")
80 }
81 }
82 screen.OnClipboard = func(data string) {
83 assertInWrite("clipboard")
84 events = append(events, "clipboard:"+data)
85 }
86
87 inWrite = true
88 screen.Write([]byte("\a\x1b[5n\x1b]9;body\a\x1b]777;notify;title;body\a" +
89 "\x1b]9;4;1\a\x1b]9;4;0\a\x1b]9;4;2\a" +
90 "\x1b]52;c;SGVsbG8=\a"))
91 inWrite = false
92
93 want := []string{
94 "bell",
95 "response:\x1b[0n",
96 "notify::body",
97 "notify:title:body",
98 "progress:clear",
99 "progress:error",
100 "clipboard:SGVsbG8=",
101 }
102 if !reflect.DeepEqual(events, want) {
103 t.Fatalf("callback events = %#v, want %#v", events, want)
104 }
105}
106
107func TestExternalSnapshotsAndHistoryRespectOwnershipContracts(t *testing.T) {
108 screen := vt.NewScreenWithHistory(4, 2, vt.HistoryConfig{MaxRows: 8, ChunkRows: 2})
109 screen.Write([]byte("A"))
110 snapshot := screen.Snapshot()
111
112 owned := snapshot.Row(0)
113 owned[0] = core.Cell{Rune: 'X'}
114 if got := snapshot.Row(0)[0].Rune; got != 'A' {
115 t.Fatalf("owned snapshot row mutation changed snapshot to %q", got)
116 }
117
118 screen.Write([]byte("\rX"))
119 if got := snapshot.Row(0)[0].Rune; got != 'A' {
120 t.Fatalf("snapshot changed after later screen write: %q", got)
121 }
122 if got := snapshot.RowIDs(); len(got) != 2 || got[0] == 0 || got[1] == 0 {
123 t.Fatalf("snapshot row IDs = %#v, want stable nonzero IDs", got)
124 }
125 rowIDs := snapshot.RowIDs()
126 rowIDs[0] = 0
127 if snapshot.RowID(0) == 0 {
128 t.Fatal("RowIDs did not return an owned copy")
129 }
130
131 history := vt.NewHistory(vt.HistoryConfig{MaxRows: 8, ChunkRows: 2})
132 appendHistoryRow(t, history, 'a', 10)
133 appendHistoryRow(t, history, 'b', 11)
134 before := history.View()
135 chunk := before.Chunk(0)
136 if chunk == nil || before.FindRowID(11) != 1 {
137 t.Fatalf("history view = len %d chunk %#v IDs %d/%d", before.Len(), chunk, before.RowID(0), before.RowID(1))
138 }
139
140 copyOfRow := before.Row(0)
141 copyOfRow[0] = core.Cell{Rune: 'x'}
142 if got := before.Row(0)[0].Rune; got != 'a' {
143 t.Fatalf("owned history row mutation changed view to %q", got)
144 }
145
146 appendHistoryRow(t, history, 'c', 12)
147 after := history.View()
148 if after.Chunk(0) != chunk {
149 t.Fatal("sealed history chunk identity changed after an append")
150 }
151 if before.Len() != 2 || before.RowID(0) != 10 || before.RowID(1) != 11 {
152 t.Fatalf("retained history view changed after append: len=%d IDs=%d/%d", before.Len(), before.RowID(0), before.RowID(1))
153 }
154
155 persistenceView := history.SnapshotView()
156 appendHistoryRow(t, history, 'd', 13)
157 if got := persistenceView.Tail().Row(0)[0].Rune; got != 'c' {
158 t.Fatalf("snapshot tail changed after history append to %q", got)
159 }
160}
161
162func TestExternalSingleOwnerUsageSerializesMutationAndCapture(t *testing.T) {
163 screen := vt.NewScreen(4, 1)
164 operations := make(chan func(), 3)
165 done := make(chan struct{})
166 go func() {
167 defer close(done)
168 for operation := range operations {
169 operation()
170 }
171 }()
172
173 operations <- func() { screen.Write([]byte("A")) }
174 captured := make(chan vt.ScreenSnapshot, 1)
175 operations <- func() { captured <- screen.Snapshot() }
176 close(operations)
177 <-done
178
179 if got := (<-captured).Row(0)[0].Rune; got != 'A' {
180 t.Fatalf("single-owner capture = %q, want A", got)
181 }
182}
183
184func appendHistoryRow(t *testing.T, history *vt.History, runeValue rune, id vt.RowID) {
185 t.Helper()
186 if err := history.AppendWithID([]core.Cell{{Rune: runeValue}}, vt.LineBound{End: 1}, id); err != nil {
187 t.Fatalf("AppendWithID(%d): %v", id, err)
188 }
189}