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