v0.9.1 ece7682

attachment_output_test.go RawHistoryFolder

1package daemon
2
3import (
4	"errors"
5	"runtime"
6	"testing"
7
8	vt "github.com/bnema/vev-vt"
9	renderer "github.com/bnema/vev-vt/ansi"
10	"github.com/bnema/vev-vt/graphics"
11	"github.com/bnema/vev/internal/domain"
12	"github.com/bnema/vev/internal/domain/layout"
13	"github.com/bnema/vev/internal/domain/terminalcap"
14	"github.com/bnema/vev/internal/protocol"
15	"github.com/bnema/vev/internal/protocol/wire"
16	"github.com/stretchr/testify/require"
17)
18
19func TestAttachmentOutputBuildsPipelinedDependencyChain(t *testing.T) {
20	stream := newOutputStateStream()
21	firstFrame := renderer.NewFrame(3, 1)
22	fillOutputStateRows(firstFrame, []string{"abc"})
23	first, ok, err := drawOutputState(t, stream, firstFrame, nil, true, 7)
24	require.NoError(t, err)
25	require.True(t, ok)
26	firstOut := unmarshalTestOutput(t, first.Payload)
27	require.Equal(t, uint64(0), firstOut.Base)
28	require.Equal(t, uint64(1), firstOut.New)
29	require.Equal(t, uint64(7), firstOut.Echo)
30
31	secondFrame := firstFrame.Clone()
32	secondFrame.Set(0, 0, renderer.Cell{Rune: 'x', Style: renderer.DefaultStyle()})
33	second, ok, err := drawOutputState(t, stream, secondFrame, []renderer.Damage{{Kind: renderer.DamageText, Width: 1, Height: 1}}, false, 8)
34	require.NoError(t, err)
35	require.True(t, ok)
36	secondOut := unmarshalTestOutput(t, second.Payload)
37	require.Equal(t, firstOut.New, secondOut.Base)
38	require.Equal(t, uint64(2), secondOut.New)
39	require.Equal(t, uint64(2), stream.outstanding())
40}
41
42func TestAttachmentOutputUsesTerminalColorProfile(t *testing.T) {
43	frame := renderer.NewFrame(1, 1)
44	frame.Set(0, 0, renderer.Cell{Rune: 'X', Style: renderer.Style{
45		Foreground:           -1,
46		Background:           -1,
47		HasForegroundRGB:     true,
48		ForegroundRGB:        renderer.RGB{R: 255, G: 0, B: 0},
49		HasBackgroundRGB:     true,
50		BackgroundRGB:        renderer.RGB{R: 0, G: 0, B: 255},
51		HasUnderlineColorRGB: true,
52		UnderlineColorRGB:    renderer.RGB{R: 0, G: 255, B: 0},
53	}})
54
55	cases := []struct {
56		name         string
57		capabilities terminalcap.Capabilities
58		want         string
59	}{
60		{
61			name:         "truecolor",
62			capabilities: terminalcap.Capabilities{Color: terminalcap.ColorCapabilities{Mode: terminalcap.TrueColor}},
63			want:         "\x1b[1;1H\x1b[0;38;2;255;0;0;48;2;0;0;255;58;2;0;255;0mX\x1b[0m",
64		},
65		{
66			name:         "indexed 256",
67			capabilities: terminalcap.Capabilities{Color: terminalcap.ColorCapabilities{Mode: terminalcap.ANSI256}},
68			want:         "\x1b[1;1H\x1b[0;38;5;196;48;5;21;58;5;46mX\x1b[0m",
69		},
70	}
71
72	for _, tc := range cases {
73		t.Run(tc.name, func(t *testing.T) {
74			stream := newOutputStateStreamForCapabilities(tc.capabilities)
75			prepared, err := stream.prepare(frame, nil, true)
76			require.NoError(t, err)
77			require.Equal(t, tc.want, string(prepared.data))
78		})
79	}
80}
81
82func TestAttachmentOutputCapacityProbeDoesNotRaceWithSend(t *testing.T) {
83	stream := newOutputStateStream(1)
84	frame := renderer.NewFrame(3, 1)
85	fillOutputStateRows(frame, []string{"abc"})
86	prepared, err := stream.prepareTestOutput(frame, nil, true)
87	require.NoError(t, err)
88
89	entered := make(chan struct{})
90	release := make(chan struct{})
91	sendDone := make(chan error, 1)
92	go func() {
93		sendDone <- prepared.send(prepared.data, 0, outputFrameSender(func(wire.Envelope) error {
94			close(entered)
95			<-release
96			return nil
97		}))
98	}()
99	awaitTestCompletion(t, entered, "output send was not admitted")
100
101	probed := make(chan struct{})
102	go func() {
103		for range 100_000 {
104			_ = stream.atCapacity()
105			runtime.Gosched()
106		}
107		close(probed)
108	}()
109	close(release)
110	require.NoError(t, <-sendDone)
111	<-probed
112	require.True(t, stream.atCapacity())
113
114	stream.ack(1, 1)
115	require.False(t, stream.atCapacity())
116	stream.rebase()
117	require.False(t, stream.atCapacity())
118}
119
120func TestAttachmentOutputEpochsAreAttachmentLocalAndPreparedFramesAreFenced(t *testing.T) {
121	frame := renderer.NewFrame(3, 1)
122	fillOutputStateRows(frame, []string{"abc"})
123	newAttachment := func() *attachmentOutput {
124		stream := newOutputStateStream()
125		ac := &attachedClient{output: stream, geometry: domain.Geometry{Size: domain.Size{Cols: 3, Rows: 1}}}
126		stream.attachment = ac
127		return stream
128	}
129
130	first := newAttachment()
131	second := newAttachment()
132	firstPrepared, err := first.prepareTestOutput(frame, nil, true)
133	require.NoError(t, err)
134	var firstFrame wire.Envelope
135	require.NoError(t, firstPrepared.send(firstPrepared.data, 0, outputFrameSender(func(frame wire.Envelope) error {
136		firstFrame = frame
137		return nil
138	})))
139	firstOutput := unmarshalTestOutput(t, firstFrame.Payload)
140	require.Equal(t, uint64(1), firstOutput.Epoch)
141	require.Equal(t, uint64(1), firstOutput.New)
142	require.True(t, firstOutput.Full)
143	first.ack(firstOutput.Epoch, firstOutput.New)
144
145	secondPrepared, err := second.prepareTestOutput(frame, nil, true)
146	require.NoError(t, err)
147	require.NoError(t, secondPrepared.send(secondPrepared.data, 0, outputFrameSender(func(wire.Envelope) error { return nil })))
148	require.Equal(t, uint64(1), second.epoch)
149	require.Equal(t, uint64(1), second.next)
150	require.Equal(t, uint64(1), first.next)
151
152	changed := frame.Clone()
153	changed.Set(0, 0, renderer.Cell{Rune: 'x', Style: renderer.DefaultStyle()})
154	pending, err := first.prepareTestOutput(changed, []renderer.Damage{{Kind: renderer.DamageText, Width: 1, Height: 1}}, false)
155	require.NoError(t, err)
156	first.rebase()
157	require.Equal(t, uint64(2), first.epoch)
158	require.Zero(t, first.next)
159	require.Zero(t, first.acked)
160	var staleSent bool
161	require.NoError(t, pending.send(pending.data, 0, outputFrameSender(func(wire.Envelope) error {
162		staleSent = true
163		return nil
164	})))
165	require.False(t, staleSent)
166	require.Zero(t, first.next)
167	first.ack(1, 1)
168	require.Zero(t, first.acked, "an ACK from the retired epoch must not advance the new epoch")
169
170	reset, err := first.prepareTestOutput(changed, []renderer.Damage{{Kind: renderer.DamageText, Width: 1, Height: 1}}, false)
171	require.NoError(t, err)
172	var resetFrame wire.Envelope
173	require.NoError(t, reset.send(reset.data, 0, outputFrameSender(func(frame wire.Envelope) error {
174		resetFrame = frame
175		return nil
176	})))
177	resetOutput := unmarshalTestOutput(t, resetFrame.Payload)
178	require.Equal(t, uint64(2), resetOutput.Epoch)
179	require.Zero(t, resetOutput.Base)
180	require.Equal(t, uint64(1), resetOutput.New)
181	require.True(t, resetOutput.Full)
182}
183
184func TestPreparedOutputDropsReplacedConnectionAndView(t *testing.T) {
185	for _, tt := range []struct {
186		name   string
187		change func(*attachedClient)
188	}{
189		{name: "connection generation", change: func(ac *attachedClient) { ac.lifecycle.generation.Add(1) }},
190		{name: "view revision", change: func(ac *attachedClient) {
191			ac.viewMu.Lock()
192			ac.view.revision++
193			ac.viewMu.Unlock()
194		}},
195	} {
196		t.Run(tt.name, func(t *testing.T) {
197			stream := newOutputStateStream()
198			ac := &attachedClient{output: stream, geometry: domain.Geometry{Size: domain.Size{Cols: 3, Rows: 1}}}
199			stream.attachment = ac
200			frame := renderer.NewFrame(3, 1)
201			fillOutputStateRows(frame, []string{"abc"})
202			initial, err := stream.prepareTestOutput(frame, nil, true)
203			require.NoError(t, err)
204			require.NoError(t, initial.send(initial.data, 0, outputFrameSender(func(wire.Envelope) error { return nil })))
205			changed := frame.Clone()
206			changed.Set(0, 0, renderer.Cell{Rune: 'x', Style: renderer.DefaultStyle()})
207			pending, err := stream.prepareTestOutput(changed, []renderer.Damage{{Kind: renderer.DamageText, Width: 1, Height: 1}}, false)
208			require.NoError(t, err)
209			tt.change(ac)
210			var sent bool
211			require.NoError(t, pending.send(pending.data, 0, outputFrameSender(func(wire.Envelope) error {
212				sent = true
213				return nil
214			})))
215			require.False(t, sent)
216			require.Equal(t, uint64(1), stream.next)
217
218			retry, err := stream.prepareTestOutput(changed, []renderer.Damage{{Kind: renderer.DamageText, Width: 1, Height: 1}}, false)
219			require.NoError(t, err)
220			require.NoError(t, retry.send(retry.data, 0, outputFrameSender(func(wire.Envelope) error { return nil })))
221			require.Equal(t, uint64(2), stream.next)
222		})
223	}
224}
225
226func TestOutputStateAckRequiresCurrentEpoch(t *testing.T) {
227	stream := newOutputStateStream()
228	stream.next = 2
229	stream.ack(2, 1)
230	require.Zero(t, stream.acked, "a stale epoch must not retire output state")
231	stream.ack(1, 1)
232	require.Equal(t, uint64(1), stream.acked)
233	stream.ack(1, 3)
234	require.Equal(t, uint64(1), stream.acked, "a future state must not advance the ACK window")
235}
236
237func TestOutputStateSideEffectsDoNotAdvanceEpochStateOrACK(t *testing.T) {
238	stream := newOutputStateStream()
239	stream.ack(1, 0)
240	beforeEpoch, beforeNext, beforeAck := stream.epoch, stream.next, stream.acked
241	out, err := stream.sideEffect([]byte("pty"), 9)
242	require.NoError(t, err)
243	require.Equal(t, beforeEpoch, out.Epoch)
244	require.Zero(t, out.Base)
245	require.Zero(t, out.New)
246	require.False(t, out.Full)
247	require.Equal(t, uint64(9), out.Echo)
248	require.Equal(t, beforeNext, stream.next)
249	require.Equal(t, beforeAck, stream.acked)
250}
251
252func TestAttachmentOutputFailedSendRetriesSnapshotWithoutAdvancing(t *testing.T) {
253	stream := newOutputStateStream()
254	initial := renderer.NewFrame(3, 1)
255	fillOutputStateRows(initial, []string{"abc"})
256	first, err := stream.prepareTestOutput(initial, nil, true)
257	require.NoError(t, err)
258	require.NoError(t, first.send(first.data, 0, outputFrameSender(func(wire.Envelope) error { return nil })))
259	require.Equal(t, uint64(1), stream.next)
260
261	changed := initial.Clone()
262	changed.Set(0, 0, renderer.Cell{Rune: 'x', Style: renderer.DefaultStyle()})
263	pending, err := stream.prepareTestOutput(changed, []renderer.Damage{{Kind: renderer.DamageText, Width: 1, Height: 1}}, false)
264	require.NoError(t, err)
265	probe, err := stream.renderer.Prepare(initial, nil, false)
266	require.NoError(t, err)
267	require.Empty(t, probe.Bytes(), "preparation must not advance the renderer shadow")
268
269	sendErr := errors.New("send failed")
270	require.ErrorIs(t, pending.send(pending.data, 0, outputFrameSender(func(wire.Envelope) error { return sendErr })), sendErr)
271	require.Equal(t, uint64(1), stream.next, "failed send must not advance the state chain")
272	pending.commitNoSend()
273	probe, err = stream.renderer.Prepare(initial, nil, false)
274	require.NoError(t, err)
275	require.Empty(t, probe.Bytes(), "failed send must retain the committed renderer shadow")
276
277	retry, err := stream.prepareTestOutput(changed, []renderer.Damage{{Kind: renderer.DamageText, Width: 1, Height: 1}}, false)
278	require.NoError(t, err)
279	var sent wire.Envelope
280	require.NoError(t, retry.send(retry.data, 0, outputFrameSender(func(frame wire.Envelope) error {
281		sent = frame
282		return nil
283	})))
284	out := unmarshalTestOutput(t, sent.Payload)
285	require.Zero(t, out.Base, "retry after ambiguous send failure must be dependency-free")
286	require.Equal(t, uint64(2), out.New, "state numbers remain monotonic across rebases")
287	require.Equal(t, uint64(2), stream.next, "successful retry advances the chain exactly once")
288	require.NoError(t, retry.send(retry.data, 0, outputFrameSender(func(wire.Envelope) error {
289		t.Fatal("completed output sent twice")
290		return nil
291	})))
292	require.Equal(t, uint64(2), stream.next)
293	probe, err = stream.renderer.Prepare(changed, nil, false)
294	require.NoError(t, err)
295	require.Empty(t, probe.Bytes(), "successful retry commits the candidate shadow")
296}
297
298func TestAttachmentOutputNoByteCommitAdvancesShadowWithoutState(t *testing.T) {
299	stream := newOutputStateStream()
300	frame := renderer.NewFrame(3, 1)
301	fillOutputStateRows(frame, []string{"abc"})
302	initial, err := stream.prepareTestOutput(frame, nil, true)
303	require.NoError(t, err)
304	require.NoError(t, initial.send(initial.data, 0, outputFrameSender(func(wire.Envelope) error { return nil })))
305
306	noOp, err := stream.prepare(frame, nil, false)
307	require.NoError(t, err)
308	require.Empty(t, noOp.data)
309	noOp.commitNoSend()
310	noOp.commitNoSend()
311	require.Equal(t, uint64(1), stream.next, "no-byte commit must not create a state frame")
312
313	probe, err := stream.renderer.Prepare(frame, nil, false)
314	require.NoError(t, err)
315	require.Empty(t, probe.Bytes(), "no-byte commit retains the prepared visual candidate")
316}
317
318func TestAttachmentOutputRepeatedUnackedScrollRemainsClientCorrect(t *testing.T) {
319	stream := newOutputStateStream()
320	client := vt.NewScreen(4, 3)
321	initial := renderer.NewFrame(4, 3)
322	fillOutputStateRows(initial, []string{"AAAA", "BBBB", "CCCC"})
323	first, ok, err := drawOutputState(t, stream, initial, nil, true, 0)
324	require.NoError(t, err)
325	require.True(t, ok)
326	mustApplyOutput(t, client, first)
327	stream.ack(1, 1)
328
329	scrolled := renderer.NewFrame(4, 3)
330	fillOutputStateRows(scrolled, []string{"BBBB", "CCCC", "N   "})
331	damage := []renderer.Damage{
332		{Kind: renderer.DamageScrollUp, X: 0, Y: 0, Width: 4, Height: 3, Count: 1},
333		{Kind: renderer.DamageText, X: 0, Y: 2, Width: 4, Height: 1},
334	}
335	second, ok, err := drawOutputState(t, stream, scrolled, damage, false, 0)
336	require.NoError(t, err)
337	require.True(t, ok)
338	mustApplyOutput(t, client, second)
339
340	// The renderer baseline is the preceding emitted frame, so repeating stale
341	// scroll damage falls back to an overwrite instead of applying a second
342	// incompatible scroll program.
343	third, ok, err := drawOutputState(t, stream, scrolled, damage, false, 0)
344	require.NoError(t, err)
345	require.True(t, ok)
346	mustApplyOutput(t, client, third)
347	for y, want := range []string{"BBBB", "CCCC", "N   "} {
348		require.Equal(t, want, outputStateRow(client.RowCells(y)))
349	}
350}
351
352func (s *attachmentOutput) render(frame renderer.Frame, damage []renderer.Damage, reset bool) ([]byte, error) {
353	prepared, err := s.prepare(frame, damage, reset)
354	if err != nil {
355		return nil, err
356	}
357	if len(prepared.data) == 0 {
358		prepared.commitNoSend()
359		return nil, nil
360	}
361	// This helper tests renderer replay bytes without publishing a wire frame.
362	// Production state advancement happens only through preparedOutput.send.
363	prepared.draw.Commit()
364	s.forceSnapshot = false
365	return prepared.data, nil
366}
367
368func outputStateFrame(stream *attachmentOutput, data []byte, reset bool, echoAck uint64) wire.Envelope {
369	stream.next++
370	base := stream.next - 1
371	if reset {
372		base = 0
373	}
374	context := publicationTestContext()
375	context.Publication = stream.next
376	output := protocol.Output{
377		Epoch: 1, Base: base, New: stream.next, Echo: echoAck,
378		Size: domain.Size{Cols: 1, Rows: 1}, Full: reset, Data: data, Context: &context,
379	}
380	return mustServerEnvelope(output)
381}
382
383// prepareTestOutput gives replay-only fixtures explicit semantic ownership;
384// production obtains this from capturedRenderState in prepareFrame.
385func (s *attachmentOutput) prepareTestOutput(frame renderer.Frame, damage []renderer.Damage, reset bool) (*preparedOutput, error) {
386	prepared, err := s.prepare(frame, damage, reset)
387	if err == nil {
388		context := publicationTestContext()
389		prepared.context = &context
390	}
391	return prepared, err
392}
393
394func drawOutputState(t *testing.T, stream *attachmentOutput, frame renderer.Frame, damage []renderer.Damage, reset bool, echoAck uint64) (wire.Envelope, bool, error) {
395	t.Helper()
396	prepared, err := stream.prepareTestOutput(frame, damage, reset)
397	if err != nil || len(prepared.data) == 0 {
398		if prepared != nil {
399			prepared.commitNoSend()
400		}
401		return wire.Envelope{}, false, err
402	}
403	var output wire.Envelope
404	err = prepared.send(prepared.data, echoAck, outputFrameSender(func(frame wire.Envelope) error {
405		output = frame
406		return nil
407	}))
408	if err != nil {
409		return wire.Envelope{}, false, err
410	}
411	return output, true, nil
412}
413
414// A resize is a state reset, but it must not turn subsequent idle or damaged
415// renders into full frames.
416func TestAttachmentOutputResizeFrameThenNoopAndDamageAreDifferential(t *testing.T) {
417	screen := vt.NewScreen(4, 2)
418	screen.Write([]byte("abcd"))
419	screen.ClearDamage()
420	screen.Resize(6, 2)
421
422	stream := newOutputStateStream()
423	resized, ok, err := drawOutputState(t, stream, captureTestFrame(screen), screen.Damage(), true, 0)
424	require.NoError(t, err)
425	require.True(t, ok)
426	resizeOutput := unmarshalTestOutput(t, resized.Payload)
427	require.Zero(t, resizeOutput.Base, "resize must emit the one reset frame")
428	screen.ClearDamage()
429
430	_, ok, err = drawOutputState(t, stream, captureTestFrame(screen), screen.Damage(), false, 0)
431	require.NoError(t, err)
432	require.False(t, ok, "an unchanged render after resize must emit nothing")
433
434	screen.Write([]byte("\x1b[1;1Hx"))
435	damaged, ok, err := drawOutputState(t, stream, captureTestFrame(screen), []renderer.Damage{{Kind: renderer.DamageText, Width: 1, Height: 1}}, false, 0)
436	require.NoError(t, err)
437	require.True(t, ok)
438	damageOutput := unmarshalTestOutput(t, damaged.Payload)
439	require.Equal(t, resizeOutput.New, damageOutput.Base, "later damage must remain incremental")
440}
441
442func TestAttachmentOutputFailedSendKeepsTextCursorAndGraphicsSpeculative(t *testing.T) {
443	scene := graphics.NewScene(graphics.Limits{})
444	asset, err := scene.AddAsset(graphics.AssetBlob{Encoded: []byte("asset"), Width: 1, Height: 1})
445	require.NoError(t, err)
446	_, err = scene.PlaceAsset(asset, graphics.PixelRect{Width: 1, Height: 1})
447	require.NoError(t, err)
448	state := &capturedRenderState{
449		route: protocol.CommittedRouteIdentity{Target: protocol.ExactSessionTarget{LifecycleID: domain.SessionLifecycleID{1}, SessionName: "work"}},
450		view:  attachmentView{tabID: "tab-1"},
451		panes: []capturedPaneRenderState{{
452			stableID: "pane-1", focused: true,
453			graphics:         scene.Snapshot(),
454			graphicsGeometry: domain.Geometry{Size: domain.Size{Cols: 1, Rows: 1}, PixelWidth: 1, PixelHeight: 1},
455			placement:        layout.Placement{Content: domain.Rect{Width: 1, Height: 1}},
456		}}}
457	graphicsState := newGraphicsOutputState()
458	output := attachmentOutputWithGraphics(graphicsState)
459	output.lastCursor = cursorOut{valid: true, row: 1, col: 1}
460	ac := &attachedClient{output: output, terminalCapabilities: terminalcap.Capabilities{KittyGraphics: true}}
461	output.attachment = ac
462	frame := renderer.NewFrame(1, 1)
463	prepared, err := output.prepareFrame(nil, state, frame, []renderer.Damage{renderer.FullRedraw()}, true, cursorOut{row: 2, col: 3})
464	require.NoError(t, err)
465
466	sendErr := errors.New("send failed")
467	require.ErrorIs(t, prepared.send(0, outputFrameSender(func(wire.Envelope) error { return sendErr })), sendErr)
468
469	require.Equal(t, cursorOut{valid: true, row: 1, col: 1}, output.lastCursor)
470	require.Empty(t, graphicsState.assets)
471	require.Empty(t, graphicsState.placements)
472	require.NotEmpty(t, graphicsState.pendingImages)
473	require.NotEmpty(t, graphicsState.pendingPlaces)
474}
475
476func TestAttachmentOutputRebaseRetiresAttachmentState(t *testing.T) {
477	output := newOutputStateStream()
478	output.next = 3
479	output.acked = 2
480	output.lastCursor = cursorOut{valid: true, row: 2, col: 3}
481	output.lastRoutePosition = protocol.RoutePosition{ActiveTabID: "tab"}
482
483	output.rebaseAttachment()
484
485	require.Equal(t, uint64(2), output.epoch)
486	require.Zero(t, output.next)
487	require.Zero(t, output.acked)
488	require.True(t, output.forceSnapshot)
489	require.Equal(t, cursorOut{valid: true, row: 2, col: 3}, output.lastCursor, "the forced frame will reassert cursor state")
490	require.Equal(t, protocol.RoutePosition{}, output.lastRoutePosition)
491}
492
493func TestAttachmentOutputDefaultsAndNormalizesWindow(t *testing.T) {
494	for _, tt := range []struct {
495		name   string
496		window []uint8
497		want   uint64
498	}{
499		{name: "omitted", want: 8},
500		{name: "zero", window: []uint8{0}, want: 8},
501		{name: "oversized", window: []uint8{9}, want: 8},
502		{name: "exact window", window: []uint8{1}, want: 1},
503	} {
504		t.Run(tt.name, func(t *testing.T) {
505			require.Equal(t, tt.want, newOutputStateStream(tt.window...).maxOutstanding)
506		})
507	}
508
509	output := newOutputStateStream()
510	output.setWindow(2)
511	require.Equal(t, uint64(2), output.maxOutstanding)
512	require.Equal(t, uint64(2), output.maxOutstandingAtomic.Load())
513}
514
515func TestNormalizeOutputWindow(t *testing.T) {
516	for _, tt := range []struct {
517		input, want uint8
518	}{{0, 8}, {1, 1}, {8, 8}, {9, 8}, {255, 8}} {
519		require.Equal(t, tt.want, normalizeOutputWindow(tt.input))
520	}
521}
522
523// TestAttachmentOutputByteWindow covers the unacknowledged byte budget that
524// runs alongside the frame window: it fills on large frames and side effects,
525// never blocks the only frame that can release it, and an ACK or a new epoch
526// releases exactly what it covers.
527func TestAttachmentOutputByteWindow(t *testing.T) {
528	const big = protocol.MaxOutputWindowBytes / 2
529	type step struct {
530		frame      int // state frame of this many data bytes
531		sideEffect int // side effect of this many data bytes
532		ack        uint64
533		rebase     bool
534	}
535	tests := []struct {
536		name         string
537		steps        []step
538		wantBytes    uint64
539		wantCapacity bool
540	}{
541		{name: "small frames stay under the byte window", steps: []step{{frame: 100}, {frame: 100}}, wantBytes: 200},
542		{name: "large frames fill the byte window", steps: []step{{frame: big}, {frame: big}}, wantBytes: 2 * big, wantCapacity: true},
543		{name: "one oversized frame in flight blocks the next", steps: []step{{frame: protocol.MaxOutputWindowBytes + 1}}, wantBytes: protocol.MaxOutputWindowBytes + 1, wantCapacity: true},
544		{name: "side effects alone never block a state frame", steps: []step{{sideEffect: protocol.MaxOutputWindowBytes}}, wantBytes: protocol.MaxOutputWindowBytes},
545		{name: "side effects count once a frame is in flight", steps: []step{{frame: 100}, {sideEffect: protocol.MaxOutputWindowBytes}}, wantBytes: protocol.MaxOutputWindowBytes + 100, wantCapacity: true},
546		{name: "ack releases the frames it covers", steps: []step{{frame: big}, {frame: big}, {ack: 1}}, wantBytes: big},
547		{name: "a side effect is released by the next state's ack", steps: []step{{sideEffect: 50}, {frame: 100}, {ack: 1}}, wantBytes: 0},
548		{name: "a new epoch forgets every charge", steps: []step{{frame: big}, {frame: big}, {rebase: true}}, wantBytes: 0},
549	}
550	for _, tt := range tests {
551		t.Run(tt.name, func(t *testing.T) {
552			stream := newOutputStateStream()
553			stream.ack(1, 0)
554			for _, s := range tt.steps {
555				switch {
556				case s.frame > 0:
557					stream.lockView()
558					stream.next++
559					stream.chargeBytesLocked(stream.next, s.frame)
560					stream.publishOutstanding()
561					stream.unlockView()
562				case s.sideEffect > 0:
563					out, err := stream.sideEffect(make([]byte, s.sideEffect), 0)
564					require.NoError(t, err)
565					stream.lockView()
566					stream.sideEffectSentLocked(out)
567					stream.unlockView()
568				case s.ack > 0:
569					require.True(t, stream.ack(stream.currentEpoch(), s.ack))
570				case s.rebase:
571					stream.rebase()
572				}
573			}
574			require.Equal(t, tt.wantBytes, stream.unackedBytesAtomic.Load())
575			require.Equal(t, tt.wantCapacity, stream.atCapacity())
576		})
577	}
578}
579
580// TestAttachmentOutputFullByteWindowKeepsImages proves the byte window gates
581// when a frame goes out, never what it carries: an upload must fit one frame,
582// so trimming images to the remaining byte budget would drop them for good.
583func TestAttachmentOutputFullByteWindowKeepsImages(t *testing.T) {
584	scene := graphics.NewScene(graphics.Limits{})
585	asset, err := scene.AddAsset(graphics.AssetBlob{Encoded: []byte("asset"), Width: 1, Height: 1})
586	require.NoError(t, err)
587	_, err = scene.PlaceAsset(asset, graphics.PixelRect{Width: 1, Height: 1})
588	require.NoError(t, err)
589	state := &capturedRenderState{
590		route: protocol.CommittedRouteIdentity{Target: protocol.ExactSessionTarget{LifecycleID: domain.SessionLifecycleID{1}, SessionName: "work"}},
591		view:  attachmentView{tabID: "tab-1"},
592		panes: []capturedPaneRenderState{{
593			stableID: "pane-1", focused: true,
594			graphics:         scene.Snapshot(),
595			graphicsGeometry: domain.Geometry{Size: domain.Size{Cols: 1, Rows: 1}, PixelWidth: 1, PixelHeight: 1},
596			placement:        layout.Placement{Content: domain.Rect{Width: 1, Height: 1}},
597		}}}
598	output := attachmentOutputWithGraphics(newGraphicsOutputState())
599	ac := &attachedClient{output: output, terminalCapabilities: terminalcap.Capabilities{KittyGraphics: true}}
600	output.attachment = ac
601	// Side effects filled the byte window while no state frame is in flight.
602	output.lockView()
603	output.chargeBytesLocked(output.next+1, protocol.MaxOutputWindowBytes)
604	output.unlockView()
605	require.False(t, output.atCapacity(), "the only frame that can release side-effect bytes is allowed")
606
607	prepared, err := output.prepareFrame(nil, state, renderer.NewFrame(1, 1), []renderer.Damage{renderer.FullRedraw()}, true, cursorOut{})
608	require.NoError(t, err)
609	require.NotNil(t, prepared.graphics, "a full byte window must not drop the frame's images")
610	require.NotEmpty(t, prepared.graphics.data)
611	require.Equal(t, protocol.MaxOutputDataLen, prepared.graphics.maxBytes+len(prepared.data)-len(prepared.graphics.data),
612		"images keep the whole frame data limit")
613}
614
615func TestAttachmentOutputSentFrameChargesItsBytes(t *testing.T) {
616	stream := newOutputStateStream()
617	frame := renderer.NewFrame(3, 1)
618	fillOutputStateRows(frame, []string{"abc"})
619	prepared, err := stream.prepareTestOutput(frame, nil, true)
620	require.NoError(t, err)
621	require.NoError(t, prepared.send(prepared.data, 0, outputFrameSender(func(wire.Envelope) error { return nil })))
622	require.Equal(t, uint64(len(prepared.data)), stream.unackedBytesAtomic.Load())
623	require.True(t, stream.ack(stream.currentEpoch(), stream.next))
624	require.Zero(t, stream.unackedBytesAtomic.Load())
625
626	failed, err := stream.prepareTestOutput(frame, nil, true)
627	require.NoError(t, err)
628	require.Error(t, failed.send(failed.data, 0, outputFrameSender(func(wire.Envelope) error { return errors.New("send failed") })))
629	require.Zero(t, stream.unackedBytesAtomic.Load(), "a frame that never left must not be charged")
630}

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