main ece7682

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

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