v0.3.0 99dc266

final_acceptance_test.go RawHistoryFolder

1//go:build linux
2
3package app
4
5import (
6	"testing"
7	"time"
8
9	"github.com/bnema/vev/internal/domain"
10	"github.com/bnema/vev/internal/ports"
11	"github.com/bnema/vev/internal/usecase/daemon"
12	"github.com/stretchr/testify/require"
13)
14
15func sendAcceptanceInput(t *testing.T, tr ports.Transport, data string) {
16	t.Helper()
17	require.NoError(t, tr.Send(ports.Frame{Type: ports.MsgInput, Payload: ports.MarshalInput(ports.Input{Data: []byte(data)})}))
18}
19
20func awaitAcceptanceFrame(t *testing.T, p *pump, typ ports.MsgType, predicate func(ports.Frame) bool) ports.Frame {
21	t.Helper()
22	deadline := time.NewTimer(5 * time.Second)
23	defer deadline.Stop()
24	for {
25		select {
26		case frame, ok := <-p.ch:
27			require.True(t, ok, "connection closed before frame")
28			if frame.Type == typ && predicate(frame) {
29				return frame
30			}
31		case <-deadline.C:
32			t.Fatal("timed out waiting for acceptance frame")
33		}
34	}
35}
36
37func awaitAcceptanceCommand(t *testing.T, p *pump, requestID uint64) ports.CommandResult {
38	t.Helper()
39	frame := awaitAcceptanceFrame(t, p, ports.MsgCommandResult, func(frame ports.Frame) bool {
40		result, err := ports.UnmarshalCommandResult(frame.Payload)
41		require.NoError(t, err)
42		return result.RequestID == requestID
43	})
44	result, err := ports.UnmarshalCommandResult(frame.Payload)
45	require.NoError(t, err)
46	return result
47}
48
49func awaitAcceptanceOutput(t *testing.T, p *pump, predicate func(ports.Output) bool) ports.Output {
50	t.Helper()
51	frame := awaitAcceptanceFrame(t, p, ports.MsgOutput, func(frame ports.Frame) bool {
52		output, err := ports.UnmarshalOutput(frame.Payload)
53		require.NoError(t, err)
54		return predicate(output)
55	})
56	output, err := ports.UnmarshalOutput(frame.Payload)
57	require.NoError(t, err)
58	return output
59}
60
61func awaitAcceptanceCommandResult(t *testing.T, tr ports.Transport, p *pump, requestID uint64, slug string) ports.CommandResult {
62	t.Helper()
63	payload, err := ports.MarshalCommandRequest(ports.CommandRequest{
64		Version: ports.ProtocolVersion, RequestID: requestID, Attached: true, Slug: slug,
65	})
66	require.NoError(t, err)
67	require.NoError(t, tr.Send(ports.Frame{Type: ports.MsgCommand, Payload: payload}))
68	return awaitAcceptanceCommand(t, p, requestID)
69}
70
71func TestAcceptanceTwoLocalAttachmentsKeepViewsOverTransports(t *testing.T) {
72	size := domain.Size{Cols: 80, Rows: 24}
73	dir, _ := startDaemon(t, daemon.WithShell("/bin/cat", nil))
74	first, firstPump := attach(t, dir, ports.IntentNew, "shared", size)
75	second, secondPump := attach(t, dir, ports.IntentAttach, "shared", size)
76	t.Cleanup(func() { _ = first.Close() })
77	t.Cleanup(func() { _ = second.Close() })
78
79	sendAcceptanceInput(t, first, "SHARED_INPUT\n")
80	awaitText(t, firstPump, size, "SHARED_INPUT")
81	awaitText(t, secondPump, size, "SHARED_INPUT")
82
83	// A tab opened by the first attachment is shared session topology, but its
84	// selected window is not. The second attachment stays on the original tab.
85	sendAcceptanceInput(t, first, "\x1b ")
86	awaitText(t, firstPump, size, "Commands")
87	sendAcceptanceInput(t, first, "CNT\r")
88	awaitText(t, firstPump, size, " 1 (cat)  2 (cat) ")
89	assertNoTextAfterInput(t, secondPump, size, " 1 (cat)  2 (cat) ")
90
91	sendAcceptanceInput(t, first, "\x1b ")
92	awaitText(t, firstPump, size, "Commands")
93	sendAcceptanceInput(t, first, "SPR\r")
94	sendAcceptanceInput(t, first, "FIRST_PANE_INPUT\n")
95	awaitText(t, firstPump, size, "FIRST_PANE_INPUT")
96	assertNoTextAfterInput(t, secondPump, size, "FIRST_PANE_INPUT")
97	sendAcceptanceInput(t, second, "SECOND_TAB_INPUT\n")
98	awaitText(t, secondPump, size, "SECOND_TAB_INPUT")
99
100	// Overlays are attachment-owned: a palette on one wire and copy mode on
101	// the other must not repaint the peer's viewport.
102	sendAcceptanceInput(t, first, "\x1b ")
103	awaitText(t, firstPump, size, "Commands")
104	assertNoTextAfterInput(t, secondPump, size, "Commands")
105	sendAcceptanceInput(t, first, "\x1b")
106
107	sendAcceptanceInput(t, second, "\x1b ")
108	awaitText(t, secondPump, size, "Commands")
109	sendAcceptanceInput(t, second, "VIS\r")
110	awaitText(t, secondPump, size, "[SCROLL]")
111	assertNoTextAfterInput(t, firstPump, size, "[SCROLL]")
112
113	// The resize keeps its attachment-local output stream while recalculating
114	// shared PTY/content geometry from the latest valid attachment claim; the
115	// peer stream remains independent.
116	resizePayload, err := ports.MarshalResize(ports.Resize{Size: domain.Size{Cols: 100, Rows: 30}})
117	require.NoError(t, err)
118	require.NoError(t, first.Send(ports.Frame{Type: ports.MsgResize, Payload: resizePayload}))
119	resized := awaitAcceptanceOutput(t, firstPump, func(output ports.Output) bool { return output.Size == (domain.Size{Cols: 100, Rows: 30}) })
120	require.Equal(t, domain.Size{Cols: 100, Rows: 30}, resized.Size)
121	assertNoTextAfterInput(t, secondPump, size, "FIRST_PANE_INPUT")
122
123	require.NoError(t, first.Send(ports.Frame{Type: ports.MsgOutputResetRequest, Payload: ports.MarshalOutputResetRequest(ports.OutputResetRequest{})}))
124	reset := awaitAcceptanceOutput(t, firstPump, func(output ports.Output) bool { return output.Base == 0 && output.Full })
125	require.True(t, reset.Full)
126	assertNoTextAfterInput(t, secondPump, size, "FIRST_PANE_INPUT")
127
128	require.NoError(t, first.Send(ports.Frame{Type: ports.MsgDetach, Payload: ports.MarshalDetach(ports.Detach{})}))
129	deadline := time.NewTimer(5 * time.Second)
130	defer deadline.Stop()
131	for {
132		select {
133		case frame := <-firstPump.ch:
134			if frame.Type == ports.MsgDetached {
135				goto detached
136			}
137		case <-deadline.C:
138			t.Fatal("timed out waiting for first attachment detach")
139		}
140	}
141
142detached:
143	sendAcceptanceInput(t, second, "q")
144	sendAcceptanceInput(t, second, "PEER_SURVIVES\n")
145	awaitText(t, secondPump, size, "PEER_SURVIVES")
146}
147
148func TestAcceptanceAttachedCommandUsesItsConnectionOnly(t *testing.T) {
149	size := domain.Size{Cols: 80, Rows: 24}
150	dir, _ := startDaemon(t, daemon.WithShell("/bin/cat", nil))
151	first, firstPump := attach(t, dir, ports.IntentNew, "commands", size)
152	second, secondPump := attach(t, dir, ports.IntentAttach, "commands", size)
153	t.Cleanup(func() { _ = first.Close() })
154	t.Cleanup(func() { _ = second.Close() })
155
156	firstResult := awaitAcceptanceCommandResult(t, first, firstPump, 101, "new-tab")
157	secondResult := awaitAcceptanceCommandResult(t, second, secondPump, 202, "new-tab")
158	require.True(t, firstResult.OK, firstResult.Text)
159	require.True(t, secondResult.OK, secondResult.Text)
160	require.Empty(t, firstResult.Output)
161	require.Empty(t, secondResult.Output)
162	require.NotEqual(t, firstResult.RequestID, secondResult.RequestID)
163
164	// Drain any asynchronous output before proving that a reset on one real
165	// IPC transport does not create a reset frame on the other.
166	for {
167		select {
168		case <-secondPump.ch:
169		default:
170			goto drained
171		}
172	}
173
174drained:
175	require.NoError(t, first.Send(ports.Frame{Type: ports.MsgOutputResetRequest, Payload: ports.MarshalOutputResetRequest(ports.OutputResetRequest{})}))
176	awaitAcceptanceOutput(t, firstPump, func(output ports.Output) bool { return output.Full && output.Base == 0 })
177	assertNoTextAfterInput(t, secondPump, size, "Commands")
178}

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