attachment_parking_test.go RawHistoryFolder

1package daemon
2
3import (
4	"errors"
5	"slices"
6	"testing"
7
8	"github.com/stretchr/testify/require"
9
10	"github.com/bnema/vev/internal/protocol"
11	"github.com/bnema/vev/internal/protocol/wire"
12)
13
14type resumeWelcomeFailureTransport struct {
15	closeTrackingTransport
16}
17
18func (*resumeWelcomeFailureTransport) Send(wire.Envelope) error { return errWelcomeSendFailed }
19
20var errWelcomeSendFailed = errors.New("welcome send failed")
21
22func TestAttachmentLossParksOnlyThatAttachment(t *testing.T) {
23	pty, release := newBlockingPTY(t)
24	defer release()
25	d := newTestDaemon(t, newFactory(t, pty), stubClock{})
26
27	firstTransport := &closeTrackingTransport{}
28	sess, first, err := d.route(helloResumeCapable(protocol.IntentNew, "work", 0), firstTransport)
29	require.NoError(t, err)
30	secondTransport := &closeTrackingTransport{}
31	_, second, err := d.route(helloResumeCapable(protocol.IntentAttach, "work", 0), secondTransport)
32	require.NoError(t, err)
33
34	d.clientGone(sess, first, firstTransport, false)
35
36	d.mu.Lock()
37	parked := d.resume.parked[first.resumeToken]
38	d.mu.Unlock()
39	require.NotNil(t, parked)
40	sess.mu.Lock()
41	_, firstAttached := sess.attachments[first]
42	_, secondAttached := sess.attachments[second]
43	sess.mu.Unlock()
44	require.False(t, firstAttached)
45	require.True(t, secondAttached)
46	require.True(t, slices.Contains(sess.snapshotAttachments(), second))
47	require.False(t, secondTransport.Closed())
48}
49
50func TestTwoAttachmentsParkIndependently(t *testing.T) {
51	pty, release := newBlockingPTY(t)
52	defer release()
53	d := newTestDaemon(t, newFactory(t, pty), stubClock{})
54	firstTransport := &closeTrackingTransport{}
55	sess, first, err := d.route(helloResumeCapable(protocol.IntentNew, "work", 0), firstTransport)
56	require.NoError(t, err)
57	secondTransport := &closeTrackingTransport{}
58	_, second, err := d.route(helloResumeCapable(protocol.IntentAttach, "work", 0), secondTransport)
59	require.NoError(t, err)
60	firstToken, secondToken := first.resumeToken, second.resumeToken
61
62	d.clientGone(sess, first, firstTransport, false)
63	d.clientGone(sess, second, secondTransport, false)
64	d.mu.Lock()
65	firstParked, secondParked := d.resume.parked[firstToken], d.resume.parked[secondToken]
66	d.mu.Unlock()
67	require.NotNil(t, firstParked)
68	require.NotNil(t, secondParked)
69	require.NotSame(t, firstParked, secondParked)
70	sess.mu.Lock()
71	require.Empty(t, sess.attachments)
72	sess.mu.Unlock()
73}
74
75func TestParkExpiryRemovesOnlyOneAttachment(t *testing.T) {
76	pty, release := newBlockingPTY(t)
77	defer release()
78	d := newTestDaemon(t, newFactory(t, pty), stubClock{})
79	firstTransport := &closeTrackingTransport{}
80	sess, first, err := d.route(helloResumeCapable(protocol.IntentNew, "work", 0), firstTransport)
81	require.NoError(t, err)
82	secondTransport := &closeTrackingTransport{}
83	_, second, err := d.route(helloResumeCapable(protocol.IntentAttach, "work", 0), secondTransport)
84	require.NoError(t, err)
85	d.clientGone(sess, first, firstTransport, false)
86	d.clientGone(sess, second, secondTransport, false)
87	firstToken, secondToken := first.resumeToken, second.resumeToken
88	d.mu.Lock()
89	firstParked := d.resume.parked[firstToken]
90	d.mu.Unlock()
91	d.expireParked(firstToken, firstParked)
92	d.mu.Lock()
93	_, firstRetained := d.resume.parked[firstToken]
94	_, secondRetained := d.resume.parked[secondToken]
95	d.mu.Unlock()
96	require.False(t, firstRetained)
97	require.True(t, secondRetained)
98}
99
100func TestResumeRotatesCredentialAndRejectsStaleTransport(t *testing.T) {
101	pty, release := newBlockingPTY(t)
102	defer release()
103	d := newTestDaemon(t, newFactory(t, pty), stubClock{})
104	oldTransport := &closeTrackingTransport{}
105	sess, ac, err := d.route(helloResumeCapable(protocol.IntentNew, "work", 0), oldTransport)
106	require.NoError(t, err)
107	oldToken := ac.resumeToken
108	oldGeneration := ac.lifecycle.generationValue()
109	d.clientGone(sess, ac, oldTransport, false)
110
111	newTransport := &closeTrackingTransport{}
112	resumed, same, ok, err := d.resumeParked(helloResumeCapable(protocol.IntentResume, "work", oldToken), newTransport, defaultSize)
113	require.NoError(t, err)
114	require.True(t, ok)
115	require.Same(t, sess, resumed)
116	require.Same(t, ac, same)
117	require.NotEqual(t, oldToken, ac.resumeToken)
118	require.Greater(t, ac.lifecycle.generationValue(), oldGeneration)
119	require.True(t, d.commitResumeClaim(ac))
120	d.mu.Lock()
121	_, oldRetained := d.resume.parked[oldToken]
122	d.mu.Unlock()
123	require.False(t, oldRetained)
124
125	before := ac.resumeToken
126	d.clientGone(sess, ac, oldTransport, false)
127	require.Equal(t, before, ac.resumeToken)
128	d.mu.Lock()
129	_, staleParked := d.resume.parked[oldToken]
130	d.mu.Unlock()
131	require.False(t, staleParked)
132}
133
134func TestSuccessfulResumeRejectsOldCredentialAfterWelcome(t *testing.T) {
135	pty, release := newBlockingPTY(t)
136	defer release()
137	d := newTestDaemon(t, newFactory(t, pty), stubClock{})
138	oldTransport := &closeTrackingTransport{}
139	sess, ac, err := d.route(helloResumeCapable(protocol.IntentNew, "work", 0), oldTransport)
140	require.NoError(t, err)
141	oldToken := ac.resumeToken
142	ac.setRouteSnapshot(protocol.RecentRouteSnapshot{Generation: 1})
143	d.clientGone(sess, ac, oldTransport, false)
144
145	resumedTransport := &closeTrackingTransport{}
146	d.handleHello(resumedTransport, helloResumeCapable(protocol.IntentResume, "work", oldToken))
147	newToken := ac.resumeToken
148	require.NotEqual(t, oldToken, newToken)
149	d.mu.Lock()
150	_, oldRetained := d.resume.parked[oldToken]
151	newParked := d.resume.parked[newToken]
152	d.mu.Unlock()
153	require.False(t, oldRetained)
154	require.NotNil(t, newParked)
155	require.NotEmpty(t, resumedTransport.Sends())
156	require.Equal(t, "Welcome", envelopeMessageName(t, resumedTransport.Sends()[0].Payload), "Welcome must remain the first server frame after Hello")
157}
158
159func TestFailedResumeHandshakeKeepsParkedCredential(t *testing.T) {
160	pty, release := newBlockingPTY(t)
161	defer release()
162	d := newTestDaemon(t, newFactory(t, pty), stubClock{})
163	oldTransport := &closeTrackingTransport{}
164	sess, ac, err := d.route(helloResumeCapable(protocol.IntentNew, "work", 0), oldTransport)
165	require.NoError(t, err)
166	token := ac.resumeToken
167	d.clientGone(sess, ac, oldTransport, false)
168
169	failed := &resumeWelcomeFailureTransport{}
170	d.handleHello(failed, helloResumeCapable(protocol.IntentResume, "work", token))
171	require.Nil(t, ac.transport())
172	d.mu.Lock()
173	parked := d.resume.parked[token]
174	claimed := parked != nil && parked.claimed
175	d.mu.Unlock()
176	require.NotNil(t, parked)
177	require.False(t, claimed)
178	require.Equal(t, token, ac.resumeToken)
179
180	_, resumed, ok, err := d.resumeParked(helloResumeCapable(protocol.IntentResume, "work", token), &closeTrackingTransport{}, defaultSize)
181	require.NoError(t, err)
182	require.True(t, ok)
183	require.Same(t, ac, resumed)
184}
185
186func TestConcurrentResumeHasExactlyOneWinner(t *testing.T) {
187	pty, release := newBlockingPTY(t)
188	defer release()
189	d := newTestDaemon(t, newFactory(t, pty), stubClock{})
190	oldTransport := &closeTrackingTransport{}
191	sess, ac, err := d.route(helloResumeCapable(protocol.IntentNew, "work", 0), oldTransport)
192	require.NoError(t, err)
193	token := ac.resumeToken
194	d.clientGone(sess, ac, oldTransport, false)
195
196	type result struct {
197		ok  bool
198		err error
199	}
200	results := make(chan result, 2)
201	for range 2 {
202		go func() {
203			_, _, ok, resumeErr := d.resumeParked(helloResumeCapable(protocol.IntentResume, "work", token), &closeTrackingTransport{}, defaultSize)
204			results <- result{ok: ok, err: resumeErr}
205		}()
206	}
207	var winners int
208	for range 2 {
209		got := <-results
210		if got.ok {
211			winners++
212		}
213	}
214	require.Equal(t, 1, winners)
215	require.NotEqual(t, token, ac.resumeToken)
216}
217
218func TestKillSessionClosesEveryAttachmentAndParkedCredential(t *testing.T) {
219	pty, release := newBlockingPTY(t)
220	defer release()
221	d := newTestDaemon(t, newFactory(t, pty), stubClock{})
222	firstTransport := &closeTrackingTransport{}
223	sess, _, err := d.route(helloResumeCapable(protocol.IntentNew, "work", 0), firstTransport)
224	require.NoError(t, err)
225	secondTransport := &closeTrackingTransport{}
226	_, _, err = d.route(helloResumeCapable(protocol.IntentAttach, "work", 0), secondTransport)
227	require.NoError(t, err)
228	parkedTransport := &closeTrackingTransport{}
229	_, parkedAttachment, err := d.route(helloResumeCapable(protocol.IntentAttach, "work", 0), parkedTransport)
230	require.NoError(t, err)
231	d.clientGone(sess, parkedAttachment, parkedTransport, false)
232	parkedToken := parkedAttachment.resumeToken
233
234	require.NoError(t, d.killSession(sess, protocol.ReasonSessionKilled, false))
235	d.waitNotifies()
236	require.True(t, firstTransport.Closed())
237	require.True(t, secondTransport.Closed())
238	require.True(t, parkedTransport.Closed())
239	d.mu.Lock()
240	_, retained := d.resume.parked[parkedToken]
241	d.mu.Unlock()
242	require.False(t, retained)
243}
244
245func TestResumeCredentialIsInvalidAfterDaemonRestart(t *testing.T) {
246	pty, release := newBlockingPTY(t)
247	defer release()
248	d := newTestDaemon(t, newFactory(t, pty), stubClock{})
249	transport := &closeTrackingTransport{}
250	_, ac, err := d.route(helloResumeCapable(protocol.IntentNew, "work", 0), transport)
251	require.NoError(t, err)
252	token := ac.resumeToken
253	restarted := newTestDaemon(t, newFactory(t, pty), stubClock{})
254	_, _, err = restarted.route(helloResumeCapable(protocol.IntentResume, "work", token), &closeTrackingTransport{})
255	var protocolErr *protoErr
256	require.ErrorAs(t, err, &protocolErr)
257	require.Equal(t, protocol.ErrNoSuchSession, protocolErr.code)
258}
259
260func TestHandleHelloAllowsMultipleAttachments(t *testing.T) {
261	pty, release := newBlockingPTY(t)
262	defer release()
263	d := newTestDaemon(t, newFactory(t, pty), stubClock{})
264	firstTransport := &closeTrackingTransport{}
265	sess, first, err := d.route(helloResumeCapable(protocol.IntentNew, "work", 0), firstTransport)
266	require.NoError(t, err)
267	secondTransport := &closeTrackingTransport{}
268	_, second, err := d.route(helloResumeCapable(protocol.IntentAttach, "work", 0), secondTransport)
269	require.NoError(t, err)
270	require.Equal(t, true, sess.attachmentRegistered(first))
271	require.Equal(t, true, sess.attachmentRegistered(second))
272	require.NotSame(t, first, second)
273}

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