ephemeral_close_test.go RawHistoryFolder

1package daemon
2
3import (
4	"sync"
5	"testing"
6
7	"github.com/bnema/vev/internal/domain"
8	"github.com/bnema/vev/internal/protocol"
9	"github.com/stretchr/testify/require"
10)
11
12func TestEphemeralCloseOnExit(t *testing.T) {
13	tests := []struct {
14		name        string
15		closeOnExit bool
16		intent      uint8
17		sessionName string
18		clients     int
19		renamed     bool
20		wantRemoved bool
21	}{
22		{name: "last ephemeral client removes session", closeOnExit: true, intent: protocol.IntentEphemeral, clients: 1, wantRemoved: true},
23		{name: "option off keeps session", closeOnExit: false, intent: protocol.IntentEphemeral, clients: 1},
24		{name: "named session is never removed", closeOnExit: true, intent: protocol.IntentNew, sessionName: "work", clients: 1},
25		{name: "renamed session is never removed", closeOnExit: true, intent: protocol.IntentEphemeral, clients: 1, renamed: true},
26		{name: "remaining client keeps session", closeOnExit: true, intent: protocol.IntentEphemeral, clients: 2},
27	}
28	for _, tt := range tests {
29		t.Run(tt.name, func(t *testing.T) {
30			pty, release := newBlockingPTY(t)
31			defer release()
32			d := newTestDaemon(t, newFactory(t, pty), stubClock{})
33			updateRuntimeConfig(d, func(rc *runtimeConfig) { rc.ephemeral = domain.EphemeralConfig{CloseOnExit: tt.closeOnExit} })
34
35			tr := &closeTrackingTransport{}
36			sess, ac, err := d.route(helloResumeCapable(tt.intent, tt.sessionName, 0), tr)
37			require.NoError(t, err)
38			for i := 1; i < tt.clients; i++ {
39				_, _, err := d.route(helloResumeCapable(protocol.IntentAttach, sess.nameSnapshot(), 0), &closeTrackingTransport{})
40				require.NoError(t, err)
41			}
42
43			d.clientGone(sess, ac, tr, true)
44			if tt.renamed {
45				require.NoError(t, d.renameSession(sess, "kept"))
46			}
47			d.reapAbandonedEphemeral(sess)
48
49			require.Equal(t, !tt.wantRemoved, d.sessionRegistered(sess))
50			if !tt.wantRemoved {
51				require.NoError(t, d.killSession(sess, protocol.ReasonSessionKilled, false))
52			}
53			d.sessWg.Wait()
54		})
55	}
56}
57
58func TestEphemeralCloseOnExitWaitsForParkExpiry(t *testing.T) {
59	tests := []struct {
60		name        string
61		otherClient bool
62		wantRemoved bool
63	}{
64		{name: "expired park removes abandoned session", wantRemoved: true},
65		{name: "expired park keeps session with another client", otherClient: true},
66	}
67	for _, tt := range tests {
68		t.Run(tt.name, func(t *testing.T) {
69			clock := &signalClock{timers: make(chan *signalTimer, 8)}
70			pty, release := newBlockingPTY(t)
71			defer release()
72			d := newTestDaemon(t, newFactory(t, pty), clock)
73
74			tr := &closeTrackingTransport{}
75			sess, ac, err := d.route(helloResumeCapable(protocol.IntentEphemeral, "", 0), tr)
76			require.NoError(t, err)
77			if tt.otherClient {
78				_, _, err := d.route(helloResumeCapable(protocol.IntentAttach, sess.nameSnapshot(), 0), &closeTrackingTransport{})
79				require.NoError(t, err)
80			}
81			token := ac.resumeToken
82
83			// A link loss parks: the session must survive so the client can resume.
84			d.clientGone(sess, ac, tr, false)
85			<-clock.timers
86			d.reapAbandonedEphemeral(sess)
87			require.True(t, d.sessionRegistered(sess), "parked attachment keeps the ephemeral session")
88
89			d.mu.Lock()
90			parked := d.resume.parked[token]
91			d.mu.Unlock()
92			require.NotNil(t, parked)
93			// Expire directly instead of firing the timer, so no watcher
94			// goroutine races the assertion below.
95			d.expireParked(token, parked)
96
97			require.Equal(t, !tt.wantRemoved, d.sessionRegistered(sess))
98			if !tt.wantRemoved {
99				require.NoError(t, d.killSession(sess, protocol.ReasonSessionKilled, false))
100			}
101			d.sessWg.Wait()
102		})
103	}
104}
105
106func TestDetachFrameEphemeralCloseOnExit(t *testing.T) {
107	tests := []struct {
108		name        string
109		closeOnExit bool
110		intent      uint8
111		sessionName string
112		detach      protocol.Detach
113		wantRemoved bool
114	}{
115		{name: "closed client removes session", closeOnExit: true, intent: protocol.IntentEphemeral, detach: protocol.Detach{Closed: true}, wantRemoved: true},
116		{name: "user detach keeps session", closeOnExit: true, intent: protocol.IntentEphemeral, detach: protocol.Detach{}},
117		{name: "option off keeps session", closeOnExit: false, intent: protocol.IntentEphemeral, detach: protocol.Detach{Closed: true}},
118		{name: "named session is kept", closeOnExit: true, intent: protocol.IntentNew, sessionName: "work", detach: protocol.Detach{Closed: true}},
119	}
120	for _, tt := range tests {
121		t.Run(tt.name, func(t *testing.T) {
122			pty, release := newBlockingPTY(t)
123			defer release()
124			d := newTestDaemon(t, newFactory(t, pty), stubClock{})
125			updateRuntimeConfig(d, func(rc *runtimeConfig) { rc.ephemeral = domain.EphemeralConfig{CloseOnExit: tt.closeOnExit} })
126			tr, sends, _ := newConn(t,
127				mustHello(tt.intent, tt.sessionName, domain.Size{Cols: 80, Rows: 24}),
128				mustClientEnvelope(tt.detach),
129			)
130
131			var hg sync.WaitGroup
132			hg.Go(func() { d.handleConn(tr) })
133			awaitFrame(t, sends, "Welcome")
134			hg.Wait()
135
136			if tt.wantRemoved {
137				require.Equal(t, 0, sessionCount(d))
138			} else {
139				require.Equal(t, 1, sessionCount(d))
140				require.NoError(t, d.killSession(firstSession(d), protocol.ReasonSessionKilled, false))
141			}
142			d.sessWg.Wait()
143			d.waitNotifies()
144		})
145	}
146}

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